Live durability: do-block, frame rotation, writer wrapper#18
Conversation
Adds three composable pieces to the live-capture path so durability features are available at the subscription layer (not just inside stream_to_file): 1. Live do-block constructor Mirrors Base.open(f, path). Live(args...; kwargs...) do client; …; end guarantees close(client) in finally — on Ctrl-C, exception, or normal exit — so users iterating records themselves get clean teardown without writing the try/finally boilerplate. Manual lifecycle still works unchanged. 2. open_dbn_writer + write_record! (public API) Thin do-block wrapper over the internal RotatingDBNFile, with the same kwargs as stream_to_file and a stable write_record!(writer, rec) API. Lets users subscribe themselves and persist records with the same crash-safe writer stream_to_file uses internally. 3. In-file zstd frame rotation (default frame_seconds = 60.0) _rotate_frame_if_needed! closes the active zstd frame via TranscodingStreams.TOKEN_END + flush and starts a new one on the same file. Result is a multi-frame .dbn.zst (standards-compliant; any zstd reader concatenates frames transparently). A hard kill loses ≤ one frame of records instead of corrupting the whole file. Also hooked into _session_monitor so quiet schemas reach disk. Also: - Base.close(Live) hardened: flag-flip happens up-front so re-entry is a no-op; channel cleanup runs unconditionally (typed-data dict is empty in untyped mode, so the iteration is a no-op there). - SymbolMappingMsg now flows through _write_record! into the .dbn.zst (DatabentoBinaryEncoding ≥ 0.1.1 fixes the v1→v3 layout re-encode, so the workaround dropping the record is no longer needed). - frame_seconds kwarg added to stream_to_file / stream_multi_to_files. - README: canonical Live example switched to do-block form; new "Writing records to disk yourself" section showing open_dbn_writer. - Version 0.1.1 → 0.1.2. New tests (1587/1587 pass, +29 vs main): - Live do-block cleanup on normal/exception/InterruptException exit. - Base.close re-entry safety. - open_dbn_writer cleanup on normal exit and InterruptException. - write_record! smoke + round-trip. - Multi-frame zstd round-trips as one continuous record stream. - SymbolMappingMsg now lands in the .dbn.zst (load-bearing for the upstream encoder fix integration). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 430dc8d231
ℹ️ 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".
| # caller needs instrument_id → raw_symbol resolution, use the | ||
| # `symbology.resolve` historical endpoint with the same symbols. | ||
| s.mapping_count += 1 | ||
| _write_record!(ctx.file, rec) |
There was a problem hiding this comment.
Require fixed DBN encoder before persisting SymbolMappingMsg
_handle_control! now writes SymbolMappingMsg to disk, but the package compat still allows DatabentoBinaryEncoding = "0.1" (including 0.1.0). The comment here explicitly depends on a fix in DatabentoBinaryEncoding ≥ 0.1.1; with 0.1.0 this path can emit inconsistent record lengths and corrupt the capture stream when a mapping record arrives. Please gate this write path on a safe encoder version (or tighten compat) so users on older 0.1.x don’t get malformed .dbn.zst output.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 45f63d0 — bumped [compat] DatabentoBinaryEncoding = "0.1.1" so the resolver can't pick up the pre-fix 0.1.0 with this write path enabled. Tightening compat over a runtime version gate keeps the failure mode at Pkg.resolve time (clear) rather than mid-stream record corruption (silent).
The new _handle_control! path writes SymbolMappingMsg through DBN.write_record into a v3-metadata file, which requires the cross-version hd.length re-derivation added in DatabentoBinaryEncoding 0.1.1 (commit 7c8b80d). Without it, the on-disk record would be a v3 body with v1's hd.length, desyncing the file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces #14. Lifts the live-capture durability features from
stream_to_file-only to the subscription layer, so any user iterating records benefits — not just file-capture callers.What this adds
1.
Livedo-block constructorMirrors
Base.open(f, path). Guaranteesclose(client)onCtrl-C, exceptions, or normal exit:Manual
connect! → subscribe! → start! → closelifecycle keeps working unchanged.2.
open_dbn_writer+write_record!(public API)Thin do-block wrapper over the internal
RotatingDBNFile. Same kwargs asstream_to_file, plus a stablewrite_record!(writer, rec):3. In-file zstd frame rotation (default
frame_seconds = 60.0)_rotate_frame_if_needed!closes the active zstd frame viaTranscodingStreams.TOKEN_END + flushand starts a new one on the same file — the resulting.dbn.zstis a standards-compliant multi-frame file (any zstd reader concatenates transparently). A hard kill (SIGKILL, OOM, power loss) loses ≤ one frame instead of corrupting the whole file. Hooked into_session_monitorso quiet schemas reach disk.Also
Base.close(Live)hardened: flag-flip up-front (re-entry is a no-op); channel cleanup pass runs regardless ofc.typed.SymbolMappingMsgnow lands in the.dbn.zst— DatabentoBinaryEncoding ≥ 0.1.1 fixes the v1→v3 layout re-encode (hd.lengthis re-derived frommetadata.version), so the previous workaround dropping the record on the floor is no longer needed.frame_secondskwarg added tostream_to_file/stream_multi_to_files.0.1.1→0.1.2.Test plan
Livedo-block cleans up on normal exit, exception, andInterruptException.Base.close(Live)re-entry is a no-op.open_dbn_writercloses onInterruptExceptionmid-write.write_record!smoke + round-trip..dbn.zstround-trips as one continuous record stream.SymbolMappingMsgnow appears in the.dbn.zstand decodes viaDBN.read_dbn(load-bearing for the upstream fix integration).stream_to_file(... duration_s = 600)— verify resulting.dbn.zstopens cleanly viaDBN.read_dbn_with_metadata.Supersedes #14.
🤖 Generated with Claude Code