Skip to content

feat(grzctl,grz-common)!: add grzctl process + streaming backend#513

Draft
tedil wants to merge 365 commits into
mainfrom
feat/grzctl-process
Draft

feat(grzctl,grz-common)!: add grzctl process + streaming backend#513
tedil wants to merge 365 commits into
mainfrom
feat/grzctl-process

Conversation

@tedil

@tedil tedil commented Feb 9, 2026

Copy link
Copy Markdown
Collaborator

Resolves #512

TODO

  • make use of IOBase for cleaner streaming interface
  • add some kind of TeeStream +- bounded queue +- threading
  • use should_qc to determine whether we need to branch after decryption to write to local storage too
  • use should_qc from recent updates to main instead
  • (optionally) cleanup inbox upon success
  • Command-hook for GRZ-QC-Workflow
  • introduce boolean selected_for_qc DB column → should be done separately
  • adapt grz-check to work with streams and have pyO3 interface
  • add more tests
  • code review

Tasks

  • Replace this entire PR description with the desired final squash commit message.
    • Make sure to follow the release-please format by placing these at the end, separated by newlines.

Multiple Conventional Commit statements can be included in the description, in addition to the PR title.

@tedil tedil requested a review from Hoeze February 9, 2026 16:45
@Hoeze

Hoeze commented Feb 9, 2026

Copy link
Copy Markdown
Collaborator

@copilot please review

Copilot AI commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

@Hoeze I've opened a new pull request, #514, to work on those changes. Once the pull request is ready, I'll request review from you.

@Hoeze Hoeze requested a review from Copilot February 9, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 process command, config model, example config, and CLI wiring.
  • Adds grz_common.pipeline streaming components (S3 source/sink, Crypt4GH stages, validators, operations utilities) and a StreamingPipelineWorker.
  • 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 .c4gh suffix. The streaming pipeline will fail earlier during Crypt4GH header parsing/decryption (so this test is likely asserting the wrong failure mode). Encrypt gzipped_content with 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-stubs is typically a typing/dev-only dependency (large optional dependency surface), and isal is a platform-dependent C extension that can break installs on unsupported environments. Consider moving boto3-stubs to a dev dependency group and keeping isal behind an optional extra (you already introduced grz-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.

Comment thread packages/grz-common/src/grz_common/pipeline/operations.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/operations.py Outdated
Comment thread packages/grz-pydantic-models/src/grz_pydantic_models/submission/metadata/v1.py Outdated
Comment thread packages/grz-pydantic-models/src/grz_pydantic_models/submission/metadata/v1.py Outdated
Comment thread packages/grz-common/src/grz_common/workers/streaming.py Outdated
Comment thread packages/grz-common/src/grz_common/workers/streaming.py Outdated
Comment thread packages/grz-common/src/grz_common/workers/streaming.py Outdated
Comment thread packages/grzctl/src/grzctl/commands/process.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/processor.py Outdated
self.threads = threads
self.max_concurrent_uploads = max_concurrent_uploads

self.pool_size = max(10, threads * (1 + max_concurrent_uploads) + 1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what is the logic behind that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

default pool size is 10, urllib will drop connections; not sure this is really needed (at least not if we stay below 10 ;) )

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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, ... ))

Comment thread packages/grz-common/src/grz_common/pipeline/processor.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
Comment thread packages/grz-common/src/grz_common/pipeline/components.py Outdated
@Hoeze Hoeze force-pushed the feat/grzctl-process branch 3 times, most recently from 9940111 to d98cec5 Compare February 13, 2026 14:22
@tedil tedil force-pushed the feat/grzctl-process branch 2 times, most recently from c085476 to 34fe475 Compare February 23, 2026 14:19
@tedil

tedil commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

TODO: replace from nacl.public import PrivateKey similar to #572

@beoinformatics beoinformatics left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

just flagging to ensure that the decision is consciously made that in case of errors we keep files uploaded so far on the archive.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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...")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

log.warning()?

beoinformatics and others added 2 commits June 23, 2026 13:00
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming processing of submissions

6 participants