Skip to content

fix: recover deadlocked Binance receivers - #6

Merged
proerror77 merged 2 commits into
mainfrom
codex/binance-lob-watchdog-fix
Jul 13, 2026
Merged

proerror77 merged 2 commits into
mainfrom
codex/binance-lob-watchdog-fix

Conversation

@proerror77

@proerror77 proerror77 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Bound WebSocket receiver cancellation to five seconds so a stuck close handshake cannot freeze the collector in systemd active state.
  • Treat cancellation timeout as process-fatal, allowing systemd to clean up old tasks/sockets instead of leaking them into another session.
  • Add an independent 180-second no-market-data watchdog outside the asyncio loop.
  • Arm the watchdog only after archive recovery completes, so legitimate zstd recovery (up to 300 seconds) cannot create a restart loop.
  • Expose watchdog and cancellation settings in both Spot and USD-M environments.

Root cause evidence

Both services had no network socket and no file growth after approximately 22:17, while systemd remained active. The Python main threads were idle in epoll_wait with no scheduled timeout, consistent with run_session hanging while gathering cancelled receiver tasks.

Verification

  • 29 unit/async tests pass.
  • Self-test, py_compile, and git diff --check pass.
  • Spec and adversarial re-reviews found no remaining P0/P1 blocker.
  • Tokyo runtime recovered: over 10 seconds Spot grew ~6.3 MB and USD-M grew ~4.2 MB.
  • Fresh health ages were 29s and 23s; both status=synced, sequence_gaps=0.
  • Final script and env files are installed on the Tokyo host; active services remain collecting and will load the final watchdog changes on the next restart.

Test plan

  • python3 -m unittest deployment/aliyun/test_binance_lob_archiver.py
  • python3 deployment/aliyun/binance_lob_archiver.py --self-test
  • python3 -m py_compile deployment/aliyun/binance_lob_archiver.py
  • git diff --check
  • Verify live segment growth and fresh health on Spot and USD-M

Summary by CodeRabbit

  • Bug Fixes

    • Improved recovery when market-data collection stalls or the service becomes unresponsive.
    • Added automatic process recovery after 180 seconds without market-data updates.
    • Prevented shutdown operations from hanging indefinitely by enforcing a five-second cleanup limit.
    • Confirmed successfully uploaded segments continue to be deleted, while low disk space only generates a warning.
  • Documentation

    • Clarified service recovery, watchdog, cleanup, storage, and upload behavior.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Aliyun Binance archiver now includes configurable process watchdog and bounded task-cancellation controls. It records market-data activity, exits after prolonged stalls, detects stuck shutdown tasks, and documents the recovery behavior.

Changes

Aliyun process recovery

