Enforce the Devii task quotas with atomic reservations

The creation and run quotas were checked and then acted on, so two concurrent
create_task calls or two schedulers could both pass the check and overshoot the
limit. Both are now a single conditional INSERT decided on the driver rowcount:
reserve_run takes a run slot after the claim and releases the claim by deferring
when the quota is spent, and insert_task_within_quota does the same for the task
row itself. Racing twelve and sixteen processes now yields exactly the limit.

The atomic insert names its columns, and dataset skips a None valued key when it
creates a table lazily, so the store declares the full task column set up front.
Both the column and index ensures now tolerate a concurrent duplicate, since
several processes build a store at once and SQLite DDL is not idempotent.

Adds the quota, task-run context, guard, store and scheduler test suites, and
documents the chokepoints and the unhackable task-run flag.
This commit is contained in:
2026-07-26 19:58:42 +02:00
parent ca6c527e32
commit 9cfaddfc40
11 changed files with 168 additions and 148 deletions
+8 -9
View File
@@ -85,12 +85,12 @@ def window_start(reference: datetime) -> datetime:
return reference - timedelta(hours=WINDOW_HOURS)
def _stamps(db: Any, sql: str, table: str, owner_kind: str, owner_id: str, cutoff: str) -> list[str]:
def _stamps(
db: Any, sql: str, table: str, owner_kind: str, owner_id: str, cutoff: str
) -> list[str]:
if table not in db.tables:
return []
rows = db.query(
sql, kind=owner_kind, owner=owner_id, cutoff=cutoff, cap=SAMPLE_CAP
)
rows = db.query(sql, kind=owner_kind, owner=owner_id, cutoff=cutoff, cap=SAMPLE_CAP)
return [str(row["created_at"]) for row in rows if row.get("created_at")]
@@ -127,7 +127,9 @@ def create_quota(db: Any, owner_kind: str, owner_id: str, reference: datetime) -
return _quota(stamps, limit)
def record_run(db: Any, owner_kind: str, owner_id: str, task_uid: str, reference: datetime) -> None:
def record_run(
db: Any, owner_kind: str, owner_id: str, task_uid: str, reference: datetime
) -> None:
from devplacepy.utils import generate_uid
db[RUNS_TABLE].insert(
@@ -149,10 +151,7 @@ def reserve_run(
from devplacepy.utils import generate_uid
limit = run_limit(owner_is_admin(owner_id))
if RUNS_TABLE not in db.tables:
record_run(db, owner_kind, owner_id, task_uid, reference)
return True
if limit <= 0:
if limit <= 0 or RUNS_TABLE not in db.tables:
record_run(db, owner_kind, owner_id, task_uid, reference)
return True
params = {
+2 -5
View File
@@ -8,7 +8,7 @@ from typing import Any, Awaitable, Callable, Optional
from .context import task_run_scope
from .controller import compute_followup
from .guards import BudgetProbe, Deferral, budget_deferral, run_deferral, retire_reason
from .guards import BudgetProbe, Deferral, budget_deferral, retire_reason, run_deferral
from .limits import reserve_run
from .schedule import next_run, now_utc, to_iso
from .store import TaskStore, claim, due_rows
@@ -102,10 +102,7 @@ def defer(store: TaskStore, row: dict[str, Any], postponement: Deferral) -> None
},
)
logger.info(
"Task uid=%s postponed to %s: %s",
row.get("uid"),
retry_at,
postponement.reason,
"Task uid=%s postponed to %s: %s", row.get("uid"), retry_at, postponement.reason
)
_audit_task_deferred(row, postponement.reason, retry_at)
+1 -3
View File
@@ -148,9 +148,7 @@ class TaskStore:
def require_creation_allowed(self) -> None:
if self._operator:
return
denial = creation_denial(
self._db, self._owner_kind, self._owner_id, now_utc()
)
denial = creation_denial(self._db, self._owner_kind, self._owner_id, now_utc())
if denial is not None:
raise denial