feat: add Zenith strategy and move latency tracing
Build and Push Docker Container / build-and-push (push) Successful in 2m18s
Build and Push Docker Container / build-and-push (push) Successful in 2m18s
- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides. - Prioritize permanent-trail space in Constrictor and health-aware hazard routes. - Deduct executor queue delays and return fallback directions on move timeouts. - Correlate response headers, replay timings, and sampled structured trace logs. - Move shared metrics publication off the response path without losing local counts. - Allow configurable tournament opponents and add a psql helper recipe. - Preserve empty latency values through compact replay encoding and migration. - Update Arena game links and use default assets for missing customizations. - Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior. - Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""Fast request-local fallback and queue-aware worker budgeting."""
|
||||
from time import perf_counter
|
||||
|
||||
def fallback_move(state):
|
||||
board = state['board']
|
||||
mine = state['you']
|
||||
head = mine['head']
|
||||
occupied = {(p['x'], p['y']) for s in board['snakes'] for p in s['body']}
|
||||
food = {(p['x'], p['y']) for p in board.get('food', [])}
|
||||
hazards = {(p['x'], p['y']) for p in board.get('hazards', [])}
|
||||
ranked = []
|
||||
for name, (dx, dy) in (('up', (0, 1)), ('down', (0, -1)), ('left', (-1, 0)), ('right', (1, 0))):
|
||||
point = head['x'] + dx, head['y'] + dy
|
||||
if not (0 <= point[0] < board['width'] and 0 <= point[1] < board['height']):
|
||||
continue
|
||||
if point in occupied:
|
||||
continue
|
||||
danger = any(
|
||||
s['id'] != mine['id'] and len(s['body']) >= len(mine['body'])
|
||||
and abs(s['head']['x'] - point[0]) + abs(s['head']['y'] - point[1]) == 1
|
||||
for s in board['snakes']
|
||||
)
|
||||
ranked.append((not danger, point not in hazards or point in food, name))
|
||||
return max(ranked)[-1] if ranked else 'up'
|
||||
|
||||
def run_budgeted_move(board, deadline, fallback, trace=None, submitted=None):
|
||||
"""Deduct executor queue wait before the strategy starts its own clock."""
|
||||
started = perf_counter()
|
||||
if trace is not None:
|
||||
trace.set(executor_queue_ms=round((started - submitted) * 1000, 3), worker_started=True)
|
||||
remaining = int((deadline - started) * 1000)
|
||||
if remaining <= 40:
|
||||
if trace is not None:
|
||||
trace.set(worker_skipped=True)
|
||||
return fallback
|
||||
original_timeout = board.timeout
|
||||
try:
|
||||
board.timeout = min(original_timeout, remaining)
|
||||
return board.snake_neat_make_a_move()
|
||||
finally:
|
||||
board.timeout = original_timeout
|
||||
if trace is not None:
|
||||
trace.set(strategy_ms=round((perf_counter() - started) * 1000, 3), worker_finished=True)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Small request-local timings; no board payloads or external tracing dependency."""
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
import os
|
||||
from threading import Lock
|
||||
from time import perf_counter
|
||||
from uuid import uuid4
|
||||
|
||||
class MoveTrace:
|
||||
def __init__(self, game_id, turn):
|
||||
self.id = uuid4().hex
|
||||
self.game_id = game_id
|
||||
self.turn = turn
|
||||
self.started = perf_counter()
|
||||
self._lock = Lock()
|
||||
self._fields = {}
|
||||
|
||||
def set(self, **fields):
|
||||
with self._lock:
|
||||
self._fields.update(fields)
|
||||
|
||||
@contextmanager
|
||||
def stage(self, name):
|
||||
started = perf_counter()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.set(**{name + '_ms': round((perf_counter() - started) * 1000, 3)})
|
||||
|
||||
def snapshot(self):
|
||||
with self._lock:
|
||||
return {'trace_id': self.id, **self._fields}
|
||||
|
||||
def log(self, logger):
|
||||
fields = self.snapshot()
|
||||
# Every timeout/slow request, plus deterministic sampling of normal moves.
|
||||
try:
|
||||
sample = max(0, int(os.getenv('MOVE_TRACE_SAMPLE_EVERY', '100')))
|
||||
slow_ms = max(0, float(os.getenv('MOVE_TRACE_SLOW_MS', '300')))
|
||||
except ValueError:
|
||||
sample, slow_ms = 100, 300
|
||||
slow_stage = any(value >= slow_ms for key, value in fields.items()
|
||||
if key.endswith('_ms') and isinstance(value, (int, float)))
|
||||
if not (fields.get('timed_out') or fields.get('metrics_publish_failed') or slow_stage
|
||||
or (sample and int(self.id[:8], 16) % sample == 0)):
|
||||
return
|
||||
logger.info('move_trace %s', json.dumps({
|
||||
'game_id': self.game_id, 'turn': self.turn, **fields,
|
||||
}, separators=(',', ':')))
|
||||
Reference in New Issue
Block a user