Skip to content

Server P10b-v: Live values only the bonds whose inputs changed (D55, D59, D60) - #164

Merged
sujitn merged 2 commits into
mainfrom
server-p10b-changed
Oct 28, 2026
Merged

sujitn merged 2 commits into
mainfrom
server-p10b-changed

Conversation

@sujitn

@sujitn sujitn commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

P10b-v: a Live tick values only the bonds whose inputs changed (D55), in Live only (D59), and a kept record is not re-stamped (D60). This is the next step in the approved P10b order, after #163.

Before this PR, every Live tick valued every pinned instrument (G21–G23). At 256 bonds, a tick carrying one quote still valued 256 bonds and published 256 records.

What counts as a change (convex-ports).

  • MarketSnapshot::same_frame(previous) compares what every valuation shares: the schema version, max_source_skew_ms, the UTC date of as_of, the configuration and its capture. A floating note's valuation date and due fixings follow the date.
  • MarketSnapshot::same_inputs(previous, id, curves) compares one instrument's reference and the InputSet from D58 (its quotes, a floating note's fixings, the curves it reads). For each read curve it also compares the curve record, its recipe and the calibration that holds it.
  • Engine snapshot::same_valuation_inputs passes the curves the context reads (read_curves, from D58).

Graph. CalculationGraph::rebind_built(built, revalue) binds a later built snapshot to the active graph.

  • It resets only the listed nodes, closed over their connected components (new Dag::component). A component is reset whole because capture reads only dependencies committed under the bound snapshot.
  • Every other node keeps its committed value, failure and generation, stays readable and is not calculated again.
  • It refuses (ContextConflict) unless the generation is later and a snapshot is already bound, and when a kept node is dirty or has an attempt in flight. An unknown id gives UnknownNode. A refused call changes nothing.

Runtime.

  • worker::changed builds each pin's context and derived quality. With a baseline whose frame matches, it keeps a pin when its quality is equal and same_valuation_inputs holds. Quality depends on time, so a quote that ages out counts as a change. It checks that results[i] is pin i's record, else RecoveryConflict.
  • worker::rebind values through rebind_built; worker::calculate values only the selected pins.
  • Actor::value serves process and the pending branch of restore_dependencies. Live uses the baseline when it has one and a full activation when it does not. Replay and Captured always value every pin (D59).
  • The tick merges new records into the kept ones by plan index. The outbox, and so the stream, carries only the new records (D60).
  • The baseline is dropped at start, on a delivery loss and on an activation refresh, so the next tick values every pin.
  • RUNTIME.md gains a paragraph on changed-only Live ticks.

Deviations from the plan's wording

  • rebind_built takes no node list. It keeps the active DAG instead of validating a new one at each tick, so the DAG cannot differ from the active one, and a tick no longer builds and validates 256 node specs.
  • Bench case. The one-quote case is live_throughput/burst1/256 in the existing throughput group, not a new live_tick/256.
  • Two planned runtime tests were dropped:
    • "A delivery loss, then new images, values both": a Live delivery loss stops the runtime (run_inputs returns Err(Source) after lose_delivery), so no later tick runs in that process. The baseline drop in lose_delivery is defensive.
    • "A re-marked stored curve values a spread context and keeps a mid-only one": measures are plan-wide, so a two-bullet plan cannot hold one bond that reads curves and one that does not. The ports test covers a read and an unread curve, and D58's read_curves tests cover which curves a context reads.
  • Refresh and override coverage comes from the existing one-bond Live tests live_runtime_applies_an_activation_at_refresh_not_at_the_next_quote and live_override_expiry_schedules_a_policy_refresh_without_a_quote_or_periodic_poll. Both publish after the refresh or expiry, which a kept record would not.

Behaviour that changes

  • The result stream carries only changed instruments.
  • /api/v1/runtime/results can hold records from earlier ticks than the generation and snapshot that health reports. A kept record keeps its revision, generation, snapshot, valuation time, input versions and live_valid_until.
  • live_judges_each_bond_only_on_its_own_inputs now expects one record at the aged tick (the stale bond), with the other bond's earlier record unchanged in runtime_results().

Not done here

  • A tick that changes nothing still copies every record when it merges (next.results.to_vec()). Only a data-deadline tick on which nothing aged can change nothing.
  • Per-tick costs that scale with the bond count are unchanged: snapshot assembly, verify and curve build, and the per-pin context and quality in changed.

