Skip to content

feat: debounced partial spans — delay emission, cancel on fast finish - #38

Merged
PabloPardoGarcia merged 1 commit into
pablo/gla2-195-pending-spansfrom
pablo/gla2-244-pending-debounce
Jul 29, 2026
Merged

feat: debounced partial spans — delay emission, cancel on fast finish#38
PabloPardoGarcia merged 1 commit into
pablo/gla2-195-pending-spansfrom
pablo/gla2-244-pending-debounce

Conversation

@PabloPardoGarcia

Copy link
Copy Markdown
Member

Implements GLA2-244, stacked on #37 (merge that first; this PR's base is its branch).

What

init(partial_spans=True, partial_spans_delay=N) (env GLASSFLOW_PARTIAL_SPANS_DELAY, clamped [0, 60]s): pending snapshots are held N seconds and emitted only if the span is still open — a span finishing within the delay produces no pending on the wire at all. Default 0 is byte-for-byte the #37 behavior, with no scheduler thread even created.

This is the project's volume escape valve: most agent spans live milliseconds, so a few seconds of delay cuts pending traffic ~95% while anything worth watching live (open longer than the delay) still appears.

Design

  • One shared scheduler thread regardless of span volume: deadline heap + (trace_id, span_id) → snapshot registry under a condition variable. schedule is O(log n), cancel is O(1) (heap entries for cancelled keys discarded lazily) — hot-path safe.
  • The feat: emit partial (pending) spans at span start #37 seams did their job: emission wraps the existing _emit, cancellation rides the processor's already-received on_end, and the snapshot is still built at on_start and held — content set during the delay (set_input…) can never leak onto a pending. A delayed pending is byte-identical to an immediate one: zero wire/backend/UI changes.
  • Fork-safe via the heartbeat sender's pattern (module-level register_at_fork over a weak set; children re-arm with an empty registry — parent spans aren't the child's).
  • Lifecycle: shutdown() drops not-yet-due pendings (their final spans are flushing at that moment; anything emitted would be instantly superseded). One documented deviation from the ticket's prose (consistent with its ACs): force_flush() does not drop — flush happens mid-operation, and killing scheduled pendings there would silently disable liveness for spans that stay open.

Tests

TDD, 9 new: config default/env/clamp, scheduler pure logic with an injected clock (not-due, emit-once, cancel, shutdown-drops, post-shutdown no-op), one real-thread smoke test via a bounded Event.wait, and end-to-end through init() (fast span → zero pendings on the wire; delay=0#37 behavior). No sleeps anywhere. Full suite 165 passed; ruff + format + mypy --strict clean.

Per-kind delay (roots immediate, leaves delayed) is noted in the ticket as the future refinement; the scheduler supports it without wire changes.

… (GLA2-244)

partial_spans_delay (env GLASSFLOW_PARTIAL_SPANS_DELAY, clamped
[0, 60]s, default 0 = the GLA2-195 emit-immediately behavior with no
scheduler thread at all): a pending snapshot is held for N seconds and
emitted only if its span is STILL OPEN — a span finishing first costs
zero network. Most agent spans live milliseconds, so a small delay cuts
pending volume drastically while the live view stays useful.

Mechanics: one shared daemon scheduler (deadline heap + key->snapshot
registry under a condition variable; cancelled heap entries discarded
lazily), NOT a timer thread per span. The snapshot is still built at
on_start and held — content set during the delay can never leak onto a
pending — and a delayed pending is byte-identical to an immediate one:
zero wire/backend/UI impact. Cancellation rides the processor's
already-received on_end. Fork children re-arm with an empty registry
(module-level register_at_fork over a weak set, the heartbeat sender's
pattern).

Lifecycle: shutdown() drops not-yet-due pendings (their final spans are
being flushed at that moment). force_flush() deliberately does NOT drop
— flush happens mid-operation, and killing scheduled pendings there
would silently disable liveness for spans that stay open; deviation
from the ticket's prose, consistent with its ACs, documented in code.

Tests use injected clocks and bounded Event waits — no sleeps.
@PabloPardoGarcia
PabloPardoGarcia merged commit 368e678 into pablo/gla2-195-pending-spans Jul 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant