python-pq 0.6.3__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_pq-0.6.3 → python_pq-0.7.1}/PKG-INFO +1 -1
- {python_pq-0.6.3 → python_pq-0.7.1}/pyproject.toml +1 -1
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/__init__.py +1 -1
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/client.py +50 -1
- python_pq-0.7.1/src/pq/worker.py +1181 -0
- python_pq-0.6.3/src/pq/worker.py +0 -655
- {python_pq-0.6.3 → python_pq-0.7.1}/LICENSE +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/README.md +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/config.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/logging.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/README +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/__init__.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/env.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/script.py.mako +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/versions/20260109T055839Z_476683af098d_initial_schema.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/versions/20260109T063747Z_2483bec70083_add_client_id.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/versions/20260205T120000Z_a1b2c3d4e5f6_add_max_concurrent.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/versions/20260205T180000Z_b7c8d9e0f1a2_add_periodic_key.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/versions/20260217T120000Z_c3d4e5f6a7b8_add_periodic_active.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/migrations/versions/__init__.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/models.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/priority.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/registry.py +0 -0
- {python_pq-0.6.3 → python_pq-0.7.1}/src/pq/serialization.py +0 -0
|
@@ -9,7 +9,8 @@ from typing import Any, Self
|
|
|
9
9
|
|
|
10
10
|
from croniter import croniter
|
|
11
11
|
from croniter.croniter import CroniterBadCronError
|
|
12
|
-
from
|
|
12
|
+
from loguru import logger
|
|
13
|
+
from sqlalchemy import create_engine, delete, func, select, update
|
|
13
14
|
from sqlalchemy.dialects.postgresql import insert
|
|
14
15
|
from sqlalchemy.engine import Engine
|
|
15
16
|
from sqlalchemy.orm import sessionmaker
|
|
@@ -507,9 +508,54 @@ class PQ:
|
|
|
507
508
|
result = session.execute(stmt)
|
|
508
509
|
return result.rowcount
|
|
509
510
|
|
|
511
|
+
def reap_stale_tasks(self, threshold: timedelta) -> int:
|
|
512
|
+
"""Mark stale RUNNING tasks as FAILED.
|
|
513
|
+
|
|
514
|
+
When a worker dies mid-execution (e.g. pod restart, OOM on the worker
|
|
515
|
+
process), in-flight tasks stay RUNNING forever because no parent
|
|
516
|
+
process remains to update their status. This method detects those
|
|
517
|
+
orphaned rows and transitions them to FAILED.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
threshold: A task is considered stale when
|
|
521
|
+
``started_at + threshold < now()``. Should be at least
|
|
522
|
+
2x ``max_runtime`` to avoid reaping legitimately running
|
|
523
|
+
tasks, e.g. ``timedelta(seconds=max_runtime * 2)``.
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Number of tasks reaped.
|
|
527
|
+
"""
|
|
528
|
+
now = datetime.now(UTC)
|
|
529
|
+
cutoff = now - threshold
|
|
530
|
+
with self.session() as session:
|
|
531
|
+
stmt = (
|
|
532
|
+
update(Task)
|
|
533
|
+
.where(
|
|
534
|
+
Task.status == TaskStatus.RUNNING,
|
|
535
|
+
Task.started_at < cutoff,
|
|
536
|
+
)
|
|
537
|
+
.values(
|
|
538
|
+
status=TaskStatus.FAILED,
|
|
539
|
+
completed_at=now,
|
|
540
|
+
error=(
|
|
541
|
+
f"Reaped: task still RUNNING after {threshold} "
|
|
542
|
+
f"(cutoff={cutoff.isoformat()}). Worker likely died."
|
|
543
|
+
),
|
|
544
|
+
)
|
|
545
|
+
.returning(Task.id, Task.name, Task.started_at)
|
|
546
|
+
)
|
|
547
|
+
reaped = list(session.execute(stmt).all())
|
|
548
|
+
for task_id, name, started_at in reaped:
|
|
549
|
+
logger.warning(
|
|
550
|
+
f"Reaped stale task '{name}' (id={task_id},"
|
|
551
|
+
f" started_at={started_at})"
|
|
552
|
+
)
|
|
553
|
+
return len(reaped)
|
|
554
|
+
|
|
510
555
|
def run_worker(
|
|
511
556
|
self,
|
|
512
557
|
*,
|
|
558
|
+
concurrency: int = 1,
|
|
513
559
|
poll_interval: float = 1.0,
|
|
514
560
|
max_runtime: float = 30 * 60,
|
|
515
561
|
priorities: Set[Priority] | None = None,
|
|
@@ -519,6 +565,8 @@ class PQ:
|
|
|
519
565
|
Each task executes in a forked child process for memory isolation.
|
|
520
566
|
|
|
521
567
|
Args:
|
|
568
|
+
concurrency: Maximum number of tasks to process simultaneously.
|
|
569
|
+
Default: 1 (sequential processing).
|
|
522
570
|
poll_interval: Seconds to sleep between polls when idle.
|
|
523
571
|
max_runtime: Maximum execution time per task in seconds. Default: 30 min.
|
|
524
572
|
priorities: If set, only process tasks with these priority levels.
|
|
@@ -528,6 +576,7 @@ class PQ:
|
|
|
528
576
|
|
|
529
577
|
run_worker(
|
|
530
578
|
self,
|
|
579
|
+
concurrency=concurrency,
|
|
531
580
|
poll_interval=poll_interval,
|
|
532
581
|
max_runtime=max_runtime,
|
|
533
582
|
priorities=priorities,
|