Repository navigation
Feat(core): [1083] transformation system packer #1102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2f794cc
33bae8b
a02efaf
4bb4192
60ba5ed
d8d6ad0
0a6540d
2950ea3
b57e364
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| """Enumerations of the Transformation System (DX-ADR-004, DX-ADR-005). | ||
| """Enumerations and plugin contracts of the Transformation System. | ||
|
|
||
| The enumerations and state machines follow DX-ADR-004 and DX-ADR-005; the | ||
| packer contract at the end of the module follows DX-ADR-006. | ||
|
|
||
| The members of each status enum are written in state-machine order and new | ||
| states must only ever be appended: the database stores them as native | ||
|
|
@@ -17,6 +20,12 @@ | |
| from __future__ import annotations | ||
|
|
||
| from enum import StrEnum | ||
| from typing import Any | ||
| from uuid import UUID | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
| from .types import UTCDatetime | ||
|
|
||
|
|
||
| class WorkgraphStatus(StrEnum): | ||
|
|
@@ -290,3 +299,106 @@ class CounterEntity(StrEnum): | |
| ), | ||
| WorkgraphStatus.CANCELLED: frozenset({TransformationStatus.CANCELLED}), | ||
| } | ||
|
|
||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # Packer contract (DX-ADR-006) | ||
| # -------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| class PackerInput(BaseModel): | ||
| """An ``Unassigned`` input claimed by the core and offered to a packer. | ||
|
|
||
| Attributes: | ||
| input_id: Identifier of the input. | ||
| parent_input_id: Input this one was split from, if any. | ||
| lfn: Logical file name, if the input is a file. | ||
| lfn_size: Size of the whole file, whatever the descriptor selects. | ||
| descriptor: Everything else describing the input. | ||
| error_count: Failed parcels the input was part of since the last reset. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| input_id: UUID | ||
| parent_input_id: UUID | None = None | ||
| lfn: str | None = None | ||
| lfn_size: int | None = None | ||
| descriptor: dict[str, Any] = {} | ||
| error_count: int = 0 | ||
|
|
||
|
|
||
| class ComputeParcel(BaseModel): | ||
| """A packer's proposal for a parcel that runs as a job. | ||
|
|
||
| Attributes: | ||
| inputs: Inputs the parcel is made of. | ||
| metadata: Free-form annotations stored on the parcel. | ||
| requirements: Additions merged over the transformation's | ||
| requirements template. | ||
| parameters: The CWL input object passed to the runner. | ||
| """ | ||
|
|
||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is the CWL compute transformation (the step itself), shouldn't it be the serialized CWL document? |
||
|
|
||
|
|
||
| class StorageDelta(BaseModel): | ||
| """What a data parcel's request costs one storage element. | ||
|
|
||
| Attributes: | ||
| se: Storage element affected. | ||
| files: Files added there if positive, freed if negative. | ||
| bytes: Bytes added there if positive, freed if negative. | ||
| """ | ||
|
|
||
| se: str | ||
| files: int | ||
| bytes: int | ||
|
|
||
|
|
||
| class DataParcel(BaseModel): | ||
| """A packer's proposal for a parcel that runs as a request. | ||
|
|
||
| Attributes: | ||
| inputs: Inputs the parcel is made of. | ||
| metadata: Free-form annotations stored on the parcel. | ||
| request: DIRAC RMS request body (DX-ADR-003). | ||
| deltas: What the request costs each storage element. | ||
| """ | ||
|
|
||
| inputs: list[UUID] = Field(min_length=1) | ||
| metadata: dict[str, Any] = {} | ||
| request: dict[str, Any] | ||
| deltas: list[StorageDelta] = [] | ||
|
|
||
|
|
||
| class BadInput(BaseModel): | ||
| """A packer's verdict that an input cannot be used at all. | ||
|
|
||
| The core moves the input to ``Problematic`` with the reason. | ||
|
|
||
| Attributes: | ||
| input_id: The input rejected. | ||
| reason: Why the packer rejected it. | ||
| """ | ||
|
|
||
| input_id: UUID | ||
| reason: str = "Unknown reason" | ||
|
|
||
|
|
||
| class DelayInput(BaseModel): | ||
| """A packer's request to withhold inputs from packing for a while. | ||
|
|
||
| Attributes: | ||
| input_ids: The inputs to withhold. | ||
| delay_until: Time before which they are not offered again. | ||
| """ | ||
|
|
||
| input_ids: list[UUID] = Field(min_length=1) | ||
| delay_until: UTCDatetime | ||
|
|
||
|
|
||
| PackerProposal = ComputeParcel | DataParcel | BadInput | DelayInput | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from uuid import uuid4 | ||
|
|
||
| import pytest | ||
| from pydantic import ValidationError | ||
|
|
||
| from diracx.core.models.transformation import ( | ||
| INPUT_TRANSITIONS, | ||
|
|
@@ -9,8 +12,11 @@ | |
| WORKGRAPH_MEMBER_STATUSES, | ||
| WORKGRAPH_TRANSITIONS, | ||
| ActionResult, | ||
| ComputeParcel, | ||
| CounterEntity, | ||
| DelayInput, | ||
| InputStatus, | ||
| PackerInput, | ||
| ParcelStatus, | ||
| TransformationKind, | ||
| TransformationStatus, | ||
|
|
@@ -66,3 +72,17 @@ def test_enum_values_are_append_only(enum_type): | |
| ) | ||
| def test_state_tables_cover_every_state(transitions, enum_type): | ||
| assert set(transitions) == set(enum_type) | ||
|
|
||
|
|
||
| def test_packer_proposals_need_inputs(): | ||
| with pytest.raises(ValidationError): | ||
| ComputeParcel(inputs=[]) | ||
| with pytest.raises(ValidationError): | ||
| DelayInput(input_ids=[], delay_until="2026-01-01T00:00:00Z") | ||
|
|
||
|
|
||
| def test_packer_input_is_frozen(): | ||
| packer_input = PackerInput(input_id=uuid4(), lfn="/vo/file") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's important to use uuid v7 and not v4, see #486 |
||
| assert packer_input.descriptor == {} | ||
| with pytest.raises(ValidationError): | ||
| packer_input.lfn = "/vo/other" # type: ignore[misc] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please clarify why this class is needed?
My understanding is that:
The only case I could think of here is if a transformation is deleted while the packer is working on its Unassigned inputs. Is it a valid concern?