Skip to content

spec: tick the signals scout coordinator every 15 minutes to spread dispatch load #86835

Description

@andrewm4894

Is your feature request related to a problem?

The Signals scout coordinator ticks every 30 minutes (COORDINATOR_INTERVAL_MINUTES = 30, products/signals/backend/scout_harness/limits.py:86). Every scout due at a tick is dispatched in the same burst, so sandbox demand arrives as a sawtooth rather than a flat line.

Dispatch is already slot-spread: _dispatch_slot (products/signals/backend/temporal/agentic/scout_coordinator.py:160) hashes each config's PK into one of dispatch_ticks_per_interval(interval) slots, so a daily scout lands on 1 of 48 slots today. The grid resolution is what bounds how finely that spread can work.

Describe the solution you'd like

Halve the tick to 15 minutes, doubling the slot grid (a daily scout gets 96 slots instead of 48) and roughly halving the expected cohort size per tick for the same fleet.

Mechanically this is one constant. The schedule spec reads it (products/signals/backend/temporal/agentic/schedule.py:47) and create_signals_scout_coordinator_schedule already does update-if-exists, so a deploy re-registers the Temporal schedule. The workflow body is unchanged, so no workflow.patched() is needed.

The spec work is in the consequences, not the edit:

1. One-time fleet-wide re-slot. Changing the tick changes ticks_per_interval, so every config's hash slot moves. _slot_anchor's docstring already accounts for a period change costing a scout one extra run; this triggers that for every enabled scout at once. The extra runs spread across the new grid rather than bunching, but deploy day carries roughly +1 sandbox run per enabled scout. Decide whether that is acceptable as-is or wants a staged rollout.

2. Per-tick caps become twice as permissive per hour. MAX_RUNS_PER_TICK = 1000 (scout_coordinator.py:59) and MAX_RUNS_PER_TEAM_PER_TICK = 50 (scout_harness/team_limits.py:55) are per-tick, so their hourly equivalents double. Both are backstops rather than routine limits, but if the point is flattening load they should probably be halved alongside. Both are flag-tunable (max_runs_per_tick_global, team_configs[].max_runs_per_tick), so that half can ship without a deploy — worth deciding whether the code constants move too.

3. Coordinator overhead doubles. Each tick reads the flag payload, resolves participating teams, walks every enabled config, and computes a rolling-24h run tally per team (_runs_today_by_team). Cheap next to the sandbox runs, but it is the part that scales with fleet size and it would run twice as often for the same dispatch count. Worth measuring tick duration before committing.

4. Copy and docs referencing "30-minute tick". At least the metadata serializer help text (scout_harness/serializers.py:2811) says "in a single 30-minute coordinator tick".

5. What it does not fix. Cron scouts (run_cron_schedule) are evaluated as wall-clock in the project timezone with no slot alignment, so every 0 9 * * * still lands on one tick regardless of grid resolution. If the observed peaks are cron-driven rather than slot-driven, a finer grid buys nothing. Measuring which one dominates should gate this change.

The floors stay put either way: run_interval_minutes has a MinValueValidator(30) and cron occurrences are validated to a 30-minute minimum gap (_CRON_MIN_GAP_SECONDS). Lowering the tick makes the coordinator poll more often; it does not let any scout run more often than every 30 minutes unless those floors are separately changed. Whether to also drop the floor to 15 is a distinct decision and probably out of scope here.

Describe alternatives you've considered

  • Leave the tick and shrink the per-tick caps instead. Flag-only, no re-slot cost, but it defers work rather than spreading it — deferred runs still pile onto the next tick.
  • Jitter dispatch within a tick. Spreads the child-workflow start times without changing the grid or triggering a re-slot, but it does not reduce the cohort a single tick has to plan and fan out.
  • Slot-align cron scouts too. Addresses the failure mode a finer grid cannot, at the cost of no longer honoring the exact wall-clock time a user configured.

Additional context

Relevant code:

  • products/signals/backend/scout_harness/limits.pyCOORDINATOR_INTERVAL_MINUTES, DUE_GRACE_SECONDS, dispatch_ticks_per_interval, interval_runs_in_tolerance_window
  • products/signals/backend/temporal/agentic/scout_coordinator.py_dispatch_slot, _slot_anchor, _overdue_seconds, _allocate_tick_budget, MAX_RUNS_PER_TICK
  • products/signals/backend/temporal/agentic/schedule.py — the Temporal schedule spec
  • products/signals/backend/scout_harness/team_limits.py — per-team tick/day caps and their flag overrides
  • products/signals/backend/test/test_scout_coordinator.py — asserts anchors land on the tick grid, so it exercises the constant

Note that interval_runs_in_tolerance_window feeds failure_streak_pause_threshold, so a tick change also shifts the circuit breaker's per-lane failure thresholds. Worth confirming that shift is a no-op for the intervals the fleet actually uses.

Suggested first step: measure the current per-tick dispatch distribution to establish whether peaks are slot-driven or cron-driven, then decide.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions