Turns the design into ordered tasks.
Base: main. Independent of jo/publisher-feed-routes.
Depends on Adapter::poll_upstream, which is its own design and plan and is on main — rust/adapter/dz-adapter-core/src/adapter.rs, merged as #101. A session transport is the one that cannot re-subscribe for reasons of its own, so without that method an instrument admitted mid-session waits for a reconnect — which is the failure that measured it. Task 5 is where the two meet, and it is the last task for that reason.
Corrected at execution. This line said the method "is not in this repository yet", and put task 5 last because of it. It is a status fact this dated document got wrong rather than an argument to re-open: the method landed before execution began, so task 5 was never blocked and the plan was executable end to end. The ordering it produced is kept — task 5 is still the last task, because the boundary's half is still the half that needs an adapter.
Tasks 1 to 3 are the protocol with no session and no socket: framing, timestamps, and the state machine over a byte stream behind a trait. Task 4 is the socket and TLS. Task 5 is the boundary's half — the logon the adapter writes and the mid-session write. Nothing before task 4 needs a network, and nothing before task 5 needs an adapter.
- Vocabulary:
GLOSSARY.mdgoverns every identifier, comment, test name, config key and metric label. A message on this transport is amessageand never adatagram; the session's own numbering is asequenceand the publisher's era is anera;sourcenever appears bare —upstream sourcewhere the venue is meant,source_idwhere the wire field is. - Every test must be shown to kill its mutant. The framing tasks have obvious mutants and the plan names them; the session tasks' mutants are the interesting ones, because a state machine that skips a transition usually still connects.
- No order entry. A task that composes a message the protocol defines for order entry has misread the design.
- Encode: field order the protocol mandates,
BodyLengthcomputed over the right span, the checksum over the right bytes, and the timestamp format at the precision the venue's own session expects. - Decode: a message split across two reads, two messages in one read, a declared length that does not match, and a checksum that does not hold.
- A message whose length or checksum does not hold is refused, not skipped, and the refusal ends the session. A corrupt message on a session-numbered connection means the numbering can no longer be trusted, and carrying on reads the next message against a sequence that has moved for a reason nobody recorded.
Test: a golden vector per direction, byte for byte, plus the four decode cases above. The split-read and two-in-one-read cases are the ones a hand-written reader gets wrong.
The revert: compute BodyLength over the whole message rather than the mandated span. The golden encode test fails. And: skip a message whose checksum does not hold instead of ending the session — a_corrupt_message_ends_the_session fails, and what it would have cost is a session that keeps reading against numbering it can no longer trust.
- A trait for the byte stream, owned by this crate, so that every case below is a test with no socket and no privileges — the move
RouteLookupmakes for the routing table andClockmakes for time. - States and transitions: sending the logon, awaiting its answer, established, logging out, closed. Nothing but session messages may be sent before established.
- The heartbeat cadence is read from the logon the adapter wrote, not from a configuration key. A key could disagree with what was logged on with, and the venue believes the logon.
- A test request when the venue has been silent for longer than the cadence, and an answer to the venue's own.
- The outbound sequence numbers every message, including the session's own.
Test: a logon answered, a logon rejected, a heartbeat due, a test request answered, the venue's logout, and a session that goes silent — each an assertion about what was written and what state followed.
The revert (the plan's centre for this task): allow a subscription to be sent before established. nothing_but_a_logon_is_sent_before_the_session_is_established fails. A state machine that skips this transition still connects and still receives, which is exactly why the test has to assert the order of what was written rather than that the session came up.
- The outbound sequence starts at 1 on every logon, with the flag that says so, and nothing is persisted.
- A configuration asking for continuity is refused at load, naming the key: a transport that silently resets against a venue expecting continuity produces a session the venue tears down for a reason our logs will not carry.
- The reason the reset is right, in the crate's own documentation rather than only in the design: a resend delivers deltas whose value has expired, and the publisher's snapshot recovery is the better repair.
Test: two successive logons both number from 1; a document asking for continuity is refused.
The revert: carry the sequence across a reconnect. a_second_logon_numbers_from_one fails.
-
Inputimplemented over the state machine:connectopens the socket and performs TLS;sendwrites what the adapter queued, framed and numbered, and the firstsendis what carries the session to established;recvreturns a decoded application message as a payload and a session message asReceived::Liveness;shutdownattempts an orderly logout and gives up quickly. - Every failure classified: a refused connection, a failed negotiation, a rejected logon, a session-level reject, a silence. The disconnect reason is a metric label with four values and this is the only layer that can see which applies.
- TLS pinned as the websocket transport pins it,
default-features = false, with the manifest saying which backends a default must not be able to pull in. - A session message is
Livenessand never a payload, for the reason that case exists: the idle guard counts time since the last payload, so a session that heartbeats forever and delivers nothing must still trip it.
Corrected at execution. The first line said
connect"drives the logon to established". It does not, and task 5 below is the reason: the driver connects, then asks the adapter what to send, so there is no logon to drive at the momentconnectruns and the firstsendis what establishes the session. That is a status fact this dated document got wrong about the code it went on to produce — and one this same plan contradicts three tasks later — rather than an argument made on the day and since lost, which is why it is corrected here and not left to stand with a note. What was argued in this task is unchanged and kept:connectowns the socket and the negotiation, the classification is this layer's because it is the only layer that can see which failure applies, and a session message is never a payload.
Test: the classification, over the scripted byte stream; and one example against a loopback endpoint for TLS and the real socket, which is the half no fake proves.
The revert: report a heartbeat as a payload. the_idle_guard_fires_on_a_session_that_only_heartbeats fails — the failure the whole Liveness case exists for.
- The transport reads the heartbeat interval out of the logon body the adapter wrote through
on_connected, frames it, numbers it, and sends nothing else before it. - A connect where the adapter wrote no logon is a refusal naming the adapter's method, not a session that waits: a transport that logged on with a body it composed itself would be signing for the venue.
- What the adapter writes through
poll_upstreamis framed and numbered on the established session, which is what makes an instrument admitted mid-session reach a subscription without a reconnect. - Sessions are per
[[source]], and two enabled sources whosecredentialstables are equal are refused at load naming both blocks. What that does not catch — two paths holding one account — is documented with its symptom, which is both sources reconnecting in step.
Test: a scripted adapter's logon body reaches the wire framed and numbered; an adapter that writes nothing at connect is a refusal; a mid-session write is framed and numbered on the same session; a document with two enabled sources sharing a credential table is refused; and — asserted rather than assumed — a document with 62 channel instances over one source opens one session.
The revert: let the transport compose a logon when the adapter wrote none. a_connect_with_no_logon_from_the_adapter_is_refused fails. That is the one where the failure is not a crash: it is this repository signing a logon on a venue's behalf.
-
Kind::Fix's doc comment stops calling the protocol an order-entry protocol without qualification. It carries market data too, and the current wording invites a market-data publisher to think the token is not for it. -
BRINGING-UP-A-FEED.mdgains the transport, and one line on the logon being the adapter's: a venue implementing this writes its logon where it writes its subscriptions. -
docs/README.mdcarries the row for this pair.
Test: scripts/check-public-repo-rules.sh, plus a read of the new prose against the glossary's banned-word table.
The plan is done when:
- a venue can read its feed over a session with no session code of its own — no framing, no sequence, no heartbeat, no logout;
- its logon signature is composed in its own repository and this repository composes none;
- an instrument admitted mid-session reaches a subscription without a reconnect;
- a session that heartbeats forever and delivers nothing trips the idle guard;
- a document with two enabled sources sharing a credential table is refused naming both, and one with 62 channel instances over one source opens one session;
and when reverting task 2's ordering or task 5's logon makes a named test fail — because a state machine that connects and a transport that signs are both things that look like they work.
Every test was shown to kill its mutant. Each revert below was applied to a committed tree, the suite was run, and the file was restored from a copy taken first.
| Reverted | What was put back | Tests that failed, and with what values |
|---|---|---|
| Task 1 | BodyLength computed over more than the mandated span |
the_declared_length_measures_the_mandated_span_and_not_the_message — declared 55 against a span of 35; and a_framed_logon_is_the_bytes_written_out_by_hand — 9=99|…|10=070 against the hand-computed 9=79|…|10=068. Four decode tests fell with them, because a length that lies is a message the decoder cannot locate the end of |
| Task 1 | a message whose checksum does not hold is skipped rather than refused | a_checksum_that_does_not_hold_is_refused — expect_err was handed Ok(false), which is the decoder silently dropping the message and reading on |
| Task 2 | anything may be sent before the session is established | nothing_but_a_logon_is_sent_before_the_session_is_established — the subscription was accepted and written, so expect_err was handed Ok(()). The session still came up afterwards, which is exactly why the test asserts the order of what was written |
| Task 2 | the cadence is a fixed 30s rather than the value the logon stated | the_cadence_is_the_one_the_logon_stated_and_no_other — with a logon of 108=10, the writes were ["A"] where ["A", "0"] was due; and silence_is_questioned_once_and_then_ends_the_session — Silent { interval: 30s } against the agreed 10s |
| Task 3 | the outbound sequence carries across a reconnect | a_second_logon_numbers_from_one — the second logon went out on 4, not 1 |
| Task 3 | persist_sequence = true accepted, and the sequence reset anyway |
a_document_asking_for_sequence_continuity_is_refused_naming_the_key — resolved to Endpoint { address: "203.0.113.10:9443", … } instead of refusing |
| Task 4 | a heartbeat reported as a payload | the_idle_guard_fires_on_a_session_that_only_heartbeats — the bound elapsed with the driver still running (Elapsed(())). The failure is not a late guard or a wrong reason: the guard never fires, and the publisher runs against a dead subscription for the life of the process |
| Task 5 | the transport composes a logon when the adapter wrote none | a_connect_with_no_logon_from_the_adapter_is_refused — the bound elapsed with the driver still running (Elapsed(())), waiting on a venue's answer to a logon this repository had signed on its behalf |
| Task 5 | two enabled sources with equal credentials tables accepted |
two_enabled_sources_with_the_same_credential_table_are_refused_naming_both — resolved with sources: ["primary-session", "second-session"], which is two logons with one credential |
| Task 5 | a venue may open more sessions than the document declares | sixty_two_channel_instances_over_one_source_open_one_session — the per-channel-instance venue was accepted, so expect_err was handed Ok(()); a_venue_that_builds_a_source_nobody_declared_is_refused fell with it |
Two of these are worth reading for their failure shape rather than their name. Task 4's and task 5's reverts do not produce a wrong value: they produce a publisher that keeps running. A session that heartbeats forever looks healthy in every series a dashboard carries, and a transport that signs its own logon connects. Both tests therefore bound the driver's run and fail on the bound, because the driver came back at all is the property under test.
One thing this plan asked for was not done. Task 4 asks for TLS to be
exercised by an example against a loopback endpoint. The real socket is —
tests/loopback.rs runs the session, the framing and the driver against a
listener on 127.0.0.1 — but TLS is not, and it is not faked. Verifying the
compiled-in trust anchors against a certificate chain needs a real endpoint, and
a self-signed root of our own would exercise a configuration this crate does not
build: it would assert that a test harness works. That is the standard
dz-ingress-websocket set for this family, in its own loopback suite, and this
follows it. What is checkable without a network is checked — that the client
configuration is constructible with the provider named rather than discovered,
which is where the rustls provider-selection panic would land.
Corrected at execution. The paragraph above says TLS "is not [exercised], and it is not faked", and that half of it no longer holds:
a_certificate_no_compiled_in_anchor_signed_is_refused, inrust/ingress/dz-ingress-fix/tests/loopback.rs, puts anrcgenself-signed listener on127.0.0.1, negotiates against it with the realrustlsclient this crate builds, and asserts the refusal isConnectFailureReason::Tlsrather than a refusal or a timeout — because those are three different operator actions. It fakes nothing and needs no root of our own: what it exercises is whether verification is on at all, which is the half a one-line change can switch off and every other test in this workspace would pass without.the_chain_is_verified_against_the_anchors_compiled_into_this_binaryholds the part a refusal alone cannot see, because an empty root store also refuses every certificate: the compiled-in anchors are asserted to be in the store, and to be all of them. So task 4's TLS exercise against a loopback endpoint was done, and this section's opening claim that it was not is a status fact this dated document got wrong about the code it went on to produce rather than an argument made on the day. The claim is left standing above as the record of what was believed, and what the paragraph argues is unchanged and still the standard: a negotiation this crate should accept is not exercised, because verifying the compiled-in anchors against a chain that leads to one of them needs a real endpoint, and a self-signed root of our own would assert that a test harness works.
No order entry, no sequence persistence, no resend. It decides nothing about which tags a venue's market-data messages carry: that is the adapter's, and it is where a protocol version difference belongs.