Skip to content

feat(stgcmt): Implement DICOMweb storage commitment - #63

Merged
nickamzol merged 2 commits into
UMEssen:mainfrom
mbits-imaging:stgcmt
Oct 30, 2026
Merged

nickamzol merged 2 commits into
UMEssen:mainfrom
mbits-imaging:stgcmt

Conversation

@feliwir

@feliwir feliwir commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Comment thread src/backend/dimse/stgcmt/scu.rs Fixed
Comment thread src/backend/dimse/stgcmt/scu.rs Fixed
Comment thread src/backend/dimse/stgcmt/scu.rs Fixed
Comment thread src/backend/dimse/stgcmt/scu.rs Fixed
Comment thread src/backend/dimse/stgcmt/scu.rs Fixed
Comment thread src/backend/dimse/stgcmt/scu.rs Fixed
@feliwir
feliwir marked this pull request as ready for review August 24, 2026 07:03
Comment thread src/api/stgcmt/service.rs Fixed
Comment thread src/api/stgcmt/service.rs Fixed
Comment thread src/api/stgcmt/service.rs Fixed
Comment thread src/api/stgcmt/service.rs Fixed
@feliwir

feliwir commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@nickamzol Can you give this a review please? We need this feature in production soon

Comment thread tests/stgcmt.rs
Comment on lines +108 to +113
struct TestInstance {
study_instance_uid: String,
series_instance_uid: String,
sop_class_uid: String,
sop_instance_uid: String,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: We should update our clippy config because these kind of pedantic lints are not useful and rather annoying.

Comment thread tests/stgcmt.rs
}

#[tokio::test]
async fn can_commit_storage_and_check_result() -> anyhow::Result<()> {
@nickamzol
nickamzol self-requested a review October 27, 2026 07:53
@nickamzol nickamzol added backend:dimse This issue affects the DIMSE backend service:stgcmt This issue affects the Storage Commitment service labels Oct 27, 2026

@nickamzol nickamzol left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!
Just a few robustness fixes, all small and local, nothing structural:

Comment thread src/backend/dimse/stgcmt/mod.rs Resolved
Comment thread src/backend/dimse/stgcmt/store.rs Resolved
Comment thread tests/stgcmt.rs
Comment on lines +108 to +113
struct TestInstance {
study_instance_uid: String,
series_instance_uid: String,
sop_class_uid: String,
sop_instance_uid: String,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: We should update our clippy config because these kind of pedantic lints are not useful and rather annoying.

Comment thread src/backend/dimse/stgcmt/store.rs Resolved
Comment thread src/backend/dimse/stgcmt/service.rs Outdated
if let Err(err) = result {
// The N-ACTION-RQ itself failed synchronously, so no N-EVENT-REPORT-RQ will ever
// arrive for this Transaction UID - allow the client to retry.
self.store.remove(&request.transaction_uid);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes the entry on every error, but for CommitError::Read errors the N-ACTION-RQ already went out: write_message succeeded and only read_message failed (e.g. a timeout). The PACS may already have the request and will then send the N-EVENT-REPORT-RQ on a new association.

If that report arrives before our timeout, complete() stores the result and then this line deletes it. The client gets a 404 even though the commit went through.

Comment thread src/backend/dimse/cstore/storescp.rs Outdated
.context("Missing tag COMMAND_FIELD (0000,0100)")?;

if command_field == COMMAND_FIELD_N_EVENT_REPORT_REQUEST {
handle_event_report(&association, message, &inner.stgcmt_store).await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch is taken when the PACS delivers the report on a new association.
If I understand correctly, a conformant SCP negotiates the SCP role (PS3.4 J.3.3.1.2 Note 1)), but our Store SCP is built with dicom-ul's DefaultNegotiation which omits role selection.

If the report comes back on the same association that carried the N-ACTION, this isn't a problem: DICOM-RST is the association requestor and the PACS is the acceptor, so under the implicit default roles the PACS is already the SCP. But delivery on a new association reverses the requestor/acceptor roles: now the PACS opens the association and becomes the requestor.

This works with Orthanc, which sends the report regardless, but we shouldn't count on every PACS being that forgiving.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know any open PACS i can test this against (as you said Orthanc doesn't have this)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was more of a theoretical consideration based on the specification. I'm not aware of any PACS that does this either, but let's not block on this. It doesn't hurt to include the role negotiation.
I'll publish this as a pre-release, so we can gather some experience with various implementations.

@feliwir
feliwir requested a review from nickamzol October 30, 2026 08:48
@nickamzol
nickamzol merged commit 0a96604 into UMEssen:main Oct 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend:dimse This issue affects the DIMSE backend service:stgcmt This issue affects the Storage Commitment service

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Storage Commitment Support

3 participants