feat(grzctl,grz-common)!: add grzctl process + streaming backend#513
feat(grzctl,grz-common)!: add grzctl process + streaming backend#513tedil wants to merge 365 commits into
grzctl process + streaming backend#513Conversation
|
@copilot please review |
There was a problem hiding this comment.
Pull request overview
Adds a new grzctl process command backed by a modular streaming pipeline (S3 download → Crypt4GH decrypt → optional validation → Crypt4GH encrypt → S3 multipart upload), plus redaction and integration tests to compare results with the manual CLI workflow.
Changes:
- Introduces
grzctl processcommand, config model, example config, and CLI wiring. - Adds
grz_common.pipelinestreaming components (S3 source/sink, Crypt4GH stages, validators, operations utilities) and aStreamingPipelineWorker. - Adds integration/unit tests for streaming pipeline behavior and CLI process flow.
Reviewed changes
Copilot reviewed 32 out of 34 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_streaming_pipeline_comparison.py | Adds integration tests comparing manual CLI steps vs streaming decrypt behavior. |
| tests/cli/test_process.py | Adds integration tests for grzctl process with mocked S3 and DB, including failure cases. |
| pyproject.toml | Adds additional dependencies and updates tooling excludes/ignores. |
| packages/grzctl/src/grzctl/models/config.py | Adds ProcessConfig and S3 multi-inbox support models. |
| packages/grzctl/src/grzctl/commands/process.py | Implements the grzctl process command orchestration. |
| packages/grzctl/src/grzctl/cli.py | Registers the new process command. |
| packages/grzctl/examples/config.process.yaml | Provides an example configuration for streaming processing. |
| packages/grz-pydantic-models/src/grz_pydantic_models/submission/metadata/v1.py | Adds metadata redaction helpers for logs + archived metadata. |
| packages/grz-db/src/grz_db/models/submission.py | Extends submission state enum for the new processing flow. |
| packages/grz-db/src/grz_db/models/base.py | Removes a magic-method docstring ignore and enforces subclass requirements. |
| packages/grz-common/tests/pipeline/test_pipeline_components.py | Adds unit tests for decompression and validation pipeline components. |
| packages/grz-common/tests/pipeline/test_crypt4gh_components.py | Adds unit tests for streaming Crypt4GH encryption/decryption components. |
| packages/grz-common/src/grz_common/workers/upload.py | Switches uploads to use streaming UploadOperation instead of boto3 transfer helpers. |
| packages/grz-common/src/grz_common/workers/streaming.py | Adds the streaming worker that runs the end-to-end pipeline and uploads logs/metadata. |
| packages/grz-common/src/grz_common/workers/download.py | Switches downloads to use streaming DownloadOperation instead of boto3 transfer helpers. |
| packages/grz-common/src/grz_common/utils/redaction.py | Adds file redaction helper used before archiving logs. |
| packages/grz-common/src/grz_common/utils/crypt.py | Refactors file encrypt/decrypt helpers to use the streaming operations wrappers. |
| packages/grz-common/src/grz_common/progress/states.py | Adds ProcessingState for streaming pipeline progress logging. |
| packages/grz-common/src/grz_common/progress/init.py | Exposes ProcessingState from the progress package. |
| packages/grz-common/src/grz_common/pipeline/validators.py | Introduces streaming validators (checksum, FASTQ, BAM). |
| packages/grz-common/src/grz_common/pipeline/utils.py | Adds signal handling + helper utilities for staged pipeline cleanup. |
| packages/grz-common/src/grz_common/pipeline/s3_keys.py | Centralizes S3 key naming for inbox/archive structures. |
| packages/grz-common/src/grz_common/pipeline/s3.py | Adds streaming S3 downloader and multipart uploader stages. |
| packages/grz-common/src/grz_common/pipeline/processing.py | Implements the tee’d streaming pipeline orchestrator used by process. |
| packages/grz-common/src/grz_common/pipeline/operations.py | Adds file-based wrappers around streaming stages for CLI subcommands. |
| packages/grz-common/src/grz_common/pipeline/crypt4gh.py | Adds streaming Crypt4GH encrypt/decrypt transformers. |
| packages/grz-common/src/grz_common/pipeline/constants.py | Re-exports multipart constants for pipeline internals. |
| packages/grz-common/src/grz_common/pipeline/compressors.py | Adds gzip decompressor with optional ISA-L acceleration. |
| packages/grz-common/src/grz_common/pipeline/base.py | Adds pipeline stage interfaces and shared context. |
| packages/grz-common/src/grz_common/pipeline/init.py | Exports the modular pipeline public API. |
| packages/grz-common/src/grz_common/constants.py | Adds multipart/upload constants and tqdm-safe logging handler. |
| packages/grz-common/pyproject.toml | Adds optional fast extra for ISA-L gzip acceleration. |
Comments suppressed due to low confidence (2)
tests/cli/test_process.py:1
- The test claims to trigger FASTQ validation failure, but it uploads non-Crypt4GH content with a
.c4ghsuffix. The streaming pipeline will fail earlier during Crypt4GH header parsing/decryption (so this test is likely asserting the wrong failure mode). Encryptgzipped_contentwith Crypt4GH using the inbox/public key before uploading, or change the test expectations to assert decryption/header failure instead of validation failure.
pyproject.toml:1 boto3-stubsis typically a typing/dev-only dependency (large optional dependency surface), andisalis a platform-dependent C extension that can break installs on unsupported environments. Consider movingboto3-stubsto a dev dependency group and keepingisalbehind an optional extra (you already introducedgrz-common[fast]), rather than making it a required runtime dependency at the workspace root.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.threads = threads | ||
| self.max_concurrent_uploads = max_concurrent_uploads | ||
|
|
||
| self.pool_size = max(10, threads * (1 + max_concurrent_uploads) + 1) |
There was a problem hiding this comment.
what is the logic behind that?
There was a problem hiding this comment.
default pool size is 10, urllib will drop connections; not sure this is really needed (at least not if we stay below 10 ;) )
There was a problem hiding this comment.
A minor thing is to make increased use of readable constants. More importantly an upper bound is not clear - is that a concern? Otherwise it could be something like:
MIN_POOL_SIZE=10
MAX_POOL_SIZE=15
...
self.pool_size = min(MAX_POOL_SIZE, max(MIN_POOL_SIZE, ... ))
9940111 to
d98cec5
Compare
c085476 to
34fe475
Compare
|
TODO: replace |
beoinformatics
left a comment
There was a problem hiding this comment.
impressive work! I am only finding 2 issues, none of them critical:
- in case of an error during file copying, the cleanup may not be complete (no abort on server side, also no deletion of partial archive data but that may be by design)
- ETag is in my view doppelt-gemoppelt with MD5 comparison and since ETags depend on server side settings I my vote would be not using them at all
| self.close() | ||
| # Sinks like open files or DevNullSink need closing to ensure flush | ||
| other.close() | ||
| return other |
There was a problem hiding this comment.
One potential issue: If the copy errors (or the validators raise on self.close()), the upload is neither committed nor aborted — it may leak an incomplete multipart upload that sits on the server side (Ceph). Worth aborting on the failure path. This issue is verified in a unit test that will be part of another branch.
There was a problem hiding this comment.
Yes, good point, in general we should go through the code and ensure we properly catch errors/exceptions and handle such cases gracefully, including multipart abort etc.
| ) | ||
|
|
||
| server_etag = resp["ETag"].strip('"') | ||
| if server_etag != local_md5_hex: |
There was a problem hiding this comment.
do we need ETag logic at all? We already compare MD5 values, so if feels like 'belt and suspenders'. Plus server-side ETag values cannot be fully relied upon, they may or may not be set to the MD5 value. My vote would be to remove the ETag logic
There was a problem hiding this comment.
No, since this is also used during archival, ETags are vitally important.
| 5. Optionally clean the inbox. | ||
|
|
||
| :param submission_metadata: The parsed metadata object containing donor and file information. | ||
| :raises RuntimeError: If consistency checks fail or any file fails validation. |
There was a problem hiding this comment.
just flagging to ensure that the decision is consciously made that in case of errors we keep files uploaded so far on the archive.
There was a problem hiding this comment.
This is one of the points that are open for discussion, let's put that on the agenda for our next meeting
| log.info("Interrogation config keep_failed is True. Leaving failed files in interrogation bucket.") | ||
| return | ||
|
|
||
| log.info("Cleaning up interrogation bucket due to failure...") |
Problem: When the process pipeline fails partway through writing a file to the archive, that file's S3 multipart upload was previously left open - its already-staged parts lingered as orphaned upload fragments that never became a usable object. Fix: Now, on failure, that file's upload is aborted and its staged parts are discarded. Test: A new unit test reproduces the original bug and verifies the fix. --------- Co-authored-by: Eckart Bindewald <eckart.bindewald@uni-tuebingen.de> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves #512
TODO
TeeStream+- bounded queue +- threadinguseshould_qcto determine whether we need to branch after decryption to write to local storage tooshould_qcfrom recent updates to main insteadselected_for_qcDB column → should be done separatelyTasks
Multiple Conventional Commit statements can be included in the description, in addition to the PR title.