Skip to content

fix(precompute): close idle/trailing windows for one-shot batch ingest (wall-clock fallback + shutdown force-close)#400

Merged
milindsrivastava1997 merged 6 commits into
mainfrom
sync/wall-clock-grace-fallback
Jun 18, 2026
Merged

fix(precompute): close idle/trailing windows for one-shot batch ingest (wall-clock fallback + shutdown force-close)#400
milindsrivastava1997 merged 6 commits into
mainfrom
sync/wall-clock-grace-fallback

Conversation

@zzylol

@zzylol zzylol commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Syncs the precompute-engine window-closing fixes from ASAPQuery-backend into ASAPQuery, and adds a shutdown force-close so a one-shot batch can never lose its trailing window.

Problem

With strict event-time semantics, a tumbling window [T, T+size) only closes when the watermark reaches T+size. For a one-shot batch ingest where every record carries (nearly) the same timestamp — e.g. NetFlow records that all fall in the same second — the per-group watermark freezes at max_ts < T+size. flush_all only advances by +1ms, so closed_windows(prev, prev+1) returns empty forever: the trailing window never closes, emit_batch is never called, and the store stays empty even though data was ingested.

Commit 1 — port the wall-clock grace fallback (backend → frontend)

Track each pane's wall-clock birth time; in flush_all, force the effective watermark past pane_start + window_size_ms for any pane alive longer than window_size_ms + wall_clock_grace_period_ms of wall-clock time, regardless of event-time. wall_clock_grace_period_ms <= 0 opts out (strict event-time only). Default 5000ms, matching the backend.

  • config: add wall_clock_grace_period_ms (serde default 5000)
  • worker: GroupState.pane_wall_clock_starts_ms + prune; injectable now_ms_fn (set_now_ms_fn for tests); record pane birth in process_group_samples; fallback block in flush_all
  • engine/main/CLI: thread the setting through (PrecomputeSettings + --wall-clock-grace-period-ms)

Commit 2 — force-close open windows on shutdown

The fallback is wall-clock gated, so a batch that ingests and immediately shuts down hasn't aged past the grace period and still leaves its trailing window open. Worker::force_close_all() (called from the Shutdown handler after the final flush_all) closes every remaining open window unconditionally, since no further samples will arrive.

It advances to a finite bound (max_pane + window_size_ms), not i64::MAX: WindowManager::closed_windows enumerates window starts one slide at a time, so i64::MAX would loop ~i64::MAX/slide times and overflow. Idempotent — drained panes are removed and their bookkeeping pruned.

Tests

  • wall_clock_fallback_closes_idle_window, wall_clock_fallback_disabled_preserves_event_time_only_semantics
  • shutdown_force_close_emits_trailing_window
  • test_update_agg_configs_enables_new_aggregation_at_runtime updated: the trailing window [10_000, 20_000) is now emitted on shutdown (previously silently lost)

Verification

  • cargo check --lib --tests --bins — clean
  • precompute_engine::worker28 passed · precompute_engine::config — passed
  • engine_config20 passed · e2e_precompute_equivalence2 passed

🤖 Generated with Claude Code

zzylol and others added 5 commits June 16, 2026 10:02
… idle windows

Sync the precompute-engine window-closing fix from ASAPQuery-backend.

Problem: with strict event-time semantics, a tumbling window [T, T+size)
only closes when the watermark reaches T+size. For a one-shot batch ingest
where every record carries (nearly) the same timestamp, the watermark
freezes and flush_all's +1ms advance is a no-op, so the trailing window
never closes — emit_batch is never called and the store stays empty even
though data was ingested.

Fix (ported from the backend's flush_all "sweep blocker #2" fallback):
track each pane's wall-clock birth time and, in flush_all, force the
effective watermark past pane_start + window_size_ms for any pane that has
been alive longer than window_size_ms + wall_clock_grace_period_ms of
wall-clock time, regardless of event-time. Set wall_clock_grace_period_ms
<= 0 to opt out and keep strict event-time-only semantics.

Changes:
- config: add wall_clock_grace_period_ms (serde default 5000ms)
- worker: GroupState.pane_wall_clock_starts_ms + prune; injectable now_ms_fn
  (default SystemTime::now, test override via set_now_ms_fn); record pane
  birth in process_group_samples; wall-clock fallback in flush_all
- engine/main/CLI: thread the setting through (PrecomputeSettings +
  --wall-clock-grace-period-ms flag)
- tests: wall_clock_fallback_closes_idle_window and the grace=0 opt-out;
  update existing config literals

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the wall-clock grace fallback. The fallback only fires once a
pane has aged window_size + grace of wall-clock time on a flush tick, so a
one-shot batch that ingests and immediately shuts down still leaves its
trailing window open — flush_all's +1ms advance never reaches the window
end and the data never lands in the store.

Add Worker::force_close_all(), invoked from the Shutdown handler after the
final flush_all(): for every group with open panes, advance to a finite
bound (max_pane + window_size_ms) and emit every remaining window. A finite
bound is used deliberately — WindowManager::closed_windows enumerates window
starts one slide at a time, so passing i64::MAX would loop ~i64::MAX/slide
times and overflow. The pass is idempotent: drained panes are removed and
their wall-clock bookkeeping pruned.

- worker: force_close_all() + Shutdown handler wiring
- tests: shutdown_force_close_emits_trailing_window; update
  test_update_agg_configs_enables_new_aggregation_at_runtime to assert the
  trailing window [10_000, 20_000) is now emitted (previously lost)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ngest

Drives the real JsonFileIngestSource -> PrecomputeEngine -> sink path with a
synthesized NetFlow JSONL whose records all fall in the same second. Sets
wall_clock_grace_period_ms=0 so only the shutdown force-close can close the
trailing window — a non-empty sink proves the force-close rescues the batch.

Verified both directions: with the Shutdown force-close the single-second
window is emitted with the summed bytes; with it disabled the run produces 0
outputs (reproducing the reported empty-store bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-fallback

# Conflicts:
#	asap-query-engine/src/precompute_engine/worker.rs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@milindsrivastava1997 milindsrivastava1997 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR's own docstring for force_close_all explains why it avoids i64::MAX (closed_windows would loop ~i64::MAX/slidetimes and overflow/hang). But there's a sibling code path with that exact bug, untouched by this PR: in theUpdateAggConfigs handler (worker.rs:206, pre-existing, not in this diff), the removed-agg-id cleanup calls:
let closed = state.window_manager.closed_windows(state.previous_watermark_ms, i64::MAX);

…ce-close

Addresses review feedback on #400. The removed-agg-id cleanup in the
UpdateAggConfigs handler advanced the watermark to i64::MAX, the exact hazard
force_close_all's docstring warns about: closed_windows enumerates window
starts one slide at a time, so with a real epoch-ms watermark it loops
~i64::MAX/slide times and overflows `start + window_size_ms` (panics in
debug). Use the same finite bound as force_close_all (max_pane +
window_size_ms).

Add test_update_agg_configs_removed_id_force_closes_at_epoch_ms_timestamps,
which opens a window at a realistic epoch-ms timestamp and removes the
agg_id; it hangs/overflows under the old i64::MAX code (verified: times out)
and passes with the finite bound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zzylol

zzylol commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 23e2bde.

The UpdateAggConfigs removed-agg cleanup (worker.rs:206) now uses the same finite bound as force_close_all instead of i64::MAX:

let closed = match active_panes.keys().next_back() {
    Some(&max_pane) => {
        let force_wm = max_pane.saturating_add(state.window_manager.window_size_ms());
        state.window_manager.closed_windows(state.previous_watermark_ms, force_wm)
    }
    None => Vec::new(), // no open panes
};

I also added test_update_agg_configs_removed_id_force_closes_at_epoch_ms_timestamps, which opens a window at a realistic epoch-ms timestamp (1_700_000_000_000) and removes the agg_id. I verified it hangs/times out under the old i64::MAX code (exit 124 at a 45s timeout — the ~i64::MAX/slide loop) and passes instantly with the finite bound.

@milindsrivastava1997
milindsrivastava1997 self-requested a review June 18, 2026 13:51
@milindsrivastava1997
milindsrivastava1997 merged commit 0977ce6 into main Jun 18, 2026
8 checks passed
@milindsrivastava1997
milindsrivastava1997 deleted the sync/wall-clock-grace-fallback branch June 18, 2026 13:51
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.

2 participants