python-pq 0.3.3__tar.gz → 0.3.4__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.3.3 → python_pq-0.3.4}/PKG-INFO +1 -1
- {python_pq-0.3.3 → python_pq-0.3.4}/pyproject.toml +1 -1
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/client.py +7 -1
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/worker.py +107 -135
- {python_pq-0.3.3 → python_pq-0.3.4}/README.md +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/__init__.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/config.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/logging.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/README +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/__init__.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/env.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/script.py.mako +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/versions/20260109T055839Z_476683af098d_initial_schema.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/versions/20260109T063747Z_2483bec70083_add_client_id.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/migrations/versions/__init__.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/models.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/priority.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/registry.py +0 -0
- {python_pq-0.3.3 → python_pq-0.3.4}/src/pq/serialization.py +0 -0
|
@@ -29,7 +29,13 @@ class PQ:
|
|
|
29
29
|
Args:
|
|
30
30
|
database_url: PostgreSQL connection string.
|
|
31
31
|
"""
|
|
32
|
-
self._engine: Engine = create_engine(
|
|
32
|
+
self._engine: Engine = create_engine(
|
|
33
|
+
database_url,
|
|
34
|
+
pool_size=5,
|
|
35
|
+
max_overflow=10,
|
|
36
|
+
pool_pre_ping=True,
|
|
37
|
+
pool_recycle=1800,
|
|
38
|
+
)
|
|
33
39
|
self._session_factory = sessionmaker(bind=self._engine)
|
|
34
40
|
|
|
35
41
|
@contextmanager
|
|
@@ -316,105 +316,86 @@ def _process_one_off_task(
|
|
|
316
316
|
Returns:
|
|
317
317
|
True if a task was processed.
|
|
318
318
|
"""
|
|
319
|
-
|
|
320
|
-
task = None
|
|
319
|
+
# Phase 1: Claim task
|
|
321
320
|
try:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
stmt
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
321
|
+
with pq.session() as session:
|
|
322
|
+
# Claim highest priority pending task with FOR UPDATE SKIP LOCKED
|
|
323
|
+
stmt = (
|
|
324
|
+
select(Task)
|
|
325
|
+
.where(Task.status == TaskStatus.PENDING)
|
|
326
|
+
.where(Task.run_at <= func.now())
|
|
327
|
+
)
|
|
328
|
+
if priorities:
|
|
329
|
+
stmt = stmt.where(Task.priority.in_([p.value for p in priorities]))
|
|
330
|
+
stmt = (
|
|
331
|
+
stmt.order_by(Task.priority.desc(), Task.run_at)
|
|
332
|
+
.with_for_update(skip_locked=True)
|
|
333
|
+
.limit(1)
|
|
334
|
+
)
|
|
335
|
+
task = session.execute(stmt).scalar_one_or_none()
|
|
336
|
+
|
|
337
|
+
if task is None:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
# Mark as running
|
|
341
|
+
task.status = TaskStatus.RUNNING
|
|
342
|
+
task.started_at = datetime.now(UTC)
|
|
343
|
+
task.attempts += 1
|
|
344
|
+
|
|
345
|
+
# Get task data for execution (before session closes)
|
|
346
|
+
name = task.name
|
|
347
|
+
payload = task.payload
|
|
348
|
+
task_id = task.id
|
|
350
349
|
|
|
351
350
|
except Exception as e:
|
|
352
|
-
session.rollback()
|
|
353
351
|
logger.error(f"Error claiming task: {e}")
|
|
354
352
|
return False
|
|
355
|
-
finally:
|
|
356
|
-
session.close()
|
|
357
353
|
|
|
358
|
-
# Execute handler in forked process
|
|
359
|
-
session = pq._session_factory()
|
|
354
|
+
# Phase 2: Execute handler in forked process
|
|
360
355
|
start = time.perf_counter()
|
|
356
|
+
status = TaskStatus.COMPLETED
|
|
357
|
+
error_msg: str | None = None
|
|
358
|
+
|
|
361
359
|
try:
|
|
362
360
|
handler = resolve_function_path(name)
|
|
363
361
|
args, kwargs = deserialize(payload)
|
|
364
362
|
_execute_in_fork(handler, args, kwargs, max_runtime=max_runtime)
|
|
365
|
-
elapsed = time.perf_counter() - start
|
|
366
|
-
|
|
367
|
-
# Mark as completed
|
|
368
|
-
task = session.get(Task, task_id)
|
|
369
|
-
if task:
|
|
370
|
-
task.status = TaskStatus.COMPLETED
|
|
371
|
-
task.completed_at = datetime.now(UTC)
|
|
372
|
-
session.commit()
|
|
373
|
-
|
|
374
|
-
logger.debug(f"Task '{name}' completed in {elapsed:.3f} s")
|
|
375
363
|
|
|
376
364
|
except TaskTimeoutError:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if task:
|
|
380
|
-
task.status = TaskStatus.FAILED
|
|
381
|
-
task.completed_at = datetime.now(UTC)
|
|
382
|
-
task.error = f"Timed out after {elapsed:.3f} s"
|
|
383
|
-
session.commit()
|
|
384
|
-
logger.error(f"Task '{name}' timed out after {elapsed:.3f} s")
|
|
365
|
+
status = TaskStatus.FAILED
|
|
366
|
+
error_msg = f"Timed out after {time.perf_counter() - start:.3f} s"
|
|
385
367
|
|
|
386
368
|
except TaskOOMError as e:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if task:
|
|
390
|
-
task.status = TaskStatus.FAILED
|
|
391
|
-
task.completed_at = datetime.now(UTC)
|
|
392
|
-
task.error = str(e)
|
|
393
|
-
session.commit()
|
|
394
|
-
logger.error(f"Task '{name}' OOM after {elapsed:.3f} s: {e}")
|
|
369
|
+
status = TaskStatus.FAILED
|
|
370
|
+
error_msg = str(e)
|
|
395
371
|
|
|
396
372
|
except TaskKilledError as e:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if task:
|
|
400
|
-
task.status = TaskStatus.FAILED
|
|
401
|
-
task.completed_at = datetime.now(UTC)
|
|
402
|
-
task.error = str(e)
|
|
403
|
-
session.commit()
|
|
404
|
-
logger.error(f"Task '{name}' killed after {elapsed:.3f} s: {e}")
|
|
373
|
+
status = TaskStatus.FAILED
|
|
374
|
+
error_msg = str(e)
|
|
405
375
|
|
|
406
376
|
except Exception as e:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if task:
|
|
410
|
-
task.status = TaskStatus.FAILED
|
|
411
|
-
task.completed_at = datetime.now(UTC)
|
|
412
|
-
task.error = str(e)
|
|
413
|
-
session.commit()
|
|
414
|
-
logger.error(f"Task '{name}' failed after {elapsed:.3f} s: {e}")
|
|
377
|
+
status = TaskStatus.FAILED
|
|
378
|
+
error_msg = str(e)
|
|
415
379
|
|
|
416
|
-
|
|
417
|
-
|
|
380
|
+
elapsed = time.perf_counter() - start
|
|
381
|
+
|
|
382
|
+
# Phase 3: Update task status
|
|
383
|
+
try:
|
|
384
|
+
with pq.session() as session:
|
|
385
|
+
task = session.get(Task, task_id)
|
|
386
|
+
if task:
|
|
387
|
+
task.status = status
|
|
388
|
+
task.completed_at = datetime.now(UTC)
|
|
389
|
+
if error_msg:
|
|
390
|
+
task.error = error_msg
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Error updating task status: {e}")
|
|
393
|
+
|
|
394
|
+
# Log result
|
|
395
|
+
if status == TaskStatus.COMPLETED:
|
|
396
|
+
logger.debug(f"Task '{name}' completed in {elapsed:.3f} s")
|
|
397
|
+
else:
|
|
398
|
+
logger.error(f"Task '{name}' failed after {elapsed:.3f} s: {error_msg}")
|
|
418
399
|
|
|
419
400
|
return True
|
|
420
401
|
|
|
@@ -449,70 +430,61 @@ def _process_periodic_task(
|
|
|
449
430
|
Returns:
|
|
450
431
|
True if a task was processed.
|
|
451
432
|
"""
|
|
452
|
-
|
|
453
|
-
name = None
|
|
454
|
-
payload = None
|
|
455
|
-
|
|
433
|
+
# Phase 1: Claim and advance schedule
|
|
456
434
|
try:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
stmt
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
435
|
+
with pq.session() as session:
|
|
436
|
+
# Claim highest priority due periodic task with FOR UPDATE SKIP LOCKED
|
|
437
|
+
stmt = select(Periodic).where(Periodic.next_run <= func.now())
|
|
438
|
+
if priorities:
|
|
439
|
+
stmt = stmt.where(Periodic.priority.in_([p.value for p in priorities]))
|
|
440
|
+
stmt = (
|
|
441
|
+
stmt.order_by(Periodic.priority.desc(), Periodic.next_run)
|
|
442
|
+
.with_for_update(skip_locked=True)
|
|
443
|
+
.limit(1)
|
|
444
|
+
)
|
|
445
|
+
periodic = session.execute(stmt).scalar_one_or_none()
|
|
446
|
+
|
|
447
|
+
if periodic is None:
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
# Get task data
|
|
451
|
+
name = periodic.name
|
|
452
|
+
payload = periodic.payload
|
|
453
|
+
|
|
454
|
+
# Advance schedule BEFORE execution
|
|
455
|
+
periodic.last_run = func.now()
|
|
456
|
+
if periodic.cron:
|
|
457
|
+
periodic.next_run = _calculate_next_run_cron(periodic.cron)
|
|
458
|
+
else:
|
|
459
|
+
periodic.next_run = func.now() + periodic.run_every
|
|
482
460
|
|
|
483
461
|
except Exception as e:
|
|
484
|
-
session.rollback()
|
|
485
462
|
logger.error(f"Error claiming periodic task: {e}")
|
|
486
463
|
return False
|
|
487
|
-
finally:
|
|
488
|
-
session.close()
|
|
489
464
|
|
|
490
|
-
# Execute handler in forked process
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
logger.debug(f"Periodic task '{name}' completed in {elapsed:.3f} s")
|
|
499
|
-
|
|
500
|
-
except TaskTimeoutError:
|
|
501
|
-
elapsed = time.perf_counter() - start
|
|
502
|
-
logger.error(f"Periodic task '{name}' timed out after {elapsed:.3f} s")
|
|
465
|
+
# Phase 2: Execute handler in forked process
|
|
466
|
+
start = time.perf_counter()
|
|
467
|
+
try:
|
|
468
|
+
handler = resolve_function_path(name)
|
|
469
|
+
args, kwargs = deserialize(payload)
|
|
470
|
+
_execute_in_fork(handler, args, kwargs, max_runtime=max_runtime)
|
|
471
|
+
elapsed = time.perf_counter() - start
|
|
472
|
+
logger.debug(f"Periodic task '{name}' completed in {elapsed:.3f} s")
|
|
503
473
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
474
|
+
except TaskTimeoutError:
|
|
475
|
+
elapsed = time.perf_counter() - start
|
|
476
|
+
logger.error(f"Periodic task '{name}' timed out after {elapsed:.3f} s")
|
|
507
477
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
478
|
+
except TaskOOMError as e:
|
|
479
|
+
elapsed = time.perf_counter() - start
|
|
480
|
+
logger.error(f"Periodic task '{name}' OOM after {elapsed:.3f} s: {e}")
|
|
511
481
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
482
|
+
except TaskKilledError as e:
|
|
483
|
+
elapsed = time.perf_counter() - start
|
|
484
|
+
logger.error(f"Periodic task '{name}' killed after {elapsed:.3f} s: {e}")
|
|
515
485
|
|
|
516
|
-
|
|
486
|
+
except Exception as e:
|
|
487
|
+
elapsed = time.perf_counter() - start
|
|
488
|
+
logger.error(f"Periodic task '{name}' failed after {elapsed:.3f} s: {e}")
|
|
517
489
|
|
|
518
|
-
return
|
|
490
|
+
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|