Repository navigation
Conversation
Add the proposal models a packer plugin yields (ComputeParcel, DataParcel, BadInput, DelayInput), the PackerInput it receives, the diracx.transformations.packers entry-point group, and the TransformationNotFoundError and InputStatusConflictError exceptions. Refs DIRACGrid#1083 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the bulk operations the packer needs: reading a transformation and its compute facet, counting inputs by status, claiming Unassigned inputs with FOR UPDATE SKIP LOCKED, inserting compute parcels and assigning their inputs, marking inputs Problematic and delaying them. Counter journalling is left to DIRACGrid#1085. Refs DIRACGrid#1083 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pack_transformation claims a transformation's Unassigned inputs, streams them to the packer plugin named by the transformation, checks every proposal (offered, not reused) and applies it: compute parcels with their requirements merged over the template, Problematic inputs and delays. Data parcels and dependency injection into plugins are not supported yet. FixedSize is a simple packer making parcels of a fixed number of inputs, holding back the remainder until the feeder is off or on a flush. Refs DIRACGrid#1083 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on task PackerSweepTask periodically schedules a PackTransformationTask for every Active transformation; each one holds a mutex on its transformation and calls pack_transformation. The sweep is disabled by default and configured through DIRACX_TASKS_PACKER_*. Refs DIRACGrid#1083 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a developer page on the packer contract, what the core checks and how to register a packer through the diracx.transformations.packers entry point, and document the DIRACX_TASKS_PACKER_* settings. Refs DIRACGrid#1083 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The task worker commits an injected DB transaction even when the task raises, so a packer run that fails part-way keeps its earlier writes. Point to the issue where the packer task and pack_transformation claim a rollback. Refs DIRACGrid#1083, DIRACGrid#1101 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This is still in draft, there are a few things I'd like to check, including #1101 |
There was a problem hiding this comment.
🟡 Changes recommended
Schema mismatches break imports and requirements persistence, while accepted grouping settings can prevent packing progress indefinitely.
3 open issues
What changed in this PR
Adds plugin-based packing to the Transformation System, turning unassigned inputs into compute parcels through core-controlled database writes.
Changes:
- Defines packer contracts and a built-in fixed-size plugin.
- Adds packing logic, database operations and scheduled tasks.
- Adds tests and configuration documentation.
| File | Description |
|---|---|
| mkdocs.yml | Adds packer documentation navigation. |
| docs/dev/explanations/transformation-system/packer.md | Explains packing and plugin development. |
| docs/admin/reference/env-variables.md.j2 | Includes packer settings generation. |
| docs/admin/reference/env-variables.md | Documents packer environment variables. |
| diracx-tasks/tests/test_packer_tasks.py | Tests task registration and delegation. |
| diracx-tasks/src/diracx/tasks/transformations/packer.py | Adds sweep and per-transformation tasks. |
| diracx-tasks/src/diracx/tasks/transformations/__init__.py | Exports packing tasks. |
| diracx-tasks/pyproject.toml | Registers task entry points. |
| diracx-logic/tests/transformations/test_packing.py | Tests packing orchestration and persistence. |
| diracx-logic/tests/transformations/test_packers.py | Tests fixed-size grouping. |
| diracx-logic/src/diracx/logic/transformations/packing.py | Loads plugins and applies validated proposals. |
| diracx-logic/src/diracx/logic/transformations/packers.py | Implements fixed-size packing. |
| diracx-logic/src/diracx/logic/transformations/__init__.py | Exports packing interfaces. |
| diracx-logic/pyproject.toml | Registers the fixed-size plugin. |
| diracx-db/tests/transformation/test_transformation_db.py | Tests packing database operations. |
| diracx-db/src/diracx/db/sql/transformation/db.py | Adds input claiming and parcel persistence. |
| diracx-core/tests/test_transformation_models.py | Tests packer model validation. |
| diracx-core/src/diracx/core/models/transformation.py | Defines packer inputs and proposals. |
| diracx-core/src/diracx/core/models/__init__.py | Exports packer models. |
| diracx-core/src/diracx/core/extensions.py | Defines the packer entry-point group. |
| diracx-core/src/diracx/core/exceptions.py | Adds transformation database exceptions. |
Review effort: Balanced
| Inputs, | ||
| ParcelInputs, | ||
| Parcels, | ||
| Requirements, |
There was a problem hiding this comment.
Makes sense, a column rename happened after I developed the code.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
francescobrivio
left a comment
There was a problem hiding this comment.
Just reporting a few comments from a Claude Artifact review of this code that we (me and Marco) discussed yesterday and we believe should probably be discussed with the experts.
|
|
||
| @property | ||
| def execution_locks(self) -> list[BaseLock]: | ||
| return [MutexLock(TRANSFORMATION, self.transformation_id)] |
There was a problem hiding this comment.
Each transformation will have to be also "acquired" by the feeder task (still being developed) which will be started exactly at the same time of the packer (when the Transformation goes from NEW to ACTIVE) and executed as a PeriodicTask.
Will the MutexLock prevent this? If so, could we simply specify more the lock with:
| return [MutexLock(TRANSFORMATION, self.transformation_id)] | |
| return [MutexLock(TRANSFORMATION, self.transformation_id, "packer")] |
Would this be enough? Or do we need to use a different Lock?
There was a problem hiding this comment.
I am learning the ropes of the project, but why you say that Feeder and Packer would start exactly at the same time? I expected it to be asynchronous and based on the database state (so, potentially running right after the transaction is comited, but not necessarily).
There was a problem hiding this comment.
Here I'm talking about the "Feeder and Packer DiracX-Tasks" which periodically look at transformations and when a transformation goes from NEW to ACTIVE they "start acting on it", so they can potentially detect the state change of a transformation at the same time and they potentially want to lock it at the same time.
At least this is what I understood...I might be completely wrong on how the locking works 😄
There was a problem hiding this comment.
I think having a packer/feeder specific lock makes sense. There is no reason why they can't run in parallel to each other. We then need a reader-writer lock to ensure the status is respected correctly. i.e.
- Feeder:
MutexLock(TRANSFORMATION, id, "feeder")plusSharedRWLock(TRANSFORMATION, id, "status") - Packer:
MutexLock(TRANSFORMATION, id, "packer")plusSharedRWLock(TRANSFORMATION, id, "status") - Anything that changes status (hooks, drain/finalize sweeps, actions):
ExclusiveRWLock(TRANSFORMATION, id, "status")
There was a problem hiding this comment.
So both locks applied at the same time right?
Is there an example somewhere in dirac/diracx how that should be coded technically?
| # Each input points at its own parcel, so this one is an executemany | ||
| await self.conn.execute( | ||
| update(Inputs) | ||
| .where( | ||
| Inputs.transformation_id == bindparam("b_transformation_id"), | ||
| Inputs.input_id == bindparam("b_input_id"), | ||
| ) | ||
| .values(last_parcel_id=bindparam("b_parcel_id")), | ||
| [ | ||
| { | ||
| "b_transformation_id": transformation_id, | ||
| "b_input_id": link["InputID"], | ||
| "b_parcel_id": link["ParcelID"], | ||
| } | ||
| for link in links | ||
| ], | ||
| ) |
There was a problem hiding this comment.
The Claude artifact we used to review this PR had a comment on this lines saying that this is an executemany (see also the comment line) asking if from a performance on aiomysql point of view it would be acceptable for 10k inputs?
(Not sure I have a smart comment about this, just reporting for the sake of clarity/review)
| if transformation["status"] != TransformationStatus.ACTIVE: | ||
| return PackingSummary(skipped=True) | ||
| if transformation["kind"] != TransformationKind.COMPUTE: | ||
| raise NotImplementedError("Packing data transformations is not supported yet") |
There was a problem hiding this comment.
the same Claude artifact points out here:
The status is read without locking the Transformations row, so a concurrent Pause does not stop a run that has already started.
To be discussed I guess?
| await transformation_db.delay_inputs( | ||
| transformation_id, proposal.input_ids, proposal.delay_until | ||
| ) |
There was a problem hiding this comment.
and again, from the same artifact:
Applied straight away rather than buffered like parcels; one statement per DelayInput.
|
|
||
|
|
||
| def test_packer_input_is_frozen(): | ||
| packer_input = PackerInput(input_id=uuid4(), lfn="/vo/file") |
The merge with main brought in the rename of Requirements to RequirementsTemplates, which TransformationDB and the packer tests still imported under the old name, so importing diracx.db.sql failed. Refs DIRACGrid#1083
|
My understanding is that this PR is supposed to give the abstractions for DiracX packer functionality. |
| inputs: list[UUID] = Field(min_length=1) | ||
| metadata: dict[str, Any] = {} | ||
| requirements: dict[str, Any] | None = None | ||
| parameters: dict[str, Any] = {} |
There was a problem hiding this comment.
If this is the CWL compute transformation (the step itself), shouldn't it be the serialized CWL document?
Or perhaps it should be the process ID (from the Process table - or something like that)?
| if inspect.isasyncgenfunction(plugin): | ||
| proposals = plugin(inputs=claim(), **kwargs) | ||
| else: | ||
| # A synchronous packer cannot await the claim, so it is offered every |
There was a problem hiding this comment.
I don't think we need to support a synchronous packer, it should just rejected in load_packer
The "simple packer" is already provided in: |
Having this only contain |
|
I didn't finish reviewing but:
10k claimed inputs seems inappropriately small from my past operation experience. We should think of it in terms of memory overhead as that's the main factor. |

