feat: replace NATS ingest with procrastinate Postgres queue (#183)

Re-architect document ingest from the shared NATS-glued document-processor to a
per-tenant, in-process model owned by nextcloud-mcp-server (Deck #183). The MCP
server now owns both sides of ingest:

- Producer (api role): the scanner defers one job per changed document into the
  app's Postgres via procrastinate (queueing_lock dedup; no execution lock, so a
  crashed worker can't deadlock a doc — Qdrant upserts are idempotent).
- Consumer (worker role): `nextcloud-mcp-server worker` drains the queue and runs
  the existing process_document pipeline; a periodic task reclaims jobs orphaned
  in `doing` by a crash.

INGEST_QUEUE selects the transport (auto: postgres when DATABASE_URL is Postgres,
else the in-process anyio queue for SQLite/dev). procrastinate manages its own
tables (applied on a fresh DB at startup and by `db upgrade`). The vector-sync
status surface reads job counts from Postgres in postgres mode. procrastinate +
psycopg3 ship in the [postgres] extra; the app's own engine still uses asyncpg
(driver unification is a follow-up handled in the rendered Helm chart).

NATS JetStream, the Postgres-queue stub, the bus status subscriber, and nats-py
are removed.

BREAKING CHANGE: the external-NATS-ingest env vars are removed
(INGEST_MODE, STATUS_BACKEND, INGEST_BUS_URL, INGEST_BUS_NUM_REPLICAS,
FACT_EVENT_EMITTER). Use INGEST_QUEUE (memory|postgres) and the `worker`
command instead. TENANT_ID is retained (no longer NATS-subject-charset-validated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-03 04:11:11 +02:00
co-authored by Claude Opus 4.8
parent b91af923d2
commit 21b7922bac
27 changed files with 1369 additions and 1120 deletions
+14 -11
View File
@@ -115,17 +115,20 @@ async def _get_processing_status(request: Request) -> dict[str, Any] | None:
return None
try:
# Get document receive stream from app state
document_receive_stream = getattr(
request.app.state, "document_receive_stream", None
# Outstanding-work view depends on the queue backend (Deck #183):
# memory → stream buffer depth; postgres → procrastinate job counts (the
# in-memory stream is absent in postgres mode, so don't early-return on it).
from nextcloud_mcp_server.vector.ingest_status import ( # noqa: PLC0415
get_ingest_pending,
)
if document_receive_stream is None:
logger.debug("document_receive_stream not available in app state")
return None
# Get pending count from stream statistics
stats = document_receive_stream.statistics()
pending_count = stats.current_buffer_used
pending = await get_ingest_pending(
task_producer=getattr(request.app.state, "task_producer", None),
document_receive_stream=getattr(
request.app.state, "document_receive_stream", None
),
ingest_queue=settings.ingest_queue,
)
# Get Qdrant client and query indexed count
indexed_count = 0
@@ -147,11 +150,11 @@ async def _get_processing_status(request: Request) -> dict[str, Any] | None:
# Continue with indexed_count = 0
# Determine status
status = "syncing" if pending_count > 0 else "idle"
status = "syncing" if pending.pending > 0 else "idle"
return {
"indexed_count": indexed_count,
"pending_count": pending_count,
"pending_count": pending.pending,
"status": status,
}