fix(workbench): serialize events and target work order sync
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from contextlib import asynccontextmanager
|
||||
from uuid import UUID
|
||||
|
||||
@@ -27,6 +28,8 @@ class WorkbenchEventHub:
|
||||
self._connection: AsyncConnection | None = None
|
||||
self._pool: AsyncConnectionPool | None = None
|
||||
self._connection_lock = asyncio.Lock()
|
||||
self._read_lock = asyncio.Lock()
|
||||
self._reads: OrderedDict[tuple, tuple[int, object]] = OrderedDict()
|
||||
self._condition = asyncio.Condition()
|
||||
self._generation = 0
|
||||
self._ready = asyncio.Event()
|
||||
@@ -59,6 +62,7 @@ class WorkbenchEventHub:
|
||||
|
||||
async def _listen_loop(self) -> None:
|
||||
retry_seconds = 1
|
||||
next_reconcile = asyncio.get_running_loop().time() + 30
|
||||
while not self._closed:
|
||||
try:
|
||||
if self._connection is None or self._connection.closed:
|
||||
@@ -76,8 +80,11 @@ class WorkbenchEventHub:
|
||||
if notification.payload.startswith(f"{self.project_id}:"):
|
||||
notified = True
|
||||
break
|
||||
if notified:
|
||||
now = asyncio.get_running_loop().time()
|
||||
if notified or now >= next_reconcile:
|
||||
await self._wake()
|
||||
if now >= next_reconcile:
|
||||
next_reconcile = now + 30
|
||||
await asyncio.sleep(0)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
@@ -109,22 +116,35 @@ class WorkbenchEventHub:
|
||||
return self._generation
|
||||
|
||||
async def oldest_cursor(self) -> int | None:
|
||||
return await self._read(lambda repository: repository.oldest_cursor())
|
||||
return await self._read(("oldest",), lambda repository: repository.oldest_cursor())
|
||||
|
||||
async def current_cursor(self) -> int:
|
||||
return await self._read(lambda repository: repository.current_cursor())
|
||||
return await self._read(("current",), lambda repository: repository.current_cursor())
|
||||
|
||||
async def read_after(self, after: int) -> list[dict]:
|
||||
return await self._read(lambda repository: repository.read_after(after))
|
||||
return await self._read(("after", after), lambda repository: repository.read_after(after))
|
||||
|
||||
async def _read(self, operation):
|
||||
async def _read(self, key: tuple, operation):
|
||||
await self.start()
|
||||
async with self._connection_lock:
|
||||
if self._connection is None or self._connection.closed:
|
||||
self._connection = await self._connect()
|
||||
self._ready.set()
|
||||
repository = WorkbenchEventRepository(self._connection, self.project_id)
|
||||
return await operation(repository)
|
||||
async with self._read_lock:
|
||||
cached = self._reads.get(key)
|
||||
if cached is not None and cached[0] == self.generation:
|
||||
self._reads.move_to_end(key)
|
||||
return cached[1]
|
||||
# Record the generation before the query: a concurrent NOTIFY must
|
||||
# invalidate even an empty result from an older database snapshot.
|
||||
generation = self.generation
|
||||
async with self._connection_lock:
|
||||
if self._connection is None or self._connection.closed:
|
||||
self._connection = await self._connect()
|
||||
self._ready.set()
|
||||
repository = WorkbenchEventRepository(self._connection, self.project_id)
|
||||
result = await operation(repository)
|
||||
self._reads[key] = (generation, result)
|
||||
self._reads.move_to_end(key)
|
||||
while len(self._reads) > 32:
|
||||
self._reads.popitem(last=False)
|
||||
return result
|
||||
|
||||
async def _drop_connection(self) -> None:
|
||||
async with self._connection_lock:
|
||||
@@ -135,6 +155,7 @@ class WorkbenchEventHub:
|
||||
|
||||
async def close(self) -> None:
|
||||
self._closed = True
|
||||
self._reads.clear()
|
||||
if self._idle_task is not None:
|
||||
self._idle_task.cancel()
|
||||
self._idle_task = None
|
||||
|
||||
Reference in New Issue
Block a user