Summary
Implements the Transformation System packer (#1083, part of #1081), following DX-ADR-006 §Packer. The packer is the core task that turns a transformation's
Unassignedinputs into parcels. It also includes one simple packer plugin, tests and developer docs.The rule from the ADR runs through the design: plugins propose, the core writes. A plugin receives a stream of claimed inputs and yields proposals; only the core touches the database.
What's included
The commits follow package boundaries and can be reviewed in order:
core: the plugin contract models in
diracx.core.models.transformation:PackerInput, plus the proposalsComputeParcel,DataParcel,BadInputandDelayInput. Also the newdiracx.transformations.packersentry-point group and two exceptions.db: bulk
TransformationDBmethods with no domain logic:Unassignedinputs withFOR UPDATE SKIP LOCKEDand keyset pagingProblematicor delayedWHERE Status = 'Unassigned'plus a rowcount check.logic:
pack_transformation, which:Also
FixedSize, a packer that makes parcels ofgroup_sizeinputs and holds back a smaller remainder until the feeder is off or a flush is requested.tasks:
PackerSweepTask, which runs periodically and schedules onePackTransformationTaskperActivetransformation. Each task holds a mutex on its transformation. The sweep is disabled by default (DIRACX_TASKS_PACKER_ENABLED).docs: a developer page on the contract and how to write and register a packer, plus the new settings in the env-variable reference.
Scope decisions
DataParcelis defined; the core raisesNotImplementedErroron it and on Data transformations.TODO(#1085). The input overview uses aGROUP BYuntil then.diracx.tasks.transformationgroup.Inputshas no reason column, so the reason for aBadInputgoes toTransformationLog.Testing
pixi run local-start: a seeded transformation with 10 inputs andgroup_size=3became 4 parcels.SKIP LOCKED.Open points
packer.py.Transformationsrow.LastParcelIDexecutemany for about 10k inputs.DelayInputis applied immediately rather than batched.group_sizelarger thanmax_inputs.This stays a draft until these points are settled.