
HYBRID MIND · AGENT ENGINEERING · RELIABILITY
Tool หนึ่งตัวค้าง อีกตัวต้องติดอยู่ในคิวเดียวกันด้วยหรือ?
Agent เตรียมวิดีโอต้องสร้างภาพตัวอย่างและอ่านโน้ต หากใช้ทรัพยากรร่วม งานอ่านโน้ตอาจติดหลังตัวสร้างภาพที่ค้างอยู่ บทนี้สอน Bulkhead แยกตาม dependency พร้อมคิวรับงานที่มีขอบเขต ก่อนปล่อยคำขอไปถึง Tool
Dependency คือบริการที่งานพึ่งพา ส่วน Bulkhead แบ่งทรัพยากรเป็นช่องเพื่อลดการลามของปัญหา การเพิ่มจำนวน Agent อย่างเดียวไม่ได้ทำให้ทรัพยากรแยกกัน
Quick Answer
กำหนด worker สูงสุด C และช่องรอ Q > 0 ต่อ dependency ให้ host จับคู่ Tool กับช่องทำงานจากทะเบียนที่กำหนดไว้ แล้วรับงานแบบไม่รอ: คิวเต็มให้ตอบ QUEUE_FULL ก่อนสร้าง Task สำหรับคำขอนั้น งานที่รับแล้วและยังไม่เสร็จจึงมีขอบเขตไม่เกิน C + Q ต่อช่อง ต่อ process ภายใต้สมมติฐานของตัวอย่าง การแยกนี้ไม่รับประกัน CPU, หน่วยความจำ, คำขอต่อวินาที หรือการหยุดงานภายนอก
บทนี้เป็นบทเรียน evergreen จากเอกสารต้นทางที่ตรวจเมื่อ 15 ตุลาคม 2026 โค้ดเป็น AUTHOR MOCK — NOT RUN ไม่มี API จริง ผลที่ยกมาเป็นผลคาดหมาย ส่วนหน้า Lab เป็น simulation และผลทดสอบ UI จริงยัง UNKNOWN
หลังอ่าน ผู้อ่านจะทำได้: เขียนสัญญารับงานสำหรับ Tool สอง dependency ที่ระบุจำนวนงานกำลังทำ งานรอ และเหตุปฏิเสธได้ พร้อมแยกคำว่า “ผู้เรียกเลิกรอ” ออกจาก “งานหยุดแล้ว”
1. จำกัดคนทำงานอย่างเดียว ยังเหลือคนต่อคิว
สมมติรับคำขอทุกชิ้นด้วย create_task() แล้วให้แต่ละ Task ไปถือ Semaphore ภายหลัง Semaphore ช่วยจำกัดผู้ผ่านเข้าไปทำงานพร้อมกัน แต่ Task ที่สร้างไว้รอสิทธิ์ยังเพิ่มตามคำขอได้ ข้อสรุปนี้เป็นการวิเคราะห์จากพฤติกรรม Semaphore และการสร้าง Task ไม่ใช่ผลวัดหน่วยความจำของระบบใด
แม้ใช้ Queue จำกัดขนาด หากเรียก await queue.put(job) จากผู้ส่งไม่จำกัด ผู้ส่งอาจไปยืนรอนอกคิว เราจึงเริ่มจำกัดที่ admission หรือจุดตัดสินใจรับภาระเพิ่ม โดยไม่สร้างห้องรอซ่อนอยู่
Azure Bulkhead pattern รองรับแนวคิดแยก pool ต่อบริการ ตัวอย่างนี้แยก worker เท่านั้น ยังไม่แยก connection pool หรือเครื่องจริง

| กลไก | คำถามที่ควบคุม | สิ่งที่ยังต้องออกแบบเพิ่ม |
|---|---|---|
| Bulkhead กับ bounded admission | dependency นี้ยึดงานพร้อมกันและค้างรอได้กี่ชิ้น | เวลารอสูงสุดและทรัพยากรร่วมจริง |
| Rate limiting | อนุญาตปริมาณงานต่อช่วงเวลาเท่าไร | จำนวนงานที่ยังไม่เสร็จ |
| Circuit Breaker | จากประวัติความล้มเหลวควรหยุดเรียกชั่วคราวหรือไม่ | การแย่ง slot และขนาดคิว |
สองกลไกหลังอ้างอิง Rate Limiting และ Circuit Breaker โดยไม่รวมไว้ในโค้ด ตอนนี้จึงเสริมขอบเขตคิวที่ต่างจาก บท Circuit Breaker
2. สถาปัตยกรรม: มีช่องรับงานสองช่อง ไม่เปิดช่องตามใจโมเดล
ทะเบียนฝั่ง host กำหนด render_preview → render และ read_notes → notes อย่างตายตัว โมเดลเลือกชื่อ Tool ที่รู้จักได้ แต่ตั้งชื่อ dependency ใหม่เพื่อสร้าง worker เพิ่มเองไม่ได้ ตัวอย่างรับเพียงรหัสงานสั้น ๆ ไม่มี prompt, URL, เอกสาร หรืออาร์กิวเมนต์อิสระเข้าไปเก็บในคิว
Tool ID + call ID
↓ host-owned registry
เลือก lane ที่มีอยู่แล้ว
↓ try_submit: รับทันทีหรือปฏิเสธ
คิวรอ Q ช่อง → worker C คน → fixture handler → Outcome
└─ เต็ม: QUEUE_FULL และหยุดตรงนี้
C นับ worker คงที่ที่ถือได้คนละหนึ่งงาน Q นับงานที่ยังอยู่ในคิว งานที่ worker หยิบแล้วออกจากจำนวน Q จึงได้ขอบเขต “งานรับแล้วและยังไม่เสร็จ ≤ C + Q” นี่เป็น invariant หรือเงื่อนไขที่แบบจำลองตั้งใจรักษา ไม่ใช่ค่าความจุที่ทดสอบแล้ว และไม่ได้แปลว่าจะรับได้ครบ C + Q ทุกครั้งที่คำขอระเบิดเข้ามาพร้อมกัน
ถ้า worker ยังไม่ได้โอกาสหยิบงาน คิวอาจเต็มขณะที่ active ยังน้อยกว่า C โค้ดเลือกปฏิเสธทันที ไม่พักรอเพิ่ม ไม่ได้สรุปว่าปลายทางเสีย
render ใช้ C = 1, Q = 2 และ notes ใช้ C = 1, Q = 1 เพื่อเห็นกลไก ไม่ใช่ค่าแนะนำ production notes ว่างก็ไม่ได้ให้ render ยืม worker โดยอัตโนมัติ
3. เดินตัวอย่าง: งานเรนเดอร์รอ แต่งานอ่านโน้ตยังมีช่องของตัวเอง
- ส่ง render-1 แล้วรอ Event ยืนยันว่า handler เริ่มทำงาน จากนั้น handler รอ Event อีกตัวที่เรายังไม่ปล่อย
- ส่ง render-2 และ render-3 เข้าคิวสองตำแหน่ง งาน render-4 ถัดมาต้องได้
QUEUE_FULLโดยไม่เรียก handler หรือสร้าง Task ใหม่ให้มัน - ส่ง notes-1 ไปอีก dependency แล้วรอผล fixture ให้เสร็จ ก่อนเปิด Event ของ render-1 ลำดับนี้ทำให้เงื่อนไขทดสอบอ่านได้โดยไม่อ้างว่า notes เร็วเท่าไร
- เลื่อนนาฬิกาจำลองจาก 0 เป็น 2 และปล่อย render-1 งาน render-2 มีอายุรอที่อนุญาตเพียง 1 จึงได้
QUEUE_EXPIREDก่อน handler เริ่ม ส่วน render-3 ยังทำงานได้
นาฬิกาข้างต้นถูกฉีดเข้าโค้ดเพื่อทดสอบกฎ ไม่ใช่เวลาที่วัดจริง ค่า queue TTL คืออายุสูงสุดก่อนเริ่ม handler ไม่ใช่ execution timeout เมื่อเริ่มแล้วตัวอย่างไม่มีตัวจับเวลาตัดงาน การใช้งานจริงจึงต้องออกแบบเวลาของ transport และผลข้างเคียงแยกอีกชั้น
Google SRE: Cascading Failures อธิบายปัญหาคิวกับ deadline จึงควรตรวจอายุก่อนเริ่ม ไม่ยืมตัวเลขคิวของระบบอื่นมาเป็นสูตร การเพิ่ม Q ไม่ได้เพิ่ม worker

