mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-04 20:22:21 +01:00
fix: stop status polling from cancelling running scheduled tasks (#5789)
* fix: stop polling GET /api/tasks/runs/recent from cancelling running tasks Two paths caused the scheduler to interrupt a running background task when the frontend Activity view polled for status: 1. GET /api/tasks/runs/recent was not in _PASSIVE_EXACT_PATHS, so _InteractiveActivityMiddleware treated it as a foreground request and called stop_background_tasks_for_foreground, cancelling any in-flight scheduled task. Add it to _PASSIVE_EXACT_PATHS alongside the other read-only polling endpoints. 2. The /api/activity/heartbeat handler called stop_background_tasks_for_foreground unconditionally, ignoring BACKGROUND_TASK_FOREGROUND_GATE=false. Wrap the call in a _gate_enabled() guard so the env var fully disables heartbeat- triggered cancellations. Fixes #5782 Signed-off-by: Christian Sidak <christian@sentineltech.eu> Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> * fix(scheduler): respect foreground gate for heartbeat --------- Signed-off-by: Christian Sidak <christian@sentineltech.eu> Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com> Co-authored-by: Alexandre Teixeira <alexandremagteixeira@gmail.com>
This commit is contained in:
co-authored by
Alexandre Teixeira
parent
a6bc86e331
commit
b2789d04fb
@@ -630,13 +630,24 @@ app.include_router(auth_router)
|
|||||||
|
|
||||||
@app.post("/api/activity/heartbeat")
|
@app.post("/api/activity/heartbeat")
|
||||||
async def activity_heartbeat():
|
async def activity_heartbeat():
|
||||||
from src.interactive_gate import mark_browser_activity
|
from src.interactive_gate import (
|
||||||
|
mark_browser_activity,
|
||||||
|
maybe_stop_background_tasks_for_heartbeat,
|
||||||
|
)
|
||||||
|
|
||||||
await mark_browser_activity()
|
await mark_browser_activity()
|
||||||
|
|
||||||
async def _stop_background():
|
async def _stop_background():
|
||||||
try:
|
try:
|
||||||
await task_scheduler.stop_background_tasks_for_foreground(reason="browser heartbeat")
|
await maybe_stop_background_tasks_for_heartbeat(
|
||||||
|
task_scheduler.stop_background_tasks_for_foreground
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.getLogger("app.foreground_gate").debug("heartbeat task stop failed", exc_info=True)
|
logging.getLogger("app.foreground_gate").debug(
|
||||||
|
"heartbeat task stop failed",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
asyncio.create_task(_stop_background())
|
asyncio.create_task(_stop_background())
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ _PASSIVE_EXACT_PATHS = {
|
|||||||
"/api/activity/heartbeat",
|
"/api/activity/heartbeat",
|
||||||
"/api/client-perf",
|
"/api/client-perf",
|
||||||
"/api/tasks/notifications",
|
"/api/tasks/notifications",
|
||||||
|
"/api/tasks/runs/recent",
|
||||||
"/api/research/active",
|
"/api/research/active",
|
||||||
"/api/email/urgency-state",
|
"/api/email/urgency-state",
|
||||||
# UI idle poll sibling of urgency-state; must not pre-empt background tasks.
|
# UI idle poll sibling of urgency-state; must not pre-empt background tasks.
|
||||||
@@ -76,6 +77,19 @@ _PASSIVE_PREFIXES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def maybe_stop_background_tasks_for_heartbeat(stop_background) -> bool:
|
||||||
|
"""Stop background work for browser activity only when the gate is enabled.
|
||||||
|
|
||||||
|
``stop_background`` is injected by the application boundary so this policy
|
||||||
|
remains independently testable without importing the full FastAPI app.
|
||||||
|
"""
|
||||||
|
if not _enabled():
|
||||||
|
return False
|
||||||
|
|
||||||
|
await stop_background(reason="browser heartbeat")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def should_track_interactive_request(path: str, method: str = "GET") -> bool:
|
def should_track_interactive_request(path: str, method: str = "GET") -> bool:
|
||||||
if not _enabled():
|
if not _enabled():
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Regression tests for polling endpoints and foreground task interruption."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_gate():
|
||||||
|
import src.interactive_gate as ig
|
||||||
|
|
||||||
|
importlib.reload(ig)
|
||||||
|
return ig
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_runs_recent_is_passive():
|
||||||
|
ig = _reload_gate()
|
||||||
|
|
||||||
|
assert not ig.should_track_interactive_request(
|
||||||
|
"/api/tasks/runs/recent", "GET"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_runs_recent_does_not_affect_other_task_paths():
|
||||||
|
ig = _reload_gate()
|
||||||
|
|
||||||
|
# A neighboring mutating path must remain interactive.
|
||||||
|
assert ig.should_track_interactive_request(
|
||||||
|
"/api/tasks/runs/recent/something", "POST"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_heartbeat_does_not_stop_background_tasks_when_gate_disabled(monkeypatch):
|
||||||
|
ig = _reload_gate()
|
||||||
|
monkeypatch.setenv("BACKGROUND_TASK_FOREGROUND_GATE", "false")
|
||||||
|
|
||||||
|
stop_calls = []
|
||||||
|
|
||||||
|
async def fake_stop_background_tasks_for_foreground(*, reason):
|
||||||
|
stop_calls.append(reason)
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
ig.maybe_stop_background_tasks_for_heartbeat(
|
||||||
|
fake_stop_background_tasks_for_foreground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
assert stop_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_heartbeat_stops_background_tasks_when_gate_enabled(monkeypatch):
|
||||||
|
ig = _reload_gate()
|
||||||
|
monkeypatch.delenv("BACKGROUND_TASK_FOREGROUND_GATE", raising=False)
|
||||||
|
|
||||||
|
stop_calls = []
|
||||||
|
|
||||||
|
async def fake_stop_background_tasks_for_foreground(*, reason):
|
||||||
|
stop_calls.append(reason)
|
||||||
|
|
||||||
|
result = asyncio.run(
|
||||||
|
ig.maybe_stop_background_tasks_for_heartbeat(
|
||||||
|
fake_stop_background_tasks_for_foreground
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert stop_calls == ["browser heartbeat"]
|
||||||
Reference in New Issue
Block a user