Files
chat_backend/llm_be/chat_backend/drive_tasks.py
T
westfarn 57a2350b8e
Deploy Beta / unit-tests (push) Successful in 11s
Unit Tests / test (push) Successful in 10s
Deploy Beta / docker (push) Successful in 21s
Deploy Beta / deploy-beta (push) Successful in 48s
Drive sync progress + documents list API + prompt heatmap (#59, #60, #94) (#61)
## Summary
- **#59** — Persist and expose Drive sync progress (`sync_total`, `sync_processed`, `sync_added`, `sync_updated`, `sync_failed`) during sync so the FE can show a progress bar.
- **#60** — Paginate/search/sort `GET /documents/` with `workspace=personal|company` scope isolation.
- **#94 (API)** — Add `GET /analytics/user_prompt_heatmap/?tz=` weekday × hour bins for user-entered prompts.

## Test plan
- [x] `manage.py test` drive sync / documents / analytics / drive_tasks suites
- [ ] Manual: trigger Drive sync and poll connections → progress fields update while pending
- [ ] Manual: `GET /documents/?workspace=personal&page=1&search=…&ordering=name`
- [ ] Deploy FE companion PR for #92/#93/#94 UI

Closes #59
Closes #60Reviewed-on: #61
2026-08-02 04:49:23 -07:00

110 lines
3.5 KiB
Python

"""Drive sync background tasks (#57).
Uses Django 6 Tasks API (same pattern as ``email_tasks``). With the default
``ImmediateBackend``, enqueue still runs in-process — we dispatch that call on
a daemon thread after commit so ``POST .../sync/`` can return 202 Pending
without waiting for Drive list/download/ingest. Swap ``TASKS`` to a durable
queue + worker later; enqueue path stays the same.
"""
from __future__ import annotations
import logging
import threading
from functools import partial
from django.conf import settings
from django.db import transaction
from django.tasks import task
from chat_backend.models import DriveConnection
from chat_backend.services.drive_sync import sync_connection
logger = logging.getLogger(__name__)
def _uses_immediate_backend() -> bool:
backend = (
(getattr(settings, "TASKS", {}) or {})
.get("default", {})
.get("BACKEND", "")
)
return "ImmediateBackend" in str(backend)
@task
def run_drive_connection_sync(connection_id: int) -> dict:
"""Load a DriveConnection and run ``sync_connection`` (#57)."""
try:
connection = DriveConnection.objects.get(pk=connection_id, is_active=True)
except DriveConnection.DoesNotExist:
logger.error(
"DriveConnection %s missing or inactive; cannot sync", connection_id
)
return {"error": "connection_not_found"}
return sync_connection(connection)
def _dispatch_sync(connection_id: int) -> None:
"""Enqueue (or run) the sync task; ImmediateBackend runs off the request thread."""
try:
if _uses_immediate_backend():
# ImmediateBackend executes during enqueue — keep HTTP snappy.
threading.Thread(
target=run_drive_connection_sync.enqueue,
kwargs={"connection_id": connection_id},
daemon=True,
name=f"drive-sync-{connection_id}",
).start()
else:
run_drive_connection_sync.enqueue(connection_id=connection_id)
except Exception:
logger.exception(
"Failed to dispatch Drive sync for connection=%s", connection_id
)
DriveConnection.objects.filter(pk=connection_id).update(
last_sync_status=DriveConnection.SyncStatus.ERROR,
last_sync_error="Failed to enqueue Drive sync task.",
)
def enqueue_drive_sync(
connection: DriveConnection, *, force: bool = False
) -> tuple[DriveConnection, bool]:
"""Mark connection pending and enqueue sync after DB commit.
Returns ``(connection, enqueued)``. If already ``pending`` and ``force`` is
false, does not enqueue a duplicate job.
"""
connection.refresh_from_db()
if (
not force
and connection.last_sync_status == DriveConnection.SyncStatus.PENDING
):
return connection, False
connection.last_sync_status = DriveConnection.SyncStatus.PENDING
connection.last_sync_error = ""
connection.sync_total = 0
connection.sync_processed = 0
connection.sync_added = 0
connection.sync_updated = 0
connection.sync_failed = 0
connection.save(
update_fields=[
"last_sync_status",
"last_sync_error",
"sync_total",
"sync_processed",
"sync_added",
"sync_updated",
"sync_failed",
"last_modified",
]
)
transaction.on_commit(
partial(_dispatch_sync, connection_id=connection.id)
)
return connection, True