1414Operations
1515----------
1616``create``
17- Initialise a fresh export job, or reset a terminal job back to
18- :attr:`ExportJobStatus.PENDING`. Refuses to overwrite an active
19- job (raises :class:`ExportJobInvalidTransitionError`).
17+ Initialise a fresh export job, or revive a terminal job, by
18+ persisting :attr:`ExportJobStatus.ACTIVE` and scheduling the
19+ driving orchestrator inline (with a deterministic instance ID
20+ derived from the job ID). Refuses to overwrite an active job
21+ (raises :class:`ExportJobInvalidTransitionError`). Mirrors the
22+ .NET ``ExportJob.Create`` flow, so a single signal is enough to
23+ launch a job.
2024``get``
2125 Returns the persisted state dict, or ``None`` if the entity has
2226 not been created (or has been deleted).
23- ``run``
24- Schedules the driving orchestrator (with a deterministic instance
25- ID derived from the job ID) and transitions the job to
26- :attr:`ExportJobStatus.ACTIVE`. Idempotent so the client may
27- safely signal it more than once.
2827``commit_checkpoint``
2928 Applies an incremental update after a single export page. When
3029 ``mark_failed_on_batch`` is true *and* ``failures`` is non-empty,
@@ -101,7 +100,6 @@ class ExportJobEntity(entities.DurableEntity):
101100
102101 OP_CREATE = "create"
103102 OP_GET = "get"
104- OP_RUN = "run"
105103 OP_COMMIT_CHECKPOINT = "commit_checkpoint"
106104 OP_MARK_COMPLETED = "mark_completed"
107105 OP_MARK_FAILED = "mark_failed"
@@ -138,7 +136,7 @@ def create(self, payload: Mapping[str, Any]) -> dict[str, Any]:
138136 job_id = self ._job_id ()
139137 current = self ._current_status ()
140138 assert_valid_transition (
141- self .OP_CREATE , current , ExportJobStatus .PENDING , job_id = job_id ,
139+ self .OP_CREATE , current , ExportJobStatus .ACTIVE , job_id = job_id ,
142140 )
143141
144142 config_dict = payload .get ("config" )
@@ -165,69 +163,51 @@ def create(self, payload: Mapping[str, Any]) -> dict[str, Any]:
165163 # Matches the .NET ``ExportJob.Create`` revive semantics so a
166164 # re-created job starts from a clean slate.
167165 state = ExportJobState (
168- status = ExportJobStatus .PENDING ,
166+ status = ExportJobStatus .ACTIVE ,
169167 config = config ,
170168 created_at = created_at ,
171169 last_modified_at = created_at ,
172170 )
173- logger .info (
174- "Created export job %r in status %s" , job_id , state .status .value ,
175- extra = {"job_id" : job_id , "operation" : "create" },
176- )
171+
172+ # The entity itself schedules the driving orchestrator inline,
173+ # so a single ``create`` signal is enough to launch a job.
174+ # Mirrors the .NET ``ExportJob.Create`` -> ``StartExportOrchestration``
175+ # flow and avoids the client having to send a second ``run``
176+ # signal (and the failure modes that come with it).
177+ instance_id = orchestrator_instance_id_for (job_id )
178+ try :
179+ self .entity_context .schedule_new_orchestration (
180+ ORCHESTRATOR_NAME ,
181+ input = {"job_id" : job_id , "config" : state .config .to_dict ()},
182+ instance_id = instance_id ,
183+ )
184+ state .orchestrator_instance_id = instance_id
185+ logger .info (
186+ "Created export job %r and scheduled orchestrator %s with "
187+ "instance ID %s" ,
188+ job_id , ORCHESTRATOR_NAME , instance_id ,
189+ extra = {"job_id" : job_id , "operation" : "create" },
190+ )
191+ except Exception as ex : # noqa: BLE001
192+ # Mirror the .NET pattern: record the failure on persisted
193+ # state and return, rather than re-raising. Re-raising
194+ # inside an entity operation can cause some entity
195+ # backends to discard the in-flight state mutations,
196+ # leaving the job with no error recorded.
197+ state .status = ExportJobStatus .FAILED
198+ state .last_error = (
199+ f"Failed to schedule orchestrator: { type (ex ).__name__ } : { ex } "
200+ )
201+ logger .exception (
202+ "Failed to schedule orchestrator for export job %r" , job_id ,
203+ extra = {"job_id" : job_id , "operation" : "create" },
204+ )
177205 return self ._save (state )
178206
179207 def get (self , _ : Any = None ) -> dict [str , Any ] | None :
180208 state = self ._load ()
181209 return state .to_dict () if state is not None else None
182210
183- def run (self , _ : Any = None ) -> dict [str , Any ] | None :
184- state = self ._load ()
185- if state is None :
186- raise ValueError ("Cannot run uninitialized export job" )
187- job_id = self ._job_id ()
188- assert_valid_transition (
189- self .OP_RUN , state .status , ExportJobStatus .ACTIVE , job_id = job_id ,
190- )
191-
192- # The entity itself schedules the driving orchestrator. The
193- # client is therefore decoupled from the orchestrator's name
194- # and input shape.
195- if state .status is ExportJobStatus .PENDING :
196- instance_id = orchestrator_instance_id_for (job_id )
197- try :
198- self .entity_context .schedule_new_orchestration (
199- ORCHESTRATOR_NAME ,
200- input = {"job_id" : job_id , "config" : state .config .to_dict ()},
201- instance_id = instance_id ,
202- )
203- state .orchestrator_instance_id = instance_id
204- logger .info (
205- "Scheduled orchestrator %s for job %r with instance ID %s" ,
206- ORCHESTRATOR_NAME , job_id , instance_id ,
207- extra = {"job_id" : job_id , "operation" : "run" },
208- )
209- except Exception as ex : # noqa: BLE001
210- # Mirror the .NET ExportJob.StartExportOrchestration pattern:
211- # record the failure on persisted state and return, rather
212- # than re-raising. Re-raising inside an entity operation
213- # can cause some entity backends to discard the in-flight
214- # state mutations, leaving the job stuck in PENDING with no
215- # error recorded. Returning ensures FAILED + last_error
216- # actually persist.
217- state .status = ExportJobStatus .FAILED
218- state .last_error = (
219- f"Failed to schedule orchestrator: { type (ex ).__name__ } : { ex } "
220- )
221- logger .exception (
222- "Failed to schedule orchestrator for export job %r" , job_id ,
223- extra = {"job_id" : job_id , "operation" : "run" },
224- )
225- return self ._save (state )
226-
227- state .status = ExportJobStatus .ACTIVE
228- state .last_error = None
229- return self ._save (state )
230-
231211 def commit_checkpoint (self , payload : Mapping [str , Any ]) -> dict [str , Any ] | None :
232212 state = self ._load ()
233213 if state is None :
0 commit comments