Open questions for the owner (from #163, unchanged)

  • After a delivery loss, readiness waits for every key's new image. Since a Live delivery loss stops the runtime, that wait is reachable only through a restart.
  • A backward wall-clock step between two ticks stops a Live runtime.

Tests

  • snapshot_contracts.rs::an_instrument_reads_the_same_inputs_until_its_own_change: a later snapshot on the same day with another generation keeps the frame and inputs. Another instrument's quote and an unread curve are not changes. The instrument's own quote, a read curve and its reference are. The next day, the skew policy and the configuration change the frame.
  • graph_snapshots.rs::rebinding_a_snapshot_keeps_settled_nodes_outside_the_listed_components: a dirty or in-flight kept node is refused. After a rebind, the listed curve and its bond reset, and a pre-rebind ticket is discarded. The other curve keeps the same cached value at generation 7, stays fresh and is not recalculated. An equal generation, an unknown node and an unbound graph are refused.
  • runtime.rs::live_values_only_the_bonds_whose_inputs_changed (two-bond Live plan): three quotes on one bond each publish that bond alone. The other keeps its record, and health reports the newest generation, revision and snapshot, staying ready.
  • runtime.rs::live_judges_each_bond_only_on_its_own_inputs updated as above.
  • The bench adds live_throughput/burst1/256.
  • live_ingress.rs::an_offered_rate_above_the_drain_rate_is_refused_with_429_and_nothing_is_lost (48349c3, after the first CI run). Windows CI saw no 429 in 64 serial batches. A tick now values only the bonds its quotes touch, so one client posting batch after batch keeps pace with the drain at 256 bonds. Main had passed by a thin margin: the whole file ran in 1.2–2.4 s on Windows.
    • Each round now posts 16 batches of 256 at once, four times the 1,024-quote ingress queue, and every refusal must be CAPACITY_EXCEEDED and retryable.
    • Locally, the first round queued 5 batches and refused 11, in each of 5 runs.
    • A key's images can now arrive out of order. The runtime admits each one, so the generation count still holds, and the quote book drops an older image as OutOfOrder without blocking the key.

Numbers

Medians are in this comment.

  • One quote per tick at 256 bonds: 19.8 ms on main, 5.5–6.1 ms here.
  • 256 bonds, bursts of 64: 19.7 ms to 9.5–9.8 ms.
  • Bursts of 512 and the one-bond and 16-bond cases are unchanged.

Verification

In WSL on 07c93d2:

  • cargo fmt --check passes.
  • tools/assurance/check.py passes with and without --base 50c3ec3e.
  • Clippy -D warnings passes on convex-ports, convex-engine, convex-server and convex-ext-redb (all targets).
  • Tests, all with --locked:
    • convex-ports: the full suite ran with only the new contract test failing, on a fixture bug. After the fix, snapshot_contracts passed 9; the full ports suite was not rerun.
    • convex-engine: 205 passed, 0 failed, 1 ignored. The runtime file passed 3 of 3 runs (23 tests each).
    • convex-server: 267 passed, 0 failed, 0 ignored.
    • On 48349c3, fmt and clippy on convex-server pass, and live_ingress passed 5 of 5 runs with --test-threads=1, as Windows CI runs it.
  • Not run natively on Windows; CI's "Server on Windows" covers it.

Summary by CodeRabbit

  • New Features
    • Live updates now recalculate and publish results only for instruments affected by changed inputs or settings. Unchanged instruments keep their existing results, reducing unnecessary updates.
    • The first update after startup or a refresh still recalculates all instruments. Replay and Captured modes continue to recalculate every instrument for each event.
    • Live results may include records from earlier snapshots when those instruments have not changed.

…D59, D60)

