Skip to content

Serialize simulation CSV writes behind a recorder (#10)#76

Open
ff225 wants to merge 1 commit into
fix/sciot-011-device-state-managerfrom
fix/sciot-010-inference-cycle-recorder
Open

Serialize simulation CSV writes behind a recorder (#10)#76
ff225 wants to merge 1 commit into
fix/sciot-011-device-state-managerfrom
fix/sciot-010-inference-cycle-recorder

Conversation

@ff225

@ff225 ff225 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #10. Second step of #45; unblocked now that #9 has landed.

Stacked on #74 — base is fix/sciot-011-device-state-manager, not main. Independent of #75, so the two can be reviewed and merged in any order once #74 is in.

Why

The simulation CSV handle, its writer and its row counter were class-level attributes on RequestHandler, opened and closed from ASGI request threads while the background I/O thread wrote rows through them. Three concrete races:

Race Where
close_simulation_csv cleared csv_file while the I/O thread was inside _write_csv_row — a queued row could reach a closed file and raise I/O operation on closed file request_handler.py:450-454, 488-494
_write_csv_row tested csv_writer, then used csv_file; nothing kept the two consistent across the gap request_handler.py:452-454
inference_counter += 1 ran on concurrent request threads, and set_simulation_csv reset it underneath them, so ids could collide request_handler.py:482, 791

What changed

New src/server/communication/inference_recorder.py. InferenceCycleRecorder owns the CSV handle, the writer and the counter behind one lock: the check and the write happen together, a row arriving after a close is dropped and reported rather than written, and row ids are handed out atomically.

Failures are contained. A row that fails to serialize is logged and swallowed instead of propagating into the background I/O worker. A failed open no longer leaves a half-open handle behind.

Debug-JSON writing moves here too, since it shares the same background thread and the same per-server-start lifecycle.

No external change. RequestHandler.set_simulation_csv / close_simulation_csv remain as delegating classmethods, so http_server.py:272,289 and scripts/simulation/simulation_runner.py are untouched.

Testing

254 passed, 1 skipped (fast suite + HTTP end-to-end). inference_recorder.py and device_state.py both at 100% coverage. New tests/unit/test_inference_recorder.py includes two concurrency tests that reproduce the races above: 8 writer threads against a thread that repeatedly closes and reopens, and 8 threads racing for row ids. ruff clean.

Note for the reviewer

The CSV schema declares min_device_time, max_device_time, min_edge_time and max_edge_time, but the request handler has never supplied them — DictWriter fills them with the empty string, so those four columns are always blank in every simulation CSV produced so far. This PR preserves that behaviour rather than quietly changing the data (the values are all available at the call site if you want them filled — say the word and it is a three-line follow-up).

The simulation CSV handle, its writer and its row counter were class-level
attributes on RequestHandler, opened and closed from ASGI request threads while
the background I/O thread wrote rows through them. Three races followed:

- `close_simulation_csv` cleared `csv_file` while the I/O thread was inside
  `_write_csv_row`, so a queued row could reach a closed file and raise
  "I/O operation on closed file";
- `_write_csv_row` tested `csv_writer` and then used `csv_file`, with nothing
  keeping the two consistent across the gap;
- `inference_counter += 1` ran on concurrent request threads, and
  `set_simulation_csv` reset it underneath them, so ids could collide.

InferenceCycleRecorder owns all of it behind one lock: the check and the write
happen together, a row that arrives after a close is dropped and reported
rather than written, and row ids are handed out atomically. A row that fails to
serialize is now logged and swallowed instead of propagating into the
background I/O worker.

The debug-JSON writing moves here too, since it shares the same background
thread and per-server-start lifecycle.

`RequestHandler.set_simulation_csv` / `close_simulation_csv` stay as delegating
classmethods; `http_server` is unchanged.

Second step of #45.
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