-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
services: Add JobState service and store
- Loading branch information
Showing
13 changed files
with
413 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from saturn_engine.client.worker_manager import WorkerManagerClient | ||
|
||
from . import BaseServices | ||
from . import Service | ||
from .http_client import HttpClient | ||
|
||
|
||
class Services(BaseServices): | ||
http_client: HttpClient | ||
|
||
|
||
class ApiClient(Service[Services, None]): | ||
name = "api_client" | ||
|
||
Services = Services | ||
|
||
client: WorkerManagerClient | ||
|
||
async def open(self) -> None: | ||
self.client = WorkerManagerClient( | ||
http_client=self.services.http_client.session, | ||
base_url=self.services.config.c.worker_manager_url, | ||
worker_id=self.services.config.c.worker_id, | ||
) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import dataclasses | ||
|
||
from saturn_engine.core import Cursor | ||
from saturn_engine.core import JobId | ||
from saturn_engine.core.api import JobsStatesSyncInput | ||
from saturn_engine.utils.asyncutils import DelayedThrottle | ||
|
||
from .. import BaseServices | ||
from .. import Service | ||
from ..api_client import ApiClient | ||
from .store import JobsStates | ||
from .store import JobsStatesSyncStore | ||
|
||
|
||
class Services(BaseServices): | ||
api_client: ApiClient | ||
|
||
|
||
@dataclasses.dataclass | ||
class Options: | ||
flush_delay: float = 10.0 | ||
auto_flush: bool = True | ||
|
||
|
||
class JobStateService(Service[Services, Options]): | ||
name = "job_state" | ||
|
||
Services = Services | ||
Options = Options | ||
|
||
_store: JobsStatesSyncStore | ||
_delayed_flush: DelayedThrottle | ||
|
||
async def open(self) -> None: | ||
self._store = JobsStatesSyncStore() | ||
self._delayed_flush = DelayedThrottle( | ||
self.flush, delay=self.options.flush_delay | ||
) | ||
|
||
def set_job_cursor(self, job_name: JobId, *, cursor: Cursor) -> None: | ||
self._store.set_job_cursor(job_name, cursor) | ||
self._maybe_flush() | ||
|
||
def set_job_completed(self, job_name: JobId) -> None: | ||
self._store.set_job_completed(job_name) | ||
self._maybe_flush() | ||
|
||
def set_job_failed(self, job_name: JobId, *, error: Exception) -> None: | ||
self._store.set_job_failed(job_name, f"{type(error).__name__}: {error}") | ||
self._maybe_flush() | ||
|
||
def set_job_cursor_state( | ||
self, | ||
job_name: JobId, | ||
*, | ||
cursor: Cursor, | ||
cursor_state: dict, | ||
) -> None: | ||
self._store.set_job_cursor_state( | ||
job_name, cursor=cursor, cursor_state=cursor_state | ||
) | ||
self._maybe_flush() | ||
|
||
def _maybe_flush(self) -> None: | ||
if self.options.auto_flush: | ||
self._delayed_flush() | ||
|
||
async def flush(self) -> None: | ||
with self._store.flush() as state: | ||
if not state.is_empty: | ||
await self.flush_state(state) | ||
|
||
async def flush_state(self, state: JobsStates) -> None: | ||
# Have to cast the job states to dict since defaultdict break dataclasses. | ||
state = dataclasses.replace(state, jobs=dict(state.jobs)) | ||
await self.services.api_client.client.sync(JobsStatesSyncInput(state=state)) | ||
|
||
async def close(self) -> None: | ||
await self._delayed_flush.flush() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import typing as t | ||
|
||
import contextlib | ||
import dataclasses | ||
from collections import defaultdict | ||
|
||
from saturn_engine.core import Cursor | ||
from saturn_engine.core import JobId | ||
from saturn_engine.core import api | ||
from saturn_engine.utils import utcnow | ||
|
||
|
||
@dataclasses.dataclass | ||
class JobCompletion(api.JobCompletion): | ||
def merge(self, other: "JobCompletion") -> "JobCompletion": | ||
self.completed_at = other.completed_at | ||
self.error = other.error | ||
return self | ||
|
||
|
||
@dataclasses.dataclass | ||
class JobState(api.JobState): | ||
completion: t.Optional[JobCompletion] = None | ||
|
||
def merge(self, other: "JobState") -> "JobState": | ||
if other.cursor: | ||
self.cursor = other.cursor | ||
self.cursors_states.update(other.cursors_states) | ||
if other.completion: | ||
completion = other.completion | ||
if self.completion: | ||
completion = self.completion.merge(other.completion) | ||
self.completion = completion | ||
return self | ||
|
||
|
||
@dataclasses.dataclass | ||
class JobsStates(api.JobsStates): | ||
jobs: dict[JobId, JobState] = dataclasses.field( | ||
default_factory=lambda: defaultdict(JobState) | ||
) | ||
|
||
def merge(self, other: "JobsStates") -> "JobsStates": | ||
for job, state in other.jobs.items(): | ||
new_state = state | ||
if job in self.jobs: | ||
new_state = self.jobs[job].merge(state) | ||
self.jobs[job] = new_state | ||
return self | ||
|
||
@property | ||
def is_empty(self) -> bool: | ||
return not self.jobs | ||
|
||
|
||
class JobsStatesSyncStore: | ||
def __init__(self) -> None: | ||
self._current_state = JobsStates() | ||
self._flushing_state: t.Optional[JobsStates] = None | ||
|
||
def set_job_cursor(self, job_name: JobId, cursor: Cursor) -> None: | ||
self._current_state.jobs[job_name].cursor = cursor | ||
|
||
def set_job_completed(self, job_name: JobId) -> None: | ||
self._current_state.jobs[job_name].completion = JobCompletion( | ||
completed_at=utcnow(), | ||
) | ||
|
||
def set_job_failed(self, job_name: JobId, error: str) -> None: | ||
self._current_state.jobs[job_name].completion = JobCompletion( | ||
completed_at=utcnow(), | ||
error=error, | ||
) | ||
|
||
def set_job_cursor_state( | ||
self, | ||
job_name: JobId, | ||
*, | ||
cursor: Cursor, | ||
cursor_state: dict, | ||
) -> None: | ||
self._current_state.jobs[job_name].cursors_states[cursor] = cursor_state | ||
|
||
@contextlib.contextmanager | ||
def flush(self) -> t.Iterator[JobsStates]: | ||
"""Allow to retrieve the jobs state in a safe-way for flushing. | ||
The yielded object won't be updated while inside the context. | ||
If an error happen inside the context, the state is restored and | ||
merged with any change that occured during the flush. | ||
""" | ||
self._flushing_state = self._current_state | ||
self._current_state = JobsStates() | ||
|
||
try: | ||
yield self._flushing_state | ||
except BaseException: | ||
self._current_state = self._flushing_state.merge(self._current_state) | ||
raise | ||
finally: | ||
self._flushing_state = None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.