7 entries (30→37 total). Muse Glimmer 28B (GGUF) avg 80.7 — the strongest model in the benchmark, 6/7 prompts Minor Logic Flaws: lfu 76 | webhook 81 | automation 89 | rust 85 | data 88 | tts 58 | mcp 88 Standout results: - rust 85 (KAT 36, Qwen3-Coder 54) — real tokio channels (mpsc::channel, not hallucinated mpsc::bounded), two-tier CancellationToken, zero clippy lints; one-line E0507 compile fix. - automation 89 — first model to print a correct summary (98/2/0/100); atomic temp+fsync+rename checkpointing. - data 88 edges out Gemma-26B's 86; mcp 88 sets the bar on a new prompt. Only weak spot: tts 58 (backpressure raises instead of awaits, like Qwen3-Coder). Captured via the native /api/v1/chat fix (real tok/sec + TTFT). Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Also gitignore checkpoint.json (automation test runtime artifact). Co-Authored-By: Claude <noreply@anthropic.com>
189 lines
5.4 KiB
Python
189 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Async batch processor with checkpointing, idempotency and graceful SIGINT.
|
|
Stdlib only, Python 3.11+.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import signal
|
|
import time
|
|
import random
|
|
|
|
CHECKPOINT_PATH = "checkpoint.json"
|
|
|
|
class ProcessingError(Exception):
|
|
pass
|
|
|
|
async def process(item: str) -> str:
|
|
"""Flaky external call ~80% success, 10-80ms latency."""
|
|
await asyncio.sleep(random.uniform(0.01, 0.08))
|
|
if random.random() < 0.2:
|
|
raise ProcessingError(f"flaky failure for {item}")
|
|
return f"ok:{item}"
|
|
|
|
def load_checkpoint():
|
|
if not os.path.exists(CHECKPOINT_PATH):
|
|
return set(), set()
|
|
try:
|
|
with open(CHECKPOINT_PATH, "r") as f:
|
|
data = json.load(f)
|
|
return set(data.get("completed", [])), set(data.get("failed", []))
|
|
except Exception:
|
|
return set(), set()
|
|
|
|
def save_checkpoint(completed, failed):
|
|
data = {"completed": list(completed), "failed": list(failed)}
|
|
tmp = CHECKPOINT_PATH + ".tmp"
|
|
with open(tmp, "w") as f:
|
|
json.dump(data, f)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.replace(tmp, CHECKPOINT_PATH) # atomic
|
|
|
|
async def handle_item(item, completed, failed, lock, sem, stop_event):
|
|
async with sem:
|
|
if stop_event.is_set():
|
|
return
|
|
backoff = 0.1
|
|
for attempt in range(3):
|
|
try:
|
|
await process(item)
|
|
async with lock:
|
|
if item not in completed and item not in failed:
|
|
completed.add(item)
|
|
save_checkpoint(completed, failed)
|
|
return
|
|
except ProcessingError:
|
|
if attempt == 2:
|
|
async with lock:
|
|
if item not in completed and item not in failed:
|
|
failed.add(item)
|
|
save_checkpoint(completed, failed)
|
|
return
|
|
await asyncio.sleep(backoff)
|
|
backoff *= 2
|
|
|
|
async def run_batch(items):
|
|
completed, failed = load_checkpoint()
|
|
initially_done = len(completed) + len(failed)
|
|
pending = [i for i in items if i not in completed and i not in failed]
|
|
|
|
stop_event = asyncio.Event()
|
|
lock = asyncio.Lock()
|
|
sem = asyncio.Semaphore(8)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
def _sigint():
|
|
if not stop_event.is_set():
|
|
stop_event.set()
|
|
try:
|
|
loop.add_signal_handler(signal.SIGINT, _sigint)
|
|
except (NotImplementedError, ValueError):
|
|
pass
|
|
|
|
start = time.monotonic()
|
|
q = asyncio.Queue()
|
|
for it in pending:
|
|
q.put_nowait(it)
|
|
|
|
async def worker():
|
|
while True:
|
|
if stop_event.is_set() and q.empty():
|
|
break
|
|
try:
|
|
item = await asyncio.wait_for(q.get(), timeout=0.1)
|
|
except asyncio.TimeoutError:
|
|
if q.empty():
|
|
break
|
|
continue
|
|
await handle_item(item, completed, failed, lock, sem, stop_event)
|
|
q.task_done()
|
|
|
|
workers = [asyncio.create_task(worker()) for _ in range(8)]
|
|
await asyncio.gather(*workers, return_exceptions=True)
|
|
|
|
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
summary = {
|
|
"succeeded": len(completed),
|
|
"failed": len(failed),
|
|
"skipped": initially_done,
|
|
"total": len(items),
|
|
"elapsed_ms": elapsed_ms,
|
|
}
|
|
print(json.dumps(summary))
|
|
return summary
|
|
|
|
def main():
|
|
items = [f"job-{i}" for i in range(100)]
|
|
asyncio.run(run_batch(items))
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Tests
|
|
async def run_tests():
|
|
if os.path.exists(CHECKPOINT_PATH):
|
|
os.remove(CHECKPOINT_PATH)
|
|
random.seed(0)
|
|
items = [f"job-{i}" for i in range(50)]
|
|
|
|
calls = {}
|
|
active = 0
|
|
max_active = 0
|
|
orig = process
|
|
|
|
async def counting(item):
|
|
nonlocal active, max_active
|
|
active += 1
|
|
if active > max_active:
|
|
max_active = active
|
|
calls[item] = calls.get(item, 0) + 1
|
|
try:
|
|
return await orig(item)
|
|
finally:
|
|
active -= 1
|
|
|
|
globals()["process"] = counting
|
|
|
|
# watcher for checkpoint validity
|
|
errors = []
|
|
async def watcher():
|
|
while True:
|
|
try:
|
|
with open(CHECKPOINT_PATH, "r") as f:
|
|
json.load(f)
|
|
except FileNotFoundError:
|
|
pass
|
|
except Exception as e:
|
|
errors.append(e)
|
|
break
|
|
await asyncio.sleep(0.001)
|
|
w = asyncio.create_task(watcher())
|
|
|
|
# first partial run -> simulate interruption
|
|
try:
|
|
await asyncio.wait_for(run_batch(items), timeout=0.05)
|
|
except asyncio.TimeoutError:
|
|
pass
|
|
|
|
# second run to finish
|
|
await run_batch(items)
|
|
w.cancel()
|
|
try:
|
|
await w
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
completed, failed = load_checkpoint()
|
|
assert len(completed) + len(failed) == len(items), "not all done"
|
|
assert not errors, f"checkpoint corrupt: {errors}"
|
|
assert max_active <= 8, f"concurrency exceeded: {max_active}"
|
|
# idempotency: no item processed again after it was completed/failed
|
|
# (calls count includes retries, but completed items must not be retried)
|
|
print("tests passed")
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
if len(sys.argv) > 1 and sys.argv[1] == "test":
|
|
asyncio.run(run_tests())
|
|
else:
|
|
main() |