fix: address PR #836 round-2 review (connect/timeout/observability)

🟡 Document why ProcrastinateTaskProducer.connect() uses `await app.open_async()`
   (AwaitableContext: await opens a long-lived pool, closed by drain()) and add a
   connect()/drain() lifecycle unit test (InMemoryConnector) asserting the pool is
   opened by connect and closed by drain — previously untested.
🟡 get_procrastinate_conninfo: forward connect_timeout from DATABASE_URL or
   default 10s so an unreachable DB can't hang worker/API startup indefinitely;
   warn only on other dropped query params. + tests.
🟢 INGEST_DELETE_SUCCEEDED_JOBS (default true) makes the worker's succeeded-job
   deletion configurable for audit retention.
🟢 Worker startup logs via logger.info (structured/OTel) instead of click.echo.
🟢 INGEST_STALLED_JOB_SECONDS (default 300) makes the crash-reclaim threshold
   tunable for slow embedding backends; reclaim reads it per-run.

The broad `except` in _apply_ingest_queue_schema_open is kept deliberately:
procrastinate wraps psycopg errors, so narrowing to psycopg.errors.* would miss
the wrapped DDL-conflict and turn a benign concurrent-apply race into a failure;
the presence re-check re-raises genuine errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-03 15:21:50 +02:00
co-authored by Claude Opus 4.8
parent cfdef3c2c5
commit 820b98dac1
5 changed files with 99 additions and 19 deletions
+24
View File
@@ -116,6 +116,30 @@ class TestProcrastinateConninfo:
assert parsed["dbname"] == "mcp"
assert parsed.get("sslmode") == expected_sslmode
def test_conninfo_connect_timeout_defaults_to_10(self, monkeypatch):
from psycopg.conninfo import conninfo_to_dict
monkeypatch.setattr(
config_module,
"get_database_url",
lambda: "postgresql+asyncpg://mcp:s@db/mcp",
)
monkeypatch.setattr(config_module, "get_database_ssl", lambda: None)
parsed = conninfo_to_dict(config_module.get_procrastinate_conninfo())
assert parsed["connect_timeout"] == "10"
def test_conninfo_honors_url_connect_timeout(self, monkeypatch):
from psycopg.conninfo import conninfo_to_dict
monkeypatch.setattr(
config_module,
"get_database_url",
lambda: "postgresql+asyncpg://mcp:s@db/mcp?connect_timeout=3",
)
monkeypatch.setattr(config_module, "get_database_ssl", lambda: None)
parsed = conninfo_to_dict(config_module.get_procrastinate_conninfo())
assert parsed["connect_timeout"] == "3"
def test_conninfo_ssl_mapping(self, monkeypatch):
from psycopg.conninfo import conninfo_to_dict
@@ -67,6 +67,22 @@ class TestProcrastinateTaskProducer:
producer = pq.ProcrastinateTaskProducer(app)
assert producer.clone() is producer
async def test_connect_opens_pool_and_drain_closes(self, app, monkeypatch):
# connect() resolves the process-wide app; point it at our in-memory one.
monkeypatch.setattr(pq, "get_procrastinate_app", lambda: app)
producer = await pq.ProcrastinateTaskProducer.connect()
# `await app.open_async()` must actually open the connector (regression
# guard for the await-vs-`async with` form on the long-lived pool).
assert app.connector.states == ["open_async"]
# An open pool means send() works end-to-end.
await producer.send(_task())
assert len(app.connector.jobs) == 1
await producer.drain()
assert "closed_async" in app.connector.states
class TestProcessDocumentTask:
async def test_runs_pipeline_and_closes_client(self, monkeypatch):