91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""Sequential execution queue — one browser, one tab at a time.
|
|
|
|
Every platform flow drives abang's real browser window. Two flows at once
|
|
would fight over focus, file inputs and dialogs, so everything funnels through
|
|
one worker task here.
|
|
|
|
q = PostQueue(inter_platform_delay=(3.0, 6.0))
|
|
await q.start()
|
|
result = await q.submit("threads", coro_factory)
|
|
|
|
`submit` returns that job's result (or raises what the job raised). Jobs run
|
|
strictly in submission order.
|
|
"""
|
|
|
|
import asyncio
|
|
import random
|
|
import time
|
|
|
|
|
|
class PostQueue:
|
|
def __init__(self, inter_platform_delay=(3.0, 6.0)):
|
|
lo, hi = inter_platform_delay
|
|
if lo < 0 or hi < lo:
|
|
raise ValueError(
|
|
f"inter_platform_delay must be (lo, hi) with 0 <= lo <= hi, got {lo, hi}"
|
|
)
|
|
self.delay_lo, self.delay_hi = lo, hi
|
|
self._queue = asyncio.Queue()
|
|
self._worker = None
|
|
self._last_finished = None
|
|
self.history = [] # (iso_time, label, status)
|
|
|
|
async def start(self):
|
|
if self._worker is None:
|
|
self._worker = asyncio.create_task(self._run())
|
|
return self
|
|
|
|
async def stop(self):
|
|
if self._worker:
|
|
self._worker.cancel()
|
|
try:
|
|
await self._worker
|
|
except asyncio.CancelledError:
|
|
pass
|
|
self._worker = None
|
|
|
|
@property
|
|
def depth(self):
|
|
return self._queue.qsize()
|
|
|
|
async def submit(self, label, factory):
|
|
"""Queue `factory()` (a zero-arg coroutine function) and await its result."""
|
|
if self._worker is None:
|
|
raise RuntimeError("PostQueue.submit() before start()")
|
|
future = asyncio.get_running_loop().create_future()
|
|
await self._queue.put((label, factory, future))
|
|
return await future
|
|
|
|
async def _run(self):
|
|
while True:
|
|
label, factory, future = await self._queue.get()
|
|
await self._space_out()
|
|
try:
|
|
result = await factory()
|
|
if not future.done():
|
|
future.set_result(result)
|
|
status = result.get("status") if isinstance(result, dict) else "ok"
|
|
except asyncio.CancelledError:
|
|
if not future.done():
|
|
future.cancel()
|
|
raise
|
|
except Exception as exc: # noqa: BLE001 — hand it to the caller intact
|
|
if not future.done():
|
|
future.set_exception(exc)
|
|
status = f"raised {type(exc).__name__}"
|
|
finally:
|
|
self._last_finished = time.time()
|
|
self._queue.task_done()
|
|
self.history.append(
|
|
(time.strftime("%Y-%m-%dT%H:%M:%S"), label, status)
|
|
)
|
|
|
|
async def _space_out(self):
|
|
"""Rate-limit gap between consecutive platform jobs."""
|
|
if self._last_finished is None:
|
|
return
|
|
gap = random.uniform(self.delay_lo, self.delay_hi)
|
|
elapsed = time.time() - self._last_finished
|
|
if elapsed < gap:
|
|
await asyncio.sleep(gap - elapsed)
|