4. โค้ด Python: ตัวรับงานที่ไม่สร้าง Task ต่อคำขอ
สถานะทั้งหมด: AUTHOR MOCK — NOT RUN ใช้ Python 3.12+ และ standard library เท่านั้น ไม่มี package ให้ติดตั้ง ไม่มี environment variable หรือ API key ให้ตั้ง คัดลอกโมดูลต่อไปนี้เป็น bulkhead_mock.py ในโฟลเดอร์ทดลองว่าง โค้ดไม่ได้เชื่อม n8n, ADK, MCP หรือระบบเรนเดอร์จริง
ไฟล์ตัวอย่างมีทะเบียน Tool, worker, admission, lifecycle และ demo ครบในไฟล์เดียว ส่วนชุดทดสอบแยกชื่อ test_bulkhead_mock.py พร้อม README อยู่ในชุดงานของบทเรียน คำสั่งเหล่านี้เป็นวิธีทดสอบสำหรับผู้อ่าน ยังไม่ได้รันระหว่างจัดทำ:
python --version
python bulkhead_mock.py
python -m unittest -v test_bulkhead_mock.py
code/bulkhead_mock.py · NOT RUN
"""AUTHOR MOCK. Python 3.12+ standard library. Authored; NOT RUN.
Single event loop, fixed trusted registry, finite queues, cooperative handlers.
No network, model, credentials, files, external side effects or production SDK.
"""
from __future__ import annotations
import asyncio
import json
import math
import re
from collections import deque
from collections.abc import Awaitable, Callable, Mapping
from dataclasses import dataclass
from types import MappingProxyType
Handler = Callable[[str], Awaitable[str]]
Clock = Callable[[], float]
@dataclass(frozen=True)
class LaneSpec:
workers: int
queue_capacity: int
@dataclass(frozen=True)
class ToolSpec:
lane: str
handler: Handler
@dataclass(frozen=True)
class Outcome:
code: str
value: str = ""
@dataclass(frozen=True)
class Admission:
accepted: bool
reason: str
result: asyncio.Future[Outcome] | None = None
@dataclass(frozen=True)
class Job:
call_id: str
tool_id: str
expires_at: float
result: asyncio.Future[Outcome]
class Lane:
def __init__(self, spec: LaneSpec) -> None:
self.spec = spec
self.queue: asyncio.Queue[Job] = asyncio.Queue(spec.queue_capacity)
self.active = 0
class Bulkhead:
"""Own from one loop and call aclose once from its lifecycle owner.
Queue expiry covers waiting time only, not execution deadlines. A caller
cancelling its result skips a queued job at dequeue, but does not cancel
a handler that already started. A cancelled queued item holds its queue
position until dequeued. aclose requests cooperative worker cancellation.
"""
def __init__(
self,
specs: Mapping[str, LaneSpec],
registry: Mapping[str, ToolSpec],
clock: Clock | None = None,
) -> None:
if not specs or not registry:
raise ValueError("fixed lanes and trusted tools required")
for spec in specs.values():
for number in (spec.workers, spec.queue_capacity):
if type(number) is not int or number <= 0:
raise ValueError("C and Q must be positive integers")
if any(tool.lane not in specs for tool in registry.values()):
raise ValueError("tool refers to an unknown lane")
self.lanes = {name: Lane(spec) for name, spec in specs.items()}
self.registry = MappingProxyType(dict(registry))
self.clock = clock
self.events: deque[dict[str, str]] = deque(maxlen=64)
self.workers: list[asyncio.Task[None]] = []
self.loop: asyncio.AbstractEventLoop | None = None
self.closed = False
def start(self) -> None:
if self.loop is not None or self.closed:
raise RuntimeError("start exactly once")
self.loop = asyncio.get_running_loop()
if self.clock is None:
self.clock = self.loop.time
for name, lane in self.lanes.items():
for index in range(lane.spec.workers):
self.workers.append(asyncio.create_task(
self._worker(name), name=f"bulkhead:{name}:{index}"
))
def _record(self, job: Job, lane: str, status: str) -> None:
# Fixed fields only: no prompts, arguments, handler results or secrets.
self.events.append({"call_id": job.call_id, "tool_id": job.tool_id,
"lane": lane, "status": status})
def try_submit(
self, tool_id: str, call_id: str, queue_ttl: float = 30.0
) -> Admission:
if self.closed or self.loop is None:
return Admission(False, "STOPPED")
if asyncio.get_running_loop() is not self.loop:
raise RuntimeError("single event loop only")
if not isinstance(call_id, str) or not re.fullmatch(
r"[A-Za-z0-9_-]{1,48}", call_id
):
return Admission(False, "INVALID_CALL_ID")
if type(queue_ttl) not in (int, float) or not math.isfinite(
queue_ttl
) or queue_ttl <= 0:
return Admission(False, "INVALID_QUEUE_TTL")
if not isinstance(tool_id, str) or len(tool_id) > 48:
return Admission(False, "UNKNOWN_TOOL")
tool = self.registry.get(tool_id)
if tool is None:
return Admission(False, "UNKNOWN_TOOL")
lane = self.lanes[tool.lane]
# No await between full/create_future/put_nowait: no same-loop producer
# can interleave here. Rejection creates no Future or per-request Task.
if lane.queue.full():
return Admission(False, "QUEUE_FULL")
assert self.clock is not None
reply: asyncio.Future[Outcome] = self.loop.create_future()
job = Job(call_id, tool_id, self.clock() + queue_ttl, reply)
try:
lane.queue.put_nowait(job)
except asyncio.QueueFull:
reply.cancel() # Defensive; no blocking put or background retry.
return Admission(False, "QUEUE_FULL")
self._record(job, tool.lane, "ACCEPTED")
return Admission(True, "ACCEPTED", reply)
async def _worker(self, name: str) -> None:
lane = self.lanes[name]
while True:
job = await lane.queue.get()
try:
if job.result.done():
status = ("CANCELLED_BEFORE_START" if job.result.cancelled()
else "RESULT_SETTLED_BEFORE_START")
self._record(job, name, status)
continue
assert self.clock is not None
if self.clock() >= job.expires_at:
job.result.set_result(Outcome("QUEUE_EXPIRED"))
self._record(job, name, "QUEUE_EXPIRED")
continue
lane.active += 1
try:
value = await self.registry[job.tool_id].handler(job.call_id)
outcome = Outcome("OK", value)
except asyncio.CancelledError:
job.result.cancel()
self._record(job, name, "WORKER_CANCELLED")
raise
except Exception:
outcome = Outcome("HANDLER_ERROR")
finally:
lane.active -= 1
if not job.result.done():
job.result.set_result(outcome)
self._record(job, name, outcome.code)
else:
status = ("FINISHED_AFTER_CALLER_CANCEL"
if job.result.cancelled()
else "FINISHED_WITH_SETTLED_RESULT")
self._record(job, name, status)
finally:
lane.queue.task_done()
def snapshot(self) -> dict[str, dict[str, int]]:
return {name: {"active": lane.active,
"waiting": lane.queue.qsize(),
"C": lane.spec.workers,
"Q": lane.spec.queue_capacity}
for name, lane in self.lanes.items()}
async def aclose(self) -> None:
if self.closed:
return
self.closed = True # Admission stops before workers are cancelled.
for worker in self.workers:
worker.cancel()
await asyncio.gather(*self.workers, return_exceptions=True)
for name, lane in self.lanes.items():
while True:
try:
job = lane.queue.get_nowait()
except asyncio.QueueEmpty:
break
try:
job.result.cancel()
self._record(job, name, "SHUTDOWN_QUEUED")
finally:
lane.queue.task_done()
async def demo() -> dict[str, object]:
started, release = asyncio.Event(), asyncio.Event()
logical_time = [0.0]
render_invocations: list[str] = []
async def render_preview(call_id: str) -> str:
render_invocations.append(call_id)
if call_id == "render-1":
started.set()
await release.wait()
return "preview fixture"
async def read_notes(call_id: str) -> str:
return "notes fixture"
pool = Bulkhead(
{"render": LaneSpec(1, 2), "notes": LaneSpec(1, 1)},
{"render_preview": ToolSpec("render", render_preview),
"read_notes": ToolSpec("notes", read_notes)},
clock=lambda: logical_time[0],
)
pool.start()
try:
first = pool.try_submit("render_preview", "render-1")
await started.wait()
expired = pool.try_submit("render_preview", "render-2", queue_ttl=1)
third = pool.try_submit("render_preview", "render-3")
rejected = pool.try_submit("render_preview", "render-4")
notes = pool.try_submit("read_notes", "notes-1")
assert first.result and expired.result and third.result and notes.result
notes_outcome = await notes.result
before_release = pool.snapshot()
logical_time[0] = 2.0 # Synthetic queue age, not elapsed wall time.
release.set()
outcomes = await asyncio.gather(first.result, expired.result, third.result)
return {"mode": "AUTHOR_MOCK_NOT_RUN_IN_ARTICLE",
"rejected": rejected.reason,
"notes": notes_outcome.code,
"before_release": before_release,
"render_outcomes": [item.code for item in outcomes],
"render_invocations": render_invocations}
finally:
release.set()
await pool.aclose()
if __name__ == "__main__":
print(json.dumps(asyncio.run(demo()), indent=2, ensure_ascii=False))
code/test_bulkhead_mock.py · NOT RUN
"""Authored acceptance/error/cancellation tests. Execution status: NOT RUN."""
import asyncio
import unittest
from bulkhead_mock import Bulkhead, LaneSpec, ToolSpec, demo
class BulkheadTests(unittest.IsolatedAsyncioTestCase):
async def test_full_lane_does_not_consume_notes_lane(self):
result = await demo()
self.assertEqual(result["rejected"], "QUEUE_FULL")
self.assertEqual(result["notes"], "OK")
self.assertEqual(result["before_release"]["render"]["active"], 1)
self.assertEqual(result["before_release"]["render"]["waiting"], 2)
self.assertEqual(result["render_outcomes"], ["OK", "QUEUE_EXPIRED", "OK"])
self.assertEqual(result["render_invocations"], ["render-1", "render-3"])
async def test_zero_negative_boolean_or_noninteger_capacity_rejected(self):
async def fixture(call_id):
return "fixture"
for c, q in ((0, 1), (1, 0), (1, -1), (True, 1), (1, 1.5)):
with self.subTest(c=c, q=q), self.assertRaises(ValueError):
Bulkhead({"a": LaneSpec(c, q)}, {"tool": ToolSpec("a", fixture)})
async def test_unknown_tool_and_invalid_inputs_not_admitted(self):
async def fixture(call_id):
return "fixture"
pool = Bulkhead({"a": LaneSpec(1, 1)}, {"tool": ToolSpec("a", fixture)})
pool.start()
try:
unknown = pool.try_submit("new_lane_from_model", "id-1")
invalid = pool.try_submit("tool", "secret prompt with spaces")
ttl = pool.try_submit("tool", "id-2", float("inf"))
self.assertEqual(unknown.reason, "UNKNOWN_TOOL")
self.assertIsNone(unknown.result)
self.assertEqual(invalid.reason, "INVALID_CALL_ID")
self.assertEqual(ttl.reason, "INVALID_QUEUE_TTL")
self.assertEqual(pool.snapshot()["a"]["waiting"], 0)
finally:
await pool.aclose()
async def test_expired_waiting_job_never_calls_handler(self):
started, release = asyncio.Event(), asyncio.Event()
clock, calls = [0.0], []
async def fixture(call_id):
calls.append(call_id)
if call_id == "first":
started.set()
await release.wait()
return "fixture"
pool = Bulkhead({"a": LaneSpec(1, 1)}, {"tool": ToolSpec("a", fixture)},
clock=lambda: clock[0])
pool.start()
try:
first = pool.try_submit("tool", "first")
await started.wait()
pending = pool.try_submit("tool", "expired", 1)
clock[0] = 2.0
release.set()
await first.result
self.assertEqual((await pending.result).code, "QUEUE_EXPIRED")
self.assertEqual(calls, ["first"])
finally:
release.set()
await pool.aclose()
async def test_cancelled_waiting_job_skipped_at_dequeue(self):
started, release = asyncio.Event(), asyncio.Event()
calls = []
async def fixture(call_id):
calls.append(call_id)
if call_id == "first":
started.set()
await release.wait()
return "fixture"
pool = Bulkhead({"a": LaneSpec(1, 1)}, {"tool": ToolSpec("a", fixture)})
pool.start()
try:
first = pool.try_submit("tool", "first")
await started.wait()
pending = pool.try_submit("tool", "cancelled")
pending.result.cancel()
self.assertEqual(pool.snapshot()["a"]["waiting"], 1)
release.set()
await first.result
await pool.lanes["a"].queue.join()
self.assertEqual(calls, ["first"])
self.assertTrue(pending.result.cancelled())
finally:
release.set()
await pool.aclose()
async def test_running_caller_cancel_does_not_release_worker_early(self):
started, release, finished = asyncio.Event(), asyncio.Event(), asyncio.Event()
async def slow(call_id):
started.set()
await release.wait()
finished.set()
return "fixture"
async def notes(call_id):
return "notes"
pool = Bulkhead({"a": LaneSpec(1, 1), "b": LaneSpec(1, 1)},
{"slow": ToolSpec("a", slow), "notes": ToolSpec("b", notes)})
pool.start()
try:
active = pool.try_submit("slow", "active")
await started.wait()
active.result.cancel()
self.assertEqual(pool.snapshot()["a"]["active"], 1)
self.assertFalse(finished.is_set())
other = pool.try_submit("notes", "other")
self.assertEqual((await other.result).code, "OK")
release.set()
await finished.wait()
await pool.lanes["a"].queue.join()
self.assertTrue(active.result.cancelled())
self.assertIn("FINISHED_AFTER_CALLER_CANCEL",
[event["status"] for event in pool.events])
finally:
release.set()
await pool.aclose()
async def test_ordinary_job_failure_keeps_worker_and_other_lane_available(self):
async def fixture(call_id):
if call_id == "bad":
raise ValueError("not copied into bounded event ledger")
return "fixture"
pool = Bulkhead({"a": LaneSpec(1, 1), "b": LaneSpec(1, 1)},
{"a_tool": ToolSpec("a", fixture),
"b_tool": ToolSpec("b", fixture)})
pool.start()
try:
bad = pool.try_submit("a_tool", "bad")
self.assertEqual((await bad.result).code, "HANDLER_ERROR")
next_a = pool.try_submit("a_tool", "next")
next_b = pool.try_submit("b_tool", "other")
self.assertEqual((await next_a.result).code, "OK")
self.assertEqual((await next_b.result).code, "OK")
self.assertTrue(all(not worker.done() for worker in pool.workers))
finally:
await pool.aclose()
async def test_shutdown_cancels_active_and_queued_results_and_closes_admission(self):
started, release, cleaned = asyncio.Event(), asyncio.Event(), asyncio.Event()
async def fixture(call_id):
started.set()
try:
await release.wait()
finally:
cleaned.set()
return "fixture"
pool = Bulkhead({"a": LaneSpec(1, 1)}, {"tool": ToolSpec("a", fixture)})
pool.start()
active = pool.try_submit("tool", "active")
await started.wait()
waiting = pool.try_submit("tool", "waiting")
await pool.aclose()
self.assertTrue(cleaned.is_set())
self.assertTrue(active.result.cancelled())
self.assertTrue(waiting.result.cancelled())
self.assertEqual(pool.try_submit("tool", "late").reason, "STOPPED")
self.assertEqual(pool.snapshot()["a"]["active"], 0)
self.assertEqual(pool.snapshot()["a"]["waiting"], 0)
self.assertTrue(all(worker.done() for worker in pool.workers))
if __name__ == "__main__":
unittest.main()
code/README.md · NOT RUN
# Bulkhead AUTHOR MOCK
Execution status: **NOT RUN**. The example and eight authored tests were written and read statically. They have not been executed, imported, compiled, benchmarked or connected to a real Agent/API.
## Runtime and setup
- Python **3.12+**; this example targets the Python3.12 asyncio API. Python3.12 is an API baseline, not a latest-version claim.
- Standard library only: asyncio, collections, dataclasses, json, math, re, types, unittest. No dependency installation or requirements file.
- Environment variables: none. Credentials/API keys: none.
- Installation: copy `bulkhead_mock.py` and `test_bulkhead_mock.py` into the same empty local directory. Do not replace real tool handlers or production configuration to follow this lesson.
- Invoke from that directory when you choose to test. Commands below are instructions only, not a record of execution.
```text
python --version
python bulkhead_mock.py
python -m unittest -v test_bulkhead_mock.py
```
## Inputs and deterministic example
The host, not a model, owns a fixed registry: render_preview→render, read_notes→notes. The render lane has C=1 fixed worker and Q=2 waiting slots. The notes lane has C=1 and Q=1. These are tiny illustrative values, not capacity recommendations.
`try_submit(tool_id, call_id, queue_ttl=30)` is synchronous on the same event loop. A call ID is 1–48 ASCII letters/digits/underscore/hyphen. No free-form prompt, payload, URL or raw document is accepted. A positive finite queue_ttl is a queue-start age limit, not an execution timeout. The default uses loop.time; the demo injects a logical clock.
The first render handler sets an Event and waits for explicit release. After it starts, two more render jobs fill the queue, the fourth is rejected without creating a request Task or result Future, and notes uses its own worker. The demo then advances the injected clock from0 to2 and releases rendering. The second render job had queue_ttl1 and expires before invocation. No sleep durations, wall-clock latency or performance score are asserted.
Expected output if the authored implementation behaves as designed (**not observed output**):
```json
{
"mode": "AUTHOR_MOCK_NOT_RUN_IN_ARTICLE",
"rejected": "QUEUE_FULL",
"notes": "OK",
"before_release": {
"render": {"active": 1, "waiting": 2, "C": 1, "Q": 2},
"notes": {"active": 0, "waiting": 0, "C": 1, "Q": 1}
},
"render_outcomes": ["OK", "QUEUE_EXPIRED", "OK"],
"render_invocations": ["render-1", "render-3"]
}
```
## Contracts and boundaries
- C and Q must be positive integers; bool,0 and negative values are rejected. Queue(maxsize=0) is unbounded in asyncio, not zero-wait. Direct zero-wait admission is a different design and is absent here.
- There are C worker Tasks per configured lane, created only at start. Admission uses a nonwaiting capacity check and put_nowait with no await between them; use from one event loop only, not arbitrary threads. A burst before workers can dequeue may be rejected even while active<C. C+Q is an upper bound, not a guarantee every slot will be admitted on every burst.
- Per-lane accepted unfinished jobs are at most C+Q while each fixed worker holds at most one job and there is no hidden staging/retry. This bounds local jobs, not bytes, caller Tasks, global traffic, remote work, transport sockets, tenants, host memory, other processes or replica counts.
- Cancelling a queued result marks it cancelled; it still occupies its waiting slot until dequeue, then the handler is skipped. Cancelling a running result does not cancel its handler or free that worker early. Completed caller-discarded results are not retried automatically.
- Ordinary handler Exceptions become HANDLER_ERROR, without raw exception details in the bounded64-entry event deque. CancelledError is re-raised. This is a teaching error contract, not durable telemetry or a security audit trail.
- aclose is called by one lifecycle owner. It stops admission, requests cancellation of fixed workers, awaits their cleanup and cancels queued results. This depends on cooperative handlers; a handler that swallows cancellation, blocks the loop or continues remote work is outside this guarantee. No hard kill, forced shutdown deadline or remote undo is implemented.
- Call IDs are bounded metadata, not authenticated identities or idempotency keys. The caller can retain completed results elsewhere; no limit on that external storage is provided.
- Exposing a raw Future is a simplified teaching interface. A production library should wrap its receipt/result API rather than let consumers set Future state directly. Workers defensively check done before starting/settling a job.
- No rate limiter, CircuitBreaker, tenant scheduler, durable queue, distributed quota, request-body limit, actual HTTP client pool, process isolation or automatic retry is implemented. No Queue.shutdown API (introduced in Python3.13) is used.
## Authored test plan — all NOT RUN
Eight unittest cases cover independent admission under a full lane, invalid capacities (includingQ0), invalid/unknown inputs, expired waiting work not invoked, queued cancellation skip, running caller cancellation retaining its worker, ordinary error isolation, and shutdown of active+queued results. Test outcomes must be recorded after an authorized real run. `Queue.join()` is used as cleanup coordination in tests and does not by itself prove job success.
The authored tests/demo await Events, results and queue.join without an internal watchdog. A regression could hang a later manual run. Run them under a test runner or operating-system watchdog with an explicit external time budget when authorized; terminating that local test is not proof of remote cancellation. No watchdog or tests were run for this article.
## Permissions and costs
The code only allocates local asyncio objects, processes fixed fixture strings and prints synthetic JSON. It has no network, filesystem writes, real LLM/API, keys, paid services, GPU, deployment, data deletion or publishing permission. An actual run consumes local CPU/memory; no cost was measured. Live integration would require separate authorization, payload bounds, authentication, operation idempotency, transport/resource-pool limits and monitoring. The MOCK is not a Sandbox/Live integration and has no production-readiness claim.
อ่านจุดตัดสินใจสำคัญห้าจุด
หนึ่ง: ตรวจค่าตั้งต้น C และ Q ต้องเป็นจำนวนเต็มบวก ไม่ยอมรับ boolean หรือศูนย์ โดยเฉพาะ Queue(maxsize=0) ของ Python หมายถึงคิวไม่จำกัด ไม่ใช่ไม่มีช่องรอ ถ้าต้องการนโยบายไม่รอเลย ต้องสร้าง direct admission อีกแบบ ไม่แทนด้วย Q = 0
สอง: รับงานแบบ synchronous ใน event loop เดียว ระหว่างตรวจ full() สร้าง Future และ put_nowait() ไม่มี await ให้ producer ใน loop เดียวแทรกเข้ามา คิวเต็มจึงไม่สร้าง Future ตามเส้นทางปกติ และไม่มี Task ต่อคำขอเลย Task ถูกสร้างเฉพาะจำนวน worker ตอน start() เท่านั้น ส่วน QueueFull ที่จับไว้เป็นทางป้องกันเพิ่มเติม ไม่กลายเป็นการรอหรือ retry เบื้องหลัง พฤติกรรม Queue อ้างอิง เอกสาร Python 3.12
สาม: อายุของงานเริ่มนับตอนรับ เก็บ expires_at จาก monotonic clock แล้วเช็กที่ dequeue ไม่ตั้งนาฬิกาใหม่หลังรอเสร็จ ค่า monotonic ของ process นี้ไม่ใช่ timestamp สำหรับส่งข้ามเครื่อง เมื่อเชื่อมระบบจริงต้องออกแบบการส่งงบเวลาให้ตรงกัน
สี่: แยกข้อผิดพลาดรายงาน Exception ธรรมดากลายเป็น HANDLER_ERROR โดยไม่เก็บข้อความดิบ ส่วน CancelledError ส่งต่อ หากใช้ TaskGroup ร่วมทุกช่องแล้วปล่อย exception ธรรมดาหลุด อาจทำให้ sibling ถูกยกเลิกด้วย
ห้า: ปิดงานอย่างมีเจ้าของ lifecycle owner เรียก aclose() หนึ่งเส้นทาง โค้ดหยุดรับก่อน ยกเลิก worker รอ cleanup แล้ว cancel ผลของงานค้างคิว ทุก get ที่สำเร็จมี task_done ใน finally แต่ Queue.join หมายถึงตัวนับงานค้างเป็นศูนย์ ไม่ใช่ใบรับรองว่าผลทุกชิ้นสำเร็จ โค้ดไม่ใช้ Queue.shutdown ซึ่ง เพิ่มใน Python 3.13
ผลคาดหมายที่ต้องตรวจเอง
rejected: QUEUE_FULL
notes: OK ก่อนปล่อย render-1
render ก่อนปล่อย: active=1, waiting=2
render_outcomes: [OK, QUEUE_EXPIRED, OK]
render_invocations: [render-1, render-3]
ผลข้างต้นเป็น expected output ที่ยัง NOT RUN ไม่ใช่ log จากการรันจริง
5. ผู้เรียกยกเลิก ไม่ได้แปลว่างานภายนอกถูกย้อนคืน
Future ในตัวอย่างเป็นช่องรับผล ใช้เปิดให้เห็นกลไกการเรียนรู้ ไม่ใช่รูปแบบ public API ที่แนะนำให้ทุกระบบเปิดให้ผู้เรียกแก้สถานะเอง หากผู้เรียกเลิกรอตอนงานยังอยู่ในคิว Future ที่ cancel แล้วจะถูกข้ามเมื่อ worker หยิบถึง แต่ยังครองช่อง Q ระหว่างนั้น ไม่ได้คืนตำแหน่งทันที
ถ้า handler เริ่มแล้ว การ cancel Future ไม่ได้ cancel worker ที่แยกกันอยู่ โค้ดปล่อยให้ handler ใช้ slot ต่อจนจบ แล้วบันทึก FINISHED_AFTER_CALLER_CANCEL โดยไม่ส่งผลกลับ วิธีนี้ตั้งใจไม่คืน slot ก่อนงานที่ถืออยู่สิ้นสุด ความสัมพันธ์ระหว่าง Future กับ Task ต้องดูว่าใครกำลัง await ใคร ไม่อนุมานจากคำว่า cancel อย่างเดียว อ่านสัญญา Future และ Task cancellation ประกอบ
การ shutdown ก็ยังเป็น cooperative cancellation คือขอให้ coroutine ยอมยกเลิก หาก handler กลืน cancellation, ทำ synchronous CPU หนัก หรือเรียก remote service ที่ไม่รองรับการหยุด ก็ไม่มีหลักฐานว่างานนั้นจบแล้ว บทนี้จึงไม่ใส่ปุ่ม hard kill หรืออ้าง rollback จากการ cancel ค่า timeout ก็ไม่ควรถูกตีความว่าเป็นเวลาสูงสุดที่งานภายนอกหยุดแน่นอน