Layer / File(s) Summary
Process-control configuration and recovery contract
deployment/aliyun/binance_lob_archiver.py, deployment/aliyun/*.env, deployment/aliyun/README.md
Adds watchdog and cancellation timeout settings, defines the cancellation exception, and documents bounded cleanup and watchdog recovery behavior.
Market-data watchdog lifecycle
deployment/aliyun/binance_lob_archiver.py, deployment/aliyun/test_binance_lob_archiver.py
Tracks the latest depth frame, arms watchdog state during collection, runs monitoring in a daemon thread, and tests the expiration deadline.
Bounded session shutdown and fatal restart
deployment/aliyun/binance_lob_archiver.py, deployment/aliyun/test_binance_lob_archiver.py
Bounds task cancellation, raises TaskCancellationStuck on timeout, records fatal health status, and tests cancellation failure handling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebSocketReceiveLoop
  participant LAST_DATA_AT
  participant run_process_watchdog
  participant systemd
  WebSocketReceiveLoop->>LAST_DATA_AT: record depth-frame timestamp
  run_process_watchdog->>LAST_DATA_AT: check elapsed time
  run_process_watchdog->>systemd: exit process after watchdog deadline
Loading
sequenceDiagram
  participant run_session
  participant cancel_tasks_bounded
  participant asyncioTasks
  participant collect
  run_session->>cancel_tasks_bounded: cancel session tasks
  cancel_tasks_bounded->>asyncioTasks: await cancellation with timeout
  cancel_tasks_bounded-->>collect: raise TaskCancellationStuck
  collect->>collect: write fatal health record and re-raise
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: improving recovery for stalled Binance receivers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/binance-lob-watchdog-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca27c24c94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await asyncio.gather(
*tasks, *resync_tasks.values(), return_exceptions=True
)
await cancel_tasks_bounded(tuple([*tasks, *resync_tasks.values()]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain queued frames before raising cancellation timeout

When cancel_tasks_bounded times out, it raises here before the existing queue-drain loop runs. In the new stuck-close fatal path, any diff/snapshot frames already read from the websocket and sitting in queue are discarded on process restart instead of being written as archived-only/replayed-unsafe as the following drain logic intends; preserve the timeout error but flush the queued frames before re-raising it.

Useful? React with 👍 / 👎.

SPOOL_DIR.mkdir(parents=True, exist_ok=True)
recover_parts()
LAST_DATA_AT = time.monotonic()
PROCESS_WATCHDOG_ARMED = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pause the watchdog during long segment compression

Arming the process watchdog for the entire collector also leaves it active while ArchiveRuntime.rotate() is compressing a segment, even though finalize_segment() is allowed to spend up to ZSTD_TIMEOUT_SECONDS (300s) in zstd. On the full-market services, if a scheduled/shutdown rotation or compression stall applies backpressure long enough that receivers stop reading for more than PROCESS_WATCHDOG_SECONDS (180s), the watchdog takes the os._exit path and drops the in-memory queue instead of letting the normal .part recovery path preserve the segment; disarm or heartbeat the watchdog around intentional long rotations.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
deployment/aliyun/binance_lob_archiver.py (1)

1031-1031: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: simplify tuple construction.

tuple([*tasks, *resync_tasks.values()]) allocates an intermediate list. A direct tuple literal avoids that.

♻️ Proposed refactor
-        await cancel_tasks_bounded(tuple([*tasks, *resync_tasks.values()]))
+        await cancel_tasks_bounded((*tasks, *resync_tasks.values()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deployment/aliyun/binance_lob_archiver.py` at line 1031, In the task
cancellation call, replace the intermediate-list tuple construction around
cancel_tasks_bounded with a direct tuple construction combining tasks and
resync_tasks.values(), preserving the same elements and ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@deployment/aliyun/binance_lob_archiver.py`:
- Line 1031: In the task cancellation call, replace the intermediate-list tuple
construction around cancel_tasks_bounded with a direct tuple construction
combining tasks and resync_tasks.values(), preserving the same elements and
ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12db39ec-5639-4854-a012-dea90b9defae

📥 Commits

Reviewing files that changed from the base of the PR and between a70bce1 and ca27c24.

📒 Files selected for processing (5)
  • deployment/aliyun/README.md
  • deployment/aliyun/binance-lob-archiver-spot.env
  • deployment/aliyun/binance-lob-archiver-usdm.env
  • deployment/aliyun/binance_lob_archiver.py
  • deployment/aliyun/test_binance_lob_archiver.py

@proerror77
proerror77 merged commit 4bb5ad2 into main Jul 13, 2026
21 checks passed
@proerror77
proerror77 deleted the codex/binance-lob-watchdog-fix branch July 13, 2026 15:11
proerror77 pushed a commit that referenced this pull request Aug 18, 2026
…ine tapes

Gate #6 (candidate ad00c63f, invocation 1cad2263b3c74d678d12adbe14d93c0b)
failed on two stacked gaps:

1. The trade-parity mode chain had no branch for a finalization-deferred
   shadow against a finalization-deferred baseline: with no early-closing
   market, neither side can emit a mature trade inside a 3600-second gate,
   so byte/field/dedupe parity fail by construction (dedupe requires a
   non-empty legacy trade set in legacy_overlap mode) and the gate died as
   continuous_overlap. Route a failed verifier with that emission pair to a
   new finalization_deferred_deferred_overlap mode: false checks must be
   confined to the byte/field/dedupe trio, both trade counts must be zero
   with no only/duplicate trade IDs, and the baseline settlement set must be
   fully covered with matching shared values; rust-only settlements are
   tolerated because the production uploader deletes baseline tapes after
   upload while the shadow retains its own. A fully passing verifier keeps
   full continuous_overlap parity. The raw verdict and the finalization
   progression evidence stay fail-closed, and the gate policy admits the
   new mode with the same shape.

2. The parity verifier reads only legacy tapes still present in the spool,
   but the production uploader deletes each tape right after upload: the
   baseline tape holding 56 settlements was deleted at 09:11, long before
   the 09:42 verification. The gate now hardlinks every lookback-window
   baseline tape into the evidence directory at observation start and again
   immediately before verification (hardlinks pin the inode at zero copy
   cost; multi-GiB copies are not acceptable), and the verifier reads that
   snapshot. The links are released once the distilled parity verdict is
   written; on failure the snapshot stays for forensics.

Also print the actual false parity checks in the non-adjudicated failure
message instead of a static family list.

Refs #878
proerror77 added a commit that referenced this pull request Aug 18, 2026
…ine tapes (#927)

* polymarket: adjudicate deferred-deferred shadow gates, snapshot baseline tapes

Gate #6 (candidate ad00c63f, invocation 1cad2263b3c74d678d12adbe14d93c0b)
failed on two stacked gaps:

1. The trade-parity mode chain had no branch for a finalization-deferred
   shadow against a finalization-deferred baseline: with no early-closing
   market, neither side can emit a mature trade inside a 3600-second gate,
   so byte/field/dedupe parity fail by construction (dedupe requires a
   non-empty legacy trade set in legacy_overlap mode) and the gate died as
   continuous_overlap. Route a failed verifier with that emission pair to a
   new finalization_deferred_deferred_overlap mode: false checks must be
   confined to the byte/field/dedupe trio, both trade counts must be zero
   with no only/duplicate trade IDs, and the baseline settlement set must be
   fully covered with matching shared values; rust-only settlements are
   tolerated because the production uploader deletes baseline tapes after
   upload while the shadow retains its own. A fully passing verifier keeps
   full continuous_overlap parity. The raw verdict and the finalization
   progression evidence stay fail-closed, and the gate policy admits the
   new mode with the same shape.

2. The parity verifier reads only legacy tapes still present in the spool,
   but the production uploader deletes each tape right after upload: the
   baseline tape holding 56 settlements was deleted at 09:11, long before
   the 09:42 verification. The gate now hardlinks every lookback-window
   baseline tape into the evidence directory at observation start and again
   immediately before verification (hardlinks pin the inode at zero copy
   cost; multi-GiB copies are not acceptable), and the verifier reads that
   snapshot. The links are released once the distilled parity verdict is
   written; on failure the snapshot stays for forensics.

Also print the actual false parity checks in the non-adjudicated failure
message instead of a static family list.

Refs #878

* polymarket: harden baseline tape snapshot against rotation, bad names, races

Address three Codex P1 review findings on snapshot_legacy_tapes:

- Refresh the active-tape hardlink on re-sweeps: when the live active
  tape rotated between sweeps, relocate the stale snapshot link to a
  synthetic strict closed-tape name and pin the new active inode, and
  never link the same inode twice (the verifier would read its rows
  twice).
- Fail closed on malformed rotated baseline tape names, mirroring
  strict_rotation_name in the parity verifier, instead of silently
  skipping them.
- Fail closed on any hardlink failure, including the uploader winning
  its deletion race against the sweep, so truncated baseline evidence
  cannot reach adjudication.

Add contract cases for the rotation refresh, the malformed-name
failure, a same-name foreign inode, and the simulated mid-link source
deletion race.

---------

Co-authored-by: Sonic Shih <sonic.shih@mandonothing.com>
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