1+ """
2+ Internal utilities to handle job management tasks through threads.
3+ """
4+
15import concurrent .futures
26import logging
37from abc import ABC , abstractmethod
913_log = logging .getLogger (__name__ )
1014
1115
12- @dataclass
16+ @dataclass ( frozen = True )
1317class _TaskResult :
1418 """
1519 Container for the result of a task execution.
@@ -32,92 +36,86 @@ class _TaskResult:
3236 stats_update : Dict [str , int ] = field (default_factory = dict ) # Optional
3337
3438
39+ @dataclass (frozen = True )
3540class Task (ABC ):
3641 """
37- Abstract base class for asynchronous tasks.
42+ Abstract base class for a unit of work associated with a job (identified by a job id)
43+ and to be processed by :py:classs:`_JobManagerWorkerThreadPool`.
44+
45+ Because the work is intended to be executed in a thread/process pool,
46+ it is recommended to keep the state of the task object as simple/immutable as possible
47+ (e.g. just some string/number attributes) and avoid sharing complex objects and state.
3848
39- A task encapsulates a unit of work, typically executed asynchronously,
40- and returns a `_TaskResult` with job-related metadata and updates.
49+ The main API for subclasses to implement is the `execute`method
50+ which should return a :py:class:`_TaskResult` object.
51+ with job-related metadata and updates.
4152
42- Implementations must override the `execute` method to define the task logic.
53+ :param job_id:
54+ Identifier of the job to start on the backend.
4355 """
4456
57+ # TODO: strictly speaking, a job id does not unambiguously identify a job when multiple backends are in play.
58+ job_id : str
59+
4560 @abstractmethod
4661 def execute (self ) -> _TaskResult :
4762 """Execute the task and return a raw result"""
4863 pass
4964
5065
51- @dataclass
52- class _JobStartTask (Task ):
66+ @dataclass ( frozen = True )
67+ class ConnectedTask (Task ):
5368 """
54- Task for starting a backend job asynchronously .
69+ Base class for tasks that involve an (authenticated) connection to a backend .
5570
56- Connects to an OpenEO backend using the provided URL and optional token,
57- retrieves the specified job, and attempts to start it.
58-
59- Usage example:
60-
61- .. code-block:: python
62-
63- task = _JobStartTask(
64- job_id="1234",
65- root_url="https://openeo.test",
66- bearer_token="secret"
67- )
68- result = task.execute()
69-
70- :param job_id:
71- Identifier of the job to start on the backend.
71+ Backend is specified by a root URL,
72+ and (optional) authentication is done through an openEO-style bearer token.
7273
7374 :param root_url:
7475 The root URL of the OpenEO backend to connect to.
7576
7677 :param bearer_token:
7778 Optional Bearer token used for authentication.
7879
79- :raises ValueError:
80- If any of the input parameters are invalid (e.g., empty strings).
8180 """
8281
83- job_id : str
8482 root_url : str
8583 bearer_token : Optional [str ]
8684
87- def __post_init__ (self ) -> None :
88- # Validation remains unchanged
89- if not isinstance (self .root_url , str ) or not self .root_url .strip ():
90- raise ValueError (f"root_url must be a non-empty string, got { self .root_url !r} " )
91- if self .bearer_token is not None and (not isinstance (self .bearer_token , str ) or not self .bearer_token .strip ()):
92- raise ValueError (f"bearer_token must be a non-empty string or None, got { self .bearer_token !r} " )
93- if not isinstance (self .job_id , str ) or not self .job_id .strip ():
94- raise ValueError (f"job_id must be a non-empty string, got { self .job_id !r} " )
85+ def get_connection (self ) -> openeo .Connection :
86+ connection = openeo .connect (self .root_url )
87+ if self .bearer_token :
88+ connection .authenticate_bearer_token (self .bearer_token )
89+ return connection
90+
91+
92+ class _JobStartTask (ConnectedTask ):
93+ """
94+ Task for starting an openEO batch job (the `POST /jobs/<job_id>/result` request).
95+ """
9596
9697 def execute (self ) -> _TaskResult :
9798 """
98- Executes the job start process using the OpenEO connection.
99-
100- Authenticates if a bearer token is provided, retrieves the job by ID,
101- and attempts to start it.
99+ Start job identified by `job_id` on the backend.
102100
103101 :returns:
104102 A `_TaskResult` with status and statistics metadata, indicating
105103 success or failure of the job start.
106104 """
105+ # TODO: move main try-except block to base class?
107106 try :
108- conn = openeo .connect (self .root_url )
109- if self .bearer_token :
110- conn .authenticate_bearer_token (self .bearer_token )
111- job = conn .job (self .job_id )
107+ job = self .get_connection ().job (self .job_id )
108+ # TODO: only start when status is "queued"?
112109 job .start ()
113- _log .info (f"Job { self .job_id } started successfully" )
110+ _log .info (f"Job { self .job_id !r } started successfully" )
114111 return _TaskResult (
115112 job_id = self .job_id ,
116113 db_update = {"status" : "queued" },
117114 stats_update = {"job start" : 1 },
118115 )
119116 except Exception as e :
120- _log .error (f"Failed to start job { self .job_id } : { e } " )
117+ _log .error (f"Failed to start job { self .job_id !r} : { e !r} " )
118+ # TODO: more insights about the failure (e.g. the exception) are just logged, but lost from the result
121119 return _TaskResult (
122120 job_id = self .job_id , db_update = {"status" : "start_failed" }, stats_update = {"start_job error" : 1 }
123121 )
@@ -175,13 +173,13 @@ def process_futures(self) -> List[_TaskResult]:
175173 if future in done :
176174 try :
177175 result = future .result ()
178-
179176 except Exception as e :
180- _log .exception (f"Error processing task : { e } " )
177+ _log .exception (f"Failed to get result from future : { e } " )
181178 result = _TaskResult (
182- job_id = task .job_id , db_update = {"status" : "start_failed" }, stats_update = {"start_job error" : 1 }
179+ job_id = task .job_id ,
180+ db_update = {"status" : "future.result() failed" },
181+ stats_update = {"future.result() error" : 1 },
183182 )
184-
185183 results .append (result )
186184 else :
187185 to_keep .append ((future , task ))
0 commit comments