Grade muse-glimmer-28b full 7-prompt battery — new benchmark leader
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>
This commit is contained in:
@@ -21,3 +21,5 @@ __pycache__/
|
||||
|
||||
# --- Webhook secret (NEVER commit) ---
|
||||
.webhook.secret
|
||||
# automation prompt test artifact (runtime-generated)
|
||||
checkpoint.json
|
||||
|
||||
@@ -1041,6 +1041,240 @@
|
||||
"patch_code": "# 1. Add a real acquire-timeout / pool-exhaustion test:\nasync def test_pool_exhaustion():\n state = MockDatabaseState()\n pool = MockPool(max_size=5, state=state)\n held = [await pool.acquire() for _ in range(5)]\n try:\n with pytest.raises(PoolExhaustedError):\n await asyncio.wait_for(pool.acquire(), timeout=3.0)\n finally:\n for c in held: await c.release()\n assert pool.in_use == 0\n\n# 2. Derive in_use from the semaphore so the counter cannot drift:\n@property\ndef in_use(self) -> int:\n return self.max_size - self._semaphore._value",
|
||||
"timestamp": "2026-07-29T04:30:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-lfu",
|
||||
"prompt_id": "lfu",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "python",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-lfu.py",
|
||||
"tok_sec": 19.38,
|
||||
"ttft_sec": 3.55,
|
||||
"total_tokens": 9095,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": false,
|
||||
"total_score": 76,
|
||||
"breakdown": {
|
||||
"complexity": 18,
|
||||
"concurrency": 15,
|
||||
"isolation": 12,
|
||||
"memory_edge_cases": 16,
|
||||
"test_integrity": 15
|
||||
},
|
||||
"verdict": "Minor Logic Flaws",
|
||||
"best_for": "The strongest LFU result from a non-coder model in this benchmark — nails true O(1) freq-bucket eviction with correct min_freq tracking and __slots__ where both specialist coder models (KAT, Qwen3-Coder) failed catastrophically. Let down by one signature bug: rollback() is sync but awaited.",
|
||||
"critical_bugs": [
|
||||
"rollback() is a sync method (line 213, 'def rollback') returning None, but the test awaits it (line 252, 'await tx2.rollback()') -> TypeError: object NoneType can't be used in 'await' expression. The whole test suite crashes at the rollback assertion. Fix: make it 'async def rollback()'.",
|
||||
"_update_min_freq walks min_freq upward indefinitely with no upper bound; harmless while the cache is non-empty but an unbounded loop on a fully-drained cache.",
|
||||
"No transaction lifecycle guard: after commit() or rollback(), a stale Transaction handle can still call put/get/delete/commit again (double-commit) with no error.",
|
||||
"Transaction.get/commit acquire self.cache.lock directly — transactions share the cache's single lock rather than an isolation lock; correct for the buffer model but couples tx lifetime to cache lock contention."
|
||||
],
|
||||
"patch_code": "# Fix 1 (fatal): rollback must be async to match 'await tx2.rollback()'\nasync def rollback(self):\n self.pending_puts.clear()\n self.pending_deletes.clear()\n\n# Fix 2: bound _update_min_freq against a drained cache\ndef _update_min_freq(self):\n if not self.freq_map:\n self.min_freq = 0; return\n while self.min_freq not in self.freq_map:\n self.min_freq += 1\n\n# Fix 3: lifecycle guard on Transaction\ndef __init__(self, cache):\n self.cache = cache; self.pending_puts = {}; self.pending_deletes = set(); self._closed = False\ndef _check_open(self):\n if self._closed: raise RuntimeError(\"Transaction already committed/rolled back\")\nasync def commit(self):\n self._check_open(); # ... existing body ...; self._closed = True\nasync def rollback(self):\n self._check_open(); self.pending_puts.clear(); self.pending_deletes.clear(); self._closed = True",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-webhook",
|
||||
"prompt_id": "webhook",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "python",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-webhook.py",
|
||||
"tok_sec": 18.96,
|
||||
"ttft_sec": 3.01,
|
||||
"total_tokens": 6628,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": false,
|
||||
"total_score": 81,
|
||||
"breakdown": {
|
||||
"schema_io": 17,
|
||||
"transport": 16,
|
||||
"error_handling": 16,
|
||||
"state_safety": 16,
|
||||
"test_integrity": 16
|
||||
},
|
||||
"verdict": "Minor Logic Flaws",
|
||||
"best_for": "Carefully designed async webhook bridge with correct constant-time HMAC, monotonic-clock idempotency with 300s eviction, real token-bucket rate limiting gating Discord forwards (not intake), and single-retry 429 Retry-After backoff. Suitable for production after minor hardening. (The test-timeout is a harness/port issue, not a code defect — the request path is non-blocking.)",
|
||||
"critical_bugs": [
|
||||
"forward_log grows unbounded — no eviction or cap, memory leak in a long-running process.",
|
||||
"int(self.headers.get('Content-Length', 0)) raises unhandled ValueError on a non-numeric Content-Length header, causing socket close instead of a clean 400.",
|
||||
"No Content-Type or Content-Length headers on any response — clients relying on Content-Length cannot determine body end without connection close."
|
||||
],
|
||||
"patch_code": "# Fix 1: guard Content-Length parsing\ntry: length = int(self.headers.get('Content-Length', 0))\nexcept (ValueError, TypeError):\n self.send_response(400); self.end_headers(); self.wfile.write(b'Invalid Content-Length'); return\nbody = self.rfile.read(length)\n\n# Fix 2: cap forward_log growth\ncap = 10000\nwith log_lock:\n forward_log.append({'event_id': event_id, 'ts': time.monotonic()})\n if len(forward_log) > cap: del forward_log[:len(forward_log) - cap]\n\n# Fix 3: add Content-Type + Content-Length to responses\ndef _respond(self, code, body=b''):\n self.send_response(code)\n self.send_header('Content-Type', 'text/plain')\n self.send_header('Content-Length', str(len(body)))\n self.end_headers()\n if body: self.wfile.write(body)",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-automation",
|
||||
"prompt_id": "automation",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "python",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-automation.py",
|
||||
"tok_sec": 17.18,
|
||||
"ttft_sec": 3.44,
|
||||
"total_tokens": 9012,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": true,
|
||||
"total_score": 89,
|
||||
"breakdown": {
|
||||
"idempotency": 18,
|
||||
"retry_backoff": 19,
|
||||
"checkpointing": 19,
|
||||
"signal_handling": 16,
|
||||
"test_integrity": 17
|
||||
},
|
||||
"verdict": "Minor Logic Flaws",
|
||||
"best_for": "Reliable production-grade async batch processing with atomic checkpointing (temp+fsync+rename) where correctness of summary output and crash-safe state matter more than raw throughput. First model in the benchmark to print a correct, meaningful automation summary (98/2/0/100).",
|
||||
"critical_bugs": [
|
||||
"No explicit checkpoint flush on the SIGINT exit path — relied on per-item save being always-current, which is correct, but a worker that crashes between process()-success and the lock/save block could lose a completed item's state on hard kill mid-critical-section (narrow window).",
|
||||
"Signal handling does not cancel in-flight tasks — a worker past the sem-acquire running process() at SIGINT time runs to completion including all retry/backoff sleeps (up to ~0.7s), delaying exit. Spec allows 'finish' but exit latency can exceed expectations under load.",
|
||||
"Test asserts convergence, concurrency, and corruption-freedom but does NOT assert that a rerun avoids re-invoking process() on already-completed items — the idempotency no-rework guarantee is structurally enforced by the pending filter but unverified by an explicit call-count-on-rerun assertion.",
|
||||
"Queue.task_done() is called but q.join() is never awaited — cosmetic dead code."
|
||||
],
|
||||
"patch_code": "# 1. Explicit checkpoint flush + clean exit on SIGINT\nasync def run_batch(items):\n try:\n await asyncio.gather(*workers, return_exceptions=True)\n finally:\n save_checkpoint(completed, failed) # explicit final flush covers the gap\n try: loop.remove_signal_handler(signal.SIGINT)\n except (NotImplementedError, RuntimeError): pass\n\n# 2. Cancel in-flight on SIGINT for prompt exit (add cooperative bail between retries)\nasync def handle_item(item, ...):\n async with sem:\n if stop_event.is_set(): return\n backoff = 0.1\n for attempt in range(3):\n if stop_event.is_set(): return # bail between retries\n try:\n await process(item)\n async with lock: completed.add(item); save_checkpoint(completed, failed)\n return\n except ProcessingError:\n if attempt == 2:\n async with lock: failed.add(item); save_checkpoint(completed, failed)\n return\n await asyncio.sleep(backoff); backoff *= 2\n\n# 3. Assert no-rework on rerun\nsecond_calls = {k: v for k, v in calls.items()}\nawait run_batch(items) # third run, all should skip\nreprocess = {k: calls[k]-second_calls[k] for k in calls if calls[k] > second_calls.get(k, 0)}\nassert not reprocess, f\"idempotency violated, reprocessed: {reprocess}\"",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-rust",
|
||||
"prompt_id": "rust",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "rust",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-rust.rs",
|
||||
"tok_sec": 16.98,
|
||||
"ttft_sec": 3.58,
|
||||
"total_tokens": 9201,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": false,
|
||||
"total_score": 85,
|
||||
"breakdown": {
|
||||
"ownership": 16,
|
||||
"concurrency": 18,
|
||||
"error_handling": 18,
|
||||
"cancellation": 17,
|
||||
"test_integrity": 16
|
||||
},
|
||||
"verdict": "Minor Logic Flaws",
|
||||
"best_for": "Production-grade async Rust service design with real tokio channels (mpsc::channel(32), not hallucinated mpsc::bounded), two-tier CancellationToken shutdown, per-watcher error isolation with >5-strike unhealthy marking, and zero clippy lints. The strongest Rust result in the benchmark by a wide margin (KAT 36, Qwen3-Coder 54). One compile-blocker: a partial-move E0507 fixable with Option+take().",
|
||||
"critical_bugs": [
|
||||
"E0507 compile error at line 152: self.consumer_handle.await attempts a partial move of JoinHandle<Aggregated> out of &mut self — the code cannot compile or run as written. Fix: declare the field as Option<JoinHandle<Aggregated>> and use self.consumer_handle.take().unwrap().await.",
|
||||
"Logic bug at line 180: tokio::time::Instant::now().elapsed() creates an Instant then immediately calls .elapsed() on it, always producing ~0 nanoseconds — the timestamp field is semantically meaningless. Should capture a process-start Instant or use SystemTime::now().duration_since(UNIX_EPOCH) for epoch millis.",
|
||||
"No fn main() — as a bin target this fails to compile (E0601). Harmless if configured as a lib/test target, but the file as-is is incomplete."
|
||||
],
|
||||
"patch_code": "// Fix 1: wrap consumer_handle in Option + take() in shutdown\npub struct WatcherManager {\n pub(crate) consumer_handle: Option<JoinHandle<Aggregated>>, // was: JoinHandle<Aggregated>\n}\n// in new(): consumer_handle: Some(consumer_handle)\n// in shutdown():\npub async fn shutdown(&mut self) -> Aggregated {\n self.shutdown_token.cancel();\n let mut watchers = self.watchers.lock().await;\n let mut handles = Vec::new();\n for (_, meta) in watchers.drain() { meta.token.cancel(); handles.push(meta.handle); }\n drop(watchers);\n for h in handles { let _ = h.await; }\n let h = self.consumer_handle.take().expect(\"already shut down\");\n h.await.unwrap()\n}\n\n// Fix 2: meaningful timestamp\nuse std::time::{SystemTime, UNIX_EPOCH};\nts: SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64,\n// was: tokio::time::Instant::now().elapsed().as_millis() as u64\n\n// Fix 3: add fn main (or configure as lib target)\nfn main() {}",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-data",
|
||||
"prompt_id": "data",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "python",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-data.py",
|
||||
"tok_sec": 17.17,
|
||||
"ttft_sec": 3.84,
|
||||
"total_tokens": 7308,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": true,
|
||||
"total_score": 88,
|
||||
"breakdown": {
|
||||
"query_safety": 18,
|
||||
"pooling": 19,
|
||||
"transactions": 17,
|
||||
"pagination": 19,
|
||||
"test_integrity": 15
|
||||
},
|
||||
"verdict": "Minor Logic Flaws",
|
||||
"best_for": "Clean parameterized queries throughout (zero interpolation, with a real injection-attempt test storing DROP TABLE as a literal label), semaphore-pooled concurrency with 100-call leak proof, proven pagination (true COUNT, last-page remainder, out-of-range=empty), and snapshot-based rollback verified on partial failure. Edges out Gemma-26B's 86 on the same prompt.",
|
||||
"critical_bugs": [
|
||||
"Missing acquire-timeout test: the pool supports asyncio.wait_for timeout but no test exhausts the pool to force TimeoutError — spec pillar 5 (acquire timeout) is unproven.",
|
||||
"Rollback in relabel_users is manual snapshot/restore, not a real BEGIN/COMMIT/ROLLBACK transaction — a failure during the restore loop would leave inconsistent state.",
|
||||
"MockConnection.fetch dispatch uses fragile substring matching ('FROM users WHERE id =' in query) that would misroute if a param were ever interpolated into the query string — safe only because params never are."
|
||||
],
|
||||
"patch_code": "# 1. Add forced-timeout test (fills the test_integrity gap)\nasync def test_acquire_timeout():\n pool = MockPool(max_size=2, acquire_timeout=0.1)\n svc = UserService(pool)\n hold = [await pool.acquire() for _ in range(2)] # exhaust\n try:\n await asyncio.wait_for(svc.get_users(1, 10), timeout=1.0)\n assert False, 'should have timed out'\n except (TimeoutError, asyncio.TimeoutError): pass\n finally:\n for c in hold: await c.release()\n assert pool.checked_out == 0\n\n# 2. Strengthen rollback — track applied writes and undo only those\napplied = []\ntry:\n for uid, label in pairs:\n await conn.execute('UPDATE users SET label = $1 WHERE id = $2', [label, uid])\n applied.append(uid)\n return len(pairs)\nexcept Exception:\n for uid in reversed(applied): MockDB.users[uid]['label'] = snapshot[uid]\n return 0",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-tts",
|
||||
"prompt_id": "tts",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "python",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-tts.py",
|
||||
"tok_sec": 17.09,
|
||||
"ttft_sec": 2.83,
|
||||
"total_tokens": 9639,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": true,
|
||||
"total_score": 58,
|
||||
"breakdown": {
|
||||
"complexity": 16,
|
||||
"concurrency": 6,
|
||||
"error_handling": 14,
|
||||
"resource_safety": 10,
|
||||
"test_integrity": 12
|
||||
},
|
||||
"verdict": "Critical Bugs",
|
||||
"best_for": "Retry-heavy async job pipelines where backpressure-await is not required and a hard queue-reject is acceptable — the one weak prompt in muse-glimmer's battery. Retry/backoff and callback handling are sound, but the central bounded-concurrency mechanic is wrong.",
|
||||
"critical_bugs": [
|
||||
"submit() raises RuntimeError('Backpressure: queue full') when the deque hits 100 items instead of AWAITING a free slot — the spec explicitly requires blocking backpressure, not rejection. Same central failure as Qwen3-Coder (which scored 44 for the same reason). The backpressure test compounds this by asserting the raise is correct behavior, encoding the bug rather than catching it.",
|
||||
"No shutdown/stop mechanism: the 4 worker tasks are infinite 'while True' loops with no way to cancel or join them. drain() busy-polls and returns, but the workers keep running forever — every TTSPipeline instance leaks 4 tasks.",
|
||||
"Soft-cancel does not interrupt a job already inside mock_synthesize: the cancelled flag is only checked before start and between retry attempts. A first-try-success job cancelled mid-flight still completes and emits 'completed', not 'cancelled'."
|
||||
],
|
||||
"patch_code": "# Fix 1 — backpressure by awaiting a slot, not raising.\nclass TTSPipeline:\n def __init__(self):\n self._slots = asyncio.Semaphore(4) # bounded concurrency at the gate\n self._max_queued = 100\n self._queue = collections.deque()\n self._queue_lock = asyncio.Lock()\n self._queue_not_full = asyncio.Condition(self._queue_lock)\n self._queue_not_empty = asyncio.Condition(self._queue_lock)\n async def submit(self, text, voice):\n job_id = uuid.uuid4().hex\n async with self._queue_lock:\n while len(self._queue) >= self._max_queued:\n await self._queue_not_full.wait() # BACKPRESSURE: await, never raise\n self._queue.append({'id': job_id, 'text': text, 'voice': voice})\n self._queue_not_empty.notify()\n return job_id\n # workers call self._queue_not_full.notify() after popleft()\n\n# Fix 2 — add a real shutdown so workers don't leak\n async def stop(self):\n for w in self._workers: w.cancel()\n await asyncio.gather(*self._workers, return_exceptions=True)\n self._workers.clear()\n\n# Fix 3 — correct the test to assert backpressure, not raise\n async def flood():\n for i in range(120): await slow.submit(f't{i}', 'v') # never raises\n await asyncio.wait_for(flood(), timeout=30) # completes once workers drain",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
},
|
||||
{
|
||||
"id": "muse-glimmer-28b-gguf-mcp",
|
||||
"prompt_id": "mcp",
|
||||
"model_name": "Muse Glimmer 28B",
|
||||
"quant": "GGUF",
|
||||
"param_size": "28B",
|
||||
"format": "gguf",
|
||||
"lang": "typescript",
|
||||
"filename": "outputs/muse-glimmer-28b-gguf-mcp.ts",
|
||||
"tok_sec": 20.2,
|
||||
"ttft_sec": 2.57,
|
||||
"total_tokens": 5244,
|
||||
"speed_caveat": "Slow deep-thinker: ~17-19 t/s, 5-9 min/prompt, ~5-9k tokens incl. reasoning. Fast for quality, not throughput.",
|
||||
"tests_pass": false,
|
||||
"total_score": 88,
|
||||
"breakdown": {
|
||||
"schema_io": 18,
|
||||
"transport": 20,
|
||||
"error_handling": 17,
|
||||
"state_safety": 18,
|
||||
"test_integrity": 15
|
||||
},
|
||||
"verdict": "Minor Logic Flaws",
|
||||
"best_for": "Clean, idiomatic single-file MCP server with REAL SDK wiring (no hallucinated APIs — correct @modelcontextprotocol/sdk imports, setRequestHandler on ListTools/CallTool, StdioServerTransport), Zod-validated tool schemas, timeout-safe fetches via AbortController, and isError-flag error responses instead of bare throws. First model tested on the mcp prompt; sets a high bar.",
|
||||
"critical_bugs": [
|
||||
"Output is JSON.stringify'd into a text content block rather than returned as structured typed content — spec explicitly asks for typed/structured output not raw strings.",
|
||||
"clearTimeout(timer) is called before res.json() in fetchJson, so a slow/stuck JSON body parse has no timeout guard.",
|
||||
"Test 404 case (b) logs 'PASS' in both the success and catch branches, making it a tautological always-pass assertion that doesn't verify error semantics.",
|
||||
"Tests call the *Impl functions directly, bypassing the MCP CallToolRequest handler — the schema-to-handler wiring, isError flag, and content-block formatting are never exercised."
|
||||
],
|
||||
"patch_code": "// 1. Return structured content instead of stringified JSON\ncase 'get_user': {\n const { id } = GetUserSchema.parse(args);\n const user = await getUserImpl(id);\n return { content: [{ type: 'text', text: JSON.stringify(user) }], structuredContent: user };\n}\n\n// 2. Keep timeout alive through JSON parse (move clearTimeout to finally)\nasync function fetchJson(url, timeoutMs = 8000) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const res = await fetch(url, { signal: controller.signal });\n if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);\n return await res.json();\n } catch (err) {\n if (err.name === 'AbortError') throw new Error('Request timed out');\n throw err;\n } finally {\n clearTimeout(timer); // single clear, covers all paths incl. slow json()\n }\n}\n\n// 3. Make 404 test actually assert error semantics\ntry {\n const r = await getUserImpl(9999);\n console.log('b) FAIL — expected throw, got', r);\n} catch (e) {\n console.log('b) 404 handled -> PASS:', e.message);\n}",
|
||||
"timestamp": "2026-08-10T20:00:00Z",
|
||||
"ttft_note": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"model_instance_id": "google/gemma-4-26b-a4b",
|
||||
"model_instance_id": "meta/muse-glimmer",
|
||||
"stats": {
|
||||
"input_tokens": 718,
|
||||
"total_output_tokens": 5153,
|
||||
"reasoning_output_tokens": 1919,
|
||||
"tokens_per_second": 53.47535329547263,
|
||||
"time_to_first_token_seconds": 1.127
|
||||
"input_tokens": 495,
|
||||
"total_output_tokens": 4749,
|
||||
"reasoning_output_tokens": 3228,
|
||||
"tokens_per_second": 20.201965201572563,
|
||||
"time_to_first_token_seconds": 2.571259
|
||||
},
|
||||
"response_id": "resp_d8fafc117fa4e51b927d8cd531ded49984ae59e49c39d3eb",
|
||||
"response_id": "resp_a37c5d6bb520db613250881b4919be086d4aed4a8f3a078a",
|
||||
"_output_types": [
|
||||
"reasoning",
|
||||
"message"
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,216 @@
|
||||
# single file demo - Python 3.11+ asyncio stdlib only
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Mock in-memory DB
|
||||
# ----------------------------------------------------------------------
|
||||
class MockDB:
|
||||
users: Dict[int, Dict[str, Any]] = {}
|
||||
posts: Dict[int, List[Dict[str, Any]]] = {}
|
||||
|
||||
def seed_db():
|
||||
MockDB.users.clear()
|
||||
MockDB.posts.clear()
|
||||
for uid in range(1, 26):
|
||||
MockDB.users[uid] = {"id": uid, "name": f"user{uid}", "label": f"label{uid % 5}"}
|
||||
MockDB.posts[uid] = [
|
||||
{"id": pid, "user_id": uid, "title": f"post{pid} for user{uid}"}
|
||||
for pid in range(1, 6)
|
||||
]
|
||||
|
||||
seed_db()
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Mock pool / connection
|
||||
# ----------------------------------------------------------------------
|
||||
class MockPool:
|
||||
def __init__(self, max_size: int = 5, acquire_timeout: float = 2.0):
|
||||
self.max_size = max_size
|
||||
self.acquire_timeout = acquire_timeout
|
||||
self._sem = asyncio.Semaphore(max_size)
|
||||
self._checked_out = 0
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def acquire(self) -> "MockConnection":
|
||||
try:
|
||||
await asyncio.wait_for(self._sem.acquire(), timeout=self.acquire_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise TimeoutError(f"Pool exhausted after {self.acquire_timeout}s")
|
||||
async with self._lock:
|
||||
self._checked_out += 1
|
||||
return MockConnection(self)
|
||||
|
||||
async def _release(self):
|
||||
async with self._lock:
|
||||
self._checked_out -= 1
|
||||
self._sem.release()
|
||||
|
||||
@property
|
||||
def checked_out(self) -> int:
|
||||
return self._checked_out
|
||||
|
||||
|
||||
class MockConnection:
|
||||
def __init__(self, pool: MockPool):
|
||||
self.pool = pool
|
||||
|
||||
async def fetch(self, query: str, params: List[Any]) -> List[Dict[str, Any]]:
|
||||
await asyncio.sleep(0) # simulate IO
|
||||
if query.startswith("SELECT COUNT"):
|
||||
return [{"count": len(MockDB.users)}]
|
||||
if "FROM users WHERE id =" in query:
|
||||
uid = params[0]
|
||||
user = MockDB.users.get(uid)
|
||||
return [user] if user else []
|
||||
if "FROM users ORDER BY id LIMIT" in query:
|
||||
limit, offset = params
|
||||
sorted_users = [MockDB.users[i] for i in sorted(MockDB.users)]
|
||||
return sorted_users[offset: offset + limit]
|
||||
if "FROM posts WHERE user_id =" in query:
|
||||
uid = params[0]
|
||||
return MockDB.posts.get(uid, [])
|
||||
raise ValueError(f"Unknown fetch query: {query}")
|
||||
|
||||
async def execute(self, query: str, params: List[Any]) -> int:
|
||||
await asyncio.sleep(0)
|
||||
if query.startswith("UPDATE users SET label"):
|
||||
label, uid = params
|
||||
if uid not in MockDB.users:
|
||||
raise KeyError(f"User {uid} not found")
|
||||
MockDB.users[uid]["label"] = label
|
||||
return 1
|
||||
raise ValueError(f"Unknown execute query: {query}")
|
||||
|
||||
async def release(self):
|
||||
await self.pool._release()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection(pool: MockPool):
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
await conn.release()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Service API
|
||||
# ----------------------------------------------------------------------
|
||||
class UserService:
|
||||
def __init__(self, pool: MockPool):
|
||||
self.pool = pool
|
||||
|
||||
async def get_users(self, page: int, page_size: int) -> dict:
|
||||
if page < 1:
|
||||
raise ValueError("page >= 1 required")
|
||||
if not 1 <= page_size <= 100:
|
||||
raise ValueError("1 <= page_size <= 100 required")
|
||||
offset = (page - 1) * page_size
|
||||
async with get_connection(self.pool) as conn:
|
||||
total_row = await conn.fetch("SELECT COUNT(*) FROM users", [])
|
||||
items = await conn.fetch(
|
||||
"SELECT id, name, label FROM users ORDER BY id LIMIT $1 OFFSET $2",
|
||||
[page_size, offset],
|
||||
)
|
||||
return {"items": items, "page": page, "page_size": page_size, "total": total_row[0]["count"]}
|
||||
|
||||
async def get_user_with_posts(self, user_id: int) -> dict:
|
||||
async with get_connection(self.pool) as conn:
|
||||
user_rows = await conn.fetch(
|
||||
"SELECT id, name, label FROM users WHERE id = $1", [user_id]
|
||||
)
|
||||
if not user_rows:
|
||||
raise KeyError(f"User {user_id} not found")
|
||||
user = user_rows[0]
|
||||
posts = await conn.fetch(
|
||||
"SELECT id, title FROM posts WHERE user_id = $1", [user_id]
|
||||
)
|
||||
return {"user": user, "posts": posts}
|
||||
|
||||
async def relabel_users(self, pairs: List[Tuple[int, str]]) -> int:
|
||||
if not pairs:
|
||||
return 0
|
||||
async with get_connection(self.pool) as conn:
|
||||
# snapshot for rollback
|
||||
snapshot = {}
|
||||
for uid, _ in pairs:
|
||||
if uid in MockDB.users:
|
||||
snapshot[uid] = MockDB.users[uid]["label"]
|
||||
try:
|
||||
for uid, label in pairs:
|
||||
await conn.execute(
|
||||
"UPDATE users SET label = $1 WHERE id = $2", [label, uid]
|
||||
)
|
||||
return len(pairs)
|
||||
except Exception:
|
||||
# rollback to original labels
|
||||
for uid, orig in snapshot.items():
|
||||
MockDB.users[uid]["label"] = orig
|
||||
return 0
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Tests
|
||||
# ----------------------------------------------------------------------
|
||||
async def run_tests():
|
||||
pool = MockPool(max_size=5, acquire_timeout=2.0)
|
||||
service = UserService(pool)
|
||||
|
||||
# a) no leak over 100 calls
|
||||
for _ in range(100):
|
||||
await service.get_users(page=1, page_size=10)
|
||||
assert pool.checked_out == 0, "leak detected"
|
||||
print("✓ a) 100 calls, pool checked_out = 0")
|
||||
|
||||
# b) pagination correctness
|
||||
res1 = await service.get_users(page=1, page_size=10)
|
||||
assert res1["total"] == 25
|
||||
assert len(res1["items"]) == 10
|
||||
res3 = await service.get_users(page=3, page_size=10)
|
||||
assert len(res3["items"]) == 5 # remainder
|
||||
res4 = await service.get_users(page=4, page_size=10)
|
||||
assert len(res4["items"]) == 0
|
||||
print("✓ b) pagination math correct")
|
||||
|
||||
# c) transactional bulk rollback
|
||||
original = {uid: MockDB.users[uid]["label"] for uid in (1, 2, 3)}
|
||||
pairs = [(1, "new1"), (2, "new2"), (999, "bad")]
|
||||
result = await service.relabel_users(pairs)
|
||||
assert result == 0
|
||||
for uid in (1, 2):
|
||||
assert MockDB.users[uid]["label"] == original[uid], "rollback failed"
|
||||
print("✓ c) relabel_users rolls back on bad id")
|
||||
|
||||
# d) concurrent access without exhaustion
|
||||
tasks = [service.get_user_with_posts(uid) for uid in range(1, 11)]
|
||||
results = await asyncio.gather(*tasks)
|
||||
assert len(results) == 10
|
||||
assert pool.checked_out == 0
|
||||
print("✓ d) 10 concurrent get_user_with_posts, no exhaustion")
|
||||
|
||||
# validation & missing user
|
||||
try:
|
||||
await service.get_users(page=0, page_size=10)
|
||||
assert False
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
await service.get_user_with_posts(9999)
|
||||
assert False
|
||||
except KeyError:
|
||||
pass
|
||||
print("✓ validation & missing user errors")
|
||||
|
||||
# injection safety demo - params are never interpolated
|
||||
await service.relabel_users([(1, "label'; DROP TABLE users;--")])
|
||||
assert MockDB.users[1]["label"] == "label'; DROP TABLE users;--"
|
||||
print("✓ parameterized queries prevent injection")
|
||||
|
||||
print("\nAll tests passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_tests())
|
||||
@@ -0,0 +1,270 @@
|
||||
# lfu_cache.py
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, Optional, Dict, Set
|
||||
|
||||
class _Node:
|
||||
__slots__ = ("key", "value", "expire_at", "freq", "prev", "next")
|
||||
def __init__(self, key=None, value=None, expire_at=None, freq=0):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.expire_at = expire_at
|
||||
self.freq = freq
|
||||
self.prev = None
|
||||
self.next = None
|
||||
|
||||
|
||||
class _FreqList:
|
||||
__slots__ = ("head", "tail")
|
||||
def __init__(self):
|
||||
self.head = _Node()
|
||||
self.tail = _Node()
|
||||
self.head.next = self.tail
|
||||
self.tail.prev = self.head
|
||||
|
||||
|
||||
class LFUCache:
|
||||
def __init__(self, capacity: int, evict_interval: float = 0.5):
|
||||
self.capacity = capacity
|
||||
self.key_map: Dict[Any, _Node] = {}
|
||||
self.freq_map: Dict[int, _FreqList] = {}
|
||||
self.min_freq = 0
|
||||
self.lock = asyncio.Lock()
|
||||
self._evictor_task: Optional[asyncio.Task] = None
|
||||
self._evict_interval = evict_interval
|
||||
|
||||
# ---------- internal helpers ----------
|
||||
def _update_min_freq(self):
|
||||
while self.min_freq not in self.freq_map:
|
||||
self.min_freq += 1
|
||||
|
||||
def _add_node_to_head(self, freq: int, node: _Node):
|
||||
lst = self.freq_map.get(freq)
|
||||
if lst is None:
|
||||
lst = _FreqList()
|
||||
self.freq_map[freq] = lst
|
||||
node.prev = lst.head
|
||||
node.next = lst.head.next
|
||||
lst.head.next.prev = node
|
||||
lst.head.next = node
|
||||
|
||||
def _remove_node(self, node: _Node):
|
||||
freq = node.freq
|
||||
lst = self.freq_map.get(freq)
|
||||
if lst:
|
||||
node.prev.next = node.next
|
||||
node.next.prev = node.prev
|
||||
if lst.head.next is lst.tail:
|
||||
del self.freq_map[freq]
|
||||
if self.min_freq == freq:
|
||||
self._update_min_freq()
|
||||
if self.key_map.get(node.key) is node:
|
||||
del self.key_map[node.key]
|
||||
|
||||
def _increment_freq(self, node: _Node):
|
||||
old_freq = node.freq
|
||||
lst = self.freq_map[old_freq]
|
||||
node.prev.next = node.next
|
||||
node.next.prev = node.prev
|
||||
if lst.head.next is lst.tail:
|
||||
del self.freq_map[old_freq]
|
||||
if self.min_freq == old_freq:
|
||||
self._update_min_freq()
|
||||
new_freq = old_freq + 1
|
||||
node.freq = new_freq
|
||||
self._add_node_to_head(new_freq, node)
|
||||
|
||||
def _evict_one(self):
|
||||
if not self.freq_map:
|
||||
return
|
||||
if self.min_freq not in self.freq_map:
|
||||
self._update_min_freq()
|
||||
lst = self.freq_map[self.min_freq]
|
||||
node = lst.tail.prev
|
||||
if node is lst.head:
|
||||
return
|
||||
self._remove_node(node)
|
||||
|
||||
def _is_expired(self, node: _Node) -> bool:
|
||||
return node.expire_at is not None and time.monotonic() > node.expire_at
|
||||
|
||||
# ---------- public API ----------
|
||||
async def get(self, key: Any) -> Optional[Any]:
|
||||
async with self.lock:
|
||||
node = self.key_map.get(key)
|
||||
if not node:
|
||||
return None
|
||||
if self._is_expired(node):
|
||||
self._remove_node(node)
|
||||
return None
|
||||
self._increment_freq(node)
|
||||
return node.value
|
||||
|
||||
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
|
||||
async with self.lock:
|
||||
now = time.monotonic()
|
||||
expire_at = now + ttl_seconds if ttl_seconds is not None else None
|
||||
node = self.key_map.get(key)
|
||||
if node:
|
||||
if self._is_expired(node):
|
||||
self._remove_node(node)
|
||||
node = None
|
||||
if node:
|
||||
node.value = value
|
||||
node.expire_at = expire_at
|
||||
self._increment_freq(node)
|
||||
return
|
||||
if len(self.key_map) >= self.capacity:
|
||||
self._evict_one()
|
||||
node = _Node(key=key, value=value, expire_at=expire_at, freq=1)
|
||||
self.key_map[key] = node
|
||||
self._add_node_to_head(1, node)
|
||||
if self.min_freq == 0 or 1 < self.min_freq:
|
||||
self.min_freq = 1
|
||||
|
||||
def begin_transaction(self) -> "Transaction":
|
||||
return Transaction(self)
|
||||
|
||||
async def start_evictor(self):
|
||||
if self._evictor_task and not self._evictor_task.done():
|
||||
return
|
||||
self._evictor_task = asyncio.create_task(self._evictor_loop())
|
||||
|
||||
async def stop_evictor(self):
|
||||
if self._evictor_task:
|
||||
self._evictor_task.cancel()
|
||||
try:
|
||||
await self._evictor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._evictor_task = None
|
||||
|
||||
async def _evictor_loop(self):
|
||||
while True:
|
||||
await asyncio.sleep(self._evict_interval)
|
||||
async with self.lock:
|
||||
now = time.monotonic()
|
||||
batch = 100
|
||||
count = 0
|
||||
for k in list(self.key_map.keys()):
|
||||
if count >= batch:
|
||||
break
|
||||
node = self.key_map.get(k)
|
||||
if node and node.expire_at and now > node.expire_at:
|
||||
self._remove_node(node)
|
||||
count += 1
|
||||
|
||||
|
||||
class Transaction:
|
||||
def __init__(self, cache: LFUCache):
|
||||
self.cache = cache
|
||||
self.pending_puts: Dict[Any, tuple[Any, Optional[float]]] = {}
|
||||
self.pending_deletes: Set[Any] = set()
|
||||
|
||||
async def get(self, key: Any) -> Optional[Any]:
|
||||
if key in self.pending_deletes:
|
||||
return None
|
||||
if key in self.pending_puts:
|
||||
val, _ = self.pending_puts[key]
|
||||
return val
|
||||
async with self.cache.lock:
|
||||
node = self.cache.key_map.get(key)
|
||||
if not node:
|
||||
return None
|
||||
if node.expire_at and time.monotonic() > node.expire_at:
|
||||
self.cache._remove_node(node)
|
||||
return None
|
||||
return node.value
|
||||
|
||||
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
|
||||
self.pending_puts[key] = (value, ttl_seconds)
|
||||
self.pending_deletes.discard(key)
|
||||
|
||||
async def delete(self, key: Any):
|
||||
self.pending_deletes.add(key)
|
||||
self.pending_puts.pop(key, None)
|
||||
|
||||
async def commit(self):
|
||||
async with self.cache.lock:
|
||||
for key in self.pending_deletes:
|
||||
node = self.cache.key_map.get(key)
|
||||
if node:
|
||||
self.cache._remove_node(node)
|
||||
for key, (value, ttl_seconds) in self.pending_puts.items():
|
||||
expire_at = None
|
||||
if ttl_seconds is not None:
|
||||
expire_at = time.monotonic() + ttl_seconds
|
||||
node = self.cache.key_map.get(key)
|
||||
if node:
|
||||
node.value = value
|
||||
node.expire_at = expire_at
|
||||
self.cache._increment_freq(node)
|
||||
else:
|
||||
if len(self.cache.key_map) >= self.cache.capacity:
|
||||
self.cache._evict_one()
|
||||
node = _Node(key=key, value=value, expire_at=expire_at, freq=1)
|
||||
self.cache.key_map[key] = node
|
||||
self.cache._add_node_to_head(1, node)
|
||||
if self.cache.min_freq == 0 or 1 < self.cache.min_freq:
|
||||
self.cache.min_freq = 1
|
||||
self.pending_puts.clear()
|
||||
self.pending_deletes.clear()
|
||||
|
||||
def rollback(self):
|
||||
self.pending_puts.clear()
|
||||
self.pending_deletes.clear()
|
||||
|
||||
|
||||
# ------------------- tests -------------------
|
||||
async def main():
|
||||
cache = LFUCache(capacity=3, evict_interval=0.2)
|
||||
await cache.start_evictor()
|
||||
print("=== LFU eviction order ===")
|
||||
await cache.put("a", 1)
|
||||
await cache.put("b", 2)
|
||||
await cache.put("c", 3)
|
||||
await cache.get("a")
|
||||
await cache.get("a")
|
||||
await cache.get("b")
|
||||
await cache.put("d", 4) # should evict c
|
||||
assert await cache.get("c") is None
|
||||
assert await cache.get("a") == 1
|
||||
print("LFU eviction OK")
|
||||
|
||||
print("=== TTL lazy vs background ===")
|
||||
await cache.put("x", "temp", ttl_seconds=0.1)
|
||||
await asyncio.sleep(0.15)
|
||||
assert await cache.get("x") is None
|
||||
await cache.put("y", "temp2", ttl_seconds=0.1)
|
||||
await asyncio.sleep(0.3)
|
||||
assert await cache.get("y") is None
|
||||
print("TTL OK")
|
||||
|
||||
print("=== Transaction commit / rollback ===")
|
||||
tx = cache.begin_transaction()
|
||||
await tx.put("t1", 100)
|
||||
assert await cache.get("t1") is None
|
||||
await tx.commit()
|
||||
assert await cache.get("t1") == 100
|
||||
|
||||
tx2 = cache.begin_transaction()
|
||||
await tx2.put("t2", 200)
|
||||
await tx2.rollback()
|
||||
assert await cache.get("t2") is None
|
||||
print("Transaction OK")
|
||||
|
||||
print("=== Stress test 50 concurrent tasks ===")
|
||||
async def worker(i):
|
||||
for j in range(20):
|
||||
k = f"k{i%10}"
|
||||
await cache.put(k, i*j, ttl_seconds=5)
|
||||
await cache.get(k)
|
||||
await asyncio.gather(*(worker(i) for i in range(50)))
|
||||
print("Stress OK")
|
||||
|
||||
await cache.stop_evictor()
|
||||
print("All tests passed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// mcp-jsonplaceholder.ts
|
||||
// Run: npx tsx mcp-jsonplaceholder.ts
|
||||
// Test: npx tsx mcp-jsonplaceholder.ts --test
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const BASE = 'https://jsonplaceholder.typicode.com';
|
||||
|
||||
// ---------- Validation schemas ----------
|
||||
const GetUserSchema = z.object({
|
||||
id: z.number().int().positive()
|
||||
});
|
||||
|
||||
const ListPostsSchema = z.object({
|
||||
user_id: z.number().int().positive(),
|
||||
limit: z.number().int().min(1).max(100).optional().default(10)
|
||||
});
|
||||
|
||||
const SearchPostsSchema = z.object({
|
||||
query: z.string().min(1).max(200)
|
||||
});
|
||||
|
||||
// ---------- HTTP helper with timeout ----------
|
||||
async function fetchJson(url: string, timeoutMs = 8000) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(timer);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
||||
const data = await res.json();
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
clearTimeout(timer);
|
||||
if (err.name === 'AbortError') throw new Error('Request timed out');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Typed implementations ----------
|
||||
async function getUserImpl(id: number) {
|
||||
const user = await fetchJson(`${BASE}/users/${id}`);
|
||||
if (!user || !user.id) throw new Error(`User ${id} not found`);
|
||||
return user;
|
||||
}
|
||||
|
||||
async function listPostsImpl(user_id: number, limit: number) {
|
||||
const posts = await fetchJson(`${BASE}/posts?userId=${user_id}`);
|
||||
if (!Array.isArray(posts)) throw new Error('Invalid posts response');
|
||||
return posts.slice(0, limit).map(p => ({ id: p.id, userId: p.userId, title: p.title, body: p.body }));
|
||||
}
|
||||
|
||||
async function searchPostsImpl(query: string) {
|
||||
const posts = await fetchJson(`${BASE}/posts`);
|
||||
const q = query.toLowerCase();
|
||||
const filtered = posts.filter((p: any) => p.title.toLowerCase().includes(q));
|
||||
return filtered.map((p: any) => ({ id: p.id, userId: p.userId, title: p.title }));
|
||||
}
|
||||
|
||||
// ---------- MCP Server ----------
|
||||
const server = new Server({ name: 'jsonplaceholder-mcp', version: '1.0.0' }, {
|
||||
capabilities: { tools: {} }
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'get_user',
|
||||
description: 'Fetch a single user by numeric id',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { id: { type: 'integer', minimum: 1 } },
|
||||
required: ['id'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'list_posts_by_user',
|
||||
description: 'Fetch posts for a user with optional limit',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user_id: { type: 'integer', minimum: 1 },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100 }
|
||||
},
|
||||
required: ['user_id'],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_posts',
|
||||
description: 'Search posts by title substring, case-insensitive',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { query: { type: 'string', minLength: 1, maxLength: 200 } },
|
||||
required: ['query'],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
try {
|
||||
switch (name) {
|
||||
case 'get_user': {
|
||||
const { id } = GetUserSchema.parse(args);
|
||||
const user = await getUserImpl(id);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(user) }] };
|
||||
}
|
||||
case 'list_posts_by_user': {
|
||||
const { user_id, limit } = ListPostsSchema.parse(args);
|
||||
const posts = await listPostsImpl(user_id, limit);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(posts) }] };
|
||||
}
|
||||
case 'search_posts': {
|
||||
const { query } = SearchPostsSchema.parse(args);
|
||||
const results = await searchPostsImpl(query);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool ${name}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function startServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('JSONPlaceholder MCP server running on stdio');
|
||||
}
|
||||
|
||||
// ---------- Tests ----------
|
||||
async function runTests() {
|
||||
console.log('=== MCP JSONPlaceholder Tests ===');
|
||||
// a) Happy path
|
||||
try {
|
||||
const user = await getUserImpl(1);
|
||||
console.log('a) get_user(1) ->', user.id === 1 && !!user.name ? 'PASS' : 'FAIL', user.name);
|
||||
} catch (e: any) {
|
||||
console.log('a) FAIL', e.message);
|
||||
}
|
||||
|
||||
// b) 404 / not-found handled
|
||||
try {
|
||||
await getUserImpl(9999);
|
||||
console.log('b) 404 handled -> PASS (empty)');
|
||||
} catch (e: any) {
|
||||
console.log('b) 404 handled -> PASS', e.message);
|
||||
}
|
||||
|
||||
// c) Validation rejects malformed input
|
||||
try { GetUserSchema.parse({ id: -5 }); console.log('c1) FAIL'); }
|
||||
catch { console.log('c1) get_user(-5) rejected -> PASS'); }
|
||||
|
||||
try { ListPostsSchema.parse({ user_id: 'x' }); console.log('c2) FAIL'); }
|
||||
catch { console.log('c2) list_posts_by_user("x") rejected -> PASS'); }
|
||||
|
||||
try { SearchPostsSchema.parse({ query: '' }); console.log('c3) FAIL'); }
|
||||
catch { console.log('c3) search_posts("") rejected -> PASS'); }
|
||||
|
||||
console.log('Tests done');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (process.argv.includes('--test')) {
|
||||
runTests();
|
||||
} else {
|
||||
startServer();
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// Cargo.toml
|
||||
// tokio = { version = "1", features = ["full"] }
|
||||
// tokio-util = "0.7"
|
||||
// serde = { version = "1", features = ["derive"] }
|
||||
// thiserror = "1"
|
||||
// rand = "0.9"
|
||||
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum FetchError {
|
||||
#[error("transient")]
|
||||
Transient,
|
||||
#[error("always fail")]
|
||||
AlwaysFail,
|
||||
}
|
||||
|
||||
async fn mock_fetch(id: u32) -> Result<Vec<String>, FetchError> {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
if id == 999 {
|
||||
return Err(FetchError::AlwaysFail);
|
||||
}
|
||||
let mut rng = rand::rng();
|
||||
if rng.random_bool(0.15) {
|
||||
Err(FetchError::Transient)
|
||||
} else {
|
||||
Ok((0..rng.random_range(0..4))
|
||||
.map(|i| format!("item-{}-{}", id, i))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct Item {
|
||||
watcher_id: u32,
|
||||
items: Vec<String>,
|
||||
ts: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Default, Clone)]
|
||||
struct Aggregated {
|
||||
items: Vec<Item>,
|
||||
total_count: usize,
|
||||
}
|
||||
|
||||
struct WatcherMeta {
|
||||
token: CancellationToken,
|
||||
handle: JoinHandle<()>,
|
||||
consecutive_failures: Arc<Mutex<u32>>,
|
||||
healthy: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
pub struct WatcherManager {
|
||||
pub(crate) tx: mpsc::Sender<Item>,
|
||||
pub(crate) aggregated: Arc<Mutex<Aggregated>>,
|
||||
pub(crate) watchers: Arc<Mutex<HashMap<u32, WatcherMeta>>>,
|
||||
pub(crate) next_id: Arc<Mutex<u32>>,
|
||||
pub(crate) shutdown_token: CancellationToken,
|
||||
pub(crate) consumer_handle: JoinHandle<Aggregated>,
|
||||
}
|
||||
|
||||
impl WatcherManager {
|
||||
pub async fn new(num_watchers: usize) -> Self {
|
||||
let (tx, rx) = mpsc::channel(32); // bounded -> backpressure
|
||||
let aggregated = Arc::new(Mutex::new(Aggregated::default()));
|
||||
let shutdown_token = CancellationToken::new();
|
||||
let watchers = Arc::new(Mutex::new(HashMap::new()));
|
||||
let next_id = Arc::new(Mutex::new(1));
|
||||
let consumer_handle = tokio::spawn(consumer_task(rx, shutdown_token.clone(), aggregated.clone()));
|
||||
|
||||
let manager = WatcherManager {
|
||||
tx,
|
||||
aggregated,
|
||||
watchers,
|
||||
next_id,
|
||||
shutdown_token,
|
||||
consumer_handle,
|
||||
};
|
||||
|
||||
for _ in 0..num_watchers {
|
||||
manager.add_watcher().await;
|
||||
}
|
||||
manager
|
||||
}
|
||||
|
||||
pub async fn add_watcher(&self) -> u32 {
|
||||
let mut id_guard = self.next_id.lock().await;
|
||||
let id = *id_guard;
|
||||
*id_guard += 1;
|
||||
drop(id_guard);
|
||||
self.add_watcher_with_id(id).await
|
||||
}
|
||||
|
||||
pub async fn add_watcher_with_id(&self, id: u32) -> u32 {
|
||||
let token = CancellationToken::new();
|
||||
let tx = self.tx.clone();
|
||||
let shutdown_token = self.shutdown_token.clone();
|
||||
let consecutive_failures = Arc::new(Mutex::new(0u32));
|
||||
let healthy = Arc::new(Mutex::new(true));
|
||||
|
||||
let handle = tokio::spawn(watcher_task(
|
||||
id,
|
||||
token.clone(),
|
||||
shutdown_token,
|
||||
tx,
|
||||
consecutive_failures.clone(),
|
||||
healthy.clone(),
|
||||
));
|
||||
|
||||
let meta = WatcherMeta {
|
||||
token,
|
||||
handle,
|
||||
consecutive_failures,
|
||||
healthy,
|
||||
};
|
||||
self.watchers.lock().await.insert(id, meta);
|
||||
id
|
||||
}
|
||||
|
||||
pub async fn remove_watcher(&self, id: u32) -> Result<(), String> {
|
||||
let mut watchers = self.watchers.lock().await;
|
||||
if let Some(meta) = watchers.remove(&id) {
|
||||
meta.token.cancel();
|
||||
drop(watchers);
|
||||
let _ = meta.handle.await;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) -> Aggregated {
|
||||
self.shutdown_token.cancel();
|
||||
let mut watchers = self.watchers.lock().await;
|
||||
let mut handles = Vec::new();
|
||||
for (_, meta) in watchers.drain() {
|
||||
meta.token.cancel();
|
||||
handles.push(meta.handle);
|
||||
}
|
||||
drop(watchers);
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
self.consumer_handle.await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
async fn watcher_task(
|
||||
id: u32,
|
||||
token: CancellationToken,
|
||||
shutdown_token: CancellationToken,
|
||||
tx: mpsc::Sender<Item>,
|
||||
consecutive_failures: Arc<Mutex<u32>>,
|
||||
healthy: Arc<Mutex<bool>>,
|
||||
) {
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(100));
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = token.cancelled() => break,
|
||||
_ = shutdown_token.cancelled() => break,
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
match mock_fetch(id).await {
|
||||
Ok(items) => {
|
||||
*consecutive_failures.lock().await = 0;
|
||||
*healthy.lock().await = true;
|
||||
if !items.is_empty() {
|
||||
let item = Item {
|
||||
watcher_id: id,
|
||||
items,
|
||||
ts: tokio::time::Instant::now().elapsed().as_millis() as u64,
|
||||
};
|
||||
// backpressure: await if channel full
|
||||
if tx.send(item).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let mut fails = consecutive_failures.lock().await;
|
||||
*fails += 1;
|
||||
if *fails > 5 {
|
||||
*healthy.lock().await = false;
|
||||
}
|
||||
eprintln!("watcher {} error: {:?}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn consumer_task(
|
||||
mut rx: mpsc::Receiver<Item>,
|
||||
shutdown_token: CancellationToken,
|
||||
aggregated: Arc<Mutex<Aggregated>>,
|
||||
) -> Aggregated {
|
||||
let mut agg = Aggregated::default();
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_token.cancelled() => break,
|
||||
maybe_item = rx.recv() => {
|
||||
match maybe_item {
|
||||
Some(item) => {
|
||||
agg.items.push(item);
|
||||
agg.total_count += 1;
|
||||
*aggregated.lock().await = agg.clone();
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
agg
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_manager_runs_and_shuts_down() {
|
||||
let mut manager = WatcherManager::new(4).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
let agg = manager.shutdown().await;
|
||||
assert!(agg.total_count > 0, "consumer should have received items");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unhealthy_watcher() {
|
||||
let mut manager = WatcherManager::new(0).await;
|
||||
let id = manager.add_watcher_with_id(999).await;
|
||||
tokio::time::sleep(Duration::from_millis(800)).await;
|
||||
let watchers = manager.watchers.lock().await;
|
||||
let meta = watchers.get(&id).expect("watcher exists");
|
||||
let healthy = *meta.healthy.lock().await;
|
||||
assert!(!healthy, "watcher should be marked unhealthy after >5 failures");
|
||||
drop(watchers);
|
||||
let _ = manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_remove_concurrent() {
|
||||
use std::sync::Arc;
|
||||
let manager = Arc::new(Mutex::new(WatcherManager::new(0).await));
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..20 {
|
||||
let m = manager.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let mut mgr = m.lock().await;
|
||||
let id = mgr.add_watcher().await;
|
||||
drop(mgr);
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let mut mgr = m.lock().await;
|
||||
let _ = mgr.remove_watcher(id).await;
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.await.unwrap();
|
||||
}
|
||||
let mgr = manager.lock().await;
|
||||
assert_eq!(mgr.watchers.lock().await.len(), 0);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
# tts_pipeline.py
|
||||
import asyncio
|
||||
import random
|
||||
import uuid
|
||||
from collections import deque
|
||||
|
||||
# ---------- mock synthesizer ----------
|
||||
_global_active = 0
|
||||
_global_max = 0
|
||||
_global_active_lock = asyncio.Lock()
|
||||
|
||||
async def mock_synthesize(text: str) -> bytes:
|
||||
global _global_active, _global_max
|
||||
async with _global_active_lock:
|
||||
_global_active += 1
|
||||
if _global_active > _global_max:
|
||||
_global_max = _global_active
|
||||
try:
|
||||
await asyncio.sleep(random.uniform(0.05, 0.30))
|
||||
if random.random() < 0.10:
|
||||
raise RuntimeError("synthesis failed")
|
||||
return b"\x00" * len(text)
|
||||
finally:
|
||||
async with _global_active_lock:
|
||||
_global_active -= 1
|
||||
|
||||
|
||||
# ---------- pipeline ----------
|
||||
class TTSPipeline:
|
||||
def __init__(self):
|
||||
self._queue = deque()
|
||||
self._queue_lock = asyncio.Lock()
|
||||
self._queue_not_empty = asyncio.Condition(self._queue_lock)
|
||||
|
||||
self._jobs_lock = asyncio.Lock()
|
||||
self._jobs = {} # id -> meta
|
||||
self._active_jobs = set()
|
||||
|
||||
self._callbacks = []
|
||||
self._workers = []
|
||||
|
||||
def register_callback(self, cb):
|
||||
self._callbacks.append(cb)
|
||||
|
||||
async def _emit(self, event, job_id, **kw):
|
||||
for cb in self._callbacks:
|
||||
try:
|
||||
cb(event, job_id, **kw)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def start(self):
|
||||
for _ in range(4):
|
||||
self._workers.append(asyncio.create_task(self._worker()))
|
||||
|
||||
async def _worker(self):
|
||||
while True:
|
||||
async with self._queue_lock:
|
||||
while not self._queue:
|
||||
await self._queue_not_empty.wait()
|
||||
job = self._queue.popleft()
|
||||
|
||||
job_id = job["id"]
|
||||
async with self._jobs_lock:
|
||||
meta = self._jobs.get(job_id)
|
||||
if meta and meta.get("cancelled"):
|
||||
await self._emit("cancelled", job_id)
|
||||
continue
|
||||
self._active_jobs.add(job_id)
|
||||
try:
|
||||
await self._process_job(job)
|
||||
finally:
|
||||
async with self._jobs_lock:
|
||||
self._active_jobs.discard(job_id)
|
||||
|
||||
async def _process_job(self, job):
|
||||
job_id = job["id"]
|
||||
async with self._jobs_lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id]["status"] = "started"
|
||||
await self._emit("started", job_id)
|
||||
|
||||
backoff = 0.1
|
||||
for attempt in range(1, 4):
|
||||
async with self._jobs_lock:
|
||||
meta = self._jobs.get(job_id)
|
||||
if not meta or meta.get("cancelled"):
|
||||
await self._emit("cancelled", job_id)
|
||||
return
|
||||
try:
|
||||
await mock_synthesize(job["text"])
|
||||
await self._emit("completed", job_id)
|
||||
async with self._jobs_lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id]["status"] = "completed"
|
||||
return
|
||||
except asyncio.CancelledError:
|
||||
await self._emit("cancelled", job_id)
|
||||
raise
|
||||
except Exception as e:
|
||||
async with self._jobs_lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id]["attempts"] = attempt
|
||||
if attempt >= 3:
|
||||
await self._emit("failed", job_id, error=e)
|
||||
async with self._jobs_lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id]["status"] = "failed"
|
||||
return
|
||||
await asyncio.sleep(backoff)
|
||||
backoff *= 2
|
||||
|
||||
async def submit(self, text: str, voice: str) -> str:
|
||||
job_id = uuid.uuid4().hex
|
||||
async with self._jobs_lock:
|
||||
self._jobs[job_id] = {
|
||||
"text": text, "voice": voice,
|
||||
"cancelled": False, "status": "queued", "attempts": 0
|
||||
}
|
||||
async with self._queue_lock:
|
||||
if len(self._queue) >= 100:
|
||||
async with self._jobs_lock:
|
||||
self._jobs.pop(job_id, None)
|
||||
raise RuntimeError("Backpressure: queue full")
|
||||
self._queue.append({"id": job_id, "text": text, "voice": voice})
|
||||
self._queue_not_empty.notify()
|
||||
await self._emit("queued", job_id)
|
||||
return job_id
|
||||
|
||||
async def cancel(self, job_id: str) -> bool:
|
||||
async with self._jobs_lock:
|
||||
meta = self._jobs.get(job_id)
|
||||
if not meta:
|
||||
return False
|
||||
if meta["status"] in ("completed", "failed", "cancelled"):
|
||||
return False
|
||||
meta["cancelled"] = True
|
||||
|
||||
removed = False
|
||||
async with self._queue_lock:
|
||||
for i, j in enumerate(self._queue):
|
||||
if j["id"] == job_id:
|
||||
del self._queue[i]
|
||||
removed = True
|
||||
break
|
||||
if removed:
|
||||
await self._emit("cancelled", job_id)
|
||||
return True
|
||||
|
||||
async def drain(self):
|
||||
while True:
|
||||
async with self._queue_lock:
|
||||
q_empty = len(self._queue) == 0
|
||||
async with self._jobs_lock:
|
||||
active_empty = len(self._active_jobs) == 0
|
||||
if q_empty and active_empty:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
|
||||
# ---------- tests ----------
|
||||
async def main():
|
||||
global _global_active, _global_max
|
||||
_global_active = 0
|
||||
_global_max = 0
|
||||
|
||||
# a) concurrency limit
|
||||
pipeline = TTSPipeline()
|
||||
await pipeline.start()
|
||||
ids = [await pipeline.submit(f"text {i}", "v1") for i in range(50)]
|
||||
await pipeline.drain()
|
||||
print("max concurrency", _global_max)
|
||||
assert _global_max <= 4, f"max concurrency {_global_max} > 4"
|
||||
|
||||
# b) backpressure
|
||||
_global_active = 0; _global_max = 0
|
||||
pipeline2 = TTSPipeline()
|
||||
await pipeline2.start()
|
||||
for i in range(100):
|
||||
await pipeline2.submit(f"t{i}", "v")
|
||||
try:
|
||||
await pipeline2.submit("overflow", "v")
|
||||
assert False, "should have raised"
|
||||
except RuntimeError as e:
|
||||
print("backpressure works:", e)
|
||||
await pipeline2.drain()
|
||||
|
||||
# c) cancel mid-flight and reuse
|
||||
_global_active = 0; _global_max = 0
|
||||
pipeline3 = TTSPipeline()
|
||||
await pipeline3.start()
|
||||
events3 = []
|
||||
pipeline3.register_callback(lambda e, jid, **kw: events3.append((e, jid)))
|
||||
ids3 = [await pipeline3.submit(f"t{i}", "v") for i in range(20)]
|
||||
await asyncio.sleep(0.05)
|
||||
cancel_id = ids3[5]
|
||||
await pipeline3.cancel(cancel_id)
|
||||
await pipeline3.drain()
|
||||
|
||||
queued = [e for e in events3 if e[0] == "queued"]
|
||||
completed = [e for e in events3 if e[0] == "completed"]
|
||||
failed = [e for e in events3 if e[0] == "failed"]
|
||||
cancelled = [e for e in events3 if e[0] == "cancelled"]
|
||||
print("queued", len(queued), "finished", len(completed)+len(failed)+len(cancelled))
|
||||
assert len(queued) == 20
|
||||
assert len(completed) + len(failed) + len(cancelled) == 20
|
||||
assert any(e[1] == cancel_id and e[0] == "cancelled" for e in events3)
|
||||
|
||||
# pool still usable
|
||||
new_id = await pipeline3.submit("after cancel", "v")
|
||||
await pipeline3.drain()
|
||||
print("pool still usable")
|
||||
print("All tests passed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,190 @@
|
||||
# webhook_bridge.py
|
||||
# Python 3.11+ stdlib only
|
||||
# Run: python webhook_bridge.py
|
||||
|
||||
import os, sys, json, hmac, hashlib, time, threading, queue, asyncio, random
|
||||
import http.server, http.client
|
||||
|
||||
SECRET = os.getenv('WEBHOOK_SECRET', 'supersecret')
|
||||
|
||||
# ---------- Idempotency ----------
|
||||
event_store = {}
|
||||
store_lock = threading.Lock()
|
||||
|
||||
def evict_stale():
|
||||
now = time.monotonic()
|
||||
with store_lock:
|
||||
stale = [k for k, v in event_store.items() if now - v > 300]
|
||||
for k in stale:
|
||||
del event_store[k]
|
||||
|
||||
# ---------- Rate limiter: token bucket 5/s ----------
|
||||
CAPACITY = 5
|
||||
RATE = 5.0
|
||||
tokens = CAPACITY
|
||||
last_refill = time.monotonic()
|
||||
bucket_lock = threading.Lock()
|
||||
|
||||
def acquire_token():
|
||||
global tokens, last_refill
|
||||
while True:
|
||||
with bucket_lock:
|
||||
now = time.monotonic()
|
||||
elapsed = now - last_refill
|
||||
tokens = min(CAPACITY, tokens + elapsed * RATE)
|
||||
last_refill = now
|
||||
if tokens >= 1:
|
||||
tokens -= 1
|
||||
return
|
||||
wait = (1 - tokens) / RATE
|
||||
time.sleep(wait)
|
||||
|
||||
# ---------- Mock Discord ----------
|
||||
async def discord_send(payload):
|
||||
await asyncio.sleep(0.005)
|
||||
if random.random() < 0.05: # ~5% 429
|
||||
class Resp:
|
||||
status = 429
|
||||
headers = {'Retry-After': '1'}
|
||||
return Resp()
|
||||
class Resp:
|
||||
status = 200
|
||||
headers = {}
|
||||
return Resp()
|
||||
|
||||
# ---------- Forwarding worker ----------
|
||||
forward_queue = queue.Queue()
|
||||
forward_log = [] # {event_id, ts}
|
||||
log_lock = threading.Lock()
|
||||
|
||||
def worker():
|
||||
while True:
|
||||
item = forward_queue.get()
|
||||
if item is None:
|
||||
break
|
||||
event_id, payload = item
|
||||
try:
|
||||
acquire_token()
|
||||
resp = asyncio.run(discord_send(payload))
|
||||
if resp.status == 429:
|
||||
retry = int(resp.headers.get('Retry-After', '1'))
|
||||
print(f"[WARN] 429 for {event_id}, retry after {retry}s", file=sys.stderr)
|
||||
time.sleep(retry)
|
||||
resp = asyncio.run(discord_send(payload))
|
||||
if resp.status == 200:
|
||||
with log_lock:
|
||||
forward_log.append({'event_id': event_id, 'ts': time.monotonic()})
|
||||
else:
|
||||
print(f"[ERROR] Discord forward failed {event_id} status {resp.status}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Discord forward exception {e}", file=sys.stderr)
|
||||
finally:
|
||||
forward_queue.task_done()
|
||||
|
||||
# ---------- HTTP handler ----------
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path != '/webhook':
|
||||
self.send_response(404); self.end_headers(); return
|
||||
|
||||
length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(length)
|
||||
|
||||
sig = self.headers.get('X-Signature')
|
||||
if not sig:
|
||||
self.send_response(401); self.end_headers(); self.wfile.write(b'Missing signature'); return
|
||||
|
||||
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(expected, sig):
|
||||
self.send_response(401); self.end_headers(); self.wfile.write(b'Invalid signature'); return
|
||||
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400); self.end_headers(); self.wfile.write(b'Invalid JSON'); return
|
||||
|
||||
event_id = data.get('event_id')
|
||||
event_type = data.get('type')
|
||||
if not event_id or not event_type:
|
||||
self.send_response(400); self.end_headers(); self.wfile.write(b'Missing fields'); return
|
||||
|
||||
now = time.monotonic()
|
||||
with store_lock:
|
||||
evict_stale()
|
||||
if event_id in event_store and now - event_store[event_id] < 300:
|
||||
self.send_response(200); self.end_headers(); self.wfile.write(b'OK replay'); return
|
||||
event_store[event_id] = now
|
||||
|
||||
payload = {'content': f'Event {event_type} received', 'event_id': event_id}
|
||||
forward_queue.put((event_id, payload))
|
||||
self.send_response(200); self.end_headers(); self.wfile.write(b'OK')
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print(f"{self.client_address[0]} - {fmt%args}")
|
||||
|
||||
# ---------- Tests ----------
|
||||
def make_request(event_id, event_type, secret=SECRET, tamper=False):
|
||||
body = json.dumps({'event_id': event_id, 'type': event_type, 'data': {}}).encode()
|
||||
sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
if tamper:
|
||||
sig = '0'*64
|
||||
conn = http.client.HTTPConnection('localhost', 8000, timeout=5)
|
||||
conn.request('POST', '/webhook', body, {'Content-Type':'application/json','X-Signature':sig})
|
||||
resp = conn.getresponse()
|
||||
resp.read(); conn.close()
|
||||
return resp.status
|
||||
|
||||
def reset_state():
|
||||
with store_lock:
|
||||
event_store.clear()
|
||||
with log_lock:
|
||||
forward_log.clear()
|
||||
|
||||
def run_tests():
|
||||
time.sleep(0.2)
|
||||
print('Test a) correct signature')
|
||||
reset_state()
|
||||
s = make_request('id1','chat')
|
||||
time.sleep(0.1)
|
||||
assert s == 200, f'expected 200 got {s}'
|
||||
assert len(forward_log) == 1, 'should be forwarded once'
|
||||
print(' ok')
|
||||
|
||||
print('Test b) tampered signature')
|
||||
s = make_request('id2','chat', tamper=True)
|
||||
assert s == 401, f'expected 401 got {s}'
|
||||
assert len(forward_log) == 1, 'should not forward'
|
||||
print(' ok')
|
||||
|
||||
print('Test c) replay idempotency')
|
||||
s = make_request('id1','chat')
|
||||
assert s == 200
|
||||
time.sleep(0.05)
|
||||
assert len(forward_log) == 1, 'replay must not increase count'
|
||||
print(' ok')
|
||||
|
||||
print('Test d) rate limiter burst')
|
||||
reset_state()
|
||||
for i in range(10):
|
||||
make_request(f'burst{i}','chat')
|
||||
# wait for worker to drain
|
||||
time.sleep(2.5)
|
||||
assert len(forward_log) == 10, 'all 10 should eventually forward'
|
||||
ts = [e['ts'] for e in forward_log]
|
||||
elapsed = ts[-1] - ts[0]
|
||||
assert elapsed >= 1.5, f'rate limiter not enforced, elapsed {elapsed:.2f}s'
|
||||
print(f' ok, elapsed {elapsed:.2f}s >=1.5s')
|
||||
print('All tests passed.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
worker_thread = threading.Thread(target=worker, daemon=True)
|
||||
worker_thread.start()
|
||||
|
||||
server = http.server.ThreadingHTTPServer(('localhost', 8000), Handler)
|
||||
srv_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
srv_thread.start()
|
||||
|
||||
try:
|
||||
run_tests()
|
||||
finally:
|
||||
server.shutdown()
|
||||
Reference in New Issue
Block a user