@@ -417,7 +417,9 @@ async def create_task_sweep_core(
417417 of the *raw* client submission so an honest retry is not spuriously rejected;
418418 when omitted it is computed from ``submission`` as received here.
419419 """
420+ from oddish .config import QuotaMode
420421 from oddish .core .quota_admission import admit_trials
422+ from oddish .core .quotas import acquire_quota_locks
421423 from oddish .core .sweeps import (
422424 build_task_submission_from_sweep ,
423425 build_trial_specs_from_sweep ,
@@ -525,17 +527,19 @@ async def create_task_sweep_core(
525527 if submission .append_to_task :
526528 # Lock order must stay ``quota advisory → task row`` to match
527529 # ``cancel_trials_if_quota_reached`` (quota first, then task FOR UPDATE).
528- # Taking the task row before ``admit_trials`` inverted that order and
530+ # Taking the task row before the quota lock inverted that order and
529531 # deadlocked under concurrent over-quota cancellation.
530532 #
531- # Also do not take the org quota lock across reconcile: that window can
532- # take tens of seconds on large tasks and starves every concurrent
533- # submit / enforcement worker (opaque 500s on /tasks/sweep).
533+ # Admit only against the locked plan: an unlocked estimate can still
534+ # ``QuotaExceeded`` (402) after a concurrent append already filled the
535+ # deficit, even when this request would insert fewer trials or none.
536+ # Hold the quota advisory only across the short locked plan + admit +
537+ # insert — not across the earlier experiment/setup work.
534538 task = await get_task_for_org_core (
535539 session , task_id = submission .task_id , org_id = org_id
536540 )
537541 # Read-only intent from the unlocked snapshot. Applied under FOR UPDATE
538- # after admission (idempotent flips).
542+ # after the quota lock (idempotent flips).
539543 want_run_analysis = bool (task .run_analysis or submission .run_analysis )
540544 want_run_probe = bool (task .run_probe or submission .run_probe )
541545
@@ -582,20 +586,10 @@ async def create_task_sweep_core(
582586 target_experiment_id = new_experiment_id or (
583587 primary_experiment .id if primary_experiment else None
584588 )
585- # Unlocked estimate for quota admission only. Authoritative plan is
586- # rebuilt under the task row lock below so concurrent appends cannot
587- # both observe the same deficit and overshoot declarative N.
588- planned_trials , _ = await _plan_append_trials (
589- session ,
590- task = task ,
591- submission = submission ,
592- target_experiment_id = target_experiment_id ,
593- default_environment = effective_default_env ,
594- allowed_environments = allowed_environments ,
595- )
596- # Quota advisory lock is acquired here (and held through commit).
597- await admit_trials (session , org_id , billed_user_id , count = len (planned_trials ))
598- # Task row lock only after quota — same order as enforcement.
589+ # Quota advisory before task FOR UPDATE (ENFORCE only; SHADOW/OFF do
590+ # not take these locks on admit or cancel either).
591+ if settings .quota_mode == QuotaMode .ENFORCE and org_id is not None :
592+ await acquire_quota_locks (session , org_id , billed_user_id )
599593 await session .refresh (task , with_for_update = True )
600594 # Allow flipping task.run_analysis from False to True on append.
601595 # ``run_analysis`` runs at trial-completion time, so updating the
@@ -625,17 +619,8 @@ async def create_task_sweep_core(
625619 default_environment = effective_default_env ,
626620 allowed_environments = allowed_environments ,
627621 )
628- if len (trials ) > len (planned_trials ):
629- # Rare: deficit grew while we waited (e.g. concurrent failures).
630- # Re-check the *full* final count — admit_trials does not accumulate
631- # the earlier estimate (it only adds ``count`` to current inflight),
632- # so a delta-only top-up would undercount headroom.
633- await admit_trials (
634- session ,
635- org_id ,
636- billed_user_id ,
637- count = len (trials ),
638- )
622+ # Authoritative count only (no-op when the locked plan is empty).
623+ await admit_trials (session , org_id , billed_user_id , count = len (trials ))
639624
640625 append_submission = submission .model_copy (
641626 update = {
0 commit comments