Mục đích: không chạy lại thứ đã chứng nhận. Trước khi test bất cứ gì, đọc file này. Chỉ chạy lại khi commit / image / config ở cột "Pinned at" đã đổi.
Ledger phủ cả ba repo. Cập nhật cùng transaction với commit làm nó đổi (G4).
Cập nhật lần cuối: 2026-08-22 15:00 UTC, data layer d58f594+.
| Repo | Kết quả | Pinned at | Lệnh |
|---|---|---|---|
| Data Layer | 700 pass / 6 skip | 833f4ce |
docker run --rm -v "$PWD:/workspace" -w /workspace 41c135dcf450 python3 -B -m unittest discover -s tests |
| execution_alpha | 80 test / 3 error | 4ed0c81 |
docker run --rm -v "$PWD:/w" -w /w execution-alpha-runtime-numba:0.2.0-v2-c11 sh -c "PYTHONPATH=/w/runtime/app python -m unittest discover -s runtime/tests" |
| Trading System | 732 pass / 0 fail | dd75441 |
— |
3 error phía alpha là baseline, không phải regression:
test_capital_model_sizing, test_combine_bracket_runtime,
test_fib_bracket_runtime — unittest.loader._FailedTest, thiếu sys.path
riêng từng alpha. Đã đo trước khi làm và sau khi làm, y hệt. Đừng điều tra lại.
| Venue | Interval đã chứng nhận | Pinned at |
|---|---|---|
| BINANCE USDM | 15m 1h 1d 1w |
C.31 / C.36 |
| OKX SWAP | 1h 1d 2d 3d 1w |
C.31 / C.36 |
OKX native mapping đã xác minh: 1h→1H, 1d→1Dutc, 2d→2Dutc, 3d→3Dutc,
1w→1Wutc. Cách đều đúng: 3,6M / 86,4M / 172,8M / 259,2M / 604,8M ms.
Image sha256:75ab6244a798b8eff53db11e539c348219e479f12b52fa2950e74fe3790c15da,
chạy từ trong event loop đang chạy — đúng hình dạng production.
| Instrument | Interval | Nến | Cách đều (ms) | |
|---|---|---|---|---|
| BINANCE USDM BTC-USDT | 15m | 5 | 900 000 | PASS |
| BINANCE USDM ETH-USDT | 15m | 5 | 900 000 | PASS |
| OKX SWAP ETH-USDT | 1h | 5 | 3 600 000 | PASS |
| BINANCE USDM ETH-USDT | 1d | 3 | 86 400 000 | PASS |
Mọi dòng: authoritative=False, execution_eligible=False,
source_role=REFERENCE, flags=['PROVIDER_PASS_THROUGH'],
cursor=PASS_THROUGH_NO_REPLAY, bar_lifecycle=FINAL. Fetch 0,07–0,11s.
Kiểm âm cũng đã pass:
- purpose
INTERNAL_EXECUTION→ từ chối (entitlement or licensing policy denied) - requirement
1m(có binding) → không route sang pass-through - request không auth tới
query_v2_1đang chạy →401 UNAUTHENTICATED
Script: scratchpad/certify_pass_through.py (session-scoped, không nằm trong repo).
| Trước | Sau | |
|---|---|---|
| Role V2 chạy | 15 | 15 |
| Container stopped | 15 | 15 |
market_data_service restarts |
0 | 0 |
DATA_STALE trên BINANCE TRADE: có từ 07:50 UTC, 34 lần trước 14:00,
recreate lúc 14:01 → không phải do rollout. Burst DEPENDENCY_UNAVAILABLE
2 phút khi stream restart, hồi lúc 14:03. Cache trade sống: 80 ms tuổi khi đo.
Đừng điều tra lại như lỗi mới.
Không cần chứng minh lại giá trị của chúng:
| Test | Đỏ với code cũ |
|---|---|
test_pass_through_event_loop.py |
5/7 (4 error, 1 fail) |
test_pass_through_grants_bound_instruments.py |
4/6 |
test_module_import_order.py |
6/10 entry point |
test_capabilities_no_false_advertisement (C.30) |
4/5 |
| Thứ | Trạng thái | Chặn ở đâu |
|---|---|---|
| Alpha tiêu thụ interval khác 1m | Chưa | Không có workload identity ALPHA (§3) |
| VN mọi feed | Chưa | Reachability + giờ giao dịch (C.37) |
| Binance/OKX Spot trên 1m | Chưa | Không có demand, binding đã tắt (C.32) |
| Bar realtime trên 1m | Chưa và không định làm | Cần streaming binding, không có |
| Feed phái sinh (funding/OI/basis) | Chưa bắt đầu | Program riêng, Section 20 |
| 4 trường tick/step lệch (C.27) | Chưa sửa | Cần rollout được duyệt |
| Tắt Spot có hiệu lực runtime | Chưa | Cần bundle refresh (C.32) |
Pass-through đã bật và đã chứng nhận, nhưng không consumer nào chạm tới được:
- Grant mang
INTERNAL_ALPHA+INTERNAL_RESEARCH, không bao giờINTERNAL_EXECUTION. - Bundle chỉ có một consumer identity:
trading-system, purposeINTERNAL_EXECUTION. - ⇒ Identity duy nhất auth được lại đúng purpose phải bị từ chối.
- Mọi alpha chạy
DATA_LAYER_CONSUMER_MODE: V1, không giữ identity V2 (C.15).
Không được lách bằng cách thêm INTERNAL_ALPHA vào manifest trading system.
Cần: cert mTLS ký bởi stable CA + keypair RS256 đăng ký trong
QDL_DATA_JWT_KEYS_JSON. Manifest đã sẵn sàng ở revision 3
(alpha-binance-paper: Binance USD-M BTC/ETH 15m; alpha-okx-paper: OKX Swap
BTC/ETH 1h).
CA private key không có trên host (cert-material/ca.crt có, ca.key không).
Nhưng đó không phải blocker thật — xem §4: script sinh TLS tạo CA mới mỗi lần
chạy, nên chỉ cần thêm principal vào danh sách.
Xong 2026-08-22 ~14:50 UTC. Vấn đề hết hạn 48 giờ đã đóng.
| Trước | Sau | |
|---|---|---|
| CA hết hạn | Aug 22 17:39 2026 | Nov 20 14:43 2026 |
| Hạn cert | -days 2 hard-code 2 chỗ |
QDL_PHASE8_CERT_DAYS, mặc định 90 |
| Consumer identity | 1 (trading-system, EXECUTION) |
2 (+ alpha-binance, ALPHA) |
| JWT key id đăng ký | 1 | 2 |
Material mới: cert-material-rotate-20260822T144323Z,
bundle/identities-rotate-20260822T144323Z,
bundle/stable.env.rotate-20260822T144323Z.
Mọi secret đối chiếu digest, giống hệt.
Đã xoay và xác minh khoẻ: kafka1/2/3 (healthy), stable_redis, rust_core_3
(generation 3, đang xử lý), 4 ingestor, binance_bar_edge, query ×2, stream ×2,
market_data_service (0 lỗi TLS sau khi trỏ lại identity mới).
Hai bài học đã trả giá — đừng lặp lại:
- Không roll được xoay CA. Recreate kafka1 một mình →
PKIX path validation failed: Path does not chain with any of the trust anchors, vì kafka2/3 còn CA cũ. Mọi peer xác thực lẫn nhau phải đổi cùng lúc. - Consumer nằm trong phạm vi xoay.
market_data_servicemount identity theo đường host; không trỏ lại thìCERTIFICATE_VERIFY_FAILEDdù server đã xoay.
stable_redis không nằm trên mesh mTLS và không cần recreate. Tôi đưa nhầm
nó vào danh sách; nó ephemeral (--appendonly no, tmpfs) nên mất cache identity:
ProjectionCacheMismatch: stable Redis cache identity is missing for a non-empty spool
Guard này đúng và đã gặp 2026-08-20. Hệ quả đã đo, không đoán: projector là
thứ nạp spool, nên spool đứng yên — newest accepted 14:47:24 trong khi đồng hồ
14:57:44. market_data_service báo DATA_STALE chính vì lý do này.
Spool lúc đóng băng: 127.584 record, 18 partition, 1440 nến 1m mỗi BAR partition = đúng cửa sổ 24 giờ.
Cách chữa: runbook có sẵn scripts/rebuild_v2_stable_projection_cache.py.
Giá phải trả: warmup 1m tụt từ 1440 nến xuống ~15, đầy lại 1 nến/phút → 24 giờ.
Pass-through không ảnh hưởng (lấy từ sàn), nên 15m/1h/1d giữ nguyên độ sâu.
Phát hiện 2026-08-22 14:23 UTC khi truy tìm CA key.
CA notBefore=Aug 20 17:39:23 2026 GMT
notAfter =Aug 22 17:39:23 2026 GMT
Còn ~3,26 giờ tính từ lúc đo. Mọi cert đều 48 giờ và cùng hạn:
| Cert | notAfter |
|---|---|
ca.crt |
Aug 22 17:39:23 |
kafka1/2/3.crt |
Aug 22 17:39:23 |
phase8-producer/core/consumer.crt |
Aug 22 17:39:24 |
stable-query.crt, stable-stream.crt |
Aug 22 17:39:25 |
stable-trading-system.crt |
Aug 22 17:39:25 |
Nguồn: scripts/phase80_generate_tls.sh dùng -days 2 cho cả CA lẫn mọi leaf
(dòng 16 và 33).
Khi hết hạn: mọi kết nối mTLS trong deployment V2 đứt — query, stream,
ingestor, Kafka broker, và market_data_service. Trading System sẽ rơi về V1
theo đúng thiết kế _select_v2, nên không mất giao dịch, nhưng V2 tắt hoàn toàn.
phase80_generate_tls.sh tự sinh CA mới (openssl req -x509 -new, dòng 15–19),
không cần CA key có sẵn. Thêm một identity ALPHA chỉ là thêm principal vào danh
sách client ở dòng 52.
Xoay PKI là bắt buộc trong ~3 giờ dù có làm gì đi nữa. Làm một lần và thêm principal alpha trong cùng lượt = đóng Phase B với blast radius bằng không so với việc phải xoay.
cert-material/ <- phase80_generate_tls.sh ghi vào đây
-> bundle/identities/* <- copy theo principal
-> stable_tls_init <- copy vào volume stable_tls
-> mọi role mount :ro
Xoay = sinh lại cert-material → làm mới bundle/identities/* → chạy lại
stable_tls_init → recreate mọi role, gồm cả kafka1/2/3 (chúng dùng keystore
riêng từ cert-material). Đây là blast radius toàn deployment và cần bạn duyệt.
Pinned at: data layer 96d0d19.
- Added the governed final-closure gates C39.1-C39.5 to the main plan.
- Fixed candidate-bundle regeneration so stable-alpha-binance mTLS and RS256 identities, both public JWT keys and exact alpha identity paths are generated from source rather than existing only in manually staged runtime material.
- Targeted direct tests: 2/2 passed.
- Complete test_phaseb_stable_deployment module: 20/20 passed.
- Disposable real PKI contract: 15/15 leaf certificates chained to the generated CA, QDL_PHASE8_CERT_DAYS=3 bounded the observed validity, ALPHA/JWT artifacts existed, ca.key was removed and temporary state was cleaned.
- Recovery runbook dry-run passed and reported only the exact isolated stable cache scope. No apply, FLUSHDB, cache deletion, service restart or V1 mutation occurred.
- Projector_v2 remains exited because the rotated ephemeral stable Redis lost its cache identity. C39.2 is not certified.
- Full current Python suite: 701 passed, 6 intentional skips, 0 failed.
- Pinned Rust 1.82 CI gate: fmt passed, clippy -D warnings passed, 74 tests passed, 0 failed. The disposable container and target state were removed.
- External PKI hardening is not certified: current generator uses one fresh CA per run, CA and leaf share one lifetime, and Kafka reads a shared certification cert-material bind mount. This is acceptable only for the current controlled candidate, not as offline-CA/external-secret production evidence.
- Owner-approved apply used confirmation token
REBUILD_QDL_V2_STABLE_PROJECTION_CACHEand the rotated environmentstable.env.rotate-20260822T144323Z. - Exact mutation: five cache users stopped; three isolated canonical-cache
SQLite paths removed; only
stable_redisflushed; projector group reset to the 900-secondmd.canonical.v2window; stream then projector started. - Result: FAILED CLOSED. Redis began repopulating, but projector rejected Binance USD-M ETHUSDT and OKX Swap ETH-USDT-SWAP TRADE/QUOTE canonical events as outside its image-local stable catalog. BTC events passed the same non-committing Kafka probe.
- Root cause: Rust ingestor runtime uses the current 22-binding catalog while
immutable Python image revision
4f411e8a216aembeds the older catalog. - Safety result: the runbook never started query roles during the failed gate; the waiting process was stopped, query roles were then restored to pre-state, projector remains fail-closed, and V1 plus Trading System were untouched.
- C39.2 is open until a same-revision immutable Python edge image and catalog digest preflight are tested and the governed rebuild passes all lag/readiness gates.
- The rebuild tool now hashes the image-local catalog in an isolated no-network container and rejects drift before any stop/delete/flush. Targeted tests: 13 passed, 0 failed.
- Focused rebuild/deployment/refresh tests: 46 passed, 0 failed. Corrected full-suite container (read-only source plus tmpfs log path): 703 passed, six intentional skips, 0 failed in 26.944 seconds. Earlier system-Python and read-only-log loader errors were harness errors and were rerun correctly.
- The first apply also exposed loss of the active pass-through Compose override because the runbook used only the base file. Override provenance is now an explicit staged-env input used by every Compose call; invalid or ambiguous paths fail before mutation. Focused suite after the fix: 47 passed, 0 failed.
- Corrected image:
sha256:bd5a8b44974c..., revision0df4360, non-root, catalog revision 3/22 bindings/8 instruments and SHA-256a148e892b642.... Baked-image focused tests passed 46/46. - Second governed rebuild: PASS; 218706 replay records, six partitions, final lag 16, observed accepted bound 69/250 and 71 Redis keys at completion.
- Post-state: query replicas/active stream/projector READY; passive stream live and STANDBY by design; later lag 37; five role logs had zero bounded TLS, catalog, collision, quarantine, traceback or error matches. Redis/SQLite cache identity matched. Disposable backup audit: 114542 events, 18 partitions, zero open gaps, zero quarantine. V1 remained healthy and untouched. C39.2 PASS.
- Harness now binds
trading-system.paper.stablerevision 2 and covers BTC/ETH for Binance USD-M and OKX Swap with per-instrument durable cursors. - First real-provider run passed 4/4 replica parity and exact N+1 stream resume. The stricter gate correctly distinguishes stale historical warmup members from the mandatory LIVE/execution-eligible latest closed BAR.
- Found and fixed one contract defect: materialized BAR history now reports
data_as_of_nsat the final BAR close boundary, matching pass-through history, while preserving venue source timestamps on individual items. - Correctness checks now include exact decimals, OHLCV, one-minute boundaries, ordering/gaps, finality, authority, coverage, latest quality, cursor/watermark and bounded latency measurements.
- Tests at the source revision before immutable build: 4 targeted PASS; 82 focused
PASS with one intentional skip; 707 full Python PASS with six intentional
skips. Runtime remains on
0df4360pending immutable rebuild and explicitly scoped edge-role recreate.
- Shared SDK correction: a fresh warmup now establishes a new durable cursor generation on first ACK, while restored-state and all later ACKs remain strictly monotonic. Retry before first ACK stays on the fresh server cursor.
- SDK module: 17 passed, 0 failed. Full Python discovery: 708 passed, 6 environment-dependent skips, 0 failed in 27.004 seconds.
- Rust correction filters only observed Binance
e=trade,p=0,q=0,X=NA,st=1status records, counts them as filtered, keeps normal trades canonical and keeps every other non-positive/malformed trade quarantined. - Rust 1.82 gate: fmt PASS; workspace clippy
-D warningsPASS; 75 tests PASS. A first workspace harness lacked OpenSSL development packages; the corrected run used the repository CI package set and passed. - Runtime mutation: none. V1, Trading System, Kafka offsets, Redis/SQLite state, stable roles and authority are unchanged. Immutable rebuild/recreate, signed cursor-generation semantics and demanded-slice health remain explicit gates.
- Python image
sha256:6bc8ac77e9d...and Rust imagesha256:685aaa68f7c7...are pinned toa3b068a, run non-root and have not replaced any runtime role. Baked Python tests: 708 passed, 6 skipped. - Stable refresh dry-run only: acquisition revision 4 -> 5; only the two Spot ingestor configs are removed; ten core/authority/active-ingestor artifacts change; env and identities remain preserved.
stable-crypto-bar-edge.jsonis the single stranded checkpoint because it pins acquisition revision 4. Apply requires an exact checkpoint backup/move, real-provider bootstrap, role recreate and rollback packet. No apply occurred.
- Date: 2026-08-23 UTC.
- Decision: owner-approved signed stable cursor generation boundary.
- Stable token schema:
qdl.handoff-cursor.v2, HMAC signed, generation bound toSQLiteDurableSpool.cache_id, opaque to all consumers. - Compatibility: unbound codecs preserve v1; stable v1 and cross-generation
tokens fail as
CURSOR_EXPIRED; V1 data APIs and public V2 fields do not change. - Focused evidence: 62 passed, one skipped.
- Full current-tree evidence: 706 passed, six skipped, zero failed.
- Same-image HEAD baseline: 702 passed, six skipped, zero failed.
- Runtime mutation: none. Production certification: pending immutable paired rollout and demanded-consumer acceptance.
Exact source 62202b2d11e2607c6211f6cc1764d18969160c6d was built as
Python image
sha256:d4a97938fd6da1b226d5a6db2f51a42047c6aab811c511bdad3541d2c6a2016d
and Rust image
sha256:66988ae4254a149447b2a4e5ff6008aa864a4071796934421f5d92dd0248bd76.
Both passed network-off executable/import probes. Read-only live preflight
matched SQLite and Redis cache identities at
ae7554250ad548e7818559c140728ed4; runtime remained unchanged.
The first execution-disabled alpha container was removed after a fail-closed 15m warmup. No execution effect occurred. Real 1m Binance/OKX acquisition and all eight demanded Trading System slices remained healthy. The defect is the registered 15m/1h alpha freshness bound of 180 seconds, shorter than the bar interval itself. The bounded repair raises only those two interval policies, bumps manifest revisions and adds a regression gate; production acceptance is still open until the repaired smoke passes.
Binance 15m and OKX 1h ALPHA freshness now cover one complete interval plus 180 seconds, and both manifests are revision 4. Targeted tests passed 35/35; the full network-off suite passed 707 with six intentional skips. Two preceding collection failures were test tmpfs permission errors and produced no runtime mutation. The repaired artifact is not yet deployed at this ledger point.
The repaired Python edge image was rolled through only the six approved Python
roles. The repeated execution-disabled alpha smoke then failed closed on the
15m provider pass-through because the cursor issuer tried to sign a canonical
replay token for an interval with no materialized binding. The response already
carried the intentional PASS_THROUGH_NO_REPLAY sentinel and watermark zero.
The in-scope correction must preserve that explicit non-replayable contract,
fail inconsistent sentinel states, retain signed generation-bound cursors for
materialized paths and repeat the complete bounded alpha smoke. Kafka, Redis,
SQLite, Rust, V1 and execution state remain outside this repair.
The query contract now preserves the explicit non-replayable cursor only for a
FRESH_SNAPSHOT response at watermark zero and rejects inconsistent states. No
durable cursor or authority is fabricated. Focused tests passed 83 with one
intentional skip; full network-off discovery passed 709 with six intentional
skips. Runtime deployment and the repeated alpha smoke remain pending.
Final Python source 31c8ca5 and image sha256:13fdb777a71f... passed the
709/6 network-off suite and the real execution-disabled alpha acceptance.
Binance BTC/ETH 1m and 15m warmups returned 500 FULL rows; both TRADE sessions
reached replay/live and ACKed. Trading System reports 8/8 demanded Binance/OKX
slices READY, three Kafka lag samples were 15/10/22, cache identities match,
quarantine is zero, and a 30-second stable-state window grew by zero bytes.
Execution DB/Redis counts are unchanged. The exact rollback checkpoint SHA is
98aef817697e...; the redacted final packet SHA is 352a1d5f345e.... Binance
and OKX crypto scope passes. DNSE remains PARTIAL_EXTERNAL; global Rust
authority promotion is outside this packet.
Removed the two disposable C39 test images and nine exact temporary paths. No
broad prune or volume deletion occurred; final/rollback images, V1, stable
state and the checkpoint backup remain. Final packet SHA is
352a1d5f345e3253620bc38d54ef387c2961650c6a3557325f2ae2cdc908c9bb.
Pinned at: data layer d4c9763, images unchanged (qdl-v2-python:2.0.14-ccd0c43
sha256:3e062a3ba38d…, qdl-v2-rust:2.0.12-3f1c50e sha256:407a67131ca6…).
- The 2026-09-15 04:31Z reboot stopped all of
qdl_v2_stable_candidate(restart: "no"on every service). Recovery started the existing containers (nevercompose up, whose base file for eleven roles lives in a removed worktree) and ran the governed cache rebuild in the runbook's exact scope. PASS: cache 912 MB, Redis 548 keys, projector lag 104/6 partitions, restart count 0 on every role. - 17 runtime containers are now
unless-stoppedat runtime and indocker-compose.v2-stable.yml;stable_admin/stable_state_init/stable_tls_initstay"no".tests/test_phaseb_stable_deployment.pypins both sets, 28/28 pass inqdl-v2-python:2.0.14-ccd0c43. - Not certified: unattended reboot.
stable_redisis still ephemeral by design, so the projectors will restart intoProjectionCacheMismatchafter the next reboot until someone runs the governed rebuild. Do not read "restart policy fixed" as "survives reboot". - Trading System
market_data_serviceconsumes V2 again:V2_PRIMARY, 60 demanded slices, 0 unhealthy,v1_fallback_count=0.
Pinned at: data layer 4433497 + this journal; images unchanged.
qdl-v2-stable-boot-recovery.serviceenabled;scripts/v2_stable_boot_recovery.pyrehearsed end-to-end on the running stack (--simulate-crash): PASS in 17 min 44 s, receiptboot-recovery-20260916T065640Z.jsonsha2566af808c74b3adefd…. Certified: unattended recovery from the post-boot state. Not certified: an actual host reboot (owner chose not to reboot); the unit'sAfter=docker.serviceordering is asserted by systemd, not observed.- Do not re-run the rehearsal to "check": it deletes the spool and costs a 15-20 min replay plus the 1m warmup dip. Re-run only if the runbook, the tool or the projector image changes.
- Serving measurement of the same day: Data Layer projection cache 1.2 s median behind the venue for trades, prices within 0.42-1.25 bps of the public ticker.
- Request latency of the V2 query endpoints measured from the Trading System identity still matches the certified snapshot (snapshot TRADE/QUOTE 6.8-8.1 ms p50). Do not re-measure to confirm.
- Not certified, open: Binance USD-M
MARK_INDEX_PRICEunavailable (40/40); OKX mark/quote intermittently exceed the sealed 2,000 ms freshness bound; Binance 1m final bars are short on volume and trade count versus the venue kline while OKX 1m bars are exact. Details and receipts indl-v2-consumer-endpoint-measurement-20260916.
Pinned at: data layer 5130f6f, image qdl-v2-python:2.0.15-5130f6f
(sha256:b3f908cb17cf…), binance_bar_edge only.
- Certified: Binance and OKX 1m final bars now equal the venue's own REST kline, 20/20 exact across BTCUSDT, ETHUSDT, BTC-USDT-SWAP and ETH-USDT-SWAP on open/high/low/close/volume/base volume/trade count. Before the fix Binance was 0/10.
- Cause: Binance kline replicas disagree about a freshly closed bar for about five seconds; the edge read once at close + 0.10 s and never revised.
- Fix: read the same bar until two consecutive reads agree and the bar is at least 6 s old, then publish exactly that venue row; fail closed otherwise.
- Cost: the final 1m bar now lands 6.7-8.2 s after close instead of about 2 s. The published "close-to-final-BAR availability p50 2.151 s" figure no longer describes Binance and must be re-measured before it is quoted again.
- OKX is unchanged (single read; its
confirm=1candles are final on arrival).
rustlspinned=0.23.45(was=0.23.43). Rust gate green in pinnedrust:1.82-slim: fmt, clippy-D warnings,cargo test --workspace --locked81 passed,cargo-deny 0.20.2advisories/bans/licenses/sources ok.- Open: the deployed
qdl-v2-rust:2.0.12-3f1c50estill contains the affectedrustls 0.23.43. Source is fixed; the runtime is not.
- Release v2.0.15 published from
main653fdb6, taga4c5ec3, CI35078347388green. - The bar-edge rollback named in that certificate
(
sha256:1c1392bf…,qdl-v2-python:2.0.12-35a7cd8) was deleted by my cleanup and is unrecoverable. The packet now pins the retainedqdl-v2-python:2.0.14-ccd0c43, verified to carry the pre-fix bar edge. - Retention rule: before deleting an image, check rollout packets and release certificates, not only running containers.
- Gateway
READY,stale_or_bad_servicesempty.market_dataREADYwith 60 demanded V2 slices, 0 unhealthy, 0 V1 fallback, after recreatingmarket_data_servicefrom its packet (image unchanged) to rebuild two dead gRPC stream sessions. - V2 endpoint latency re-measured at 20 iterations per hop: all inside the certified band. OHLCV 20/20 exact. Do not re-measure without a reason.
ingestor_binance_spotandingestor_okx_spotretired: their sealed config files do not exist, no consumer demands spot, dead since 2026-09-03.- Open: the Rust runtime still carries
rustls 0.23.43.Dockerfile.qdl-rust-runtimeis committed, the rebuild is proven (qdl-v2-rust:2.0.15-c5a5be0,sha256:5d1d7f02b904..., same ten binaries and non-root user as the deployed image) and the packetrust-rustls-2.0.15-c5a5be0-20260916T0950Zis prepared. The five-role recreate needs owner approval.
- All five Rust roles run
qdl-v2-rust:2.0.15-c5a5be0(rustls 0.23.45); RUSTSEC-2026-0285 closed in runtime. Canonical topic advanced 132,736 records through the rollout, projector lag 155/500. - P18 evidence files normalised to the evidence owner; every declared receipt verifies by SHA-256 and the E01 matrix verifier is PASS with zero findings.
- QUOTE meets the 2,000 ms gate on both venues: p95 782-785 ms, max 1366 ms, 0 of 902 samples over 1500 ms. The earlier rejection count does not survive a larger sample. Do not re-measure without a reason.
- Open: OKX
MARK_INDEX_PRICEp99 1947 ms, max 2110 ms, 26 of 421 samples over 1500 ms. Ingest is 35-132 ms; the age is the wait for a newer paired record. Cause is pairingmark-pricewith once-per-secondindex-tickers. Proposed fix: publish on either component's update and keep the per-component 2,000 ms policy. Needs owner approval; the bound must not be widened.
- The reducer already emits the pair on either component's update. The envelope deliberately stamps the oldest component's confirmation time; changing it would let a stale index look fresh. Withdrawn.
- OKX publishes index every 613 ms median (max gap 1,078 ms) and mark every 231 ms. The pair's age floor is the index's publication rate.
- The consumed path is the reference batch, measured at 217 ms (Binance) and 935 ms (OKX), both execution eligible: the gate is met where it matters. The durable-projection tail is a venue-bound characteristic, not a defect.
- Entry 20 is wrong on two points. The reference-batch path is breaching the sealed 2,000 ms gate in production, and OKX's index publication rate is not the floor. Measured, not inferred.
- Production, one hour of
market_data_servicelogs: 118MARK_INDEX_PRICEslice disconnects, 117 OKX;index_priceage p50 2255 ms, max 2973 ms, whilemark_priceat the same instants is p50 36 ms. Service stateDEGRADED, reconnect count 294 on DOGE-USDT-SWAP. The consumer reports the Data Layer's own typedDATA_STALEproblem; it applies no rule of its own. - Cause, measured against OKX from this host with no Data Layer in the path:
/api/v5/market/index-tickersreturns rows stamped p50 901 ms and max 2511 ms behind the clock, while/api/v5/public/mark-pricereturns 31 ms. The REST index endpoint serves a stale cached row. The 935 ms figure in entry 20 was a median that hid this tail. - The venue is fine.
wss://ws.okx.com:8443/ws/v5/publicindex-tickers, 470 frames in 60 s:tslag p50 81 ms, max 354 ms, push interval p50 256 ms. The "613 ms publication rate" in entry 20 was the REST row's change rate, not the venue's index rate. - Fix, not applied: serve the reference batch's OKX
INDEXcomponent from theindex-tickersbinding the deployment already subscribes to (qdl/runtime/stable_deployment.py:195), REST only as fallback; the per-component 2,000 ms policy stays sealed. Owner approval and reference-batch re-certification required;v2.0.15is released. - Pinned at:
market_data_servicerunning image of 2026-09-16, OKX public REST and WS measured 2026-09-16. Re-measure only if OKX changes the endpoint.
- Entry 21 found a real defect but not the main one. Measuring all feeds
instead of mark/index: TRADE p50 356 s on Binance and 390 s on OKX,
BOOK_SNAPSHOT rejected
DATA_STALEon every probed instrument, Binance MARK_INDEX_PRICEDATA_NOT_READY, BAR 1m 130 s. QUOTE alone is healthy at p50 403-573 ms. Consumer: 27 of 60 slices unhealthy,DEGRADED. - Projector lag on
stable-projector-v1: ~145,000 total against a documented gate of 500 total / 250 per partition; partition 4 is 6.1 minutes behind. Production 945 msg/s, consumption 768 msg/s, deficit 178 msg/s. - Root cause:
stream_v2_activeruns at 104% of its 0.75-CPU quota while the identicalstream_v2_passivesits at 0.17%, becauseqdl/runtime/stable_ingest.py:383treats the two ingest URLs as ordered failover rather than load sharing. All three projectors post through the one throttled process and wait at 43% of their own quota. Host has 16 cores. - Fix not applied:
docker update --cpus=2.5on that one container, live, no recreate, no cache identity change, reversible with--cpus=0.75. The command was refused by the agent session's shared-resource permission policy. Owner action required. Both-endpoint load sharing is deliberately deferred: the two stream processes share one WAL SQLite database, which allows one writer at a time. - Pinned at: stack
qdl_v2_stable_candidateas running 2026-09-16 14:05Z; every container'sNanoCpus/Memorycaptured for rollback. Re-measure lag after any quota change; the backlog must drain, not merely stop growing.
- Withdrawn from earlier reasoning: the "60x re-parse per subscription"
claim (fan-out matches
(stream, partition_key)first,gateway.py:276; the parse runs ~once per event, ~0.1% CPU) and the header-at-append fix built on it. The CPU quota raise is not step one either: the process is at 33.1% loop + 36.3% across 55 worker threads, and the worker share is SQLite. - Cause: every delivered record calls
advance_token->handoff.issue()->spool.high_watermark()under the spoolRLock(sqlite_spool.py:116), the lockappend_manyholds through its fsync. At ~945 records/s the delivery path serialises against ingest inside the one writer-lease process; replay on reconnect (322 per 10 min) pays the same per unmatched record (grpc_service.py:254). - Measured now: consumer
DEGRADED, 23/60 unhealthy, 22 execution-ready; projector lag 202,163 (p4 96,772); active loop p95 59.4 ms vs passive 2.3 ms; ingest decode 12.6 us/event (1.2% CPU); 522 B per canonical record; Kafka 97.85 GB at the 24 h broker default with no recorded reason. - Plan: DL-V2 R1 in the unified plan, anchor
dl-v2-r1-delivery-lock-20260916. Nothing applied; awaiting owner approval. - Pinned at: stack as running 2026-09-16 15:15Z. Re-measure only after an R1 step lands.
- Code: R1.1 took the durable read off the live delivery path, R1.2 collapsed replay token advances, R1.3 named the stale reason, R1.8 made a latest-state feed keep the newest record in a bounded buffer, R1.9 signs a known cursor on the loop instead of paying a thread hop. R1.4 on the Trading System side keeps a valid snapshot view instead of tearing the slice down. 62 tests across two repositories; the data layer suite is 1,455.
- Measured gain: consumption 767 -> 1,239 records/s. TRADE freshness 355,952 ms -> 926 ms, BOOK_SNAPSHOT from rejected to 942-1,205 ms, OKX MARK_INDEX from rejected to 575-731 ms, projector lag 161,732 -> 322 with the 500/250 gate passing at the time.
- Ceiling: one writer by
ActivePassiveGatewayLeaseplus one interpreter lock per process caps ingest near one core, about 1,240 records/s. Evening load reached 2,749 raw records/s against 950 in the afternoon, the backlog regrew to 2.6 M and the consumer fell to 27/60 ready. The same deficit existed before any change (945 produced vs 767 consumed). - Ruled out with numbers: duplication (4/17/9 against millions), ingestor reconnect storms (39 renewals in 20 min, exactly the 30 s cadence), CPU starvation (projectors 53-63% of quota, stream 43% of a 2.00 quota), decode cost (12.6 us/event, 1.2% of a core).
- Open, owner decision: the governed offset reset to restore service now (attempted, refused by this session's shared-resource policy, nothing was reset); and whether to shard the gateway lease so both stream processes ingest disjoint partitions, which changes the single-writer invariant.
- Open, next correction: recreating a stream container stalls every projector on a dead pooled connection plus a 409 from the no-longer-active peer. Each rollout in this phase needed a projector restart.
- CPU and retention: ceilings are now per service with the measurement on each, declared total 12.25 -> 14.35 on a 16-core host, eight services reduced. Canonical Kafka retention 24 h -> 6 h with the reason recorded; raw stays 24 h because it cannot be refetched and canonical is derived from it.
- Pinned at:
qdl-v2-python:2.0.16-e87ef8don both stream processes and2.0.16-190217bon both query readers; projectors, ingestors, rust cores, kafka and redis unchanged. Rollback images in~/.local/state/qdl-v2/dlv2-r1-190217b-20260916T163120Z/README.md.
- Release:
v2.0.16, certificateupgrade/evidence/releases/v2.0.16/, predecessorv2.0.15(be1b94fe...c761ffe). Seven python roles recreated onqdl-v2-python:2.0.16-df4b8aa(sha256:3c1af2c74d5f2d9981d3ae9c5b098a0ba2f918f49735d5f7bbc3b87a637ede26), built fromdf4b8aa8f24e9b6f7dfec5da9c5121cd0fd07b98,restarts=0. Rollbackqdl-v2-python:2.0.15-5130f6f(sha256:b3f908cb17cf9363afb9258ef2ae08e4cdfbc0cb26d01b5bd371559ad82b6bea), packet~/.local/state/qdl-v2/dlv2-r1-190217b-20260916T163120Z. - Unchanged and pinned:
binance_bar_edgeon2.0.15-5130f6f; the five Rust roles onqdl-v2-rust:2.0.15-c5a5be0(sha256:5d1d7f02b904dc37611febbfea6930a6cf69448544e4d1b065d8624b5528f0d1), which already closes RUSTSEC-2026-0285 in runtime; V1 fallbackqdl-v1-fallback:v1.2.4-2b0dcf7(sha256:dbfb57844977513ae7ec0a4782e04da0213028a789753c6b991f26043b615d65); consumertradingsystem-image:v1.2.5-75df46e. - Post-recovery measurement, 2026-09-17T04:11:44Z, eight iterations through
the real data plane:
QUOTEp50 437-700 ms,TRADEp50 804-1,711 ms,BOOK_SNAPSHOTp50 1,186-1,361 ms, OKXMARK_INDEX_PRICEp50 785-892 ms, all against a 2,000 ms policy thatTRADEwas missing by 355,952 ms before. OHLCV 20/20 exact, allFINAL. Request latency p50: snapshots 6.5-7.2 ms, book 76.6 ms, 1m warmup 119 ms, batched warmup 649 ms. - 31-minute window, 30 samples: consumer
V2_PRIMARYwithfb=0on every sample, 60 slices demanded,READYon 26 of 30, worst sample 3 of 60 slices transiently unhealthy, execution-ready slices p50 45. - The backlog question, answered with numbers. 24 consumer-group snapshots over 356 s: produced 132,880 records, consumed 132,894 — the projector consumed 14 more than were produced, both at 373 rec/s. There is no backlog. Lag oscillates 138-1,273 records and the spikes wander across partitions 2-5, which is a queue breathing, not a stuck partition.
- The gate was the wrong gate.
MAX_ACCEPTED_LAG = 500/MAX_ACCEPTED_PARTITION_LAG = 250inscripts/rebuild_v2_stable_projection_cache.py:31is the convergence gate of the cache-rebuild runbook: three consecutive acceptable samples prove a replay drained. At 373 rec/s, 500 records is 1.3 seconds of work, so an instantaneous sample of a healthy queue crosses it — 5 of 30 window samples did, while the consumer stayed ready and never fell back. Reusing it as a steady-state health gate was my error, not a defect in the runbook. Steady state is the produced-versus-consumed rate plus lag in seconds of work: p50 1.02 s, p95 1.58 s, max 2.19 s. Do not loosen the runbook constant — it is correct for convergence and the 17m44s boot-recovery rehearsal (entry 12) depends on it. - Disk, measured after the retention change. Across the three brokers:
canonical
55.6 -> 17.0 GB, raw40.1 -> 50.7 GB(still 24 h, and the realtime volume is higher than when the before figure was taken), total95.7 -> 67.7 GB. Host filesystem 52%. Verified live:retention.ms=21600000is aDYNAMIC_TOPIC_CONFIGonmd.canonical.v2, raw inherits the 24 h static broker config. - Entry 24 corrected: the declared CPU total after the R1 ceilings is
13.35, not 14.35, summed over the 17 roles this stack runs on a 16-core
host (12.25 before). Eight services were reduced, five raised: kafka1/2/3
0.75 -> 1.00and both stream processes0.75 -> 2.00. - Open, next slice: express steady-state projector health in seconds of work rather than an instantaneous record count, and stop quoting the runbook constant as a health gate.
- Still open, untouched: Binance
MARK_INDEX_PRICEanswersDATA_NOT_READY; 1 of 16 OKXMARK_INDEX_PRICEsamples rejected onEVENT_AGE(entry 21 cause unchanged); recreating a stream container still stalls the projectors on a dead pooled connection plus a 409 from the no-longer-active peer. - Governed recovery: the backlog of 3,002,343 was cleared by a consumer-group
offset reset to latest on
stable-projector-v1(stop projectors, reset, start), authorised by the owner for a pre-production stack. Canonical records between the old committed offset and latest were not projected; raw still holds them and canonical retention is 6 h.
- The CPU was misallocated, not short.
cpu.stat60-second deltas: the two stream processes held 4.00 cores at 0.0% throttle whilequery_v2_2was denied 15.51 CPU-seconds every 60 seconds at a 0.50 ceiling,query_v2_128.5%,rust_core_224.9%, kafka2/3 15.8%/12.1%. That denial was the p95 tail. - Applied live with
docker update --cpus, zero containers recreated, and mirrored intodocker-compose.v2-stable.yml. stream ×22.00 -> 1.25, query ×20.50 -> 1.00, rust_core_20.75 -> 1.00, kafka2/31.00 -> 1.25, binance_bar_edge0.35 -> 0.75, stable_redis0.25 -> 0.50. Declared total13.35 -> 14.25; measured use stays about 4.4 of 16 cores. - Measured gain, same benchmark, 20 iterations, 33 minutes apart: request
latency p95 summed over 29 V2 endpoints
6,767 -> 2,527 ms, -63%. QUOTE p9581-101 -> 9.6-28 ms, TRADE p9587-105 -> 10-40 ms, BOOK_SNAPSHOT p95210-289 -> 84-110 ms, batched warmup p951,589 -> 418 ms, instrument lookup p9571.5 -> 5.2 ms. Throttle after: query 2.0%/4.5%, rust_core_2 2.0%, kafka2/3 1.2%/1.1%. All threeEVENT_AGErejections disappeared. Event age unchanged or better, OHLCV still 20/20 exact, consumer back toV2_PRIMARY60/60 ready, 0 unhealthy, 0 fallback. - Raw tick retention 24 h -> 8 h (owner decision): dynamic topic config
retention.ms=28800000onmd.raw.realtime.v2, no restart. Raw per broker16.92 -> 5.13 GiB, Kafka per broker23.56 -> 11.76 GiB, host filesystem52% -> 39%, free141 -> 178 GB. Warmup contract untouched: the SQLite spool keeps 24 h, canonical keeps 6 h. - Images pruned by digest with an
until=24hfilter:22.22 -> 16.31 GB, 2.07 GB reclaimed. Container logs truncated in place439 -> 160 MB. - Log rotation declared but not yet active.
x-logging(max-size 50m,max-file 3) is wired into the kafka, python and rust anchors and intostable_redis, covering all 17 running roles; it applies at the next recreate. Deliberately not activated today by restarting a stack certified an hour earlier. - A regression I caused and reverted. Lowering the stream ceiling to 1.25
was wrong. The benchmark measures the snapshot path through the query readers;
the consumer's slice health measures the streaming subscription through the
gateway. The consumer's unhealthy-slice mean went
0.27 -> 2.0(max3 -> 7), every one aQUOTEslice, which is aLATEST_STATEfeed on that path, whilestream_v2_activethrottle went0.0% -> 4.5%.v1_fallback_countandv2_error_countstayed 0 the whole time, so nothing failed, but the margin narrowed. Both stream processes are back atcpus: 2.00; declared total15.75. The query gain came from query/kafka/rust getting more, not from stream getting less. Revert confirmed over 12 samples: mean back to0.33against the0.27baseline, max2, nine of twelve samples clean, and the residue is single thin-symbolMARK_INDEX_PRICEslices rather than theQUOTEcluster. - Second pass on the two that did not respond:
binance_bar_edge0.50 -> 0.75halved it to 9.4% and theexhausted retrieswarning has not recurred;stable_redis0.25 -> 0.50took it to 0.0%. - Corrects an earlier guess of mine: projector partition assignment is even, two partitions each. The CPU spread across the three projectors is traffic per partition key, not a rebalancing fault.
- Withdrawn: the suggestion to drop Kafka
ReplicationFactor3 -> 2. After the 8 h retention cut the prize is about 11 GiB, not worth tradingmin.insync.replicas=2for. - Pinned at:
qdl-v2-python:2.0.16-df4b8aa,qdl-v2-rust:2.0.15-c5a5be0,qdl-v2-python:2.0.15-5130f6fon the bar edge. No image changed.
- The serving path is free. Across fourteen realtime endpoints the data layer's reported freshness and the consumer's independently computed durable age agree within 3-4 ms. The age is baked in before anything serves it.
- Stage delays, lag over that stage's own rate:
rust_coreraw->canonical 0.89 s, projector canonical->spool 0.46 s, query spool->consumer 0.003 s.rust_coreis the dominant term. Corrected in entry 28: the 0.89 s reading was taken minutes after the projector recreate while the system was still settling. A clean baseline an hour later put the same stage at 0.34 s, so it is about 1.5x the projector, not 4x. The ordering holds; the multiple does not. - R1.22:
QDL_STABLE_PROJECTOR_BATCH_WAIT_SECONDS0.10 -> 0.02. Projector queue delay 0.46 -> 0.22 s, -52%, at a load 27% higher (493 vs 387 rec/s); lag 177 -> 108. The fivefold rise in drains cost nothing: projector throttle went down 4.0% -> 1.7% and 4.3% -> 1.8%, andstream_v2_active, which takes every round trip, throttles 0.0%. - Attributed honestly: that change alone moved consumer-visible age 12,205 -> 11,488 ms, -6%, inside the noise of two 20-iteration samples an hour apart. It is kept for the measured queue improvement at lower CPU cost, not for the age number.
- Whole session, 05:04 -> 06:53: event age p50 summed over fourteen
endpoints 12,943 -> 10,460 ms, -19%, improving on 13 of 14; request p50
3,269 -> 1,353 ms; request p956,767 -> 2,265 ms; rejections 5/78 -> 2/80, the two remaining being the known BinanceMARK_INDEX_PRICE; OHLCV 20/20. Limit of this figure: it bundles the CPU rebalance, the batch wait and the broker recreates across two hours of differing market conditions. The direction is consistent on 13 of 14 endpoints; the attribution is not separable beyond the isolated -6% above. - R1.18 landed:
parse_canonical_progressandsteady_state_lag_secondswith 19 new tests; the convergence gate and its existing tests are untouched, and both readers now carry docstrings saying what they are and are not for. The new measure returns None rather than a number for a stalled projector, a group reset, a mid-interval rebalance or a non-positive interval. - Log rotation live on 6 of 17 roles - three projectors, three brokers,
about 95% of log volume. First measured broker restart on this stack: each
recreate left a projector backlog of 3,458-20,200 that drained in about three
minutes, ISR returned to 3 on all 154 partitions, and the consumer fell to a
worst of 34/60 ready before returning to 60/60 about five minutes after the
last broker.
v1_fallback_countandv2_error_countstayed 0: it degraded, it never failed over. - Trap caught before it fired. The compose override chain recorded in the
container labels is incomplete - it omits the R1 stream and query image
overrides, so
up -dagainst it resolves stream and query tosha256:8bd10da6...(2.0.12) and moves the bar edge. Every recreate went through a packet pinning all 17 roles to their running digest; 0 of 17 drifted, before or after. - Disk:
150 -> 96 GB,52% -> 34%, free 194 GB. Build cache13.52 -> 4.85 GB, six orphaned volumes removed. - Suite: 1,514 tests, 0 failures, 7 skipped, 405 s in
qdl-v2-python:2.0.16-df4b8aa. - Pinned at: no image changed. All seven python roles
qdl-v2-python:2.0.16-df4b8aa, bar edge2.0.15-5130f6f, rustqdl-v2-rust:2.0.15-c5a5be0, brokersapache/kafka@sha256:9516fb76. Rollback packet~/.local/state/qdl-v2/dlv2-r122-projector-age-20260917T062751Z/. - Next lever, already measured:
rust_coreat 0.89 s is not CPU-starved (2.0% throttle, 0.10-0.45 cores), so the cost is a batching or flush interval in the Rust core. Not opened in this session rather than left half-done.
- Root cause, one for both incidents: containers that had run for weeks were holding configuration generations that no longer exist on disk. Nothing here is a new defect; it is old drift becoming visible the first time each process was asked to read its configuration again. 16 of 17 roles are now proven restartable, one at a time, with observed recovery.
- Corrects entry 27: the
rust_corestage is 0.34 s, not 0.89 s. The higher figure was sampled minutes after the projector recreate while the system was still settling. It is about 1.5x the projector, not 4x. - Withdrawn:
rust_corebatch_size256 -> 64. A single-core A/B showed0.38 -> 0.20 s(-47%) with both controls worse, which looked decisive and did not reproduce: with all three cores changed, twelve samples over five minutes gave 0.41 s against a 0.34 s baseline. Reverted. Unproven, and it costs four times the Kafka transactions. The method lesson: in 90-second windows the variance of this signal exceeds the effect, and controls moving the other way inside one short window prove nothing. - Incident, 22 minutes, caused by this session. Recreating
binance_bar_edgefor log rotation exposed a checkpoint atcatalog_revision 7 / acquisition_revision 14that no configuration on this host still produces (image 8/16, packets 9/17). Recovered onto a fresh state path, old checkpoint preserved and backed up. The bootstrap cost was mine to foresee and I did not: I verified the 10,000-row cap per binding and did not multiply by 140 bindings. About a million records in five minutes, projector backlog 601,622,TRADEage 250-310 s, every other feed rejected on freshness, consumer down to 24 of 60.v1_fallback_countandv2_error_countstayed 0 throughout: it degraded, it never failed over. - The throttle watchdog written that morning paid for itself immediately.
The projectors were throttled 67-92% at 0.50/0.75 while
docker statsshowed 50-75% and looked like headroom. Raised live to 2.00: drain114 -> 900 rec/s, backlog cleared in 15 minutes. They settled at 0.9 of a core with 0.0% throttle, so they were returned to 1.00, not to 0.50/0.75. - Open defect, not guessed at: no component publishes OKX realtime bars. The
acquisition catalog marks OKX
bar-1mRUST_NATIVE; the bar edge's realtime loop takesPYTHON_RESTonly and correctly skips them; the rust core config has 197 bindings and zero bar bindings;ingestor_okx_swapcarries no BAR feed. Measured: Binancebar-1m27 s old, OKXbar-1m1000 s and ageing. They worked until today only because the old bar edge was still running the orphaned revision 14 catalog. Three options are written up in the plan; all need an owner decision, and option 1 touches the image v2.0.16 was certified on. - Log rotation: active on 14 of 17 roles.
stream_v2_active/passiveexcluded (recreate stalls the projectors, for 1-2 MB of log) andstable_redisexcluded (recreate destroys the projection cache identity by design). - VN/DNSE not testable from this host:
openapi.dnse.com.vnrefuses TCP 443 from container and host, with and without proxy, whileapi.dnse.com.vnandservices.entrade.com.vnanswer. Credentials and FPT/VN30F1M bindings are present; the endpoint is unreachable. - State:
QUOTE381-436 ms,TRADE319-839 ms,BOOK_SNAPSHOT767-1265 ms, OKXMARK_INDEX_PRICE590-753 ms, BinanceBAR1m27 s, projector lag ~180, disk 34%, no image changed anywhere in this session. Consumer tops out near 51-55 of 60 until the OKX bar owner is decided.
Pinned at: dev ff34867, qdl-v2-python:2.0.17-436171f
(sha256:97d2f593f0a6…), qdl-v2-rust:2.0.17-d9adee3
(sha256:4aa578fc0be7…), 13 of 17 roles recreated from
dlv2-r125-rollout-20260917T125950Z.
- OKX native bars never reached the canonical stream, and the reason was a
cross-lane comparison. 1,067 quarantine records, every one
StaleGeneration"connection generation is stale", while raw carried all 70 candle channels and provisional candles were filtered correctly. The ingestor runs one socket per feed class, each with its own generation counter: OKX stood at BOOK 53,986, QUOTE 82, TRADE 70, MARK_INDEX 37 and BAR 23. The ordering fence compared those as bare integers before it looked at the session. The system proved it itself - the bar lane reconnected at 11:29Z from generation 22 to 23 and the brand-new session was fenced identically. Repaired by scoping the fence to one lane; an unparsable session id keeps the original comparison, so nothing an unknown producer sends becomes newly acceptable. After the repair and a live ingestor reconnect: OKXbar-1mp50 1.19 s, p95 1.67 s, all five instruments, zero candle quarantines. - The spool failed every write closed while inside its own retention policy.
2,252,414,976 + 966,902,232 + 1,900,544 bytes against a 3 GiB bound: 7,720
bytes of headroom, two hours of
503to every projector. The WAL frames were already checkpointed - PASSIVE recycles a WAL but never shrinks the file- and one TRUNCATE reclaimed all 922 MB in 0.08 s with no reader blocking
it. The bound was also tighter than
max_recordspermits: 183 partitions × a 10,064-record window = 1,841,712 rows, and the live cache held 1,301,097 rows in 1,114 MB of payload inside a 2,312 MB file. Bound now 6 GiB, derived; WAL reclaimed at its ownjournal_size_limit. Live: 2,378 MB of 6,144 MB, WAL 64 MB.
- and one TRUNCATE reclaimed all 922 MB in 0.08 s with no reader blocking
it. The bound was also tighter than
- The backlog could not drain, and the disk was not why. Consumption had
converged on production at 759 events/s with projectors at 0.49 of a 2.00
ceiling, no throttling, and the spool's own volume benchmarking 40,280
rows/s at
synchronous=FULL. The projector asked Kafka for one record at a time and paid a thread hop for each. Batched: 1,029 events/s. Honest limit: the thread hop was real but not dominant, and the remaining ceiling is the projector's single event-loop thread. Recorded, not guessed at. - Binance mark price and native klines are a venue condition on this host, not
our configuration. Binance USD-M answers
{"result":null}to a subscribe for@markPrice@1sand@kline_1mand then sends nothing - 80 s, zero frames - on the same socket wherebtcusdt@tradedelivered 353 frames in 12 s. Four subscription shapes tried for mark price, all zero. This is the conditionproduction_catalog.pyalready recorded for klines; it holds for mark price too. Neither can be repaired from the WebSocket path. - Binance's seven-second bars are evidence, not caution. A closed BTCUSDT 1m kline was still changing 5.04 s after its close boundary, ETHUSDT 3.96 s. The bar edge's 6 s settlement plus two confirmations is exactly sized for that. Cutting it would publish bars the venue then revises.
- The BAR gate was wrong and is now measured.
max_freshness_ms = 180000is a tolerance, and the canonical envelope timestamps a bar by its open time, so "age of the newest record" can never read below 60 s for a 1m bar. The instrument is publish time minus close time, final bars only. - Still open:
stable_redisrecords a compose chain from a deleted worktree; repairing it needs a redis recreate plus the projection cache rebuild runbook, which is not something to run while a backlog is draining. With six partitions, three replicas and range assignment, a drained replica cannot take work from a loaded one - measured mid-drain at projector-1 idle on 43 and 26 records while projector-3 carried 1.29M.
30. Entry 29 corrected: the OKX fence was losing to the bar edge, not to another connection (2026-09-17)
Pinned at: dev 1acf87a, qdl-v2-rust:2.0.17-1acf87a, three cores and two
ingestors recreated from dlv2-r125-rollout-20260917T125950Z.
- What entry 29 got wrong. It named cross-lane connection generations as the cause. That repair is real and its tests hold, but it was not the cause: after it shipped, three of five OKX instruments published and ETH and SOL kept being quarantined on live frames carrying the single business session's generation 25, while neither the raw nor the canonical stream contained any OKX generation above 25 at all. The configurations of the failing and the working instruments were identical.
- How the real cause was found. Not by a third hypothesis. The rejection was
made self-describing - it now logs the tracked session and generation it
compared against - and answered on the first occurrence:
tracked_session qdl-v2-stable-okx-rest-r1-g1789653808007759187,tracked_generation 1789653808007759187. - The cause. The bar edge publishes REST bootstrap and repair rows carrying a nanosecond timestamp where a connection generation belongs. One such row on a bar partition fences every native candle behind it for the life of the core process: 25 never exceeds 1.79e18. The timestamp decodes to 14:03:28Z, the minute the bar edge was recreated in this session's rollout and bootstrapped its history; quarantines began at 14:20. The 09:16 outage has the same shape - the bar edge was given a new state path at 07:53.
- Why the first repair let it through. When it could not parse an identity it kept the bare comparison and called that conservative. It is the opposite: an unparsable identity is a different producer whose numbering means something else, so comparing the two is meaningless rather than careful. Only two positively identified identities in the same lane may fence each other.
- Why the suite never caught it. The test helpers built session identities
(
s1,session-1) the running system never emits, so the tests exercised a comparison production never makes. They now use the production shape; two tests went red on the correct behaviour and were fixed rather than the code. - Proof. After rollout: 10 of 10
bar-1mendpoints publishing, 8 closed bars each over eight minutes, OKX fromokx-business-001-26and Binance from the bar edge on the same partitions, zero stale-generation rejections across all three cores, consumer unhealthy slices 40 → 8 of 60. - The repair that proved it. Republishing 1,074 missing bars through the documented repair tool re-poisoned every OKX bar partition within minutes. That is the clearest statement of the defect available: the sanctioned repair path and the native path could not coexist. 1,106 rows are now back and the gap is zero.
- The sink retry is proven, not just tested.
stream_v2_activewas recreated on its own at 15:50:19Z with the projectors untouched. All three kept their original start time and restart count 0, logged no reconnect failure, and the group stayed at lag 279 with bar partitions 33 s fresh on both venues. Every R1 stream rollout before this one needed a manual projector restart; this is the first that did not. stable_rediswas deferred for the wrong stated reason, and the real one is worse. The reason given was that a backlog was draining. That had already stopped being true. The actual cost, read from the runbook rather than remembered:rebuild_v2_stable_projection_cache.pyaccepts exactly one compose override (scripts/rebuild_v2_stable_projection_cache.py:57-71) while the running chain has thirteen and the env file declares none, so it would recreate every service from the bare compose file and discard every image pin including this release; and it deletes the durable spool outright (:49-53) - 2.3 GB holding seven days of bar history and the 1,106 rows repaired an hour earlier - replaying only 900 s afterwards. The correct sequence is to consolidate the thirteen overrides into one file first. That is a maintenance window, not a release step, and consolidating them is worth doing properly because this chain is the origin of the whole config-generation drift class in entry 28.
The owner compared this session's numbers against v2.0.16 and asked why they had become seconds. Two of the three quantities involved were different quantities, and one regression was real.
- Same quantity, same benchmark, run twice. Snapshot reads 6.4-7.5 ms then,
6.7-9.2 ms now. Instrument lookup 4.1 then, 4.3-5.2 now.
BOOK_SNAPSHOT73-78 ms then, 44-50 ms now. Durable event age: QUOTE 437-700 then, 577-780 now; TRADE 804-1,711 then, 728-1,575 now. Nothing there moved by more than noise, and one endpoint class halved. - The real regression: BAR warmup, 117-156 ms → 364-428 ms. Not caused by
this release.
stable_source.historyreads the entire retained window for any BAR requirement, because a history repair legitimately appends older bars after live ones and the market tail cannot be taken from the append tail. At 04:11Z those windows were far from full and OKX bars did not exist at all; they are full now, partly because this session repaired 1,106 bars into them. - Where the time goes, measured rather than reasoned.
read_tailon one 1m partition: 1 row 0.0 ms, 1,000 rows 17.9 ms, 10,064 rows 199.8 ms - the cost is materialising a Cursor, a decoded header mapping and a dataclass per row, almost all discarded. Decode and sort inside_recordscost 41.4 ms more. - Three variants, and the numbers chose. Decoding each payload twice, 41.4 ms. Carrying the decoded envelope to avoid the second decode - the first idea - 52.1 ms, worse, because ten thousand live protobuf objects cost more than the decode they save. Carrying only the open time as an integer, 26.5 ms. All three returned identical rows.
- The attempt that broke it. Reading the window as plain rows and resolving
only the survivors through
find_eventspassed 137 tests and then failed every BAR warmup in production withwarmup batch item failed inside the bounded executor. Reverted within minutes; warmup verified working again at 390-475 ms. The suite did not catch it, which is the finding worth keeping: the bounded-read contract is pinned by a test double and a spy, and neither exercises the executor the real query path runs inside. - What the real fix needs. The spool must know a bar's market time without decoding it - a column and an index on the durable write path, with a migration. That is a designed slice, not something to improvise while a release is waiting, and today is the second time that lesson was paid for.
The release head was pushed and CI came back failure. So had the five pushes
before it. Every one failed at the same place: contract-tests step 10,
Test Rust generated contracts.
- The gate is three clauses, and I had been running one.
cargo fmt --all -- --check && cargo clippy --workspace --all-targets --locked -- -D warnings && cargo test --workspace --locked. I rancargo test, got 43 + 36 green, and wroterust_gateinto the certificate as if that were the gate.fmtis the first clause, so nothing after it ever ran on CI. - What broke it was the fix for the outage. Replacing synthetic session ids
(
s1,session-1) with production-shaped ones (qdl-test-lane-001-1-1700000000000000000) pushed sixtracker.observecalls past rustfmt's width limit. Green locally, red on CI, from1acf87atof104d7c. - Why it went unseen for six runs. I was watching production - quarantine counts, projector lag, spool headroom - and production was the thing at risk. CI was a tab I never opened, and the certificate said the Rust gate had run.
- How it was read. The job-log endpoint needs admin rights on the repo and
returned 403, but
/actions/runs/<id>/jobsgives every step's conclusion without them - enough to name the failing step. Reproduced locally in the samerust:1.82image, which printed the exact rustfmt diff. - Closed.
cargo fmt --all(053ea9b, whitespace only), then the full three-clause command in that image: FMT OK, CLIPPY OK with-D warningsand zero warnings, 165 tests passed, 1 ignored across the workspace. The certificate'srust_gatenow records the command, the toolchain, all three results and this correction. - The rule. A gate is the command CI runs, not the part of it that is convenient to run by hand. Reporting a subset under the gate's name is the same class of error as entry 32's test doubles: the check passed, and it was not checking the thing.
Clearing build artifacts after the tag was pushed, every digest in
certificate.json was mapped back to a local image id. Two of the four did not
point where their tag said.
active_rust_image_digestreadsha256:4aa578fc, which isqdl-v2-rust:2.0.17-d9adee3- the first lane fix, the one whose_ => truefallback was the defect. The tag beside it,qdl-v2-rust:2.0.17-1acf87a, was correct.active_python_image_digestreadsha256:97d2f593, which isqdl-v2-python:2.0.17-436171f, an image built before the warmup change was reverted.image_source_commitreadff34867, the commit the certificate was first drafted at, and not a commit either running image was built from.- What actually runs, read from the containers: five rust roles on
sha256:b05d4446, eight python roles onsha256:42fe008a. The running stack was right the whole time; the paper describing it was not.
The cause is plain: the digests were captured by hand when the certificate was drafted, and two rebuilds later - the revert and the second lane fix - nobody went back for them. A rollback driven by that block would have deployed the build that caused the outage.
Fixed by taking each digest from docker image inspect together with the
image's own org.opencontainers.image.revision label, so tag, digest and commit
check each other, and by recording the previous values inside the certificate
rather than quietly replacing them.
The tag was already published when this was found. The corrected certificate
is on main; the asset attached to the GitHub release still carries the stale
digests, and replacing it needs the release deleted and the tag re-pushed, which
is the owner's call, not mine.
Same class as entries 32 and 33: the check ran, and it was not checking the thing. Here the check was a human copy of a value that later moved.
Closed the same day. The owner deleted the release; the tag was deleted on
the remote and re-cut at 658b6e8, the corrected head of main. The asset now
published under v2.0.17 is byte-identical to the certificate in the repository
(sha256:0eaf55d3f06c3fcd0a68ce3b0518868efedbdc31842380cf888efef3529d3583,
19,408 bytes) and names sha256:42fe008a and sha256:b05d4446 - the two images
the containers are running. The superseded builds 2.0.17-436171f and
2.0.17-d9adee3 were then removed by digest; their identity survives in
digest_correction.previous_values and their source survives in Git.
One practical note for the next time evidence has to be read back from a
release: the browser download URL returns 404 through this host's proxy, because
it redirects to objects.githubusercontent.com. The asset endpoint with
--noproxy '*' and Accept: application/octet-stream returns it.
R1.27 Phase 2. Applied 06:11:13Z, rolled back 06:21:04Z, one role each way.
- The thing it was built for worked. Binance mark/index went from zero
canonical partitions to five within a minute, newest record one second
old. For five months that plane did not exist, because the ingestor opened
the base Binance decommissioned on 2026-04-23 and
/marketchannels are acknowledged and then silent. - A feed that had nothing to do with the fix stopped. Binance
bookfroze at the recreate: nine partitions 425 seconds stale whilequote,tradeandmark_index_pricesat at 0-1 seconds and OKX was fresh throughout. Book feeds Risk's L2 gate, so the rollback was not a judgement call. - The rollback identified the cause and restored the system in one command. Book returned to 0 seconds on the pinned image, which rules out the restart: the previous binary resyncs its book and this one does not.
- Five hypotheses were closed before rolling back, so the retry does not
re-walk them: the REST snapshot endpoint answered HTTP 200 from the
ingestor's own network; all four lanes were
LIVEwith fresh transport; the cores quarantined nothing; the ingestor logged no error in ten minutes; and@depth@100msclassifies to/publicwith the lanes opened as the feed table says. The frames reach the socket and do not become canonical, and why is still unknown. - One acceptance item was wrong before the work started. I wrote that the
alpha cache would read Binance mark/index from the canonical plane. It cannot:
adapters/market_data/data_layer_v2.py:752routesMARK_INDEX_PRICEto the reference batch unconditionally, in the consumer, so no data-layer change can move it. The routed lane is the prerequisite for that switch, not the switch.
The finding worth keeping. Phase 1 was green three ways - fmt, clippy under
-D warnings, 177 Rust and 1,579 Python tests - and none of it covered the one
feed whose bootstrap is stateful. Neither suite takes a Binance BOOK lane from
subscription through the REST snapshot to a verified book. Entry 32 said the
same thing about a warmup: the check ran, and it was not checking the thing.
Twice now a unit-green change has been proved wrong only by production.
So the order is fixed before the next attempt: an offline harness that fails when the book bridge does not complete, then a diagnostic that makes a book which never verifies say so - today it is silent in both processes, exactly the condition entry 30 paid for - and only then a second recreate.
36. The lane-aware book fence is in production, and the counter that would have found it (2026-09-18)
R1.27 Phase 2b. qdl-v2-rust:2.0.17-e9cb4b7 (sha256:432f4b62e567) on
rust_core, rust_core_2 and rust_core_3, recreated one at a time at
07:41:52Z, 07:45:41Z and 07:48:58Z, each verified before the next.
- All five gate items held. Running with
restart 0and no error line; progress resuming within seconds; quarantines flat at 4/4/0; every Binance and OKXbook,quoteandtradepartition newest 0 s; consumerexecution_ready34 → 36 → 38 → 36, never below where it started, withv1_fallbackandv2_errorat 0 throughout. - Backward compatibility was measured, not argued. Zero
IGNORED_STALE_GENERATION, zerol2_frame_refusedand zerol2_session_beganon all three cores over twenty minutes. The running ingestors change their session id on every reconnect and never their lane, so the new decision path is not reached and today's behaviour is unchanged. filtered_by_outcomepaid for itself on its first use. A core restart now saysBUFFERED_AWAITING_BOOTSTRAP,REJECTED_AWAITING_SNAPSHOT,BOOTSTRAP_APPLIED,KEEPALIVE- and each froze once the books had their snapshots. Before 2a that was onefilteredtotal, which is exactly why a book dying silently and a book bootstrapping normally were indistinguishable on the morning of 2026-09-18.
Two readings that would be misread without saying so. Quarantine counters
are per process: a fresh core starts at zero, so 4/4/0 cannot be compared with
the old processes' 2/9/5. And Binance mark_index_price sits at 5,632 s and
climbing - those five partitions were created by the failed 2c attempt and have
had no producer since the rollback; nothing reads them, and they age until 2c
lands.
What this entry is not. It is not evidence that the book fence fix works, because nothing has yet changed a lane identity in production. It is evidence that the fix is inert when it should be inert, which is the only thing 2b can prove and the reason it is a separate phase from 2c.
R1.27 Phase 2c on the fixed cores. Applied 08:04:55Z, rolled back 08:13:52Z.
- The 2a fix fired and was readable. Nine
l2_session_beganevents, one per Binance book binding, each namingprevious_generation 96againstframe_generation 1on a provably different lane. ZeroIGNORED_STALE_GENERATION, zerol2_frame_refusedon all three cores. The same moment on 2026-09-18 at 06:11Z produced no log line and a dead feed; entry 30's rule, applied to a second fence, worked. - The routed lane delivered its purpose. Binance
mark_index_pricereached five canonical partitions at 0 s, against zero partitions before the first attempt. Eight of nine books stayed at 0-1 s; quote and trade never moved. - One book did not recover.
ethusdt-261225, a dated quarterly, ran at a 0.24 s median gap for the forty minutes before the roll - 5,996 rows - then published four rows and stopped. Snapshots kept being applied and the bridge never completed: buffered 498 → 2,157, bootstrap 7 → 24, quarantines 4 → 17, looping about every thirty seconds. The rollback returned it to 0 s within three minutes, which identifies the cause as this path. - Two gate items failed.
execution_ready37 → 32 at T+2 and 33 at T+10, measured where the gate says to measure. And the book item passed only in letter: "newest age 0 s" was true of the feed while one of its partitions was dead.
The finding worth keeping is about the instrument, not the bug. A feed-level
number hid a dead partition, exactly as a bare filtered count hid a dead feed
two days ago. Each time the fix was to make the measure one level finer, and
each time the next failure was one level finer still. filtered_by_outcome is
per core; it needs to be per binding. The acceptance item needs to read every
partition, not the newest. This is entry 33 again in a new place: a subset
reported under the whole gate's name.
What is no longer in doubt. The lane rule, begin_session, and the logging
all work in production under the exact condition that defeated them. What
remains is narrower and offline-reproducible: after begin_session clears the
bootstrap buffer, one book fails to bridge its snapshot to its deltas while
eight others succeed.
R1.27 Phase 2d, source only. Two 2c attempts were diagnosed from aggregate counters and both diagnoses were guesses. The third diagnosis is not: the raw topic keeps eight hours (R1.20), so the window was still there and 8,184 raw frames were captured - the book that stalled, a healthy peer on the same lane, both lanes, deltas and REST anchors.
- The stream was never broken.
puchaining on the routed lane is 2,047 of 2,047 for the stalled book and 4,135 of 4,135 for the peer, zero breaks. Measuring continuity byUinstead reports ~100% gapped on both lanes, including the one where the book was healthy - the measure was wrong, not the data. Holes inUare normal in USD-M and are why the venue addedpu. - The bridge is where it failed. Of eighteen REST anchors in the window, the shipped rule could bridge 2 for the stalled book and 11 for the peer; the venue's documented rule bridges 8 and 14.
- Quoted from Binance, "How to manage a local order book correctly": drop
any event where
u<lastUpdateId; the first processed event should haveU<=lastUpdateIdANDu>=lastUpdateId. Ourcontinuitycompared againstlastUpdateId + 1on both sides and droppedu <= lastUpdateId, so the event ending exactly on the anchor - the one the venue names as the bridge - was discarded as a duplicate and the next event read as a gap. An observed anchor shows it directly:Y = 11588170543546with the following event'suequal toY. - Why it waited five months to appear. A book already
Readyis never re-bootstrapped; the 30 s refresh returnsKeepaliveand this rule is never reached. Phase 2c forced every book to re-bootstrap at once, and the slowest of the nine needed the exact rule to find a bridge.
Corrected to "no hole between the anchor and this event": drop when
u < lastUpdateId, apply when U <= lastUpdateId + 1. That admits the venue's
bridge and a perfectly contiguous successor, and nothing else. Only Binance uses
RangeBridgeThenPrevious; OKX is untouched. Three of the captured frames are
committed as a fixture and replayed by
a_captured_binance_anchor_bridges_on_the_event_that_ends_on_it; restoring the
off-by-one turns it red, so the production stall now lives in a unit test.
Two measures changed, because the measure failed twice before the code did.
qdl_realtime_core_l2_status_changed emits one line per book per status change
with both statuses, the generation, last_sequence, snapshot_sequence and the
pending bootstrap depth - a per-core counter cannot tell one book looping from
every book bootstrapping. And scripts/verify_stable_feed_partitions.py
enumerates every partition of every feed and fails on the worst rather than the
newest; run against production it reports 188 partitions and fails on exactly
the five Binance mark/index partitions the rolled-back 2c left without a
producer.
The pattern worth keeping. Entry 35 was a bare filtered counter hiding a
dead feed. Entry 37 was a feed-level "newest age" hiding a dead partition. Both
times the fix was to make the measure one level finer and both times the next
failure hid one level below it. This time the measure was inverted instead:
enumerate and fail on the worst. A number that answers "is anything fresh" can
always be satisfied by the healthy majority; only one that answers "is anything
stale" cannot.
Gate: fmt ok, clippy -D warnings ok with zero warnings, cargo test --workspace --locked 184 passed, 1 ignored; Python suite 1,579 OK,
7 skipped. Nothing was built, recreated or deployed.
Pinned at image qdl-v2-rust:2.0.17-ee7f1b3
(sha256:a863f7e11c158545f98a4a5f708bc481da013cca393aa2ad1ba2ef9748dc11b9,
revision label ee7f1b3); runtime config
/home/bobby/.local/state/qdl-v2/dlv2-r127-2c-retry-20260918T090834Z/bundle/runtime,
which differs from the sealed bundle in exactly two files -
ingestor-binance-usdm.json and stable-acquisition-bindings.yaml - both
carrying only the routed pair wss://fstream.binance.com/public/ws and
wss://fstream.binance.com/market/ws. Roles recreated: rust_core 09:11:34Z,
rust_core_2 09:15:00Z, rust_core_3 09:15:55Z, ingestor_binance_usdm
09:22:05Z. Nothing else in the 17-role stack was touched.
Certified. shadow-certified is the wrong word here and so is
production-ready: this is production-authoritative. The stack it runs in
is RUST_PRIMARY and serves the live consumer, which read it throughout with
v1_fallback_count=0 and v2_error_count=0.
What it fixes, as one measurement. scripts/verify_stable_feed_partitions.py
before and after the ingestor roll:
09:20Z binance mark_index_price n=5 stale=5 worst_age=4014s exit=1
09:24Z binance mark_index_price n=5 stale=0 worst_age= 3s
188 partitions, 0 stale, 0 empty exit=0
Those five partitions had no producer because markPrice@1s is a /market
stream and the ingestor was still dialling the base Binance decommissioned on
2026-04-23. The data layer had zero canonical Binance mark/index events
before this roll and now has them at a measured 1000 ms cadence.
Soaked. The full eight-item acceptance passed at T+2 (09:24Z), T+10 (09:32Z)
and T+30 (09:52Z): four routed lanes LIVE at gen=2; 188 partitions with 0
stale and 0 empty at all three points; l2_frame_refused=0 on all three cores;
all 9 Binance books READY with pending=0 by name, not by counter;
execution_ready_v2_slices 31 -> 35 -> 36. The QUIET slices at T+2 were the
consumer re-establishing after the session generation moved 1 -> 2 and were gone
by T+10.
Why the third attempt worked where two failed. Not luck and not a retry. The cores carried the corrected bridge rule (entry 38) before the ingestor produced frames under new lane names, and the acceptance was the inverted one: enumerate every partition and fail on the worst. Entry 37's attempt passed its own gate while a partition was dead; this gate could not have.
Four quantities, per routed feed, steady state, from
scripts/report_feed_latency_quantities.py (committed with this change; median
per partition over a 300 s window, 200 events each, 24 Binance partitions, none
quiet):
| feed | 1 venue->recv | 2 recv->pub | 3 pub->durable | 4 venue->durable | p95 total | event period |
|---|---|---|---|---|---|---|
| book (9) | 26 ms | 0 ms | 396-578 ms | 424-608 ms | 0.79-1.22 s | 102-256 ms |
| quote (5) | 27 ms | 0 ms | 427-485 ms | 454-512 ms | 0.72-0.81 s | 55-81 ms |
| trade (5) | 27-28 ms | 0 ms | 496-737 ms | 523-765 ms | 0.58-0.99 s | bursty |
| mark_index_price (5) | 72-74 ms | 0 ms | 437-606 ms | 509-685 ms | 0.85-1.57 s | 1000 ms |
The /market edge is ~46 ms further away than /public; that is the route, not
our code. markPrice@1s arrives at a measured 1000 ms period, the documented
cadence. The dominant hop on every feed is published -> durable, the
projector's commit, already measured in R1.22 - routing did not change it.
The same run against OKX, for contrast and because the owner asked for every endpoint rather than the one being changed:
| feed | 1 venue->recv | 2 recv->pub | 3 pub->durable | 4 venue->durable | p95 total |
|---|---|---|---|---|---|
| book (9) | 28-31 ms | 0 ms | 436-564 ms | 466-594 ms | 0.86-1.21 s |
| quote (5) | 28 ms | 0 ms | 382-577 ms | 411-605 ms | 0.68-0.97 s |
| trade (5) | 29 ms | 0 ms | 493-713 ms | 521-742 ms | 0.68-1.77 s |
| mark_index_price (5) | 45-90 ms | 145 ms | 449-550 ms | 735-1013 ms | 1.27-2.11 s |
OKX MARK_INDEX_PRICE is the one feed in the stack with a non-zero
received -> published hop: 145 ms of normalisation, which is where its total
exceeds Binance's despite a comparable wire and commit. Nothing in R1.27 touched
it and nothing here explains it; it is recorded so it is not rediscovered.
Open, and not caused by this roll. The consumer reports three to five
Binance and three to four OKX QUOTE slices STALE at age_seconds 19-51
while the spool's own quote partitions read 0 s. Symmetric across both venues,
present in the 09:20Z measurement taken before the ingestor moved, and OKX was
not touched by this program. It is a consumer-side QUOTE delivery gap and it
needs its own investigation.
Rollback was prepared, proven twice in entries 35 and 37, and not used:
--env-file rollback.env on the same override pins core 432f4b62 and ingestor
b05d4446 against the sealed bundle.
Pinned at commit of this entry; qdl-v2-rust not rebuilt, no role recreated,
config/v2/stable-acquisition-bindings.yaml revision 16 to 17 in the repository
only. The running stack still publishes Binance 1m over REST.
Certified. tested locally and nothing above it. The admission evidence is
live and from the venue; the lane it admits has never run here.
The admission evidence - scripts/certify_binance_native_bar_admission.py,
against wss://fstream.binance.com/market/ws with a SUBSCRIBE, the same control
shape the ingestor uses:
15 of 15 final klines, five symbols, three closes each
arrival after close: min 0.043 s, median 0.253 s, max 1.144 s
REST identical to the WS final bar: 1/15 at +0 s, 6/15 at +2 s,
8/15 at +4 s, 15/15 at +6 s
The websocket bar is what the venue settles on; REST takes up to six seconds to
agree with it. The 6 s settlement guard is the cost of reading bars over REST,
not a correctness requirement. production_catalog.py had kept Binance BAR on
REST behind a comment asking for exactly this evidence - evidence that could not
exist while the ingestor dialled the base Binance decommissioned on 2026-04-23.
One provider event identity per closed bar. A closed kline now keys on
open_time:close_time, identical to the REST path, so the REST row and the
native row for one bar are one event. A provisional kline keeps its frame-scoped
key. OKX had this by design (canonicalize_okx_bar); Binance did not, and the
cutover needs it because both producers are briefly live. Rust and Python were
changed together and the two Binance bar goldens regenerated; the other 17 did
not move.
One kind, two provider shapes. The bar edge bootstraps warmup history over
REST for every enabled BAR demand regardless of mode, and the core refuses two
bindings sharing a source_id, so a native binding still receives REST-shaped
frames. Found by the C40 live-parity corpus failing with "Binance kline frame
requires k object". The dispatch now sends k to the kline path and row to the
REST path, in both languages.
Gate. cargo fmt --all -- --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked: clean, clean,
186 passed / 0 failed / 1 ignored (184 before). Python suite 1,588 OK,
7 skipped (1,579 before), in qdl-v2-python:2.0.17-5c01cb6. Two new Rust
tests and nine new Python tests, including one that is red if the frame-scoped
sequence is restored.
Reconciliation baseline, before any cutover:
scripts/reconcile_native_bars_against_rest.py compared 25 canonical bars across
the five symbols against REST for the same open_time and found 0
disagreements. It publishes nothing and records no revision; see below.
Why it is not rolled. Two things, neither of which is a code defect.
- The consumer contract for a revised bar is unsettled, which the plan
itself names as a precondition of Phase 3 rather than a step inside it.
adapters/market_data/data_layer_v2.py:407-420treatsFINALandREVISEDidentically and carries neitherrevisionnorsupersedes_event_id, so what a strategy does with a bar it already acted on is undefined. That is why the reconciliation shipped here reads and reports rather than publishes. - The bar edge's checkpoint pins
acquisition_revision._restore_stateraisesstable BAR checkpoint acquisition_revision differs from runtime authorityon a mismatch; the running edge is on revision 14 from its own packet and this change makes the repository 17. Recreating it therefore needs a checkpoint migration that no step of Phase 3 describes - R1.24's "a regeneration is a migration", one layer down.
And one correction. Phase 3 item 3 claimed the bar edge "filters by mode for
OKX but not for Binance". It has been venue-neutral since 302eb21
(2026-08-25): measured against the shipped plan, 70 Binance BAR bindings polled
and 0 OKX. The claim was copied into the plan from a docstring on 2026-09-18
without reading stable_bar_edge.py - the failure rule E1 exists to prevent,
committed while writing that plan. The rule now has a name,
recurring_rest_bar_bindings, and a test on both venues.
Pinned at qdl-v2-rust:2.0.18-3ecf0ac
(sha256:eec6388421845ef570cb3878145d0c5e0398fc44cc2a662805298295cfb0976a) on
rust_core, rust_core_2, rust_core_3 and ingestor_binance_usdm;
qdl-v2-python:2.0.18-<channel fix>
(sha256:afb053ee14fc663c01e8871619d5043cfc449db2b5d215c1cce09ba75dba6898) on
binance_bar_edge. Runtime config
/home/bobby/.local/state/qdl-v2/dlv2-r128-native-bar-20260918T120904Z/runtime.
Rolled 12:17:11Z to 12:26:22Z. Eleven other roles untouched.
Certified production-authoritative: the stack is RUST_PRIMARY and served
the live consumer throughout with v1_fallback_count and v2_error_count at 0.
What changed, measured. Close-to-canonical for Binance USD-M 1m:
before (REST edge) 6,630 ms
after (native /market) 1,107 - 4,852 ms, median ~1,500 ms
every bar: origin=VENUE_NATIVE, lifecycle=FINAL
No gap anywhere. scripts/verify_stable_feed_partitions.py at T+4:
188 partitions, 0 stale, 0 empty. That is the point of the order used, and
it was not the order this plan originally specified.
The order, and why. The cores route a raw envelope to a binding by its
native_channel, and a binding has one channel, so the REST and native
producers cannot both be routable at once. strict_subscription_scope is true,
so a mismatched frame is quarantined rather than ending the consume loop
(qdl-realtime-core.rs:326). That made the cheap failure the REST one: cores
first, ingestor immediately after, bar edge last. The alternative - bar edge
first, as the plan said - would have left two to four minutes with no 1m
producer in V2 at all.
Two regenerations refused, both the R1.24 trap. Generating the ingestor
config from the current catalog removes the five Binance MARK_INDEX
bindings; generating the core configs removes ten of them, five per venue.
Those are the bindings R1.27 brought back to life the same morning. Both were
applied as targeted transforms instead: the ingestor config gained five BAR
bindings and kept everything else, the core configs changed five bindings'
native_channel and provider_kind and nothing else, verified by diffing the
result against the running config before it was applied. The R1.24 migration
that moved MARK_INDEX to the reference path is still unfinished, and every
regeneration will keep trying to finish it.
One defect found in production and fixed in the same window. After the core
roll, the bar edge was still stamping its REST warmup rows rest-klines/1m
while the cores had moved to btcusdt@kline_1m. Every one was quarantined as
FencingRejected - 1,447 to 3,486 per core - and the 1m warmup repair was
publishing into nothing. The canonicaliser already accepted both provider shapes
under one kind; the capture channel had to follow the acquisition mode too. Once
that shipped the quarantine counters stopped moving.
Checkpoint migration. scripts/migrate_stable_bar_edge_checkpoint.py carried
the bar edge across acquisition revision 16 to 17 with all 140 watermarks, the
connection generation and the canonical cache identity, keeping the previous file
at .pre-r17. The edge restored and did not re-bootstrap its watermarks; it did
run its ordinary bounded history repair, which is the pre-existing gap between
warmup_rows=10000 and the spool's retention, not an effect of the migration.
Reconciliation, after the cutover: 20 canonical bars across the five symbols
compared against Binance REST for the same open_time, 0 disagreements.
PROVISIONAL_BAR is live: 8,271 to 11,326 per core in the first twenty
minutes. A filtered provisional kline now says why it was filtered.
Rollback, unused: --env-file rollback.env on the same override pins the
previous rust and python digests and the sealed R1.27 bundle; the checkpoint
restores from .pre-r17.
Pinned at commit 5676656 (the instruments), running cores on
qdl-v2-rust:2.0.19-003b5f9, projectors on qdl-v2-python:2.0.17-5c01cb6
with ceilings raised to 2.0 by docker update; compose at the proven baseline.
Certified incident. Nothing here is a pass.
What happened. Every role's CPU ceiling was raised because every role
showed throttling: kafka2 13.2% of periods, ingestor_okx_swap 5.8% for 5,945 s,
query_v2_1 5.4% for 1,372 s. Throttling fell as intended. Venue-to-durable
latency, unmeasured between the two rounds of raises, went from 475 ms to
17,942 ms at p50, the cores fell behind the raw topic by 13-21 s, sixteen
partitions went stale and the consumer dropped from 50 to 31 ready slices.
v1_fallback_count and v2_error_count stayed 0 throughout; no endpoint
stopped and no consumer fell back.
Why. On a 16-vcore host at load 12, the tight ceilings were the only admission control the stack had. Raising fourteen of them at once let every tier burst together; the producers were raised further than the consumers, which manufactured a backlog; and the first revert came while that backlog was still draining, which put the cores into a lag spiral at 0.50 CPU. The data layer's actual draw reached 6.1 vcore against 3.5 for everything else on the host - portal, trading system, alphas combined.
Recovery, partial. Cores and projectors were given 2.0 and left there. The
cores are healthy (raw_age min 6-29 ms). One quote partition,
binance-usdm-solusdt-quote, is still 42 minutes behind and losing ground;
the projectors are not CPU-bound and log nothing, and the cause is unknown. The
projector spans that would name it were committed in 5676656 and not
deployed before the tuning began - the order this revision's own text
prescribed and its author did not follow.
Also. Five preflight containers had been left running without --rm,
spinning on a missing env var for three to seven hours each. Two were removed
in R1.28 and the removal was reported as complete; three
(goofy_heisenberg, nervous_moore, exciting_kare) were still running and
were removed while writing this entry. Four images with no container remain:
2.9 GB, listed for deletion in the R1.29 guide, Phase 0d.
Recorded rather than edited out because the failure is the method, and the guide that follows it in the plan is built from these numbers.
43. R1.29 Phase 0 to Phase 3: the instruments landed, every tuning candidate was rejected (2026-09-18)
Pinned at commit of this entry. Running images, each digest from
docker image inspect:
| role | image | digest |
|---|---|---|
rust_core, rust_core_2, rust_core_3 |
qdl-v2-rust:2.0.19-003b5f9 |
sha256:b7b9d153f0ed… |
ingestor_binance_usdm |
qdl-v2-rust:2.0.18-3ecf0ac |
sha256:eec638842184… |
ingestor_okx_swap |
qdl-v2-rust:2.0.17-1acf87a |
sha256:b05d44467942… |
projector_v2, _2, _3 |
qdl-v2-python:2.0.19-211bf14 |
sha256:1d99cc83f8e0… |
stream_v2_active, _passive |
qdl-v2-python:2.0.19-40629b7 |
sha256:656e7e043dea… |
query_v2_1, _2 |
qdl-v2-python:2.0.17-5c01cb6 |
sha256:42fe008a1b9f… |
binance_bar_edge |
qdl-v2-python:2.0.18-5ea5915 |
sha256:afb053ee14fc… |
The projectors are one commit behind the streams. 40629b7 differs only in
serve_stable_stream, so their behaviour is identical and R3 left them alone.
Certified. The instruments are production-authoritative. The tuning is
measured and rejected - nothing from Phase 2 is in the running stack.
Closing state, 20:17Z: 188 partitions, 0 stale, 0 empty;
v1_fallback_count and v2_error_count 0; consumer 49-56 ready and 30-37
execution-ready across the session; data layer draw 4.64 vcore against a 5.0
budget; compose ceiling sum 20.75, the baseline, unchanged; no unnamed
container; nine qdl-v2-* images, every one of them referenced except the
ingestor's one-step rollback.
What landed. Three roles that had never logged now log. The projector and
the stream had no handler at all - stable_bar_edge.main calls
logging.basicConfig and nothing on their paths ever did - so the projector's
"generation failed; reconnecting" and the gateway's subscription counters were
written into a root logger with nothing attached, for as long as those roles
have existed. uvicorn took the handler back off the stream after basicConfig
put it on, which the first C5 roll proved and log_config=None fixed.
What that instrument then answered, in one session:
- The 42-minute
solusdt-quotestall was consumer lag on the canonical topic, 18 and 36 minutes on two projectors,durable_appendabout 1 s. It drained on its own at twice real time and nothing was changed to make it. rust_coreat a 0.50 ceiling is disproven:raw_agemin 8 ms to 5,755 ms in twelve minutes, pinned at 51% of its ceiling. Paid for under R4 byrust_core_2, which drew 0.09 of a 1.00 ceiling at the same instant - the three cores stopped being equally loaded across R1.24, R1.27 and R1.28.- The QUOTE livelock is
rejected_at_push254,834 againstaged_out_at_read956, and there is not oneslow_consumerline: the gateway never ejected a consumer, so those reconnects are the consumer's own. The guide's own hypothesis was the other way round.
Every tuning candidate was rejected on its own criterion, which is the
point of writing the criterion first. C3 cut kafka2's throttling from 16.9% to
4.2% and still failed - p50 fell on two feeds of four while draw went 5.08 to
5.63 vcore and load rose. C2 raised the raw_age it was meant to lower, and
the commit improvement that looked like its doing appeared equally on the two
cores it had not touched.
Three corrections of this executor's own reports, recorded rather than edited out:
- A drain check read column 7 (
p95) where the criterion names column4 total, so two windows were called failures and were not. - The incident entry said the compose file was back at baseline. It was not:
the revert was a
git checkoutof a file whose raises had been committed, and the ceiling sum was 26.00 until 18:20Z. - R1.28 reported Binance weekly bars at
origin=RECON.BarOriginisAGGREGATED 2, BACKFILLED 3, RECONCILED 4; the value was 3, which isBACKFILLEDand is correct for a bootstrapped weekly bar. There was never an anomaly.
Gate. Python suite 1,598 OK, 7 skipped, in qdl-v2-python:2.0.17-5c01cb6.
No Rust source changed after 003b5f9, whose three-clause gate was green at
186 passed.
Not done, and why. Phase 3 item 1 - the thirteen Binance intervals - is gated on Phase 2 holding twenty-four hours and waits. Phase 4's push and release were refused by this environment's publication control and need the owner. R1.30 is opened for the MARK_INDEX regeneration trap, which R1.28 survived only because someone diffed by hand.
Pinned at. dev at 766d5ae; qdl-v2-rust:2.0.19-003b5f9
(sha256:b7b9d153f0ed…), qdl-v2-python:2.0.20-e7fd0c9
(sha256:a8419c182180…). Catalog revision 8, acquisition revision 17,
routing revision 18 - none moved.
Roles and digests as certified:
| role | image |
|---|---|
rust_core ×3, ingestor_okx_swap |
qdl-v2-rust:2.0.19-003b5f9 |
ingestor_binance_usdm |
qdl-v2-rust:2.0.18-3ecf0ac |
projector_v2 ×3, query_v2 ×2, binance_bar_edge |
qdl-v2-python:2.0.20-e7fd0c9 |
stream_v2_active, stream_v2_passive |
qdl-v2-python:2.0.19-40629b7 |
ingestor_binance_usdm and the two stream roles are at functional parity with
the release commit: the streams have zero files changed in qdl/stream/
since their image, and the ingestor's one file is qdl-realtime-core.rs, which
is not the binary it runs (docker inspect shows
/usr/local/bin/qdl-native-raw-ingestor).
No Rust image was built. git diff 003b5f9..HEAD -- rust/ Cargo.* generated/rust
is empty, so the deployed 2.0.19 image already is HEAD's Rust; ingestor_okx_swap
was moved onto it rather than onto anything new.
A lossy regeneration is now refused, not caught by eye.
scripts/assert_runtime_bundle_is_not_lossy.py, 15 tests. Against the live
bundle it reads 629 bindings in five files and refuses all forty MARK_INDEX
identities. A first cut keyed only on source_id read 576 in three and missed
both ingestors entirely - which is the count-neutral half of the R1.30 trap. It
earned itself the same hour: the unapplied ingestor-okx-swap.json in the r128
packet holds 24 identities against the running 94.
The VN plane is pinned to V1 by test, not by memory. All four
vn_primary_v2 requirements already routed V1_PRIMARY/NONE; three further
fences hold - BAR_EDGE_RUNTIMES, the stable-vn Compose profile, and no DNSE
binding on any ingestor. 10 tests. Owner decision, 2026-09-19.
A projector that cannot close its broker no longer stops projecting.
projector_v2 and projector_v2_3 were Up, silent and idle from 05:07 and
05:09, both stopping immediately after attempt=5 with retry_max_seconds at
5.0. The only await in the recovery path was an unbounded
asyncio.to_thread(broker.close). Kafka had rebalanced all six
md.canonical.v2 partitions onto the one survivor, lag to 47,258. Bounded
at 10 s, 6 tests driving the real supervisor with a blocking broker. After the
roll: three consumers, two partitions each, lag 318.
Seven of thirteen Binance intervals admitted on the routed lane: 3m, 5m,
15m, 30m, 1h, 2h, 6h, each PASS on 5 of 5 symbols, final bar 0.079-0.527 s
after close against 7,820-22,473 ms on REST. The lane is proven for all
thirteen - subscription accepted, 63-64 provisional frames each in 45 s, and
every long interval's t/T matching canonical_interval_ms with 1w
anchored to Monday. Six captures run unattended to their own boundaries at
0.03 vcore.
The script could not have been run per interval before this: its budget was
60 * (BAR_COUNT + 1), one minute per bar, so every interval other than 1m gave
up before its first boundary and printed nothing. 1m is unchanged at 160 s, so
entry 40's evidence still stands.
R1.30's gate is answered, and the answer is no. R1.24 left one unknown and
R1.30 restated it: can the reference path serve MARK_INDEX_PRICE? It serves
it and cannot meet the bound the manifest grants. Measured through
OkxRestClient itself, on a 90-100 ms round trip:
| manifest | max_freshness_ms: 2000, EXECUTION, ten requirements |
OKX REST index-tickers ts |
764 - 1,414 ms stale at the venue |
| consumer observes | 2,088 - 2,501 ms, p50 2,212 |
| canonical binding | 286 - 418 ms end to end |
Both terms the consumer divides are stamped at fetch, so no cache, lane or bucket on this side closes it.
17/17 roles Up, 4.86 vcore of the 5.0 budget, quarantines 0,
scope_quarantines 0, raw_age_ms mean 138. 188/188 partitions inside their
own declared stale_after_ms - read from a --rm side container with the
state volume mounted read-only, which is the only way this should have been read
all session. Consumer errors per minute against the pre-session window:
DataLayerError -96%, SilentSliceError -47%,
StaleExecutionReferenceError -32%.
Gate. Python suite 1,628 tests, 7 skipped, 0 real failures in
qdl-v2-python:2.0.20-e7fd0c9. The four reported errors were all
RotatingFileHandler opening /app/logs/app.log under a mount the container's
uid could not write; the same four modules run 14 tests and pass with it
writable. No Rust source changed after 003b5f9.
Cleanup. Build cache 15.57 → 6.46 GB, disk 113 → 107 G. Deleted by digest:
qdl-v2-rust:2.0.17-ee7f1b3, qdl-v2-python:2.0.20-6bd9e74 (a duplicate
2.0.20 tag), two anonymous 0 B volumes. Kept deliberately: one rollback digest
per role, rust:<none> (the builder base pinned by
Dockerfile.qdl-rust-runtime), the reuse image set, and the volumes
stable_authority_db, qdl-cargo-home, qdl_c40_authority_admin_packets. No
probe container survives; docker ps -a shows none unnamed.
- A probe restarted a production role. Every endpoint reading ran
docker execinsidequery_v2_1withcache_size=-64000and a full-tableGROUP BY. That role has a 512 MiB limit and serves at 120 MiB; at 06:03 it exited 0 and Docker restarted it. - "The spool is 2.3 s behind" - four times, reversed four times. The probe
read the clock after its own 1.2 s query. Corrected: 286-418 ms, which is
canonical_age261 plusdurable_append66. - "33 of 188 partitions are stale" was a draining backlog, not a state.
- "The 750 ms TTL moved the tail by 580 ms" - it cannot have.
received_at_nsandobserved_at_nsare both stamped at fetch and travel on the cached result, so a cache hit reports the same number. The drop was venue variance. The TTL is kept for a different reason: the cache age is an unreported addition to the age of an execution-grade input. - "Four packets are unmounted evidence" - four of the eleven are live bind
mounts carrying the running configuration; the check used
--filter volume=, which does not match bind mounts.
Item 4 is not implemented. Its fix is to declare the ten MARK_INDEX_PRICE
bindings in the source catalog so the spool can serve them - which reverses
the direction R1.24 chose, on an execution-grade risk input. The latency
consequence of R1.24 is measured here; R1.24's own reasoning is not re-examined.
That is an owner decision, not a release-day edit, and committing a revision-9
catalog the runtime is not on would manufacture exactly the drift entries 39-43
spend their pages chasing.
Six intervals are in flight, not certified. Their boundaries fall at 08:00Z, 12:00Z, 00:00Z and 2026-09-21.
Nothing was pushed, merged, tagged or released. The owner reserved that
("nếu ổn thì tôi sẽ duyệt") and has not given it. Twenty commits wait on dev.
Pinned at. dev at the commit carrying this entry.
qdl-v2-python:2.0.20-95d9595
(sha256:9039236e7a8e570f2364b470b33386ab702bc1dde5ae9d5e7d90a4dda531e8f0),
qdl-v2-rust:2.0.19-003b5f9 (sha256:b7b9d153f0ed…),
qdl-v2-rust:2.0.18-3ecf0ac (sha256:eec638842184…). Catalog revision 9,
source policy 1, authority 1.
| role | image |
|---|---|
projector_v2 ×3, query_v2 ×2, stream_v2_active, stream_v2_passive, binance_bar_edge |
qdl-v2-python:2.0.20-95d9595 |
rust_core ×3, ingestor_okx_swap |
qdl-v2-rust:2.0.19-003b5f9 |
ingestor_binance_usdm |
qdl-v2-rust:2.0.18-3ecf0ac |
No image was built for this entry. The two changes are test-only
(5f85410) and a new read-only script (b7d62e7).
Suite green: 1675 tests, 0 failures, 7 skipped. The three failures at the
gate were caused by this session's own headroom change (64 → 2064) and were
stale literals pinning 10_064. The substantive one,
test_public_bar_warmup_scans_physical_tail_before_market_selection, was
reparametrised on the headroom rather than re-pinned, and then mutation
tested: forcing the bar scan back to the public window fails it with
PARTIAL != FULL, so it still discriminates instead of passing vacuously.
History is correct for past dates, checked against the venue, not against
ourselves. 14/14 Binance intervals, every one FULL, compared field by field
with Binance REST at both ends of the window:
| iv | 1m | 5m | 15m | 1h | 4h | 6h | 12h | 1d | 3d | 1w |
|---|---|---|---|---|---|---|---|---|---|---|
| time | 431ms | 444ms | 323ms | 395ms | 265ms | 269ms | 107ms | 98ms | 100ms | 58ms |
| back | 0.1d | 0.4d | 1.3d | 5.0d | 20.0d | 30.1d | 60.4d | 120.4d | 362.4d | 845.4d |
Delivery to a consumer, measured from where the consumer stands. Stream delivery is the venue's stamp to arrival; the subscribe gate is 40 attempts per feed with the trading system's own certificate, 200 subscribes, 0 refused:
| feed | p50 | p95 | max | budget | margin |
|---|---|---|---|---|---|
| TRADE | 627.7ms | 1029.8ms | 1242.1ms | 3000ms | 1758ms |
| QUOTE | 643.2ms | 1171.2ms | 1188.8ms | 2000ms | 811ms |
| MARK_INDEX_PRICE | 648.7ms | 1285.9ms | 1647.6ms | 2000ms | 352ms |
| BOOK_SNAPSHOT | 625.7ms | 1497.7ms | 1652.7ms | 60000ms | wide |
| BOOK_DELTA | 747.1ms | 1249.5ms | 1562.3ms | 2000ms | 438ms |
MARK_INDEX_PRICE has the thinnest margin in the system at 352 ms and is the
one feed that a pipeline hiccup can push past its BLOCK policy. That is a
property of a 2,000 ms budget against a ~650 ms path, not a defect, and it is
named here so the next reader does not rediscover it as an incident.
Endpoint inventory. 216 catalog bindings: 206 live, 0 over their own
budget, 10 with no event stored. The ten are the four DNSE bindings - owner
decision, served by V1, and live V2 refuses them with required data is not available - and six spot bindings that no ingestor produces and no consumer
manifest requests. The 53 ingestor subscriptions are 24 binance-usdm and 29
okx, none spot, so the six are dead catalog weight rather than a gap.
CPU budget 5 → 6 vcore, spent by measurement. The stack was at 4.70 of 5.0
(94%). The seventeen per-container limits already sum to 17.0 vcore on a
16-vcore host, so the raise went to the four cgroups actually losing time:
kafka2 14.4% of periods throttled (9,612 s) and kafka3 5.2% (2,480 s) to
1.75; rust_core_2 8.9% and stable_redis 1.8% to 0.75. kafka1 at 1.4% and
the projectors and gateways at ≈0% were left alone. Applied with
docker update --cpus, no container recreated - which is what keeps
stable_redis away from ProjectionCacheMismatch. Now 5.66 of 6.0; the
headroom was absorbed at once, which is the evidence the brokers were starved.
Memory was checked separately because a cpus limit throttles and cannot OOM:
worst headroom is kafka2 at 50.2% of 2 GiB, so there is no OOM exposure.
That verification was not a valid comparison, and this corrects it. The
"before" figures are cumulative counters over up to 47 hours of container
life; the "after" was a single 120 s window. They are not the same quantity,
and the control proves it rather than rescuing it: kafka1, deliberately not
raised, reads 1.2% in that window against raised kafka2 at 1.4%. The
window was simply quiet. No throttling improvement is claimed. This
program's own method, from entry 33, is cpu.stat 60-second deltas judged
against request-latency p95 - not cumulative counters, and not throttling as
the target metric.
Two of the four raises rest on stale evidence. Entry 33 already records
stable_redis 0.25 -> 0.50 taking it to 0.0%; the 1.8% used to justify
0.75 here was three days cumulative and mostly historical, so that raise fixed
nothing. And rust_core_2 was cut 1.00 -> 0.50 deliberately under R4 to pay
for rust_core, holding the three-core total constant; raising it to 0.75
partially undoes that decision and the R4 constraint no longer holds.
Judged on the target metric instead, 200 subscribes across five feeds before
and after, same probe: summed p95 6,234 -> 5,642 ms (-9.5%), summed max
7,294 -> 6,443 ms (-11.7%), summed p50 3,292 -> 3,475 ms (+5.6%), and
0 refused both times. MARK_INDEX_PRICE max, the only figure that actually
binds against a 2,000 ms BLOCK policy, went 1,647.6 -> 1,373.9 ms, so the
thinnest margin in the system widened from 352 ms to 626 ms. Ambient load
was higher during the second run, 11.6 against 10.2, from
live_data_executor at 169.79% outside this stack, and data layer draw fell
4.70 -> 3.76-4.29.
Re-measured properly, and one of the four raises was withdrawn. The figures
above are cumulative counters since container start, which is the wrong basis;
entry 33's method is cpu.stat 60-second deltas. Measured that way, with
kafka1 and kafka2 held fixed as controls:
| role | at 0.75 | at 0.50 | draw | verdict |
|---|---|---|---|---|
rust_core_2 |
0.3% | 3.6% | 0.183 either way | raise justified |
stable_redis |
0.0% | 0.0% | 0.034 either way | raise withdrawn |
rust_core_2 draws 0.183 at both ceilings, so its throttling is burst shape
rather than volume: it needs headroom above its average, and 0.75 is kept on
that evidence. stable_redis draws 0.034 - 7% of a 0.50 ceiling - and
throttles 0.0% at either, exactly as entry 33 already recorded when it took it
to 0.0% at 0.50. That raise fixed nothing and was reverted to 0.50.
The controls are what make this readable: kafka1 moved 0.5% → 1.0% and
kafka2 1.5% → 0.7% with no change made to either, so anything under about one
percentage point is ambient noise here. rust_core_2's 3.3-point jump is not.
Final ceilings: kafka2 and kafka3 1.75, rust_core_2 0.75, stable_redis
unchanged at 0.50, kafka1 and every green role untouched. Net ceiling
added is +1.25, not the +1.50 first applied. cpu-budget.override.yml was
corrected to match, and its rendered values were checked against the live
cgroups role by role.
Left for the owner, not changed: query_v2_1 throttles 1.9-3.3% across
these windows, the highest rate in the stack, while drawing 0.072 of a 1.00
ceiling. That is the same burst shape as rust_core_2 and it is not a role
this session was asked to touch, so it is recorded rather than tuned.
The OKX regression was one instance of a structural fault, now closed. There
is no single recorded compose chain for this stack: each container stores the
chain used the last time it was created, and they disagree badly -
ingestor_okx_swap 19 files, the B1 roles 18, rust_core_2 15, kafka2 10,
and stable_redis 1, whose base compose file is a worktree that no longer
exists. Recreating any role from its own label therefore drops every override
added after that role was last built, which is exactly how the OKX ingestor fell
back an image.
The complete chain is now written down as canonical-chain.txt in the R1.31 cpu
packet, with a README stating the two prohibitions - never up -d without
--no-deps, because every stored config hash was computed from a shorter chain
and a whole-stack up would recreate all seventeen; and never recreate
stable_redis, whose base file is gone and whose recreation freezes the spool.
Verified rather than asserted: rendering that chain and comparing image,
cpus and healthcheck against docker inspect for all seventeen running
roles gives 0 mismatch, stable_redis included.
R1.29's C3 is why this is written out rather than declared a win: it cut
kafka2 throttling 16.9% -> 4.2% and was still reverted, because p50 fell
on two feeds of four while draw and load rose. C3 paid for kafka2 by cutting
stream_v2_passive 2.00 -> 1.75; this raise took nothing away, which is the
material difference. The honest verdict is that the raise is not proven
beneficial and not proven harmful. It is kept because the owner raised the
budget, the tails improved, the binding margin widened and no consumer was
refused - not because throttling moved.
17/17 roles Up, 14 reporting healthy; the three rust_core replicas carry
no healthcheck, which needs a Rust change and is recorded with B3.
Four superseded python builds deleted by digest: 2.0.20-4baada5,
2.0.20-d661428, 2.0.20-7f8dac2, 2.0.18-5ea5915. Images 33 → 29,
14.82 → 14.04 GB - layer sharing, not the nominal 3.5 GB. Unused build cache
994.8 MB → 0, 6.60 → 5.61 GB. No volume removed: all three dangling ones
are the three kept deliberately (qdl-cargo-home,
qdl_c40_authority_admin_packets, stable_authority_db). No container
removed: the only two stopped are the stack's own one-shot inits, and every
probe this session ran --rm. Kept as the one rollback per role -
2.0.20-e7fd0c9 (projector/query/bar-edge), 2.0.19-40629b7 (stream),
2.0.17-1acf87a (OKX ingestor, and referenced by three files in the active
chain). tradingsystem-image:v1.2.4-8ef859a was left alone as another repo's
rollback target.
- A regression I caused, in the B1 roll. At 08:21:55Z
ingestor_okx_swapwas recreated with a chain carryingr125-rollout.override.yml(old digest pin) but notokx-ingestor-image.override.yml, so it fell from2.0.19-003b5f9to2.0.17-1acf87a- the digest that override names as its own rollback - undoing R1.31 item 1 and leaving the OKX producer ninerust/files behind the three cores it feeds. Not a live outage: 206 bindings live and none over budget throughout. Repaired by appending the missing override and recreating that one role; the rendered diff was exactly one line and the B1 healthcheck survived the merge. - "Sixteen bindings are over budget." No. The first cut of the liveness
report compared the spool's
committed_at_nsagainststale_after_ms. The runtime reads aPROVIDER_CONFIRMATIONbinding - all tenMARK_INDEX_PRICEones - fromreceived_at_ns(stable_source.py:483-492), which is the whole point of R1.24. Against the runtime's own rule: 206 live, 0 over budget. - "TRADE stream p50 is 1530 ms, a regression." It was measured six minutes after the projector cold start at 08:29:26, while the restart backlog drained. Steady state is 619 ms. The spikes are confined to 08:29:41-08:30:21 and 08:35:16-08:35:59 and nothing exceeds 3 s after 08:36.
- "MARK_INDEX_PRICE is refused on EVENT_AGE." Same window. Steady state is
0 refusals in 200 subscribes, and the instrument is Binance USD-M, so the
OKX
index-tickersfinding never applied to it.
stable_redis remains a single point of failure and was not touched beyond
its cpu ceiling. Recreating it is the documented way to freeze the spool.
One optimisation is measured and deferred. Each projector posts every canonical batch to both gateways and the non-holder answers 409: 6,080 rejected POSTs per projector per 30 minutes, ~36,000 an hour across three. Removing it is projector code on the write path and belongs with B2/B3.
Released on the owner's approval, 2026-09-19 09:47Z. dev pushed
98ac38a..554cd3d; main fast-forwarded to the same commit and pushed
fd8f62e..554cd3d; annotated tag v2.0.20 (f87f1e4, dereferencing to
554cd3d) pushed, its body the 132-line certificate that is this release's
manifest. The GitHub release is published, not a draft and not a prerelease,
authored BobbyAxerol:
https://github.com/BobbyAxerol/quant-data-layer/releases/tag/v2.0.20.
Published with scripts/publish_github_release.py against a fine-grained token
held in a 0600 file outside the repository; the token was never printed and the
tool refused to replace an existing release. gh is not installed on this host,
which is why the script exists.
Status: SOURCE_CANDIDATE_READY_MEASUREMENT_PENDING. This entry does not replace previous immutable certificates or certify a runtime rollout. Published v2.2.2 provenance is in upgrade/evidence/releases/v2.2.2/ and the Unified Plan publication closure; historical entries above are not a current runtime inventory.
Candidate source 43bbcf0209f49e9cc1af6802af8f8a207a53ee3c, branch fix/p184-dl-closure. Rust 69e3b2b214331...; Python 77682eea5054...; exact digests/roles/rollback in Phase C packet. D2 SDK 2.1.0 response tolerance and IDR-062 top-change materialization are locally tested. Rust 102 PASS/1 existing ignored isolated-Redis test; SDK suites 28 and 91 PASS; tools 8 PASS; packaged Python 29 PASS (overlapping source checks). Read-only BAR diagnostic: 20 retained 15m/30m windows, 214752 rows, zero gaps; not a proof beyond retained bounds. No shared mutation or latency improvement claim.
External evidence: ~/.local/state/qdl-v2/p184-phaseb-20261007/evidence/phase-b-receipt.json. Inherited Phase A 4eb08dc, no catalogue/C2 rerun. Release waits for real/capture candidate resource/coherence evidence, TS DR01-DR08 and broker delta, and owner approval of the actual 12-role packet (handoff listed 13). D1/D3 remain deferred. Only source/docs, two candidate images and scoped test cleanup completed.
Status: PAIRED_OFFLINE_MEASURED_NOT_RUNTIME_CERTIFIED. Does not supersede any release certificate. Paired result: identical ETH Binance/OKX public capture and configs on deployed 658a9570 / rc2 69e3b2b2; no shared changes. Top-change -> next snapshot p95 922/900 ms -> same-event 0 ms (provider timeline, NOT zero consumer latency). 44/41 actual top changes, exact delta parity; CPU observed .0600->.0627 vCPU and RSS HWM 19324->19612 KiB for one core/two bindings. 17 measurement/capture tests PASS. Capture hashed/deleted; test network/containers/keys cleaned; no new images or broad cache cleanup. Phase C: cores first, then Claude's post-rollout >=117/120 per-venue coherence receipt plus CPU/lag gates, then other roles. Prior pre-rollout coherence wording is superseded. Runtime remains HELD, TS DR01-DR08/broker delta and runtime approval still required. No release/push/merge.
Application rc2 remains43bbcf0, undeployed. See readiness report and active pair/capacity. Active pair PASS at135/156top-changes/minute; current whole-stack5vCPU gate FAIL (mean5.9618, n60intervals/307.156s). TS snapshot119/120usable; one strict LAST refusal. Stream overflow+4 and TS cache lag require DR07 attribution. Exact delivery lag/ Redis-ACK/alpha cache latency NOT_MEASURED. Phase C HOLD, not authorized/deployed. Only TS DR01-DR08 includingDR07 +broker delta are release dependencies; no Paper feature construction added. Prior certificates kept immutable and scoped, no freshness/quota/resource threshold change. No required endpoint replacement identified.
Owner-approved bar edge51450612 (sourced6d2637) replacedf7351c3b at02:17:33UTC. No other role changed in this Codex continuation. Existing658a9570 cores retained; coherence uses owner-approved TS IDR076 productpath. Failed0d8da150 excluded. Prepared v2.2.3 names17runtime images and rollback; NOT certificate/tag. Busywindowmean4.882532vCPU,n30/299.992s meets frozen300smean<=5; p955.564052/max5.870669 exceed5. Canonical/raw952.48/ 826.28offsets/s exceed threebaselines, markersincluded, not3000payload/sproof. Streamoverflow+2/replay+15901: TSPaperBTC TRADEqueueclosures bothvenues, followed byBinance replayOUT_OF_RANGE remain affectedrecoveryfindings. No evidence erased. IDR075prepared/tested: DL25/TS28PASS, realpublicpacket validatedbothsides. Target manifest/JWT11,bindingV3r23/64products; runtime10/r22. Fourinversefeeds addedonly topreparedartifacts, linearINDEX alreadyexists. Pairedruntimepreflight, DR01-08/ brokerreceipts, releaseattestation/CI and approvedrollout remain gates. No new build/push/merge/tag or further runtime mutation. See mainplan for cleanup.