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:
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user