swayfx-mosaic/tooling/probes/step20c-rotated-partial-popup/budget.py

45 lines
1.6 KiB
Python

"""Persistent one-run-per-mode budget for the independent static-R90 campaign."""
import fcntl
import json
import os
from pathlib import Path
MODES = ('control', 'private_standard', 'private_mosaic')
class Budget:
def __init__(self, lab):
self.lab = Path(lab)
self.lock = (self.lab / 'partial-campaign.lock').open('a')
fcntl.flock(self.lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
self.path = self.lab / 'partial-campaign-budget.json'
def read(self):
if not self.path.exists():
if (self.lab / 'runs').exists() and any((self.lab / 'runs').iterdir()):
raise ValueError('missing ledger with retained run')
return {'version': 1, 'runs': []}
state = json.loads(self.path.read_text())
runs = state.get('runs')
if state.get('version') != 1 or not isinstance(runs, list) or len(runs) != len(set(runs)) or any(r not in MODES for r in runs):
raise ValueError('invalid ledger')
return state
def reserve(self, mode):
state = self.read()
if mode not in MODES or mode in state['runs'] or len(state['runs']) >= 3:
raise ValueError('run budget exhausted or duplicate mode')
state['runs'].append(mode)
tmp = self.path.with_suffix('.tmp')
with tmp.open('w') as output:
json.dump(state, output, indent=2)
output.write('\n')
output.flush()
os.fsync(output.fileno())
tmp.replace(self.path)
fd = os.open(self.lab, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(fd)
finally:
os.close(fd)
return state