|
| 1 | +""" |
| 2 | +Simple, quick & dirty tracer module to track task status for entities in a |
| 3 | +shared redis instance. This is useful to share task status across services for |
| 4 | +quick lookup (psql would be too expensive), e.g. to show a loading spinner in |
| 5 | +the UI if something is processing. This module currently is a quick shot to |
| 6 | +solve the status lookup for specific queues, it should be refactored at one |
| 7 | +point into a more general tracer module that e.g. can also store processing |
| 8 | +exceptions in postgres. For this, creating a better class-based queue system and |
| 9 | +merge the tracing into it should be considered. |
| 10 | +
|
| 11 | +The tracer is used in the @task decorator (middleware) when tracer_uri=... is set |
| 12 | +in the kwargs. e.g.: |
| 13 | +
|
| 14 | +```python |
| 15 | +@task(queue="my-queue", trace_uri="redis://localhost") |
| 16 | +def process(job) -> None: |
| 17 | + pass |
| 18 | +``` |
| 19 | +
|
| 20 | +(See test suite for example) |
| 21 | +
|
| 22 | +The tracer backend accepts any uri (sql, file-like, ...), but it is preferable |
| 23 | +to use redis for performance reasons. |
| 24 | +""" |
| 25 | + |
| 26 | +from functools import cache |
| 27 | + |
| 28 | +from anystore.store import get_store |
| 29 | +from anystore.types import Uri |
| 30 | +from anystore.util import join_relpaths |
| 31 | + |
| 32 | +from openaleph_procrastinate.model import Status |
| 33 | +from openaleph_procrastinate.settings import OpenAlephSettings |
| 34 | + |
| 35 | + |
| 36 | +class Tracer: |
| 37 | + def __init__(self, queue: str, task: str, uri: Uri | None = None) -> None: |
| 38 | + if uri is None: |
| 39 | + settings = OpenAlephSettings() |
| 40 | + uri = settings.redis_url |
| 41 | + self._store = get_store(uri or "memory://") |
| 42 | + self.queue = queue |
| 43 | + self.task = task |
| 44 | + |
| 45 | + def _make_key(self, entity_id: str) -> str: |
| 46 | + return join_relpaths( |
| 47 | + "openaleph-procrastinate", "tracer", self.queue, self.task, entity_id |
| 48 | + ) |
| 49 | + |
| 50 | + def mark(self, entity_id: str, status: Status) -> None: |
| 51 | + """Mark an entity status for the given queue and task. If status is |
| 52 | + 'succeeded' remove the data from the tracer.""" |
| 53 | + key = self._make_key(entity_id) |
| 54 | + if status == "succeeded": |
| 55 | + return self._store.delete(key, ignore_errors=True) |
| 56 | + self._store.put(key, status) |
| 57 | + |
| 58 | + def add(self, entity_id: str) -> None: |
| 59 | + """Mark as todo""" |
| 60 | + self.mark(entity_id, "todo") |
| 61 | + |
| 62 | + def start(self, entity_id: str) -> None: |
| 63 | + """Mark as doing""" |
| 64 | + self.mark(entity_id, "doing") |
| 65 | + |
| 66 | + def finish(self, entity_id: str) -> None: |
| 67 | + """Mark done which is actually popping (deleting) from the tracer""" |
| 68 | + self.mark(entity_id, "succeeded") |
| 69 | + |
| 70 | + def is_processing(self, entity_id: str) -> bool: |
| 71 | + """Check if a task for the entity_id is either pending or doing""" |
| 72 | + key = self._make_key(entity_id) |
| 73 | + if not self._store.exists(key): |
| 74 | + return False |
| 75 | + status = self._store.get(key) |
| 76 | + return status in ("todo", "doing") |
| 77 | + |
| 78 | + |
| 79 | +@cache |
| 80 | +def get_tracer(queue: str, task: str, uri: Uri | None) -> Tracer: |
| 81 | + return Tracer(queue, task, uri) |
0 commit comments