A Live tick compares each pin with the previous tick's snapshot: the frame
(configuration, capture, skew policy, valuation date), its reference, its
InputSet (quotes, a floating note's fixings, each curve it reads) and its
derived quality. Only changed pins are valued and published; the others keep
their records exactly as valued. The graph gains rebind_built, a
snapshot-bound partial activation that resets only the listed components.
Replay and Captured still value every pin at each event.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2c480505-1db1-4e07-801b-3e29839b4acd
Commits

Reviewing files that changed from the base of the PR and between 07c93d2 and 48349c3.

Files selected for processing: 1
  • crates/convex-server/tests/live_ingress.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

Live ticks now value instruments whose inputs or derived quality changed and retain unchanged result records. The runtime selectively rebinds graph components and updates selected results. Startup and refresh still value all instruments. Replay and Captured continue to value all instruments.

Changes: Selective Live Valuation

Layer / File(s) Summary
Snapshot input comparison
crates/convex-ports/src/snapshot.rs, crates/convex-engine/src/snapshot.rs, crates/convex-engine/src/runtime/worker.rs, crates/convex-ports/tests/snapshot_contracts.rs
Snapshot methods compare valuation frames and instrument inputs. Worker baseline selection checks input and quality changes and validates baseline result counts and subjects.
Selective graph rebinding
crates/convex-engine/src/calc_graph.rs, crates/convex-engine/src/calc_graph/dag.rs, crates/convex-engine/tests/graph_snapshots.rs
Graph rebinding resets the connected components of selected nodes for a newer snapshot. Tests cover retained values, invalid rebinds, and stale tickets.
Live tick selection and result updates
crates/convex-engine/src/runtime/actor.rs, crates/convex-engine/src/runtime/worker.rs, crates/convex-engine/tests/runtime.rs, crates/convex-engine/RUNTIME.md, crates/convex-engine/benches/runtime_benchmarks.rs, crates/convex-server/tests/live_ingress.rs
The actor selects changed instruments, clears the baseline after delivery loss or refresh, and merges selected records. Runtime tests and documentation describe retained results. The benchmark adds a single-quote case for 256 instruments, and the ingress test exercises concurrent batches and capacity refusals.

Priority: Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant RuntimeActor
  participant RuntimeWorker
  participant MarketSnapshot
  participant CalculationGraph
  participant SnapshotValuator
  RuntimeActor->>RuntimeWorker: Compare baseline inputs and qualities
  RuntimeWorker->>MarketSnapshot: Compare frame and instrument inputs
  MarketSnapshot-->>RuntimeWorker: Return comparison results
  RuntimeWorker-->>RuntimeActor: Return selected pins and baseline
  RuntimeActor->>RuntimeWorker: Rebind and calculate selected pins
  RuntimeWorker->>CalculationGraph: Rebind selected graph nodes
  RuntimeWorker->>SnapshotValuator: Value selected instruments
  RuntimeWorker-->>RuntimeActor: Return selected valuation records
  RuntimeActor->>RuntimeActor: Merge records and save baseline
Loading

Possibly related PRs

  • sujitn/convex#161: PR #161 pins Live configuration and reference inputs; this change compares those inputs in its baseline.
  • sujitn/convex#162: PR #162 manages runtime calculation by plan index; this change selects plan indices for calculation and publication.
  • sujitn/convex#163: PR #163 defines per-instrument valuation inputs and quality; this change compares them to select Live work.
Merge Risk: Low · up to 48349

The overload test may fail on a healthy runtime, but this does not establish a production defect. The risk is bounded to test reliability.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 68.42% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: Live valuation now processes only bonds whose inputs changed. The referenced design items are relevant to the changeset.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 68.42% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 19 functions across 11 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

❤️ Share

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

@sujitn

sujitn commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

Medians

ci profile, WSL2, same machine as #163. Main is 50c3ec3's sources with this branch's bench file, so main also runs the new one-quote case. Main and the branch ran back to back today.

Case main 50c3ec3 07c93d2 (2 runs)
live_tick (1 bond) 303 µs 298–301 µs
1 bond, bursts of 64 628 µs 608–621 µs
16 bonds, bursts of 64 2.70 ms 2.52–2.54 ms
256 bonds, bursts of 64 19.7 ms 9.5–9.8 ms
256 bonds, bursts of 512 33.2 ms 31.8–33.1 ms
256 bonds, one quote per tick (burst1/256) 19.8 ms 5.5–6.1 ms
snapshot/assemble/256 4.63 ms 4.65–4.69 ms
  • One quote per tick at 256 bonds is 3.2–3.6× faster, below the 7–8 ms the design estimated. Most of what remains is per-tick work that covers every bond: snapshot assembly alone is 4.6–4.7 ms.
  • Bursts of 64 at 256 bonds take half the time. A tick now values only the bonds its quotes touch, so throughput goes from about 3,250 to 6,500–6,700 quotes/s.
  • Bursts of 512 touch every bond, so they gain nothing, as the design said.
  • The 1-bond and 16-bond cases are within this machine's noise.
  • The first main run stopped in live_tick: a one-bond tick never published within its 5 s wait. That is the WSL wall-clock step described in Server P10b-iv: each bond is judged only on its own inputs (D58); readiness needs one accepted result (D58a) #163, and that run's numbers are not used. The main column comes from the second run.

D55 values only the bonds a tick's quotes touch, so at 256 bonds one
client posting batch after batch keeps pace with the drain and the queue
never fills; Windows CI saw no 429 in 64 serial batches. Each round now
posts 16 batches of 256 at once, four times the 1,024-quote ingress
queue, and every refusal must be CAPACITY_EXCEEDED and retryable.
@sujitn
sujitn merged commit 5f06aa2 into main Oct 28, 2026
16 checks passed
@sujitn
sujitn deleted the server-p10b-changed branch November 2, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant