feat: RBAC for RTC write modes (Gerenciador ADMIN only, denials audited), central backpressure for heavy jobs (HEAVY_JOB_CONCURRENCY_LIMIT), concurrency simulation evidence
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+19
-5
@@ -6,8 +6,9 @@ REPROCESS_ALL, REPROCESS_PRODUCTS); automation never authorizes Firebird writes.
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy import select, update, func, text
|
||||
|
||||
from .config import settings
|
||||
from .models import Installation, Job
|
||||
|
||||
FULL_KINDS = ("INITIAL_SYNC", "REPROCESS_ALL") # read every product, reclassify all
|
||||
@@ -65,13 +66,26 @@ def claim_next_job(session, agent):
|
||||
recover_stale_running(session, agent)
|
||||
if session.scalar(select(Job.id).where(Job.agent_id == agent.id, Job.status == "RUNNING").limit(1)) is not None:
|
||||
return None
|
||||
candidate = session.scalar(
|
||||
select(Job.id)
|
||||
pending = session.execute(
|
||||
select(Job.id, Job.kind)
|
||||
.where(Job.agent_id == agent.id, Job.status == "PENDING")
|
||||
.order_by(Job.created_at.asc(), Job.id.asc())
|
||||
.limit(1)
|
||||
)
|
||||
.limit(10)
|
||||
).all()
|
||||
if not pending:
|
||||
return None
|
||||
limit = settings.heavy_job_concurrency_limit
|
||||
heavy_slot_free = True
|
||||
if limit > 0 and any(kind in FULL_KINDS for _, kind in pending):
|
||||
if session.bind is not None and session.bind.dialect.name == "postgresql":
|
||||
session.execute(text("SELECT pg_advisory_xact_lock(7305001)")) # serialize slot accounting across concurrent pollers; released at commit
|
||||
running_heavy = session.scalar(select(func.count()).select_from(Job).where(Job.status == "RUNNING", Job.kind.in_(FULL_KINDS),
|
||||
Job.started_at > datetime.now(timezone.utc) - STALE_RUNNING_AFTER)) or 0
|
||||
heavy_slot_free = running_heavy < limit
|
||||
# oldest job this Agent may run now: a light job is never held behind a heavy one that is waiting for a slot
|
||||
candidate = next((job_id for job_id, kind in pending if kind not in FULL_KINDS or heavy_slot_free), None)
|
||||
if candidate is None:
|
||||
session.rollback() # releases the advisory lock; the heavy job stays PENDING (queue) and the Agent keeps polling
|
||||
return None
|
||||
now = datetime.now(timezone.utc)
|
||||
result = session.execute(
|
||||
|
||||
Reference in New Issue
Block a user