65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""Durable debits. Offline tests initialize temporary ledgers only.
|
|
|
|
The offline LAB never has a runtime ledger. Future registration is a separate
|
|
explicitly authorized ticket; failed reservations/actions are never refunded.
|
|
"""
|
|
import fcntl
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import time
|
|
from model import require
|
|
|
|
LIMITS = dict(run=1, chain=2, grab=4, click=9, capture=16, camera=2, transition=1, move=24)
|
|
|
|
|
|
def durable(path, state):
|
|
fd, tmp = tempfile.mkstemp(prefix='.debit-', dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, 'w') as out:
|
|
json.dump(state, out); out.flush(); os.fsync(out.fileno())
|
|
os.replace(tmp, path)
|
|
fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(fd)
|
|
finally:
|
|
os.close(fd)
|
|
finally:
|
|
if os.path.exists(tmp):
|
|
os.unlink(tmp)
|
|
|
|
|
|
def initial_for_offline_test(path):
|
|
"""No production initialization API: the caller proves isolated test storage."""
|
|
path = Path(path)
|
|
require(path.parent.name.startswith('geometry-ledger-test-'), 'test-ledger-only')
|
|
with path.open('x') as out:
|
|
json.dump(dict(limits=LIMITS, used=dict.fromkeys(LIMITS, 0), events=[]), out)
|
|
out.flush(); os.fsync(out.fileno())
|
|
|
|
|
|
def valid(state):
|
|
require(state.get('limits') == LIMITS and set(state.get('used', {})) == set(LIMITS), 'ledger-schema')
|
|
require(all(type(state['used'][k]) is int and 0 <= state['used'][k] <= LIMITS[k] for k in LIMITS), 'ledger-counts')
|
|
events = state.get('events')
|
|
require(isinstance(events, list) and len(events) == sum(state['used'].values()), 'ledger-events')
|
|
require(all(isinstance(e, dict) and e.get('seq') == i and e.get('kind') in LIMITS and
|
|
type(e.get('ns')) is int and e['ns'] > 0 for i, e in enumerate(events)), 'ledger-sequences')
|
|
require(all(sum(e['kind'] == k for e in events) == state['used'][k] for k in LIMITS), 'ledger-accounting')
|
|
return state
|
|
|
|
|
|
def reserve(path, *kinds):
|
|
path = Path(path)
|
|
require(path.is_file() and not path.is_symlink(), 'existing-ledger-required')
|
|
with path.with_suffix('.lock').open('a+') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
state = valid(json.loads(path.read_text()))
|
|
require(bool(kinds) and all(k in LIMITS for k in kinds), 'ledger-debit-kinds')
|
|
require(all(state['used'][k] + kinds.count(k) <= LIMITS[k] for k in kinds), 'ledger-exhausted')
|
|
for k in kinds:
|
|
state['used'][k] += 1
|
|
state['events'].append(dict(seq=len(state['events']), kind=k, ns=time.monotonic_ns()))
|
|
durable(path, state)
|
|
return state
|