6. ขอบเขตที่คิวไม่ช่วย และสิ่งที่ควรเก็บหลักฐาน
สอง lane นี้ยังแชร์ event loop และ process หากแทน Event ด้วย blocking I/O หรือการคำนวณ CPU แบบ synchronous ทั้งสองช่องอาจหยุดสลับงานได้ ข้อจำกัดนี้สอดคล้องกับ คำแนะนำเรื่อง blocking code ของ asyncio การแยก Task ไม่เท่ากับแยกเครื่อง, thread, GPU, หน่วยความจำ หรือ connection pool
C + Q ไม่นับ byte, caller Tasks, request handlers หรือ replica อื่น จึงต้องคุม ingress/body/global bounds เพิ่ม และคงทะเบียน lane ไว้กับ host ตัวอย่างไม่มี tenant fairness, คิวถาวร, distributed quota หรือการรับประกันส่งงานสำเร็จหนึ่งครั้ง
ควรเก็บเหตุรับ/ปฏิเสธ, Tool ID, lane, active, waiting, expiry และผลหลัง caller เลิกรอ โค้ดเก็บ metadata ใน deque ไม่เกิน 64 เหตุการณ์ ไม่มี prompt, เอกสาร, ผล Tool หรือ secret deque นี้เก็บเฉพาะ call_id, tool_id, lane และ status ไม่บันทึก active/waiting/expiry หรือคำขอที่ปฏิเสธ ค่าที่ควรเก็บในระบบจริงข้างต้นจึงยังต้องออกแบบเพิ่ม บันทึกอาจถูกทับ Call ID ไม่ใช่ authenticated identity หรือ idempotency key
คิวเต็มในเครื่องของเราเป็น local overload ไม่ใช่หลักฐานว่า API ปลายทางล่ม จึงไม่มี automatic retry ในโมดูลนี้ การส่งกลับเข้าคิวเดิมทันทีเพิ่มภาระเดิมได้ หากต้องใช้ retry ให้มีงบและเงื่อนไขแยกตาม บท Retry Budget ส่วน production ต้องประเมินจำนวน process, pool ของ client, payload และผลข้างเคียงร่วมกัน ไม่มีตัวเลข throughput, latency, ราคา หรือคะแนนความปลอดภัยจริงจากตัวอย่างนี้
7. Brief ที่คัดลอกไปมอบหมายงานได้
เป้าหมาย: สร้าง AUTHOR MOCK ของ Bulkhead ต่อ dependency สองช่อง
ข้อมูลและเครื่องมือ: Python3.12+ standard library; render_preview และ read_notes เป็น fixture เท่านั้น
ขอบเขต: host-owned registry คงที่; render C1/Q2, notes C1/Q1; ไม่เรียก network/API หรือเพิ่มสิทธิ์
สิ่งส่งมอบ: โมดูล, README, tests, expected Event sequence และบันทึกสถานะการรันตามจริง
เกณฑ์ตรวจรับ: render เต็มแล้วปฏิเสธก่อนเรียก handler; notes จบได้ก่อนปล่อย render; งานหมดอายุไม่เริ่ม
จุดหยุด: ค่าความจุผิด, Tool ไม่รู้จัก, คิวเต็ม หรือหมดอายุต้องคืนเหตุผล; ไม่ retry อัตโนมัติหรือสร้าง Task ซ่อน
เกณฑ์ตรวจรับเพิ่มเติมสำหรับผู้ทดสอบคือ active ไม่เกิน C และ waiting ไม่เกิน Q ตลอด trace, ผู้เรียก cancel งานรอแล้ว handler ไม่ถูกเรียก, ผู้เรียก cancel งานเริ่มแล้ว slot ไม่ถูกคืนก่อน handler สิ้นสุด, งาน error หนึ่งชิ้นไม่ทำให้ worker อีกช่องหาย และ shutdown ทำให้ Future ของงานรับแล้วไม่มีตัวที่รอตลอดโดยไม่มีคำอธิบาย เงื่อนไขท้ายนี้ต้องทดสอบกับ handler ที่ร่วมมือยกเลิกตามข้อตกลง
unittest แปดกรณีรวม Q = 0, Tool แปลก และ expiry ยัง NOT RUN การมีไฟล์ test ไม่ใช่ผลผ่าน ก่อนเปลี่ยนเป็น Live ต้องมีอำนาจสำหรับ API ค่าใช้จ่าย และการเขียนข้อมูลแยก
8. Lab: ดูการแย่งช่อง แล้วตรวจว่าขอบเขตเริ่มตรงไหน
3D Lab เฉพาะเรื่องให้สำรวจ worker กับตำแหน่งคิวของสอง dependency ส่วน interactive แยกช่วยตรวจว่ารายงานสถานะงานกล่าวเกินหลักฐานหรือไม่ ค่าเป็นชิ้นงานจำลองตามกฎ ไม่มีเวลาจริงหรือคะแนนประสิทธิภาพ อ่านสมมติฐานของแต่ละสถานการณ์ก่อนเทียบ: การแจก slot ในภาพไม่ใช่การรัน Python หรือการยืนยันลำดับ scheduler ของ asyncio
แบบจำลองตามกฎ • ไม่ใช่ผลทดสอบจริง
3D Lab: งบทรัพยากรเท่ากัน แบ่งคิวต่างกัน
ทั้งสองแบบมี worker รวม 2 ตัว และที่รอรวม 3 ช่อง เลือกส่งงาน A ที่ค้างอยู่ แล้วสังเกตงาน B ใหม่หนึ่งงานซึ่งไม่ต้องใช้ผลของ A สมมติว่างาน A เริ่มใช้ worker ได้แล้วและยังไม่จบ ไม่มีเวลา ราคา หรืออัตราความสำเร็จที่วัดจริงในภาพนี้
C คือจำนวน worker ที่ทำงานพร้อมกัน; Q คือช่องรอ งานถูกปฏิเสธก่อนเพิ่มเมื่อขอบเขตเต็ม กราฟนับงานที่รับไว้ใน process นี้ ไม่ได้นับ Tasks ทั้งระบบหรือทรัพยากรบริการปลายทาง
ตัวอย่างตั้งต้น: A รับได้ 3 จาก 4 งาน ส่วน B มีช่องทำงานของตนเอง ไม่มีการเรียก Tool จริง
เลือกการ์ดเพื่ออ่านข้อความอธิบาย ไม่ใช่การสั่ง Agent
อ่านแบบสำรองและขอบเขตของภาพ
เมื่อส่ง A สี่งาน: pool ร่วม C2/Q3 ให้งาน A ทำงานสองและรอสอง งาน B ใหม่เข้าคิวรอ จึงยังเริ่มไม่ได้ เมื่อแยก A C1/Q2 กับ B C1/Q1 งาน A รับสามและปฏิเสธหนึ่ง งาน B เริ่มกับ worker ของ B ได้ ทั้งสองแบบมี C รวมสองและ Q รวมสามเท่ากัน
นี่เป็นภาพอธิบายการจัดสรรที่กำหนดลำดับไว้ ไม่ใช่ผลรัน Python หรือการจำลอง scheduler ทุกจังหวะ หากงาน B ต้องใช้ผล A อยู่แล้ว การแยกคิวไม่ได้ทำให้ผล A พร้อม หาก CPU/โค้ด synchronous ปิดกั้น event loop ร่วม ทั้งสองคิวอาจหยุดก้าวหน้า ต้องควบคุมทรัพยากรร่วมชั้นอื่นแยกกัน
Visual Interactive แยก • สถานะตามกฎสมมติ
รับเข้าคิวแล้ว พูดได้แค่ไหน?
เลือกหลักฐานที่มีจริงในสถานการณ์สมมติ แล้วตรวจว่ารายงานของ Agent กล่าวเกินหลักฐานหรือไม่ ปุ่มไม่เรียก API และไม่เปลี่ยนระบบภายนอก
QUEUED: รับไว้รอ ยังอ้างความสำเร็จไม่ได้
ติดตาม Future/ผลลัพธ์ของงานเดิม ไม่สร้างงานซ้ำเพียงเพราะรอนาน
คัดลอกด้วยคำสั่งของอุปกรณ์ ไม่มีการเขียน Clipboard อัตโนมัติ
ตารางสำรองและข้อจำกัด
- QUEUED เป็นหลักฐานการรับไว้รอ ไม่ใช่ผลสำเร็จ
- REJECTED_FULL ไม่มีงานใหม่ที่ถูกรับไว้ ไม่ retry วนเอง
- EXPIRED_BEFORE_START ไม่เรียก implementation ในตัวอย่าง
- ยกเลิกงานที่ยังรอ: Future ถูกยกเลิก แต่ช่องคิวอาจยังค้างจน worker ดึงออก
- ยกเลิก Future หลังเริ่ม: handler ในตัวอย่างยังทำต่อและยังใช้ worker จนกลับหรือถูกปิดอย่างร่วมมือ ไม่ใช่การยกเลิกคำสั่งปลายทาง
- RESULT_AVAILABLE คือมี fixture result ของ Tool หนึ่งงาน ไม่ยืนยัน whole Workflow หรือบริการจริง
- HANDLER_ERROR จำกัดผลผิดพลาดเป็นของงานนั้นตามโค้ดตัวอย่าง แต่ไม่ได้รับรอง event loop/process ทั้งหมด
ฝึกนำทักษะไปใช้กับวงจรวิดีโอ
ใช้ Video Lab สำรวจ workflow แล้วเติมสัญญาแบ่ง dependency ลง Prompt ตาม brief นี้ ปลั๊กอินไม่ได้ติดตั้ง Bulkhead หรือบังคับขอบเขตตอนรันจริง ค่าเวลาและเงินเป็นสมมติฐาน ไม่ใช่ผลวัด
เป้าหมาย: วางแผนวิดีโอสั้นและระบุ admission ของแต่ละ dependency
ข้อมูลและเครื่องมือ: โครงเรื่องสมมติ, Video Lab เพื่อสำรวจ workflow, fixture render_preview/read_notes
ขอบเขต: อ่านและวางแผนเท่านั้น; render C1/Q2 กับ notes C1/Q1; ไม่เรนเดอร์หรือเผยแพร่จริง
สิ่งส่งมอบ: Prompt/brief ที่ระบุ Tool→lane, เหตุปฏิเสธ, อายุคิว และพฤติกรรมเมื่อ caller cancel
เกณฑ์ตรวจรับ: คิว render เต็มแล้วไม่มีงานซ่อนเพิ่ม; notes ไม่ใช้ worker ของ render; ทุกผลแยก simulation ชัด
จุดหยุด: คิวเต็ม/หมดอายุให้รายงานเหตุผล; ถ้าจะเรียก API หรือเผยแพร่ให้หยุดที่แผนและขออำนาจเฉพาะ
จากคลิปดิบ
สู่จักรวาลคอนเทนต์
ChatGPT ตัดต่อวิดีโอแทนคนได้จริงแค่ไหน? สำรวจระบบ AI Video Editing Agent ผ่านฉากสามมิติที่โต้ตอบได้ พร้อมแยกข้อเท็จจริง สร้างพรอมป์ต์ และจำลองต้นทุนก่อนลงมือทำจริง
ออกแบบระบบตัดต่อในมุมมอง 3D
ลากบนฉากเพื่อหมุนมุมมอง แตะโหนดเพื่อดูหน้าที่ของ AI ในแต่ละขั้นตอน
จัดระเบียบไฟล์ ตรวจคุณภาพภาพและเสียง และกำหนดสิทธิ์เข้าถึงให้ AI Agent
แยกความสามารถจริงจากคำโฆษณา
กดแต่ละประเด็นเพื่ออ่านรายละเอียดและแหล่งข้อมูล
ChatGPT Work เข้าถึงไฟล์ในเครื่องได้ตรวจสอบแล้ว / มีเงื่อนไข+
Astra ใช้โปรแกรมคอมพิวเตอร์ทำงานได้ตรวจสอบแล้ว / ไม่ใช่โปรแกรมตัดต่อในตัว+
ตัดเสร็จใน 5–10 นาทีทุกกรณียังพิสูจน์เป็นเกณฑ์ทั่วไปไม่ได้+
ถูกกว่าจ้างคน 10 เท่าต้องกำหนดต้นทุนให้เทียบกันได้+
หนึ่งไอเดีย สามรูปแบบคอนเทนต์
ปรับรูปแบบวิดีโอ แล้วดูกรอบภาพตัวอย่างและ Prompt เปลี่ยนตามแบบเรียลไทม์
ช่วยตัดต่อ?
พร้อมนำไปใช้กับ AI Agent
ตัวสร้าง Prompt นี้ไม่อัปโหลดคลิป ไม่เรียก API เสียเงิน และไม่ตัดต่อวิดีโอจริง ต้องใช้กับเครื่องมือที่ผู้ใช้อนุญาตภายหลัง
จำลองเวลาและต้นทุนของคุณเอง
เลื่อนตัวเลขเพื่อเปรียบเทียบสถานการณ์สมมติ ไม่ใช่ผล benchmark ที่ยืนยันแล้ว
ตัวเลขเป็นตัวอย่างเพื่อช่วยตัดสินใจ ไม่ใช่ผลประหยัดที่รับประกัน
AI ที่ตัดคลิปเป็น ไม่ได้ทำงานลำพัง
สิ่งที่น่าสนใจของ GPT-6 Astra และ GPT-6.1 Sol ไม่ได้อยู่ที่ว่าตัวโมเดลมีปุ่ม Render MP4 ซ่อนอยู่ แต่คือความสามารถในการจัดการงานหลายขั้นตอนร่วมกับเครื่องมืออื่น: อ่านไฟล์ วางลำดับเรื่อง ใช้แอป ตรวจผลลัพธ์ และแก้งานตามข้อเสนอแนะของมนุษย์
สำหรับคนทำคอนเทนต์ ความต่างสำคัญคือการเปลี่ยนจาก “ส่งคลิปให้ AI แล้วหวังว่าจะได้ผลงาน” ไปเป็น Workflow ที่ตรวจรับได้ทุกขั้น ตั้งแต่การเลือกช่วงเด็ด การสร้างซับ การตรวจคำผิด จนถึงการเรนเดอร์ไฟล์ตามสัดส่วนของแต่ละแพลตฟอร์ม
ข้อควรระวังคือระบบ Work ที่รันบนเว็บหรือมือถือไม่ได้หมายความว่า AI เข้าถึงคลิปบนคอมพิวเตอร์บ้านได้ทันที ต้องทำงานในสภาพแวดล้อมที่เปิดให้เข้าถึงไฟล์จริง มีเครื่องมือตัดต่อ และมีสิทธิ์ใช้งานอย่างชัดเจน
รวบรวมไฟล์ในโฟลเดอร์ที่เข้าถึงได้ ระบุสิทธิ์และข้อจำกัด
ให้ Agent เสนอ Hook, ช่วงตัด และโครงเรื่องก่อนตัดต่อ
ตรวจชื่อเฉพาะ ตัวเลข ภาษาไทย และความถูกต้องของคำกล่าวอ้าง
ตรวจความละเอียด เสียง ระยะเวลา safe zone และคำบรรยาย
หากส่วนโต้ตอบใช้ไม่ได้ ให้อ่านแผนภาพกับ brief แทน ผลปุ่ม คีย์บอร์ด มือถือ และ reduced motion จริงยัง UNKNOWN
9. แบบฝึก: แยกขอบเขตต่อช่องออกจากจำนวนงานทั้งระบบ
ช่อง A มี C = 2, Q = 3 โดยสอง worker เริ่มงานแล้วและคิวว่าง มีงาน A ใหม่มาถึงอีก 7 ชิ้นก่อน worker ว่าง ช่อง B มี worker และช่องรอของตนเองที่ว่างอยู่ ให้ระบุจำนวนที่ A รับรอ จำนวนที่ปฏิเสธ งานรับแล้วที่ยังไม่เสร็จ และเหตุผลที่ B ยังมีทางรับงานของตน จากนั้นเพิ่ม process แบบเดียวกันอีกหนึ่งตัว: ขอบเขตทั้งสอง process ยังเท่ากับตัวเดิมหรือไม่?
แนวตรวจคำตอบ
A รับรอ 3 และปฏิเสธ 4 รวม active กับ waiting เป็น 5 B มีทางรับเพราะไม่ได้ใช้คิวหรือ worker ของ A แต่ยังแชร์ทรัพยากรอื่นได้ อีก process มีขอบเขตของตนเอง จึงห้ามอ้าง C + Q เดิมเป็น quota กลางรวมทุก process ถ้าตอบเพียง “เพิ่ม Semaphore” หรือ “คิวใหญ่ขึ้นก็หาย” ยังไม่อธิบาย admission ที่ขอบเขตทางเข้า
FAQ
ใช้ Semaphore อย่างเดียวได้ไหม?
ได้ถ้าความต้องการคือจำกัดผู้ผ่านเข้าใช้งานพร้อมกัน แต่ต้องรู้ว่าใครจำกัดจำนวนผู้รออยู่ด้วย ถ้าสร้าง Task สำหรับทุกคำขอก่อนรอ Semaphore ก็ยังไม่มีขอบเขต waiting จาก Semaphore เพียงตัวเดียว
ตั้ง Q = 0 เพื่อไม่ให้รอได้หรือไม่?
ใช้กับ asyncio.Queue แบบนั้นไม่ได้ เพราะ maxsize ไม่เกินศูนย์หมายถึงไม่จำกัด ตัวอย่างจึงปฏิเสธค่าเหล่านี้ นโยบาย zero-wait ต้องมี direct admission ที่นิยามแยก
คิวเต็มควร retry เมื่อไร?
บทนี้ไม่มี automatic retry ให้คืนเหตุผลและหยุดเส้นทางนั้นก่อน ผู้ประสานงานต้องพิจารณางบ เวลา ผลข้างเคียง และภาระปัจจุบัน ไม่ถือว่า QUEUE_FULL เป็นคำสั่งให้ส่งซ้ำทันที
กดยกเลิกแล้ว slot จะว่างเลยไหม?
ตามสัญญา MOCK นี้ งานรอจะถูกข้ามตอน dequeue แต่ยังใช้ช่องคิวก่อนถึงตอนนั้น งานที่เริ่มแล้วใช้ worker ต่อจนจบหรือจนการ shutdown ยกเลิกได้จริง ไม่รับรองการ undo ฝั่ง remote
นำโมดูลนี้ขึ้น production ได้เลยหรือไม่?
ยังไม่มีหลักฐานรองรับ โค้ดและ tests ยัง NOT RUN และไม่มี transport, authentication, body limits, durable state หรือ distributed admission ต้องทดสอบตามขอบเขตที่ได้รับอนุญาตก่อนเชื่อมระบบจริง
แหล่งต้นทางและขอบเขตหลักฐาน
ตรวจเมื่อ 15 ตุลาคม 2026 ใช้ Python3.12 เป็นฐาน API ไม่ใช่คำกล่าวว่ารุ่นล่าสุด ค่า C/Q, registry, cancellation contract และ fixture เป็นการออกแบบผู้เขียน ไม่มีผลวัด production
- Azure: Bulkhead — การแบ่ง pool และผลแลกเปลี่ยน
- Google SRE: Cascading Failures และ Handling Overload — คิว ภาระ และ deadline
- Python3.12 Queue, Synchronization, Tasks, Futures และ Developing with asyncio — สัญญา API ที่ใช้
- Python3.13 Queue — ตรวจเฉพาะขอบเขตรุ่นของ shutdown
- Azure Rate Limiting และ Circuit Breaker — แยกหน้าที่จาก Bulkhead

ใส่ความเห็น