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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-pq
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Postgres-backed job queue for Python
5
5
  Author: ricwo
6
6
  Author-email: ricwo <r@cogram.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-pq"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "Postgres-backed job queue for Python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -29,7 +29,13 @@ class PQ:
29
29
  Args:
30
30
  database_url: PostgreSQL connection string.
31
31
  """
32
- self._engine: Engine = create_engine(database_url)
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
- session = pq._session_factory()
320
- task = None
319
+ # Phase 1: Claim task
321
320
  try:
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
- session.commit()
345
-
346
- # Get task data for execution
347
- name = task.name
348
- payload = task.payload
349
- task_id = task.id
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
- elapsed = time.perf_counter() - start
378
- task = session.get(Task, task_id)
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
- elapsed = time.perf_counter() - start
388
- task = session.get(Task, task_id)
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
- elapsed = time.perf_counter() - start
398
- task = session.get(Task, task_id)
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
- elapsed = time.perf_counter() - start
408
- task = session.get(Task, task_id)
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
- finally:
417
- session.close()
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
- session = pq._session_factory()
453
- name = None
454
- payload = None
455
-
433
+ # Phase 1: Claim and advance schedule
456
434
  try:
457
- # Claim highest priority due periodic task with FOR UPDATE SKIP LOCKED
458
- stmt = select(Periodic).where(Periodic.next_run <= func.now())
459
- if priorities:
460
- stmt = stmt.where(Periodic.priority.in_([p.value for p in priorities]))
461
- stmt = (
462
- stmt.order_by(Periodic.priority.desc(), Periodic.next_run)
463
- .with_for_update(skip_locked=True)
464
- .limit(1)
465
- )
466
- periodic = session.execute(stmt).scalar_one_or_none()
467
-
468
- if periodic is None:
469
- return False
470
-
471
- # Get task data
472
- name = periodic.name
473
- payload = periodic.payload
474
-
475
- # Advance schedule BEFORE execution
476
- periodic.last_run = func.now()
477
- if periodic.cron:
478
- periodic.next_run = _calculate_next_run_cron(periodic.cron)
479
- else:
480
- periodic.next_run = func.now() + periodic.run_every
481
- session.commit()
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
- if name is not None:
492
- start = time.perf_counter()
493
- try:
494
- handler = resolve_function_path(name)
495
- args, kwargs = deserialize(payload)
496
- _execute_in_fork(handler, args, kwargs, max_runtime=max_runtime)
497
- elapsed = time.perf_counter() - start
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
- except TaskOOMError as e:
505
- elapsed = time.perf_counter() - start
506
- logger.error(f"Periodic task '{name}' OOM after {elapsed:.3f} s: {e}")
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
- except TaskKilledError as e:
509
- elapsed = time.perf_counter() - start
510
- logger.error(f"Periodic task '{name}' killed after {elapsed:.3f} s: {e}")
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
- except Exception as e:
513
- elapsed = time.perf_counter() - start
514
- logger.error(f"Periodic task '{name}' failed after {elapsed:.3f} s: {e}")
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
- return True
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 False
490
+ return True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes