-
Notifications
You must be signed in to change notification settings - Fork 2
Add a Connection.listen() helper. #1
base: master
Are you sure you want to change the base?
Conversation
Actually, it turned out that I needed to do more: I needed to be able to listen/unlisten to certain channels dynamically, but on the same connection. I came up with this: import trio
from psycopg2 import sql
class SubscriptionListener:
def __init__(self, conn):
self.conn = conn
self.subscriptions = {}
async def __aenter__(self):
# Use a contextstack like aiostream?
self.conn._connection.autocommit = True
self.nursery_context = trio.open_nursery()
self.nursery = await self.nursery_context.__aenter__()
self.nursery.start_soon(self.proc)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
# self.nursery.cancel_scope.cancel()
await self.nursery_context.__aexit__(exc_type, exc_val, exc_tb)
async def proc(self):
while True:
await wait_for_notify_message(self.conn)
while self.conn._connection.notifies:
notification = self.conn._connection.notifies.pop(0)
queue = self.subscriptions[notification.channel]
await queue.put(notification)
async def listen(self, *channels):
queries = sql.SQL('; ').join([
sql.SQL("LISTEN {}").format(sql.Identifier(channel))
for channel in channels
])
async with await self.conn.cursor() as cursor:
await cursor.execute(queries)
queue = trio.Queue(0)
for channel in channels:
self.subscriptions[channel] = queue
try:
while True:
event = await queue.get()
yield event
finally:
with trio.open_cancel_scope() as scope:
scope.shield = True
queries = sql.SQL('; ').join([
sql.SQL("UNLISTEN {}").format(sql.Identifier(channel))
for channel in channels
])
async with await self.conn.cursor() as cursor:
await cursor.execute(queries)
for channel in channels:
del self.subscriptions[channel]
async def wait_for_notify_message(conn):
while True:
await trio.hazmat.wait_readable(conn._sock)
await conn.poll()
if conn._connection.notifies:
return I am using it like this: async with trio.open_nursery() as nursery:
async with pool.acquire() as listen_conn:
listener = SubscriptionListener(listen_conn)
async with listener:
gen = listener.listen(channel_name)
async with aclosing(gen):
async for event in gen:
pass It is mostly the `wait_for_notify_message helper that accesses library internals. I am not sure which part would make sense as part of |
No clue what happened with CircleCI here, closing/re-opening to see if it picks it up |
Personally, I would simply just not make it an async generator. It's a bit more work, but making a fully fledged class that a Also, this needs a test added. |
If I understand you correctly, by using this syntax:
We would make |
Yes, that's the idea. |
Codecov Report
@@ Coverage Diff @@
## master #1 +/- ##
==========================================
- Coverage 95.9% 89.66% -6.24%
==========================================
Files 6 6
Lines 244 271 +27
==========================================
+ Hits 234 243 +9
- Misses 10 28 +18
Continue to review full report at Codecov.
|
So after the
Sidenote: Maybe I can do this outside of
But that seems very messy? https://github.com/aio-libs/aiopg can solve this more easily, because they just have an (There is I remember a trio ticket on Github about letting multiple readers wait the same So, the "permanently reading" approach in trio requires a nursery, and thus would change the API of riopg - the connection context manager would become required rather than being optional, as it is now. I updated this patch with a solution where I tried to have my cake and eat it too: If a background task is started which reads, then the In addition there is separate code to expose the notifications via a A couple ways this approach could be evolved:
Curious what you think. |
I want to use NOTIFY/LISTEN in postgres, and async should be great for this, we should be able to get the events as a stream. This is my attempt at implementing this.
There is a bug here. The finally clause is supposed to send the
UNLISTEN
command, and I would expect this to work incurio
from my tests, because given a generator such as this:called such as this:
Then the message
after shutdown
will be printed (curio enforces thecurio.meta.finalize
context manager to be used.However, on trio the message will not be printed (or in our case, the
UNLISTEN
will not be sent), because when theawait
within the finally clause goes back to the event loop, the task is still considered to be cancelled, and the Cancelled exception is raised again.In trio, we have to do:
Now, I wanted to add this ability to
multio
, and here is the snatch. In curio,disable_cancellation
is an async context manager, in trioopen_cancel_scope
is a sync one. We cannot convert the trio one to an async one, because at that point,async with
would go back to the trio event loop before the shield will go into effect, thus defeating the whole purpose here.My best suggestion would be: Given that curio does the right thing here, multio needs to support some kind of feature that is a noop on curio, and helps us run a async code in a finally branch. Maybe something like:
with multio.asynclib.safe_finally: