Files
privateGPT/private_gpt/arq/runner.py
Javier Martinez cd8ca2214a feat: resumable chat worker + tool worker + async tokenizer (#2298)
* fix: avoid to block the loop

* fix: blocks in expansion

* fix: remove maximum concurrent users

...

* fix: multiplexer

* fix: readers

* fix: more fixes

...

* fix: impl

* feat: tool scheduler

* feat: add adaptative

* feat: add chat worker

* fix: max

* feat: add chat/tools workers

* fix: mypy

* feat: add generic scheduler

* fix: get result

* feat: do serializable the tool executor

* fix: tools

* fix: config

* fix: config

* fix: args

* fix: config

* fix: serializer

* Revert "fix: blocks in expansion"

This reverts commit a2110f94a8.

* fix: unify all logic

* feat: add ingestion scheduler

* fix: settings

* fix: config

* feat: add arq worker to chat

* fix: arq worker

* fix: add nest

* fix: mypy

* fix: await

* fix: script stress

* fix: tokenizer

* fix: chat scheduler

* fix: mypy

* fix: add async tokenizer

* fix: improve condense

* fix: tool scheduler

* feat: add initial real async chat worker

* fix: mypy

* fix: do resumable local executor

...

...

...

fix: revert usleess changes

fix: remove parent chat job

fix: refactor

fix: loop

ref: rename models

fix: chat engine

fix: mypy

...

...

...

fix: fix deps

* fix: tests

* fix: tests

* ...

* fix: stream

* fix: config

* fix: scheduler

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Handle PGPT_WORKER_MODE=celery in health check worker status

* fix: cancel

* fix: arch

* fix: test ingestion

* fix: deserialization of chat messages

* fix: broken results

* fix: mypy

* fix: test

* fix: config

* fix: remove arq tool worker

* fix: output cls

* fix: preserve early resumable tool callbacks

* fix: preserve async tool result order

* refactor: address worker PR review comments

* fix: mypy

* test: colocate ARQ chat enqueue coverage

* fix: remove redis from tests

* test: isolate chat mocks and cancellation timing

* fix: tests

(cherry picked from commit 218b599c66)

# Conflicts:
#	tests/server/chat/anthropic/test_anthropic_client.py
#	tests/server/chat/anthropic/test_langchain_anthropic.py
#	tests/server/chat/test_chat_knowledge_revamp.py
#	tests/server/chat/test_chat_routes.py
#	tests/server/chat/test_chat_routes_skills_integration.py

* fix: tests

(cherry picked from commit fc5ec0f72a)

* fix: ruff

* fix: test

* fix: worker config

(cherry picked from commit 1371c275a1)

* fix: principal

* test: remove flaky chat cancellation assertion

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-15 12:12:45 +02:00

141 lines
4.5 KiB
Python

import asyncio
import contextlib
import os
import signal
import subprocess
import sys
from collections.abc import Callable
from arq.typing import StartupShutdown
from arq.worker import Worker
from private_gpt.arq.hooks import on_job_end
from private_gpt.arq.lifecycle import shutdown, startup
from private_gpt.arq.settings import get_queue_name, get_redis_settings
from private_gpt.arq.tasks import autodiscover_registered_tasks
from private_gpt.settings.settings import Settings, settings
def _default_concurrency() -> int:
cpu_count = os.cpu_count() or 1
if cpu_count <= 1:
return 1
return 1 << (cpu_count.bit_length() - 1)
def _task_packages() -> tuple[str, ...]:
configured = os.environ.get("PGPT_ARQ_TASK_PACKAGES", "")
task_packages = tuple(
package.strip() for package in configured.split(",") if package.strip()
)
if not task_packages:
raise ValueError("PGPT_ARQ_TASK_PACKAGES must configure at least one package")
return task_packages
def _queue_name() -> str:
queue = os.environ.get("PGPT_ARQ_QUEUE", "").strip()
if not queue:
raise ValueError("PGPT_ARQ_QUEUE must configure a queue")
return get_queue_name(queue)
def run_arq_worker(
*,
settings_resolver: Callable[[], Settings] = settings,
startup_hook: StartupShutdown = startup,
shutdown_hook: StartupShutdown = shutdown,
) -> None:
app_module = os.environ.get("PGPT_WORKER_APP_MODULE", "private_gpt")
current_settings = settings_resolver()
task_packages = _task_packages()
queue_name = _queue_name()
max_jobs = int(os.environ.get("PGPT_ARQ_MAX_JOBS", str(_default_concurrency())))
job_timeout = int(os.environ.get("PGPT_ARQ_JOB_TIMEOUT", "21600"))
api_enabled = os.environ.get("API_ENABLED", "true").lower() == "true"
api_port = os.environ.get("API_PORT", "8091")
healthcheck_app = f"{app_module}.arq.healthcheck:app"
procs: list[subprocess.Popen[bytes]] = []
def _cleanup(signum: int = 0, frame: object | None = None) -> None:
del frame
for proc in procs:
proc.terminate()
for proc in procs:
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
if signum:
raise SystemExit(0)
signal.signal(signal.SIGTERM, _cleanup)
signal.signal(signal.SIGINT, _cleanup)
if api_enabled:
print(f"Starting arq worker healthcheck on port {api_port}")
procs.append(
subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
healthcheck_app,
"--host",
"0.0.0.0",
"--port",
api_port,
"--no-access-log",
"--log-level",
"critical",
]
)
)
async def _main() -> None:
worker = Worker(
functions=autodiscover_registered_tasks(*task_packages),
queue_name=queue_name,
redis_settings=get_redis_settings(current_settings),
on_startup=startup_hook,
on_shutdown=shutdown_hook,
on_job_end=on_job_end,
handle_signals=False,
allow_abort_jobs=True,
max_jobs=max_jobs,
max_tries=1,
retry_jobs=False,
keep_result=0,
job_timeout=job_timeout,
health_check_interval=30,
job_completion_wait=5,
)
loop = asyncio.get_running_loop()
stop_event = asyncio.Event()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, stop_event.set)
worker_task = asyncio.create_task(worker.async_run())
stop_task = asyncio.create_task(stop_event.wait())
done, pending = await asyncio.wait(
{worker_task, stop_task}, return_when=asyncio.FIRST_COMPLETED
)
if stop_task in done and not worker_task.done():
await worker.close()
with contextlib.suppress(asyncio.CancelledError):
await worker_task
for task in pending:
task.cancel()
print(
f"Starting arq worker queue={queue_name} task_packages={','.join(task_packages)} "
f"max_jobs={max_jobs} job_timeout={job_timeout}"
)
try:
asyncio.run(_main())
finally:
_cleanup()
if __name__ == "__main__":
run_arq_worker()