Hi Benjamin,
I did a couple tests and can confirm that setting the new Python.AllowThreads
configuration option to true
has an unlocking effect on the output.AnswerBuffer
calls, meaning that our HTTP threads (each running auth-related python code using orthanc.RegisterIncomingHttpRequestFilter
) no longer seem to be blocked even while output.AnswerBuffer
is executing.
The same seems to be true for orthanc.RestApiGet
, that doesn’t block either with Python.AllowThreads
set to true
.
For completeness’ sake, here is the piece of code we originally created for extending our DICOMDIR downloads with the microdicom viewer:
def OnMedia(output, uri, **request):
viewer_path = "/microdicom-viewer"
study_id = uri.split("/")[2]
study_data = io.BytesIO(orthanc.RestApiGet(uri))
with zipfile.ZipFile(study_data, "a") as study_zip:
for root, _, files in os.walk(viewer_path):
prefix = pathlib.Path(root)
for viewer_file in files:
name = str(prefix / viewer_file)
study_zip.write(name, arcname=name.replace(viewer_path, ""),
compress_type=zipfile.ZIP_DEFLATED)
study_zip.write("/microdicom-license/license.lic", "Win32/license.lic")
study_zip.write("/microdicom-license/license.lic", "x64/license.lic")
output.SetHttpHeader("Content-Disposition", f'attachment; filename="{study_id}.zip"')
output.AnswerBuffer(study_data.getvalue(), "application/zip")
if os.path.isfile("/microdicom-license/license.lic"):
orthanc.LogInfo("Micordicom license was found under /microdicom-license/license.lic, registering extension for /studies/(.*)/media...")
orthanc.RegisterRestCallback(f"/studies/(.*)/media", OnMedia)
There was also a more complex implementation that used multiple python processes but after a while we realized that output.AnswerBuffer
really must be running in the “master process” (as the docs state) so that was still a bottleneck.
We identified 4 major problems with our implementation:
- (solved)
orthanc.RestApiGet
blocks, but we can use the http module and call /media ourselves (need to add a new URL path instead of the original /media endpoint to avoid recursion)
- (solved)
output.AnswerBuffer
blocks and it must run in the “master process” according to the docs. This means that even with multiple python subprocesses, clients will block each other
while any one of the is running output.AnswerBuffer
- connection timeouts - clients don’t get a response for a very long time, there is no way to send content-length (or any) HTTP header upfront from python code
- does in-memory zipping - will quickly result in oom killer with just a few concurrent downloads (Gb size studies)
This new Python.AllowThreads
seems to be solving the first 2 of those problems. The warnings about thread-safety in the docs are a bit alarming thought, I have little knowledge of multi-threaded programs. Even if that isn’t an issue for us, the last 2 of our problems still remain. As a result we decided to implement the custom zipping in an outside service.
Nonetheless, thanks a lot for your input, this thread might serve others well in the future.