Open dmmulroy opened 5 days ago
I'll wait for the minimal repro before going any further.
Will need to see at which point the messages are getting stuck, just to make sure it isn't the underlying postgres.js library.
I've spent a ton of time digging on this and have learned more than I ever wanted to about postgres and tcp 😂 I'm inclined to believe that this is not an Effect bug at this point.
If I had to point to a potential point of failure that I wanna spend more time it would be this in @effect/sql-pg
listen: (channel: string) =>
Stream.asyncPush<string, SqlError>((emit) =>
Effect.acquireRelease(
Effect.tryPromise({
try: () => client.listen(channel, (payload) => emit.single(payload)),
catch: (cause) => new SqlError({ cause, message: "Failed to listen" })
}),
({ unlisten }) => Effect.promise(() => unlisten())
)
),
client.listen
takes a third argument, onlisten
that might be useful for tapping into. Under the covers this called in the following circumstances:
this is what the postgres
npm libraries impl of listen
looks like:
async function listen(name, fn, onlisten) {
const listener = { fn, onlisten }
const sql = listen.sql || (listen.sql = Postgres({
...options,
max: 1,
idle_timeout: null,
max_lifetime: null,
fetch_types: false,
onclose() {
Object.entries(listen.channels).forEach(([name, { listeners }]) => {
delete listen.channels[name]
Promise.all(listeners.map(l => listen(name, l.fn, l.onlisten).catch(() => { /* noop */ })))
})
},
onnotify(c, x) {
c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x))
}
}))
const channels = listen.channels || (listen.channels = {})
, exists = name in channels
if (exists) {
channels[name].listeners.push(listener)
const result = await channels[name].result
listener.onlisten && listener.onlisten()
return { state: result.state, unlisten }
}
channels[name] = { result: sql`listen ${
sql.unsafe('"' + name.replace(/"/g, '""') + '"')
}`, listeners: [listener] }
const result = await channels[name].result
listener.onlisten && listener.onlisten()
return { state: result.state, unlisten }
Specifically in this block is where reconnects happen:
onclose() {
Object.entries(listen.channels).forEach(([name, { listeners }]) => {
delete listen.channels[name]
Promise.all(listeners.map(l => listen(name, l.fn, l.onlisten).catch(() => { /* noop */ })))
})
},
The recursive calls for retries/reconnect seems like it should be fine w/ how y'all are using emit
from Stream.asyncPush
but haven't had the time to patch deps and add logging yet.
How frequent are the messages? And how is postgres hosted in this case?
I think I've resolved the issue - the frequency of messages is very low atm due to it just being my testing. That being said, switching from using Supabases pooler/Pooled Connection to a direct connection, seems to have resolved this issue.
Still concerning that the underlying postgres library gave no indication of disconnects or errors though
I think I've resolved the issue - the frequency of messages is very low atm due to it just being my testing. That being said, switching from using Supabases pooler/Pooled Connection to a direct connection, seems to have resolved this issue.
Still concerning that the underlying postgres library gave no indication of disconnects or errors though
It seems they are disabling timeouts for the connection, which could be an issue too.
Might be a supabase issue? https://github.com/supabase/supavisor/issues/85
What version of Effect is running?
^3.8.4
What steps can reproduce the bug?
It's on my todo list today to attempt to reproduce in a minimal repo, but here is the code
What is the expected behavior?
The stream should process items for the pg channel indefinitely.
What do you see instead?
After some period of time, the stream stops processing items without any indication of why and without the fiber dying.
Additional information