Record the annotated rebuild, bounded graphics matrices, private release-shape smoke, and remaining integration gates. Preserve STEP20C NEXT and production multi-output NOT YET SUPPORTED.
122 lines
6.4 KiB
Python
122 lines
6.4 KiB
Python
"""EXPERIMENTAL persistent overnight budget.
|
|
WHY: failures consume the same budget as successes.
|
|
CONTRACT: reserve before action; no refunds or reset operation.
|
|
INVARIANT: exclusive flock covers validation and durable atomic replacement.
|
|
"""
|
|
import fcntl
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import time
|
|
|
|
LIMITS = {'scene_build': 2, 'mosaic_build': 2, 'stage': 2, 'run': 18, 'capture': 220}
|
|
CAMPAIGNS = {'C': 3, 'D1': 2, 'D2': 2, 'D3': 2, 'D4': 2, 'E1': 1, 'E2': 1, 'E3': 2, 'F': 2, 'G': 1}
|
|
|
|
def _sync_dir(parent):
|
|
fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(fd)
|
|
finally:
|
|
os.close(fd)
|
|
|
|
def initialize(path):
|
|
path = Path(path)
|
|
with path.with_suffix('.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
state = {'limits': LIMITS, 'campaign_limits': CAMPAIGNS,
|
|
'used': dict.fromkeys(LIMITS, 0), 'campaign_used': dict.fromkeys(CAMPAIGNS, 0),
|
|
'runs': {}, 'events': []}
|
|
# O_EXCL makes reinitialization an error, never an implicit reset.
|
|
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
with os.fdopen(fd, 'w') as out:
|
|
json.dump(state, out, indent=2); out.write('\n'); out.flush(); os.fsync(out.fileno())
|
|
_sync_dir(path.parent)
|
|
return state
|
|
|
|
def _validate(state):
|
|
"""WHY: corrupted persisted counters must fail closed before any action."""
|
|
if not isinstance(state, dict) or state.get('limits') != LIMITS or state.get('campaign_limits') != CAMPAIGNS:
|
|
raise RuntimeError('budget contract changed or corrupt')
|
|
for key, limits in [('used', LIMITS), ('campaign_used', CAMPAIGNS)]:
|
|
values = state.get(key)
|
|
if not isinstance(values, dict) or set(values) != set(limits):
|
|
raise RuntimeError('corrupt counter keys')
|
|
if any(type(values[k]) is not int or not 0 <= values[k] <= limits[k] for k in limits):
|
|
raise RuntimeError('corrupt counter values')
|
|
events, runs = state.get('events'), state.get('runs')
|
|
if not isinstance(events, list) or not isinstance(runs, dict):
|
|
raise RuntimeError('corrupt event/run data')
|
|
counted = dict.fromkeys(LIMITS, 0)
|
|
campaigns = dict.fromkeys(CAMPAIGNS, 0)
|
|
replay = {}
|
|
for i, event in enumerate(events):
|
|
if not isinstance(event, dict) or type(event.get('sequence')) is not int or event['sequence'] != i:
|
|
raise RuntimeError('corrupt event sequence')
|
|
kind, amount = event.get('kind'), event.get('amount')
|
|
if kind not in LIMITS or type(amount) is not int or amount <= 0 or type(event.get('reserved_at_ns')) is not int or event['reserved_at_ns'] <= 0:
|
|
raise RuntimeError('corrupt reservation event')
|
|
counted[kind] += amount
|
|
campaign, run_id = event.get('campaign'), event.get('run_id')
|
|
if kind == 'run':
|
|
if amount != 1 or campaign not in CAMPAIGNS or not isinstance(run_id, str) or not run_id or run_id in replay:
|
|
raise RuntimeError('corrupt run event')
|
|
replay[run_id] = {'campaign': campaign, 'captures': 0}
|
|
campaigns[campaign] += 1
|
|
elif kind == 'capture':
|
|
if run_id not in replay or campaign is not None:
|
|
raise RuntimeError('corrupt capture event')
|
|
replay[run_id]['captures'] += amount
|
|
elif campaign is not None or run_id is not None:
|
|
raise RuntimeError('unexpected build event association')
|
|
if counted != state['used'] or campaigns != state['campaign_used'] or set(replay) != set(runs):
|
|
raise RuntimeError('events do not reconcile counters')
|
|
for run_id, expected in replay.items():
|
|
run = runs[run_id]
|
|
if not isinstance(run, dict) or set(run) != {'campaign', 'captures', 'duration_s'} or run.get('campaign') != expected['campaign'] or type(run.get('captures')) is not int or run['captures'] != expected['captures']:
|
|
raise RuntimeError('corrupt run record')
|
|
if not 0 <= run['captures'] <= (12 if run['campaign'] == 'F' else 20) or type(run.get('duration_s')) not in (int, float) or not 0 < run['duration_s'] <= 150:
|
|
raise RuntimeError('corrupt run bounds')
|
|
|
|
def reserve(path, kind, *, campaign=None, run_id=None, amount=1, duration_s=150):
|
|
path = Path(path)
|
|
if kind not in LIMITS or type(amount) is not int or amount <= 0:
|
|
raise ValueError('invalid reservation')
|
|
with path.with_suffix('.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
state = json.loads(path.read_text())
|
|
_validate(state)
|
|
if state['used'][kind] + amount > LIMITS[kind]:
|
|
raise RuntimeError('global budget exhausted: ' + kind)
|
|
if kind == 'run':
|
|
if amount != 1 or campaign not in CAMPAIGNS or not isinstance(run_id, str) or not run_id or run_id in state['runs']:
|
|
raise ValueError('unique run and known campaign required')
|
|
if type(duration_s) not in (int, float) or not 0 < duration_s <= 150:
|
|
raise ValueError('run duration exceeds 150 seconds')
|
|
if state['campaign_used'][campaign] >= CAMPAIGNS[campaign]:
|
|
raise RuntimeError('campaign budget exhausted')
|
|
state['campaign_used'][campaign] += 1
|
|
state['runs'][run_id] = {'campaign': campaign, 'captures': 0, 'duration_s': duration_s}
|
|
elif kind == 'capture':
|
|
if campaign is not None or run_id not in state['runs']:
|
|
raise ValueError('capture requires reserved run')
|
|
run = state['runs'][run_id]
|
|
if run['captures'] + amount > (12 if run['campaign'] == 'F' else 20):
|
|
raise RuntimeError('run capture budget exhausted')
|
|
run['captures'] += amount
|
|
elif campaign is not None or run_id is not None:
|
|
raise ValueError('build cannot associate run')
|
|
state['used'][kind] += amount
|
|
event = {'sequence': len(state['events']), 'kind': kind, 'amount': amount,
|
|
'campaign': campaign, 'run_id': run_id, 'reserved_at_ns': time.time_ns()}
|
|
state['events'].append(event)
|
|
fd, temporary = tempfile.mkstemp(prefix='.budget-', dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, 'w') as out:
|
|
json.dump(state, out, indent=2); out.write('\n'); out.flush(); os.fsync(out.fileno())
|
|
os.replace(temporary, path)
|
|
_sync_dir(path.parent)
|
|
finally:
|
|
if os.path.exists(temporary):
|
|
os.unlink(temporary)
|
|
return event
|