Skip to content

Commit 341c8b8

Browse files
committed
Fixed a bug where the RPC coalescer would leave the application with no request even though there were consumers
1 parent 1c3a24a commit 341c8b8

File tree

3 files changed

+32
-5
lines changed

3 files changed

+32
-5
lines changed

.changeset/early-eyes-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solana/rpc': patch
3+
---
4+
5+
Fixed a bug where coalesced RPC calls could end up aborted even though there were still interested consumers. This would happen if the consumer count fell to zero, then rose above zero again, in the same runloop.

packages/rpc/src/__tests__/rpc-request-coalescer-test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ describe('RPC request coalescer', () => {
7979
const expectationB = expect(responsePromiseB).rejects.toBe(mockErrorB);
8080
await Promise.all([expectationA, expectationB]);
8181
});
82+
it('does not abort the transport when the number of consumers increases, falls to zero, then increases again in the same runloop', async () => {
83+
expect.assertions(2);
84+
const abortControllerA = new AbortController();
85+
const abortControllerB = new AbortController();
86+
coalescedTransport({ payload: null, signal: abortControllerA.signal }).catch(() => {});
87+
coalescedTransport({ payload: null, signal: abortControllerB.signal }).catch(() => {});
88+
// Both abort, bringing the consumer count to zero.
89+
abortControllerA.abort('o no A');
90+
abortControllerB.abort('o no B');
91+
// New request comes in at the last moment before the end of the runloop.
92+
coalescedTransport({ payload: null });
93+
await jest.runOnlyPendingTimersAsync();
94+
expect(mockTransport).toHaveBeenCalledTimes(1);
95+
const transportAbortSignal = mockTransport.mock.lastCall![0].signal!;
96+
expect(transportAbortSignal.aborted).toBe(false);
97+
});
8298
describe('multiple coalesced requests each with an abort signal', () => {
8399
let abortControllerA: AbortController;
84100
let abortControllerB: AbortController;
@@ -120,9 +136,13 @@ describe('RPC request coalescer', () => {
120136
abortControllerA.abort('o no');
121137
await expect(responsePromiseA).rejects.toBe('o no');
122138
});
123-
it('aborts the transport when all of the requests abort', () => {
139+
it('aborts the transport at the end of the runloop when all of the requests abort', async () => {
140+
expect.assertions(1);
141+
responsePromiseA.catch(() => {});
142+
responsePromiseB.catch(() => {});
124143
abortControllerA.abort('o no A');
125144
abortControllerB.abort('o no B');
145+
await jest.runOnlyPendingTimersAsync();
126146
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
127147
const transportAbortSignal = mockTransport.mock.lastCall![0].signal!;
128148
expect(transportAbortSignal.aborted).toBe(true);

packages/rpc/src/rpc-request-coalescer.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ export function getRpcTransportWithRequestCoalescing<TTransport extends RpcTrans
6969
const handleAbort = (e: AbortSignalEventMap['abort']) => {
7070
signal.removeEventListener('abort', handleAbort);
7171
coalescedRequest.numConsumers -= 1;
72-
if (coalescedRequest.numConsumers === 0) {
73-
const abortController = coalescedRequest.abortController;
74-
abortController.abort(EXPLICIT_ABORT_TOKEN);
75-
}
72+
Promise.resolve().then(() => {
73+
if (coalescedRequest.numConsumers === 0) {
74+
const abortController = coalescedRequest.abortController;
75+
abortController.abort(EXPLICIT_ABORT_TOKEN);
76+
}
77+
});
7678
reject((e.target as AbortSignal).reason);
7779
};
7880
signal.addEventListener('abort', handleAbort);

0 commit comments

Comments
 (0)