From 64f15bd24d92af03c97ba8f6469ff863442b2904 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 28 Aug 2024 14:33:32 -0400 Subject: [PATCH 01/44] Add celery task signature --- Cargo.lock | 996 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- src/lib.rs | 1 + src/registration_task.rs | 56 +++ 4 files changed, 1035 insertions(+), 21 deletions(-) create mode 100644 src/registration_task.rs diff --git a/Cargo.lock b/Cargo.lock index a8a2150..7ee67fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -65,6 +76,54 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "amq-protocol" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0234884b3641db74d22ccc20fc2594db5f23d7d41ade5c93d7ee33d200960c" +dependencies = [ + "amq-protocol-tcp", + "amq-protocol-types", + "amq-protocol-uri", + "cookie-factory", + "nom", + "serde", +] + +[[package]] +name = "amq-protocol-tcp" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265dca43d9dbb3d5bbb0b3ef1b0cd9044ce3aa5d697d5b66cde974d1f6063f09" +dependencies = [ + "amq-protocol-uri", + "tcp-stream", + "tracing", +] + +[[package]] +name = "amq-protocol-types" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7412353b58923fa012feb9a64ccc0c811747babee2e5a2fd63eb102dc8054c3" +dependencies = [ + "cookie-factory", + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "amq-protocol-uri" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be91352c805d5704784e079117d5291fd5bf2569add53c914ebce6d1a795d33" +dependencies = [ + "amq-protocol-types", + "percent-encoding", + "url", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -86,6 +145,173 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel", + "async-executor", + "async-io 2.3.3", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.3.0", + "once_cell", +] + +[[package]] +name = "async-global-executor-trait" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33dd14c5a15affd2abcff50d84efd4009ada28a860f01c14f9d654f3e81b3f75" +dependencies = [ + "async-global-executor", + "async-trait", + "executor-trait", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +dependencies = [ + "async-lock 3.4.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-reactor-trait" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e" +dependencies = [ + "async-io 1.13.0", + "async-trait", + "futures-core", + "reactor-trait", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -108,6 +334,12 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.77" @@ -257,6 +489,38 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.15.4" @@ -305,12 +569,62 @@ dependencies = [ "serde", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +[[package]] +name = "celery" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e814f3bf30847a0d694ef03091cd2246543ff6e7c3535b83c226ebb980048" +dependencies = [ + "async-trait", + "base64 0.21.7", + "celery-codegen", + "chrono", + "colored", + "futures", + "futures-lite 1.13.0", + "globset", + "hostname", + "lapin", + "log", + "once_cell", + "rand", + "redis", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-executor-trait", + "tokio-reactor-trait", + "tokio-stream", + "uuid", +] + +[[package]] +name = "celery-codegen" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0f4f3440943210cc64fab5a5feb8d8ef021bf248a33cc37840185bfcf1710e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -357,11 +671,68 @@ checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.4", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid", + "der", + "spki", + "x509-cert", +] + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.8" @@ -381,6 +752,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + [[package]] name = "core-foundation" version = "0.9.4" @@ -528,10 +905,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "der_derive", + "flagset", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "deranged" version = "0.3.11" @@ -542,6 +946,15 @@ dependencies = [ "serde", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "deunicode" version = "1.4.3" @@ -711,6 +1124,23 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dotenvy" version = "0.15.7" @@ -850,6 +1280,36 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "executor-trait" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a1052dd43212a7777ec6a69b117da52f5e52f07aec47d00c1a2b33b85d06b08" +dependencies = [ + "async-trait", +] + [[package]] name = "exr" version = "1.72.0" @@ -876,6 +1336,15 @@ dependencies = [ "rand", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.1.0" @@ -910,6 +1379,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + [[package]] name = "flate2" version = "1.0.28" @@ -1030,6 +1505,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -1112,6 +1615,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "h2" version = "0.3.24" @@ -1200,6 +1716,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -1329,7 +1851,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -1365,7 +1887,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls", + "rustls 0.21.10", "tokio", "tokio-rustls", ] @@ -1411,7 +1933,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.3.1", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower", "tower-service", @@ -1523,19 +2045,49 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "inventory" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.6", "widestring", "windows-sys 0.48.0", "winreg 0.50.0", @@ -1553,7 +2105,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.52.0", ] @@ -1621,6 +2173,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lapin" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209b09a06f4bd4952a0fd0594f90d53cf4496b062f59acc838a2823e1bb7d95c" +dependencies = [ + "amq-protocol", + "async-global-executor-trait", + "async-reactor-trait", + "async-trait", + "executor-trait", + "flume", + "futures-core", + "futures-io", + "parking_lot", + "pinky-swear", + "reactor-trait", + "serde", + "tracing", + "waker-fn", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1665,6 +2239,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1804,6 +2384,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1863,7 +2453,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -1876,6 +2466,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -2023,11 +2622,12 @@ dependencies = [ [[package]] name = "oxidicom" -version = "2.0.0" +version = "3.0.0-a1" dependencies = [ "aliri_braid", "anyhow", "camino", + "celery", "chris", "dicom", "figment", @@ -2053,6 +2653,34 @@ dependencies = [ "walkdir", ] +[[package]] +name = "p12-keystore" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7b60d0b2dcace322e6e8c4499c4c8bdf331c1bae046a54be5e4191c3610286" +dependencies = [ + "cbc", + "cms", + "der", + "des", + "hex", + "hmac", + "pkcs12", + "pkcs5", + "rand", + "rc2", + "sha1", + "sha2", + "thiserror", + "x509-parser", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2082,6 +2710,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pear" version = "0.2.9" @@ -2152,6 +2790,29 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinky-swear" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfae3ead413ca051a681152bd266438d3bfa301c9bdf836939a14c721bb2a21" +dependencies = [ + "doc-comment", + "flume", + "parking_lot", + "tracing", +] + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -2163,6 +2824,36 @@ dependencies = [ "spki", ] +[[package]] +name = "pkcs12" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" +dependencies = [ + "cms", + "const-oid", + "der", + "digest", + "spki", + "x509-cert", + "zeroize", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2192,6 +2883,37 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2323,6 +3045,48 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + +[[package]] +name = "reactor-trait" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58" +dependencies = [ + "async-trait", + "futures-core", + "futures-io", +] + +[[package]] +name = "redis" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8455fa3621f6b41c514946de66ea0531f57ca017b2e6c7cc368035ea5b46df" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2391,7 +3155,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.10", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -2558,6 +3322,29 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.34" @@ -2567,7 +3354,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] @@ -2579,10 +3366,50 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.7", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-connector" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a980454b497c439c274f2feae2523ed8138bbd3d323684e1435fec62f800481" +dependencies = [ + "log", + "rustls 0.23.12", + "rustls-native-certs", + "rustls-pki-types", + "rustls-webpki 0.102.7", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2618,6 +3445,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.15" @@ -2636,6 +3474,15 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98a01dab6acf992653be49205bdd549f32f17cb2803e8eacf1560bf97259aae8" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2660,6 +3507,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -2789,6 +3647,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -2883,6 +3747,16 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.6" @@ -2955,7 +3829,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -2969,7 +3843,7 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.21.10", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -3191,6 +4065,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3221,6 +4106,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "tcp-stream" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495b0abdce3dc1f8fd27240651c9e68890c14e9d9c61527b1ce44d8a5a7bd3d5" +dependencies = [ + "cfg-if", + "p12-keystore", + "rustls-connector", + "rustls-pemfile 2.1.2", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -3228,8 +4125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "rustix", + "fastrand 2.1.0", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -3239,7 +4136,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix", + "rustix 0.38.34", "windows-sys 0.48.0", ] @@ -3344,11 +4241,22 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-executor-trait" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802ccf58e108fe16561f35348fabe15ff38218968f033d587e399a84937533cc" +dependencies = [ + "async-trait", + "executor-trait", + "tokio", +] + [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -3380,13 +4288,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-reactor-trait" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9481a72f36bd9cbb8d6dd349227c4783e234e4332cfe806225bc929c4b92486" +dependencies = [ + "async-trait", + "futures-core", + "futures-io", + "reactor-trait", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.10", "tokio", ] @@ -3565,9 +4487,9 @@ dependencies = [ "ipnet", "once_cell", "rand", - "rustls", + "rustls 0.21.10", "rustls-pemfile 1.0.4", - "rustls-webpki", + "rustls-webpki 0.101.7", "smallvec", "thiserror", "tinyvec", @@ -3591,7 +4513,7 @@ dependencies = [ "parking_lot", "rand", "resolv-conf", - "rustls", + "rustls 0.21.10", "smallvec", "thiserror", "tokio", @@ -3731,6 +4653,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -4090,6 +5018,34 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 75273a3..9e35432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "oxidicom" description = "DICOM receiver for ChRIS backend" -version = "2.0.0" +version = "3.0.0-a1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -29,6 +29,7 @@ futures = "0.3.30" time = { version = "0.3.36", features = ["macros", "parsing"] } ulid = "1.1.2" figment = { version = "0.10.19", features = ["env"] } +celery = "0.5.5" [dev-dependencies] rstest = "0.21.0" diff --git a/src/lib.rs b/src/lib.rs index 1c53546..768dd3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ mod series_key_set; mod settings; mod thread_pool; mod transfer; +mod registration_task; pub use config::get_config; pub use dicomrs_settings::DicomRsSettings; diff --git a/src/registration_task.rs b/src/registration_task.rs new file mode 100644 index 0000000..b27e649 --- /dev/null +++ b/src/registration_task.rs @@ -0,0 +1,56 @@ +//! Celery task definition of the PACSSeries registration function in CUBE, +//! for submitting tasks to CUBE (Python)'s celery worker from our Rust code. + +#![allow(unused_variables)] +#![allow(unreachable_code)] + +use std::num::NonZeroUsize; + +/// A function stub with the same signature as the `register_pacs_series` celery task +/// in *CUBE*'s Python code. +#[celery::task(name = "pacsfiles.tasks.register_pacs_series")] +fn register_pacs_series( + patient_id: String, + patient_name: String, + study_date: String, + study_instance_uid: String, + study_description: String, + series_description: String, + series_instance_uid: String, + pacs_name: String, + path: String, + ndicom: NonZeroUsize, +) { + unimplemented!() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::num::NonZeroUsize; + + #[tokio::test] + async fn test_try_celery_wip() -> anyhow::Result<()> { + let app = celery::app!( + broker = AMQPBroker { "amqp://queue:5672" }, + tasks = [register_pacs_series], + task_routes = [ "pacsfiles.tasks.register_pacs_series" => "main2" ], + ) + .await?; + + let task = register_pacs_series::new( + "Jennings Zhang".to_string(), + "abc123ismyID".to_string(), + "2024-08-28".to_string(), + "StudyInstance123".to_string(), + "hello from rust".to_string(), + "SeriesInstance456".to_string(), + "i am so cool".to_string(), + "MyPACS".to_string(), + "SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRA".to_string(), + NonZeroUsize::new(192).unwrap(), + ); + app.send_task(task).await?; + Ok(()) + } +} From ee8476a5d24c275ee3a43ea90ee609b8c365ad75 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 28 Aug 2024 15:01:05 -0400 Subject: [PATCH 02/44] Document proposed changes to oxidicom --- CUSTOM_SPEC.md | 314 ----- DEVELOP.md | 56 - HOWITWORKS.md | 17 - README.md | 64 +- ...S Retrieval Performance-1711608936513.json | 1142 ----------------- grafana/screenshot.png | Bin 292336 -> 0 bytes 6 files changed, 22 insertions(+), 1571 deletions(-) delete mode 100644 CUSTOM_SPEC.md delete mode 100644 DEVELOP.md delete mode 100644 HOWITWORKS.md delete mode 100644 grafana/ChRIS PACS Retrieval Performance-1711608936513.json delete mode 100644 grafana/screenshot.png diff --git a/CUSTOM_SPEC.md b/CUSTOM_SPEC.md deleted file mode 100644 index ca9c703..0000000 --- a/CUSTOM_SPEC.md +++ /dev/null @@ -1,314 +0,0 @@ -# "Oxidicom Custom Metadata" Spec - -`oxidicom` will push empty files to CUBE and register them under the `api/v1/pacsfiles/` API. -These files contain useful information about the PACS retrieval process, such as: - -- [`NumberOfSeriesRelatedInstances`](#numberofseriesrelatedinstances) -- Number of DICOM files received by `oxidicom` for each series per [association](#association). -- Any errors (TODO) - -These empty files will always live under the path `SERVICES/PACS/org.fnndsc.oxidicom` and be searchable by -`pacs_identifier=org.fnndsc.oxidicom`. - -## Background - -A "PACS Pull" is initiated when _pfdcm_ asks the (hospital) PACS server to send us DICOMs. -The hospital PACS server will open a TCP connection with `oxidicom` and send it some DICOM objects. -In a typical _ChRIS_ workflow, users pull DICOM **series**, which contain zero or more DICOM **instances**. -Each DICOM instance is represented by one DICOM file. - -_CUBE_ keeps track of individual DICOM files in its `api/v1/pacsfiles/` API. - -### Series-Wise Convention - -At the FNNDSC, structural MRI is our biggest area of research. - -- One DICOM instance is a 2D MRI slice. -- One DICOM series is a 3D MRI scan (or a 4D fMRI). -- One DICOM study is a collection of MRI scans. - -Since a DICOM series is "one scan," `oxidicom` keeps track of the series being received. - -### DICOM Terminology - -The hospital PACS _pushes_ data to us, hence the hospital PACS is a _client._ (We often call it a "PACS Server," -however during the retrieval of DICOM files, the PACS' role is a client.) - -#### Association - -The TCP connection made by the hospital PACS to `oxidicom` in which DICOM files are received is called a -**DICOM association.** During an association, we typically receive one series or one study, which consists -of [1, N) DICOM instances. - -In reality, the PACS could possibly send us a study, a patient, anything, or nothing. `oxidicom` will -accept whatever it is given without fuss. - -## Association ULID Path - -We typically assume some properties are upheld by the DICOM protocol: - -- StudyInstanceUID is globally unique for all studies -- SeriesInstanceUID is globally unique for all series -- The NumberOfSeriesRelatedInstances for a series will always be the same - -In reality, a PACS server will push to us whatever it wants. `oxidicom` does not assume the above are -always true. - -`oxidicom` assigns a [ULID](https://github.com/ulid/spec) to each [Association](#association). -It will register key-value pairs to - -``` -SERVICES/PACS/org.fnndsc.oxidicom/{ABSOLUTE_SERIES_DIR}/{ASSOCIATION_ULID}/{KEY}={VALUE} -``` - -### Example - -Suppose you'd expect a DICOM file to be registered at - -``` -SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/96-1.3.12.2.1107.5.2.19.45152.2013030808105959806985847.dcm -``` - -The `ABSOLUTE_SERIES_DIR` is `SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06`. - -After trying to retrieve the series once, you will find the following files to be created: - -``` -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7TF03EZD364005NP332RBQ/NumberOfSeriesRelatedInstances=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7TF03EZD364005NP332RBQ/OxidicomAttemptedPushCount=192 -``` - -Let's say that you attempt to retrieve the series a second time. You will now find: - -``` -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WN2KMQ36T7E85SVX6G4V4/NumberOfSeriesRelatedInstances=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WN2KMQ36T7E85SVX6G4V4/OxidicomAttemptedPushCount=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7TF03EZD364005NP332RBQ/NumberOfSeriesRelatedInstances=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7TF03EZD364005NP332RBQ/OxidicomAttemptedPushCount=192 -``` - -What if the hospital PACS _misbehaves_, sending us a different `NumberOfSeriesRelatedInstances` on the third retrieve attempt? -You will find: - -``` -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WP273YRHSH33TC3BNDJEB/NumberOfSeriesRelatedInstances=43 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WP273YRHSH33TC3BNDJEB/OxidicomAttemptedPushCount=43 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WN2KMQ36T7E85SVX6G4V4/NumberOfSeriesRelatedInstances=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WN2KMQ36T7E85SVX6G4V4/OxidicomAttemptedPushCount=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7TF03EZD364005NP332RBQ/NumberOfSeriesRelatedInstances=192 -SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/HOSPITAL_PACS/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7TF03EZD364005NP332RBQ/OxidicomAttemptedPushCount=192 -``` - -## Key-Value Pairs - -The basename of a file representing a key-value pair will always be `{KEY}={VALUE}`. The file contents will be empty. -Furthermore, the key will also be the value of `ProtocolName` and the value will be the value of `SeriesDescription`. - -This naming convention facilitates search. For example, suppose you want to get the `NumberOfSeriesRelatedInstances` -for a series with `SeriesInstanceUID=1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0`. Make a GET request to - -``` -/api/v1/pacsfiles/search/?pacs_identifier=org.fnndsc.oxidicom&SeriesInstanceUID=1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0&ProtocolName=NumberOfSeriesRelatedInstances -``` - -Or, leave out the `&ProtocolName=` query to get both `NumberOfSeriesRelatedInstances` and `OxidicomAttemptedPushCount`. - -## Example Response Body from CUBE - -```json -{ - "count": 4, - "next": null, - "previous": null, - "results": [ - { - "url": "https://example.org/api/v1/pacsfiles/1747/", - "id": 1747, - "creation_date": "2024-03-20T17:22:41.432808-04:00", - "fname": "SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WP273YRHSH33TC3BNDJEB/OxidicomAttemptedPushCount=192", - "fsize": 0, - "PatientID": "1449c1d", - "PatientName": "", - "PatientBirthDate": null, - "PatientAge": null, - "PatientSex": "", - "StudyDate": "2013-03-08", - "AccessionNumber": "", - "Modality": "", - "ProtocolName": "OxidicomAttemptedPushCount", - "StudyInstanceUID": "1.2.840.113845.11.1000000001785349915.20130308061609.6346698", - "StudyDescription": "", - "SeriesInstanceUID": "1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0", - "SeriesDescription": "192", - "pacs_identifier": "org.fnndsc.oxidicom", - "file_resource": "https://example.org/api/v1/pacsfiles/1747/OxidicomAttemptedPushCount=192" - }, - { - "url": "https://example.org/api/v1/pacsfiles/1553/", - "id": 1553, - "creation_date": "2024-03-20T17:22:17.754581-04:00", - "fname": "SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/5-SAG_MPRAGE_220_FOV-a27cf06/01HZ7WP273YRHSH33TC3BNDJEB/NumberOfSeriesRelatedInstances=192", - "fsize": 0, - "PatientID": "1449c1d", - "PatientName": "", - "PatientBirthDate": null, - "PatientAge": null, - "PatientSex": "", - "StudyDate": "2013-03-08", - "AccessionNumber": "", - "Modality": "", - "ProtocolName": "NumberOfSeriesRelatedInstances", - "StudyInstanceUID": "1.2.840.113845.11.1000000001785349915.20130308061609.6346698", - "StudyDescription": "", - "SeriesInstanceUID": "1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0", - "SeriesDescription": "192", - "pacs_identifier": "org.fnndsc.oxidicom", - "file_resource": "https://example.org/api/v1/pacsfiles/1553/NumberOfSeriesRelatedInstances=192" - }, - { - "url": "https://example.org/api/v1/pacsfiles/2126/", - "id": 2126, - "creation_date": "2024-03-20T17:23:29.017147-04:00", - "fname": "SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/401-anat-T1w-661b8fc/772bc789-429e-474d-aa64-044b4002f56e/OxidicomAttemptedPushCount=384", - "fsize": 0, - "PatientID": "02", - "PatientName": "", - "PatientBirthDate": null, - "PatientAge": null, - "PatientSex": "", - "StudyDate": "2013-07-17", - "AccessionNumber": "", - "Modality": "", - "ProtocolName": "OxidicomAttemptedPushCount", - "StudyInstanceUID": "1.2.826.0.1.3680043.2.1143.2592092611698916978113112155415165916", - "StudyDescription": "", - "SeriesInstanceUID": "1.2.826.0.1.3680043.2.1143.515404396022363061013111326823367652", - "SeriesDescription": "384", - "pacs_identifier": "org.fnndsc.oxidicom", - "file_resource": "https://example.org/api/v1/pacsfiles/2126/OxidicomAttemptedPushCount=384" - }, - { - "url": "https://example.org/api/v1/pacsfiles/1742/", - "id": 1742, - "creation_date": "2024-03-20T17:22:40.538847-04:00", - "fname": "SERVICES/PACS/org.fnndsc.oxidicom/SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/401-anat-T1w-661b8fc/772bc789-429e-474d-aa64-044b4002f56e/NumberOfSeriesRelatedInstances=384", - "fsize": 0, - "PatientID": "02", - "PatientName": "", - "PatientBirthDate": null, - "PatientAge": null, - "PatientSex": "", - "StudyDate": "2013-07-17", - "AccessionNumber": "", - "Modality": "", - "ProtocolName": "NumberOfSeriesRelatedInstances", - "StudyInstanceUID": "1.2.826.0.1.3680043.2.1143.2592092611698916978113112155415165916", - "StudyDescription": "", - "SeriesInstanceUID": "1.2.826.0.1.3680043.2.1143.515404396022363061013111326823367652", - "SeriesDescription": "384", - "pacs_identifier": "org.fnndsc.oxidicom", - "file_resource": "https://example.org/api/v1/pacsfiles/1742/NumberOfSeriesRelatedInstances=384" - } - ] -} -``` - -## NumberOfSeriesRelatedInstances - -For each series of an [association](#association), `oxidicom` will ask the PACS server for the -[NumberOfSeriesRelatedInstances](https://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.3.4.html). -NumberOfSeriesRelatedInstances is one of: - -- a Nat, e.g. 192 -- literal "unknown" - -"unknown" will be registered in any case of error, e.g. - -- `oxidicom` was not configured with `OXIDICOM_PACS_ADDRESS` so it does not know how to contact the PACS -- The PACS did not return a value -- The PACS returned an invalid value - -## OxidicomAttemptedPushCount - -After all files for an [association](#association) were pushed, `oxidicom` will register the number of files it -_attempted_ to push as `OxidicomAttemptedPushCount`. - -### OxidicomAttemptedPushCount Errors - -- If the value for `OxidicomAttemptedPushCount` is not the same as the value for `NumberOfSeriesRelatedInstances`, - the PACS server is misbehaved. -- If the value for `OxidicomAttemptedPushCount` is not the same as the `count` reported by CUBE - `api/v1/pacsfiles/search/?SeriesInstanceUID=x.x.x.xxxxx`, CUBE is misbehaved. - -## File Appearance Timing - -The file for `NumberOfSeriesRelatedInstances=*` will be relatively slow to appear, because it can only be queried -for after the first DICOM instance of a series is received. - -The file `OxidicomAttemptedPushCount=*` is guaranteed to be the last file to be registered. In other words, the -appearance of the file `OxidicomAttemptedPushCount=*` indicates the retrieval is "done" and no more DICOM files -will be received for the series (in this association). - -Here's what a timeline **might** look like for a retrieve of 192 DICOM instances: - -``` - time ---> -DICOM Association [====================] - | | | -Push to CUBE | [=====|=====|=============================] - | | | | | | - | | | | | OxidicomAttemptedPushCount=192 received by CUBE - | | | | | - | | | | Last DICOM received by CUBE from oxidicom - | | | | - | | | Last DICOM received by oxidicom from PACS - | | | - | | NumberOfSeriesRelatedInstances=192 received by CUBE - | | - | First DICOM received by CUBE from oxidicom - | - First DICOM received by oxidicom from PACS -``` - -Data reception and handling are asynchronous. In testing, it is often the case that the DICOM association -pushes data much faster than storage speed. In this situation, the spans look like - -``` - time ---> -DICOM Association [====================] - -Push to CUBE [=====================================] -``` - -## Suggested Client Implementation - -A simple client implementation would just poll for the existence of a `OxidicomAttemptedPushCount=*` to know -when a PACS retrieve operation is complete. Doing so assumes that (a) the PACS server is well-behaved, -(b) everything between PFDCM <--> PACS <--> oxidicom <--> Postgres <--> CUBE is working smoothly. These -assumptions are usually true, however this implementation can cause silent errors. - -Ideally, a client who wants to monitor the progress of a PACS pull operation _should_ do: - -1. Poll until `NumberOfSeriesRelatedInstances=*` appears, so that you know how many DICOM files to expect. -2. Poll the value of `count` until it is equal to the `NumberOfSeriesRelatedInstances` -3. Poll until `OxidicomAttemptedPushCount=*` appears, to triple-check that everything worked. - -The GET requests corresponding to the steps above would be: - -1. The `NumberOfSeriesRelatedInstances`, e.g. `api/v1/pacsfiles/search/?min_creation_date=TTTTTTTT&pacs_identifier=org.fnndsc.oxidicom&SeriesInstanceUID=x.x.x.xxxxx&ProtocolName=NumberOfSeriesRelatedInstances` -2. The `count` of real DICOM files, e.g. `api/v1/pacsfiles/search/?pacs_identifier=HOSPITALPACS&SeriesInstanceUID=x.x.x.xxxxx` -3. The `OxidicomAttemptedPushCount`, e.g. `api/v1/pacsfiles/search/?min_creation_date=TTTTTTTT&pacs_identifier=HOSPITALPACS&SeriesInstanceUID=x.x.x.xxxxx&ProtocolName=OxidicomAttemptedPushCount` - -Where: - -- `HOSPITALPACS` is the PACS AE title the DICOMs are being retrieved from -- `x.x.x.xxxxx` is the SeriesInstanceUID of interest -- `TTTTTTTT` is the timestamp the retrieve operation was initiated by PFDCM - -Explanation of query string parameters: - -- `pacs_identifier=HOSPITALPACS&SeriesInstanceUID=x.x.x.xxxxx` searches for DICOM files of the series -- `pacs_identifier=org.fnndsc.oxidicom&SeriesInstanceUID=x.x.x.xxxxx` searches for "Oxidicom Custom Metadata" for the series -- `pacs_identifier=org.fnndsc.oxidicom&ProtocolName=NumberOfSeriesRelatedInstances` searches for files representing `NumberOfSeriesRelatedInstances=*` -- `pacs_identifier=org.fnndsc.oxidicom&ProtocolName=OxidicomAttemptedPushCount` searches for files representing `OxidicomAttemptedPushCount=*` -- `min_creation_date=TTTTTTTT` limits search results to only the most recent PACS retrieve attempt (ignoring the "Oxidicom Custom Metadata" of prior attempts) diff --git a/DEVELOP.md b/DEVELOP.md deleted file mode 100644 index 52ebe04..0000000 --- a/DEVELOP.md +++ /dev/null @@ -1,56 +0,0 @@ -## PostgreSQL Tables - -### pacsfiles_pacs - -``` - Table "public.pacsfiles_pacs" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description -------------+-----------------------+-----------+----------+----------------------------------+----------+-------------+--------------+------------- - id | bigint | | not null | generated by default as identity | plain | | | - identifier | character varying(20) | | not null | | extended | | | -Indexes: - "pacsfiles_pacs_pkey" PRIMARY KEY, btree (id) - "pacsfiles_pacs_identifier_fc12db8a_like" btree (identifier varchar_pattern_ops) - "pacsfiles_pacs_identifier_key" UNIQUE CONSTRAINT, btree (identifier) -Referenced by: - TABLE "pacsfiles_pacsfile" CONSTRAINT "pacsfiles_pacsfile_pacs_id_a6922e71_fk" FOREIGN KEY (pacs_id) REFERENCES pacsfiles_pacs(id) DEFERRABLE INITIALLY DEFERRED -Access method: heap -``` - -### pacsfiles_pacsfile - -``` - Table "public.pacsfiles_pacsfile" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------------------+--------------------------+-----------+----------+----------------------------------+----------+-------------+--------------+------------- - id | bigint | | not null | generated by default as identity | plain | | | - creation_date | timestamp with time zone | | not null | | plain | | | - fname | character varying(512) | | not null | | extended | | | - PatientID | character varying(100) | | not null | | extended | | | - PatientName | character varying(150) | | not null | | extended | | | - StudyInstanceUID | character varying(100) | | not null | | extended | | | - StudyDescription | character varying(400) | | not null | | extended | | | - SeriesInstanceUID | character varying(100) | | not null | | extended | | | - SeriesDescription | character varying(400) | | not null | | extended | | | - pacs_id | bigint | | not null | | plain | | | - PatientAge | integer | | | | plain | | | - PatientBirthDate | date | | | | plain | | | - PatientSex | character varying(1) | | not null | | extended | | | - Modality | character varying(15) | | not null | | extended | | | - ProtocolName | character varying(64) | | not null | | extended | | | - StudyDate | date | | not null | | plain | | | - AccessionNumber | character varying(100) | | not null | | extended | | | -Indexes: - "pacsfiles_pacsfile_pkey" PRIMARY KEY, btree (id) - "pacsfiles_pacsfile_AccessionNumber_530efc42" btree ("AccessionNumber") - "pacsfiles_pacsfile_AccessionNumber_530efc42_like" btree ("AccessionNumber" varchar_pattern_ops) - "pacsfiles_pacsfile_PatientID_65a17c84" btree ("PatientID") - "pacsfiles_pacsfile_PatientID_65a17c84_like" btree ("PatientID" varchar_pattern_ops) - "pacsfiles_pacsfile_StudyDate_5821d7cf" btree ("StudyDate") - "pacsfiles_pacsfile_fname_475c1816_like" btree (fname varchar_pattern_ops) - "pacsfiles_pacsfile_fname_key" UNIQUE CONSTRAINT, btree (fname) - "pacsfiles_pacsfile_pacs_id_a6922e71" btree (pacs_id) -Foreign-key constraints: - "pacsfiles_pacsfile_pacs_id_a6922e71_fk" FOREIGN KEY (pacs_id) REFERENCES pacsfiles_pacs(id) DEFERRABLE INITIALLY DEFERRED -Access method: heap -``` diff --git a/HOWITWORKS.md b/HOWITWORKS.md deleted file mode 100644 index d81d34e..0000000 --- a/HOWITWORKS.md +++ /dev/null @@ -1,17 +0,0 @@ -# How It Works - -## Direct to Postgres Database File Registration - -Historically, _oxidicom_ would make an HTTP request to _CUBE_ and _CUBE_ would put a row into the database -for each file. In version 2 of _oxidicom_, it registers files to the database directly for performance optimizations: - -- We avoid the slow Python code of _CUBE_ -- _oxidicom_ can (a) check for duplicates and (b) insert multiple rows, all in one transaction. - -### Possible Approaches - -I have considered two possible behaviors: - -- "simple": _oxidicom_ registers files to the database in batches. -- "smart": _oxidicom_ registers files at the end of an association, and attempts to validate whether the association - was typical (i.e. number of files per series is equal to `NumberOfSeriesRelatedInstances`) diff --git a/README.md b/README.md index 9319b02..5ba11aa 100644 --- a/README.md +++ b/README.md @@ -4,43 +4,24 @@ [![MIT License](https://img.shields.io/github/license/fnndsc/oxidicom)](https://github.com/FNNDSC/oxidicom/blob/master/LICENSE) [![CI](https://github.com/FNNDSC/oxidicom/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/oxidicom/actions/workflows/ci.yml) -`oxidicom` is a high-performance DICOM receiver for the +_oxidicom_ is a high-performance DICOM receiver for the [_ChRIS_ backend](https://github.com/FNNDSC/ChRIS_ultron_backEnd) (CUBE). -It **partially** replaces [pfdcm](https://github.com/FNNDSC/pfdcm). -More technically, `oxidicom` implements a DICOM C-STORE service class provider (SCP), -a "server," which listens for incoming DICOM files. For every DICOM file received, -it writes it to the storage of _CUBE_ and "registers" the file with _CUBE_. - -## Improvements over pfdcm - -Rewriting the functionality of `pfdcm` in Rust and with a modern design has led to several advantages: - -- Performance: registration of retrieved DICOM files to _CUBE_ happens in real-time instead of being - done in stages and polled until completion. -- Simplicity: client can simply check for the number of PACS files existing in CUBE (for a given - SeriesInstanceUID) instead of having to ask pfdcm for intermediate progress information (and having - to poll pfdcm to completion). -- Observability: `oxidicom` outputs structured logs and also sends traces to OpenTelemetry collector. -- Scalability: manual implementation of C-STORE makes `oxidicom` horizontally scalable (opposed to - relying on dcmtk's `storescp`, which is harder to scale because it spawns subprocesses). - -Prior to `oxidicom`, `pfdcm` was the major bottleneck in the _ChRIS_ PACS query/retrieval architecture. -Prior to `oxidicom` version 2, [CUBE was the bottleneck](https://github.com/FNNDSC/ChRIS_ultron_backEnd/issues/546). -Since `oxidicom` version 2, the _ChRIS_ architecture is fully able to keep up with user requests and -the data being sent to it from PACS, being capable of receiving >1,000s of DICOM files per second (with good hardware). +More technically, _oxidicom_ implements a DICOM C-STORE service class provider (SCP), +meaning it is a "server" which receives DICOM data over TCP. For every DICOM file received, +_oxidicom_ writes it to the storage of _CUBE_ and "registers" the file with _CUBE_. ## Environment Variables -Only `OXIDICOM_DB_CONNECTION` and `OXIDICOM_FILES_ROOT` are required. Those configure how oxidicom connects to CUBE. +Only `OXIDICOM_AMQP_ADDRESS` and `OXIDICOM_FILES_ROOT` are required. Those configure how oxidicom connects to _CUBE_. The other variables are either for optional features or performance tuning. | Name | Description | |----------------------------------|-----------------------------------------------------------------------------------------------------| -| `OXIDICOM_DB_CONNECTION` | (required) PostgreSQL connection string | -| `OXIDICOM_DB_POOL` | Database connection pool size | -| `OXIDICOM_DB_BATCH_SIZE` | Maximum number of files to register per request | +| `OXIDICOM_AMQP_ADDRESS` | (required) AMQP address of the RabbitMQ used by _CUBE_'s celery workers | | `OXIDICOM_FILES_ROOT` | (required) Path to where _CUBE_'s storage is mounted | +| `OXIDICOM_PROGRESS_NATS_ADDRESS` | (optional) NATS server where to send progress messages | +| `OXIDICOM_PROGRESS_INTERVAL_MS` | Minimum delay between progress messages per study | | `OXIDICOM_SCP_AET` | DICOM AE title (hospital PACS pushing to `oxidicom` should be configured to push to this name) | | `OXIDICOM_SCP_STRICT` | Whether receiving PDUs must not surpass the negotiated maximum PDU length. | | `OXIDICOM_SCP_UNCOMPRESSED_ONLY` | Only accept native/uncompressed transfer syntaxes | @@ -62,33 +43,32 @@ Behind the scenes, _oxidicom_ has three components connected by asynchronous cha 1. listener: receives DICOM objects over TCP 2. writer: writes DICOM objects to storage -3. registerer: writes DICOM metadata to CUBE's database +3. sender: emits progress messages to NATS and series registration jobs to celery `OXIDICOM_LISTENER_THREADS` controls the parallelism of the listener, whereas `TOKIO_WORKER_THREADS` controls the async runtime's thread pool which is shared between the writer and registerer. (The reason why we have two thread pools is an implementation detail: the Rust ecosystem suffers from a sync/async divide.) +## Scaling + +Large amounts of incoming data can be handled by horizontally scaling _oxidicom_. +It is easy to increase its number of replicas. However, the task queue for +registering the data to _CUBE_ will fill up. If you try to increase the number of +_CUBE_ celery workers, then the PostgreSQL database will get strained. + ## Failure Modes -`oxidicom` is designed to be fault-tolerant. Furthermore, it makes few assumptions -about whether the PACS is well-behaved. For instance, an error with an individual +_oxidicom_ is designed to be fault-tolerant. For instance, an error with an individual DICOM instance does not terminate the association (meaning, subsequent DICOM instances will still have the chance to be received). -Receiving the same DICOM data is idempotent. The database row will not be overwritten. -The duplicate DICOMs will be indicated in a corresponding OpenTelemetry span attribute. - -## "Oxidicom Custom Metadata" Spec - -The _ChRIS_ API does not provide any mechanism for knowing when a DICOM series has been pulled in completion. -A DICOM series contains 0 or more DICOM instances. _CUBE_ tracks each DICOM instance individually, but _CUBE_ -does not track how many instances _should_ there be for a series (`NumberOfSeriesRelatedInstances`). - -https://github.com/FNNDSC/ChRIS_ultron_backEnd/issues/544 +No assumptions are made about the PACS being well-behaved. _oxidicom_ does not care +if the PACS sends illegal data (e.g. the wrong number of DICOM instances for a series). -As a hacky workaround for this shortcoming, `oxidicom` will push dummy files into _CUBE_ as PACSFiles -under the space `SERVICES/PACS/org.fnndsc.oxidicom`. See [CUSTOM_SPEC.md](./CUSTOM_SPEC.md). +Receiving the same DICOM data more than once will overwrite the existing file in storage, +and another task to register the series will be sent to _CUBE_'s celery workers. _CUBE_'s +workers are going to throw an error when this happens. The overall behavior is idempotent. ## PACS Address Configuration diff --git a/grafana/ChRIS PACS Retrieval Performance-1711608936513.json b/grafana/ChRIS PACS Retrieval Performance-1711608936513.json deleted file mode 100644 index dba8c0c..0000000 --- a/grafana/ChRIS PACS Retrieval Performance-1711608936513.json +++ /dev/null @@ -1,1142 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "description": "Monitoring of the oxidicom component of ChRIS backend.", - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 28, - "links": [], - "panels": [ - { - "datasource": { - "type": "quickwit-quickwit-datasource", - "uid": "PA291FB16905E2BD2" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1 - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 0 - }, - "id": 10, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - } - }, - "pluginVersion": "10.4.0", - "targets": [ - { - "alias": "", - "bucketAggs": [], - "datasource": { - "type": "quickwit-quickwit-datasource", - "uid": "PA291FB16905E2BD2" - }, - "metrics": [ - { - "id": "1", - "settings": { - "limit": "10000", - "sortDirection": "desc" - }, - "type": "logs" - } - ], - "query": "service_name:oxidicom AND span_name:push_to_chris", - "refId": "A", - "timeField": "" - } - ], - "title": "CUBE PACSFIle Registration Timing", - "transformations": [ - { - "id": "filterFieldsByName", - "options": { - "include": { - "names": [ - "span_duration_millis", - "resource_attributes.k8s.pod.start_time" - ] - } - } - } - ], - "type": "histogram" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Memory Limit" - }, - "properties": [ - { - "id": "custom.fillOpacity", - "value": 0 - }, - { - "id": "custom.lineStyle", - "value": { - "dash": [ - 10, - 10 - ], - "fill": "dash" - } - }, - { - "id": "custom.showPoints", - "value": "never" - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 8, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "container_memory_working_set_bytes{namespace=\"$namespace\", container!=\"\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"}", - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "max(kube_pod_container_resource_limits{job=\"kube-state-metrics\", namespace=\"$namespace\", resource=\"memory\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"})", - "hide": false, - "instant": false, - "legendFormat": "Memory Limit", - "range": true, - "refId": "B" - } - ], - "title": "Oxidicom / Memory Usage (WSS)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "CPU Limit" - }, - "properties": [ - { - "id": "custom.fillOpacity", - "value": 0 - }, - { - "id": "custom.lineStyle", - "value": { - "dash": [ - 10, - 10 - ], - "fill": "dash" - } - }, - { - "id": "custom.showPoints", - "value": "never" - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 16, - "y": 0 - }, - "id": 1, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{namespace=\"$namespace\", container!=\"\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"}", - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "max(kube_pod_container_resource_limits{job=\"kube-state-metrics\", namespace=\"$namespace\", resource=\"cpu\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"})", - "hide": false, - "instant": false, - "legendFormat": "CPU Limit", - "range": true, - "refId": "B" - } - ], - "title": "Oxidicom / CPU Usage", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "CPU Limit" - }, - "properties": [ - { - "id": "custom.fillOpacity", - "value": 0 - }, - { - "id": "custom.lineStyle", - "value": { - "dash": [ - 10, - 10 - ], - "fill": "dash" - } - }, - { - "id": "custom.showPoints", - "value": "never" - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 - }, - "id": 3, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate{namespace=\"$namespace\", container!=\"\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"chris-server\", namespace=\"$namespace\"}", - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "max(kube_pod_container_resource_limits{job=\"kube-state-metrics\", namespace=\"$namespace\", resource=\"cpu\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"chris-server\", namespace=\"$namespace\"})", - "hide": false, - "instant": false, - "legendFormat": "CPU Limit", - "range": true, - "refId": "B" - } - ], - "title": "ChRIS Backend WSGI API Server / CPU Usage", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Memory Limit" - }, - "properties": [ - { - "id": "custom.fillOpacity", - "value": 0 - }, - { - "id": "custom.lineStyle", - "value": { - "dash": [ - 10, - 10 - ], - "fill": "dash" - } - }, - { - "id": "custom.showPoints", - "value": "never" - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 8 - }, - "id": 4, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "container_memory_working_set_bytes{namespace=\"$namespace\", container!=\"\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"chris-server\", namespace=\"$namespace\"}", - "instant": false, - "legendFormat": "{{pod}}", - "range": true, - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "max(kube_pod_container_resource_limits{job=\"kube-state-metrics\", namespace=\"$namespace\", resource=\"memory\"}\n* on (pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"chris-server\", namespace=\"$namespace\"})", - "hide": false, - "instant": false, - "legendFormat": "Memory Limit", - "range": true, - "refId": "B" - } - ], - "title": "ChRIS Backend WSGI API Server / Memory Usage (WSS)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 7, - "x": 0, - "y": 16 - }, - "id": 5, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "sum(\n irate(container_network_receive_bytes_total{job=\"kubelet\", metrics_path=\"/metrics/cadvisor\", namespace=\"$namespace\"}[$__rate_interval])\n * on (namespace, pod) group_left()\n kube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"}\n) by (pod)", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "Receive" - } - ], - "title": "Oxidicom / Receive Bandwidth", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 7, - "x": 7, - "y": 16 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "sum(\n irate(container_network_transmit_bytes_total{job=\"kubelet\", metrics_path=\"/metrics/cadvisor\", namespace=\"$namespace\"}[$__rate_interval])\n * on (namespace, pod) group_left()\n kube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"}\n) by (pod)", - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "Receive" - } - ], - "title": "Oxidicom / Transmit Bandwidth", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 100, - "gradientMode": "opacity", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "binBps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 10, - "x": 14, - "y": 16 - }, - "id": 7, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "editorMode": "code", - "expr": "sum by(pod) (\n rate(container_fs_writes_bytes_total{job=\"kubelet\", metrics_path=\"/metrics/cadvisor\", device=~\"(/dev/)?(mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|md.+|dasd.+)\", container!=\"\", namespace=\"$namespace\"}[$__rate_interval])\n * on (namespace, pod) group_left()\nkube_pod_labels{label_app_kubernetes_io_name=\"oxidicom\", namespace=\"$namespace\"}\n)", - "hide": false, - "instant": false, - "legendFormat": "__auto", - "range": true, - "refId": "B" - } - ], - "title": "Oxidicom / Disk Write", - "type": "timeseries" - }, - { - "datasource": { - "type": "tempo", - "uid": "tempo" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 24 - }, - "id": 9, - "links": [ - { - "title": "Trace Link", - "url": "http://localhost:32005/d/cdgslo52g3474b&var-traceId=${__data.fields[\"traceID\"]}" - } - ], - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true - }, - "pluginVersion": "10.4.0", - "targets": [ - { - "datasource": { - "type": "tempo", - "uid": "tempo" - }, - "filters": [ - { - "id": "7596b8a2", - "operator": "=", - "scope": "span" - }, - { - "id": "service-name", - "operator": "=", - "scope": "resource", - "tag": "service.name", - "value": [ - "\"oxidicom\"" - ] - }, - { - "id": "span-name", - "operator": "=", - "scope": "span", - "tag": "name", - "value": [ - "\"$spanName\"" - ] - } - ], - "limit": 300, - "queryType": "traceqlSearch", - "refId": "A", - "tableType": "traces" - } - ], - "title": "Trace Table", - "type": "table" - } - ], - "refresh": "", - "schemaVersion": 39, - "tags": [ - "ChRIS" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "kk", - "value": "kk" - }, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "definition": "label_values(kube_namespace_status_phase,namespace)", - "hide": 0, - "includeAll": false, - "multi": false, - "name": "namespace", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(kube_namespace_status_phase,namespace)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 1, - "type": "query" - }, - { - "current": { - "selected": true, - "text": "push_to_chris", - "value": "push_to_chris" - }, - "description": "Span names of oxidicom.\n\nValues can be obtained by running\n\n```shell\ngrep -rhoP '(?<=tracer\\.in_span\\(\").+?(?=\")' src | sed 's/$/,/'\n```", - "hide": 0, - "includeAll": false, - "label": "Span Name", - "multi": false, - "name": "spanName", - "options": [ - { - "selected": false, - "text": "association", - "value": "association" - }, - { - "selected": true, - "text": "push_to_chris", - "value": "push_to_chris" - }, - { - "selected": false, - "text": "getset_number_of_series_related_instances", - "value": "getset_number_of_series_related_instances" - }, - { - "selected": false, - "text": "register_all_attempted_pushes", - "value": "register_all_attempted_pushes" - } - ], - "query": "association,\npush_to_chris,\ngetset_number_of_series_related_instances,\nregister_all_attempted_pushes,", - "queryValue": "", - "skipUrlSync": false, - "type": "custom" - } - ] - }, - "time": { - "from": "2024-03-28T06:39:09.559Z", - "to": "2024-03-28T06:41:02.157Z" - }, - "timepicker": {}, - "timezone": "browser", - "title": "ChRIS PACS Retrieval Performance", - "uid": "cdgslo52g3474b", - "version": 2, - "weekStart": "" -} \ No newline at end of file diff --git a/grafana/screenshot.png b/grafana/screenshot.png deleted file mode 100644 index 202ffc9f7b976cbb621df5087d1d512907fe3229..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 292336 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

#1^(^>kK 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 From 5e63b89ae04d4259248b6df113e3f65f242acbf5 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 28 Aug 2024 16:18:01 -0400 Subject: [PATCH 03/44] WIP --- Cargo.lock | 549 +---------------------------- Cargo.toml | 2 +- README.md | 79 +---- src/batcher.rs | 63 ---- src/lib.rs | 6 +- src/{registerer.rs => notifier.rs} | 15 +- src/registration_synchronizer.rs | 2 +- src/registration_task.rs | 2 +- src/run_everything.rs | 24 +- src/settings.rs | 22 +- 10 files changed, 41 insertions(+), 723 deletions(-) delete mode 100644 src/batcher.rs rename src/{registerer.rs => notifier.rs} (89%) diff --git a/Cargo.lock b/Cargo.lock index 7ee67fd..f153fe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,19 +28,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.2" @@ -70,12 +57,6 @@ dependencies = [ "syn 2.0.52", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "amq-protocol" version = "7.2.1" @@ -351,15 +332,6 @@ dependencies = [ "syn 2.0.52", ] -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic" version = "0.6.0" @@ -476,9 +448,6 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -dependencies = [ - "serde", -] [[package]] name = "block-buffer" @@ -783,21 +752,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.4.0" @@ -826,15 +780,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -1119,7 +1064,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] @@ -1141,20 +1085,11 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "either" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" -dependencies = [ - "serde", -] [[package]] name = "encode_unicode" @@ -1263,17 +1198,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -1373,12 +1297,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "flagset" version = "0.4.6" @@ -1403,7 +1321,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -1488,17 +1406,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.30" @@ -1687,28 +1594,12 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.3", -] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "hermit-abi" @@ -1728,15 +1619,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -1746,15 +1628,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "hostname" version = "0.3.1" @@ -1834,6 +1707,22 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + [[package]] name = "hyper" version = "0.14.28" @@ -2200,9 +2089,6 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] [[package]] name = "lebe" @@ -2216,23 +2102,6 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libsqlite3-sys" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2288,16 +2157,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.7.1" @@ -2394,23 +2253,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2426,17 +2268,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.18" @@ -2444,7 +2275,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -2633,6 +2463,7 @@ dependencies = [ "figment", "fs-err", "futures", + "humantime-serde", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", @@ -2643,7 +2474,6 @@ dependencies = [ "seahash", "serde", "snafu", - "sqlx", "thiserror", "time", "tokio", @@ -2704,12 +2534,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -2813,17 +2637,6 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - [[package]] name = "pkcs12" version = "0.1.0" @@ -2854,16 +2667,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.30" @@ -3252,31 +3055,11 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rstest" version = "0.21.0" @@ -3695,16 +3478,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -3767,12 +3540,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -3792,229 +3559,6 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" -dependencies = [ - "itertools 0.12.1", - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" -dependencies = [ - "ahash", - "atoi", - "byteorder", - "bytes", - "crc", - "crossbeam-queue", - "either", - "event-listener 2.5.3", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashlink", - "hex", - "indexmap 2.2.5", - "log", - "memchr", - "once_cell", - "paste", - "percent-encoding", - "rustls 0.21.10", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlformat", - "thiserror", - "time", - "tokio", - "tokio-stream", - "tracing", - "url", - "webpki-roots", -] - -[[package]] -name = "sqlx-macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 1.0.109", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 1.0.109", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" -dependencies = [ - "atoi", - "base64 0.21.7", - "bitflags 2.5.0", - "byteorder", - "bytes", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "time", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" -dependencies = [ - "atoi", - "base64 0.21.7", - "bitflags 2.5.0", - "byteorder", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "time", - "tracing", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" -dependencies = [ - "atoi", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "sqlx-core", - "time", - "tracing", - "url", - "urlencoding", -] - -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "strsim" version = "0.10.0" @@ -4418,7 +3962,6 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4585,24 +4128,12 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.9.0" @@ -4620,12 +4151,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "uuid" version = "1.7.0" @@ -4684,12 +4209,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -4801,16 +4320,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "whoami" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall", - "wasite", -] - [[package]] name = "widestring" version = "1.0.2" @@ -5052,26 +4561,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "zerocopy" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.52", -] - [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 9e35432..ef3f907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,13 +23,13 @@ tracing = "0.1.40" tracing-subscriber = "0.3.18" aliri_braid = "0.4.0" anyhow = "1.0.86" -sqlx = { version = "0.7.4", features = ["postgres", "time", "runtime-tokio-rustls", "macros"], default-features = false } tokio = { version = "1.38.0", features = ["full"] } futures = "0.3.30" time = { version = "0.3.36", features = ["macros", "parsing"] } ulid = "1.1.2" figment = { version = "0.10.19", features = ["env"] } celery = "0.5.5" +humantime-serde = "1.1.1" [dev-dependencies] rstest = "0.21.0" diff --git a/README.md b/README.md index 5ba11aa..beb70eb 100644 --- a/README.md +++ b/README.md @@ -7,79 +7,7 @@ _oxidicom_ is a high-performance DICOM receiver for the [_ChRIS_ backend](https://github.com/FNNDSC/ChRIS_ultron_backEnd) (CUBE). -More technically, _oxidicom_ implements a DICOM C-STORE service class provider (SCP), -meaning it is a "server" which receives DICOM data over TCP. For every DICOM file received, -_oxidicom_ writes it to the storage of _CUBE_ and "registers" the file with _CUBE_. - -## Environment Variables - -Only `OXIDICOM_AMQP_ADDRESS` and `OXIDICOM_FILES_ROOT` are required. Those configure how oxidicom connects to _CUBE_. -The other variables are either for optional features or performance tuning. - -| Name | Description | -|----------------------------------|-----------------------------------------------------------------------------------------------------| -| `OXIDICOM_AMQP_ADDRESS` | (required) AMQP address of the RabbitMQ used by _CUBE_'s celery workers | -| `OXIDICOM_FILES_ROOT` | (required) Path to where _CUBE_'s storage is mounted | -| `OXIDICOM_PROGRESS_NATS_ADDRESS` | (optional) NATS server where to send progress messages | -| `OXIDICOM_PROGRESS_INTERVAL_MS` | Minimum delay between progress messages per study | -| `OXIDICOM_SCP_AET` | DICOM AE title (hospital PACS pushing to `oxidicom` should be configured to push to this name) | -| `OXIDICOM_SCP_STRICT` | Whether receiving PDUs must not surpass the negotiated maximum PDU length. | -| `OXIDICOM_SCP_UNCOMPRESSED_ONLY` | Only accept native/uncompressed transfer syntaxes | -| `OXIDICOM_SCP_PROMISCUOUS` | Whether to accept unknown abstract syntaxes. | -| `OXIDICOM_SCP_MAX_PDU_LENGTH` | Maximum PDU length | -| `OXIDICOM_PACS_ADDRESS` | PACS server addresses (recommended, see [PACS address configuration](#pacs-address-configuration)) | -| `OXIDICOM_LISTENER_THREADS` | Maximum number of concurrent SCU clients to handle. (see [Performance Tuning](#performance-tuning)) | -| `OXIDICOM_LISTENER_PORT` | TCP port number to listen on | -| `OXIDICOM_VERBOSE` | Set as `yes` to show debugging messages | -| `TOKIO_WORKER_THREADS` | Number of threads to use for the async runtime | -| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry Collector gRPC endpoint | -| `OTEL_RESOURCE_ATTRIBUTES` | Resource attributes, e.g. `service.name=oxidicom-test` | - -See [src/settings.rs](src/settings.rs) for the source of truth on the table above and default values of optional settings. - -## Performance Tuning - -Behind the scenes, _oxidicom_ has three components connected by asynchronous channels: - -1. listener: receives DICOM objects over TCP -2. writer: writes DICOM objects to storage -3. sender: emits progress messages to NATS and series registration jobs to celery - -`OXIDICOM_LISTENER_THREADS` controls the parallelism of the listener, whereas -`TOKIO_WORKER_THREADS` controls the async runtime's thread pool which is shared -between the writer and registerer. (The reason why we have two thread pools is -an implementation detail: the Rust ecosystem suffers from a sync/async divide.) - -## Scaling - -Large amounts of incoming data can be handled by horizontally scaling _oxidicom_. -It is easy to increase its number of replicas. However, the task queue for -registering the data to _CUBE_ will fill up. If you try to increase the number of -_CUBE_ celery workers, then the PostgreSQL database will get strained. - -## Failure Modes - -_oxidicom_ is designed to be fault-tolerant. For instance, an error with an individual -DICOM instance does not terminate the association (meaning, subsequent DICOM -instances will still have the chance to be received). - -No assumptions are made about the PACS being well-behaved. _oxidicom_ does not care -if the PACS sends illegal data (e.g. the wrong number of DICOM instances for a series). - -Receiving the same DICOM data more than once will overwrite the existing file in storage, -and another task to register the series will be sent to _CUBE_'s celery workers. _CUBE_'s -workers are going to throw an error when this happens. The overall behavior is idempotent. - -## PACS Address Configuration - -The environment variable `OXIDICOM_PACS_ADDRESS` should be a dictionary of AE titles to their IPv4 sockets -(IP address and port number). - -The PACS server address for a client AE title is used to look up the `NumberOfSeriesRelatedInstances`. -For example, suppose `OXIDICOM_PACS_ADDRESS={BCH="1.2.3.4:4242"}`. When we receive DICOMs from `BCH`, `oxidicom` -will do a C-FIND to `1.2.3.4:4242`, asking them what is the `NumberOfSeriesRelatedInstances` for the -received DICOMs. When we receive DICOMs from `MGH`, the PACS address is unknown, so `oxidicom` will set -`NumberOfSeriesRelatedInstances=unknown`. +Documentation: https://chrisproject.org/docs/oxidicom ## Development @@ -108,11 +36,6 @@ The `just` command, without arguments, will: 3. Push sample data into Orthanc 4. Run unit and integration tests -### Observability - -`oxidicom` exports traces to OpenTelemetry collector. There is a span for the association -(TCP connection from PACS server to send us DICOM objects). - ### Usage of `opentelemetry` v.s. `tracing` in the codebase `dicom-rs` itself uses the `tracing` crate, though for the spans described above, diff --git a/src/batcher.rs b/src/batcher.rs deleted file mode 100644 index d39190d..0000000 --- a/src/batcher.rs +++ /dev/null @@ -1,63 +0,0 @@ -/// Provides a wrapper around [Vec::push] which returns the Vec when its length reaches `batch_size`. -pub(crate) struct Batcher { - pub batch: Vec, - pub batch_size: usize, -} - -impl Batcher { - pub fn new(batch_size: usize) -> Self { - Self { - batch: Vec::with_capacity(batch_size), - batch_size, - } - } - - pub fn push(mut self, x: T) -> (Self, Option>) { - self.batch.push(x); - if self.batch.len() >= self.batch_size { - (Self::new(self.batch_size), Some(self.batch)) - } else { - (self, None) - } - } - - pub fn into_inner(self) -> Vec { - self.batch - } -} - -// Note: implementing Drop annoyingly causes E0509 -// cannot move out of type `Batcher`, which implements the `Drop` trait -// -// impl Drop for Batcher { -// fn drop(&mut self) { -// if !self.batch.is_empty() { -// tracing::warn!( -// "Batcher<{}> dropped but it was not empty. {} objects ignored." -// std::any::type_name::(), -// self.batch.len() -// ) -// } -// } -// } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_batcher() { - let batches0 = Batcher::new(3); - let (batches1, r0) = batches0.push("ChRIS"); - assert_eq!(r0, None); - let (batches2, r1) = batches1.push("is"); - assert_eq!(r1, None); - let (batches3, r2) = batches2.push("an"); - assert_eq!(r2, Some(vec!["ChRIS", "is", "an"])); - let (batches4, r3) = batches3.push("open-source"); - assert_eq!(r3, None); - let (batches5, r4) = batches4.push("software"); - assert_eq!(r4, None); - assert_eq!(batches5.into_inner(), vec!["open-source", "software"]) - } -} diff --git a/src/lib.rs b/src/lib.rs index 768dd3c..8fd4949 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,5 @@ mod association_error; -mod chrisdb_client; -// mod cube_sender; -// mod custom_metadata; mod association_series_state_loop; -mod batcher; mod config; mod dicomrs_settings; mod enums; @@ -13,7 +9,7 @@ mod listener_tcp_loop; mod pacs_file; mod patient_age; mod private_sop_uids; -mod registerer; +mod notifier; mod registration_synchronizer; mod run_everything; mod sanitize; diff --git a/src/registerer.rs b/src/notifier.rs similarity index 89% rename from src/registerer.rs rename to src/notifier.rs index 1697bbc..ea66463 100644 --- a/src/registerer.rs +++ b/src/notifier.rs @@ -5,8 +5,6 @@ use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::task::JoinHandle; -use crate::batcher::Batcher; -use crate::chrisdb_client::{CubePostgresClient, PacsFileDatabaseError}; use crate::error::HandleLoopError; use crate::pacs_file::PacsFileRegistrationRequest; @@ -14,24 +12,20 @@ use crate::pacs_file::PacsFileRegistrationRequest; /// /// - Received `Some`: add item to the batch. When batch is full, give everything to the `client` /// - Received `None`: flush current batch to the `client` -pub async fn cube_pacsfile_registerer( +pub async fn cube_pacsfile_notifier( mut receiver: UnboundedReceiver>, - client: CubePostgresClient, - batch_size: usize, + celery: Arc, ) -> Result<(), HandleLoopError> { // We have two loops: // 1. The receiver loop receives DICOM metadata from the receiver, and adds them to a batch. // When the batch is full, we create a task to send the DICOM metadata to the database. // 2. The joiner_loop simply blocks until every task is complete. - let client = Arc::new(client); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let receiver_loop = async { - let mut batches = Batcher::new(batch_size); while let Some(event) = receiver.recv().await { - batches = handle_event(event, batches, &client, &tx).unwrap(); + handle_event(event, &celery, &tx).unwrap(); } drop(tx); - flush_to_database(batches, client).await }; // join tasks and take note of any errors. @@ -64,8 +58,7 @@ type RegistrationTask = JoinHandle>; /// Returns the batch's next state. fn handle_event( event: Option, - prev: Batcher, - client: &Arc, + client: &Arc, tx: &UnboundedSender, ) -> Result, SendError> { let (next, full_batch) = match event { diff --git a/src/registration_synchronizer.rs b/src/registration_synchronizer.rs index 0793188..1891528 100644 --- a/src/registration_synchronizer.rs +++ b/src/registration_synchronizer.rs @@ -10,7 +10,7 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::task::JoinHandle; /// `registration_synchronizer` is intended as a way to synchronize requests before they are -/// sent to [crate::registerer::cube_pacsfile_registerer]. It guarantees that the "flush" command +/// sent to [crate::notifier::cube_pacsfile_notifier]. It guarantees that the "flush" command /// can be invoked after all tasks for an association are complete. pub(crate) async fn registration_synchronizer( mut receiver: UnboundedReceiver<(SeriesKeySet, PendingRegistration)>, diff --git a/src/registration_task.rs b/src/registration_task.rs index b27e649..1aa2b63 100644 --- a/src/registration_task.rs +++ b/src/registration_task.rs @@ -9,7 +9,7 @@ use std::num::NonZeroUsize; /// A function stub with the same signature as the `register_pacs_series` celery task /// in *CUBE*'s Python code. #[celery::task(name = "pacsfiles.tasks.register_pacs_series")] -fn register_pacs_series( +pub(crate) fn register_pacs_series( patient_id: String, patient_name: String, study_date: String, diff --git a/src/run_everything.rs b/src/run_everything.rs index 7e8c91b..e24505d 100644 --- a/src/run_everything.rs +++ b/src/run_everything.rs @@ -1,12 +1,10 @@ use crate::association_series_state_loop::association_series_state_loop; -use crate::chrisdb_client::CubePostgresClient; use crate::get_config; -use sqlx::postgres::PgPoolOptions; use std::net::{Ipv4Addr, SocketAddrV4}; use tokio::sync::mpsc; use crate::listener_tcp_loop::dicom_listener_tcp_loop; -use crate::registerer::cube_pacsfile_registerer; +use crate::notifier::cube_pacsfile_notifier; use crate::registration_synchronizer::registration_synchronizer; use crate::settings::OxidicomEnvOptions; use futures::FutureExt; @@ -29,21 +27,15 @@ pub async fn run_everything_from_env(finite_connections: Option) -> anyho /// 3. A database connection pool which registers written files async fn run_everything( OxidicomEnvOptions { - db, - files_root, - scp, - scp_max_pdu_length, - pacs_address, - listener_threads, - listener_port, + amqp_address, files_root, progress_nats_address, progress_interval, scp, scp_max_pdu_length, pacs_address, listener_threads, listener_port }: OxidicomEnvOptions, finite_connections: Option, ) -> anyhow::Result<()> { - let db_pool = PgPoolOptions::new() - .max_connections(db.pool.get()) - .connect(&db.connection) - .await?; - let cubedb_client = CubePostgresClient::new(db_pool, None); + let celery = celery::app!( + broker = AMQPBroker { amqp_address }, + tasks = [crate::registration_task::register_pacs_series], + task_routes = [ "pacsfiles.tasks.register_pacs_series" => "main2" ], + ).await?; let (tx_association, rx_association) = mpsc::unbounded_channel(); let (tx_storetasks, rx_storetasks) = mpsc::unbounded_channel(); @@ -64,7 +56,7 @@ async fn run_everything( association_series_state_loop(rx_association, tx_storetasks, files_root) .map(|r| r.unwrap()), registration_synchronizer(rx_storetasks, tx_register), - cube_pacsfile_registerer(rx_register, cubedb_client, db.batch_size.get()) + cube_pacsfile_notifier(rx_register, celery) )?; listener_handle.await? } diff --git a/src/settings.rs b/src/settings.rs index 8056a9c..b952760 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,12 +4,15 @@ use crate::DicomRsSettings; use camino::Utf8PathBuf; use serde::Deserialize; use std::collections::HashMap; -use std::num::{NonZeroU32, NonZeroUsize}; +use std::num::NonZeroUsize; #[derive(Debug, Deserialize)] pub struct OxidicomEnvOptions { - pub db: DatabaseOptions, + pub amqp_address: String, pub files_root: Utf8PathBuf, + pub progress_nats_address: String, + #[serde(with = "humantime_serde")] + pub progress_interval: std::time::Duration, pub scp: DicomRsSettings, #[serde(default)] pub scp_max_pdu_length: usize, @@ -21,22 +24,7 @@ pub struct OxidicomEnvOptions { pub listener_port: u16, } -#[derive(Debug, Deserialize)] -pub struct DatabaseOptions { - pub connection: String, - #[serde(default = "default_pool_size")] - pub pool: NonZeroU32, - #[serde(default = "default_batch_size")] - pub batch_size: NonZeroUsize, -} -fn default_pool_size() -> NonZeroU32 { - NonZeroU32::new(10).unwrap() -} - -fn default_batch_size() -> NonZeroUsize { - NonZeroUsize::new(20).unwrap() -} fn default_listener_threads() -> NonZeroUsize { NonZeroUsize::new(8).unwrap() From 1a3d81803a315762aa50dec54b22cb32a7e0e059 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Fri, 6 Sep 2024 23:53:17 -0400 Subject: [PATCH 04/44] Working with CUBE version 6 --- .github/workflows/ci.yml | 98 +- Cargo.lock | 1104 ++++++----------- Cargo.toml | 36 +- Dockerfile | 12 +- README.md | 30 +- docker-compose.yml | 127 +- get_data.sh | 4 +- justfile | 50 - monitoring/docker-compose.yml | 22 + .../otel-collector-config.yaml | 0 orthanc.json | 2 +- run.sh | 15 - src/association_series_state_loop.rs | 242 +--- src/config.rs | 13 - src/dicomrs_settings.rs | 24 +- src/enums.rs | 27 +- src/findscu.rs | 306 ----- src/lib.rs | 16 +- src/listener_tcp_loop.rs | 29 +- src/main.rs | 30 +- src/notifier.rs | 130 +- src/pacs_file.rs | 216 ++-- src/private_sop_uids.rs | 2 +- src/registration_synchronizer.rs | 104 -- src/registration_task.rs | 71 +- src/run_everything.rs | 43 +- src/sanitize.rs | 10 +- src/scp.rs | 12 +- src/series_key_set.rs | 91 -- src/series_synchronizer.rs | 162 +++ src/settings.rs | 27 +- src/thread_pool.rs | 2 +- src/types.rs | 139 +++ tests/assertions/expected.rs | 44 + tests/assertions/mod.rs | 169 +-- tests/assertions/model.rs | 22 + tests/integration_test.rs | 75 +- tests/orthanc_client/mod.rs | 1 + 38 files changed, 1282 insertions(+), 2225 deletions(-) delete mode 100644 justfile create mode 100644 monitoring/docker-compose.yml rename otel-collector-config.yaml => monitoring/otel-collector-config.yaml (100%) delete mode 100755 run.sh delete mode 100644 src/config.rs delete mode 100644 src/findscu.rs delete mode 100644 src/registration_synchronizer.rs delete mode 100644 src/series_key_set.rs create mode 100644 src/series_synchronizer.rs create mode 100644 src/types.rs create mode 100644 tests/assertions/expected.rs create mode 100644 tests/assertions/model.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7e34b4..bfbabdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,3 @@ -# On push: build latest images - name: CI on: @@ -11,104 +9,22 @@ on: - '.github/**' - '**.rs' - 'Cargo.*' - - 'justfile' - 'Dockerfile' - 'docker-compose.yml' - - 'run.sh' pull_request: branches: [ master ] jobs: test: name: Test - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: FNNDSC/miniChRIS-docker@master - - uses: taiki-e/install-action@v2 - with: - tool: just - - name: Start Orthanc - run: docker compose up -d orthanc + - name: Start services + run: docker compose up -d - name: Download example data - run: docker compose up get-data + run: docker compose run --rm get-data - name: Compile test binary - run: just test --no-run - - name: Integration test - run: just test - build: - name: Build - runs-on: ubuntu-22.04 - steps: - - name: Decide image tags - id: info - shell: python - run: | - import os - import itertools - - def join_tag(t): - registry, repo, tag = t - return f'{registry}/{repo}:{tag}'.lower() - - registries = ['docker.io', 'ghcr.io'] - repos = ['${{ github.repository }}'.lower()] - if '${{ github.ref_type }}' == 'branch': - tags = ['latest'] - elif '${{ github.ref_type }}' == 'tag': - tag = '${{ github.ref_name }}' - version = tag[1:] if tag.startswith('v') else tag - tags = ['latest', version] - else: - tags = [] - - product = itertools.product(registries, repos, tags) - tags_csv = ','.join(map(join_tag, product)) - outputs = { - 'tags_csv' : tags_csv, - } - with open(os.environ['GITHUB_OUTPUT'], 'a') as out: - for k, v in outputs.items(): - out.write(f'{k}={v}\n') - - uses: FNNDSC/miniChRIS-docker@master # need to run CUBE for sqlx to do compile-time validation of SQL queries - - uses: docker/setup-buildx-action@v3 - with: - # builder needs to be able to see the Postgres database running in the minichris-local network, - # so that the sqlx crate can do compile-time verification of SQL commands. - driver-opts: network=minichris-local - - name: Login to DockerHub - if: github.event_name == 'push' || github.event_name == 'release' - id: dockerhub_login - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Login to GitHub Container Registry - if: github.event_name == 'push' || github.event_name == 'release' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Get database IP - id: read-network - # sqlx crate needs the environment variable DATABASE_URL to be set for compile-time validation of SQL commands. - # For unknown reasons, docker container service name DNS doesn't work inside the build, so we need to get the - # database container's IP address. - run: | - db_container_name=$(docker ps -f 'label=com.docker.compose.service=db' --format '{{ .Names }}') - ip_address_with_subnet=$(docker network inspect minichris-local --format "{{ range .Containers }}{{ if (eq .Name \"$db_container_name\") }}{{ .IPv4Address }}{{ end }}{{ end }}") - ip_address="${ip_address_with_subnet%/*}" - database_url="postgresql://chris:chris1234@$ip_address:5432/chris" - echo "db_container_name=$db_container_name ip_address=$ip_address database_url=$database_url" - docker run --rm --network minichris-local docker.io/library/postgres:16 psql "$database_url" -c 'SELECT 1 + 1' - echo "DATABASE_URL=$database_url" >> "$GITHUB_OUTPUT" - - name: Build image - uses: docker/build-push-action@v5 - id: docker_build - with: - tags: ${{ steps.info.outputs.tags_csv }} - push: ${{ steps.dockerhub_login.outcome == 'success' }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: DATABASE_URL=${{ steps.read-network.outputs.DATABASE_URL }} + run: cargo test --no-run + - name: Run tests + run: cargo test diff --git a/Cargo.lock b/Cargo.lock index f153fe3..ccfb439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" [[package]] name = "arc-swap" @@ -347,6 +347,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -355,18 +366,17 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "http", + "http-body", + "http-body-util", "itoa", "matchit", "memchr", @@ -375,7 +385,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 1.0.1", "tower", "tower-layer", "tower-service", @@ -383,17 +393,20 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", + "http", + "http-body", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", ] @@ -531,9 +544,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "camino" -version = "1.1.7" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] @@ -600,38 +613,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chris" -version = "0.5.0-a.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7beb2649ef5f7b7f5b8b6e3973cd1f4fbd9b04910b1c0341f8ab7500457fefb4" -dependencies = [ - "aliri_braid", - "anyhow", - "async-stream", - "async-trait", - "bytes", - "camino", - "console", - "fake", - "fs-err", - "futures", - "itertools 0.12.1", - "reqwest 0.11.26", - "reqwest-middleware", - "serde", - "serde_json", - "serde_urlencoded", - "serde_with", - "shrinkwraprs", - "thiserror", - "time", - "tokio", - "tokio-util", - "trust-dns-resolver", - "uuid", -] - [[package]] name = "chrono" version = "0.4.35" @@ -644,7 +625,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -657,6 +638,45 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive", + "clap_lex", + "indexmap 1.9.3", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cms" version = "0.2.3" @@ -702,19 +722,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -802,41 +809,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.52", -] - -[[package]] -name = "darling_macro" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.52", -] - [[package]] name = "data-encoding" version = "2.5.0" @@ -888,7 +860,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -900,17 +871,11 @@ dependencies = [ "cipher", ] -[[package]] -name = "deunicode" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" - [[package]] name = "dicom" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ce3ab4fff819ddbe45480236178f1554b495d234a4cf7a8b4fb290f55ac2a1" +checksum = "e3d6cec19c540ab79e4397ea698a57cd3b44c7cc1cb0babd634db4b2e9b8777a" dependencies = [ "dicom-core", "dicom-dictionary-std", @@ -925,12 +890,12 @@ dependencies = [ [[package]] name = "dicom-core" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19a8e43b528a3c896f5046cf91a24ce4e7bbddbaf4cb3a48c4b432ec2c3c0" +checksum = "500c25f05161cedd6e274980d1bbc9cd2f5edfe52da6c3e8fd8c8d37cd4f7bce" dependencies = [ "chrono", - "itertools 0.12.1", + "itertools", "num-traits", "safe-transmute", "smallvec", @@ -949,25 +914,27 @@ dependencies = [ [[package]] name = "dicom-dump" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb468f12bd755d94ca026b782b7af502c2217410eea847019454617cdb9682d" +checksum = "79b98a367df98145f88a7c20609ee832ff349fc77fd49210e888433499cb8721" dependencies = [ "dicom-core", "dicom-dictionary-std", "dicom-encoding", + "dicom-json", "dicom-object", "dicom-transfer-syntax-registry", "owo-colors", + "serde_json", "snafu", "terminal_size", ] [[package]] name = "dicom-encoding" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f2557317d34adb46e5fe6488733b06eae106848b993b06a157424460dd6b85" +checksum = "4a8e32b29019487bcd1fced6a66c7ced056470aa1af3c523f13f3263972880b9" dependencies = [ "byteordered", "dicom-core", @@ -977,11 +944,27 @@ dependencies = [ "snafu", ] +[[package]] +name = "dicom-json" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f9e37c472e15178efcbbd17413bb36414c9734b4663731b1b3955474780e81" +dependencies = [ + "base64 0.22.1", + "dicom-core", + "dicom-dictionary-std", + "dicom-object", + "num-traits", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "dicom-object" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec39019e8a985d90f92d0e67667d8a4dfe8c5f31e597cbbbeb683ad8d675eb3" +checksum = "34257e682886305269e15eca257ddb38b3d059f3a7be40603d20d86c8afff507" dependencies = [ "byteordered", "dicom-core", @@ -989,7 +972,7 @@ dependencies = [ "dicom-encoding", "dicom-parser", "dicom-transfer-syntax-registry", - "itertools 0.12.1", + "itertools", "smallvec", "snafu", "tracing", @@ -997,9 +980,9 @@ dependencies = [ [[package]] name = "dicom-parser" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e31319f54653057364e38e6aeb743fbd479bcef6fe72a5f8acc26772008ca2" +checksum = "8ba2c04664fd806b65e77007ee056712ef39f3c2ec996cad92a0d33a671bc710" dependencies = [ "chrono", "dicom-core", @@ -1012,9 +995,9 @@ dependencies = [ [[package]] name = "dicom-pixeldata" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986238212dbeb64316f0d742330dee8b19c8e1e47da6293aff08a6149b9a0044" +checksum = "28d85d2bb4695a4231f401ed6c567bca1aef6e1cf1355c919ab6d1f59cd8b60e" dependencies = [ "byteorder", "dicom-core", @@ -1031,9 +1014,9 @@ dependencies = [ [[package]] name = "dicom-transfer-syntax-registry" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b2d4b093621ee6faa1260259ad45e98882f38803ead77ca284fa520e907ae" +checksum = "a3611d21f7ee5cb7faf2945b4223589bcfd3b7dd58d40b755b507c602c0ac4a9" dependencies = [ "byteordered", "dicom-core", @@ -1046,9 +1029,9 @@ dependencies = [ [[package]] name = "dicom-ul" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e159b195d023e6286c03136190a6a9b95a8db7f3f9336e51e2a09b6ca0ce7d" +checksum = "95fbdecc082dc74972b556d06df60ac31184ad6acd88684fb2a63581019087a0" dependencies = [ "byteordered", "dicom-encoding", @@ -1091,12 +1074,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoding" version = "0.2.33" @@ -1170,18 +1147,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-as-inner" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.52", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -1250,16 +1215,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "fake" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c25829bde82205da46e1823b2259db6273379f626fc211f126f65654a2669be" -dependencies = [ - "deunicode", - "rand", -] - [[package]] name = "fastrand" version = "1.9.0" @@ -1535,25 +1490,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "h2" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.2.5", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.5" @@ -1565,7 +1501,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http", "indexmap 2.2.5", "slab", "tokio", @@ -1601,6 +1537,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1639,17 +1584,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.1.0" @@ -1661,17 +1595,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.0" @@ -1679,7 +1602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -1690,8 +1613,8 @@ checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", - "http 1.1.0", - "http-body 1.0.0", + "http", + "http-body", "pin-project-lite", ] @@ -1723,30 +1646,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hyper" -version = "0.14.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.24", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.6", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.3.1" @@ -1756,10 +1655,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1769,28 +1669,32 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 0.2.12", - "hyper 0.14.28", - "rustls 0.21.10", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", ] [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper 0.14.28", + "hyper", + "hyper-util", "pin-project-lite", "tokio", - "tokio-io-timeout", + "tower-service", ] [[package]] @@ -1801,7 +1705,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.3.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -1818,9 +1722,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http", + "http-body", + "hyper", "pin-project-lite", "socket2 0.5.6", "tokio", @@ -1852,22 +1756,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.5.0" @@ -1914,7 +1802,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -1925,7 +1812,6 @@ checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", - "serde", ] [[package]] @@ -1970,18 +1856,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.6", - "widestring", - "windows-sys 0.48.0", - "winreg 0.50.0", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -2007,27 +1881,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -2103,14 +1959,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" @@ -2136,15 +1986,6 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "match_cfg" version = "0.1.0" @@ -2169,16 +2010,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2197,13 +2028,24 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "names" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc" +dependencies = [ + "clap", + "rand", ] [[package]] @@ -2277,16 +2119,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - [[package]] name = "object" version = "0.32.2" @@ -2357,9 +2189,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" +checksum = "4c365a63eec4f55b7efeceb724f1336f26a9cf3427b70e59e2cd2a5b947fba96" dependencies = [ "futures-core", "futures-sink", @@ -2371,13 +2203,13 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94c69209c05319cdf7460c6d4c055ed102be242a0a6245835d7bc42c6ec7f54" +checksum = "6b925a602ffb916fb7421276b86756027b37ee708f9dce2dbdcc51739f07e727" dependencies = [ "async-trait", "futures-core", - "http 0.2.12", + "http", "opentelemetry", "opentelemetry-proto", "opentelemetry_sdk", @@ -2389,9 +2221,9 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984806e6cf27f2b49282e2a05e288f30594f3dbc74eb7a6e99422bc48ed78162" +checksum = "30ee9f20bff9c984511a02f082dc8ede839e4a9bf15cc2487c8d6fea5ad850d9" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -2401,39 +2233,36 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1869fb4bb9b35c5ba8a1e40c9b128a7b4c010d07091e864a29da19e4fe2ca4d7" +checksum = "1cefe0543875379e47eb5f1e68ff83f45cc41366a92dfd0d073d513bf68e9a05" [[package]] name = "opentelemetry_sdk" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" +checksum = "692eac490ec80f24a17828d49b40b60f5aeaccdfe6a503f939713afd22bc28df" dependencies = [ "async-trait", "futures-channel", "futures-executor", "futures-util", - "lazy_static", + "glob", "once_cell", "opentelemetry", - "ordered-float", "percent-encoding", "rand", + "serde_json", "thiserror", "tokio", "tokio-stream", ] [[package]] -name = "ordered-float" -version = "4.2.0" +name = "os_str_bytes" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" -dependencies = [ - "num-traits", -] +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "overload" @@ -2458,25 +2287,28 @@ dependencies = [ "anyhow", "camino", "celery", - "chris", "dicom", "figment", "fs-err", "futures", "humantime-serde", + "names", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", "regex", - "reqwest 0.12.4", + "reqwest", "rstest", "seahash", "serde", + "serde_json", "snafu", + "tempfile", "thiserror", "time", "tokio", + "tokio-stream", "tracing", "tracing-subscriber", "ulid", @@ -2738,6 +2570,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -2762,9 +2618,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" dependencies = [ "bytes", "prost-derive", @@ -2772,23 +2628,17 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools", "proc-macro2", "quote", "syn 2.0.52", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.35" @@ -2901,9 +2751,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -2936,64 +2786,21 @@ checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" [[package]] name = "reqwest" -version = "0.11.26" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.24", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", - "hyper-rustls", - "ipnet", - "js-sys", - "log", - "mime", - "mime_guess", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.10", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-rustls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", - "winreg 0.50.0", -] - -[[package]] -name = "reqwest" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.3.1", + "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -3004,11 +2811,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -3017,32 +2824,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.52.0", -] - -[[package]] -name = "reqwest-middleware" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" -dependencies = [ - "anyhow", - "async-trait", - "http 0.2.12", - "reqwest 0.11.26", - "serde", - "task-local-extensions", - "thiserror", -] - -[[package]] -name = "resolv-conf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error", + "windows-registry", ] [[package]] @@ -3062,9 +2844,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" dependencies = [ "futures", "futures-timer", @@ -3074,9 +2856,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" dependencies = [ "cfg-if", "glob", @@ -3141,18 +2923,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.12" @@ -3162,7 +2932,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.7", + "rustls-webpki", "subtle", "zeroize", ] @@ -3174,10 +2944,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a980454b497c439c274f2feae2523ed8138bbd3d323684e1435fec62f800481" dependencies = [ "log", - "rustls 0.23.12", + "rustls", "rustls-native-certs", "rustls-pki-types", - "rustls-webpki 0.102.7", + "rustls-webpki", ] [[package]] @@ -3187,21 +2957,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -3218,16 +2979,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.102.7" @@ -3301,16 +3052,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "seahash" version = "4.1.0" @@ -3348,18 +3089,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -3368,11 +3109,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ + "indexmap 2.2.5", "itoa", + "memchr", "ryu", "serde", ] @@ -3389,36 +3132,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" -dependencies = [ - "base64 0.21.7", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.2.5", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.52", -] - [[package]] name = "sha1" version = "0.10.6" @@ -3456,19 +3169,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shrinkwraprs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e6744142336dfb606fe2b068afa2e1cca1ee6a5d8377277a92945d81fa331" -dependencies = [ - "bitflags 1.3.2", - "itertools 0.8.2", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3501,18 +3201,18 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "snafu" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75976f4748ab44f6e5332102be424e7c2dc18daeaf7e725f2040c3ebb133512e" +checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b19911debfb8c2fb1107bc6cb2d61868aaf53a988449213959bb1b5b1ed95f" +checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" dependencies = [ "heck", "proc-macro2", @@ -3609,6 +3309,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -3622,34 +3331,25 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] -[[package]] -name = "task-local-extensions" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" -dependencies = [ - "pin-utils", -] - [[package]] name = "tcp-stream" version = "0.28.0" @@ -3659,19 +3359,29 @@ dependencies = [ "cfg-if", "p12-keystore", "rustls-connector", - "rustls-pemfile 2.1.2", + "rustls-pemfile", ] [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand 2.1.0", + "once_cell", "rustix 0.38.34", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", ] [[package]] @@ -3684,20 +3394,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -3773,21 +3489,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.5.6", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3801,21 +3516,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -3848,19 +3553,20 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.21.10", + "rustls", + "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -3899,23 +3605,26 @@ dependencies = [ [[package]] name = "tonic" -version = "0.11.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.7", + "base64 0.22.1", "bytes", - "h2 0.3.24", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", "prost", + "socket2 0.5.6", "tokio", "tokio-stream", "tower", @@ -4013,59 +3722,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "trust-dns-proto" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna 0.4.0", - "ipnet", - "once_cell", - "rand", - "rustls 0.21.10", - "rustls-pemfile 1.0.4", - "rustls-webpki 0.101.7", - "smallvec", - "thiserror", - "tinyvec", - "tokio", - "tokio-rustls", - "tracing", - "url", -] - -[[package]] -name = "trust-dns-resolver" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6" -dependencies = [ - "cfg-if", - "futures-util", - "ipconfig", - "lru-cache", - "once_cell", - "parking_lot", - "rand", - "resolv-conf", - "rustls 0.21.10", - "smallvec", - "thiserror", - "tokio", - "tokio-rustls", - "tracing", - "trust-dns-proto", - "webpki-roots", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -4080,9 +3736,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ulid" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259" +checksum = "04f903f293d11f31c0c29e4148f6dc0d033a7f80cebc0282bea147611667d289" dependencies = [ "getrandom", "rand", @@ -4098,15 +3754,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4128,12 +3775,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "untrusted" version = "0.9.0" @@ -4147,7 +3788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna", "percent-encoding", ] @@ -4275,19 +3916,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "wasm-streams" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "web-sys" version = "0.3.69" @@ -4308,24 +3936,12 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "weezl" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - [[package]] name = "winapi" version = "0.3.9" @@ -4363,7 +3979,37 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -4381,7 +4027,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4401,17 +4056,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4422,9 +4078,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4434,9 +4090,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4446,9 +4102,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4458,9 +4120,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4470,9 +4132,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4482,9 +4144,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4494,9 +4156,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -4507,26 +4169,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "x509-cert" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index ef3f907..058dbec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,33 +7,35 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dicom = "0.7.0" -snafu = "0.8.2" -thiserror = "1.0.61" -camino = { version = "1.1.7", features = ["serde1"] } +dicom = "0.7.1" +snafu = "0.8.4" +thiserror = "1.0.63" +camino = { version = "1.1.9", features = ["serde1"] } fs-err = { version = "2.11.0", features = ["tokio"] } -serde = { version = "1.0.203", features = ["derive"] } +serde = { version = "1.0.210", features = ["derive"] } seahash = { version = "4.1.0", features = ["use_std"] } -regex = "1.10.4" -opentelemetry = { version = "0.23.0", features = ["metrics"] } -opentelemetry_sdk = { version = "0.23.0", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.16.0", features = ["trace", "grpc-tonic"] } -opentelemetry-semantic-conventions = "0.15.0" +regex = "1.10.6" +opentelemetry = { version = "0.24.0", features = ["metrics"] } +opentelemetry_sdk = { version = "0.24.1", features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.17.0", features = ["trace", "grpc-tonic"] } +opentelemetry-semantic-conventions = "0.16.0" tracing = "0.1.40" tracing-subscriber = "0.3.18" aliri_braid = "0.4.0" -anyhow = "1.0.86" -tokio = { version = "1.38.0", features = ["full"] } +anyhow = "1.0.87" +tokio = { version = "1.40.0", features = ["full"] } futures = "0.3.30" time = { version = "0.3.36", features = ["macros", "parsing"] } -ulid = "1.1.2" +ulid = "1.1.3" figment = { version = "0.10.19", features = ["env"] } celery = "0.5.5" humantime-serde = "1.1.1" [dev-dependencies] -rstest = "0.21.0" +rstest = "0.22.0" walkdir = "2.5.0" -chris = { version = "0.5.0-a.1", features = ["rustls"], default-features = false } -tokio = { version = "1.36.0", features = ["rt"] } -reqwest = { version = "0.12.4", features = ["json"] } +reqwest = { version = "0.12.7", features = ["json"] } +tempfile = "3.12.0" +names = "0.14.0" +serde_json = "1.0.128" +tokio-stream = { version = "0.1.16", features = ["fs"] } diff --git a/Dockerfile b/Dockerfile index 53cecbd..c9aa1f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -# important: must be running miniChRIS-docker and in the minichris-local network +# TODO how do I build for ARM? -FROM docker.io/lukemathwalker/cargo-chef:0.1.66-rust-1.78-alpine3.18 AS chef +FROM docker.io/lukemathwalker/cargo-chef:0.1.67-rust-1.81.0-alpine3.19 AS chef WORKDIR /app ARG CARGO_TERM_COLOR=always @@ -13,18 +13,10 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --locked --target x86_64-unknown-linux-musl --recipe-path recipe.json COPY . . -# need to set DATABASE_URL for sqlx crate to do compile-time validation of SQL commands -ARG DATABASE_URL RUN cargo build --release --locked --target x86_64-unknown-linux-musl FROM scratch COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/oxidicom /app/oxidicom -LABEL org.opencontainers.image.authors="Jennings Zhang , FNNDSC " \ - org.opencontainers.image.url="https://github.com/FNNDSC/oxidicom" \ - org.opencontainers.image.licenses="MIT" \ - org.opencontainers.image.title="oxidicom" \ - org.opencontainers.image.description="DICOM file receiver for ChRIS backend" - EXPOSE 11111 CMD ["/app/oxidicom"] diff --git a/README.md b/README.md index beb70eb..a0ca379 100644 --- a/README.md +++ b/README.md @@ -11,30 +11,28 @@ Documentation: https://chrisproject.org/docs/oxidicom ## Development -The development scripts are hard-coded to work with an instance of _miniChRIS_. -Follow these instructions to spin up the backend: -https://github.com/FNNDSC/miniChRIS-docker#readme +You'll need: [Docker Compose](https://docs.docker.com/compose/) and [Rust](https://rustup.rs/). -To speak to _CUBE_, `oxidicom` needs to run in a Docker container in the same network and mounting -the same volume as _CUBE_'s container. This is coded up in `./docker-compose.yml`. +Start [RabbitMQ](https://hub.docker.com/_/rabbitmq) and [Orthanc](https://www.orthanc-server.com/) +services for testing, then download test data: -You need to have installed: - -- Docker Compose -- https://github.com/casey/just +```shell +docker compose run --rm get-data +``` -Simply run +Run all tests: ```shell -just test +cargo test ``` -The `just` command, without arguments, will: +Clean up: + +```shell +docker compose down -v +``` -1. Run Orthanc -2. Download sample data -3. Push sample data into Orthanc -4. Run unit and integration tests +## Notes ### Usage of `opentelemetry` v.s. `tracing` in the codebase diff --git a/docker-compose.yml b/docker-compose.yml index fa075b7..c8db7ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,128 +1,33 @@ -# You *must* use `just` to run docker compose commands. -# Running docker compose commands directly is not recommended. - services: + rabbitmq: + image: docker.io/library/rabbitmq:3 + ports: + - "5672:5672" orthanc: image: docker.io/jodogne/orthanc-plugins:1.12.3 volumes: - ./orthanc.json:/etc/orthanc/orthanc.json:ro - orthanc:/var/lib/orthanc/db - ports: - - "4242:4242" - - "8042:8042" - networks: - test-oxidicom: + network_mode: "host" + healthcheck: + test: ["CMD", "wget", "-O", "/dev/null", "http://localhost:8042/patients"] + interval: 2s + timeout: 4s + retries: 3 + start_period: 30s get-data: - build: - dockerfile_inline: | - FROM docker.io/library/alpine:latest - RUN apk add parallel curl jq bash + image: ghcr.io/fnndsc/utils:7c65939 command: /get_data.sh - tty: true - attach: true volumes: - ./get_data.sh:/get_data.sh:ro depends_on: orthanc: + condition: service_healthy + rabbitmq: condition: service_started - networks: - test-oxidicom: profiles: - - oxidicom - oxidicom: - image: docker.io/library/rust:1.78-bookworm - user: 1001:0 - group_add: - - ${GID-0} - volumes: &RUST_VOLUMES - - cargo-home:/cargo - - cargo-target:/target - - ./:/src:ro - - minichris-files:/data:rw - working_dir: /src - environment: - OXIDICOM_FILES_ROOT: /data - OXIDICOM_DB_CONNECTION: postgresql://chris:chris1234@db:5432/chris - - OXIDICOM_SCP_AET: OXIDICOMTEST - OXIDICOM_PACS_ADDRESS: '{OXITESTORTHANC="orthanc:4242"}' - OXIDICOM_LISTENER_PORT: 11112 - OXIDICOM_LISTENER_THREADS: 8 - OXIDICOM_VERBOSE: "yes" - OTEL_EXPORTER_OTLP_PROTOCOL: grpc - OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317" - OTEL_RESOURCE_ATTRIBUTES: service.name=oxidicom-test - - OXIDICOM_TEST_URL: http://chris:8000/api/v1/ - OXIDICOM_TEST_USERNAME: chris - OXIDICOM_TEST_PASSWORD: chris1234 - - CARGO_TARGET_DIR: /target - CARGO_HOME: /cargo - CARGO_TERM_COLOR: always - # RUST_BACKTRACE: full - - # DATABASE_URL must be set for the sqlx crate to validate queries at compile time. - # see https://docs.rs/sqlx/latest/sqlx/macro.query.html#requirements - DATABASE_URL: "postgresql://chris:chris1234@db:5432/chris" - depends_on: - orthanc: - condition: service_started - rust-target-dir-permissions: - condition: service_completed_successfully - get-data: - condition: service_completed_successfully - profiles: - - oxidicom - networks: - minichris-local: - test-oxidicom: - rust-target-dir-permissions: - image: docker.io/library/rust:1.78-bookworm - volumes: *RUST_VOLUMES - command: chmod g+rwx /target /cargo - profiles: - - oxidicom - networks: - test-oxidicom: - - openobserve: - image: public.ecr.aws/zinclabs/openobserve:v0.10.6-rc1 - environment: - ZO_ROOT_USER_EMAIL: dev@babymri.org - ZO_ROOT_USER_PASSWORD: chris1234 - ZO_DATA_DIR: /data - ports: - - "5080:5080" - volumes: - - openobserve:/data - restart: unless-stopped - profiles: - - observe - networks: - test-oxidicom: - otel-collector: - image: docker.io/otel/opentelemetry-collector:0.101.0 - restart: unless-stopped - command: ["--config=/etc/otel-collector-config.yaml"] - volumes: - - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro - profiles: - - observe - networks: - test-oxidicom: + - tools + network_mode: "host" # so that Orthanc in a container can send data out volumes: orthanc: - openobserve: - cargo-target: - cargo-home: - minichris-files: - external: true - name: minichris-files - -networks: - test-oxidicom: - minichris-local: - external: true - name: minichris-local diff --git a/get_data.sh b/get_data.sh index 6a5ce4a..022b262 100755 --- a/get_data.sh +++ b/get_data.sh @@ -6,7 +6,7 @@ GITHUB_TARBALLS=( https://api.github.com/repos/datalad/example-dicom-structural/tarball/f077bcc8d502ce8155507bd758cb3b7ccc887f40 ) -until instances=$(curl -sf http://orthanc:8042/instances); do +until instances=$(curl -sf http://localhost:8042/instances); do printf . sleep 1 done @@ -26,7 +26,7 @@ for url in "${GITHUB_TARBALLS[@]}"; do done find -type f -iname '*.dcm' \ - | parallel --progress -j 4 "curl -sfX POST http://orthanc:8042/instances -H Expect: -H 'Content-Type: application/dicom' --data-binary @'{}' -o /dev/null" + | parallel --progress -j 4 "curl -sfX POST http://localhost:8042/instances -H Expect: -H 'Content-Type: application/dicom' --data-binary @'{}' -o /dev/null" cd / rm -rf $tmpdir diff --git a/justfile b/justfile deleted file mode 100644 index 824672b..0000000 --- a/justfile +++ /dev/null @@ -1,50 +0,0 @@ -# Run `cargo check` -check: - ./run.sh cargo check - -# Run `cargo clippy` -clippy: - ./run.sh cargo clippy - -# Run tests. -# -# Examples: -# -# Run all tests, including integration tests: -# -# just test -# -# Run a specific unit test: -# -# just test chrisdb_client::tests::test_query_for_existing -# -test test_name="": reset - test_name={{test_name}}; ./run.sh cargo test $test_name - -# Run in debug mode -run: - ./run.sh cargo run - -# Stop the run server -kill: - docker compose kill oxidicom - -# Delete all PACSFiles from CUBE -reset: - ./reset.sh - -# Start Orthanc -orthanc: - docker compose up -d - -# Start an observability stack for distributed tracing -observe: - docker compose --profile observe up -d - -# Remove all data and containers -down: - docker compose --profile observe down -v - -# Run the psql shell -psql: - docker exec -it minichris-docker-db-1 psql postgresql://chris:chris1234@localhost:5432/chris diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 0000000..dbfd33e --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,22 @@ +services: + openobserve: + image: public.ecr.aws/zinclabs/openobserve:v0.10.6-rc1 + environment: + ZO_ROOT_USER_EMAIL: dev@babymri.org + ZO_ROOT_USER_PASSWORD: chris1234 + ZO_DATA_DIR: /data + ports: + - "5080:5080" + volumes: + - openobserve:/data + restart: unless-stopped + + otel-collector: + image: docker.io/otel/opentelemetry-collector:0.101.0 + restart: unless-stopped + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro + +volumes: + openobserve: diff --git a/otel-collector-config.yaml b/monitoring/otel-collector-config.yaml similarity index 100% rename from otel-collector-config.yaml rename to monitoring/otel-collector-config.yaml diff --git a/orthanc.json b/orthanc.json index 9ffb293..0804cc6 100644 --- a/orthanc.json +++ b/orthanc.json @@ -312,7 +312,7 @@ * and the third one is the TCP port number corresponding * to the DICOM protocol on the remote modality (usually 104). **/ - "OXIDICOMTEST" : ["OXIDICOMTEST", "oxidicom", 11112 ] + "OXIDICOMTEST" : ["OXIDICOMTEST", "localhost", 11112 ] /** * A fourth parameter is available to enable patches for diff --git a/run.sh b/run.sh deleted file mode 100755 index 804ff56..0000000 --- a/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Purpose: run `cargo test` in a container, where the container has access to -# the CUBE container's network and volumes. - -HERE="$(dirname "$(readlink -f "$0")")" -cd "$HERE" - -chmod g+r Cargo.lock Cargo.toml - -if [ "$CI" = "true" ]; then - notty='-T' -fi - -export GID=$(id -g) -exec docker compose --profile oxidicom run --rm --use-aliases $notty oxidicom "$@" diff --git a/src/association_series_state_loop.rs b/src/association_series_state_loop.rs index a9288f1..fcd16a1 100644 --- a/src/association_series_state_loop.rs +++ b/src/association_series_state_loop.rs @@ -1,11 +1,8 @@ -//! Functionality related to tracking the state of series being received -//! and writing DICOM objects to files. -use crate::dicomrs_settings::{ClientAETitle, OurAETitle}; -use crate::enums::{AssociationEvent, PendingRegistration}; +use crate::dicomrs_settings::AETitle; +use crate::enums::{AssociationEvent, SeriesEvent}; use crate::error::{DicomRequiredTagError, DicomStorageError, HandleLoopError}; -use crate::findscu::FindScuParameters; -use crate::pacs_file::{BadTag, PacsFileRegistration, PacsFileRegistrationRequest}; -use crate::series_key_set::SeriesKeySet; +use crate::pacs_file::{BadTag, PacsFileRegistration}; +use crate::types::{DicomFilePath, DicomInfo, PendingDicomInstance, SeriesCount, SeriesKey}; use camino::{Utf8Path, Utf8PathBuf}; use dicom::object::DefaultDicomObject; use std::collections::HashMap; @@ -17,23 +14,15 @@ use ulid::Ulid; /// Stateful handling of [AssociationEvent]. /// -/// Most importantly, it writes received DICOM instances to files in storage. -/// It also handles the creation of "Oxidicom Custom Metadata" files -/// (`NumberOfSeriesRelatedInstances=M` and `OxidicomAttemptedPushCount=N`). -/// -/// It keeps track of the series received for each association, and handles them accordingly: -/// -/// - On the first instance of a new series, attempt to contact the PACS the instance was sent from, -/// and query for the `NumberOfSeriesRelatedInstances`. -/// - On every DICOM instance received, extract its metadata, and create a task to store the -/// instance as a DICOM file. -/// - At the end of every association, create all the `OxidicomAttemptedPushCount` files for each -/// series of the finished association, and finally send [PendingRegistration::End]. +/// - On every DICOM instance received, read its metadata (such as PatientName, Modality, ...), +/// and create a (tokio) task in which the data is written to storage as a DICOM file. +/// - At the end of every association, send a [SeriesEvent::Finish] for each series we saw +/// during the association. pub(crate) async fn association_series_state_loop( mut receiver: UnboundedReceiver, - sender: UnboundedSender<(SeriesKeySet, PendingRegistration)>, + sender: UnboundedSender<(SeriesKey, PendingDicomInstance)>, files_root: Utf8PathBuf, -) -> Result, SendError<(SeriesKeySet, PendingRegistration)>> { +) -> Result, SendError<(SeriesKey, PendingDicomInstance)>> { let mut inflight_associations: HashMap = Default::default(); let mut everything_ok = true; let files_root = Arc::new(files_root); @@ -62,39 +51,21 @@ pub(crate) async fn association_series_state_loop( /// Helper function which handles most of what [association_series_state_loop] is supposed to do. /// /// Since this function is not async, it helps to protect the invariant that -/// [PendingRegistration::End] will be the last sent message of a series (there is no async +/// [SeriesEvent::Finish] will be the last sent message of a series (there is no async /// code to cause a race condition). fn match_event( event: AssociationEvent, inflight_associations: &mut HashMap, files_root: &Arc, -) -> Result, ()> { +) -> Result, ()> { match event { - AssociationEvent::Start { - ulid, - aec, - aet, - pacs_address, - } => { - if pacs_address.is_none() { - tracing::warn!( - association_ulid = ulid.to_string(), - "OXIDICOM_PACS_ADDRESS not configured for this association." - ); - } - inflight_associations.insert(ulid, Association::new(aec, aet, pacs_address)); - Ok(Vec::with_capacity(0)) + AssociationEvent::Start { ulid, aec } => { + inflight_associations.insert(ulid, Association::new(aec)); + Ok(vec![]) } AssociationEvent::DicomInstance { ulid, dcm } => { - match receive_dicom_instance(ulid, dcm, inflight_associations, &files_root) { - Ok((series, tasks)) => { - let pending_tasks = tasks - .into_iter() - .map(PendingRegistration::Task) - .map(|task| (series.clone(), task)) - .collect(); - Ok(pending_tasks) - } + match receive_dicom_instance(ulid, dcm, inflight_associations, files_root) { + Ok((series, task)) => Ok(vec![(series, SeriesEvent::Instance(task))]), Err(e) => { tracing::error!(association_ulid = ulid.to_string(), message = e.to_string()); Err(()) @@ -105,15 +76,14 @@ fn match_event( let association = inflight_associations .remove(&ulid) .expect("Unknown association ULID"); - Ok(finish_association(ulid, association.series, &files_root)) + Ok(finish_association(association.series)) } } } /// Receive a DICOM instance. It will be taken note of in `inflight_associations`. /// -/// - On the first DICOM instance of a series received: try to ask the PACS server for the `NumberOfSeriesRelatedInstances`. -/// - For every DICOM instance received: create a task to store the DICOM instance as a file +/// For every DICOM instance received: create a task to store the DICOM instance as a file /// /// The tasks are returned. fn receive_dicom_instance( @@ -121,181 +91,61 @@ fn receive_dicom_instance( dcm: DefaultDicomObject, inflight_associations: &mut HashMap, files_root: &Arc, -) -> Result< - ( - SeriesKeySet, - Vec>>, - ), - DicomRequiredTagError, -> { +) -> Result<(SeriesKey, JoinHandle>), DicomRequiredTagError> { let association = inflight_associations .get_mut(&ulid) .expect("Unknown association ULID"); let pacs_name = association.aec.clone(); let (pacs_file, bad_tags) = PacsFileRegistration::new(pacs_name, dcm)?; - report_bad_tags(&pacs_file.request, ulid, bad_tags); - let series_key_set = SeriesKeySet::from(pacs_file.request.clone()); + report_bad_tags(&pacs_file.data, ulid, bad_tags); + let series_key = SeriesKey::new( + pacs_file.data.SeriesInstanceUID.clone(), + pacs_file.data.pacs_name.clone(), + ); + if let Some(state) = association.series.get_mut(&series_key) { + state.count += 1; + } else { + association + .series + .insert(series_key.clone(), SeriesCount::new(pacs_file.data.clone())); + } let storage_task = { let files_root = Arc::clone(files_root); - tokio::task::spawn_blocking(move || { - store_dicom(&files_root, &pacs_file).map(|_| pacs_file.request) - }) - }; - - let tasks = if let Some(count) = association.series.get_mut(&series_key_set) { - *count += 1; - vec![storage_task] - } else { - association.series.insert(series_key_set.clone(), 1); - let numrelatedinstances_task = - start_numrelatedinstances_task(ulid, series_key_set.clone(), association, files_root); - vec![storage_task, numrelatedinstances_task] + tokio::task::spawn_blocking(move || write_dicom_wotel(&files_root, &pacs_file)) }; - Ok((series_key_set, tasks)) + Ok((series_key, storage_task)) } /// Creates messages for the end of an association. -/// -/// For each series with one or more instance: -/// -/// - Create a task for creating the "Oxidicom Custom Metadata" `OxidicomAttemptedPushCount=N` file. -/// - Create a [PendingRegistration::End] fn finish_association( - ulid: Ulid, - series_counts: HashMap, - files_root: &Arc, -) -> Vec<(SeriesKeySet, PendingRegistration)> { - let mut messages = Vec::with_capacity(series_counts.len() * 2); - for (series, count) in &series_counts { - let files_root = Arc::clone(files_root); - let pacs_file = series.clone().into_oxidicom_custom_pacsfile( - ulid, - "OxidicomAttemptedPushCount", - count.to_string(), - ); - let task = - tokio::task::spawn( - async move { create_blank_file_tokio(&files_root, pacs_file).await }, - ); - messages.push((series.clone(), PendingRegistration::Task(task))); - } - let endings = series_counts + series_counts: HashMap, +) -> Vec<(SeriesKey, PendingDicomInstance)> { + series_counts .into_iter() - .map(|(series, _count)| (series, PendingRegistration::End)); - messages.extend(endings); - messages -} - -/// Create a blank file in place of the [PacsFileRegistrationRequest], and return it if successful. -/// -/// Intended to be used for creating "Oxidicom Custom Metadata" files. -fn create_blank_file( - files_root: &Utf8Path, - pacs_file: PacsFileRegistrationRequest, -) -> Result { - let path = files_root.join(&pacs_file.path); - if let Some(parent) = path.parent() { - fs_err::create_dir_all(parent).map_err(|e| { - tracing::error!(path = parent.as_str(), message = e.to_string()); - })? - } - fs_err::File::create(&path).map(|_| pacs_file).map_err(|e| { - tracing::error!(path = path.into_string(), message = e.to_string()); - }) -} - -/// Async version of [create_blank_file]. -async fn create_blank_file_tokio( - files_root: &Utf8Path, - pacs_file: PacsFileRegistrationRequest, -) -> Result { - let path = files_root.join(&pacs_file.path); - if let Some(parent) = path.parent() { - fs_err::tokio::create_dir_all(parent).await.map_err(|e| { - tracing::error!(path = parent.as_str(), message = e.to_string()); - })? - } - fs_err::tokio::File::create(&path) - .await - .map(|_| pacs_file) - .map_err(|e| { - tracing::error!(path = path.into_string(), message = e.to_string()); - }) -} - -/// Start a task for producing the "Oxidicom Custom Metadata" `NumberOfSeriesRelatedInstances=N` file. -fn start_numrelatedinstances_task( - ulid: Ulid, - series_key_set: SeriesKeySet, - association: &Association, - files_root: &Arc, -) -> JoinHandle> { - let findscu_params = maybe_findscu(ulid, &series_key_set, association); - let series_key_set = series_key_set.clone(); - let files_root = Arc::clone(files_root); - tokio::task::spawn_blocking(move || { - let value = findscu_params - .and_then(|p| p.get_number_of_series_related_instances().ok()) - .map(|n| n.to_string()) - .unwrap_or_else(|| "unknown".to_string()); - let pacs_file = series_key_set.into_oxidicom_custom_pacsfile( - ulid, - "NumberOfSeriesRelatedInstances", - value, - ); - create_blank_file(&files_root, pacs_file) - }) -} - -/// If [Association::pacs_address] is [Some], create and return [FindScuParameters]. -fn maybe_findscu( - ulid: Ulid, - series_key_set: &SeriesKeySet, - association: &Association, -) -> Option { - if let Some(pacs_address) = &association.pacs_address { - let findscu_params = FindScuParameters { - ulid, - pacs_address: pacs_address.to_string(), - aec: association.aec.clone(), - aet: association.aet.clone(), - study_instance_uid: series_key_set.StudyInstanceUID.to_string(), - series_instance_uid: series_key_set.SeriesInstanceUID.to_string(), - }; - Some(findscu_params) - } else { - None - } + .map(|(s, c)| (s, SeriesEvent::Finish(c))) + .collect() } /// Information about a DICOM "association" which is a TCP connection from a PACS server /// who is pushing DICOM files to us. struct Association { /// AE title of the PACS pushing to us - aec: ClientAETitle, - /// Our AE title - aet: OurAETitle, - /// Address where we are receiving DICOMs from - pacs_address: Option, + aec: AETitle, /// The unique series we are receiving during this association. - /// Typically, in _ChRIS_ one series will be pulled per association. However, - /// it is possible for a PACS server to push any number or fraction of a series to us. - series: HashMap, + series: HashMap, } impl Association { - fn new(aec: ClientAETitle, aet: OurAETitle, pacs_address: Option) -> Self { + fn new(aec: AETitle) -> Self { Self { aec, - aet, - pacs_address, series: Default::default(), } } } /// Wraps [write_dicom] with OpenTelemetry logging. -fn store_dicom(files_root: &Utf8Path, pacs_file: &PacsFileRegistration) -> Result<(), ()> { +fn write_dicom_wotel(files_root: &Utf8Path, pacs_file: &PacsFileRegistration) -> Result<(), ()> { match write_dicom(pacs_file, files_root) { Ok(path) => tracing::info!(event = "storage", path = path.into_string()), Err(e) => { @@ -311,7 +161,7 @@ fn write_dicom>( pacs_file: &PacsFileRegistration, files_root: P, ) -> Result { - let output_path = files_root.as_ref().join(&pacs_file.request.path); + let output_path = files_root.as_ref().join(pacs_file.data.path.as_str()); if let Some(parent_dir) = output_path.parent() { fs_err::create_dir_all(parent_dir)?; } @@ -321,7 +171,7 @@ fn write_dicom>( /// Report bad tags via OpenTelemetry. fn report_bad_tags>( - pacs_file: &PacsFileRegistrationRequest, + pacs_file: &DicomInfo, ulid: Ulid, bad_tags: T, ) { @@ -336,7 +186,7 @@ fn report_bad_tags>( .join(","); tracing::warn!( association_ulid = ulid.to_string(), - path = &pacs_file.path, + path = pacs_file.path.as_str(), bad_tags = bad_tags_csv ) } diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 3a86e30..0000000 --- a/src/config.rs +++ /dev/null @@ -1,13 +0,0 @@ -use figment::providers::Env; -use figment::Figment; -use std::sync::OnceLock; - -static CONFIG: OnceLock = OnceLock::new(); - -pub fn get_config() -> &'static Figment { - CONFIG.get_or_init(|| { - Figment::new() - .merge(Env::prefixed("OXIDICOM_").split("_")) - .merge(Env::prefixed("OXIDICOM_")) - }) -} diff --git a/src/dicomrs_settings.rs b/src/dicomrs_settings.rs index 98a5e7f..29dda90 100644 --- a/src/dicomrs_settings.rs +++ b/src/dicomrs_settings.rs @@ -5,19 +5,15 @@ use dicom::transfer_syntax::TransferSyntaxRegistry; use dicom::ul::association::server::AcceptAny; use dicom::ul::ServerAssociationOptions; -/// Our AE title. -#[braid(serde)] -pub struct OurAETitle; - /// The AE title of a peer PACS server pushing DICOMs to us. #[braid(serde)] -pub struct ClientAETitle; +pub struct AETitle; #[derive(Debug, serde::Deserialize)] pub struct DicomRsSettings { /// Our AE title. #[serde(default = "default_aet")] - pub aet: OurAETitle, + pub aet: String, /// Whether receiving PDUs must not surpass the negotiated maximum PDU length. #[serde(default)] pub strict: bool, @@ -29,13 +25,13 @@ pub struct DicomRsSettings { pub promiscuous: bool, } -impl<'a> Into> for DicomRsSettings { - fn into(self) -> ServerAssociationOptions<'a, AcceptAny> { +impl<'a> From for ServerAssociationOptions<'a, AcceptAny> { + fn from(value: DicomRsSettings) -> Self { let mut options = dicom::ul::association::ServerAssociationOptions::new() .accept_any() - .ae_title(self.aet.to_string()) - .strict(self.strict); - if self.uncompressed_only { + .ae_title(value.aet.to_string()) + .strict(value.strict); + if value.uncompressed_only { options = options .with_transfer_syntax(uids::IMPLICIT_VR_LITTLE_ENDIAN) .with_transfer_syntax(uids::EXPLICIT_VR_LITTLE_ENDIAN); @@ -49,10 +45,10 @@ impl<'a> Into> for DicomRsSettings { for uid in ABSTRACT_SYNTAXES { options = options.with_abstract_syntax(*uid); } - options.promiscuous(self.promiscuous) + options.promiscuous(value.promiscuous) } } -fn default_aet() -> OurAETitle { - OurAETitle::from_static("ChRIS") +fn default_aet() -> String { + "ChRIS".to_string() } diff --git a/src/enums.rs b/src/enums.rs index 888d6b8..ba5686f 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,9 +1,7 @@ use dicom::object::DefaultDicomObject; -use tokio::task::JoinHandle; use ulid::Ulid; -use crate::dicomrs_settings::{ClientAETitle, OurAETitle}; -use crate::pacs_file::PacsFileRegistrationRequest; +use crate::dicomrs_settings::AETitle; /// Events which occur during an association. pub(crate) enum AssociationEvent { @@ -12,11 +10,7 @@ pub(crate) enum AssociationEvent { /// UUID uniquely identifying the TCP connection instance ulid: Ulid, /// AE title of the client sending us DICOMs - aec: ClientAETitle, - /// Our AE title - aet: OurAETitle, - /// Address of the client sending us DICOMs - pacs_address: Option, + aec: AETitle, }, /// Received a DICOM file. DicomInstance { @@ -35,14 +29,11 @@ pub(crate) enum AssociationEvent { }, } -/// A message sent from [crate::association_series_state_loop::association_series_state_loop] -/// to [crate::registration_synchronizer::registration_synchronizer]. -pub(crate) enum PendingRegistration { - /// A task which, if successful, produces a [PacsFileRegistrationRequest] which should - /// be added to a batch in preparation for registration to the database. - /// - /// Error handling should be done by the sender, so the [Err] type is `()`. - Task(JoinHandle>), - /// Indicates that no other tasks shall be sent for a given series. - End, +/// An event which occurs during the reception of a DICOM series. +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum SeriesEvent { + /// DICOM instance received for a series. + Instance(T), + /// No more DICOM data will be received for the series. + Finish(F), } diff --git a/src/findscu.rs b/src/findscu.rs deleted file mode 100644 index 17e3a9f..0000000 --- a/src/findscu.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! DICOM FIND to get NumberOfSeriesRelatedInstances. -//! -//! Mostly based on -//! https://github.com/Enet4/dicom-rs/tree/7c0e5ab895e2f57c432cece41077f13abd4d7f71/findscu - -use crate::dicomrs_settings::{ClientAETitle, OurAETitle}; -use anyhow::{bail, Context}; -use dicom::core::{DataElement, PrimitiveValue, VR}; -use dicom::dicom_value; -use dicom::dictionary_std::{tags, uids}; -use dicom::encoding::TransferSyntaxIndex; -use dicom::object::{InMemDicomObject, StandardDataDictionary}; -use dicom::transfer_syntax::{entries, TransferSyntaxRegistry}; -use dicom::ul::pdu::{PDataValue, PDataValueType}; -use dicom::ul::{ClientAssociationOptions, Pdu}; -use opentelemetry::trace::{Status, TraceContextExt, Tracer}; -use opentelemetry::{global, KeyValue}; -use std::borrow::Cow; -use std::io::Read; -use ulid::Ulid; - -pub(crate) struct FindScuParameters { - pub(crate) ulid: Ulid, - pub(crate) pacs_address: String, - pub(crate) aec: ClientAETitle, - pub(crate) aet: OurAETitle, - pub(crate) study_instance_uid: String, - pub(crate) series_instance_uid: String, -} - -impl FindScuParameters { - pub(crate) fn get_number_of_series_related_instances(&self) -> Result { - let tracer = global::tracer(env!("CARGO_PKG_NAME")); - tracer.in_span("findscu", |cx| { - cx.span().set_attributes(self.to_otel_attributes()); - match self.try_get_number_of_series_related_instances(&cx) { - Ok(num) => { - cx.span().set_status(Status::Ok); - Ok(num) - } - Err(err) => { - tracing::error!( - association_ulid = self.ulid.to_string(), - pacs_address = &self.pacs_address, - aec = self.aec.as_str(), - aet = self.aet.as_str(), - StudyInstanceUID = &self.study_instance_uid, - SeriesInstanceUID = &self.series_instance_uid, - message = err.to_string(), - ); - cx.span().set_status(Status::Error { - description: Cow::Owned(err.to_string()), - }); - Err(()) - } - } - }) - } - - fn try_get_number_of_series_related_instances( - &self, - cx: &opentelemetry::Context, - ) -> anyhow::Result { - let abstract_syntax = uids::STUDY_ROOT_QUERY_RETRIEVE_INFORMATION_MODEL_FIND; - let scu_opt = ClientAssociationOptions::new() - .with_abstract_syntax(abstract_syntax) - .calling_ae_title(self.aet.as_str()) - .called_ae_title(self.aec.as_str()) - .max_pdu_length(16384); - let mut scu = scu_opt.establish_with(&self.pacs_address)?; - let pc_selected = scu - .presentation_contexts() - .first() - .context("Could not select presentation context")?; - let pc_selected_id = pc_selected.id; - let ts = TransferSyntaxRegistry - .get(&pc_selected.transfer_syntax) - .context("Poorly negotiated transfer syntax")?; - let cmd = find_req_command(abstract_syntax, 1); - let mut cmd_data = Vec::with_capacity(128); - cmd.write_dataset_with_ts(&mut cmd_data, &entries::IMPLICIT_VR_LITTLE_ENDIAN.erased()) - .context("Failed to write command")?; - let mut iod_data = Vec::with_capacity(128); - self.to_dicom_query() - .write_dataset_with_ts(&mut iod_data, ts) - .context("failed to write identifier dataset")?; - let pdu = Pdu::PData { - data: vec![PDataValue { - presentation_context_id: pc_selected_id, - value_type: PDataValueType::Command, - is_last: true, - data: cmd_data, - }], - }; - scu.send(&pdu).context("Could not send command")?; - let pdu = Pdu::PData { - data: vec![PDataValue { - presentation_context_id: pc_selected_id, - value_type: PDataValueType::Data, - is_last: true, - data: iod_data, - }], - }; - scu.send(&pdu).context("Could not send C-Find request")?; - - let mut i = 0; - let mut dicoms: Vec = Default::default(); - loop { - let rsp_pdu = scu - .receive() - .context("Failed to receive response from remote node")?; - - match rsp_pdu { - Pdu::PData { data } => { - let data_value = &data[0]; - - let cmd_obj = InMemDicomObject::read_dataset_with_ts( - &data_value.data[..], - &entries::IMPLICIT_VR_LITTLE_ENDIAN.erased(), - )?; - let status = cmd_obj - .get(tags::STATUS) - .context("status code from response is missing")? - .to_int::() - .context("failed to read status code")?; - if status == 0 { - if i == 0 { - cx.span().add_event( - "status", - vec![ - KeyValue::new("status", status), - KeyValue::new("description", "No results matching query"), - ], - ) - } - break; - } else if status == 0xFF00 || status == 0xFF01 { - // fetch DICOM data - let dcm = { - let mut rsp = scu.receive_pdata(); - let mut response_data = Vec::new(); - rsp.read_to_end(&mut response_data) - .context("Failed to read response data")?; - - InMemDicomObject::read_dataset_with_ts(&response_data[..], ts) - .context("Could not read response data set")? - }; - - // might be wrong, see https://github.com/Enet4/dicom-rs/issues/479 - // check DICOM status, - // as some implementations might report status code 0 - // upon sending the response data - let status = dcm - .get(tags::STATUS) - .map(|ele| ele.to_int::()) - .transpose() - .context("failed to read status code")? - .unwrap_or(0); - - dicoms.push(dcm); - - if status == 0 { - break; - } - i += 1; - } else { - cx.span().add_event( - "status", - vec![ - KeyValue::new("status", status), - KeyValue::new("description", "Operation failed"), - ], - ); - break; - } - } - - pdu @ Pdu::Unknown { .. } - | pdu @ Pdu::AssociationRQ { .. } - | pdu @ Pdu::AssociationAC { .. } - | pdu @ Pdu::AssociationRJ { .. } - | pdu @ Pdu::ReleaseRQ - | pdu @ Pdu::ReleaseRP - | pdu @ Pdu::AbortRQ { .. } => { - let _ = scu.abort(); - tracing::error!("Unexpected SCP response: {:?}", pdu); - bail!("Unexpected SCP response") - } - } - } - let _ = scu.release(); - self.get_number_from(dicoms) - } - - fn to_dicom_query(&self) -> InMemDicomObject { - let mut obj = InMemDicomObject::new_empty(); - obj.put(DataElement::new( - tags::QUERY_RETRIEVE_LEVEL, - VR::CS, - PrimitiveValue::from("SERIES"), - )); - obj.put(DataElement::new( - tags::STUDY_INSTANCE_UID, - VR::UI, - PrimitiveValue::from(self.study_instance_uid.as_str()), - )); - obj.put(DataElement::new( - tags::SERIES_INSTANCE_UID, - VR::UI, - PrimitiveValue::from(self.series_instance_uid.as_str()), - )); - obj.put(DataElement::new( - tags::NUMBER_OF_SERIES_RELATED_INSTANCES, - VR::IS, - PrimitiveValue::Empty, - )); - obj - } - - /// Extract and parse the value for `NumberOfSeriesRelatedInstances` among several DICOM objects. - fn get_number_from( - &self, - dicoms: impl IntoIterator, - ) -> anyhow::Result { - dicoms - .into_iter() - .filter(|dcm| { - dcm.get(tags::SERIES_INSTANCE_UID) - .and_then(|ele| ele.string().ok()) - .map(|uid| uid.replace('\0', "")) - .is_some_and(|uid| uid.trim() == &self.series_instance_uid) - }) - .find_map(|dcm| { - dcm.get(tags::NUMBER_OF_SERIES_RELATED_INSTANCES) - .and_then(|ele| ele.string().ok()) - .and_then(|s| { - s.trim() - .parse() - .map_err(|e| { - tracing::warn!( - error = "Invalid number returned from PACS", - pacs_address = &self.pacs_address, - ae_title = self.aec.as_str(), - SeriesInstanceUID = &self.series_instance_uid, - tag = "NumberOfSeriesRelatedInstances", - value = s - ); - e - }) - .ok() - }) - }) - .ok_or_else(|| { - anyhow::Error::msg( - "No valid value for NumberOfSeriesRelatedInstances found for the series", - ) - }) - } - - fn to_otel_attributes(&self) -> Vec { - vec![ - KeyValue::new("association_ulid", self.ulid.to_string()), - KeyValue::new("pacs_address", self.pacs_address.to_string()), - KeyValue::new("aec", self.aec.to_string()), - KeyValue::new("aet", self.aet.to_string()), - KeyValue::new("StudyInstanceUID", self.study_instance_uid.to_string()), - KeyValue::new("SeriesInstanceUID", self.series_instance_uid.to_string()), - ] - } -} - -fn find_req_command( - sop_class_uid: &str, - message_id: u16, -) -> InMemDicomObject { - InMemDicomObject::command_from_element_iter([ - // SOP Class UID - DataElement::new( - tags::AFFECTED_SOP_CLASS_UID, - VR::UI, - PrimitiveValue::from(sop_class_uid), - ), - // command field - DataElement::new( - tags::COMMAND_FIELD, - VR::US, - // 0020H: C-FIND-RQ message - dicom_value!(U16, [0x0020]), - ), - // message ID - DataElement::new(tags::MESSAGE_ID, VR::US, dicom_value!(U16, [message_id])), - //priority - DataElement::new( - tags::PRIORITY, - VR::US, - // medium - dicom_value!(U16, [0x0000]), - ), - // data set type - DataElement::new( - tags::COMMAND_DATA_SET_TYPE, - VR::US, - dicom_value!(U16, [0x0001]), - ), - ]) -} diff --git a/src/lib.rs b/src/lib.rs index 8fd4949..7a10085 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,26 +1,24 @@ mod association_error; mod association_series_state_loop; -mod config; mod dicomrs_settings; mod enums; mod error; -mod findscu; mod listener_tcp_loop; +mod notifier; mod pacs_file; mod patient_age; mod private_sop_uids; -mod notifier; -mod registration_synchronizer; +mod registration_task; mod run_everything; mod sanitize; mod scp; -mod series_key_set; +mod series_synchronizer; mod settings; mod thread_pool; mod transfer; -mod registration_task; +mod types; -pub use config::get_config; pub use dicomrs_settings::DicomRsSettings; -pub use run_everything::run_everything_from_env; -pub use series_key_set::OXIDICOM_CUSTOM_PACS_NAME; +pub use registration_task::register_pacs_series; +pub use run_everything::run_everything; +pub use settings::OxidicomEnvOptions; diff --git a/src/listener_tcp_loop.rs b/src/listener_tcp_loop.rs index b86f36b..a9dcd2c 100644 --- a/src/listener_tcp_loop.rs +++ b/src/listener_tcp_loop.rs @@ -1,11 +1,10 @@ -use crate::dicomrs_settings::{ClientAETitle, DicomRsSettings}; +use crate::dicomrs_settings::DicomRsSettings; use crate::enums::AssociationEvent; use crate::scp::handle_association; use crate::thread_pool::ThreadPool; use opentelemetry::trace::{Status, TraceContextExt, Tracer}; use opentelemetry::{global, Context, KeyValue}; use opentelemetry_semantic_conventions as semconv; -use std::collections::HashMap; use std::net::{SocketAddrV4, TcpListener, TcpStream}; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; @@ -14,20 +13,22 @@ use tokio::sync::mpsc::UnboundedSender; /// /// Every TCP connection is handled by [handle_association], which transmits DICOM instance file /// objects through the given `handler`. -pub fn dicom_listener_tcp_loop( +pub fn dicom_listener_tcp_loop( address: SocketAddrV4, config: DicomRsSettings, finite_connections: Option, n_threads: usize, max_pdu_length: usize, handler: UnboundedSender, - pacs_addresses: HashMap, -) -> anyhow::Result<()> { + on_start: Option, +) -> anyhow::Result<()> +where + F: FnOnce(SocketAddrV4), +{ let listener = TcpListener::bind(address)?; - tracing::info!("listening on: tcp://{}", address); + if let Some(f) = on_start { f(address) }; + let mut pool = ThreadPool::new(n_threads, "dicom_listener"); - let ae_title = Arc::new(config.aet.clone()); - let pacs_addresses = Arc::new(pacs_addresses); let options = Arc::new(config.into()); let handler = Arc::new(handler); let incoming: Box>> = @@ -42,8 +43,6 @@ pub fn dicom_listener_tcp_loop( Ok(scu_stream) => { let options = Arc::clone(&options); let handler = Arc::clone(&handler); - let ae_title = Arc::clone(&ae_title); - let pacs_address = Arc::clone(&pacs_addresses); pool.execute(move || { let ulid = ulid::Ulid::new(); let _context_guard = cx.attach(); @@ -57,15 +56,7 @@ pub fn dicom_listener_tcp_loop( ]; context.span().set_attributes(peer_attributes); } - match handle_association( - scu_stream, - &options, - max_pdu_length, - &handler, - ulid, - &ae_title, - &pacs_address, - ) { + match handle_association(scu_stream, &options, max_pdu_length, &handler, ulid) { Ok(..) => { handler .send(AssociationEvent::Finish { ulid, ok: true }) diff --git a/src/main.rs b/src/main.rs index 108b1b2..de65743 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,32 @@ //! Initialize OpenTelemetry, then call [oxidicom::run_everything_from_env]. -use oxidicom::get_config; +use figment::providers::Env; +use figment::Figment; +use opentelemetry::trace::TraceError; +use opentelemetry_sdk::trace::TracerProvider; +use std::sync::LazyLock; #[tokio::main(flavor = "multi_thread")] async fn main() -> anyhow::Result<()> { init_tracing_subscriber().unwrap(); init_otel_tracing().unwrap(); - let result = oxidicom::run_everything_from_env(None).await; + let result = run_everything_from_env(None).await; opentelemetry::global::shutdown_tracer_provider(); result } -fn init_otel_tracing() -> Result -{ +/// Calls [run_everything] using configuration from environment variables. +/// +/// Function parameters are prioritized over environment variable values. +/// +/// `finite_connections`: shut down the server after the given number of DICOM associations. +pub async fn run_everything_from_env(finite_connections: Option) -> anyhow::Result<()> { + let settings = CONFIG.extract()?; + let on_start = |addr| tracing::info!("listening on: tcp://{}", addr); + oxidicom::run_everything(settings, finite_connections, Some(on_start)).await +} + +fn init_otel_tracing() -> Result { opentelemetry_otlp::new_pipeline() .tracing() .with_exporter(opentelemetry_otlp::new_exporter().tonic()) @@ -20,7 +34,7 @@ fn init_otel_tracing() -> Result Result<(), tracing::dispatcher::SetGlobalDefaultError> { - let level = if get_config().extract_inner_lossy("verbose").unwrap_or(false) { + let level = if CONFIG.extract_inner_lossy("verbose").unwrap_or(false) { tracing::Level::INFO } else { tracing::Level::WARN @@ -31,3 +45,9 @@ fn init_tracing_subscriber() -> Result<(), tracing::dispatcher::SetGlobalDefault .finish(), ) } + +static CONFIG: LazyLock = LazyLock::new(|| { + Figment::new() + .merge(Env::prefixed("OXIDICOM_").split("_")) + .merge(Env::prefixed("OXIDICOM_")) +}); diff --git a/src/notifier.rs b/src/notifier.rs index ea66463..0eec526 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -1,33 +1,25 @@ -use opentelemetry::trace::{FutureExt, Status, TraceContextExt, Tracer}; -use opentelemetry::Context; +use crate::enums::SeriesEvent; +use crate::error::HandleLoopError; +use crate::types::{SeriesCount, SeriesKey}; use std::sync::Arc; -use tokio::sync::mpsc::error::SendError; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::mpsc::UnboundedReceiver; use tokio::task::JoinHandle; -use crate::error::HandleLoopError; -use crate::pacs_file::PacsFileRegistrationRequest; - /// Forward objects from `receiver` to the given `client`. /// /// - Received `Some`: add item to the batch. When batch is full, give everything to the `client` /// - Received `None`: flush current batch to the `client` pub async fn cube_pacsfile_notifier( - mut receiver: UnboundedReceiver>, + mut receiver: UnboundedReceiver<(SeriesKey, SeriesEvent, SeriesCount>)>, celery: Arc, ) -> Result<(), HandleLoopError> { - // We have two loops: - // 1. The receiver loop receives DICOM metadata from the receiver, and adds them to a batch. - // When the batch is full, we create a task to send the DICOM metadata to the database. - // 2. The joiner_loop simply blocks until every task is complete. let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let receiver_loop = async { - while let Some(event) = receiver.recv().await { - handle_event(event, &celery, &tx).unwrap(); + while let Some((series, event)) = receiver.recv().await { + tx.send(handle_event(series, event, &celery)).unwrap(); } drop(tx); }; - // join tasks and take note of any errors. let mut everything_ok = true; let joiner_loop = async { @@ -37,10 +29,7 @@ pub async fn cube_pacsfile_notifier( } } }; - - let (last_flush, _) = tokio::join!(receiver_loop, joiner_loop); - last_flush - .map_err(|_| HandleLoopError("Last flush of PACS files metadata to database failed."))?; + tokio::join!(receiver_loop, joiner_loop); if everything_ok { Ok(()) } else { @@ -50,78 +39,49 @@ pub async fn cube_pacsfile_notifier( } } -/// A tokio task of [CubePostgresClient::register] -type RegistrationTask = JoinHandle>; +type RegistrationTask = JoinHandle>; -/// Receives `event` and calls [register_task] when needed, sending the task to `tx`. -/// -/// Returns the batch's next state. fn handle_event( - event: Option, + series: SeriesKey, + event: SeriesEvent, SeriesCount>, client: &Arc, - tx: &UnboundedSender, -) -> Result, SendError> { - let (next, full_batch) = match event { - None => take_batch(prev), - Some(pacs_file) => prev.push(pacs_file), - }; - if let Some(files) = full_batch { - let task = register_task(client, files); - tx.send(task)?; - } - Ok(next) -} - -/// Empties the batch and returns its contents. -fn take_batch(batches: Batcher) -> (Batcher, Option>) { - let batch_size = batches.batch_size; - let batch = batches.into_inner(); - let next_batches = Batcher::new(batch_size); - if batch.is_empty() { - tracing::debug!("batch is empty"); - (next_batches, None) - } else { - (next_batches, Some(batch)) - } -} - -/// Wraps [CubePostgresClient::register] with [tokio::spawn] and [tracing]. -fn register_task( - client: &Arc, - files: Vec, ) -> RegistrationTask { - let client = Arc::clone(client); - tokio::spawn(async move { - let tracer = opentelemetry::global::tracer(env!("CARGO_PKG_NAME")); - let span = tracer.start("register_to_postgres"); - let cx = Context::current_with_span(span); - - let n_files = files.len(); - let result = client.register(files).with_context(cx.clone()).await; - match &result { - Ok(_) => { - tracing::info!(task = "register", count = n_files); - cx.span().set_status(Status::Ok); - } - Err(e) => { - tracing::error!(task = "register", error = e.to_string()); - cx.span().set_status(Status::error(e.to_string())); - } + match event { + SeriesEvent::Instance(result) => tokio::spawn(async move { + // dbg!((series, result)); + Ok(()) + }), + SeriesEvent::Finish(count) => { + let client = Arc::clone(client); + tokio::spawn( + async move { send_registration_task_to_celery(series, count, &client).await }, + ) } - cx.span().end(); - result - }) + } } -/// Consume the `batch` and give everything to [CubePostgresClient::register] -async fn flush_to_database>( - batch: Batcher, - client: C, -) -> Result<(), PacsFileDatabaseError> { - let remaining = batch.into_inner(); - if remaining.is_empty() { - Ok(()) - } else { - client.as_ref().register(&remaining).await +async fn send_registration_task_to_celery( + series: SeriesKey, + count: SeriesCount, + client: &celery::Celery, +) -> Result<(), ()> { + match client.send_task(count.into_task()).await { + Ok(r) => { + tracing::info!( + pacs_name = series.pacs_name.as_str(), + SeriesInstanceUID = series.SeriesInstanceUID, + celery_task_id = r.task_id, + celery_task_name = "register_pacs_series" + ); + Ok(()) + } + Err(e) => { + tracing::error!( + pacs_name = series.pacs_name.as_str(), + SeriesInstanceUID = series.SeriesInstanceUID, + message = e.to_string() + ); + Err(()) + } } } diff --git a/src/pacs_file.rs b/src/pacs_file.rs index 54c27b7..65c70fe 100644 --- a/src/pacs_file.rs +++ b/src/pacs_file.rs @@ -1,64 +1,119 @@ -//! ChRIS backend PACSFile object representation. -//! -//! ## Notes -//! -//! `PatientAge` should be in days. -//! https://github.com/FNNDSC/pypx/blob/7b83154d7c6d631d81eac8c9c4a2fc164ccc2ebc/pypx/register.py#L459-L465 -#![allow(non_snake_case)] - use std::fmt::Display; -use crate::dicomrs_settings::ClientAETitle; +use crate::dicomrs_settings::AETitle; use dicom::dictionary_std::tags; use dicom::object::{DefaultDicomObject, Tag}; use crate::error::{name_of, DicomRequiredTagError, RequiredTagError}; use crate::patient_age::parse_age; use crate::sanitize::sanitize_path; +use crate::types::{DicomFilePath, DicomInfo}; /// A wrapper of [PacsFileRegistrationRequest] along with the [DefaultDicomObject] it was created from. pub struct PacsFileRegistration { - pub request: PacsFileRegistrationRequest, + pub data: DicomInfo, pub obj: DefaultDicomObject, } impl PacsFileRegistration { - /// Wraps [PacsFileRegistrationRequest::new]. pub(crate) fn new( - pacs_name: ClientAETitle, + pacs_name: AETitle, obj: DefaultDicomObject, ) -> Result<(Self, Vec), DicomRequiredTagError> { - match PacsFileRegistrationRequest::new(pacs_name, &obj) { - Ok((request, bad_tags)) => Ok((Self { request, obj }, bad_tags)), + match get_series_tags(pacs_name, &obj) { + Ok((data, bad_tags)) => Ok((Self { data, obj }, bad_tags)), Err(error) => Err(DicomRequiredTagError { obj, error }), } } } -/// Data necessary to register a DICOM file to CUBE's database in the `pacsfiles_pacsfile` table. -/// -/// Historically, this struct represented the JSON payload to `POST api/v1/pacs/`. However, -/// we register files directly to the database instead of via CUBE for performance reasons. -#[derive(Debug, Clone)] -pub struct PacsFileRegistrationRequest { - pub path: String, - pub PatientID: String, - pub StudyDate: time::Date, - pub StudyInstanceUID: String, - pub SeriesInstanceUID: String, - pub pacs_name: ClientAETitle, - - pub PatientName: Option, - pub PatientBirthDate: Option, - pub PatientAge: Option, // i32 because PostgreSQL - pub PatientSex: Option, - pub AccessionNumber: Option, - pub Modality: Option, - pub ProtocolName: Option, - pub StudyDescription: Option, - pub SeriesDescription: Option, +#[allow(non_snake_case)] +fn get_series_tags( + pacs_name: AETitle, + dcm: &DefaultDicomObject, +) -> Result<(DicomInfo, Vec), RequiredTagError> { + let mut bad_tags = vec![]; + // required fields + let StudyInstanceUID = ttr(dcm, tags::STUDY_INSTANCE_UID)?; + let SeriesInstanceUID = ttr(dcm, tags::SERIES_INSTANCE_UID)?; + let SOPInstanceUID = ttr(dcm, tags::SOP_INSTANCE_UID)?; + let PatientID = ttr(dcm, tags::PATIENT_ID)?; + let StudyDate_string = ttr(dcm, tags::STUDY_DATE)?; // required by CUBE + let StudyDate_format = time::macros::format_description!("[year][month][day]"); // DICOM DA format + let StudyDate = time::Date::parse(&StudyDate_string, &StudyDate_format).map_err(|_| { + RequiredTagError::Bad(BadTag { + tag: tags::STUDY_DATE, + value: Some(StudyDate_string.to_string()), + }) + })?; + + // optional values + let PatientName = tts(dcm, tags::PATIENT_NAME); + let PatientBirthDate = tts(dcm, tags::PATIENT_BIRTH_DATE); + let StudyDescription = tts(dcm, tags::STUDY_DESCRIPTION); + let AccessionNumber = tts(dcm, tags::ACCESSION_NUMBER); + let SeriesDescription = tts(dcm, tags::SERIES_DESCRIPTION); + + // SeriesNumber and InstanceNumber are not fields of a ChRIS PACSFile. + // They should be integers, and they also should appear in the fname. + let InstanceNumber = tt(dcm, tags::INSTANCE_NUMBER).map(MaybeU32::from); + let SeriesNumber = tt(dcm, tags::SERIES_NUMBER).map(MaybeU32::from); + + // Numerical value + let PatientAgeStr = tt(dcm, tags::PATIENT_AGE); + let PatientAge = PatientAgeStr.and_then(|age| { + let num = parse_age(age.trim()); + if num.is_none() { + bad_tags.push(BadTag { + tag: tags::PATIENT_AGE, + value: Some(age.to_string()), + }) + }; + num + }); + + // https://github.com/FNNDSC/pypx/blob/7b83154d7c6d631d81eac8c9c4a2fc164ccc2ebc/bin/px-push#L175-L195 + let path = format!( + "SERVICES/PACS/{}/{}-{}-{}/{}-{}-{}/{:0>5}-{}-{}/{:0>4}-{}.dcm", + sanitize_path(&pacs_name), + // Patient + sanitize_path(PatientID.as_str()), + sanitize_path(PatientName.as_deref().unwrap_or("")), + sanitize_path(PatientBirthDate.as_deref().unwrap_or("")), + // Study + sanitize_path(StudyDescription.as_deref().unwrap_or("StudyDescription")), + sanitize_path(AccessionNumber.as_deref().unwrap_or("AccessionNumber")), + sanitize_path(StudyDate_string.as_str()), + // Series + SeriesNumber.unwrap_or_else(|| MaybeU32::String("SeriesNumber".to_string())), + sanitize_path(SeriesDescription.as_deref().unwrap_or("SeriesDescription")), + &hash(SeriesInstanceUID.as_str())[..7], + // Instance + InstanceNumber.unwrap_or_else(|| MaybeU32::String("InstanceNumber".to_string())), + sanitize_path(SOPInstanceUID) + ); + let path = DicomFilePath::new(path); + let pacs_file = DicomInfo { + path, + pacs_name, + PatientID, + StudyDate, + StudyInstanceUID, + SeriesInstanceUID, + PatientName, + PatientBirthDate, + PatientAge, + PatientSex: tts(dcm, tags::PATIENT_SEX), + AccessionNumber, + Modality: tts(dcm, tags::MODALITY), + ProtocolName: tts(dcm, tags::PROTOCOL_NAME), + StudyDescription, + SeriesDescription, + }; + Ok((pacs_file, bad_tags)) } +/// An invalid DICOM tag key-value pair. #[derive(Debug)] pub struct BadTag { pub tag: Tag, @@ -71,96 +126,9 @@ impl Display for BadTag { } } -impl PacsFileRegistrationRequest { - pub fn new( - pacs_name: ClientAETitle, - dcm: &DefaultDicomObject, - ) -> Result<(Self, Vec), RequiredTagError> { - let mut bad_tags = vec![]; - // required fields - let StudyInstanceUID = ttr(dcm, tags::STUDY_INSTANCE_UID)?; - let SeriesInstanceUID = ttr(dcm, tags::SERIES_INSTANCE_UID)?; - let SOPInstanceUID = ttr(dcm, tags::SOP_INSTANCE_UID)?; - let PatientID = ttr(dcm, tags::PATIENT_ID)?; - let StudyDate_string = ttr(dcm, tags::STUDY_DATE)?; // required by CUBE - let StudyDate_format = time::macros::format_description!("[year][month][day]"); // DICOM DA format - let StudyDate = time::Date::parse(&StudyDate_string, &StudyDate_format).map_err(|_| { - RequiredTagError::Bad(BadTag { - tag: tags::STUDY_DATE, - value: Some(StudyDate_string.to_string()), - }) - })?; - - // optional values - let PatientName = tts(dcm, tags::PATIENT_NAME); - let PatientBirthDate = tts(dcm, tags::PATIENT_BIRTH_DATE); - let StudyDescription = tts(dcm, tags::STUDY_DESCRIPTION); - let AccessionNumber = tts(dcm, tags::ACCESSION_NUMBER); - let SeriesDescription = tts(dcm, tags::SERIES_DESCRIPTION); - - // SeriesNumber and InstanceNumber are not fields of a ChRIS PACSFile. - // They should be integers, and they also should appear in the fname. - let InstanceNumber = tt(dcm, tags::INSTANCE_NUMBER).map(MaybeU32::from); - let SeriesNumber = tt(dcm, tags::SERIES_NUMBER).map(MaybeU32::from); - - // Numerical value - let PatientAgeStr = tt(dcm, tags::PATIENT_AGE); - let PatientAge = PatientAgeStr.and_then(|age| { - let num = parse_age(age.trim()); - if num.is_none() { - bad_tags.push(BadTag { - tag: tags::PATIENT_AGE, - value: Some(age.to_string()), - }) - }; - num - }); - - // https://github.com/FNNDSC/pypx/blob/7b83154d7c6d631d81eac8c9c4a2fc164ccc2ebc/bin/px-push#L175-L195 - let path = format!( - "SERVICES/PACS/{}/{}-{}-{}/{}-{}-{}/{:0>5}-{}-{}/{:0>4}-{}.dcm", - sanitize_path(&pacs_name), - // Patient - sanitize_path(PatientID.as_str()), - sanitize_path(PatientName.as_deref().unwrap_or("")), - sanitize_path(PatientBirthDate.as_deref().unwrap_or("")), - // Study - sanitize_path(StudyDescription.as_deref().unwrap_or("StudyDescription")), - sanitize_path(AccessionNumber.as_deref().unwrap_or("AccessionNumber")), - sanitize_path(StudyDate_string.as_str()), - // Series - SeriesNumber.unwrap_or_else(|| MaybeU32::String("SeriesNumber".to_string())), - sanitize_path(SeriesDescription.as_deref().unwrap_or("SeriesDescription")), - &hash(SeriesInstanceUID.as_str())[..7], - // Instance - InstanceNumber.unwrap_or_else(|| MaybeU32::String("InstanceNumber".to_string())), - sanitize_path(SOPInstanceUID) - ); - - let pacs_file = Self { - path, - pacs_name, - PatientID, - StudyDate, - StudyInstanceUID, - SeriesInstanceUID, - PatientName, - PatientBirthDate, - PatientAge, - PatientSex: tts(dcm, tags::PATIENT_SEX), - AccessionNumber, - Modality: tts(dcm, tags::MODALITY), - ProtocolName: tts(dcm, tags::PROTOCOL_NAME), - StudyDescription, - SeriesDescription, - }; - Ok((pacs_file, bad_tags)) - } -} - /// Required string tag fn ttr(dcm: &DefaultDicomObject, tag: Tag) -> Result { - tts(dcm, tag).ok_or_else(|| RequiredTagError::Missing(tag)) + tts(dcm, tag).ok_or(RequiredTagError::Missing(tag)) } /// Optional string tag (with null bytes removed) diff --git a/src/private_sop_uids.rs b/src/private_sop_uids.rs index 8b931ae..e2f6c66 100644 --- a/src/private_sop_uids.rs +++ b/src/private_sop_uids.rs @@ -1,6 +1,6 @@ //! Private SOP class UIDs. //! -//! https://dcm4chee-arc-cs.readthedocs.io/en/latest/networking/specs/storage/storage.html +//! /// Private Siemens AX Frame Sets Storage pub const SIEMENS_AX_FRAME_SETS_STORAGE: &str = "1.3.12.2.1107.5.99.3.11"; diff --git a/src/registration_synchronizer.rs b/src/registration_synchronizer.rs deleted file mode 100644 index 1891528..0000000 --- a/src/registration_synchronizer.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::enums::PendingRegistration; -use crate::error::HandleLoopError; -use crate::pacs_file::PacsFileRegistrationRequest; -use crate::series_key_set::SeriesKeySet; -use futures::StreamExt; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::mpsc::error::SendError; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::task::JoinHandle; - -/// `registration_synchronizer` is intended as a way to synchronize requests before they are -/// sent to [crate::notifier::cube_pacsfile_notifier]. It guarantees that the "flush" command -/// can be invoked after all tasks for an association are complete. -pub(crate) async fn registration_synchronizer( - mut receiver: UnboundedReceiver<(SeriesKeySet, PendingRegistration)>, - sender: UnboundedSender>, -) -> Result<(), HandleLoopError> { - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let receiver_loop = async { - let mut inflight_series: HashMap> = Default::default(); - let sender = Arc::new(sender); - while let Some((series, event)) = receiver.recv().await { - match event { - PendingRegistration::Task(task) => { - enqueue_registration_and_insert(series, task, &sender, &mut inflight_series) - } - PendingRegistration::End => { - let tasks_for_series = inflight_series - .remove(&series) - .expect("No tasks were received for the series."); - let sender = Arc::clone(&sender); - let task = tokio::task::spawn(async move { - wait_on_all_then_flush(tasks_for_series, &sender).await - }); - tx.send(task).unwrap() - } - } - } - drop(tx); - }; - let mut everything_ok = true; - let joiner_loop = async { - while let Some(handle) = rx.recv().await { - if let Err(e) = handle.await.unwrap() { - tracing::error!("{}", e.to_string()); - everything_ok = false; - } - } - }; - tokio::join!(receiver_loop, joiner_loop); - if everything_ok { - Ok(()) - } else { - Err(HandleLoopError( - "There was an error in registration_synchronizer", - )) - } -} - -/// Create a task which joins the given `task`. If the given `task` is [Ok], send the -/// [PacsFileRegistrationRequest] to `sender`. -/// -/// Insert the created task into `inflight_series`. -fn enqueue_registration_and_insert( - series: SeriesKeySet, - task: JoinHandle>, - sender: &Arc>>, - inflight_series: &mut HashMap< - SeriesKeySet, - Vec>>>>, - >, -) { - let sender = Arc::clone(&sender); - let register_task = tokio::task::spawn(async move { - if let Ok(pacs_file) = task.await.unwrap() { - sender.send(Some(pacs_file)) - } else { - Ok(()) - } - }); - if let Some(v) = inflight_series.get_mut(&series) { - v.push(register_task); - } else { - inflight_series.insert(series, vec![register_task]); - } -} - -/// Wait on all the tasks, then send [None] to `sender`. -async fn wait_on_all_then_flush( - tasks: Vec>>, - sender: &UnboundedSender>, -) -> Result<(), SendError>> { - futures::stream::iter(tasks) - .map(|handle| async { handle.await.unwrap() }) - .buffer_unordered(usize::MAX) - .for_each(|result| async { - if let Err(error) = result { - tracing::error!("{}", error.to_string()) - } - }) - .await; - sender.send(None) -} diff --git a/src/registration_task.rs b/src/registration_task.rs index 1aa2b63..9b09ffe 100644 --- a/src/registration_task.rs +++ b/src/registration_task.rs @@ -3,54 +3,39 @@ #![allow(unused_variables)] #![allow(unreachable_code)] +#![allow(non_snake_case)] +#![allow(clippy::too_many_arguments)] -use std::num::NonZeroUsize; +use crate::dicomrs_settings::AETitle; +use crate::types::SeriesPath; /// A function stub with the same signature as the `register_pacs_series` celery task /// in *CUBE*'s Python code. +/// +/// ### `PatientAge` must be in days +/// +/// `PatientAge` is in days and its type is `i32` because that is the column's type +/// in *CUBE*'s PostgreSQL database. +/// +/// https://github.com/FNNDSC/pypx/blob/7b83154d7c6d631d81eac8c9c4a2fc164ccc2ebc/pypx/register.py#L459-L465 #[celery::task(name = "pacsfiles.tasks.register_pacs_series")] -pub(crate) fn register_pacs_series( - patient_id: String, - patient_name: String, - study_date: String, - study_instance_uid: String, - study_description: String, - series_description: String, - series_instance_uid: String, - pacs_name: String, - path: String, - ndicom: NonZeroUsize, +pub fn register_pacs_series( + PatientID: String, + StudyDate: String, + StudyInstanceUID: String, + SeriesInstanceUID: String, + pacs_name: AETitle, + path: SeriesPath, + ndicom: usize, + PatientName: Option, + PatientBirthDate: Option, + PatientAge: Option, + PatientSex: Option, + AccessionNumber: Option, + Modality: Option, + ProtocolName: Option, + StudyDescription: Option, + SeriesDescription: Option, ) { unimplemented!() } - -#[cfg(test)] -mod tests { - use super::*; - use std::num::NonZeroUsize; - - #[tokio::test] - async fn test_try_celery_wip() -> anyhow::Result<()> { - let app = celery::app!( - broker = AMQPBroker { "amqp://queue:5672" }, - tasks = [register_pacs_series], - task_routes = [ "pacsfiles.tasks.register_pacs_series" => "main2" ], - ) - .await?; - - let task = register_pacs_series::new( - "Jennings Zhang".to_string(), - "abc123ismyID".to_string(), - "2024-08-28".to_string(), - "StudyInstance123".to_string(), - "hello from rust".to_string(), - "SeriesInstance456".to_string(), - "i am so cool".to_string(), - "MyPACS".to_string(), - "SERVICES/PACS/MyPACS/123456-crazy/brain_crazy_study/SAG_T1_MPRA".to_string(), - NonZeroUsize::new(192).unwrap(), - ); - app.send_task(task).await?; - Ok(()) - } -} diff --git a/src/run_everything.rs b/src/run_everything.rs index e24505d..1961b1c 100644 --- a/src/run_everything.rs +++ b/src/run_everything.rs @@ -1,41 +1,42 @@ use crate::association_series_state_loop::association_series_state_loop; -use crate::get_config; use std::net::{Ipv4Addr, SocketAddrV4}; use tokio::sync::mpsc; use crate::listener_tcp_loop::dicom_listener_tcp_loop; use crate::notifier::cube_pacsfile_notifier; -use crate::registration_synchronizer::registration_synchronizer; +use crate::series_synchronizer::series_synchronizer; use crate::settings::OxidicomEnvOptions; use futures::FutureExt; -/// Calls [run_everything] using configuration from environment variables. -/// -/// Function parameters are prioritized over environment variable values. -/// -/// `finite_connections`: shut down the server after the given number of DICOM associations. -pub async fn run_everything_from_env(finite_connections: Option) -> anyhow::Result<()> { - let config = get_config(); - let settings = config.extract()?; - run_everything(settings, finite_connections).await -} - /// Runs everything in parallel: /// /// 1. A TCP server loop to listen for incoming DICOM objects /// 2. A file storage handler which writes DICOM files to disk -/// 3. A database connection pool which registers written files -async fn run_everything( +/// 3. A notifier which sends events to CUBE as DICOM files are received +pub async fn run_everything( OxidicomEnvOptions { - amqp_address, files_root, progress_nats_address, progress_interval, scp, scp_max_pdu_length, pacs_address, listener_threads, listener_port + amqp_address, + files_root, + progress_nats_address, + progress_interval, + scp, + scp_max_pdu_length, + listener_threads, + listener_port, + queue_name, }: OxidicomEnvOptions, finite_connections: Option, -) -> anyhow::Result<()> { + on_start: Option, +) -> anyhow::Result<()> +where + F: FnOnce(SocketAddrV4) + Send + 'static, +{ let celery = celery::app!( broker = AMQPBroker { amqp_address }, tasks = [crate::registration_task::register_pacs_series], - task_routes = [ "pacsfiles.tasks.register_pacs_series" => "main2" ], - ).await?; + task_routes = [ "pacsfiles.tasks.register_pacs_series" => &queue_name ], + ) + .await?; let (tx_association, rx_association) = mpsc::unbounded_channel(); let (tx_storetasks, rx_storetasks) = mpsc::unbounded_channel(); @@ -48,14 +49,14 @@ async fn run_everything( listener_threads.get(), scp_max_pdu_length, tx_association, - pacs_address, + on_start, ) }); tokio::try_join!( association_series_state_loop(rx_association, tx_storetasks, files_root) .map(|r| r.unwrap()), - registration_synchronizer(rx_storetasks, tx_register), + series_synchronizer(rx_storetasks, tx_register), cube_pacsfile_notifier(rx_register, celery) )?; listener_handle.await? diff --git a/src/sanitize.rs b/src/sanitize.rs index 34b8578..d20b869 100644 --- a/src/sanitize.rs +++ b/src/sanitize.rs @@ -1,5 +1,5 @@ use regex::Regex; -use std::sync::OnceLock; +use std::sync::LazyLock; /// Replace disallowed characters with "_". /// https://github.com/FNNDSC/pypx/blob/7619c15f4d2303d6d5ca7c255d81d06c7ab8682b/pypx/repack.py#L424 @@ -7,10 +7,8 @@ use std::sync::OnceLock; /// Also, it's necessary to handle NUL bytes... pub(crate) fn sanitize_path>(s: S) -> String { let s_nonull = s.as_ref().replace('\0', ""); - VALID_CHARS_RE - .get_or_init(|| Regex::new(r#"[^A-Za-z0-9\.\-]+"#).unwrap()) - .replace_all(&s_nonull, "_") - .to_string() + VALID_CHARS_RE.replace_all(&s_nonull, "_").to_string() } -static VALID_CHARS_RE: OnceLock = OnceLock::new(); +static VALID_CHARS_RE: LazyLock = + LazyLock::new(|| Regex::new(r#"[^A-Za-z0-9\.\-]+"#).unwrap()); diff --git a/src/scp.rs b/src/scp.rs index 10e12e5..a0cdf38 100644 --- a/src/scp.rs +++ b/src/scp.rs @@ -1,9 +1,8 @@ //! Handles incoming request to store a DICOM file. //! //! File mostly copied from dicom-rs. -//! https://github.com/Enet4/dicom-rs/blob/dbd41ed3a0d1536747c6b8ea2b286e4c6e8ccc8a/storescp/src/main.rs +//! -use std::collections::HashMap; use std::net::TcpStream; use dicom::core::{DataElement, VR}; @@ -21,7 +20,7 @@ use tokio::sync::mpsc::UnboundedSender; use ulid::Ulid; use crate::association_error::{AssociationError, AssociationError::*}; -use crate::dicomrs_settings::{ClientAETitle, OurAETitle}; +use crate::dicomrs_settings::AETitle; use crate::enums::AssociationEvent; /// Handle an "association" from an "SCU" (i.e. handle when someone is trying to give us DICOM files). @@ -35,22 +34,17 @@ pub fn handle_association( max_pdu_length: usize, channel: &UnboundedSender, ulid: Ulid, - aet: &OurAETitle, - pacs_addresses: &HashMap, ) -> Result<(), AssociationError> { let mut association = options.establish(scu_stream).map_err(CouldNotEstablish)?; let context = opentelemetry::Context::current(); - let aec = ClientAETitle::from(association.client_ae_title()); - let pacs_address = pacs_addresses.get(&aec).map(|s| s.to_string()); + let aec = AETitle::from(association.client_ae_title()); context .span() .set_attribute(KeyValue::new("aet", aec.to_string())); channel .send(AssociationEvent::Start { ulid, - aet: aet.clone(), aec: aec.clone(), - pacs_address, }) .unwrap(); diff --git a/src/series_key_set.rs b/src/series_key_set.rs deleted file mode 100644 index 72a6eed..0000000 --- a/src/series_key_set.rs +++ /dev/null @@ -1,91 +0,0 @@ -#![allow(non_snake_case)] - -use ulid::Ulid; - -use crate::dicomrs_settings::ClientAETitle; -use crate::pacs_file::PacsFileRegistrationRequest; - -/// The set of fields of a [PacsFileRegistrationRequest] which uniquely identifies a DICOM series -/// in CUBE. -/// -/// For well-behaved PACS, `SeriesInstanceUID` would be all you need. However, we do not assume -/// the PACS is well-behaved nor the DICOM tags to be 100% valid. -#[derive(Debug, Hash, PartialEq, Eq, Clone)] -pub struct SeriesKeySet { - pub dir_path: String, - pub PatientID: String, - pub StudyDate: time::Date, - pub StudyInstanceUID: String, - pub SeriesInstanceUID: String, - pub pacs_name: ClientAETitle, -} - -impl From for SeriesKeySet { - fn from( - PacsFileRegistrationRequest { - path, - PatientID, - StudyDate, - StudyInstanceUID, - SeriesInstanceUID, - pacs_name, - .. - }: PacsFileRegistrationRequest, - ) -> Self { - Self { - dir_path: dirname(&path).to_string(), - PatientID, - StudyDate, - StudyInstanceUID, - SeriesInstanceUID, - pacs_name, - } - } -} - -fn dirname(s: &str) -> &str { - s.rsplit_once('/') - .map(|(l, _)| l) - .expect("fname does not contain a slash.") -} - -/// The special `pacs_name` used by Oxidicom to register "Oxidicom Custom Metadata" files to CUBE. -/// -/// Note: must be 20 characters or fewer. This is a restriction of CUBE. -pub const OXIDICOM_CUSTOM_PACS_NAME: &str = "org.fnndsc.oxidicom"; - -impl SeriesKeySet { - /// Serialize a key-value pair from an association as an "Oxidicom Custom Metadata" file. - pub(crate) fn into_oxidicom_custom_pacsfile( - self, - association_ulid: Ulid, - key: &str, - value: impl AsRef, - ) -> PacsFileRegistrationRequest { - let path = format!( - "SERVICES/PACS/{}/{}/{}/{}={}", - OXIDICOM_CUSTOM_PACS_NAME, - self.dir_path, - association_ulid, - key, - value.as_ref() - ); - PacsFileRegistrationRequest { - path, - PatientID: self.PatientID, - StudyDate: self.StudyDate, - StudyInstanceUID: self.StudyInstanceUID, - SeriesInstanceUID: self.SeriesInstanceUID, - pacs_name: ClientAETitle::from_static(OXIDICOM_CUSTOM_PACS_NAME), - PatientName: None, - PatientBirthDate: None, - PatientAge: None, - PatientSex: None, - AccessionNumber: None, - Modality: None, - ProtocolName: Some(key.to_string()), - StudyDescription: None, - SeriesDescription: Some(value.as_ref().to_string()), - } - } -} diff --git a/src/series_synchronizer.rs b/src/series_synchronizer.rs new file mode 100644 index 0000000..9b3d62d --- /dev/null +++ b/src/series_synchronizer.rs @@ -0,0 +1,162 @@ +use crate::enums::SeriesEvent; +use crate::error::HandleLoopError; +use futures::StreamExt; +use std::collections::HashMap; +use std::hash::Hash; +use std::sync::Arc; +use tokio::sync::mpsc::error::SendError; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::task::JoinHandle; + +/// Waits on the [JoinHandle] of [PendingDicomInstance] for each `K`, so that +/// [SeriesEvent::Finish] is the last message to be sent to `sender` for the respective `K`. +pub(crate) async fn series_synchronizer< + K: Eq + Hash + Send + Clone + 'static, + T: Send + 'static, + L: Send + 'static, +>( + mut receiver: UnboundedReceiver<(K, SeriesEvent, L>)>, + sender: UnboundedSender<(K, SeriesEvent)>, +) -> Result<(), HandleLoopError> { + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let receiver_loop = async { + let mut inflight_series: HashMap> = Default::default(); + let sender = Arc::new(sender); + while let Some((series, event)) = receiver.recv().await { + match event { + SeriesEvent::Instance(task) => { + enqueue_and_insert(series, task, &sender, &mut inflight_series) + } + SeriesEvent::Finish(final_message) => { + let tasks_for_series = inflight_series + .remove(&series) + .expect("No tasks were received for the series."); + let sender = Arc::clone(&sender); + let task = tokio::task::spawn(async move { + wait_on_all_then_flush(tasks_for_series, &sender, series, final_message) + .await + }); + tx.send(task).unwrap() + } + } + } + drop(tx); + }; + let mut everything_ok = true; + let joiner_loop = async { + while let Some(handle) = rx.recv().await { + if let Err(e) = handle.await.unwrap() { + tracing::error!("{}", e.to_string()); + everything_ok = false; + } + } + }; + tokio::join!(receiver_loop, joiner_loop); + if everything_ok { + Ok(()) + } else { + Err(HandleLoopError( + "There was an error in registration_synchronizer", + )) + } +} + +/// Create a task which joins the given `task`. If the given `task` is [Ok], send its return value +/// to `sender`. +/// +/// Insert the created task into `inflight_series`. +fn enqueue_and_insert< + K: Clone + Eq + Hash + Send + 'static, + T: Send + 'static, + F: Send + 'static, +>( + series: K, + task: JoinHandle, + sender: &Arc)>>, + inflight_series: &mut HashMap< + K, + Vec)>>>>, + >, +) { + let sender = Arc::clone(sender); + let series_clone = series.clone(); + let register_task = tokio::task::spawn(async move { + sender.send((series_clone, SeriesEvent::Instance(task.await.unwrap()))) + }); + if let Some(v) = inflight_series.get_mut(&series) { + v.push(register_task); + } else { + inflight_series.insert(series, vec![register_task]); + } +} + +/// Wait on all the tasks, then send [None] to `sender`. +async fn wait_on_all_then_flush( + tasks: Vec>>, + sender: &UnboundedSender<(K, SeriesEvent)>, + series: K, + last: F, +) -> Result<(), SendError<(K, SeriesEvent)>> { + futures::stream::iter(tasks) + .map(|handle| async { handle.await.unwrap() }) + .buffer_unordered(usize::MAX) + .for_each(|result| async { + if let Err(error) = result { + tracing::error!("{}", error.to_string()) + } + }) + .await; + sender.send((series, SeriesEvent::Finish(last))) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + use tokio::sync::mpsc::unbounded_channel; + + #[tokio::test(flavor = "multi_thread")] + async fn test_synchronizer() { + let (source_tx, source_rx) = unbounded_channel(); + let (sink_tx, mut sink_rx) = unbounded_channel(); + let synchronizer = series_synchronizer(source_rx, sink_tx); + let source = async move { + let duration = Duration::from_millis(100); + source_tx.send(("A", dummy_task(100, "second"))).unwrap(); + source_tx.send(("A", dummy_task(150, "third"))).unwrap(); + source_tx.send(("A", dummy_task(50, "first"))).unwrap(); + source_tx + .send(("A", SeriesEvent::Finish("finish"))) + .unwrap(); + }; + let sink = async move { + assert_eq!( + sink_rx.recv().await, + Some(("A", SeriesEvent::Instance("first"))) + ); + assert_eq!( + sink_rx.recv().await, + Some(("A", SeriesEvent::Instance("second"))) + ); + assert_eq!( + sink_rx.recv().await, + Some(("A", SeriesEvent::Instance("third"))) + ); + assert_eq!( + sink_rx.recv().await, + Some(("A", SeriesEvent::Finish("finish"))) + ); + }; + let (_, _, result) = tokio::join!(source, sink, synchronizer); + result.unwrap(); + } + + fn dummy_task(ms: u64, ret: R) -> SeriesEvent, F> { + let duration = Duration::from_millis(ms); + let task = tokio::spawn(async move { + tokio::time::sleep(duration).await; + ret + }); + SeriesEvent::Instance(task) + } +} diff --git a/src/settings.rs b/src/settings.rs index b952760..168796f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,30 +1,33 @@ //! Oxidicom settings, which are configurable using environment variables. -use crate::dicomrs_settings::ClientAETitle; use crate::DicomRsSettings; use camino::Utf8PathBuf; use serde::Deserialize; -use std::collections::HashMap; use std::num::NonZeroUsize; #[derive(Debug, Deserialize)] pub struct OxidicomEnvOptions { pub amqp_address: String, pub files_root: Utf8PathBuf, - pub progress_nats_address: String, - #[serde(with = "humantime_serde")] + #[serde(default = "default_queue_name")] + pub queue_name: String, + pub progress_nats_address: Option, + #[serde(with = "humantime_serde", default = "default_progress_interval")] pub progress_interval: std::time::Duration, pub scp: DicomRsSettings, - #[serde(default)] + #[serde(default = "default_max_pdu_length")] pub scp_max_pdu_length: usize, - #[serde(default)] - pub pacs_address: HashMap, #[serde(default = "default_listener_threads")] pub listener_threads: NonZeroUsize, #[serde(default = "default_listener_port")] pub listener_port: u16, } - +/// The name of the queue used by the `register_pacs_series` celery task in *CUBE*'s code. +/// +/// https://github.com/FNNDSC/ChRIS_ultron_backEnd/blob/b3cb0afa068b2cfb5a89eea22ff9b41437dc6f2a/chris_backend/core/celery.py#L36 +fn default_queue_name() -> String { + "main2".to_string() +} fn default_listener_threads() -> NonZeroUsize { NonZeroUsize::new(8).unwrap() @@ -33,3 +36,11 @@ fn default_listener_threads() -> NonZeroUsize { fn default_listener_port() -> u16 { 11111 } + +fn default_progress_interval() -> std::time::Duration { + std::time::Duration::from_nanos(1) +} + +fn default_max_pdu_length() -> usize { + 16384 +} diff --git a/src/thread_pool.rs b/src/thread_pool.rs index 094c312..5726300 100644 --- a/src/thread_pool.rs +++ b/src/thread_pool.rs @@ -1,5 +1,5 @@ //! Thread pool implementation from The Book. -//! https://doc.rust-lang.org/book/ch20-02-multithreaded.html +//! use std::sync::{mpsc, Arc, Mutex}; use std::thread; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..06484c8 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,139 @@ +#![allow(non_snake_case)] + +use crate::dicomrs_settings::AETitle; +use crate::enums::SeriesEvent; +use crate::registration_task::register_pacs_series; +use aliri_braid::braid; +use celery::task::Signature; +use time::macros::format_description; +use tokio::task::JoinHandle; + +/// Path in storage to a DICOM instance file. +#[braid(serde)] +pub(crate) struct DicomFilePath; + +/// Path in storage to a DICOM series folder. +#[braid(serde)] +pub(crate) struct SeriesPath; + +impl From for SeriesPath { + fn from(path: DicomFilePath) -> Self { + path.as_str() + .rsplit_once('/') + .map(|(dir, _fname)| dir) + .map(Self::from) + .unwrap() + } +} + +impl From> for DicomInfo { + fn from(value: DicomInfo) -> Self { + Self { + PatientID: value.PatientID, + StudyDate: value.StudyDate, + StudyInstanceUID: value.StudyInstanceUID, + SeriesInstanceUID: value.SeriesInstanceUID, + pacs_name: value.pacs_name, + path: value.path.into(), + PatientName: value.PatientName, + PatientBirthDate: value.PatientBirthDate, + PatientAge: value.PatientAge, + PatientSex: value.PatientSex, + AccessionNumber: value.AccessionNumber, + Modality: value.Modality, + ProtocolName: value.ProtocolName, + StudyDescription: value.StudyDescription, + SeriesDescription: value.SeriesDescription, + } + } +} + +/// The DICOM series metadata needed for *CUBE*'s serializer to register a PACS series +/// as a `PACSSeries` object. +#[derive(Debug, Clone)] +pub(crate) struct DicomInfo

{ + pub PatientID: String, + pub StudyDate: time::Date, + pub StudyInstanceUID: String, + pub SeriesInstanceUID: String, + pub pacs_name: AETitle, + pub path: P, + pub PatientName: Option, + pub PatientBirthDate: Option, + pub PatientAge: Option, + pub PatientSex: Option, + pub AccessionNumber: Option, + pub Modality: Option, + pub ProtocolName: Option, + pub StudyDescription: Option, + pub SeriesDescription: Option, +} + +impl DicomInfo { + /// Create task. + pub fn into_task(self, ndicom: usize) -> Signature { + register_pacs_series::new( + self.PatientID, + self.StudyDate + .format(format_description!("[year]-[month]-[day]")) + .unwrap(), + self.StudyInstanceUID, + self.SeriesInstanceUID, + self.pacs_name, + self.path, + ndicom, + self.PatientName, + self.PatientBirthDate, + self.PatientAge, + self.PatientSex, + self.AccessionNumber, + self.Modality, + self.ProtocolName, + self.StudyDescription, + self.SeriesDescription, + ) + } +} + +/// A DICOM series and the number of files received for it. +pub(crate) struct SeriesCount { + pub info: DicomInfo, + pub count: usize, +} + +impl SeriesCount { + pub(crate) fn new(dcm: DicomInfo) -> Self { + Self { + count: 1, + info: dcm.into(), + } + } + + pub(crate) fn into_task(self) -> Signature { + self.info.into_task(self.count) + } +} + +/// An [SeriesEvent] for a pending task of writing a DICOM file to storage. +pub(crate) type PendingDicomInstance = SeriesEvent>, SeriesCount>; + +/// The set of metadata which uniquely identifies a DICOM series in *CUBE*. +/// +/// https://github.com/FNNDSC/ChRIS_ultron_backEnd/blob/v6.1.0/chris_backend/pacsfiles/models.py#L60 +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct SeriesKey { + /// Series instance UID + #[allow(non_snake_case)] + pub SeriesInstanceUID: String, + /// AE title of PACS the series was received from + pub pacs_name: AETitle, +} + +impl SeriesKey { + pub fn new(series_instance_uid: String, pacs_name: AETitle) -> Self { + Self { + SeriesInstanceUID: series_instance_uid, + pacs_name, + } + } +} diff --git a/tests/assertions/expected.rs b/tests/assertions/expected.rs new file mode 100644 index 0000000..c1291fb --- /dev/null +++ b/tests/assertions/expected.rs @@ -0,0 +1,44 @@ +use crate::assertions::model::SeriesParams; +use std::collections::HashSet; +use std::sync::LazyLock; + +pub static EXPECTED_SERIES: LazyLock> = LazyLock::new(|| { + HashSet::from([ + SeriesParams { + PatientID: "1449c1d".to_string(), + StudyDate: "2013-03-08".to_string(), + StudyInstanceUID: "1.2.840.113845.11.1000000001785349915.20130308061609.6346698".to_string(), + SeriesInstanceUID: "1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0".to_string(), + pacs_name: "OXITESTORTHANC".to_string(), + path: "SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06".to_string(), + ndicom: 192, + PatientName: Some("anonymized".to_string()), + PatientBirthDate: Some("20090701".to_string()), + PatientAge: Some(1096, ), + PatientSex: Some("M".to_string()), + AccessionNumber: Some("98edede8b2".to_string()), + Modality: Some("MR".to_string()), + ProtocolName: Some("SAG MPRAGE 220 FOV".to_string()), + StudyDescription: Some("MR-Brain w/o Contrast".to_string()), + SeriesDescription: Some("SAG MPRAGE 220 FOV".to_string()) + }, + SeriesParams { + PatientID: "02".to_string(), + StudyDate: "2013-07-17".to_string(), + StudyInstanceUID: "1.2.826.0.1.3680043.2.1143.2592092611698916978113112155415165916".to_string(), + SeriesInstanceUID: "1.2.826.0.1.3680043.2.1143.515404396022363061013111326823367652".to_string(), + pacs_name: "OXITESTORTHANC".to_string(), + path: "SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc".to_string(), + ndicom: 384, + PatientName: Some("Jane_Doe".to_string()), + PatientBirthDate: Some("19660101".to_string()), + PatientAge: None, + PatientSex: Some("F".to_string()), + AccessionNumber: None, + Modality: Some("MR".to_string()), + ProtocolName: Some("anat-T1w".to_string()), + StudyDescription: Some("Hanke_Stadler^0024_transrep".to_string()), + SeriesDescription: Some("anat-T1w".to_string()), + }, + ]) +}); diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index dd662a9..b2b563d 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -1,95 +1,98 @@ -use chris::types::{CubeUrl, Username}; -use chris::ChrisClient; -use figment::providers::Env; -use figment::Figment; +mod expected; +mod model; -use crate::{CALLED_AE_TITLE, EXAMPLE_SERIES_INSTANCE_UIDS}; +pub use expected::EXPECTED_SERIES; +use std::collections::HashSet; -pub async fn run_assertions(expected_counts: &[usize]) { - let client = get_client_from_env().await; - for (series, expected_count) in EXAMPLE_SERIES_INSTANCE_UIDS - .iter() - .zip(expected_counts.into_iter()) - { - let actual_count = client - .pacsfiles() - .series_instance_uid(*series) - .pacs_identifier(CALLED_AE_TITLE) - .search() - .get_count() - .await - .unwrap(); - assert_eq!(actual_count, *expected_count); - - // the "Oxidicom Custom Metadata" spec should store the NumberOfSeriesRelatedInstances - // in a blank file with the filename NumberOfSeriesRelatedInstances=value, - // and searchable by ProtocolName. - let custom_file_num_related = client.pacsfiles() - .pacs_identifier(oxidicom::OXIDICOM_CUSTOM_PACS_NAME) - .series_instance_uid(*series) - .protocol_name("NumberOfSeriesRelatedInstances") - .search() - .get_first() - .await - .unwrap() - .expect("\"Oxidicom Custom Metadata\" file for NumberOfSeriesRelatedInstances not found. Usually, it should be registered before all DICOM instances are done being registered.") - .object; +use crate::assertions::model::SeriesParams; +use camino::Utf8Path; +use celery::broker::{AMQPBrokerBuilder, BrokerBuilder}; +use celery::prelude::BrokerError; +use celery::protocol::MessageBody; +use futures::{stream, StreamExt, TryStreamExt}; +use oxidicom::register_pacs_series; - // The value should be stored as the SeriesDescription - let actual_value = custom_file_num_related.series_description; - let expected_value = Some(expected_count.to_string()); - assert_eq!(actual_value, expected_value); - - let actual_basename = custom_file_num_related - .fname - .as_str() - .rsplit_once('/') - .map(|(_l, r)| r) - .unwrap_or(custom_file_num_related.fname.as_str()); - let expected_basename = format!("NumberOfSeriesRelatedInstances={expected_count}"); - assert_eq!(actual_basename, &expected_basename); +pub async fn assert_files_stored(storage_path: &Utf8Path) { + stream::iter(&*EXPECTED_SERIES) + .for_each_concurrent(EXPECTED_SERIES.len(), |series| { + assert_series_path(storage_path, series) + }) + .await; +} - // the "Oxidicom Custom Metadata" spec should store the OxidicomAttemptedPushCount - // in a blank file with the filename OxidicomAttemptedPushCount=value, - // and searchable by ProtocolName. - let custom_file_num_attempts = client.pacsfiles() - .pacs_identifier(oxidicom::OXIDICOM_CUSTOM_PACS_NAME) - .series_instance_uid(*series) - .protocol_name("OxidicomAttemptedPushCount") - .search() - .get_first() - .await - .unwrap() - .expect("\"Oxidicom Custom Metadata\" file for OxidicomAttemptedPushCount not found. It should be registered after the last DICOM file was pushed.") - .object; - assert_eq!( - custom_file_num_attempts.series_description, - Some(expected_count.to_string()) - ) - } +async fn assert_series_path(storage_path: &Utf8Path, series: &SeriesParams) { + let series_dir = storage_path.join(&series.path); + let count = tokio::fs::read_dir(series_dir) + .await + .map(tokio_stream::wrappers::ReadDirStream::new) + .unwrap() + .map(|result| async { + let entry = result.unwrap(); + assert!( + entry.file_type().await.unwrap().is_file(), + "{:?} is not a file. PACSSeries folder may only contain files.", + entry.path() + ); + assert_eq!( + entry + .path() + .extension() + .expect("Found file without file extension") + .to_str() + .expect("Found file with invalid UTF-8 file extension"), + ".dcm", + "{:?} does not have a .dcm file extension.", + entry.path() + ); + entry + }) + .count() + .await; + assert_eq!(count, series.ndicom); } -async fn get_client_from_env() -> ChrisClient { - let TestSettings { - url, - username, - password, - } = Figment::from(Env::prefixed("OXIDICOM_TEST_")) - .extract() +pub async fn assert_rabbitmq_messages(address: &str, queue_name: &str) { + let broker = Box::new(AMQPBrokerBuilder::new(address)) + .declare_queue(queue_name) + .build(1000) + .await .unwrap(); + let error_handler = Box::new(move |e: BrokerError| panic!("{:?}", e)); + let (_consumer_tag, consumer) = broker.consume(queue_name, error_handler).await.unwrap(); - let account = chris::Account::new(&url, &username, &password); - let token = account.get_token().await.unwrap(); - ChrisClient::build(url, username, token) - .unwrap() - .connect() + // Deserialize deliveries into messages + let messages_stream = consumer.try_filter_map(|delivery| async move { + delivery.ack().await.unwrap(); + let body = delivery + .try_deserialize_message() + .and_then(|m| m.body::()) + .unwrap(); + Ok(Some(body)) + }); + + // Read the expected number of messages from the stream + let params: HashSet = stream::iter(0..EXPECTED_SERIES.len()) + .zip(messages_stream) + .map(|(_, r)| r) + .try_filter_map(|r| async { Ok(Some(deserialize_params(r))) }) + .try_collect() .await - .unwrap() + .map_err(|e| format!("{}", e)) + .unwrap(); + + assert_eq!(&*EXPECTED_SERIES, ¶ms); } -#[derive(serde::Deserialize)] -struct TestSettings { - url: CubeUrl, - username: Username, - password: String, +fn deserialize_params( + body: MessageBody, +) -> D { + if let serde_json::Value::Array(v) = serde_json::to_value(body).unwrap() { + return v + .into_iter() + .map(serde_json::from_value) + .filter_map(|r| r.ok()) + .next() + .expect("No elements were deserializable to the specified type."); + } + panic!("Expected body to be an array, but it is not.") } diff --git a/tests/assertions/model.rs b/tests/assertions/model.rs new file mode 100644 index 0000000..5c0d786 --- /dev/null +++ b/tests/assertions/model.rs @@ -0,0 +1,22 @@ +#![allow(non_snake_case)] + +/// Parameters of the [oxidicom::register_pacs_series] celery task. +#[derive(Debug, Hash, Eq, PartialEq, serde::Deserialize)] +pub struct SeriesParams { + pub PatientID: String, + pub StudyDate: String, + pub StudyInstanceUID: String, + pub SeriesInstanceUID: String, + pub pacs_name: String, + pub path: String, + pub ndicom: usize, + pub PatientName: Option, + pub PatientBirthDate: Option, + pub PatientAge: Option, + pub PatientSex: Option, + pub AccessionNumber: Option, + pub Modality: Option, + pub ProtocolName: Option, + pub StudyDescription: Option, + pub SeriesDescription: Option, +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 94fd3f5..0087e5e 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,23 +1,15 @@ -use futures::StreamExt; - -use oxidicom::run_everything_from_env; - -use crate::assertions::run_assertions; +use crate::assertions::{assert_files_stored, assert_rabbitmq_messages, EXPECTED_SERIES}; use crate::orthanc_client::orthanc_store; +use camino::Utf8Path; +use futures::StreamExt; +use oxidicom::{run_everything, DicomRsSettings, OxidicomEnvOptions}; +use std::num::NonZeroUsize; mod assertions; mod orthanc_client; -const EXAMPLE_SERIES_INSTANCE_UIDS: [&str; 2] = [ - // https://github.com/FNNDSC/SAG-anon - "1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0", - // https://github.com/datalad/example-dicom-structural - "1.2.826.0.1.3680043.2.1143.515404396022363061013111326823367652", -]; - -const ORTHANC_URL: &str = "http://orthanc:8042"; +const ORTHANC_URL: &str = "http://localhost:8042"; const CALLING_AE_TITLE: &str = "OXIDICOMTEST"; -const CALLED_AE_TITLE: &str = "OXITESTORTHANC"; /// Runs the DICOM listener and pushes 2 series to it in parallel. #[tokio::test(flavor = "multi_thread")] @@ -29,22 +21,55 @@ async fn test_run_everything_from_env() { ) .unwrap(); - let server_handle = tokio::spawn(run_everything_from_env(Some( - EXAMPLE_SERIES_INSTANCE_UIDS.len(), - ))); - // N.B. it might be necessary to wait for the TCP server to come up. - // tokio::time::sleep(std::time::Duration::from_secs(5)).await; - let instances_counts: Vec<_> = futures::stream::iter(EXAMPLE_SERIES_INSTANCE_UIDS) - .map(|series_instance_uid| async move { + let queue_name = names::Generator::default().next().unwrap(); + let temp_dir = tempfile::tempdir().unwrap(); + let temp_dir_path = Utf8Path::from_path(temp_dir.path()).unwrap(); + + let num_to_handle = Some(EXPECTED_SERIES.len()); + let options = create_test_options(temp_dir_path, queue_name.to_string()); + let amqp_address = options.amqp_address.clone(); + let (start_tx, mut start_rx) = tokio::sync::mpsc::unbounded_channel(); + let on_start = move |x| start_tx.send(x).unwrap(); + let server = run_everything(options, num_to_handle, Some(on_start)); + let server_handle = tokio::spawn(server); + + // wait for message from `on_start` indicating server is ready for connections + start_rx.recv().await.unwrap(); + + futures::stream::iter(EXPECTED_SERIES.iter().map(|s| s.SeriesInstanceUID.as_str())) + .for_each_concurrent(4, |series_instance_uid| async move { let res = orthanc_store(ORTHANC_URL, CALLING_AE_TITLE, series_instance_uid) .await .unwrap(); assert_eq!(res.failed_instances_count, 0); - res.instances_count }) - .buffered(4) - .collect() .await; server_handle.await.unwrap().unwrap(); - run_assertions(&instances_counts).await; + + tokio::join!( + assert_files_stored(&temp_dir_path), + assert_rabbitmq_messages(&amqp_address, &queue_name) + ); +} + +fn create_test_options>( + files_root: P, + queue_name: String, +) -> OxidicomEnvOptions { + OxidicomEnvOptions { + amqp_address: "amqp://localhost:5672".to_string(), + files_root: files_root.as_ref().to_path_buf(), + queue_name, + progress_nats_address: None, + progress_interval: Default::default(), + scp: DicomRsSettings { + aet: "OXIDICOMTEST".to_string(), + strict: false, + uncompressed_only: false, + promiscuous: true, + }, + scp_max_pdu_length: 16384, + listener_threads: NonZeroUsize::new(2).unwrap(), + listener_port: 11112, + } } diff --git a/tests/orthanc_client/mod.rs b/tests/orthanc_client/mod.rs index dc3392e..5625fb0 100644 --- a/tests/orthanc_client/mod.rs +++ b/tests/orthanc_client/mod.rs @@ -91,6 +91,7 @@ struct StoreRequest { timeout: u32, } +#[allow(unused)] #[derive(serde::Deserialize)] #[serde(rename_all = "PascalCase")] pub struct StoreResponse { From 061edde2081556cf730b46e5afc10e8aa03492ec Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Fri, 6 Sep 2024 23:59:16 -0400 Subject: [PATCH 05/44] Add Swatinem/rust-cache --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfbabdd..af00850 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: run: docker compose up -d - name: Download example data run: docker compose run --rm get-data + - name: Cache rust build + uses: Swatinem/rust-cache@v2 - name: Compile test binary run: cargo test --no-run - name: Run tests From ffefcb920bc95f161522b61d2be542e98a052612 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sat, 7 Sep 2024 00:02:29 -0400 Subject: [PATCH 06/44] Add CARGO_TERM_COLOR=always --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af00850..55379bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: test: name: Test runs-on: ubuntu-24.04 + env: + CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v4 - name: Start services From e078b496d60361ce5306270b12b29799aef2fba8 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sat, 7 Sep 2024 13:28:51 -0400 Subject: [PATCH 07/44] Assert files against snapshot --- Cargo.lock | 60 +++- Cargo.toml | 3 + src/listener_tcp_loop.rs | 4 +- tests/assertions/mod.rs | 73 +++-- tests/expected_files.txt | 576 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 676 insertions(+), 40 deletions(-) create mode 100644 tests/expected_files.txt diff --git a/Cargo.lock b/Cargo.lock index ccfb439..959d850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock 3.4.0", + "blocking", + "futures-lite 2.3.0", +] + [[package]] name = "async-global-executor" version = "2.4.1" @@ -332,6 +343,17 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "async-walkdir" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20235b6899dd1cb74a9afac0abf5b4a20c0e500dd6537280f4096e1b9f14da20" +dependencies = [ + "async-fs", + "futures-lite 2.3.0", + "thiserror", +] + [[package]] name = "atomic" version = "0.6.0" @@ -1040,6 +1062,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -2285,6 +2313,7 @@ version = "3.0.0-a1" dependencies = [ "aliri_braid", "anyhow", + "async-walkdir", "camino", "celery", "dicom", @@ -2297,6 +2326,8 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", + "pathdiff", + "pretty_assertions", "regex", "reqwest", "rstest", @@ -2366,6 +2397,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +dependencies = [ + "camino", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -2384,7 +2424,7 @@ checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" dependencies = [ "inlinable_string", "pear_codegen", - "yansi", + "yansi 1.0.1", ] [[package]] @@ -2561,6 +2601,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi 0.5.1", +] + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -2613,7 +2663,7 @@ dependencies = [ "quote", "syn 2.0.52", "version_check", - "yansi", + "yansi 1.0.1", ] [[package]] @@ -4197,6 +4247,12 @@ dependencies = [ "time", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 058dbec..9732bae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,6 @@ tempfile = "3.12.0" names = "0.14.0" serde_json = "1.0.128" tokio-stream = { version = "0.1.16", features = ["fs"] } +async-walkdir = "2.0.0" +pathdiff = { version = "0.2.1", features = ["camino"] } +pretty_assertions = "1.4.0" diff --git a/src/listener_tcp_loop.rs b/src/listener_tcp_loop.rs index a9dcd2c..af1d5cf 100644 --- a/src/listener_tcp_loop.rs +++ b/src/listener_tcp_loop.rs @@ -26,7 +26,9 @@ where F: FnOnce(SocketAddrV4), { let listener = TcpListener::bind(address)?; - if let Some(f) = on_start { f(address) }; + if let Some(f) = on_start { + f(address) + }; let mut pool = ThreadPool::new(n_threads, "dicom_listener"); let options = Arc::new(config.into()); diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index b2b563d..f9cec97 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -1,54 +1,53 @@ mod expected; mod model; -pub use expected::EXPECTED_SERIES; -use std::collections::HashSet; - use crate::assertions::model::SeriesParams; -use camino::Utf8Path; +use async_walkdir::WalkDir; +use camino::{Utf8Path, Utf8PathBuf}; use celery::broker::{AMQPBrokerBuilder, BrokerBuilder}; use celery::prelude::BrokerError; use celery::protocol::MessageBody; +pub use expected::EXPECTED_SERIES; use futures::{stream, StreamExt, TryStreamExt}; use oxidicom::register_pacs_series; +use std::collections::HashSet; pub async fn assert_files_stored(storage_path: &Utf8Path) { - stream::iter(&*EXPECTED_SERIES) - .for_each_concurrent(EXPECTED_SERIES.len(), |series| { - assert_series_path(storage_path, series) - }) - .await; + let (expected, actual) = tokio::join!(expected_files(), find_files(storage_path)); + pretty_assertions::assert_eq!(expected, actual) } -async fn assert_series_path(storage_path: &Utf8Path, series: &SeriesParams) { - let series_dir = storage_path.join(&series.path); - let count = tokio::fs::read_dir(series_dir) - .await - .map(tokio_stream::wrappers::ReadDirStream::new) - .unwrap() - .map(|result| async { - let entry = result.unwrap(); - assert!( - entry.file_type().await.unwrap().is_file(), - "{:?} is not a file. PACSSeries folder may only contain files.", - entry.path() - ); - assert_eq!( - entry - .path() - .extension() - .expect("Found file without file extension") - .to_str() - .expect("Found file with invalid UTF-8 file extension"), - ".dcm", - "{:?} does not have a .dcm file extension.", - entry.path() - ); - entry +async fn expected_files() -> Vec { + let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("expected_files.txt"); + let content = tokio::fs::read_to_string(path).await.unwrap(); + content + .split("\n") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() +} + +async fn find_files(storage_path: &Utf8Path) -> Vec { + let mut files: Vec = WalkDir::new(storage_path) + .try_filter_map(|entry| async move { + if entry.file_type().await.unwrap().is_file() { + let path = Utf8PathBuf::from_path_buf(entry.path()) + .map(|p| pathdiff::diff_utf8_paths(p, storage_path).unwrap()) + .map(|p| p.into_string()) + .expect("Invalid UTF-8 path found"); + Ok(Some(path)) + } else { + Ok(None) + } }) - .count() - .await; - assert_eq!(count, series.ndicom); + .try_collect() + .await + .unwrap(); + files.sort(); + files } pub async fn assert_rabbitmq_messages(address: &str, queue_name: &str) { diff --git a/tests/expected_files.txt b/tests/expected_files.txt new file mode 100644 index 0000000..b6fffcd --- /dev/null +++ b/tests/expected_files.txt @@ -0,0 +1,576 @@ +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0001-1.2.826.0.1.3680043.2.1143.1590429688519720198888333603882344634.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0002-1.2.826.0.1.3680043.2.1143.2597549897469968192491738327176465409.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0003-1.2.826.0.1.3680043.2.1143.1967017491222927818930210085422605097.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0004-1.2.826.0.1.3680043.2.1143.2955466775247431831876523008273966445.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0005-1.2.826.0.1.3680043.2.1143.2553993609941932463220238641968906197.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0006-1.2.826.0.1.3680043.2.1143.679395799709023456632013299715024484.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0007-1.2.826.0.1.3680043.2.1143.4997326513949586306349505422781500349.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0008-1.2.826.0.1.3680043.2.1143.4155843983835982918633958751440201708.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0009-1.2.826.0.1.3680043.2.1143.3463273594277583079395132518247896473.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0010-1.2.826.0.1.3680043.2.1143.5973973204096910287068276726313442611.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0011-1.2.826.0.1.3680043.2.1143.302830525625070736876235947703110492.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0012-1.2.826.0.1.3680043.2.1143.7590262421094656055050458765807940492.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0013-1.2.826.0.1.3680043.2.1143.2670366524754195257819794217717456167.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0014-1.2.826.0.1.3680043.2.1143.1061453621628462197938188256131326926.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0015-1.2.826.0.1.3680043.2.1143.4894992123954961188203212932790791388.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0016-1.2.826.0.1.3680043.2.1143.8560416497416786237016278308075766151.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0017-1.2.826.0.1.3680043.2.1143.9252751336560909425545248973719477040.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0018-1.2.826.0.1.3680043.2.1143.5861393573137533838406807401424911863.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0019-1.2.826.0.1.3680043.2.1143.5364776790865331447142145204366720958.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0020-1.2.826.0.1.3680043.2.1143.743025912380023785728574397312556586.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0021-1.2.826.0.1.3680043.2.1143.3055050769746224818251231647122161825.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0022-1.2.826.0.1.3680043.2.1143.5327774415585413133992957891916057377.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0023-1.2.826.0.1.3680043.2.1143.2552780570893626570528108309801460590.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0024-1.2.826.0.1.3680043.2.1143.1492752101402940729675901239957053244.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0025-1.2.826.0.1.3680043.2.1143.3241344159453926134840804231055758196.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0026-1.2.826.0.1.3680043.2.1143.7312182773192728628985578316265780368.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0027-1.2.826.0.1.3680043.2.1143.3928476316002857611712909000542668398.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0028-1.2.826.0.1.3680043.2.1143.4748554213706852790597788309209971531.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0029-1.2.826.0.1.3680043.2.1143.8286139588891670051174746162121542902.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0030-1.2.826.0.1.3680043.2.1143.9582794224126159438698485849601649456.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0031-1.2.826.0.1.3680043.2.1143.8008913717169098540610792347819054601.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0032-1.2.826.0.1.3680043.2.1143.4897046450570154352509022322319310401.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0033-1.2.826.0.1.3680043.2.1143.5151600375812859568010851736474405411.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0034-1.2.826.0.1.3680043.2.1143.4896294121636756235462529270464616139.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0035-1.2.826.0.1.3680043.2.1143.6770293666349999655271612283038258177.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0036-1.2.826.0.1.3680043.2.1143.1154346563990638562326432365795675060.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0037-1.2.826.0.1.3680043.2.1143.4767336425999104619702632569579893625.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0038-1.2.826.0.1.3680043.2.1143.2893793478528639664393063042139667733.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0039-1.2.826.0.1.3680043.2.1143.9008596485616503279922647854247838256.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0040-1.2.826.0.1.3680043.2.1143.4614123079304626220424198355893147949.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0041-1.2.826.0.1.3680043.2.1143.6951414199280058659914361649192913568.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0042-1.2.826.0.1.3680043.2.1143.7161047984065961680774538916607291055.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0043-1.2.826.0.1.3680043.2.1143.2892703532902083136406301031507553128.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0044-1.2.826.0.1.3680043.2.1143.294956075194107091733086230082778096.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0045-1.2.826.0.1.3680043.2.1143.3671535254214595454810274502988630237.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0046-1.2.826.0.1.3680043.2.1143.5173373512954465520832173671744978651.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0047-1.2.826.0.1.3680043.2.1143.4207161188173045757633079049319763314.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0048-1.2.826.0.1.3680043.2.1143.5301902125924357990830520376227991324.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0049-1.2.826.0.1.3680043.2.1143.412702095042663238453880758465072849.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0050-1.2.826.0.1.3680043.2.1143.6759705588563767569000332385671297036.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0051-1.2.826.0.1.3680043.2.1143.9141078499851262544391185092492716416.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0052-1.2.826.0.1.3680043.2.1143.657470508961452889246451710490593864.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0053-1.2.826.0.1.3680043.2.1143.6037684724636976865826458363677112066.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0054-1.2.826.0.1.3680043.2.1143.4405248901967801773523291919728491144.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0055-1.2.826.0.1.3680043.2.1143.8087198326058989105554586497199388747.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0056-1.2.826.0.1.3680043.2.1143.418408249846549219035757444900963936.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0057-1.2.826.0.1.3680043.2.1143.193220425943013254586433409457556752.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0058-1.2.826.0.1.3680043.2.1143.7758672135757813635194405929796555317.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0059-1.2.826.0.1.3680043.2.1143.38097696044873551249191548978833883.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0060-1.2.826.0.1.3680043.2.1143.5795541876649045130332353563167556906.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0061-1.2.826.0.1.3680043.2.1143.771609997669478621223333138319150922.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0062-1.2.826.0.1.3680043.2.1143.4408971169453586856073077596984192273.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0063-1.2.826.0.1.3680043.2.1143.4750818075837536590089800891398573685.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0064-1.2.826.0.1.3680043.2.1143.4117879147820252038218860424037769845.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0065-1.2.826.0.1.3680043.2.1143.166641280421005512028266119680283386.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0066-1.2.826.0.1.3680043.2.1143.303432897634759592673716043075664082.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0067-1.2.826.0.1.3680043.2.1143.5900086019208279127799273158479431540.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0068-1.2.826.0.1.3680043.2.1143.992867616261533113934159180352952259.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0069-1.2.826.0.1.3680043.2.1143.3871003033375610387922643255827436882.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0070-1.2.826.0.1.3680043.2.1143.497703992301774027294273539069959020.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0071-1.2.826.0.1.3680043.2.1143.1102601322713901590979434159715568715.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0072-1.2.826.0.1.3680043.2.1143.4142443109023657935772879023282517995.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0073-1.2.826.0.1.3680043.2.1143.2673667068614334906349638894231252381.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0074-1.2.826.0.1.3680043.2.1143.1233661567460749961179920397672980134.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0075-1.2.826.0.1.3680043.2.1143.1076130949714731453910996546554166953.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0076-1.2.826.0.1.3680043.2.1143.2431868462787905898450355083679694589.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0077-1.2.826.0.1.3680043.2.1143.3040107651975515971727451443643989104.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0078-1.2.826.0.1.3680043.2.1143.3422475331472744056917054417355734314.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0079-1.2.826.0.1.3680043.2.1143.102075515710755040744196748299590272.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0080-1.2.826.0.1.3680043.2.1143.7677973578614438578403590768225513305.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0081-1.2.826.0.1.3680043.2.1143.1298481056420344049305664417253934540.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0082-1.2.826.0.1.3680043.2.1143.2409440321484222292955929557407077579.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0083-1.2.826.0.1.3680043.2.1143.9625323981197495460705421017648011058.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0084-1.2.826.0.1.3680043.2.1143.7403604751636615534296608024359252332.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0085-1.2.826.0.1.3680043.2.1143.3612168303290256759837636161619640066.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0086-1.2.826.0.1.3680043.2.1143.5243016030243653560955768772939071067.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0087-1.2.826.0.1.3680043.2.1143.6905243767445160481765117185254074769.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0088-1.2.826.0.1.3680043.2.1143.6113005834470191814271973777529407667.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0089-1.2.826.0.1.3680043.2.1143.7982500895562275475328721493984461953.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0090-1.2.826.0.1.3680043.2.1143.3954988608814349145586492612326221140.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0091-1.2.826.0.1.3680043.2.1143.4898507287876643568660713408098865752.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0092-1.2.826.0.1.3680043.2.1143.4503785709384533626721342107549746819.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0093-1.2.826.0.1.3680043.2.1143.9863750225086692060639115392569017504.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0094-1.2.826.0.1.3680043.2.1143.4576344477446989468155663024407158687.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0095-1.2.826.0.1.3680043.2.1143.8506652058919218029042003352327552649.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0096-1.2.826.0.1.3680043.2.1143.9276455181002391008563173484081971179.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0097-1.2.826.0.1.3680043.2.1143.9943771359810482953448109650403047475.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0098-1.2.826.0.1.3680043.2.1143.3825126424630052958135743830727870185.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0099-1.2.826.0.1.3680043.2.1143.9934110981241721938237066753897456912.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0100-1.2.826.0.1.3680043.2.1143.7863421630163401023414619319672109131.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0101-1.2.826.0.1.3680043.2.1143.2870638906174863237200358993520410265.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0102-1.2.826.0.1.3680043.2.1143.6513686727223515859906265385714561202.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0103-1.2.826.0.1.3680043.2.1143.9963371674208539796845605027946764817.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0104-1.2.826.0.1.3680043.2.1143.160772462256888015017915911251571404.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0105-1.2.826.0.1.3680043.2.1143.2707168259543455202813407188113405581.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0106-1.2.826.0.1.3680043.2.1143.9979793508250394756437371044589161623.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0107-1.2.826.0.1.3680043.2.1143.6391272319116875502564471898364193616.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0108-1.2.826.0.1.3680043.2.1143.5476653822299124760632870359072359562.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0109-1.2.826.0.1.3680043.2.1143.6322065350367499070836115239556784492.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0110-1.2.826.0.1.3680043.2.1143.3807512411542837285702701993999871305.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0111-1.2.826.0.1.3680043.2.1143.8383751579987247273947413640322140299.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0112-1.2.826.0.1.3680043.2.1143.4097004122935744078049703931598316293.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0113-1.2.826.0.1.3680043.2.1143.6898866874717061195058285330235518296.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0114-1.2.826.0.1.3680043.2.1143.6236626800646362335969268100933843358.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0115-1.2.826.0.1.3680043.2.1143.1310428228287054469166746946245552260.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0116-1.2.826.0.1.3680043.2.1143.2823349364056260883187215057506917721.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0117-1.2.826.0.1.3680043.2.1143.6102055073207729120631386213476383321.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0118-1.2.826.0.1.3680043.2.1143.3317429560882451242290611312611254515.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0119-1.2.826.0.1.3680043.2.1143.6655106967489948112614452596516217548.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0120-1.2.826.0.1.3680043.2.1143.6243050417519550823030740349663399474.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0121-1.2.826.0.1.3680043.2.1143.8248943958489881079589884166149536950.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0122-1.2.826.0.1.3680043.2.1143.6586225001811206194872016179050824866.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0123-1.2.826.0.1.3680043.2.1143.8609367810193655639093325010680317421.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0124-1.2.826.0.1.3680043.2.1143.2583179749099018545814587813660587729.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0125-1.2.826.0.1.3680043.2.1143.4144037137185088415640572376508278706.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0126-1.2.826.0.1.3680043.2.1143.7184707048063582188955307902033833438.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0127-1.2.826.0.1.3680043.2.1143.7572785057625744499595817818997950327.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0128-1.2.826.0.1.3680043.2.1143.2903750169622448468709780274002444606.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0129-1.2.826.0.1.3680043.2.1143.6021622685381728802152259549880521207.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0130-1.2.826.0.1.3680043.2.1143.7832173177239431474998415965836742594.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0131-1.2.826.0.1.3680043.2.1143.7918919638745026544293051009014003639.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0132-1.2.826.0.1.3680043.2.1143.2756319650237329225313705728135058460.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0133-1.2.826.0.1.3680043.2.1143.9116107933781936223237333371360037015.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0134-1.2.826.0.1.3680043.2.1143.4013521601111595871959368114353546710.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0135-1.2.826.0.1.3680043.2.1143.9330749224365435757905281815403865025.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0136-1.2.826.0.1.3680043.2.1143.8283346042427966635860744927165840985.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0137-1.2.826.0.1.3680043.2.1143.5143263488390263549072338615358249756.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0138-1.2.826.0.1.3680043.2.1143.685741832755955344904518333867695943.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0139-1.2.826.0.1.3680043.2.1143.4763823967413970570770834106332076996.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0140-1.2.826.0.1.3680043.2.1143.7956982671845857198763198802102199296.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0141-1.2.826.0.1.3680043.2.1143.5329519633036213175860933457626573317.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0142-1.2.826.0.1.3680043.2.1143.4320958276389900769020505608833317125.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0143-1.2.826.0.1.3680043.2.1143.7951563915149968060103021763506991764.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0144-1.2.826.0.1.3680043.2.1143.9062633712058552434004665856273480587.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0145-1.2.826.0.1.3680043.2.1143.8422846942303802705098846103181037316.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0146-1.2.826.0.1.3680043.2.1143.1580769685201563864008120899300072887.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0147-1.2.826.0.1.3680043.2.1143.5268298712677931976976096400023382367.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0148-1.2.826.0.1.3680043.2.1143.2155185363583489318092474728637849698.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0149-1.2.826.0.1.3680043.2.1143.932259828245003453290487866838266154.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0150-1.2.826.0.1.3680043.2.1143.7210638939331882537867606096148596333.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0151-1.2.826.0.1.3680043.2.1143.4292000598269219065705761603175494762.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0152-1.2.826.0.1.3680043.2.1143.4703749660158810929993212941284640441.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0153-1.2.826.0.1.3680043.2.1143.6708686119688950536135713423711455008.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0154-1.2.826.0.1.3680043.2.1143.3976893502364806003153165781166784979.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0155-1.2.826.0.1.3680043.2.1143.4520877352286282408403378065937440525.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0156-1.2.826.0.1.3680043.2.1143.1026463800913653588114055926363365185.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0157-1.2.826.0.1.3680043.2.1143.5625796541063076701135583697947870535.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0158-1.2.826.0.1.3680043.2.1143.4637438974843474254684694610797024707.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0159-1.2.826.0.1.3680043.2.1143.9233540823745613544536696948291923991.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0160-1.2.826.0.1.3680043.2.1143.8227516348202602698820014187357380294.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0161-1.2.826.0.1.3680043.2.1143.2814413083442543410142174737152196884.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0162-1.2.826.0.1.3680043.2.1143.1526703321072235983012507309944944931.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0163-1.2.826.0.1.3680043.2.1143.3503994489912552022422294322225286043.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0164-1.2.826.0.1.3680043.2.1143.2930818456096067141342736598879300183.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0165-1.2.826.0.1.3680043.2.1143.948538460145509675573071387601162020.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0166-1.2.826.0.1.3680043.2.1143.1386231354762079540986653976983908504.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0167-1.2.826.0.1.3680043.2.1143.8271765929044460647588577709683667978.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0168-1.2.826.0.1.3680043.2.1143.5981715074323114214870145236057218784.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0169-1.2.826.0.1.3680043.2.1143.5288417713859106604743112533071149283.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0170-1.2.826.0.1.3680043.2.1143.9352124035885059531467089293335895511.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0171-1.2.826.0.1.3680043.2.1143.7535483020628362081330571272340841868.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0172-1.2.826.0.1.3680043.2.1143.1694412475124881600501214935170764588.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0173-1.2.826.0.1.3680043.2.1143.7490637592650401301689335601421567841.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0174-1.2.826.0.1.3680043.2.1143.1015221699824893357824073389871590444.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0175-1.2.826.0.1.3680043.2.1143.1707623456140587286479028829348425434.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0176-1.2.826.0.1.3680043.2.1143.3107341573219625277637581876130127218.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0177-1.2.826.0.1.3680043.2.1143.5206855517062981894711626060424748802.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0178-1.2.826.0.1.3680043.2.1143.1007991686751254541238545094791980712.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0179-1.2.826.0.1.3680043.2.1143.6209771500556037377956423068636241589.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0180-1.2.826.0.1.3680043.2.1143.296190950953413355069207694506855124.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0181-1.2.826.0.1.3680043.2.1143.3067108651085104892179825355091023056.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0182-1.2.826.0.1.3680043.2.1143.7505594616083043605043809065916281153.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0183-1.2.826.0.1.3680043.2.1143.9579785695361991933527670405390204180.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0184-1.2.826.0.1.3680043.2.1143.114323873832570745344499349301536364.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0185-1.2.826.0.1.3680043.2.1143.5700112888294994218862144407811529409.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0186-1.2.826.0.1.3680043.2.1143.9806733433642064486196725350074401083.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0187-1.2.826.0.1.3680043.2.1143.9599889455606910754368810788249051532.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0188-1.2.826.0.1.3680043.2.1143.9745717643660887395523327049101570063.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0189-1.2.826.0.1.3680043.2.1143.6219043514656200513835093807878553313.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0190-1.2.826.0.1.3680043.2.1143.2539204068809890780679474904889975501.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0191-1.2.826.0.1.3680043.2.1143.564113755916104200212873457082858859.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0192-1.2.826.0.1.3680043.2.1143.6234614650850117957817825883101920107.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0193-1.2.826.0.1.3680043.2.1143.3340329905954041598031375323794268916.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0194-1.2.826.0.1.3680043.2.1143.8868883137480395682410386350821421574.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0195-1.2.826.0.1.3680043.2.1143.4013630519998969340802859627408181844.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0196-1.2.826.0.1.3680043.2.1143.1252921151700527600802783844414325850.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0197-1.2.826.0.1.3680043.2.1143.6507597961785138563496420087586428567.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0198-1.2.826.0.1.3680043.2.1143.6311623703306512430461302175694682700.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0199-1.2.826.0.1.3680043.2.1143.8149015537283471329321946784462207545.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0200-1.2.826.0.1.3680043.2.1143.4728297236263590733561957799110258041.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0201-1.2.826.0.1.3680043.2.1143.6776421975506547277942666726457192481.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0202-1.2.826.0.1.3680043.2.1143.1830285131039869239242998208234289458.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0203-1.2.826.0.1.3680043.2.1143.121009655393591952147961012095101572.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0204-1.2.826.0.1.3680043.2.1143.8253698425900443807129607236979789327.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0205-1.2.826.0.1.3680043.2.1143.6789042003877051257993914156570849257.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0206-1.2.826.0.1.3680043.2.1143.2265504095547368715469900748715028739.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0207-1.2.826.0.1.3680043.2.1143.8196622054630914190130235803759042697.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0208-1.2.826.0.1.3680043.2.1143.163337808644072633193392217713665751.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0209-1.2.826.0.1.3680043.2.1143.5561938799805174105166486552855327014.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0210-1.2.826.0.1.3680043.2.1143.9629925530010710978839328521160892874.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0211-1.2.826.0.1.3680043.2.1143.3465944104777314210207481669826448051.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0212-1.2.826.0.1.3680043.2.1143.6919587211958918135831540402678189965.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0213-1.2.826.0.1.3680043.2.1143.5068439037970681840409821532465623910.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0214-1.2.826.0.1.3680043.2.1143.5596709630810490988014610540536410126.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0215-1.2.826.0.1.3680043.2.1143.2827471326162556042483385561524681116.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0216-1.2.826.0.1.3680043.2.1143.6204375921268661647482632186873596930.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0217-1.2.826.0.1.3680043.2.1143.7247122142570724898156160565799553691.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0218-1.2.826.0.1.3680043.2.1143.9540663387476759560633671238820199207.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0219-1.2.826.0.1.3680043.2.1143.5882524590485786168516753720869742593.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0220-1.2.826.0.1.3680043.2.1143.1299764487818394823816406287494047154.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0221-1.2.826.0.1.3680043.2.1143.9215287640629563195895816340649535184.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0222-1.2.826.0.1.3680043.2.1143.5172314035063328145862287857553185472.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0223-1.2.826.0.1.3680043.2.1143.7479266467308171081259419856793056360.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0224-1.2.826.0.1.3680043.2.1143.3348456040453553139665778962544368876.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0225-1.2.826.0.1.3680043.2.1143.2362029004277501520272884222804482542.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0226-1.2.826.0.1.3680043.2.1143.8865057320082198796481495893274849323.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0227-1.2.826.0.1.3680043.2.1143.2291047013597377972284770271979055467.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0228-1.2.826.0.1.3680043.2.1143.2987405476868385453950882968526002360.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0229-1.2.826.0.1.3680043.2.1143.1039316245668356959287618408565556890.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0230-1.2.826.0.1.3680043.2.1143.9067149285609577500514770492839059965.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0231-1.2.826.0.1.3680043.2.1143.1767374511523061209582099401489521607.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0232-1.2.826.0.1.3680043.2.1143.4889557518648904326130783133291491753.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0233-1.2.826.0.1.3680043.2.1143.5045574856209221731463192507211017322.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0234-1.2.826.0.1.3680043.2.1143.6360291542794107895534316764251842925.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0235-1.2.826.0.1.3680043.2.1143.4946677763322183537044103589799480048.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0236-1.2.826.0.1.3680043.2.1143.2746237505140560120648372847413069331.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0237-1.2.826.0.1.3680043.2.1143.6357111767077576597713002910502209962.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0238-1.2.826.0.1.3680043.2.1143.5175221702607092941561803790974498353.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0239-1.2.826.0.1.3680043.2.1143.3998067013976673744684821439804459440.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0240-1.2.826.0.1.3680043.2.1143.4217705659291681286898435173018803088.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0241-1.2.826.0.1.3680043.2.1143.8503180534582904692829403630567560094.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0242-1.2.826.0.1.3680043.2.1143.2152657029623173765598039266779933978.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0243-1.2.826.0.1.3680043.2.1143.3432110273628130394756959118111446430.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0244-1.2.826.0.1.3680043.2.1143.6745899347520711300555111282552199212.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0245-1.2.826.0.1.3680043.2.1143.1984268525938195875082323589009679364.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0246-1.2.826.0.1.3680043.2.1143.6464202967985457265027610632365111167.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0247-1.2.826.0.1.3680043.2.1143.4819012717684694466997089949130876641.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0248-1.2.826.0.1.3680043.2.1143.4924520151204775820877596699013308344.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0249-1.2.826.0.1.3680043.2.1143.7651582618135333715327239570929047628.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0250-1.2.826.0.1.3680043.2.1143.5154742331780569680581377240033664883.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0251-1.2.826.0.1.3680043.2.1143.6979619912577633406263300208891226399.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0252-1.2.826.0.1.3680043.2.1143.6565203579828981069050289005537634210.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0253-1.2.826.0.1.3680043.2.1143.8402441821584334174796685937580544610.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0254-1.2.826.0.1.3680043.2.1143.4749922357167661075369587416562094632.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0255-1.2.826.0.1.3680043.2.1143.6278375215236720968870435953882579610.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0256-1.2.826.0.1.3680043.2.1143.9790779616924498502566039133811597784.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0257-1.2.826.0.1.3680043.2.1143.4831282812748576717442948008880440550.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0258-1.2.826.0.1.3680043.2.1143.4083611014887904541492347053551059899.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0259-1.2.826.0.1.3680043.2.1143.2675102201845244091347914838265371894.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0260-1.2.826.0.1.3680043.2.1143.3910340508857031840321795975903496373.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0261-1.2.826.0.1.3680043.2.1143.317314148369812642009842296570991298.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0262-1.2.826.0.1.3680043.2.1143.7824907023714088776431688761757471319.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0263-1.2.826.0.1.3680043.2.1143.1808854527483697663069958459832105460.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0264-1.2.826.0.1.3680043.2.1143.4225007341479800886877873220692513607.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0265-1.2.826.0.1.3680043.2.1143.4146659184068341518334462899353854787.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0266-1.2.826.0.1.3680043.2.1143.6031026985273393780425292044112253888.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0267-1.2.826.0.1.3680043.2.1143.5672554206846167316204572207008558338.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0268-1.2.826.0.1.3680043.2.1143.3454656859517608226452714191008691059.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0269-1.2.826.0.1.3680043.2.1143.1028401677060252170525083245510114100.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0270-1.2.826.0.1.3680043.2.1143.1861772298250683643519024935628851255.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0271-1.2.826.0.1.3680043.2.1143.1001792383502145621563793186004686941.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0272-1.2.826.0.1.3680043.2.1143.3984809171839675869127072342207700091.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0273-1.2.826.0.1.3680043.2.1143.4065636877624104117334945879103944340.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0274-1.2.826.0.1.3680043.2.1143.726197577621240707725978328525027262.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0275-1.2.826.0.1.3680043.2.1143.277089226752874299434223005162017106.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0276-1.2.826.0.1.3680043.2.1143.5099171737705478831697265646870033792.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0277-1.2.826.0.1.3680043.2.1143.1183372773610008692323754706852478356.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0278-1.2.826.0.1.3680043.2.1143.4272375159140626929859415893144149063.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0279-1.2.826.0.1.3680043.2.1143.8546599396659649499111619520384105123.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0280-1.2.826.0.1.3680043.2.1143.7331318049475608937489234942939477919.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0281-1.2.826.0.1.3680043.2.1143.810076362774221010128885724317488270.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0282-1.2.826.0.1.3680043.2.1143.6354968542626860857200945058545426549.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0283-1.2.826.0.1.3680043.2.1143.5306622479458650066956372697740274074.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0284-1.2.826.0.1.3680043.2.1143.3892436952366133182282306412381252317.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0285-1.2.826.0.1.3680043.2.1143.1477794045097908668407106728189742377.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0286-1.2.826.0.1.3680043.2.1143.3268476454810517202011867880705124188.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0287-1.2.826.0.1.3680043.2.1143.6184747770419149063409230640050088759.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0288-1.2.826.0.1.3680043.2.1143.4744932865585869283032982956117009441.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0289-1.2.826.0.1.3680043.2.1143.8753878122686375702454681534155995116.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0290-1.2.826.0.1.3680043.2.1143.6223160805985766709329255876409300760.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0291-1.2.826.0.1.3680043.2.1143.2005513166938452116801128035743938059.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0292-1.2.826.0.1.3680043.2.1143.7259415236168098881861278858287408283.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0293-1.2.826.0.1.3680043.2.1143.4519216353629958908253148345127363846.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0294-1.2.826.0.1.3680043.2.1143.2763750729156162029382348057355234026.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0295-1.2.826.0.1.3680043.2.1143.225348864142221953464926187181230167.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0296-1.2.826.0.1.3680043.2.1143.2743436878191837437547256457563105512.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0297-1.2.826.0.1.3680043.2.1143.5444200643599583490176194579571784014.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0298-1.2.826.0.1.3680043.2.1143.2479552721014488681298019000058116097.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0299-1.2.826.0.1.3680043.2.1143.7846505790771543500304186860846957149.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0300-1.2.826.0.1.3680043.2.1143.4394924367499310069806644450887848472.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0301-1.2.826.0.1.3680043.2.1143.7399394759964758278023941806977917602.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0302-1.2.826.0.1.3680043.2.1143.6117047877291624115907763758908325141.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0303-1.2.826.0.1.3680043.2.1143.9352885474845848311249482883411832639.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0304-1.2.826.0.1.3680043.2.1143.1846479718801859696753957932303165790.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0305-1.2.826.0.1.3680043.2.1143.3562810549547744101750774707737075453.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0306-1.2.826.0.1.3680043.2.1143.740124467735924162569271159712015449.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0307-1.2.826.0.1.3680043.2.1143.2630429401711608002499029945175528595.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0308-1.2.826.0.1.3680043.2.1143.7479992599617002741090579641932653361.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0309-1.2.826.0.1.3680043.2.1143.6195266585735815384201440104644099932.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0310-1.2.826.0.1.3680043.2.1143.1552158082241052288998223980512574931.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0311-1.2.826.0.1.3680043.2.1143.5258607229239690573341397137428665555.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0312-1.2.826.0.1.3680043.2.1143.900429590523695899480230971068057326.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0313-1.2.826.0.1.3680043.2.1143.5054615740597116325049573614175799089.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0314-1.2.826.0.1.3680043.2.1143.519413748279466143176341542797933080.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0315-1.2.826.0.1.3680043.2.1143.1375741092845449320755819174735702068.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0316-1.2.826.0.1.3680043.2.1143.5251884925362369937016679142142623144.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0317-1.2.826.0.1.3680043.2.1143.7082795796912764059712507230509307849.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0318-1.2.826.0.1.3680043.2.1143.7091344189305711411904755510747861015.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0319-1.2.826.0.1.3680043.2.1143.5698767691624371373392262403693778943.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0320-1.2.826.0.1.3680043.2.1143.8620570212442171857607137949342348430.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0321-1.2.826.0.1.3680043.2.1143.3214307141284505325006995742973279095.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0322-1.2.826.0.1.3680043.2.1143.2491512176482722785016111303224122109.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0323-1.2.826.0.1.3680043.2.1143.3524984880122962163322142314488108391.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0324-1.2.826.0.1.3680043.2.1143.5387371482199240207846574600170722058.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0325-1.2.826.0.1.3680043.2.1143.5936459653109304052310037991965668224.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0326-1.2.826.0.1.3680043.2.1143.4172737522888934520489201173039378631.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0327-1.2.826.0.1.3680043.2.1143.5205142781608973989885515706902755259.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0328-1.2.826.0.1.3680043.2.1143.2577553609997804530638250102277497553.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0329-1.2.826.0.1.3680043.2.1143.96376506194019736756880267490036819.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0330-1.2.826.0.1.3680043.2.1143.7556017111860948719110651947722838234.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0331-1.2.826.0.1.3680043.2.1143.7980170295326065434086375780975261994.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0332-1.2.826.0.1.3680043.2.1143.8635860963330269775225481828116367312.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0333-1.2.826.0.1.3680043.2.1143.2485629503901704141911956418083978912.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0334-1.2.826.0.1.3680043.2.1143.2523250178691134297762362471192698886.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0335-1.2.826.0.1.3680043.2.1143.7033887141755639775490778493940380999.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0336-1.2.826.0.1.3680043.2.1143.4162218581350705297276969548328808589.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0337-1.2.826.0.1.3680043.2.1143.1329581888157226556536391615110139935.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0338-1.2.826.0.1.3680043.2.1143.3652520575508047327210499538417397172.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0339-1.2.826.0.1.3680043.2.1143.8259281519831658880768604218288537052.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0340-1.2.826.0.1.3680043.2.1143.9018715899080652177044630182772420143.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0341-1.2.826.0.1.3680043.2.1143.9684116285074007340059939703917547546.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0342-1.2.826.0.1.3680043.2.1143.9732955585497566021718367778314379288.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0343-1.2.826.0.1.3680043.2.1143.4832590729124467726234290786040664480.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0344-1.2.826.0.1.3680043.2.1143.8918281802833466387440462480644549734.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0345-1.2.826.0.1.3680043.2.1143.8425835598453961747620174196294940678.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0346-1.2.826.0.1.3680043.2.1143.6991404042755623064660458406066027185.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0347-1.2.826.0.1.3680043.2.1143.1526511864118850402230503372655111672.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0348-1.2.826.0.1.3680043.2.1143.9142559540706674178292584807137388883.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0349-1.2.826.0.1.3680043.2.1143.4903943949576341504027596295494953069.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0350-1.2.826.0.1.3680043.2.1143.7871478225090657809629916322521307225.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0351-1.2.826.0.1.3680043.2.1143.7124057668117188704705410664365364954.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0352-1.2.826.0.1.3680043.2.1143.7677618671221126889533904775929984928.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0353-1.2.826.0.1.3680043.2.1143.2082242379333921480302129217957078385.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0354-1.2.826.0.1.3680043.2.1143.8367317520825670874733167273331384524.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0355-1.2.826.0.1.3680043.2.1143.5423009957473732500820666543885013686.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0356-1.2.826.0.1.3680043.2.1143.7285052535308281345184777149339153909.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0357-1.2.826.0.1.3680043.2.1143.9643116571109128951051733780252258253.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0358-1.2.826.0.1.3680043.2.1143.635199164255803228755824795472656557.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0359-1.2.826.0.1.3680043.2.1143.3740339707614842779488698654847318696.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0360-1.2.826.0.1.3680043.2.1143.4687645589301929340675041047970013731.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0361-1.2.826.0.1.3680043.2.1143.4220707169543869249179922681581125700.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0362-1.2.826.0.1.3680043.2.1143.296036666864656122162692513767872625.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0363-1.2.826.0.1.3680043.2.1143.7243872360793717668829796513978229849.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0364-1.2.826.0.1.3680043.2.1143.5175787264791366830895061927805248630.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0365-1.2.826.0.1.3680043.2.1143.5020505599618402887878331144762583619.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0366-1.2.826.0.1.3680043.2.1143.5928239540050742521445644115162775537.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0367-1.2.826.0.1.3680043.2.1143.6854913432446737327447625772580745278.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0368-1.2.826.0.1.3680043.2.1143.3456771114643347005671841061012450956.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0369-1.2.826.0.1.3680043.2.1143.1446937767643293846695890545315178414.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0370-1.2.826.0.1.3680043.2.1143.9664933223766511138212658664600014159.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0371-1.2.826.0.1.3680043.2.1143.4245780022317820191316058521571032538.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0372-1.2.826.0.1.3680043.2.1143.2963536166409049366991885712393050597.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0373-1.2.826.0.1.3680043.2.1143.9827955755139425709389315033574977981.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0374-1.2.826.0.1.3680043.2.1143.9837511905824101120089300368908537613.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0375-1.2.826.0.1.3680043.2.1143.660541916748903047150141202578097899.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0376-1.2.826.0.1.3680043.2.1143.8541056460929063190401038130981940659.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0377-1.2.826.0.1.3680043.2.1143.3381391861208479732141898199827435650.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0378-1.2.826.0.1.3680043.2.1143.4174789290656774926733448517901972683.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0379-1.2.826.0.1.3680043.2.1143.4796584246807303881544843128472138541.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0380-1.2.826.0.1.3680043.2.1143.3498167302811097740156610169978657042.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0381-1.2.826.0.1.3680043.2.1143.9445758447430267593384143871158728355.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0382-1.2.826.0.1.3680043.2.1143.7606206255980057221785178202727962264.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0383-1.2.826.0.1.3680043.2.1143.6453512530005137327831207230938152528.dcm +SERVICES/PACS/OXITESTORTHANC/02-Jane_Doe-19660101/Hanke_Stadler_0024_transrep-AccessionNumber-20130717/00401-anat-T1w-661b8fc/0384-1.2.826.0.1.3680043.2.1143.1419835265191232339373162153743354304.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0001-1.3.12.2.1107.5.2.19.45152.2013030808110258929186035.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0002-1.3.12.2.1107.5.2.19.45152.2013030808110261698786039.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0003-1.3.12.2.1107.5.2.19.45152.2013030808110259940386037.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0004-1.3.12.2.1107.5.2.19.45152.2013030808110256555586033.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0005-1.3.12.2.1107.5.2.19.45152.2013030808110251492986029.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0006-1.3.12.2.1107.5.2.19.45152.2013030808110255864486031.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0007-1.3.12.2.1107.5.2.19.45152.2013030808110245643686025.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0008-1.3.12.2.1107.5.2.19.45152.2013030808110250837286027.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0009-1.3.12.2.1107.5.2.19.45152.2013030808110245009586023.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0010-1.3.12.2.1107.5.2.19.45152.2013030808110244209386021.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0011-1.3.12.2.1107.5.2.19.45152.2013030808110234454086015.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0012-1.3.12.2.1107.5.2.19.45152.2013030808110240192886019.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0013-1.3.12.2.1107.5.2.19.45152.2013030808110238343586017.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0014-1.3.12.2.1107.5.2.19.45152.2013030808110227462186007.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0015-1.3.12.2.1107.5.2.19.45152.2013030808110230739286013.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0016-1.3.12.2.1107.5.2.19.45152.2013030808110228156286009.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0017-1.3.12.2.1107.5.2.19.45152.2013030808110229553086011.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0018-1.3.12.2.1107.5.2.19.45152.2013030808110217396486003.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0019-1.3.12.2.1107.5.2.19.45152.2013030808110213708785999.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0020-1.3.12.2.1107.5.2.19.45152.2013030808110218338186005.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0021-1.3.12.2.1107.5.2.19.45152.2013030808110210194085997.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0022-1.3.12.2.1107.5.2.19.45152.2013030808110215733186001.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0023-1.3.12.2.1107.5.2.19.45152.201303080811025197685991.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0024-1.3.12.2.1107.5.2.19.45152.201303080811023268985989.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0025-1.3.12.2.1107.5.2.19.45152.201303080811026735485995.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0026-1.3.12.2.1107.5.2.19.45152.201303080811026137685993.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0027-1.3.12.2.1107.5.2.19.45152.2013030808110195027685987.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0028-1.3.12.2.1107.5.2.19.45152.2013030808110192863385983.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0029-1.3.12.2.1107.5.2.19.45152.2013030808110188036985979.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0030-1.3.12.2.1107.5.2.19.45152.2013030808110192966985985.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0031-1.3.12.2.1107.5.2.19.45152.2013030808110192661285981.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0032-1.3.12.2.1107.5.2.19.45152.2013030808110186464285977.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0033-1.3.12.2.1107.5.2.19.45152.2013030808110182010785975.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0034-1.3.12.2.1107.5.2.19.45152.2013030808110182004985974.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0035-1.3.12.2.1107.5.2.19.45152.2013030808110173040285969.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0036-1.3.12.2.1107.5.2.19.45152.2013030808110173893385971.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0037-1.3.12.2.1107.5.2.19.45152.2013030808110167672385961.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0038-1.3.12.2.1107.5.2.19.45152.2013030808110169000885967.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0039-1.3.12.2.1107.5.2.19.45152.2013030808110167984385965.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0040-1.3.12.2.1107.5.2.19.45152.2013030808110167733585963.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0041-1.3.12.2.1107.5.2.19.45152.2013030808110154305485955.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0042-1.3.12.2.1107.5.2.19.45152.2013030808110152877585953.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0043-1.3.12.2.1107.5.2.19.45152.2013030808110149471485951.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0044-1.3.12.2.1107.5.2.19.45152.2013030808110156833085957.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0045-1.3.12.2.1107.5.2.19.45152.2013030808110158087085959.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0046-1.3.12.2.1107.5.2.19.45152.2013030808110145659285947.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0047-1.3.12.2.1107.5.2.19.45152.2013030808110148338285949.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0048-1.3.12.2.1107.5.2.19.45152.2013030808110141119185945.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0049-1.3.12.2.1107.5.2.19.45152.2013030808110130561085937.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0050-1.3.12.2.1107.5.2.19.45152.2013030808110137450885943.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0051-1.3.12.2.1107.5.2.19.45152.2013030808110130683485939.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0052-1.3.12.2.1107.5.2.19.45152.2013030808110133953685941.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0053-1.3.12.2.1107.5.2.19.45152.2013030808110128099085935.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0054-1.3.12.2.1107.5.2.19.45152.2013030808110122776585933.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0055-1.3.12.2.1107.5.2.19.45152.2013030808110115081285929.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0056-1.3.12.2.1107.5.2.19.45152.2013030808110090741285917.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0057-1.3.12.2.1107.5.2.19.45152.2013030808110118559785931.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0058-1.3.12.2.1107.5.2.19.45152.2013030808110096354785919.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0059-1.3.12.2.1107.5.2.19.45152.2013030808110099982285923.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0060-1.3.12.2.1107.5.2.19.45152.201303080811016905585925.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0061-1.3.12.2.1107.5.2.19.45152.2013030808110087109885915.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0062-1.3.12.2.1107.5.2.19.45152.201303080811016931485927.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0063-1.3.12.2.1107.5.2.19.45152.2013030808110074358285913.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0064-1.3.12.2.1107.5.2.19.45152.2013030808110098527585921.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0065-1.3.12.2.1107.5.2.19.45152.2013030808110071982385909.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0066-1.3.12.2.1107.5.2.19.45152.2013030808110072369585911.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0067-1.3.12.2.1107.5.2.19.45152.2013030808110050473285899.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0068-1.3.12.2.1107.5.2.19.45152.2013030808110068374485907.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0069-1.3.12.2.1107.5.2.19.45152.2013030808110042490385893.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0070-1.3.12.2.1107.5.2.19.45152.2013030808110049968785897.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0071-1.3.12.2.1107.5.2.19.45152.2013030808110055346785903.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0072-1.3.12.2.1107.5.2.19.45152.2013030808110053617185901.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0073-1.3.12.2.1107.5.2.19.45152.2013030808110068367585906.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0074-1.3.12.2.1107.5.2.19.45152.2013030808110048712285895.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0075-1.3.12.2.1107.5.2.19.45152.2013030808110022337585885.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0076-1.3.12.2.1107.5.2.19.45152.2013030808110029991985891.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0077-1.3.12.2.1107.5.2.19.45152.2013030808110018402485881.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0078-1.3.12.2.1107.5.2.19.45152.2013030808110018104885879.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0079-1.3.12.2.1107.5.2.19.45152.2013030808110015438585877.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0080-1.3.12.2.1107.5.2.19.45152.2013030808110020331385883.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0081-1.3.12.2.1107.5.2.19.45152.201303080811008030185875.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0082-1.3.12.2.1107.5.2.19.45152.201303080811006319185873.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0083-1.3.12.2.1107.5.2.19.45152.2013030808110022760285887.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0084-1.3.12.2.1107.5.2.19.45152.2013030808110024284285889.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0085-1.3.12.2.1107.5.2.19.45152.2013030808105999828685871.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0086-1.3.12.2.1107.5.2.19.45152.2013030808105985341385869.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0087-1.3.12.2.1107.5.2.19.45152.2013030808105984449785867.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0088-1.3.12.2.1107.5.2.19.45152.2013030808105981523185863.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0089-1.3.12.2.1107.5.2.19.45152.2013030808105984430385865.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0090-1.3.12.2.1107.5.2.19.45152.2013030808105968049685853.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0091-1.3.12.2.1107.5.2.19.45152.2013030808105980663585859.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0092-1.3.12.2.1107.5.2.19.45152.2013030808105964831685851.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0093-1.3.12.2.1107.5.2.19.45152.2013030808105978270285857.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0094-1.3.12.2.1107.5.2.19.45152.2013030808105975870085855.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0095-1.3.12.2.1107.5.2.19.45152.2013030808105981376785861.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0096-1.3.12.2.1107.5.2.19.45152.2013030808105959806985847.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0097-1.3.12.2.1107.5.2.19.45152.2013030808105963142885849.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0098-1.3.12.2.1107.5.2.19.45152.2013030808105936142485825.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0099-1.3.12.2.1107.5.2.19.45152.2013030808105943925185831.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0100-1.3.12.2.1107.5.2.19.45152.2013030808105947776285839.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0101-1.3.12.2.1107.5.2.19.45152.2013030808105927004985815.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0102-1.3.12.2.1107.5.2.19.45152.2013030808105932115485823.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0103-1.3.12.2.1107.5.2.19.45152.2013030808105926072285813.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0104-1.3.12.2.1107.5.2.19.45152.2013030808105916713985803.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0105-1.3.12.2.1107.5.2.19.45152.2013030808105919622985805.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0106-1.3.12.2.1107.5.2.19.45152.201303080810594335485789.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0107-1.3.12.2.1107.5.2.19.45152.2013030808105927377185819.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0108-1.3.12.2.1107.5.2.19.45152.2013030808105913553685797.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0109-1.3.12.2.1107.5.2.19.45152.2013030808105910442085794.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0110-1.3.12.2.1107.5.2.19.45152.2013030808105878350485763.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0111-1.3.12.2.1107.5.2.19.45152.2013030808105886647385771.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0112-1.3.12.2.1107.5.2.19.45152.2013030808105879733085765.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0113-1.3.12.2.1107.5.2.19.45152.2013030808105890597785777.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0114-1.3.12.2.1107.5.2.19.45152.2013030808105881263085767.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0115-1.3.12.2.1107.5.2.19.45152.2013030808105860620385749.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0116-1.3.12.2.1107.5.2.19.45152.2013030808105873799285759.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0117-1.3.12.2.1107.5.2.19.45152.2013030808105895469785781.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0118-1.3.12.2.1107.5.2.19.45152.2013030808105854016685745.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0119-1.3.12.2.1107.5.2.19.45152.2013030808105861852485751.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0120-1.3.12.2.1107.5.2.19.45152.2013030808105849271985739.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0121-1.3.12.2.1107.5.2.19.45152.2013030808105840050885721.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0122-1.3.12.2.1107.5.2.19.45152.2013030808105831242085711.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0123-1.3.12.2.1107.5.2.19.45152.2013030808105847645685735.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0124-1.3.12.2.1107.5.2.19.45152.2013030808105846035385733.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0125-1.3.12.2.1107.5.2.19.45152.2013030808105832001085713.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0126-1.3.12.2.1107.5.2.19.45152.2013030808105834376885715.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0127-1.3.12.2.1107.5.2.19.45152.201303080810589800685697.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0128-1.3.12.2.1107.5.2.19.45152.2013030808105813734785703.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0129-1.3.12.2.1107.5.2.19.45152.2013030808105839591285717.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0130-1.3.12.2.1107.5.2.19.45152.2013030808105820428385709.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0131-1.3.12.2.1107.5.2.19.45152.201303080810581734185691.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0132-1.3.12.2.1107.5.2.19.45152.2013030808105787169485677.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0133-1.3.12.2.1107.5.2.19.45152.201303080810582543085693.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0134-1.3.12.2.1107.5.2.19.45152.201303080810583258785695.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0135-1.3.12.2.1107.5.2.19.45152.2013030808105782377085671.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0136-1.3.12.2.1107.5.2.19.45152.2013030808105788470585679.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0137-1.3.12.2.1107.5.2.19.45152.2013030808105771573685661.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0138-1.3.12.2.1107.5.2.19.45152.2013030808105776043685665.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0139-1.3.12.2.1107.5.2.19.45152.2013030808105785763185675.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0140-1.3.12.2.1107.5.2.19.45152.2013030808105780152485669.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0141-1.3.12.2.1107.5.2.19.45152.2013030808105747274085643.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0142-1.3.12.2.1107.5.2.19.45152.2013030808105767042185659.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0143-1.3.12.2.1107.5.2.19.45152.2013030808105758087285649.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0144-1.3.12.2.1107.5.2.19.45152.2013030808105746647785639.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0145-1.3.12.2.1107.5.2.19.45152.2013030808105766362885657.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0146-1.3.12.2.1107.5.2.19.45152.2013030808105747027185641.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0147-1.3.12.2.1107.5.2.19.45152.2013030808105713401385599.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0148-1.3.12.2.1107.5.2.19.45152.2013030808105734821485629.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0149-1.3.12.2.1107.5.2.19.45152.2013030808105717291185607.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0150-1.3.12.2.1107.5.2.19.45152.2013030808105716172485605.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0151-1.3.12.2.1107.5.2.19.45152.2013030808105731718285623.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0152-1.3.12.2.1107.5.2.19.45152.2013030808105713533385601.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0153-1.3.12.2.1107.5.2.19.45152.2013030808105721024085615.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0154-1.3.12.2.1107.5.2.19.45152.2013030808105714623685603.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0155-1.3.12.2.1107.5.2.19.45152.2013030808105693392185587.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0156-1.3.12.2.1107.5.2.19.45152.2013030808105683775785575.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0157-1.3.12.2.1107.5.2.19.45152.2013030808105675348185565.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0158-1.3.12.2.1107.5.2.19.45152.2013030808105691039985581.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0159-1.3.12.2.1107.5.2.19.45152.2013030808105681768285573.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0160-1.3.12.2.1107.5.2.19.45152.2013030808105664357985555.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0161-1.3.12.2.1107.5.2.19.45152.2013030808105656089185549.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0162-1.3.12.2.1107.5.2.19.45152.2013030808105684668185577.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0163-1.3.12.2.1107.5.2.19.45152.2013030808105681195285571.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0164-1.3.12.2.1107.5.2.19.45152.2013030808105679536885567.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0165-1.3.12.2.1107.5.2.19.45152.2013030808105674633585561.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0166-1.3.12.2.1107.5.2.19.45152.2013030808105636967285535.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0167-1.3.12.2.1107.5.2.19.45152.2013030808105640717085537.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0168-1.3.12.2.1107.5.2.19.45152.2013030808105636729985533.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0169-1.3.12.2.1107.5.2.19.45152.2013030808105616825585511.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0170-1.3.12.2.1107.5.2.19.45152.2013030808105619763585513.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0171-1.3.12.2.1107.5.2.19.45152.2013030808105653573785545.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0172-1.3.12.2.1107.5.2.19.45152.2013030808105633835385527.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0173-1.3.12.2.1107.5.2.19.45152.2013030808105598888685495.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0174-1.3.12.2.1107.5.2.19.45152.2013030808105631819085525.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0175-1.3.12.2.1107.5.2.19.45152.2013030808105589783485487.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0176-1.3.12.2.1107.5.2.19.45152.2013030808105595489285493.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0177-1.3.12.2.1107.5.2.19.45152.2013030808105628571885521.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0178-1.3.12.2.1107.5.2.19.45152.2013030808105613027985505.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0179-1.3.12.2.1107.5.2.19.45152.2013030808105593484285491.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0180-1.3.12.2.1107.5.2.19.45152.2013030808105577868585475.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0181-1.3.12.2.1107.5.2.19.45152.2013030808105562823785457.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0182-1.3.12.2.1107.5.2.19.45152.2013030808105576770785473.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0183-1.3.12.2.1107.5.2.19.45152.2013030808105561901985453.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0184-1.3.12.2.1107.5.2.19.45152.2013030808105562925785459.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0185-1.3.12.2.1107.5.2.19.45152.2013030808105550546785443.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0186-1.3.12.2.1107.5.2.19.45152.2013030808105578565885477.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0187-1.3.12.2.1107.5.2.19.45152.2013030808105564243785461.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0188-1.3.12.2.1107.5.2.19.45152.2013030808105567563785463.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0189-1.3.12.2.1107.5.2.19.45152.2013030808105517130085417.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0190-1.3.12.2.1107.5.2.19.45152.2013030808105512578785411.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0191-1.3.12.2.1107.5.2.19.45152.2013030808105486367685381.dcm +SERVICES/PACS/OXITESTORTHANC/1449c1d-anonymized-20090701/MR-Brain_w_o_Contrast-98edede8b2-20130308/00005-SAG_MPRAGE_220_FOV-a27cf06/0192-1.3.12.2.1107.5.2.19.45152.2013030808105485455785379.dcm From c274d4a15935684cf90074866be28ec6483d2b65 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sat, 7 Sep 2024 13:56:52 -0400 Subject: [PATCH 08/44] Add validation of instance count to get_data.sh --- get_data.sh | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/get_data.sh b/get_data.sh index 022b262..76cea41 100755 --- a/get_data.sh +++ b/get_data.sh @@ -12,8 +12,28 @@ until instances=$(curl -sf http://localhost:8042/instances); do done echo -if [ "$(jq -r length <<< "$instances")" != '0' ]; then - echo 'Already have data' +get_instance_counts () { + curl -sf http://localhost:8042/series \ + | jq -r '.[]' \ + | parallel ' + curl -sf http://localhost:8042/series/{} \ + | jq -r ".MainDicomTags.SeriesInstanceUID + \" \" + (.Instances | length | tostring)"' \ + | sort +} + +series_has_data () { + echo "$1" | grep -qF "$2 $3" || (echo "Error: $2 has wrong number of DICOM instances"; return 1) +} + +check_has_data () { + local data="$(get_instance_counts)" + echo "$data" + series_has_data "$data" '1.2.826.0.1.3680043.2.1143.515404396022363061013111326823367652' 384 + series_has_data "$data" '1.3.12.2.1107.5.2.19.45152.2013030808061520200285270.0.0.0' 192 +} + +if check_has_data > /dev/null; then + echo "Already have data" exit 0 fi @@ -30,3 +50,5 @@ find -type f -iname '*.dcm' \ cd / rm -rf $tmpdir + +check_has_data From 5ec03a208dbafce7c37eba839757c83e1ea1be11 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sat, 7 Sep 2024 14:20:49 -0400 Subject: [PATCH 09/44] Add codecov --- .github/workflows/ci.yml | 18 ++++++++++++++++-- README.md | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55379bb..d491260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,21 @@ jobs: run: docker compose run --rm get-data - name: Cache rust build uses: Swatinem/rust-cache@v2 + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov - name: Compile test binary - run: cargo test --no-run + run: | + source <(cargo llvm-cov show-env --export-prefix) + cargo test --no-run --locked - name: Run tests - run: cargo test + run: cargo llvm-cov test --locked --codecov --output-path codecov.json + - name: Print service logs + if: failure() + run: docker compose logs + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov.json + fail_ci_if_error: true diff --git a/README.md b/README.md index a0ca379..048562b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![GitHub tag](https://img.shields.io/github/v/tag/FNNDSC/oxidicom?filter=v*.*.*&label=version)](https://github.com/FNNDSC/oxidicom/pkgs/container/oxidicom) [![MIT License](https://img.shields.io/github/license/fnndsc/oxidicom)](https://github.com/FNNDSC/oxidicom/blob/master/LICENSE) [![CI](https://github.com/FNNDSC/oxidicom/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/oxidicom/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/FNNDSC/oxidicom/graph/badge.svg?token=24J11SWJEA)](https://codecov.io/gh/FNNDSC/oxidicom) _oxidicom_ is a high-performance DICOM receiver for the [_ChRIS_ backend](https://github.com/FNNDSC/ChRIS_ultron_backEnd) (CUBE). From e2ce6bb45d68870789f2c89be96a3fab9ca45977 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sat, 7 Sep 2024 14:43:54 -0400 Subject: [PATCH 10/44] Replace parallel with xargs Could fix some errors on GitHub Actions --- get_data.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/get_data.sh b/get_data.sh index 76cea41..7c1cffe 100755 --- a/get_data.sh +++ b/get_data.sh @@ -15,8 +15,8 @@ echo get_instance_counts () { curl -sf http://localhost:8042/series \ | jq -r '.[]' \ - | parallel ' - curl -sf http://localhost:8042/series/{} \ + | xargs -I _ sh -c ' + curl -sf http://localhost:8042/series/_ \ | jq -r ".MainDicomTags.SeriesInstanceUID + \" \" + (.Instances | length | tostring)"' \ | sort } @@ -46,7 +46,7 @@ for url in "${GITHUB_TARBALLS[@]}"; do done find -type f -iname '*.dcm' \ - | parallel --progress -j 4 "curl -sfX POST http://localhost:8042/instances -H Expect: -H 'Content-Type: application/dicom' --data-binary @'{}' -o /dev/null" + | xargs -I _ curl -sfX POST http://localhost:8042/instances -H Expect: -H 'Content-Type: application/dicom' --data-binary @_ -o /dev/null cd / rm -rf $tmpdir From 9c08bdea9a9372285eb26e46f169d47867d9afc0 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 01:48:27 -0400 Subject: [PATCH 11/44] Send LONK to NATS --- Cargo.lock | 187 +++++++++++++++++++++++++++ Cargo.toml | 2 + docker-compose.yml | 4 + src/association_series_state_loop.rs | 69 +++++----- src/lib.rs | 1 + src/lonk.rs | 48 +++++++ src/main.rs | 4 +- src/notifier.rs | 146 +++++++++++++++++---- src/registration_task.rs | 2 +- src/run_everything.rs | 9 +- src/series_synchronizer.rs | 1 - src/settings.rs | 2 +- src/types.rs | 25 +--- tests/integration_test.rs | 40 +++++- 14 files changed, 447 insertions(+), 93 deletions(-) create mode 100644 src/lonk.rs diff --git a/Cargo.lock b/Cargo.lock index 959d850..7dee109 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,40 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-nats" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71e5a1bab60f46b0b005f4808b8ee83ef6d577608923de938403393c9a30cf8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "memchr", + "nkeys", + "nuid", + "once_cell", + "portable-atomic", + "rand", + "regex", + "ring", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki", + "serde", + "serde_json", + "serde_nanos", + "serde_repr", + "thiserror", + "time", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "tryhard", + "url", +] + [[package]] name = "async-reactor-trait" version = "1.1.0" @@ -563,6 +597,9 @@ name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] [[package]] name = "camino" @@ -831,6 +868,32 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -882,6 +945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1096,6 +1160,28 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "signature", + "subtle", +] + [[package]] name = "either" version = "1.10.0" @@ -1267,6 +1353,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "figment" version = "0.10.19" @@ -2093,6 +2185,21 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nkeys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de02c883c178998da8d0c9816a88ef7ef5c58314dd1585c97a4a5679f3ab337" +dependencies = [ + "data-encoding", + "ed25519", + "ed25519-dalek", + "getrandom", + "log", + "rand", + "signatory", +] + [[package]] name = "nom" version = "7.1.3" @@ -2113,6 +2220,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nuid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +dependencies = [ + "rand", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2313,7 +2429,9 @@ version = "3.0.0-a1" dependencies = [ "aliri_braid", "anyhow", + "async-nats", "async-walkdir", + "bytes", "camino", "celery", "dicom", @@ -2539,6 +2657,16 @@ dependencies = [ "spki", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -2589,6 +2717,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-atomic" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" + [[package]] name = "powerfmt" version = "0.2.0" @@ -3170,6 +3304,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_nanos" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3228,6 +3382,28 @@ dependencies = [ "libc", ] +[[package]] +name = "signatory" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" +dependencies = [ + "pkcs8", + "rand_core", + "signature", + "zeroize", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3778,6 +3954,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tryhard" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9f0a709784e86923586cff0d872dba54cd2d2e116b3bc57587d15737cfce9d" +dependencies = [ + "futures", + "pin-project-lite", + "tokio", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 9732bae..7df5a11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ ulid = "1.1.3" figment = { version = "0.10.19", features = ["env"] } celery = "0.5.5" humantime-serde = "1.1.1" +async-nats = "0.36.0" +bytes = "1.5.0" [dev-dependencies] rstest = "0.22.0" diff --git a/docker-compose.yml b/docker-compose.yml index c8db7ee..dfbfd05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,10 @@ services: image: docker.io/library/rabbitmq:3 ports: - "5672:5672" + nats: + image: docker.io/library/nats:2.10.20 + ports: + - "4222:4222" orthanc: image: docker.io/jodogne/orthanc-plugins:1.12.3 volumes: diff --git a/src/association_series_state_loop.rs b/src/association_series_state_loop.rs index fcd16a1..e291665 100644 --- a/src/association_series_state_loop.rs +++ b/src/association_series_state_loop.rs @@ -2,7 +2,7 @@ use crate::dicomrs_settings::AETitle; use crate::enums::{AssociationEvent, SeriesEvent}; use crate::error::{DicomRequiredTagError, DicomStorageError, HandleLoopError}; use crate::pacs_file::{BadTag, PacsFileRegistration}; -use crate::types::{DicomFilePath, DicomInfo, PendingDicomInstance, SeriesCount, SeriesKey}; +use crate::types::{DicomFilePath, DicomInfo, PendingDicomInstance, SeriesKey, SeriesPath}; use camino::{Utf8Path, Utf8PathBuf}; use dicom::object::DefaultDicomObject; use std::collections::HashMap; @@ -12,6 +12,22 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::task::JoinHandle; use ulid::Ulid; +struct Association { + pacs_name: AETitle, + series: HashMap>, +} + +impl Association { + fn new(pacs_name: AETitle) -> Self { + Self { + pacs_name, + series: Default::default(), + } + } +} + +type InflightAssociations = HashMap; + /// Stateful handling of [AssociationEvent]. /// /// - On every DICOM instance received, read its metadata (such as PatientName, Modality, ...), @@ -23,7 +39,7 @@ pub(crate) async fn association_series_state_loop( sender: UnboundedSender<(SeriesKey, PendingDicomInstance)>, files_root: Utf8PathBuf, ) -> Result, SendError<(SeriesKey, PendingDicomInstance)>> { - let mut inflight_associations: HashMap = Default::default(); + let mut inflight_associations: InflightAssociations = Default::default(); let mut everything_ok = true; let files_root = Arc::new(files_root); while let Some(event) = receiver.recv().await { @@ -55,7 +71,7 @@ pub(crate) async fn association_series_state_loop( /// code to cause a race condition). fn match_event( event: AssociationEvent, - inflight_associations: &mut HashMap, + inflight_associations: &mut InflightAssociations, files_root: &Arc, ) -> Result, ()> { match event { @@ -83,32 +99,30 @@ fn match_event( /// Receive a DICOM instance. It will be taken note of in `inflight_associations`. /// -/// For every DICOM instance received: create a task to store the DICOM instance as a file +/// For every DICOM instance received: create a task to store the DICOM instance as a file. +/// When the task finishes, it returns the count of files stored. /// /// The tasks are returned. fn receive_dicom_instance( ulid: Ulid, dcm: DefaultDicomObject, - inflight_associations: &mut HashMap, + inflight_associations: &mut InflightAssociations, files_root: &Arc, -) -> Result<(SeriesKey, JoinHandle>), DicomRequiredTagError> { +) -> Result<(SeriesKey, JoinHandle>), DicomRequiredTagError> { let association = inflight_associations .get_mut(&ulid) .expect("Unknown association ULID"); - let pacs_name = association.aec.clone(); + let pacs_name = association.pacs_name.clone(); let (pacs_file, bad_tags) = PacsFileRegistration::new(pacs_name, dcm)?; report_bad_tags(&pacs_file.data, ulid, bad_tags); let series_key = SeriesKey::new( pacs_file.data.SeriesInstanceUID.clone(), pacs_file.data.pacs_name.clone(), ); - if let Some(state) = association.series.get_mut(&series_key) { - state.count += 1; - } else { - association - .series - .insert(series_key.clone(), SeriesCount::new(pacs_file.data.clone())); - } + association + .series + .entry(series_key.clone()) + .or_insert_with(|| pacs_file.data.clone().into()); let storage_task = { let files_root = Arc::clone(files_root); tokio::task::spawn_blocking(move || write_dicom_wotel(&files_root, &pacs_file)) @@ -118,7 +132,7 @@ fn receive_dicom_instance( /// Creates messages for the end of an association. fn finish_association( - series_counts: HashMap, + series_counts: HashMap>, ) -> Vec<(SeriesKey, PendingDicomInstance)> { series_counts .into_iter() @@ -126,31 +140,16 @@ fn finish_association( .collect() } -/// Information about a DICOM "association" which is a TCP connection from a PACS server -/// who is pushing DICOM files to us. -struct Association { - /// AE title of the PACS pushing to us - aec: AETitle, - /// The unique series we are receiving during this association. - series: HashMap, -} - -impl Association { - fn new(aec: AETitle) -> Self { - Self { - aec, - series: Default::default(), - } - } -} - /// Wraps [write_dicom] with OpenTelemetry logging. -fn write_dicom_wotel(files_root: &Utf8Path, pacs_file: &PacsFileRegistration) -> Result<(), ()> { +fn write_dicom_wotel( + files_root: &Utf8Path, + pacs_file: &PacsFileRegistration, +) -> Result<(), DicomStorageError> { match write_dicom(pacs_file, files_root) { Ok(path) => tracing::info!(event = "storage", path = path.into_string()), Err(e) => { tracing::error!(event = "storage", error = e.to_string()); - return Err(()); + return Err(e); } } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 7a10085..ae0cfde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod dicomrs_settings; mod enums; mod error; mod listener_tcp_loop; +mod lonk; mod notifier; mod pacs_file; mod patient_age; diff --git a/src/lonk.rs b/src/lonk.rs new file mode 100644 index 0000000..5d0912f --- /dev/null +++ b/src/lonk.rs @@ -0,0 +1,48 @@ +//! Implementation of the **Light Oxidicom NotifiKations** encoding specification. +//! +//! Documentation: + +use crate::error::DicomStorageError; +use crate::types::SeriesKey; +use bytes::Bytes; + +const MESSAGE_NDICOM: u8 = 0x01; +const MESSAGE_ERROR: u8 = 0x02; +const DONE_MESSAGE: [u8; 1] = [0x00]; + +pub(crate) fn done_message() -> Bytes { + Bytes::from_static(&DONE_MESSAGE) +} + +/// Encode a LONK progress message. +pub(crate) fn progress_message(ndicom: u32) -> Bytes { + let payload: Vec = [MESSAGE_NDICOM] + .into_iter() + .chain(ndicom.to_le_bytes()) + .collect(); + Bytes::from(payload) +} + +/// Encode a LONK error message. +pub(crate) fn error_message(e: DicomStorageError) -> Bytes { + let mut payload = e.to_string().into_bytes(); + payload.insert(0, MESSAGE_ERROR); + Bytes::from(payload) +} + +/// Get the NATS subject name for a series. +/// +/// Specification: +pub(crate) fn subject_of(series: &SeriesKey) -> String { + format!( + "oxidicom.{}.{}", + &series.pacs_name, + sanitize_subject_part(&series.SeriesInstanceUID) + ) +} + +/// Sanitize a string so that it only contains allowed characters for NATS subjects. +/// https://docs.nats.io/nats-concepts/subjects#characters-allowed-and-recommended-for-subject-names +fn sanitize_subject_part(name: &str) -> String { + name.replace(&[' ', '.', '*', '>'], "_").replace('\0', "") +} diff --git a/src/main.rs b/src/main.rs index de65743..0e41b08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,8 @@ use std::sync::LazyLock; #[tokio::main(flavor = "multi_thread")] async fn main() -> anyhow::Result<()> { - init_tracing_subscriber().unwrap(); - init_otel_tracing().unwrap(); + init_tracing_subscriber()?; + init_otel_tracing()?; let result = run_everything_from_env(None).await; opentelemetry::global::shutdown_tracer_provider(); result diff --git a/src/notifier.rs b/src/notifier.rs index 0eec526..b2f0e15 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -1,22 +1,41 @@ use crate::enums::SeriesEvent; -use crate::error::HandleLoopError; -use crate::types::{SeriesCount, SeriesKey}; +use crate::error::{DicomStorageError, HandleLoopError}; +use crate::lonk::{done_message, error_message, progress_message, subject_of}; +use crate::types::{DicomInfo, SeriesKey, SeriesPath}; +use bytes::Bytes; +use std::collections::HashMap; use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc::UnboundedReceiver; use tokio::task::JoinHandle; +type SeriesCounts = HashMap; + /// Forward objects from `receiver` to the given `client`. /// /// - Received `Some`: add item to the batch. When batch is full, give everything to the `client` /// - Received `None`: flush current batch to the `client` pub async fn cube_pacsfile_notifier( - mut receiver: UnboundedReceiver<(SeriesKey, SeriesEvent, SeriesCount>)>, + mut receiver: UnboundedReceiver<( + SeriesKey, + SeriesEvent, DicomInfo>, + )>, celery: Arc, + nats_client: Option, + progress_interval: Duration, ) -> Result<(), HandleLoopError> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let receiver_loop = async { + let mut counts: SeriesCounts = Default::default(); while let Some((series, event)) = receiver.recv().await { - tx.send(handle_event(series, event, &celery)).unwrap(); + tx.send(handle_event( + &mut counts, + series, + event, + &celery, + nats_client.clone(), + )) + .unwrap(); } drop(tx); }; @@ -42,34 +61,115 @@ pub async fn cube_pacsfile_notifier( type RegistrationTask = JoinHandle>; fn handle_event( - series: SeriesKey, - event: SeriesEvent, SeriesCount>, - client: &Arc, + counts: &mut SeriesCounts, + series_key: SeriesKey, + event: SeriesEvent, DicomInfo>, + celery_client: &Arc, + nats_client: Option, ) -> RegistrationTask { match event { - SeriesEvent::Instance(result) => tokio::spawn(async move { - // dbg!((series, result)); - Ok(()) - }), - SeriesEvent::Finish(count) => { - let client = Arc::clone(client); - tokio::spawn( - async move { send_registration_task_to_celery(series, count, &client).await }, - ) + SeriesEvent::Instance(result) => { + let payload = count_series(series_key.clone(), counts, result); + tokio::spawn(async move { + maybe_send_lonk(nats_client, &series_key, payload).await + }) + } + SeriesEvent::Finish(series_info) => { + let celery_client = Arc::clone(celery_client); + let ndicom = counts.remove(&series_key).unwrap_or(0); + tokio::spawn(async move { + let (a, b) = tokio::join!( + maybe_send_final_progress_messages(nats_client, &series_key, ndicom), + send_registration_task_to_celery(series_info, ndicom, &celery_client) + ); + a.and(b) + }) } } } -async fn send_registration_task_to_celery( +/// If `result` is success: increment the count for the series. +/// Returns a message which _oxidicom_ should send to NATS conveying the status of `result`. +fn count_series( series: SeriesKey, - count: SeriesCount, + counts: &mut SeriesCounts, + result: Result<(), DicomStorageError>, +) -> Bytes { + match result { + Ok(_) => { + let count = counts.entry(series).or_insert(0); + *count += 1; + progress_message(*count) + } + Err(e) => error_message(e), + } +} + +async fn maybe_send_lonk( + client: Option, + series: &SeriesKey, + payload: Bytes, +) -> Result<(), ()> { + if let Some(client) = client { + send_lonk(client, series, payload) + .await + .map_err(|e| { + tracing::error!(error = e.to_string()); + () + }) + } else { + Ok(()) + } +} + +async fn send_lonk( + client: async_nats::Client, + series: &SeriesKey, + payload: Bytes, +) -> Result<(), async_nats::PublishError> { + client.publish(subject_of(series), payload).await +} + +async fn maybe_send_final_progress_messages( + client: Option, + series: &SeriesKey, + ndicom: u32, +) -> Result<(), ()> { + if let Some(client) = client { + send_final_progress_messages(client, series, ndicom) + .await + .map_err(|e| { + tracing::error!(error = e.to_string()); + () + }) + } else { + Ok(()) + } +} + +async fn send_final_progress_messages( + client: async_nats::Client, + series: &SeriesKey, + ndicom: u32, +) -> Result<(), async_nats::PublishError> { + let subject = subject_of(series); + client.publish(subject.clone(), progress_message(ndicom)).await?; + client.publish(subject, done_message()).await +} + +async fn send_registration_task_to_celery( + series: DicomInfo, + ndicom: u32, client: &celery::Celery, ) -> Result<(), ()> { - match client.send_task(count.into_task()).await { + let pacs_name = series.pacs_name.clone(); + let series_instance_uid = series.SeriesInstanceUID.clone(); + let task = series.into_task(ndicom); + match client.send_task(task).await { Ok(r) => { tracing::info!( - pacs_name = series.pacs_name.as_str(), - SeriesInstanceUID = series.SeriesInstanceUID, + pacs_name = pacs_name.as_str(), + SeriesInstanceUID = series_instance_uid, celery_task_id = r.task_id, celery_task_name = "register_pacs_series" ); @@ -77,8 +177,8 @@ async fn send_registration_task_to_celery( } Err(e) => { tracing::error!( - pacs_name = series.pacs_name.as_str(), - SeriesInstanceUID = series.SeriesInstanceUID, + pacs_name = pacs_name.as_str(), + SeriesInstanceUID = series_instance_uid, message = e.to_string() ); Err(()) diff --git a/src/registration_task.rs b/src/registration_task.rs index 9b09ffe..89ddd25 100644 --- a/src/registration_task.rs +++ b/src/registration_task.rs @@ -26,7 +26,7 @@ pub fn register_pacs_series( SeriesInstanceUID: String, pacs_name: AETitle, path: SeriesPath, - ndicom: usize, + ndicom: u32, PatientName: Option, PatientBirthDate: Option, PatientAge: Option, diff --git a/src/run_everything.rs b/src/run_everything.rs index 1961b1c..3be0cf9 100644 --- a/src/run_everything.rs +++ b/src/run_everything.rs @@ -17,7 +17,7 @@ pub async fn run_everything( OxidicomEnvOptions { amqp_address, files_root, - progress_nats_address, + nats_address, progress_interval, scp, scp_max_pdu_length, @@ -37,6 +37,11 @@ where task_routes = [ "pacsfiles.tasks.register_pacs_series" => &queue_name ], ) .await?; + let nats_client = if let Some(address) = nats_address { + Some(async_nats::connect(address).await?) + } else { + None + }; let (tx_association, rx_association) = mpsc::unbounded_channel(); let (tx_storetasks, rx_storetasks) = mpsc::unbounded_channel(); @@ -57,7 +62,7 @@ where association_series_state_loop(rx_association, tx_storetasks, files_root) .map(|r| r.unwrap()), series_synchronizer(rx_storetasks, tx_register), - cube_pacsfile_notifier(rx_register, celery) + cube_pacsfile_notifier(rx_register, celery, nats_client, progress_interval) )?; listener_handle.await? } diff --git a/src/series_synchronizer.rs b/src/series_synchronizer.rs index 9b3d62d..8553d8f 100644 --- a/src/series_synchronizer.rs +++ b/src/series_synchronizer.rs @@ -121,7 +121,6 @@ mod tests { let (sink_tx, mut sink_rx) = unbounded_channel(); let synchronizer = series_synchronizer(source_rx, sink_tx); let source = async move { - let duration = Duration::from_millis(100); source_tx.send(("A", dummy_task(100, "second"))).unwrap(); source_tx.send(("A", dummy_task(150, "third"))).unwrap(); source_tx.send(("A", dummy_task(50, "first"))).unwrap(); diff --git a/src/settings.rs b/src/settings.rs index 168796f..431b6e2 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,7 +10,7 @@ pub struct OxidicomEnvOptions { pub files_root: Utf8PathBuf, #[serde(default = "default_queue_name")] pub queue_name: String, - pub progress_nats_address: Option, + pub nats_address: Option, #[serde(with = "humantime_serde", default = "default_progress_interval")] pub progress_interval: std::time::Duration, pub scp: DicomRsSettings, diff --git a/src/types.rs b/src/types.rs index 06484c8..7722096 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,6 +2,7 @@ use crate::dicomrs_settings::AETitle; use crate::enums::SeriesEvent; +use crate::error::DicomStorageError; use crate::registration_task::register_pacs_series; use aliri_braid::braid; use celery::task::Signature; @@ -71,7 +72,7 @@ pub(crate) struct DicomInfo

{ impl DicomInfo { /// Create task. - pub fn into_task(self, ndicom: usize) -> Signature { + pub fn into_task(self, ndicom: u32) -> Signature { register_pacs_series::new( self.PatientID, self.StudyDate @@ -95,27 +96,9 @@ impl DicomInfo { } } -/// A DICOM series and the number of files received for it. -pub(crate) struct SeriesCount { - pub info: DicomInfo, - pub count: usize, -} - -impl SeriesCount { - pub(crate) fn new(dcm: DicomInfo) -> Self { - Self { - count: 1, - info: dcm.into(), - } - } - - pub(crate) fn into_task(self) -> Signature { - self.info.into_task(self.count) - } -} - /// An [SeriesEvent] for a pending task of writing a DICOM file to storage. -pub(crate) type PendingDicomInstance = SeriesEvent>, SeriesCount>; +pub(crate) type PendingDicomInstance = + SeriesEvent>, DicomInfo>; /// The set of metadata which uniquely identifies a DICOM series in *CUBE*. /// diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 0087e5e..934e979 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -4,6 +4,7 @@ use camino::Utf8Path; use futures::StreamExt; use oxidicom::{run_everything, DicomRsSettings, OxidicomEnvOptions}; use std::num::NonZeroUsize; +use std::time::Duration; mod assertions; mod orthanc_client; @@ -21,31 +22,56 @@ async fn test_run_everything_from_env() { ) .unwrap(); - let queue_name = names::Generator::default().next().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let temp_dir_path = Utf8Path::from_path(temp_dir.path()).unwrap(); - let num_to_handle = Some(EXPECTED_SERIES.len()); + let queue_name = names::Generator::default().next().unwrap(); let options = create_test_options(temp_dir_path, queue_name.to_string()); let amqp_address = options.amqp_address.clone(); - let (start_tx, mut start_rx) = tokio::sync::mpsc::unbounded_channel(); + let (nats_shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel(1); + let nats = async_nats::connect(options.nats_address.as_ref().unwrap()) + .await + .unwrap(); + let mut subscriber = nats.subscribe("oxidicom").await.unwrap(); + let nats_subscriber_loop = tokio::spawn(async move { + let mut messages = Vec::new(); + loop { + tokio::select! { + Some(v) = subscriber.next() => messages.push(v), + Some(_) = shutdown_rx.recv() => break, + } + } + messages + }); + + let (start_tx, start_rx) = tokio::sync::oneshot::channel(); let on_start = move |x| start_tx.send(x).unwrap(); + + let num_to_handle = Some(EXPECTED_SERIES.len()); let server = run_everything(options, num_to_handle, Some(on_start)); let server_handle = tokio::spawn(server); // wait for message from `on_start` indicating server is ready for connections - start_rx.recv().await.unwrap(); + start_rx.await.unwrap(); + // tell Orthanc to send the test data to us futures::stream::iter(EXPECTED_SERIES.iter().map(|s| s.SeriesInstanceUID.as_str())) - .for_each_concurrent(4, |series_instance_uid| async move { + .for_each_concurrent(2, |series_instance_uid| async move { let res = orthanc_store(ORTHANC_URL, CALLING_AE_TITLE, series_instance_uid) .await .unwrap(); assert_eq!(res.failed_instances_count, 0); }) .await; + + // wait for server to shut down server_handle.await.unwrap().unwrap(); + // shutdown the NATS subscriber + nats_shutdown_tx.send(true).await.unwrap(); + nats_subscriber_loop.await.unwrap(); + + // run all assertions tokio::join!( assert_files_stored(&temp_dir_path), assert_rabbitmq_messages(&amqp_address, &queue_name) @@ -60,8 +86,8 @@ fn create_test_options>( amqp_address: "amqp://localhost:5672".to_string(), files_root: files_root.as_ref().to_path_buf(), queue_name, - progress_nats_address: None, - progress_interval: Default::default(), + nats_address: Some("localhost:4222".to_string()), + progress_interval: Duration::from_millis(1), scp: DicomRsSettings { aet: "OXIDICOMTEST".to_string(), strict: false, From da25c3a3e7299f8fac4b96524850321e6aa58f5b Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 12:02:36 -0400 Subject: [PATCH 12/44] SubjectLimiter --- src/lib.rs | 1 + src/limiter.rs | 271 ++++++++++++++++++++++++++++++++++++++ src/notifier.rs | 26 +++- tests/assertions/mod.rs | 14 ++ tests/integration_test.rs | 12 +- 5 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 src/limiter.rs diff --git a/src/lib.rs b/src/lib.rs index ae0cfde..9abd030 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod association_series_state_loop; mod dicomrs_settings; mod enums; mod error; +mod limiter; mod listener_tcp_loop; mod lonk; mod notifier; diff --git a/src/limiter.rs b/src/limiter.rs new file mode 100644 index 0000000..7495d73 --- /dev/null +++ b/src/limiter.rs @@ -0,0 +1,271 @@ +use std::collections::HashMap; +use std::future::Future; +use std::hash::Hash; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::Semaphore; + +/// A synchronization and rate-limiting mechanism. +pub(crate) struct SubjectLimiter(PureSubjectLimiter); + +impl SubjectLimiter { + /// Create a new [SubjectLimiter] which rate-limits functions per subject + /// to be called no more than once per given `interval`. + pub fn new(interval: Duration) -> Self { + Self(PureSubjectLimiter::new(Instant::now() - interval, interval)) + } + + /// Wraps the given async function `f`, calling it if it isn't currently + /// running not has been called recently (within the duration specified + /// to [`SubjectLimiter::new`]). Otherwise, does nothing (i.e. `f` is not called). + pub async fn lock(&self, subject: S, f: Fut) -> Option + where + Fut: Future, + { + self.0 + .lock(Instant::now(), || Instant::now(), subject, f) + .await + } + + /// Forget a subject. Blocks until the function running for the subject is done. + /// Attempting to call [`SubjectLimiter::lock`] _while_ `forget` is running + /// will do nothing (but calling [`SubjectLimiter::lock`] _after_ `forget` is + /// done will re-insert the subject). + pub async fn forget(&self, subject: &S) { + self.0.forget(subject).await + } +} + +struct SubjectState { + semaphore: Arc, + last_sent: Instant, +} + +impl SubjectState { + fn new(last_sent: Instant) -> Self { + Self { + semaphore: Arc::new(Semaphore::new(1)), + last_sent, + } + } +} + +/// Pure implementation of [SubjectLimiter]. +struct PureSubjectLimiter { + subjects: std::sync::Mutex>, + start: Instant, + interval: Duration, +} + +impl PureSubjectLimiter { + fn new(start: Instant, interval: Duration) -> Self { + Self { + subjects: Default::default(), + start, + interval, + } + } + + async fn lock(&self, now: Instant, later: L, subject: S, f: Fut) -> Option + where + L: FnOnce() -> Instant, + Fut: Future, + { + let (try_acquire, last_sent) = { + // note: don't want to keep self.subjects locked while running `f` + let mut subjects = self.subjects.lock().unwrap(); + let state = subjects + .entry(subject.clone()) + .or_insert_with(|| SubjectState::new(self.start)); + let permit = Arc::clone(&state.semaphore).try_acquire_owned(); + (permit, state.last_sent) + }; + if now - last_sent < self.interval { + return None; + } + if let Ok(_permit_raii) = try_acquire { + let ret = f.await; + let mut subjects = self.subjects.lock().unwrap(); + if let Some(state) = subjects.get_mut(&subject) { + state.last_sent = later(); + } + Some(ret) + } else { + None + } + } + + async fn forget(&self, subject: &S) { + let acquire = { + // note: don't want to keep self.subjects locked while awaiting on the semaphore. + let subjects = self.subjects.lock().unwrap(); + subjects + .get(subject) + .map(|state| Arc::clone(&state.semaphore).acquire_owned()) + }; + // self.subjects RAII dropped, we can acquire the semaphore now. + if let Some(acquire) = acquire { + match acquire.await { + Ok(_owned_permit) => { + let mut subjects = self.subjects.lock().unwrap(); + if let Some(state) = subjects.remove(subject) { + state.semaphore.close(); + } + } + Err(_) => { + tracing::warn!(subject = subject, "SubjectLimiter::forget called twice"); + } + } + } else { + tracing::warn!( + subject = subject, + "SubjectLimiter::forget called on unknown subject" + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use tokio::task::JoinHandle; + + #[tokio::test] + async fn test_lock() { + let interval = Duration::from_millis(100); + let limiter = Arc::new(SubjectLimiter::new(interval)); + let task_a = create_task(&limiter, "subject1"); + let task_b = create_task(&limiter, "subject1"); + let task_c = create_task(&limiter, "subject2"); + + let (ret_a, ret_b, ret_c) = tokio::try_join!(task_a, task_b, task_c).unwrap(); + assert!( + ret_a.is_some(), + "task_a was not called, but it should have been called because it was the first function." + ); + assert!( + ret_b.is_none(), + "task_b was called, but it should not have been called because task a recently ran." + ); + assert!( + ret_c.is_some(), + "task_c was not called, but it should have been called because it is a different subject than task_a." + ); + tokio::time::sleep(interval).await; + let task_d = create_task(&limiter, "subject1"); + let ret_d = task_d.await.unwrap(); + assert!( + ret_d.is_some(), + "task_d was not called, but it should have been called because \"subject1\" has not been busy for a while." + ); + } + + fn create_task( + limiter: &Arc>, + subject: S, + ) -> JoinHandle> { + let limiter = Arc::clone(&limiter); + tokio::spawn(async move { + limiter + .lock(subject, async { + tokio::time::sleep(Duration::from_millis(10)).await; + }) + .await + }) + } + + #[tokio::test] + async fn test_lock_during_busy_forget() { + let interval = Duration::from_millis(200); + let limiter = Arc::new(SubjectLimiter::new(interval)); + let finished = Arc::new(std::sync::Mutex::new(false)); + let (tx, rx) = tokio::sync::oneshot::channel(); + let start = Instant::now(); + let task_a = { + let limiter = Arc::clone(&limiter); + let finished = Arc::clone(&finished); + let start = start.clone(); + tokio::spawn(async move { + limiter + .lock("subject1", async { + tx.send(()).unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; + *finished.lock().unwrap() = true; + start.elapsed() + }) + .await + }) + }; + rx.await.unwrap(); + limiter.forget(&"subject1").await; + let outer_elapsed = start.elapsed(); + let task_a_elapsed = task_a.await.unwrap().unwrap(); + assert!( + outer_elapsed >= task_a_elapsed, + "SubjectLimiter::forget should have taken as long as task_a slept for, \ + because it should wait on task_a to finish. \ + outer_elapsed={outer_elapsed:?} task_a_elapsed={task_a_elapsed:?}" + ); + assert!(*finished.lock().unwrap()); + } + + #[tokio::test] + async fn test_forget_called_twice_shouldnt_blow_up() { + let interval = Duration::from_millis(200); + let limiter = Arc::new(SubjectLimiter::new(interval)); + let (tx, rx) = tokio::sync::oneshot::channel(); + let task_a = { + let limiter = Arc::clone(&limiter); + tokio::spawn(async move { + limiter + .lock("subject1", async { + tx.send(()).unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; + }) + .await + }) + }; + rx.await.unwrap(); + tokio::join!(limiter.forget(&"subject1"), limiter.forget(&"subject1")); + task_a.await.unwrap().unwrap(); + } + + #[tokio::test] + async fn test_different_subjects_not_locked() { + let interval = Duration::from_millis(100); + let limiter = Arc::new(SubjectLimiter::new(interval)); + let (tx, mut rx) = tokio::sync::mpsc::channel(8); + let task_a = { + let limiter = Arc::clone(&limiter); + let tx = tx.clone(); + tokio::spawn(async move { + limiter + .lock("subject1", async { + tx.send(1).await.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + tx.send(3).await.unwrap(); + }) + .await + }) + }; + let task_b = { + let limiter = Arc::clone(&limiter); + let tx = tx.clone(); + tokio::spawn(async move { + limiter + .lock("subject2", async { + tokio::time::sleep(Duration::from_millis(100)).await; + tx.send(2).await.unwrap(); + }) + .await + }) + }; + let (a, b) = tokio::try_join!(task_a, task_b).unwrap(); + assert!(a.is_some()); + assert!(b.is_some()); + let actual = [rx.recv().await, rx.recv().await, rx.recv().await]; + let expected = [Some(1), Some(2), Some(3)]; + assert_eq!(actual, expected); + } +} diff --git a/src/notifier.rs b/src/notifier.rs index b2f0e15..3a76b9b 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -1,5 +1,6 @@ use crate::enums::SeriesEvent; use crate::error::{DicomStorageError, HandleLoopError}; +use crate::limiter::SubjectLimiter; use crate::lonk::{done_message, error_message, progress_message, subject_of}; use crate::types::{DicomInfo, SeriesKey, SeriesPath}; use bytes::Bytes; @@ -27,6 +28,7 @@ pub async fn cube_pacsfile_notifier( let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let receiver_loop = async { let mut counts: SeriesCounts = Default::default(); + let limiter = Arc::new(SubjectLimiter::new(progress_interval)); while let Some((series, event)) = receiver.recv().await { tx.send(handle_event( &mut counts, @@ -34,6 +36,7 @@ pub async fn cube_pacsfile_notifier( event, &celery, nats_client.clone(), + Arc::clone(&limiter), )) .unwrap(); } @@ -66,12 +69,13 @@ fn handle_event( event: SeriesEvent, DicomInfo>, celery_client: &Arc, nats_client: Option, + limiter: Arc>, ) -> RegistrationTask { match event { SeriesEvent::Instance(result) => { let payload = count_series(series_key.clone(), counts, result); tokio::spawn(async move { - maybe_send_lonk(nats_client, &series_key, payload).await + maybe_send_lonk(nats_client, limiter, &series_key, payload).await }) } SeriesEvent::Finish(series_info) => { @@ -79,7 +83,7 @@ fn handle_event( let ndicom = counts.remove(&series_key).unwrap_or(0); tokio::spawn(async move { let (a, b) = tokio::join!( - maybe_send_final_progress_messages(nats_client, &series_key, ndicom), + maybe_send_final_progress_messages(nats_client, limiter, &series_key, ndicom), send_registration_task_to_celery(series_info, ndicom, &celery_client) ); a.and(b) @@ -107,11 +111,12 @@ fn count_series( async fn maybe_send_lonk( client: Option, + limiter: Arc>, series: &SeriesKey, payload: Bytes, ) -> Result<(), ()> { if let Some(client) = client { - send_lonk(client, series, payload) + send_lonk(client, limiter, series, payload) .await .map_err(|e| { tracing::error!(error = e.to_string()); @@ -124,17 +129,26 @@ async fn maybe_send_lonk( async fn send_lonk( client: async_nats::Client, + limiter: Arc>, series: &SeriesKey, payload: Bytes, ) -> Result<(), async_nats::PublishError> { - client.publish(subject_of(series), payload).await + let subject = subject_of(series); + limiter + .lock(subject.clone(), client.publish(subject, payload)) + .await + .unwrap_or(Ok(())) } async fn maybe_send_final_progress_messages( client: Option, + limiter: Arc>, series: &SeriesKey, ndicom: u32, ) -> Result<(), ()> { + // ensures prior progress messages are done being sent + limiter.forget(&subject_of(series)).await; + if let Some(client) = client { send_final_progress_messages(client, series, ndicom) .await @@ -153,7 +167,9 @@ async fn send_final_progress_messages( ndicom: u32, ) -> Result<(), async_nats::PublishError> { let subject = subject_of(series); - client.publish(subject.clone(), progress_message(ndicom)).await?; + client + .publish(subject.clone(), progress_message(ndicom)) + .await?; client.publish(subject, done_message()).await } diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index f9cec97..2051bf5 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -95,3 +95,17 @@ fn deserialize_params( } panic!("Expected body to be an array, but it is not.") } + +pub async fn assert_lonk_messages(messages: Vec) { + println!("DUMPING MESSAGES"); + for message in messages { + let hex = message + .payload + .iter() + .map(|b| format!("{b:#04x}")) + .collect::>() + .join(" "); + println!("{} <-- {}", message.subject, hex); + } + println!("DUMPING MESSAGES FINISH"); +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 934e979..7a1be38 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,4 +1,4 @@ -use crate::assertions::{assert_files_stored, assert_rabbitmq_messages, EXPECTED_SERIES}; +use crate::assertions::*; use crate::orthanc_client::orthanc_store; use camino::Utf8Path; use futures::StreamExt; @@ -32,7 +32,7 @@ async fn test_run_everything_from_env() { let nats = async_nats::connect(options.nats_address.as_ref().unwrap()) .await .unwrap(); - let mut subscriber = nats.subscribe("oxidicom").await.unwrap(); + let mut subscriber = nats.subscribe("oxidicom.>").await.unwrap(); let nats_subscriber_loop = tokio::spawn(async move { let mut messages = Vec::new(); loop { @@ -68,13 +68,15 @@ async fn test_run_everything_from_env() { server_handle.await.unwrap().unwrap(); // shutdown the NATS subscriber + tokio::time::sleep(core::time::Duration::from_secs(1)).await; nats_shutdown_tx.send(true).await.unwrap(); - nats_subscriber_loop.await.unwrap(); + let lonk_messages = nats_subscriber_loop.await.unwrap(); // run all assertions tokio::join!( assert_files_stored(&temp_dir_path), - assert_rabbitmq_messages(&amqp_address, &queue_name) + assert_rabbitmq_messages(&amqp_address, &queue_name), + assert_lonk_messages(lonk_messages) ); } @@ -87,7 +89,7 @@ fn create_test_options>( files_root: files_root.as_ref().to_path_buf(), queue_name, nats_address: Some("localhost:4222".to_string()), - progress_interval: Duration::from_millis(1), + progress_interval: Duration::from_millis(50), scp: DicomRsSettings { aet: "OXIDICOMTEST".to_string(), strict: false, From 4d2f23867ea5c5ef4d607f5c92af235f6aea2cd6 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 13:38:04 -0400 Subject: [PATCH 13/44] Rework SubjectLimiter::lock to return a RAII --- src/limiter.rs | 229 +++++++++++++++++++------------------- src/notifier.rs | 52 ++++----- tests/integration_test.rs | 8 +- 3 files changed, 143 insertions(+), 146 deletions(-) diff --git a/src/limiter.rs b/src/limiter.rs index 7495d73..a08b229 100644 --- a/src/limiter.rs +++ b/src/limiter.rs @@ -1,30 +1,32 @@ use std::collections::HashMap; -use std::future::Future; +use std::fmt::Debug; use std::hash::Hash; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use tokio::sync::Semaphore; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; + +/// Something that can be used as a subject key. +pub(crate) trait Subject: Eq + Hash + Clone + Debug {} +impl Subject for T {} /// A synchronization and rate-limiting mechanism. -pub(crate) struct SubjectLimiter(PureSubjectLimiter); +pub(crate) struct SubjectLimiter(KindaPureSubjectLimiter); -impl SubjectLimiter { +impl SubjectLimiter { /// Create a new [SubjectLimiter] which rate-limits functions per subject /// to be called no more than once per given `interval`. pub fn new(interval: Duration) -> Self { - Self(PureSubjectLimiter::new(Instant::now() - interval, interval)) + Self(KindaPureSubjectLimiter::new( + Instant::now() - interval, + interval, + )) } /// Wraps the given async function `f`, calling it if it isn't currently /// running not has been called recently (within the duration specified /// to [`SubjectLimiter::new`]). Otherwise, does nothing (i.e. `f` is not called). - pub async fn lock(&self, subject: S, f: Fut) -> Option - where - Fut: Future, - { - self.0 - .lock(Instant::now(), || Instant::now(), subject, f) - .await + pub fn lock(&self, subject: S) -> Option> { + self.0.lock(Instant::now(), subject) } /// Forget a subject. Blocks until the function running for the subject is done. @@ -50,49 +52,59 @@ impl SubjectState { } } -/// Pure implementation of [SubjectLimiter]. -struct PureSubjectLimiter { - subjects: std::sync::Mutex>, +/// (Not actually) pure implementation of [SubjectLimiter]. +/// +/// In the past, I thought it would be easier to test [SubjectLimiter] if it were +/// implemented purely, but I changed my mind about that. +struct KindaPureSubjectLimiter { + subjects: Arc>>, start: Instant, interval: Duration, } -impl PureSubjectLimiter { +/// A [RAII](https://github.com/rust-unofficial/patterns/blob/main/src/patterns/behavioural/RAII.md) +/// for synchronization by calling [`SubjectLimiter::lock`]. +pub(crate) struct Permit { + _permit: OwnedSemaphorePermit, + subject: S, + subjects: Arc>>, +} + +impl Drop for Permit { + fn drop(&mut self) { + let mut subjects = self.subjects.lock().unwrap(); + if let Some(state) = subjects.get_mut(&self.subject) { + state.last_sent = Instant::now(); // impure + } + } +} + +impl KindaPureSubjectLimiter { fn new(start: Instant, interval: Duration) -> Self { Self { - subjects: Default::default(), + subjects: Arc::new(Default::default()), start, interval, } } - async fn lock(&self, now: Instant, later: L, subject: S, f: Fut) -> Option - where - L: FnOnce() -> Instant, - Fut: Future, - { - let (try_acquire, last_sent) = { - // note: don't want to keep self.subjects locked while running `f` - let mut subjects = self.subjects.lock().unwrap(); - let state = subjects - .entry(subject.clone()) - .or_insert_with(|| SubjectState::new(self.start)); - let permit = Arc::clone(&state.semaphore).try_acquire_owned(); - (permit, state.last_sent) - }; - if now - last_sent < self.interval { + fn lock(&self, now: Instant, subject: S) -> Option> { + let mut subjects = self.subjects.lock().unwrap(); + let state = subjects + .entry(subject.clone()) + .or_insert_with(|| SubjectState::new(self.start)); + if now - state.last_sent < self.interval { return None; } - if let Ok(_permit_raii) = try_acquire { - let ret = f.await; - let mut subjects = self.subjects.lock().unwrap(); - if let Some(state) = subjects.get_mut(&subject) { - state.last_sent = later(); - } - Some(ret) - } else { - None - } + Arc::clone(&state.semaphore) + .try_acquire_owned() + .ok() + .map(|permit| permit) + .map(|permit| Permit { + _permit: permit, + subject, + subjects: Arc::clone(&self.subjects), + }) } async fn forget(&self, subject: &S) { @@ -113,12 +125,15 @@ impl PureSubjectLimiter { } } Err(_) => { - tracing::warn!(subject = subject, "SubjectLimiter::forget called twice"); + tracing::warn!( + subject = format!("{subject:?}"), + "SubjectLimiter::forget called twice" + ); } } } else { tracing::warn!( - subject = subject, + subject = format!("{subject:?}"), "SubjectLimiter::forget called on unknown subject" ); } @@ -128,79 +143,72 @@ impl PureSubjectLimiter { #[cfg(test)] mod tests { use super::*; - use std::sync::Arc; use tokio::task::JoinHandle; #[tokio::test] async fn test_lock() { let interval = Duration::from_millis(100); - let limiter = Arc::new(SubjectLimiter::new(interval)); - let task_a = create_task(&limiter, "subject1"); - let task_b = create_task(&limiter, "subject1"); - let task_c = create_task(&limiter, "subject2"); - - let (ret_a, ret_b, ret_c) = tokio::try_join!(task_a, task_b, task_c).unwrap(); - assert!( - ret_a.is_some(), - "task_a was not called, but it should have been called because it was the first function." + let limiter = SubjectLimiter::new(interval); + let task_a = create_task(&limiter, "subject1").expect( + "task_a was not called, but it should have been called \ + because it was the first function.", ); + let task_b = create_task(&limiter, "subject1"); assert!( - ret_b.is_none(), + task_b.is_none(), "task_b was called, but it should not have been called because task a recently ran." ); - assert!( - ret_c.is_some(), - "task_c was not called, but it should have been called because it is a different subject than task_a." + let task_c = create_task(&limiter, "subject2").expect( + "task_c was not called, but it should have been called \ + because it is a different subject than task_a.", ); - tokio::time::sleep(interval).await; - let task_d = create_task(&limiter, "subject1"); - let ret_d = task_d.await.unwrap(); - assert!( - ret_d.is_some(), - "task_d was not called, but it should have been called because \"subject1\" has not been busy for a while." + + tokio::time::sleep(interval * 2).await; + let task_d = create_task(&limiter, "subject1").expect( + "task_d was not called, but it should have been called \ + because \"subject1\" has not been busy for a while.", ); + tokio::try_join!(task_a, task_c, task_d).unwrap(); } - fn create_task( - limiter: &Arc>, + fn create_task( + limiter: &SubjectLimiter, subject: S, - ) -> JoinHandle> { - let limiter = Arc::clone(&limiter); - tokio::spawn(async move { - limiter - .lock(subject, async { - tokio::time::sleep(Duration::from_millis(10)).await; - }) - .await - }) + ) -> Option> { + if let Some(raii) = limiter.lock(subject) { + let task = tokio::spawn(async move { + let _raii_binding = raii; + tokio::time::sleep(Duration::from_millis(10)).await; + }); + Some(task) + } else { + None + } } #[tokio::test] async fn test_lock_during_busy_forget() { let interval = Duration::from_millis(200); - let limiter = Arc::new(SubjectLimiter::new(interval)); - let finished = Arc::new(std::sync::Mutex::new(false)); + let limiter = SubjectLimiter::new(interval); + let finished = Arc::new(Mutex::new(false)); let (tx, rx) = tokio::sync::oneshot::channel(); let start = Instant::now(); let task_a = { - let limiter = Arc::clone(&limiter); let finished = Arc::clone(&finished); let start = start.clone(); + let raii = limiter.lock("subject1").unwrap(); tokio::spawn(async move { - limiter - .lock("subject1", async { - tx.send(()).unwrap(); - tokio::time::sleep(Duration::from_millis(100)).await; - *finished.lock().unwrap() = true; - start.elapsed() - }) - .await + let _raii_binding = raii; + tx.send(()).unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; + *finished.lock().unwrap() = true; + start.elapsed() }) }; rx.await.unwrap(); limiter.forget(&"subject1").await; let outer_elapsed = start.elapsed(); - let task_a_elapsed = task_a.await.unwrap().unwrap(); + let task_a_elapsed = task_a.await.unwrap(); assert!( outer_elapsed >= task_a_elapsed, "SubjectLimiter::forget should have taken as long as task_a slept for, \ @@ -213,57 +221,46 @@ mod tests { #[tokio::test] async fn test_forget_called_twice_shouldnt_blow_up() { let interval = Duration::from_millis(200); - let limiter = Arc::new(SubjectLimiter::new(interval)); + let limiter = SubjectLimiter::new(interval); let (tx, rx) = tokio::sync::oneshot::channel(); let task_a = { - let limiter = Arc::clone(&limiter); + let raii = limiter.lock("subject1"); tokio::spawn(async move { - limiter - .lock("subject1", async { - tx.send(()).unwrap(); - tokio::time::sleep(Duration::from_millis(100)).await; - }) - .await + let _raii_binding = raii; + tx.send(()).unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; }) }; rx.await.unwrap(); tokio::join!(limiter.forget(&"subject1"), limiter.forget(&"subject1")); - task_a.await.unwrap().unwrap(); + task_a.await.unwrap(); } #[tokio::test] async fn test_different_subjects_not_locked() { let interval = Duration::from_millis(100); - let limiter = Arc::new(SubjectLimiter::new(interval)); + let limiter = SubjectLimiter::new(interval); let (tx, mut rx) = tokio::sync::mpsc::channel(8); let task_a = { - let limiter = Arc::clone(&limiter); let tx = tx.clone(); + let raii = limiter.lock("subject1").unwrap(); tokio::spawn(async move { - limiter - .lock("subject1", async { - tx.send(1).await.unwrap(); - tokio::time::sleep(Duration::from_millis(200)).await; - tx.send(3).await.unwrap(); - }) - .await + let _raii_binding = raii; + tx.send(1).await.unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + tx.send(3).await.unwrap(); }) }; let task_b = { - let limiter = Arc::clone(&limiter); let tx = tx.clone(); + let raii = limiter.lock("subject2").unwrap(); tokio::spawn(async move { - limiter - .lock("subject2", async { - tokio::time::sleep(Duration::from_millis(100)).await; - tx.send(2).await.unwrap(); - }) - .await + let _raii_binding = raii; + tokio::time::sleep(Duration::from_millis(100)).await; + tx.send(2).await.unwrap(); }) }; - let (a, b) = tokio::try_join!(task_a, task_b).unwrap(); - assert!(a.is_some()); - assert!(b.is_some()); + tokio::try_join!(task_a, task_b).unwrap(); let actual = [rx.recv().await, rx.recv().await, rx.recv().await]; let expected = [Some(1), Some(2), Some(3)]; assert_eq!(actual, expected); diff --git a/src/notifier.rs b/src/notifier.rs index 3a76b9b..8d017cc 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -30,15 +30,17 @@ pub async fn cube_pacsfile_notifier( let mut counts: SeriesCounts = Default::default(); let limiter = Arc::new(SubjectLimiter::new(progress_interval)); while let Some((series, event)) = receiver.recv().await { - tx.send(handle_event( + let task = handle_event( &mut counts, series, event, &celery, nats_client.clone(), - Arc::clone(&limiter), - )) - .unwrap(); + &limiter, + ); + if let Some(task) = task { + tx.send(task).unwrap(); + } } drop(tx); }; @@ -69,25 +71,31 @@ fn handle_event( event: SeriesEvent, DicomInfo>, celery_client: &Arc, nats_client: Option, - limiter: Arc>, -) -> RegistrationTask { + limiter: &Arc>, +) -> Option { match event { SeriesEvent::Instance(result) => { let payload = count_series(series_key.clone(), counts, result); - tokio::spawn(async move { - maybe_send_lonk(nats_client, limiter, &series_key, payload).await + limiter.lock(series_key.clone()).map(|raii| { + tokio::spawn(async move { + let _raii_binding = raii; + maybe_send_lonk(nats_client, &series_key, payload).await + }) }) } SeriesEvent::Finish(series_info) => { let celery_client = Arc::clone(celery_client); + let limiter = Arc::clone(limiter); let ndicom = counts.remove(&series_key).unwrap_or(0); - tokio::spawn(async move { + let task = tokio::spawn(async move { + limiter.forget(&series_key).await; let (a, b) = tokio::join!( - maybe_send_final_progress_messages(nats_client, limiter, &series_key, ndicom), + maybe_send_final_progress_messages(nats_client, &series_key, ndicom), send_registration_task_to_celery(series_info, ndicom, &celery_client) ); a.and(b) - }) + }); + Some(task) } } } @@ -111,17 +119,14 @@ fn count_series( async fn maybe_send_lonk( client: Option, - limiter: Arc>, series: &SeriesKey, payload: Bytes, ) -> Result<(), ()> { if let Some(client) = client { - send_lonk(client, limiter, series, payload) - .await - .map_err(|e| { - tracing::error!(error = e.to_string()); - () - }) + send_lonk(client, series, payload).await.map_err(|e| { + tracing::error!(error = e.to_string()); + () + }) } else { Ok(()) } @@ -129,26 +134,17 @@ async fn maybe_send_lonk( async fn send_lonk( client: async_nats::Client, - limiter: Arc>, series: &SeriesKey, payload: Bytes, ) -> Result<(), async_nats::PublishError> { - let subject = subject_of(series); - limiter - .lock(subject.clone(), client.publish(subject, payload)) - .await - .unwrap_or(Ok(())) + client.publish(subject_of(series), payload).await } async fn maybe_send_final_progress_messages( client: Option, - limiter: Arc>, series: &SeriesKey, ndicom: u32, ) -> Result<(), ()> { - // ensures prior progress messages are done being sent - limiter.forget(&subject_of(series)).await; - if let Some(client) = client { send_final_progress_messages(client, series, ndicom) .await diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 7a1be38..38658d2 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -67,8 +67,12 @@ async fn test_run_everything_from_env() { // wait for server to shut down server_handle.await.unwrap().unwrap(); - // shutdown the NATS subscriber - tokio::time::sleep(core::time::Duration::from_secs(1)).await; + // Shutdown the NATS subscriber after waiting a little bit. + // Note: instead of shutting itself down after receiving the correct number + // of "DONE" messages, we prefer the naive approach of waiting 500ms instead, + // so that here in the test we can assert that the "DONE" messages do indeed + // come last and no out-of-order/race condition errors are happening. + tokio::time::sleep(Duration::from_millis(500)).await; nats_shutdown_tx.send(true).await.unwrap(); let lonk_messages = nats_subscriber_loop.await.unwrap(); From e1ee1b60b683a81694e80431477f4797f5078d93 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 14:38:43 -0400 Subject: [PATCH 14/44] Add cross-compilation and remove Dockerfile --- .github/workflows/ci.yml | 93 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d491260..340e74e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,17 +9,18 @@ on: - '.github/**' - '**.rs' - 'Cargo.*' - - 'Dockerfile' - 'docker-compose.yml' pull_request: branches: [ master ] +env: + CARGO_TERM_COLOR: always + jobs: test: name: Test runs-on: ubuntu-24.04 - env: - CARGO_TERM_COLOR: always + if: false # FIXME delete line when I am done figuring things out steps: - uses: actions/checkout@v4 - name: Start services @@ -46,3 +47,89 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: codecov.json fail_ci_if_error: true + build-rust: + name: Build Rust binary + runs-on: ubuntu-24.04 + # FIXME uncomment line below when I am done figuring things out + # needs: [ test ] + strategy: + matrix: + target: + - aarch64-unknown-linux-musl + - x86_64-unknown-linux-musl + steps: + - uses: actions/checkout@v4 + - name: Cache rust build + uses: Swatinem/rust-cache@v2 + - name: Install cross-compilation tools + uses: taiki-e/setup-cross-toolchain-action@v1 + with: + target: ${{ matrix.target }} + - name: Build + run: cargo build --release --locked + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: 'build__${{ matrix.target }}' + path: 'target/${{ matrix.target }}/release/oxidicom' + if-no-files-found: 'error' + build-docker: + name: Build container image + runs-on: ubuntu-24.04 + needs: [ build-rust ] + # if: github.event_name == 'push' + steps: + - name: Download x86_64 binary + uses: actions/download-artifact@v4 + - name: Print out all files + run: find -type f + - name: Move binaries + run: | + mkdir -vp dist/linux/amd64 dist/linux/arm64 + mv -v build__x86_64-unknown-linux-musl/oxidicom dist/linux/amd64/oxidicom + mv -v build__aarch64-unknown-linux-musl/oxidicom dist/linux/arm64/oxidicom + - name: Create Dockerfile + run: | + cat > Dockerfile << EOF + # syntax=docker/dockerfile:1 + FROM scratch + ARG TARGETPLATFORM + COPY ./dist/\$TARGETPLATFORM/oxidicom /oxidicom + CMD ["/oxidicom"] + EOF + - uses: docker/metadata-action@v5 + id: meta + with: + images: | + docker.io/fnndsc/oxidicom + ghcr.io/fnndsc/oxidicom + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From b44a0bfa28d8d9214b3fb7a822b92772c0a0cf8f Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 15:12:55 -0400 Subject: [PATCH 15/44] Switch to houseabsolute/actions-rust-cross --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 340e74e..75ddd5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,12 +61,12 @@ jobs: - uses: actions/checkout@v4 - name: Cache rust build uses: Swatinem/rust-cache@v2 - - name: Install cross-compilation tools - uses: taiki-e/setup-cross-toolchain-action@v1 + - name: Build + uses: houseabsolute/actions-rust-cross@ad283b2fc65ad1f3a04fb8bf8b2b829aad4a9318 with: target: ${{ matrix.target }} - - name: Build - run: cargo build --release --locked + command: build + args: --release --locked - name: Upload artifacts uses: actions/upload-artifact@v4 with: From 86eea0adb225c4ddebfe69711bedfb2e2dd5b099 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 15:19:00 -0400 Subject: [PATCH 16/44] Remove FIXMEs --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75ddd5b..1785c7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,6 @@ jobs: test: name: Test runs-on: ubuntu-24.04 - if: false # FIXME delete line when I am done figuring things out steps: - uses: actions/checkout@v4 - name: Start services @@ -50,8 +49,7 @@ jobs: build-rust: name: Build Rust binary runs-on: ubuntu-24.04 - # FIXME uncomment line below when I am done figuring things out - # needs: [ test ] + needs: [ test ] strategy: matrix: target: From 3c8b42433d85ea362e51f65b9c916913a2c1e56c Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Sun, 8 Sep 2024 15:40:18 -0400 Subject: [PATCH 17/44] chmod binary --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1785c7d..41018b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,11 +81,12 @@ jobs: uses: actions/download-artifact@v4 - name: Print out all files run: find -type f - - name: Move binaries + - name: Move binaries and mark executable run: | mkdir -vp dist/linux/amd64 dist/linux/arm64 mv -v build__x86_64-unknown-linux-musl/oxidicom dist/linux/amd64/oxidicom mv -v build__aarch64-unknown-linux-musl/oxidicom dist/linux/arm64/oxidicom + chmod -v 555 dist/linux/{amd64,arm64}/oxidicom - name: Create Dockerfile run: | cat > Dockerfile << EOF From 33838f22a5431a349b3b83a313035b8e22d16bb1 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 10:29:24 -0400 Subject: [PATCH 18/44] Test NATS messages --- src/association_series_state_loop.rs | 2 +- src/dicomrs_settings.rs | 5 --- src/enums.rs | 2 +- src/lib.rs | 3 +- src/lonk.rs | 14 +++---- src/pacs_file.rs | 2 +- src/registration_task.rs | 2 +- src/scp.rs | 2 +- src/types.rs | 7 +++- tests/assertions/mod.rs | 60 +++++++++++++++++++++++----- tests/integration_test.rs | 4 +- 11 files changed, 71 insertions(+), 32 deletions(-) diff --git a/src/association_series_state_loop.rs b/src/association_series_state_loop.rs index e291665..4579d46 100644 --- a/src/association_series_state_loop.rs +++ b/src/association_series_state_loop.rs @@ -1,8 +1,8 @@ -use crate::dicomrs_settings::AETitle; use crate::enums::{AssociationEvent, SeriesEvent}; use crate::error::{DicomRequiredTagError, DicomStorageError, HandleLoopError}; use crate::pacs_file::{BadTag, PacsFileRegistration}; use crate::types::{DicomFilePath, DicomInfo, PendingDicomInstance, SeriesKey, SeriesPath}; +use crate::AETitle; use camino::{Utf8Path, Utf8PathBuf}; use dicom::object::DefaultDicomObject; use std::collections::HashMap; diff --git a/src/dicomrs_settings.rs b/src/dicomrs_settings.rs index 29dda90..e42f595 100644 --- a/src/dicomrs_settings.rs +++ b/src/dicomrs_settings.rs @@ -1,14 +1,9 @@ use crate::transfer::ABSTRACT_SYNTAXES; -use aliri_braid::braid; use dicom::dictionary_std::uids; use dicom::transfer_syntax::TransferSyntaxRegistry; use dicom::ul::association::server::AcceptAny; use dicom::ul::ServerAssociationOptions; -/// The AE title of a peer PACS server pushing DICOMs to us. -#[braid(serde)] -pub struct AETitle; - #[derive(Debug, serde::Deserialize)] pub struct DicomRsSettings { /// Our AE title. diff --git a/src/enums.rs b/src/enums.rs index ba5686f..9f56c88 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,7 +1,7 @@ use dicom::object::DefaultDicomObject; use ulid::Ulid; -use crate::dicomrs_settings::AETitle; +use crate::AETitle; /// Events which occur during an association. pub(crate) enum AssociationEvent { diff --git a/src/lib.rs b/src/lib.rs index 9abd030..150297a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ mod enums; mod error; mod limiter; mod listener_tcp_loop; -mod lonk; +pub mod lonk; mod notifier; mod pacs_file; mod patient_age; @@ -24,3 +24,4 @@ pub use dicomrs_settings::DicomRsSettings; pub use registration_task::register_pacs_series; pub use run_everything::run_everything; pub use settings::OxidicomEnvOptions; +pub use types::{AETitle, AETitleRef, SeriesKey}; diff --git a/src/lonk.rs b/src/lonk.rs index 5d0912f..8124cfe 100644 --- a/src/lonk.rs +++ b/src/lonk.rs @@ -6,16 +6,16 @@ use crate::error::DicomStorageError; use crate::types::SeriesKey; use bytes::Bytes; -const MESSAGE_NDICOM: u8 = 0x01; -const MESSAGE_ERROR: u8 = 0x02; -const DONE_MESSAGE: [u8; 1] = [0x00]; +pub const MESSAGE_NDICOM: u8 = 0x01; +pub const MESSAGE_ERROR: u8 = 0x02; +pub const DONE_MESSAGE: [u8; 1] = [0x00]; -pub(crate) fn done_message() -> Bytes { +pub fn done_message() -> Bytes { Bytes::from_static(&DONE_MESSAGE) } /// Encode a LONK progress message. -pub(crate) fn progress_message(ndicom: u32) -> Bytes { +pub fn progress_message(ndicom: u32) -> Bytes { let payload: Vec = [MESSAGE_NDICOM] .into_iter() .chain(ndicom.to_le_bytes()) @@ -24,7 +24,7 @@ pub(crate) fn progress_message(ndicom: u32) -> Bytes { } /// Encode a LONK error message. -pub(crate) fn error_message(e: DicomStorageError) -> Bytes { +pub fn error_message(e: DicomStorageError) -> Bytes { let mut payload = e.to_string().into_bytes(); payload.insert(0, MESSAGE_ERROR); Bytes::from(payload) @@ -33,7 +33,7 @@ pub(crate) fn error_message(e: DicomStorageError) -> Bytes { /// Get the NATS subject name for a series. /// /// Specification: -pub(crate) fn subject_of(series: &SeriesKey) -> String { +pub fn subject_of(series: &SeriesKey) -> String { format!( "oxidicom.{}.{}", &series.pacs_name, diff --git a/src/pacs_file.rs b/src/pacs_file.rs index 65c70fe..ba5bfed 100644 --- a/src/pacs_file.rs +++ b/src/pacs_file.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use crate::dicomrs_settings::AETitle; +use crate::AETitle; use dicom::dictionary_std::tags; use dicom::object::{DefaultDicomObject, Tag}; diff --git a/src/registration_task.rs b/src/registration_task.rs index 89ddd25..b3889ad 100644 --- a/src/registration_task.rs +++ b/src/registration_task.rs @@ -6,8 +6,8 @@ #![allow(non_snake_case)] #![allow(clippy::too_many_arguments)] -use crate::dicomrs_settings::AETitle; use crate::types::SeriesPath; +use crate::AETitle; /// A function stub with the same signature as the `register_pacs_series` celery task /// in *CUBE*'s Python code. diff --git a/src/scp.rs b/src/scp.rs index a0cdf38..9e82c3a 100644 --- a/src/scp.rs +++ b/src/scp.rs @@ -20,8 +20,8 @@ use tokio::sync::mpsc::UnboundedSender; use ulid::Ulid; use crate::association_error::{AssociationError, AssociationError::*}; -use crate::dicomrs_settings::AETitle; use crate::enums::AssociationEvent; +use crate::AETitle; /// Handle an "association" from an "SCU" (i.e. handle when someone is trying to give us DICOM files). /// diff --git a/src/types.rs b/src/types.rs index 7722096..7b64735 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,5 @@ #![allow(non_snake_case)] -use crate::dicomrs_settings::AETitle; use crate::enums::SeriesEvent; use crate::error::DicomStorageError; use crate::registration_task::register_pacs_series; @@ -17,6 +16,10 @@ pub(crate) struct DicomFilePath; #[braid(serde)] pub(crate) struct SeriesPath; +/// The AE title of a peer PACS server pushing DICOMs to us. +#[braid(serde)] +pub struct AETitle; + impl From for SeriesPath { fn from(path: DicomFilePath) -> Self { path.as_str() @@ -104,7 +107,7 @@ pub(crate) type PendingDicomInstance = /// /// https://github.com/FNNDSC/ChRIS_ultron_backEnd/blob/v6.1.0/chris_backend/pacsfiles/models.py#L60 #[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct SeriesKey { +pub struct SeriesKey { /// Series instance UID #[allow(non_snake_case)] pub SeriesInstanceUID: String, diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index 2051bf5..8e9e6e6 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -9,7 +9,7 @@ use celery::prelude::BrokerError; use celery::protocol::MessageBody; pub use expected::EXPECTED_SERIES; use futures::{stream, StreamExt, TryStreamExt}; -use oxidicom::register_pacs_series; +use oxidicom::{register_pacs_series, AETitle, SeriesKey}; use std::collections::HashSet; pub async fn assert_files_stored(storage_path: &Utf8Path) { @@ -96,16 +96,54 @@ fn deserialize_params( panic!("Expected body to be an array, but it is not.") } -pub async fn assert_lonk_messages(messages: Vec) { - println!("DUMPING MESSAGES"); - for message in messages { - let hex = message - .payload +pub fn assert_lonk_messages(messages: Vec) { + for series in &*EXPECTED_SERIES { + let series_key = SeriesKey { + SeriesInstanceUID: series.SeriesInstanceUID.to_string(), + pacs_name: AETitle::from(series.pacs_name.as_str()), + }; + let subject = oxidicom::lonk::subject_of(&series_key); + let messages_of_series: Vec<_> = messages .iter() - .map(|b| format!("{b:#04x}")) - .collect::>() - .join(" "); - println!("{} <-- {}", message.subject, hex); + .filter(|message| message.subject.as_str() == &subject) + .collect(); + assert_messages_for_series(&messages_of_series, series.ndicom as u32) + } +} + +fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom: u32) { + assert!( + messages.len() >= 3, + "There must be at least 3 messages per series: (1) first progress message, \ + (2) last progress message, (3) done message" + ); + + assert_eq!( + messages.last().unwrap().payload, + oxidicom::lonk::done_message() + ); + + let second_last = &messages[messages.len() - 2].payload; + assert_eq!(second_last[0], oxidicom::lonk::MESSAGE_NDICOM); + let last_ndicom = u32::from_le_bytes([ + second_last[1], + second_last[2], + second_last[3], + second_last[4], + ]); + assert_eq!(last_ndicom, expected_ndicom); + + let mut prev = 0; + for message in &messages[..messages.len() - 2] { + let payload = &message.payload; + let first_byte = *payload.first().unwrap(); + assert_eq!(first_byte, oxidicom::lonk::MESSAGE_NDICOM); + assert_eq!(payload.len(), 1 + size_of::()); + let num = u32::from_le_bytes([payload[1], payload[2], payload[3], payload[4]]); + assert!( + num > prev, + "ndicom progress message value must always increase." + ); + prev = num; } - println!("DUMPING MESSAGES FINISH"); } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 38658d2..efd1852 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -77,10 +77,12 @@ async fn test_run_everything_from_env() { let lonk_messages = nats_subscriber_loop.await.unwrap(); // run all assertions + + assert_lonk_messages(lonk_messages); + tokio::join!( assert_files_stored(&temp_dir_path), assert_rabbitmq_messages(&amqp_address, &queue_name), - assert_lonk_messages(lonk_messages) ); } From 961b5ebcc3adb8dd689ba509789f89af5753fab1 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 19:34:49 -0400 Subject: [PATCH 19/44] Change order of assertions --- tests/assertions/mod.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index 8e9e6e6..949233c 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -118,21 +118,6 @@ fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom (2) last progress message, (3) done message" ); - assert_eq!( - messages.last().unwrap().payload, - oxidicom::lonk::done_message() - ); - - let second_last = &messages[messages.len() - 2].payload; - assert_eq!(second_last[0], oxidicom::lonk::MESSAGE_NDICOM); - let last_ndicom = u32::from_le_bytes([ - second_last[1], - second_last[2], - second_last[3], - second_last[4], - ]); - assert_eq!(last_ndicom, expected_ndicom); - let mut prev = 0; for message in &messages[..messages.len() - 2] { let payload = &message.payload; @@ -146,4 +131,19 @@ fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom ); prev = num; } + + let second_last = &messages[messages.len() - 2].payload; + assert_eq!(second_last[0], oxidicom::lonk::MESSAGE_NDICOM); + let last_ndicom = u32::from_le_bytes([ + second_last[1], + second_last[2], + second_last[3], + second_last[4], + ]); + assert_eq!(last_ndicom, expected_ndicom); + + assert_eq!( + messages.last().unwrap().payload, + oxidicom::lonk::done_message() + ); } From 430f98c678091e98fab476c85827ccb5e699402a Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 20:00:04 -0400 Subject: [PATCH 20/44] Print last 3 payloads for debugging --- tests/assertions/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index 949233c..e9e546d 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -132,6 +132,14 @@ fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom prev = num; } + let last_three_payloads = messages[messages.len() - 3..] + .iter() + .map(|message| &message.payload) + .map(|payload| payload.iter().map(|b| format!("{b:#04x}")).collect::>().join(" ")) + .collect::>() + .join("\n"); + tracing::info!("Last 3 payloads for series:\n---\n{}\n---", last_three_payloads); + let second_last = &messages[messages.len() - 2].payload; assert_eq!(second_last[0], oxidicom::lonk::MESSAGE_NDICOM); let last_ndicom = u32::from_le_bytes([ From 17b510cd6404ed4f548492357749af8e6b967902 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 20:35:14 -0400 Subject: [PATCH 21/44] Rewrite test_forget_waits_until_unlocked --- src/limiter.rs | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/limiter.rs b/src/limiter.rs index a08b229..bc55699 100644 --- a/src/limiter.rs +++ b/src/limiter.rs @@ -187,35 +187,26 @@ mod tests { } #[tokio::test] - async fn test_lock_during_busy_forget() { + async fn test_forget_waits_until_unlocked() { let interval = Duration::from_millis(200); let limiter = SubjectLimiter::new(interval); - let finished = Arc::new(Mutex::new(false)); - let (tx, rx) = tokio::sync::oneshot::channel(); - let start = Instant::now(); + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let task_finished = Arc::new(Mutex::new(false)); + let task_a = { - let finished = Arc::clone(&finished); - let start = start.clone(); let raii = limiter.lock("subject1").unwrap(); + let task_finished = Arc::clone(&task_finished); tokio::spawn(async move { let _raii_binding = raii; - tx.send(()).unwrap(); - tokio::time::sleep(Duration::from_millis(100)).await; - *finished.lock().unwrap() = true; - start.elapsed() + started_tx.send(()).unwrap(); + tokio::time::sleep(Duration::from_millis(200)).await; + *task_finished.lock().unwrap() = true; }) }; - rx.await.unwrap(); + started_rx.await.unwrap(); limiter.forget(&"subject1").await; - let outer_elapsed = start.elapsed(); - let task_a_elapsed = task_a.await.unwrap(); - assert!( - outer_elapsed >= task_a_elapsed, - "SubjectLimiter::forget should have taken as long as task_a slept for, \ - because it should wait on task_a to finish. \ - outer_elapsed={outer_elapsed:?} task_a_elapsed={task_a_elapsed:?}" - ); - assert!(*finished.lock().unwrap()); + assert!(Arc::into_inner(task_finished).unwrap().into_inner().unwrap()); + task_a.await.unwrap(); } #[tokio::test] From 3c6e7c59f6834c36a0f74602ab2c78a4eca53fdc Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 20:52:36 -0400 Subject: [PATCH 22/44] Add a GHA job to rerun tests --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41018b7..e7c7ff9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,3 +132,30 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + # Re-reun tests many times to detect race conditions. + # https://github.com/FNNDSC/oxidicom/issues/4 + retest: + name: Rerun tests many times + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Start services + run: docker compose up -d + - name: Download example data + run: docker compose run --rm get-data + - name: Cache rust build + uses: Swatinem/rust-cache@v2 + - name: Compile test binary + run: cargo test --no-run --locked + - name: Run tests + run: | + set +e + for i in {1..20}; do + cargo test > log + rc=$? + if [ "$rc" != '0' ]; then + cat log + exit "$rc" + fi + done From 96854540f0e808c80c65271b5cbeb7c97af16c5b Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 20:57:12 -0400 Subject: [PATCH 23/44] Wait for a longer time --- tests/integration_test.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index efd1852..f5250ec 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -69,17 +69,16 @@ async fn test_run_everything_from_env() { // Shutdown the NATS subscriber after waiting a little bit. // Note: instead of shutting itself down after receiving the correct number - // of "DONE" messages, we prefer the naive approach of waiting 500ms instead, - // so that here in the test we can assert that the "DONE" messages do indeed - // come last and no out-of-order/race condition errors are happening. - tokio::time::sleep(Duration::from_millis(500)).await; + // of "DONE" messages, we prefer the naive approach of waiting instead, + // so that here in the test we can assert that the "DONE" messages do + // indeed come last and no out-of-order/race condition errors are happening. + // https://github.com/FNNDSC/oxidicom/issues/4 + tokio::time::sleep(Duration::from_secs(2)).await; nats_shutdown_tx.send(true).await.unwrap(); let lonk_messages = nats_subscriber_loop.await.unwrap(); // run all assertions - assert_lonk_messages(lonk_messages); - tokio::join!( assert_files_stored(&temp_dir_path), assert_rabbitmq_messages(&amqp_address, &queue_name), From 42a0e5f19ce1134ac5529d2cc81f95e24e986d31 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 9 Sep 2024 21:01:58 -0400 Subject: [PATCH 24/44] Sleep for a really long time in GHA --- tests/integration_test.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index f5250ec..8ec904d 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -73,7 +73,7 @@ async fn test_run_everything_from_env() { // so that here in the test we can assert that the "DONE" messages do // indeed come last and no out-of-order/race condition errors are happening. // https://github.com/FNNDSC/oxidicom/issues/4 - tokio::time::sleep(Duration::from_secs(2)).await; + tokio::time::sleep(sleep_duration()).await; nats_shutdown_tx.send(true).await.unwrap(); let lonk_messages = nats_subscriber_loop.await.unwrap(); @@ -106,3 +106,11 @@ fn create_test_options>( listener_port: 11112, } } + +fn sleep_duration() -> Duration { + if env!("CI") == "true" { + Duration::from_secs(10) + } else { + Duration::from_millis(500) + } +} From e6bb83d1ea2fbaf5bb4af7dbf518a4b1a2957f2d Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 09:18:34 -0400 Subject: [PATCH 25/44] Sanitize pacs_name too --- src/lonk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lonk.rs b/src/lonk.rs index 8124cfe..d9fe8a5 100644 --- a/src/lonk.rs +++ b/src/lonk.rs @@ -36,7 +36,7 @@ pub fn error_message(e: DicomStorageError) -> Bytes { pub fn subject_of(series: &SeriesKey) -> String { format!( "oxidicom.{}.{}", - &series.pacs_name, + sanitize_subject_part(series.pacs_name.as_str()), sanitize_subject_part(&series.SeriesInstanceUID) ) } From cf00b42af2cca8f84bf766f9a21720b3a4aed087 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 09:20:37 -0400 Subject: [PATCH 26/44] Replace env! with option_env! --- tests/integration_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 8ec904d..579d1b3 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -108,7 +108,7 @@ fn create_test_options>( } fn sleep_duration() -> Duration { - if env!("CI") == "true" { + if matches!(option_env!("CI"), Some("true")) { Duration::from_secs(10) } else { Duration::from_millis(500) From a4716ed3789ad86568875d494505eb4c2c59ba8d Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 17:07:50 -0400 Subject: [PATCH 27/44] Count failures --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7c7ff9..16faae7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,11 +151,21 @@ jobs: - name: Run tests run: | set +e + failed_count=0 + mkdir logs for i in {1..20}; do cargo test > log rc=$? + echo "Attempt #$i --> $rc" if [ "$rc" != '0' ]; then - cat log - exit "$rc" + ((failed_count++)) + mv log logs/$(date +%s).log fi done + + if [ "$failed_count" != 0 ]; then + cat logs/*.log + echo "::error ::$failed_count out of 20 attempts failed." + exit 1 + fi + From 40338198daa0c885cd6a24ab055050e5ada26562 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 17:16:19 -0400 Subject: [PATCH 28/44] Rework how the nats_subscriber_loop ends --- tests/integration_test.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 579d1b3..0e246c2 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -28,7 +28,6 @@ async fn test_run_everything_from_env() { let queue_name = names::Generator::default().next().unwrap(); let options = create_test_options(temp_dir_path, queue_name.to_string()); let amqp_address = options.amqp_address.clone(); - let (nats_shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel(1); let nats = async_nats::connect(options.nats_address.as_ref().unwrap()) .await .unwrap(); @@ -36,9 +35,10 @@ async fn test_run_everything_from_env() { let nats_subscriber_loop = tokio::spawn(async move { let mut messages = Vec::new(); loop { + // Loop until no more messages received for a while. tokio::select! { Some(v) = subscriber.next() => messages.push(v), - Some(_) = shutdown_rx.recv() => break, + _ = tokio::time::sleep(sleep_duration()) => break, } } messages @@ -67,14 +67,7 @@ async fn test_run_everything_from_env() { // wait for server to shut down server_handle.await.unwrap().unwrap(); - // Shutdown the NATS subscriber after waiting a little bit. - // Note: instead of shutting itself down after receiving the correct number - // of "DONE" messages, we prefer the naive approach of waiting instead, - // so that here in the test we can assert that the "DONE" messages do - // indeed come last and no out-of-order/race condition errors are happening. - // https://github.com/FNNDSC/oxidicom/issues/4 - tokio::time::sleep(sleep_duration()).await; - nats_shutdown_tx.send(true).await.unwrap(); + // get messages from NATS let lonk_messages = nats_subscriber_loop.await.unwrap(); // run all assertions From ff73cc783e6d8a08f93e3c1dfc9a097d845f109b Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 17:29:39 -0400 Subject: [PATCH 29/44] Change order of assertions --- tests/integration_test.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 0e246c2..78793be 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -71,11 +71,9 @@ async fn test_run_everything_from_env() { let lonk_messages = nats_subscriber_loop.await.unwrap(); // run all assertions + assert_files_stored(&temp_dir_path).await; + assert_rabbitmq_messages(&amqp_address, &queue_name).await; assert_lonk_messages(lonk_messages); - tokio::join!( - assert_files_stored(&temp_dir_path), - assert_rabbitmq_messages(&amqp_address, &queue_name), - ); } fn create_test_options>( From 640cc69fb916a153fb9b63f24692c9f6f491dba9 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 17:41:59 -0400 Subject: [PATCH 30/44] Use tracing_subscriber::EnvFilter --- Cargo.lock | 38 +++++++++++++++++++++++++++++++++----- Cargo.toml | 2 +- README.md | 1 + src/main.rs | 7 +------ tests/integration_test.rs | 2 +- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7dee109..a8d1331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1606,8 +1606,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", ] [[package]] @@ -2112,6 +2112,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -2941,8 +2950,17 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -2953,9 +2971,15 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -3940,10 +3964,14 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index 7df5a11..e1ba80a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ opentelemetry_sdk = { version = "0.24.1", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.17.0", features = ["trace", "grpc-tonic"] } opentelemetry-semantic-conventions = "0.16.0" tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } aliri_braid = "0.4.0" anyhow = "1.0.87" tokio = { version = "1.40.0", features = ["full"] } diff --git a/README.md b/README.md index 048562b..d152796 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ docker compose run --rm get-data Run all tests: ```shell +export RUST_LOG=oxidicom=debug,integration_test=debug # optional cargo test ``` diff --git a/src/main.rs b/src/main.rs index 0e41b08..85e7b4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,14 +34,9 @@ fn init_otel_tracing() -> Result { } fn init_tracing_subscriber() -> Result<(), tracing::dispatcher::SetGlobalDefaultError> { - let level = if CONFIG.extract_inner_lossy("verbose").unwrap_or(false) { - tracing::Level::INFO - } else { - tracing::Level::WARN - }; tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() - .with_max_level(level) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .finish(), ) } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 78793be..f333f9e 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -17,7 +17,7 @@ const CALLING_AE_TITLE: &str = "OXIDICOMTEST"; async fn test_run_everything_from_env() { tracing::subscriber::set_global_default( tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::INFO) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .finish(), ) .unwrap(); From 1ea90fe019f1f6e66189c115483942bc98760d41 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 17:42:36 -0400 Subject: [PATCH 31/44] cargo fmt --- src/limiter.rs | 5 ++++- tests/assertions/mod.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/limiter.rs b/src/limiter.rs index bc55699..856c000 100644 --- a/src/limiter.rs +++ b/src/limiter.rs @@ -205,7 +205,10 @@ mod tests { }; started_rx.await.unwrap(); limiter.forget(&"subject1").await; - assert!(Arc::into_inner(task_finished).unwrap().into_inner().unwrap()); + assert!(Arc::into_inner(task_finished) + .unwrap() + .into_inner() + .unwrap()); task_a.await.unwrap(); } diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index e9e546d..47fcb7b 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -135,10 +135,19 @@ fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom let last_three_payloads = messages[messages.len() - 3..] .iter() .map(|message| &message.payload) - .map(|payload| payload.iter().map(|b| format!("{b:#04x}")).collect::>().join(" ")) + .map(|payload| { + payload + .iter() + .map(|b| format!("{b:#04x}")) + .collect::>() + .join(" ") + }) .collect::>() .join("\n"); - tracing::info!("Last 3 payloads for series:\n---\n{}\n---", last_three_payloads); + tracing::info!( + "Last 3 payloads for series:\n---\n{}\n---", + last_three_payloads + ); let second_last = &messages[messages.len() - 2].payload; assert_eq!(second_last[0], oxidicom::lonk::MESSAGE_NDICOM); From f970fdd3532cabad70ec16ad2b4f5afdf1644eda Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 17:42:58 -0400 Subject: [PATCH 32/44] Delete reset.sh --- reset.sh | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100755 reset.sh diff --git a/reset.sh b/reset.sh deleted file mode 100755 index bd27ecb..0000000 --- a/reset.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -e - -docker exec chris python manage.py shell -c ' -from django.conf import settings -from core.storage import connect_storage -from pacsfiles.models import PACSFile, PACS - -for pacs_file in PACSFile.objects.all(): - pacs_file.delete() - -for pacs in PACS.objects.all(): - pacs.delete() - -storage = connect_storage(settings) -for f in storage.ls("SERVICES/PACS"): - storage.delete_obj(f) -' From bdf2e6dd971249109dfe54c17a69a95e45a6c8a8 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 10 Sep 2024 18:51:32 -0400 Subject: [PATCH 33/44] Add a bunch of tracing statements --- .github/workflows/ci.yml | 4 +++ src/limiter.rs | 53 ++++++++++++++++++++-------- src/notifier.rs | 76 ++++++++++++++++++++++++++++------------ tests/assertions/mod.rs | 33 +++++++++-------- 4 files changed, 112 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16faae7..5fbf64f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,8 @@ jobs: source <(cargo llvm-cov show-env --export-prefix) cargo test --no-run --locked - name: Run tests + env: + RUST_LOG: oxidicom=debug,integration_test=debug run: cargo llvm-cov test --locked --codecov --output-path codecov.json - name: Print service logs if: failure() @@ -149,6 +151,8 @@ jobs: - name: Compile test binary run: cargo test --no-run --locked - name: Run tests + env: + RUST_LOG: oxidicom::notifier=trace,oxidicom::limiter=trace,integration_test=trace run: | set +e failed_count=0 diff --git a/src/limiter.rs b/src/limiter.rs index 856c000..b626c7c 100644 --- a/src/limiter.rs +++ b/src/limiter.rs @@ -25,7 +25,7 @@ impl SubjectLimiter { /// Wraps the given async function `f`, calling it if it isn't currently /// running not has been called recently (within the duration specified /// to [`SubjectLimiter::new`]). Otherwise, does nothing (i.e. `f` is not called). - pub fn lock(&self, subject: S) -> Option> { + pub fn lock(&self, subject: S) -> Result, LockError> { self.0.lock(Instant::now(), subject) } @@ -38,6 +38,16 @@ impl SubjectLimiter { } } +/// Reasons why [`SubjectLimiter::lock`] would not lock. +#[derive(thiserror::Error, Debug, Eq, PartialEq, Copy, Clone)] +pub(crate) enum LockError { + #[error("not enough time has elapsed since the lock was released.")] + TooSoon, + + #[error("the lock is currently held.")] + Busy, +} + struct SubjectState { semaphore: Arc, last_sent: Instant, @@ -76,6 +86,7 @@ impl Drop for Permit { if let Some(state) = subjects.get_mut(&self.subject) { state.last_sent = Instant::now(); // impure } + tracing::trace!("permit dropped"); } } @@ -88,17 +99,23 @@ impl KindaPureSubjectLimiter { } } - fn lock(&self, now: Instant, subject: S) -> Option> { + fn lock(&self, now: Instant, subject: S) -> Result, LockError> { let mut subjects = self.subjects.lock().unwrap(); let state = subjects .entry(subject.clone()) - .or_insert_with(|| SubjectState::new(self.start)); + .and_modify(|_| { + tracing::trace!(subject = format!("{:?}", &subject), "old subject"); + }) + .or_insert_with(|| { + tracing::debug!(subject = format!("{:?}", &subject), "new subject"); + SubjectState::new(self.start) + }); if now - state.last_sent < self.interval { - return None; + return Err(LockError::TooSoon); } Arc::clone(&state.semaphore) .try_acquire_owned() - .ok() + .map_err(|_| LockError::Busy) .map(|permit| permit) .map(|permit| Permit { _permit: permit, @@ -113,16 +130,25 @@ impl KindaPureSubjectLimiter { let subjects = self.subjects.lock().unwrap(); subjects .get(subject) - .map(|state| Arc::clone(&state.semaphore).acquire_owned()) + .map(|state| Arc::clone(&state.semaphore)) }; // self.subjects RAII dropped, we can acquire the semaphore now. - if let Some(acquire) = acquire { - match acquire.await { + if let Some(semaphore) = acquire { + if semaphore.available_permits() == 0 { + tracing::trace!( + subject = format!("{subject:?}"), + "lock is held, we will have to wait for it." + ); + } else { + tracing::trace!(subject = format!("{subject:?}"), "lock is not held"); + } + match semaphore.acquire_owned().await { Ok(_owned_permit) => { let mut subjects = self.subjects.lock().unwrap(); if let Some(state) = subjects.remove(subject) { state.semaphore.close(); } + tracing::debug!(subject = format!("{subject:?}"), "forgotten"); } Err(_) => { tracing::warn!( @@ -175,15 +201,12 @@ mod tests { limiter: &SubjectLimiter, subject: S, ) -> Option> { - if let Some(raii) = limiter.lock(subject) { - let task = tokio::spawn(async move { + limiter.lock(subject).ok().map(|raii| { + tokio::spawn(async move { let _raii_binding = raii; tokio::time::sleep(Duration::from_millis(10)).await; - }); - Some(task) - } else { - None - } + }) + }) } #[tokio::test] diff --git a/src/notifier.rs b/src/notifier.rs index 8d017cc..3c11e28 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -1,6 +1,6 @@ use crate::enums::SeriesEvent; use crate::error::{DicomStorageError, HandleLoopError}; -use crate::limiter::SubjectLimiter; +use crate::limiter::{LockError, SubjectLimiter}; use crate::lonk::{done_message, error_message, progress_message, subject_of}; use crate::types::{DicomInfo, SeriesKey, SeriesPath}; use bytes::Bytes; @@ -76,12 +76,7 @@ fn handle_event( match event { SeriesEvent::Instance(result) => { let payload = count_series(series_key.clone(), counts, result); - limiter.lock(series_key.clone()).map(|raii| { - tokio::spawn(async move { - let _raii_binding = raii; - maybe_send_lonk(nats_client, &series_key, payload).await - }) - }) + send_lonk_task(limiter, series_key, payload, nats_client) } SeriesEvent::Finish(series_info) => { let celery_client = Arc::clone(celery_client); @@ -100,6 +95,49 @@ fn handle_event( } } +/// Maybe runs [send_lonk] in a [tokio::spawn] task, with tracing. +fn send_lonk_task( + limiter: &SubjectLimiter, + series_key: SeriesKey, + payload: Bytes, + client: Option, +) -> Option { + if let Some(client) = client { + match limiter.lock(series_key.clone()) { + Ok(raii) => { + let task = tokio::spawn(async move { + let _raii_binding = raii; + send_lonk(client, &series_key, payload) + .await + .map_err(|error| { + tracing::error!( + SeriesInstanceUID = &series_key.SeriesInstanceUID, + pacs_name = series_key.pacs_name.as_str(), + "{:?}", + error + ) + }) + }); + Some(task) + } + Err(reason) => { + let message = match reason { + LockError::TooSoon => "a prior notification was sent recently", + LockError::Busy => "a prior notification is currently being sent", + }; + tracing::trace!( + SeriesInstanceUID = series_key.SeriesInstanceUID, + pacs_name = series_key.pacs_name.as_str(), + "Notification not sent because {message}.", + ); + None + } + } + } else { + None + } +} + /// If `result` is success: increment the count for the series. /// Returns a message which _oxidicom_ should send to NATS conveying the status of `result`. fn count_series( @@ -117,26 +155,20 @@ fn count_series( } } -async fn maybe_send_lonk( - client: Option, - series: &SeriesKey, - payload: Bytes, -) -> Result<(), ()> { - if let Some(client) = client { - send_lonk(client, series, payload).await.map_err(|e| { - tracing::error!(error = e.to_string()); - () - }) - } else { - Ok(()) - } -} - async fn send_lonk( client: async_nats::Client, series: &SeriesKey, payload: Bytes, ) -> Result<(), async_nats::PublishError> { + tracing::debug!( + SeriesInstanceUID = &series.SeriesInstanceUID, + pacs_name = series.pacs_name.as_str(), + payload = payload + .iter() + .map(|b| format!("{b:#04x}")) + .collect::>() + .join(" ") + ); client.publish(subject_of(series), payload).await } diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index 47fcb7b..dc38378 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -112,6 +112,22 @@ pub fn assert_lonk_messages(messages: Vec) { } fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom: u32) { + tracing::debug!( + "Received data from NATS:\n---\n{}\n---", + messages + .iter() + .map(|message| &message.payload) + .map(|payload| { + payload + .iter() + .map(|b| format!("{b:#04x}")) + .collect::>() + .join(" ") + }) + .collect::>() + .join("\n") + ); + assert!( messages.len() >= 3, "There must be at least 3 messages per series: (1) first progress message, \ @@ -132,23 +148,6 @@ fn assert_messages_for_series(messages: &[&async_nats::Message], expected_ndicom prev = num; } - let last_three_payloads = messages[messages.len() - 3..] - .iter() - .map(|message| &message.payload) - .map(|payload| { - payload - .iter() - .map(|b| format!("{b:#04x}")) - .collect::>() - .join(" ") - }) - .collect::>() - .join("\n"); - tracing::info!( - "Last 3 payloads for series:\n---\n{}\n---", - last_three_payloads - ); - let second_last = &messages[messages.len() - 2].payload; assert_eq!(second_last[0], oxidicom::lonk::MESSAGE_NDICOM); let last_ndicom = u32::from_le_bytes([ From 05cd80decbbf6223c7c94642df6906ab5747a062 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 11 Sep 2024 13:35:47 -0400 Subject: [PATCH 34/44] cargo update --- Cargo.lock | 633 +++++++++++++++++++++++++++++------------------------ 1 file changed, 352 insertions(+), 281 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8d1331..4de7960 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aes" version = "0.8.4" @@ -30,9 +36,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -54,7 +60,7 @@ checksum = "d1eb7c4fcde1858a6796c18a729b661346d38e05a207e2d9028bce822fc20283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -156,7 +162,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", "synstructure", ] @@ -168,7 +174,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -185,13 +191,13 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-lite 2.3.0", "slab", ] @@ -215,7 +221,7 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel", "async-executor", - "async-io 2.3.3", + "async-io 2.3.4", "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", @@ -255,9 +261,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ "async-lock 3.4.0", "cfg-if", @@ -265,11 +271,11 @@ dependencies = [ "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.2", - "rustix 0.38.34", + "polling 3.7.3", + "rustix 0.38.37", "slab", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -357,7 +363,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -368,13 +374,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -416,9 +422,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -469,17 +475,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.0", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -514,9 +520,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -561,15 +567,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -594,9 +600,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" dependencies = [ "serde", ] @@ -621,9 +627,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] [[package]] name = "celery" @@ -674,9 +683,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -720,7 +729,7 @@ version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -805,24 +814,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -848,9 +857,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -891,14 +900,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" @@ -935,7 +944,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -1151,7 +1160,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -1184,9 +1193,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding" @@ -1254,9 +1263,9 @@ checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1323,7 +1332,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -1340,9 +1349,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" @@ -1380,12 +1389,12 @@ checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1508,7 +1517,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-core", "futures-io", "parking", @@ -1523,7 +1532,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -1574,9 +1583,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -1587,9 +1596,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -1599,22 +1608,22 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -1622,7 +1631,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.2.5", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1631,9 +1640,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1647,9 +1656,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" @@ -1657,6 +1666,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1717,9 +1732,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -1727,12 +1742,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -1740,9 +1755,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1768,9 +1783,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -1835,9 +1850,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -1846,7 +1861,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", "tower", "tower-service", @@ -1888,12 +1903,12 @@ dependencies = [ [[package]] name = "image" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "exr", "image-webp", "num-traits", @@ -1906,12 +1921,12 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" dependencies = [ "byteorder-lite", - "thiserror", + "quick-error", ] [[package]] @@ -1926,12 +1941,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1978,17 +1993,17 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] @@ -2010,9 +2025,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jpeg-decoder" @@ -2031,9 +2046,9 @@ checksum = "2fefe5a4fb12fa836172dc53cc36c37af693f6197ae702f931faad8774caf926" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2062,9 +2077,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lebe" @@ -2074,9 +2089,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "linux-raw-sys" @@ -2086,15 +2101,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2102,9 +2117,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "match_cfg" @@ -2129,9 +2144,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -2147,14 +2162,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -2265,18 +2289,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" -version = "0.32.2" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -2298,11 +2322,11 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -2319,7 +2343,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -2330,9 +2354,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -2425,11 +2449,12 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" dependencies = [ - "supports-color", + "supports-color 2.1.0", + "supports-color 3.0.1", ] [[package]] @@ -2497,15 +2522,15 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2513,15 +2538,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2563,7 +2588,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -2598,14 +2623,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2632,7 +2657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand 2.1.1", "futures-io", ] @@ -2692,7 +2717,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -2713,17 +2738,17 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.2" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix 0.38.37", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2740,9 +2765,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -2756,9 +2784,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] @@ -2789,9 +2817,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2804,7 +2832,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", "version_check", "yansi 1.0.1", ] @@ -2829,14 +2857,20 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2873,9 +2907,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2935,11 +2969,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] @@ -2950,8 +2984,8 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -2965,13 +2999,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.4", ] [[package]] @@ -2982,15 +3016,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "relative-path" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" @@ -3076,21 +3110,21 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.52", + "syn 2.0.77", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -3120,22 +3154,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "once_cell", "ring", @@ -3173,9 +3207,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -3183,15 +3217,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -3200,21 +3234,21 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safe-transmute" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a01dab6acf992653be49205bdd549f32f17cb2803e8eacf1560bf97259aae8" +checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" [[package]] name = "salsa20" @@ -3236,11 +3270,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3268,11 +3302,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -3281,9 +3315,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -3291,9 +3325,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" @@ -3312,7 +3346,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -3321,7 +3355,7 @@ version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.5.0", "itoa", "memchr", "ryu", @@ -3345,7 +3379,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -3397,11 +3431,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3445,9 +3485,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snafu" @@ -3464,10 +3504,10 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -3482,9 +3522,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3517,9 +3557,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "supports-color" @@ -3531,6 +3571,15 @@ dependencies = [ "is_ci", ] +[[package]] +name = "supports-color" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77" +dependencies = [ + "is_ci", +] + [[package]] name = "syn" version = "1.0.109" @@ -3544,9 +3593,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -3576,7 +3625,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -3585,7 +3634,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] @@ -3619,9 +3668,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", - "fastrand 2.1.0", + "fastrand 2.1.1", "once_cell", - "rustix 0.38.34", + "rustix 0.38.37", "windows-sys 0.59.0", ] @@ -3640,7 +3689,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.38.34", + "rustix 0.38.37", "windows-sys 0.48.0", ] @@ -3667,7 +3716,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -3724,9 +3773,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -3750,7 +3799,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.52.0", ] @@ -3774,7 +3823,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -3825,9 +3874,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -3838,17 +3887,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.5.0", "toml_datetime", "winnow", ] @@ -3874,7 +3923,7 @@ dependencies = [ "percent-encoding", "pin-project", "prost", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", "tokio-stream", "tower", @@ -3905,15 +3954,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3934,7 +3983,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", ] [[package]] @@ -4027,9 +4076,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -4048,9 +4097,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -4059,9 +4108,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] @@ -4080,9 +4129,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "waker-fn" @@ -4117,34 +4166,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -4154,9 +4204,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4164,28 +4214,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -4225,11 +4275,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -4427,9 +4477,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -4474,11 +4524,32 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zune-core" @@ -4497,9 +4568,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" dependencies = [ "zune-core", ] From 92dab898574fb5903ffcdbe37c9886d90e8fff30 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 11 Sep 2024 14:25:14 -0400 Subject: [PATCH 35/44] Compute checksums for debugging --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fbf64f..9bf4dcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,11 +67,15 @@ jobs: target: ${{ matrix.target }} command: build args: --release --locked + - name: sha256sum + run: sha256sum target/${{ matrix.target }}/release oxidicom | tee oxidicom.sha256sum - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: 'build__${{ matrix.target }}' - path: 'target/${{ matrix.target }}/release/oxidicom' + path: | + target/${{ matrix.target }}/release/oxidicom + oxidicom.sha256sum if-no-files-found: 'error' build-docker: name: Build container image @@ -83,6 +87,10 @@ jobs: uses: actions/download-artifact@v4 - name: Print out all files run: find -type f + - name: Print expected checksums + run: find -type f -name '*.sha256sum' -exec cat '{}' \; + - name: Calculate actual checksums + run: find -type f -name 'oxidicom' -exec sha256sum '{}' \; - name: Move binaries and mark executable run: | mkdir -vp dist/linux/amd64 dist/linux/arm64 From bc602b6e1ae6d2791892eb071739e434645d779d Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 11 Sep 2024 14:32:09 -0400 Subject: [PATCH 36/44] Remove broken "Compile test binary" step --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bf4dcf..8a48974 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,6 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - - name: Compile test binary - run: | - source <(cargo llvm-cov show-env --export-prefix) - cargo test --no-run --locked - name: Run tests env: RUST_LOG: oxidicom=debug,integration_test=debug From e27eaf9618f84b1fee3b9886e99aa1c40998526d Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 11 Sep 2024 14:41:45 -0400 Subject: [PATCH 37/44] Fix ci.yml --- .github/workflows/ci.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a48974..fa7bb39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,28 +63,31 @@ jobs: target: ${{ matrix.target }} command: build args: --release --locked - - name: sha256sum - run: sha256sum target/${{ matrix.target }}/release oxidicom | tee oxidicom.sha256sum + - name: Move binary + run: | + mkdir dist + mv target/${{ matrix.target }}/release/oxidicom dist/oxidicom + - name: Calculate checksum + run: | + cd oxidicom + sha256 oxidicom | tee oxidicom.sha256sum - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: 'build__${{ matrix.target }}' - path: | - target/${{ matrix.target }}/release/oxidicom - oxidicom.sha256sum + path: dist if-no-files-found: 'error' build-docker: name: Build container image runs-on: ubuntu-24.04 needs: [ build-rust ] - # if: github.event_name == 'push' steps: - name: Download x86_64 binary uses: actions/download-artifact@v4 - name: Print out all files run: find -type f - name: Print expected checksums - run: find -type f -name '*.sha256sum' -exec cat '{}' \; + run: find -type f -name '*.sha256sum' -exec sh -c 'echo {} && cat {}' \; - name: Calculate actual checksums run: find -type f -name 'oxidicom' -exec sha256sum '{}' \; - name: Move binaries and mark executable From 51dd48776ec597d5b4dc7e904cfffe8cb2f73026 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 11 Sep 2024 14:47:00 -0400 Subject: [PATCH 38/44] Reuse send_lonk helper function --- src/notifier.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/notifier.rs b/src/notifier.rs index 3c11e28..3c277f9 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -107,7 +107,7 @@ fn send_lonk_task( Ok(raii) => { let task = tokio::spawn(async move { let _raii_binding = raii; - send_lonk(client, &series_key, payload) + send_lonk(&client, &series_key, payload) .await .map_err(|error| { tracing::error!( @@ -156,7 +156,7 @@ fn count_series( } async fn send_lonk( - client: async_nats::Client, + client: &async_nats::Client, series: &SeriesKey, payload: Bytes, ) -> Result<(), async_nats::PublishError> { @@ -178,7 +178,7 @@ async fn maybe_send_final_progress_messages( ndicom: u32, ) -> Result<(), ()> { if let Some(client) = client { - send_final_progress_messages(client, series, ndicom) + send_final_progress_messages(&client, series, ndicom) .await .map_err(|e| { tracing::error!(error = e.to_string()); @@ -190,15 +190,12 @@ async fn maybe_send_final_progress_messages( } async fn send_final_progress_messages( - client: async_nats::Client, + client: &async_nats::Client, series: &SeriesKey, ndicom: u32, ) -> Result<(), async_nats::PublishError> { - let subject = subject_of(series); - client - .publish(subject.clone(), progress_message(ndicom)) - .await?; - client.publish(subject, done_message()).await + send_lonk(client, series, progress_message(ndicom)).await?; + send_lonk(client, series, done_message()).await } async fn send_registration_task_to_celery( From 39b6aa07ceb8e81b814201f9e56bcf2bd1721c52 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 11 Sep 2024 14:56:45 -0400 Subject: [PATCH 39/44] Oopsies --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa7bb39..806bf20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: mv target/${{ matrix.target }}/release/oxidicom dist/oxidicom - name: Calculate checksum run: | - cd oxidicom + cd dist sha256 oxidicom | tee oxidicom.sha256sum - name: Upload artifacts uses: actions/upload-artifact@v4 From e65f5fec204c8bfd2ce8d359828a950215bcf828 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Mon, 23 Sep 2024 12:18:29 -0400 Subject: [PATCH 40/44] Add OXIDICOM_DEV_SLEEP --- src/notifier.rs | 4 ++++ src/run_everything.rs | 9 ++++++++- src/settings.rs | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/notifier.rs b/src/notifier.rs index 3c277f9..e247429 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -24,6 +24,7 @@ pub async fn cube_pacsfile_notifier( celery: Arc, nats_client: Option, progress_interval: Duration, + sleep: Option, ) -> Result<(), HandleLoopError> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let receiver_loop = async { @@ -40,6 +41,9 @@ pub async fn cube_pacsfile_notifier( ); if let Some(task) = task { tx.send(task).unwrap(); + if let Some(sleep_duration) = sleep { + tokio::time::sleep(sleep_duration).await; + } } } drop(tx); diff --git a/src/run_everything.rs b/src/run_everything.rs index 3be0cf9..6d1f4f0 100644 --- a/src/run_everything.rs +++ b/src/run_everything.rs @@ -24,6 +24,7 @@ pub async fn run_everything( listener_threads, listener_port, queue_name, + dev_sleep, }: OxidicomEnvOptions, finite_connections: Option, on_start: Option, @@ -62,7 +63,13 @@ where association_series_state_loop(rx_association, tx_storetasks, files_root) .map(|r| r.unwrap()), series_synchronizer(rx_storetasks, tx_register), - cube_pacsfile_notifier(rx_register, celery, nats_client, progress_interval) + cube_pacsfile_notifier( + rx_register, + celery, + nats_client, + progress_interval, + dev_sleep + ) )?; listener_handle.await? } diff --git a/src/settings.rs b/src/settings.rs index 431b6e2..c2f6cc1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -20,6 +20,8 @@ pub struct OxidicomEnvOptions { pub listener_threads: NonZeroUsize, #[serde(default = "default_listener_port")] pub listener_port: u16, + #[serde(with = "humantime_serde")] + pub dev_sleep: Option, } /// The name of the queue used by the `register_pacs_series` celery task in *CUBE*'s code. From c1fb19a295ef76de22f03763d28629cb6ee448f8 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 25 Sep 2024 15:07:30 -0400 Subject: [PATCH 41/44] Info when OXIDICOM_DEV_SLEEP is set --- src/notifier.rs | 1 + src/settings.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/notifier.rs b/src/notifier.rs index e247429..1202c4a 100644 --- a/src/notifier.rs +++ b/src/notifier.rs @@ -42,6 +42,7 @@ pub async fn cube_pacsfile_notifier( if let Some(task) = task { tx.send(task).unwrap(); if let Some(sleep_duration) = sleep { + tracing::info!("OXIDICOM_DEV_SLEEP is set, sleeping for {:?}. Please unset this option in production!", sleep_duration); tokio::time::sleep(sleep_duration).await; } } diff --git a/src/settings.rs b/src/settings.rs index c2f6cc1..c88bc73 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -20,7 +20,7 @@ pub struct OxidicomEnvOptions { pub listener_threads: NonZeroUsize, #[serde(default = "default_listener_port")] pub listener_port: u16, - #[serde(with = "humantime_serde")] + #[serde(with = "humantime_serde", default)] pub dev_sleep: Option, } From 54e9cd7c0345daa066caf0412dd0f7a533cb5f83 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Thu, 26 Sep 2024 00:54:34 -0400 Subject: [PATCH 42/44] Do not panic for "No tasks were received" bug --- src/series_synchronizer.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/series_synchronizer.rs b/src/series_synchronizer.rs index 8553d8f..4bc3afe 100644 --- a/src/series_synchronizer.rs +++ b/src/series_synchronizer.rs @@ -11,7 +11,7 @@ use tokio::task::JoinHandle; /// Waits on the [JoinHandle] of [PendingDicomInstance] for each `K`, so that /// [SeriesEvent::Finish] is the last message to be sent to `sender` for the respective `K`. pub(crate) async fn series_synchronizer< - K: Eq + Hash + Send + Clone + 'static, + K: Eq + Hash + Send + Clone + std::fmt::Debug + 'static, T: Send + 'static, L: Send + 'static, >( @@ -28,15 +28,19 @@ pub(crate) async fn series_synchronizer< enqueue_and_insert(series, task, &sender, &mut inflight_series) } SeriesEvent::Finish(final_message) => { - let tasks_for_series = inflight_series - .remove(&series) - .expect("No tasks were received for the series."); - let sender = Arc::clone(&sender); - let task = tokio::task::spawn(async move { - wait_on_all_then_flush(tasks_for_series, &sender, series, final_message) - .await - }); - tx.send(task).unwrap() + if let Some(tasks_for_series) = inflight_series.remove(&series) { + let sender = Arc::clone(&sender); + let task = tokio::task::spawn(async move { + wait_on_all_then_flush(tasks_for_series, &sender, series, final_message) + .await + }); + tx.send(task).unwrap() + } else { + tracing::error!( + series = format!("{series:?}"), + "No tasks were received for the series. This is a bug.", + ); + } } } } From da4665b3243db68a152a1f4353fa822adcc06017 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Thu, 26 Sep 2024 13:57:45 -0400 Subject: [PATCH 43/44] Change default_progress_interval=500ms --- src/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.rs b/src/settings.rs index c88bc73..ec66e35 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -40,7 +40,7 @@ fn default_listener_port() -> u16 { } fn default_progress_interval() -> std::time::Duration { - std::time::Duration::from_nanos(1) + std::time::Duration::from_millis(500) } fn default_max_pdu_length() -> usize { From 157c98ca7264b681dfdcc1fc090f7957a3019e9b Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Wed, 2 Oct 2024 19:01:57 -0400 Subject: [PATCH 44/44] Tolerate YYYY-MM-DD date string --- README.md | 1 + src/pacs_file.rs | 49 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d152796..4fa0557 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Start [RabbitMQ](https://hub.docker.com/_/rabbitmq) and [Orthanc](https://www.or services for testing, then download test data: ```shell +docker compose up -d docker compose run --rm get-data ``` diff --git a/src/pacs_file.rs b/src/pacs_file.rs index ba5bfed..105ffa4 100644 --- a/src/pacs_file.rs +++ b/src/pacs_file.rs @@ -1,13 +1,12 @@ use std::fmt::Display; -use crate::AETitle; -use dicom::dictionary_std::tags; -use dicom::object::{DefaultDicomObject, Tag}; - use crate::error::{name_of, DicomRequiredTagError, RequiredTagError}; use crate::patient_age::parse_age; use crate::sanitize::sanitize_path; use crate::types::{DicomFilePath, DicomInfo}; +use crate::AETitle; +use dicom::dictionary_std::tags; +use dicom::object::{DefaultDicomObject, Tag}; /// A wrapper of [PacsFileRegistrationRequest] along with the [DefaultDicomObject] it was created from. pub struct PacsFileRegistration { @@ -38,14 +37,12 @@ fn get_series_tags( let SeriesInstanceUID = ttr(dcm, tags::SERIES_INSTANCE_UID)?; let SOPInstanceUID = ttr(dcm, tags::SOP_INSTANCE_UID)?; let PatientID = ttr(dcm, tags::PATIENT_ID)?; - let StudyDate_string = ttr(dcm, tags::STUDY_DATE)?; // required by CUBE - let StudyDate_format = time::macros::format_description!("[year][month][day]"); // DICOM DA format - let StudyDate = time::Date::parse(&StudyDate_string, &StudyDate_format).map_err(|_| { - RequiredTagError::Bad(BadTag { - tag: tags::STUDY_DATE, - value: Some(StudyDate_string.to_string()), - }) - })?; + let StudyDate_string = ttr(dcm, tags::STUDY_DATE)?; + let StudyDate = parse_study_date( + StudyDate_string.as_str(), + &pacs_name, + &SeriesInstanceUID, + )?; // optional values let PatientName = tts(dcm, tags::PATIENT_NAME); @@ -126,6 +123,34 @@ impl Display for BadTag { } } +fn parse_study_date( + s: &str, + pacs_name: &AETitle, + series_instance_uid: &str, +) -> Result { + let da_format = time::macros::format_description!("[year][month][day]"); + time::Date::parse(s, &da_format) + .or_else(|_| { + let alt_format = time::macros::format_description!("[year]-[month]-[day]"); + let parsed = time::Date::parse(s, &alt_format); + if parsed.is_ok() { + tracing::warn!( + SeriesInstanceUID = series_instance_uid, + pacs_name = pacs_name.as_str(), + StudyDate = s, + "StudyDate is not a valid DICOM DA string, but was successfully parsed as YYYY-MM-DD" + ) + } + parsed + }) + .map_err(|_| { + RequiredTagError::Bad(BadTag { + tag: tags::STUDY_DATE, + value: Some(s.to_string()), + }) + }) +} + /// Required string tag fn ttr(dcm: &DefaultDicomObject, tag: Tag) -> Result { tts(dcm, tag).ok_or(RequiredTagError::Missing(tag))