Repository navigation
Mirror the relay's packets to a second destination for Python - #247
Conversation
jmacd867
left a comment
There was a problem hiding this comment.
Thanks for this, it's a clean piece of work. The tests are thorough, and checking that the packets_for_fields extraction is byte-identical to the old code was a good call. I pulled the branch, cargo test passes (21 tests), and I traced the changes through. Comments below, most important first.
Should fix before merge
1. "Never slows iMotions" isn't true as written. ADR 0003 says "The mirror can never slow the recording" and README.md:128 says the mirror "never affects the iMotions stream". But the TCP thread writes each packet to iMotions, then to the mirror, before moving on to the next one (relay/src/state.rs:725-729). So a stalled Python reader holds up the next iMotions write by up to 100ms, and a connect holds the thread for up to 500ms. That's around 10 samples arriving late. If iMotions timestamps packets on arrival (our timestamp field is empty), those samples are out of step with the biometrics. The ADR also contradicts itself a few sentences later, where it says a connect blocks for 500ms.
Two ways to fix it:
- Reword the ADR and README to give the actual limits instead of "never". This is the minimum.
- If we really want "never", give the mirror its own writer thread with a bounded queue, and drop mirror packets when the queue is full.
2. A slow reader may never get dropped. MIRROR_WRITE_TIMEOUT applies to each individual write call, and write_all keeps retrying after a partial write. A Python process that reads slowly, rather than stopping, could keep every call under 100ms. In that case the mirror is never dropped and iMotions runs at Python's pace. a_python_process_that_stops_reading_is_dropped_not_waited_on only covers a reader that never reads, and its < 20s bound (relay/src/update.rs:696) is loose enough to hide a slowdown. Could you add a test with a reader that drains slowly? A time limit on the whole mirror_send would close this.
Worth addressing
3. Document what the Python side has to do. If the clustering process stops reading during a slow refit, the mirror is dropped and doesn't come back on its own. So whatever replaces data_logger.py has to read the socket on its own thread. A line in the ADR's Consequences section or in inference/README.md would save the next person from finding this out mid-flight.
Smaller things
- "Sample" has two meanings. GLOSSARY.md defines a Sample as one complete set of telemetry values, but
PAIRED_SAMPLES(state.rs:198) andeight_fields_become_one_packet_per_sample_in_order(state.rs:893) use it for a single iMotions entry. Meanwhilestate.rs:163andsend_sampleuse the glossary meaning. A different word for the iMotions one would keep them apart. - One name for the second destination. It's currently "mirror", "Python stream" (UI and event log), "Python mirror" (thread logs), "Python side" and "sink". I'd pick one, probably "mirror", and add it to GLOSSARY.md.
mirror_rowbuilds itstext_inputinline (relay/src/view.rs:228). CONTRIBUTING.md asks for a separate helper for each element, liketcp_addr_input.connect_mirrorre-parses the address thatvalidate_tcp_addrhas already checked, and copies its error message. One of the two could go.- Optional: the event-name list now exists in three places (the 4-field branch, the 8-field branch and
PAIRED_SAMPLES). Also, about 8 tests inupdate.rsrepeat the same connect-plus-mirror setup and could share a fixture.
Checked and looks good
- Packets queue rather than drop during a mirror connect.
- Disconnecting iMotions also clears the mirror, and stale
MirrorConnectedmessages are discarded. packets_for_fieldsmatches the old inline code.- "Connect TCP" is required before "Connect Python", and "Send Packet" reaches both destinations.
Like you said, none of this has been run against live X-Plane or iMotions.
|
All of it addressed. Thanks, both of the first two were right and the second one was the important one. 1. "Never slows iMotions". You were right that it was false and that the ADR contradicted itself two paragraphs later. I took the second option rather than rewording, because of your point 2: the first real consumer of this mirror is a clustering process doing periodic refits, which is exactly a slow reader, so the pacing case is the expected usage rather than an edge case. The mirror's socket now belongs to its own writer thread, and the TCP thread reaches it through a bounded queue (256 packets, about 1.5 seconds) with a non-blocking try-send. Connecting happens on that thread too. What is guaranteed now is narrow and true: the TCP thread never waits on the mirror. What is not guaranteed is delivery of every copy, and the ADR says so instead of claiming "never". New test 2. Slow reader never dropped. Fixed by the same change, and Also replaced the loose 3. What the Python side must do. In the ADR's Consequences and in Smaller things. All taken. Left alone: the event-name list in three places and the test fixture duplication. Both are fair, but I would rather not grow this diff further. Happy to do either in a follow-up. Re-verified end to end against a running |
jmacd867
left a comment
There was a problem hiding this comment.
#248 has merged, so this needs a rebase on main. It already conflicts in GLOSSARY.md and inference/README.md after #244. I test-merged it with #248, and three things will bite that git won't flag:
- A test fails.
a_malformed_mirror_address_is_refused_before_the_thread_sees_itasserts the error contains"Invalid IP address", but #248 changed the message to "Invalid address. Enter a literal IP and port, for example :5050". With both merged: 22 pass, 1 fails. - The mirror box would show the iMotions hint.
mirror_connectreusesvalidate_tcp_addr, so a bad Python address would get "for example :5050". Could each box supply its own example, for instance by passing the hint intovalidate_tcp_addr? That fixes the test too. - The conflicts in
state.rsandview.rsare just constants added in the same spot. Keep both blocks instate.rs. Inview.rs, keep onlyDEFAULT_MIRROR_PLACEHOLDER, because #248 removedDEFAULT_TCP_PLACEHOLDERand the input now usesIMOTIONS_ADDR_HINTfromstate.rs.
Also from the rebase:
- The README still says
inference/Data/data_logger.py. Onmainit's nowinference/pilot_inference/capture/data_logger.py. - There's a second ADR 0003 in progress,
0003-live-clustering-runs-in-a-python-sidecar.md. Whichever lands second needs renumbering. - The PR body says
Connect Pythonand the README saysConnect mirror. Which is the button called?
5874edf to
a5f2a7d
Compare
…destination Lets iMotions and the Python logger receive the same flight at once.
The TCP thread now only pushes into a bounded queue, so connecting and writing to the mirror can no longer delay iMotions. Copies are dropped when the queue is full, reported once per episode.
a5f2a7d to
c394be7
Compare
|
Thanks for this. The writer-thread rewrite is the right fix: the TCP thread now only does a non-blocking A few things left, most important first. Should fix before merge1. A bad mirror address still gets the iMotions example. 2. "Sink" in the tests. The new GLOSSARY entry lists "sink" under Avoid, and 3. The queue comment promises more than the code does. Worth addressing
Smaller, take or leave
The event-name list and the test fixtures are fine as a follow-up, like you said. Not checked against live X-Plane or iMotions. |
validate_tcp_addr now takes the hint for the box being validated, so a bad mirror address no longer suggests an iMotions one.
|
Good catch on all of it. Your review landed about six minutes before I pushed the rebase, so most of it was already done, and one thing was not. Already in, from the rebase:
Fixed now, in 201c300:
On the ADR number: nothing else in any open PR adds one, so yours is unpushed. Mine is in review, so I will keep 0003 and yours can be 0004. Happy to flip if you would rather the sidecar decision took the lower number, it is a file rename either way. 23 tests, green over four consecutive runs. |
jmacd867
left a comment
There was a problem hiding this comment.
Approving. The writer thread and the per-box hints fix the things that mattered, and CI passes. The remaining points from my 22:59 review are wording and comments, fine as a follow-up. The one I'd do first is the MIRROR_QUEUE_CAPACITY comment, since whoever writes the clustering side will read it.
Part of #245. Does not close it: this is the relay side only, the live clustering itself is still open.
Why
The relay held one TCP connection, so a flight could feed iMotions or the Python logger, never both. #245 names this as the blocker for live performance groups, and it also forced a choice on every real flight: record the biometrics study properly, or collect ML data.
What it does
The TCP thread now keeps an optional second connection, the mirror, and writes every packet to both from one shared list, so the two sinks cannot drift apart. In the GUI: a second address box with
Connect mirror/Disconnect mirror, placeholder127.0.0.1:5001to matchdata_logger.py's default port.The mirror's socket is owned by its own writer thread, reached through a bounded queue with a non-blocking push, so the TCP thread never waits on it. Connecting, writing and failing all happen off that thread. What is not promised is every copy: a full queue drops packets, reported once when the mirror falls behind and once when it catches up. That covers #245's "a slow refit never blocks packet forwarding to iMotions".
docs/adr/0003records why the mirror hangs off the primary connection rather than the relay holding two equal ones, and what would justify revisiting that.Refactor
Packet building moved out of the thread into
packets_for_fields. Both destinations are written from its output, and the 4, 8 and odd-length mappings became testable without a socket. I verified the extraction is byte-identical to the old inline code for every field count from 0 to 19, against a transcription of the previous implementation.Testing
21 tests, 13 new, suite runs in about 2 seconds.
the_python_mirror_receives_the_same_packets_as_imotionsasserts both sinks get byte-identical outputimotions_keeps_streaming_when_the_mirror_cannot_connectpoints the mirror at a dead portimotions_survives_the_python_side_dying_mid_streamkills Python mid-flighta_python_process_that_stops_reading_is_dropped_not_waited_onaccepts the connection then never reads, and checks the mirror is dropped on the write timeout while iMotions keeps receivinga_long_run_reaches_both_sinks_in_orderpushes 800 packets to each and checks for driftEnd to end against the real logger: I drove this code into a running
data_logger.pyand got five correct CSV rows withpilot_idandflight_idpopulated, while the iMotions stand-in received the same bytes.Mutation checked the three load-bearing tests by breaking the code deliberately. Each failure was caught by its intended test and nothing else.
Not verifiable here: anything needing live X-Plane or iMotions. The buttons compile and are wired, but nobody has clicked them against real hardware.
Known limitations, all deliberate
Connect TCPfirst, because it lives inside that connection. With no iMotions, pointConnect TCPstraight at the Python listener as before.Connect mirroragain. Note that a reconnect starts a newflight_idunless--flight-idwas passed, same trap as Record pilot_id and flight_id in the logged CSV #219.Send Packetreaches both sinks, so it checks both links at once and writes a row of zeros to the logger as well.