-
-
Notifications
You must be signed in to change notification settings - Fork 350
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(channels): Postgres backends (#2803)
* wip Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * some debugging Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * formatting Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * use a separate connection to publish/listen Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * reintroduce flaky Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Fix typing Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Add psycopg backend Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Fix backend issues Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Undo test debugging changes Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * mark groups Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Ensure channel names ar quoted Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * sleep debugging Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * update docs Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Add missing test Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Fix docs link Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Add missing listener test Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Formatting Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Fix test typing Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> * Fix some coverage issue Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> --------- Signed-off-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com> Co-authored-by: Cody Fincher <204685+cofin@users.noreply.github.com>
- Loading branch information
1 parent
2409574
commit 6300249
Showing
14 changed files
with
234 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
.scannerwork/ | ||
.unasyncd_cache/ | ||
.venv/ | ||
.venv* | ||
.vscode/ | ||
__pycache__/ | ||
build/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
asyncpg | ||
======= | ||
|
||
.. automodule:: litestar.channels.backends.asyncpg | ||
:members: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,5 @@ backends | |
base | ||
memory | ||
redis | ||
psycopg | ||
asyncpg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
psycopg | ||
======= | ||
|
||
.. automodule:: litestar.channels.backends.psycopg | ||
:members: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
from contextlib import AsyncExitStack | ||
from functools import partial | ||
from typing import AsyncGenerator, Awaitable, Callable, Iterable, overload | ||
|
||
import asyncpg | ||
|
||
from litestar.channels import ChannelsBackend | ||
from litestar.exceptions import ImproperlyConfiguredException | ||
|
||
|
||
class AsyncPgChannelsBackend(ChannelsBackend): | ||
_listener_conn: asyncpg.Connection | ||
_queue: asyncio.Queue[tuple[str, bytes]] | ||
|
||
@overload | ||
def __init__(self, dsn: str) -> None: | ||
... | ||
|
||
@overload | ||
def __init__( | ||
self, | ||
*, | ||
make_connection: Callable[[], Awaitable[asyncpg.Connection]], | ||
) -> None: | ||
... | ||
|
||
def __init__( | ||
self, | ||
dsn: str | None = None, | ||
*, | ||
make_connection: Callable[[], Awaitable[asyncpg.Connection]] | None = None, | ||
) -> None: | ||
if not (dsn or make_connection): | ||
raise ImproperlyConfiguredException("Need to specify dsn or make_connection") | ||
|
||
self._subscribed_channels: set[str] = set() | ||
self._exit_stack = AsyncExitStack() | ||
self._connect = make_connection or partial(asyncpg.connect, dsn=dsn) | ||
|
||
async def on_startup(self) -> None: | ||
self._queue = asyncio.Queue() | ||
self._listener_conn = await self._connect() | ||
|
||
async def on_shutdown(self) -> None: | ||
await self._listener_conn.close() | ||
del self._queue | ||
|
||
async def publish(self, data: bytes, channels: Iterable[str]) -> None: | ||
dec_data = data.decode("utf-8") | ||
|
||
conn = await self._connect() | ||
try: | ||
for channel in channels: | ||
await conn.execute("SELECT pg_notify($1, $2);", channel, dec_data) | ||
finally: | ||
await conn.close() | ||
|
||
async def subscribe(self, channels: Iterable[str]) -> None: | ||
for channel in set(channels) - self._subscribed_channels: | ||
await self._listener_conn.add_listener(channel, self._listener) # type: ignore[arg-type] | ||
self._subscribed_channels.add(channel) | ||
|
||
async def unsubscribe(self, channels: Iterable[str]) -> None: | ||
for channel in channels: | ||
await self._listener_conn.remove_listener(channel, self._listener) # type: ignore[arg-type] | ||
self._subscribed_channels = self._subscribed_channels - set(channels) | ||
|
||
async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: | ||
while self._queue: | ||
yield await self._queue.get() | ||
self._queue.task_done() | ||
|
||
async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: | ||
raise NotImplementedError() | ||
|
||
def _listener(self, /, connection: asyncpg.Connection, pid: int, channel: str, payload: object) -> None: | ||
if not isinstance(payload, str): | ||
raise RuntimeError("Invalid data received") | ||
self._queue.put_nowait((channel, payload.encode("utf-8"))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from __future__ import annotations | ||
|
||
from contextlib import AsyncExitStack | ||
from typing import AsyncGenerator, Iterable | ||
|
||
import psycopg | ||
|
||
from .base import ChannelsBackend | ||
|
||
|
||
def _safe_quote(ident: str) -> str: | ||
return '"{}"'.format(ident.replace('"', '""')) | ||
|
||
|
||
class PsycoPgChannelsBackend(ChannelsBackend): | ||
_listener_conn: psycopg.AsyncConnection | ||
|
||
def __init__(self, pg_dsn: str) -> None: | ||
self._pg_dsn = pg_dsn | ||
self._subscribed_channels: set[str] = set() | ||
self._exit_stack = AsyncExitStack() | ||
|
||
async def on_startup(self) -> None: | ||
self._listener_conn = await psycopg.AsyncConnection.connect(self._pg_dsn, autocommit=True) | ||
await self._exit_stack.enter_async_context(self._listener_conn) | ||
|
||
async def on_shutdown(self) -> None: | ||
await self._exit_stack.aclose() | ||
|
||
async def publish(self, data: bytes, channels: Iterable[str]) -> None: | ||
dec_data = data.decode("utf-8") | ||
async with await psycopg.AsyncConnection.connect(self._pg_dsn) as conn: | ||
for channel in channels: | ||
await conn.execute("SELECT pg_notify(%s, %s);", (channel, dec_data)) | ||
|
||
async def subscribe(self, channels: Iterable[str]) -> None: | ||
for channel in set(channels) - self._subscribed_channels: | ||
# can't use placeholders in LISTEN | ||
await self._listener_conn.execute(f"LISTEN {_safe_quote(channel)};") # pyright: ignore | ||
|
||
self._subscribed_channels.add(channel) | ||
|
||
async def unsubscribe(self, channels: Iterable[str]) -> None: | ||
for channel in channels: | ||
# can't use placeholders in UNLISTEN | ||
await self._listener_conn.execute(f"UNLISTEN {_safe_quote(channel)};") # pyright: ignore | ||
self._subscribed_channels = self._subscribed_channels - set(channels) | ||
|
||
async def stream_events(self) -> AsyncGenerator[tuple[str, bytes], None]: | ||
async for notify in self._listener_conn.notifies(): | ||
yield notify.channel, notify.payload.encode("utf-8") | ||
|
||
async def get_history(self, channel: str, limit: int | None = None) -> list[bytes]: | ||
raise NotImplementedError() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters