Skip to content

Commit 5fd29f0

Browse files
committed
fix(events): keep a channel not ready while its subscribe fails or answers a closed connection
A failed SUBSCRIBE no longer settles ready(): the stream's opening deadline bounds the wait, and the next connection subscribes again. A subscribe answered after its connection closed is ignored, so it cannot mark a reconnected channel ready before that connection has subscribed.
1 parent a6460c4 commit 5fd29f0

2 files changed

Lines changed: 30 additions & 8 deletions

File tree

‎apps/sim/lib/events/pubsub.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,29 @@ describe('createPubSubChannel over Redis', () => {
6969
expect(await settled(channel.ready())).toBe(true)
7070
})
7171

72-
it('settles when the subscribe fails, so streams are not held back', async () => {
72+
it('is not ready while its subscribe fails, and retries on the next connection', async () => {
7373
const channel = createPubSubChannel({ channel: 'test', label: 'Test' })
7474
subscriber().emit('ready')
7575

7676
subscriber().subscribed[0](new Error('NOPERM'))
77+
expect(await settled(channel.ready())).toBe(false)
78+
79+
subscriber().emit('close')
80+
subscriber().emit('ready')
81+
subscriber().subscribed[1](null)
82+
expect(await settled(channel.ready())).toBe(true)
83+
})
84+
85+
it('ignores a subscribe answered after its connection closed', async () => {
86+
const channel = createPubSubChannel({ channel: 'test', label: 'Test' })
87+
subscriber().emit('ready')
88+
89+
subscriber().emit('close')
90+
subscriber().subscribed[0](null)
91+
expect(await settled(channel.ready())).toBe(false)
7792

93+
subscriber().emit('ready')
94+
subscriber().subscribed[1](null)
7895
expect(await settled(channel.ready())).toBe(true)
7996
})
8097
})

‎apps/sim/lib/events/pubsub.ts‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export interface PubSubChannel<T> {
1818
subscribe(handler: (event: T) => void): () => void
1919
/**
2020
* Settles once this process receives the channel's publications; anything published before
21-
* then reaches no subscriber here.
21+
* then reaches no subscriber here. Stays pending while the channel cannot subscribe, so a caller
22+
* that must not wait indefinitely bounds the wait.
2223
*/
2324
ready(): Promise<void>
2425
dispose(): void
@@ -36,6 +37,8 @@ class RedisPubSubChannel<T> implements PubSubChannel<T> {
3637
private disposed = false
3738
/** Whether the current connection has subscribed; a dropped connection has to again. */
3839
private listening = false
40+
/** Counts closed connections, so a subscribe answered after its connection closed is ignored. */
41+
private closedConnections = 0
3942
private subscribed: Promise<void> = Promise.resolve()
4043
private markSubscribed: () => void = noop
4144

@@ -67,21 +70,23 @@ class RedisPubSubChannel<T> implements PubSubChannel<T> {
6770

6871
this.awaitSubscription()
6972
// Subscribes on every ready connection: ioredis resubscribes after a reconnect on its own but
70-
// does not report when that lands, and SUBSCRIBE is idempotent. Readiness settles on failure
71-
// too: nothing retries a failed subscribe, so waiting on it would only hold back every stream
72-
// on this channel.
73+
// does not report when that lands, and SUBSCRIBE is idempotent. A failed subscribe leaves the
74+
// channel not ready; the next connection tries again.
7375
this.sub.on('ready', () => {
76+
const connection = this.closedConnections
7477
this.sub.subscribe(config.channel, (err) => {
78+
if (connection !== this.closedConnections) return
7579
if (err) {
7680
logger.error(`Failed to subscribe to ${config.label} channel:`, err)
77-
} else {
78-
this.listening = true
79-
logger.info(`Subscribed to ${config.label} channel`)
81+
return
8082
}
83+
this.listening = true
84+
logger.info(`Subscribed to ${config.label} channel`)
8185
this.markSubscribed()
8286
})
8387
})
8488
this.sub.on('close', () => {
89+
this.closedConnections += 1
8590
if (!this.listening) return
8691
this.listening = false
8792
this.awaitSubscription()

0 commit comments

Comments
 (0)