You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #28 patched a symptom — a stray ASSOCIATION_RECEIVED that dcmrecv's stdout can deliver after a worker has finalized its association, which (pre-fix) re-registered a phantom association on the consumer side. The guard there is safe because ASSOCIATION_RECEIVED carries no payload.
This issue tracks the underlying root cause, which is still open and is the more important bug: why do dcmrecv lifecycle events (ASSOCIATION_RECEIVED, and potentially STORED_FILE/FILE_RECEIVED) ever arrive after the pool has begun finalizing an association?
Why it matters
ASSOCIATION_FINALIZED is contractually the terminal event for an association — every FILE_RECEIVED must precede it. That guarantee is the whole reason FINALIZED exists (we can't rely on Association Release being last, since file move/parse work may still be in flight; see #25).
If a FILE_RECEIVED can arrive after finalization, then FINALIZED fired too early — and that is a potential lost image:
FILE_RECEIVED is deliberately not suppressed (correctly — it represents an object already written to disk; dropping it would lose data).
But an un-suppressed late FILE_RECEIVED arriving after FINALIZED means the consumer already closed that association's books, so the file is mis-/under-counted or stranded.
So the same timing flaw that produces the (harmless) stray ASSOCIATION_RECEIVED could, in principle, produce a (harmful) late FILE_RECEIVED. We need to understand and close the timing flaw, not just filter its payload-free symptom.
Hypotheses to investigate
stdout/stderr cross-stream reordering.DcmtkProcess.handleData maintains separate buffers for stdout and stderr and emits line events from each; the parser is fed from both. Node provides no ordering guarantee between the two pipes. If dcmrecv writes some lifecycle lines to stdout and others to stderr, the parser can observe them out of dcmrecv's actual write order (e.g. Association Release parsed before a trailing Stored received object…). First thing to confirm: which stream(s) does dcmrecv emit Stored received object… vs Association Release/Association Received on?
Worker reuse / shared child stream. The long-lived dcmrecv child's continuous stdout straddles association boundaries; the pool's per-association state machine is decoupled from it.
(Note: Node's EventEmitter is synchronous and ordered, so this is not intra-emitter reordering — it's cross-pipeline/cross-stream timing.)
Proposed investigation
Instrument every late event (one that arrives while worker.finalized is set, in both wireAssociationReceived and wireFileReceived) and record:
the file path (for FILE_RECEIVED) and whether it's already in worker.files (duplicate vs new path),
the source stream (stdout vs stderr) of the originating line,
timing relative to ASSOCIATION_COMPLETE / ASSOCIATION_FINALIZED.
Run under the extreme-concurrency soak (50-parallel single-file, the harness that surfaced the ~1/1500).
Key question: is any late FILE_RECEIVED ever a new path? If yes → confirmed lost-image risk → fix finalize timing/ordering so FINALIZED is genuinely terminal.
Candidate fixes (depending on findings)
If cross-stream reordering: merge stdout+stderr into a single ordered line stream before parsing, or anchor association boundaries to a single stream.
If deferral insufficiency: gate FINALIZED on a stronger drain condition rather than a fixed one-tick setImmediate.
Acceptance criteria
Root mechanism identified and documented.
Demonstrated that no FILE_RECEIVED can be observed after ASSOCIATION_FINALIZED for an association (the terminal-event contract holds), or that any such event is provably a harmless duplicate.
Summary
PR #28 patched a symptom — a stray
ASSOCIATION_RECEIVEDthat dcmrecv's stdout can deliver after a worker has finalized its association, which (pre-fix) re-registered a phantom association on the consumer side. The guard there is safe becauseASSOCIATION_RECEIVEDcarries no payload.This issue tracks the underlying root cause, which is still open and is the more important bug: why do dcmrecv lifecycle events (
ASSOCIATION_RECEIVED, and potentiallySTORED_FILE/FILE_RECEIVED) ever arrive after the pool has begun finalizing an association?Why it matters
ASSOCIATION_FINALIZEDis contractually the terminal event for an association — everyFILE_RECEIVEDmust precede it. That guarantee is the whole reasonFINALIZEDexists (we can't rely onAssociation Releasebeing last, since file move/parse work may still be in flight; see #25).If a
FILE_RECEIVEDcan arrive after finalization, thenFINALIZEDfired too early — and that is a potential lost image:FILE_RECEIVEDis deliberately not suppressed (correctly — it represents an object already written to disk; dropping it would lose data).FILE_RECEIVEDarriving afterFINALIZEDmeans the consumer already closed that association's books, so the file is mis-/under-counted or stranded.So the same timing flaw that produces the (harmless) stray
ASSOCIATION_RECEIVEDcould, in principle, produce a (harmful) lateFILE_RECEIVED. We need to understand and close the timing flaw, not just filter its payload-free symptom.Hypotheses to investigate
DcmtkProcess.handleDatamaintains separate buffers for stdout and stderr and emitslineevents from each; the parser is fed from both. Node provides no ordering guarantee between the two pipes. If dcmrecv writes some lifecycle lines to stdout and others to stderr, the parser can observe them out of dcmrecv's actual write order (e.g.Association Releaseparsed before a trailingStored received object…). First thing to confirm: which stream(s) does dcmrecv emitStored received object…vsAssociation Release/Association Receivedon?wireAssociationCompletedefers the drain by a singlesetImmediate(DicomReceiver: FILE_RECEIVED fires after worker reset to idle — ASSOCIATION_FINALIZED ordering broken #25). Under heavy load a still-buffered stdout chunk may be parsed afterfinalizeAssociationhas run (_finalized = true), i.e. after the one-tick window.(Note: Node's
EventEmitteris synchronous and ordered, so this is not intra-emitter reordering — it's cross-pipeline/cross-stream timing.)Proposed investigation
Instrument every late event (one that arrives while
worker.finalizedis set, in bothwireAssociationReceivedandwireFileReceived) and record:FILE_RECEIVED) and whether it's already inworker.files(duplicate vs new path),ASSOCIATION_COMPLETE/ASSOCIATION_FINALIZED.Run under the extreme-concurrency soak (50-parallel single-file, the harness that surfaced the ~1/1500).
Key question: is any late
FILE_RECEIVEDever a new path? If yes → confirmed lost-image risk → fix finalize timing/ordering soFINALIZEDis genuinely terminal.Candidate fixes (depending on findings)
FINALIZEDon a stronger drain condition rather than a fixed one-ticksetImmediate.Acceptance criteria
FILE_RECEIVEDcan be observed afterASSOCIATION_FINALIZEDfor an association (the terminal-event contract holds), or that any such event is provably a harmless duplicate.References
ASSOCIATION_RECEIVED)setImmediatefinalize deferral +FINALIZEDterminal event)src/servers/DicomReceiver.ts(finalizeAssociation,wireAssociationComplete,wireFileReceived,wireAssociationReceived),src/DcmtkProcess.ts(handleData/processLines— separate stdout/stderr buffers)