Skip to content

Commit 18a3827

Browse files
authored
Eagerly start subscription heartbeats (#7871)
# Overview Eagerly start subscription heartbeats in case the subscription has long-running setup steps before returning its async generator. When a subscription is configured, `subscribe` can return either an `AsyncGenerator` or a `Promise<AsyncGenerator>`. If an `AsyncGenerator` is returned, the previous code works fine. We get the generator right away and set up the heartbeat to keep things alive, even if the generator has long-running setup code before returning values. On the other hand, if the `Promise<AsyncGenerator>` takes longer than the hearbeat interval to resolve, the router will terminate the subscription. When the subscription actually starts generating data, the callback returns 404, as it has already been cleaned up. We want the system to gracefully wait for the setup code to complete, since we will do so if the setup code is placed inside the iterator itself. ## Example In this example, `subscribe` returns a `Promise<AsyncGenerator>`, but the `Promise` resolves quickly. This results in the heartbeat being started prior to the long-running setup. The heartbeat keeps the subscription alive until the setup is completed, then it can start sending data to the client. ``` return { subscribe: async () => { return { async *[Symbol.asyncIterator]() { // The heartbeat is already started, so this can wait as long as it wants await setupWithLongDelay(); while (running) { yield data; } }, }; }, } ``` On the other hand, waiting for the setup before resolving the `Promise` leads to complications. ``` return { subscribe: async () => { // The router kills this request because it doesn't start the heartbeat in time await setupWithLongDelay(); return { async *[Symbol.asyncIterator]() { while (running) { yield data; } }, }; }, } ``` ## Solution This change eagerly starts the heartbeat interval prior to awaiting the returned value from `subscribe`. This allows long-running setup code to run concurrently with the heartbeats. Previously the second example would result in a `SUBSCRIPTION_HEARTBEAT_ERROR` and subscription termination. Now, the router receives heartbeats so it correctly waits for the setup and data in both cases.
1 parent 7e64902 commit 18a3827

File tree

3 files changed

+45
-30
lines changed

3 files changed

+45
-30
lines changed

.changeset/angry-carrots-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/server": patch
3+
---
4+
5+
Subscription heartbeats are initialized prior to awaiting subscribe(). This allows long-running setup to happen in the returned Promise without the subscription being terminated prior to resolution.

packages/server/src/__tests__/plugin/subscriptionCallback/index.test.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ describe('SubscriptionCallbackPlugin', () => {
165165
"SubscriptionCallback[1234-cats]: Received new subscription request",
166166
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
167167
"SubscriptionManager[1234-cats]: \`check\` request successful",
168+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
168169
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
169170
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
170-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
171171
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
172172
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
173173
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -274,9 +274,9 @@ describe('SubscriptionCallbackPlugin', () => {
274274
"SubscriptionCallback[1234-cats]: Received new subscription request",
275275
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
276276
"SubscriptionManager[1234-cats]: \`check\` request successful",
277+
"SubscriptionManager[1234-cats]: Heartbeat disabled for http://mock-router-url.com",
277278
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
278279
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
279-
"SubscriptionManager[1234-cats]: Heartbeat disabled for http://mock-router-url.com",
280280
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
281281
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
282282
"TESTING: Triggering first update",
@@ -387,9 +387,9 @@ describe('SubscriptionCallbackPlugin', () => {
387387
"SubscriptionCallback[1234-cats]: Received new subscription request",
388388
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
389389
"SubscriptionManager[1234-cats]: \`check\` request successful",
390+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
390391
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
391392
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
392-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
393393
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
394394
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
395395
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -505,9 +505,9 @@ describe('SubscriptionCallbackPlugin', () => {
505505
"SubscriptionCallback[1234-cats]: Received new subscription request",
506506
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
507507
"SubscriptionManager[1234-cats]: \`check\` request successful",
508+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
508509
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
509510
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
510-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
511511
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
512512
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
513513
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -653,17 +653,17 @@ describe('SubscriptionCallbackPlugin', () => {
653653
"SubscriptionCallback[1234-cats]: Received new subscription request",
654654
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
655655
"SubscriptionManager[1234-cats]: \`check\` request successful",
656+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
656657
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
657658
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
658-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
659659
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
660660
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
661661
"SubscriptionCallback[5678-dogs]: Received new subscription request",
662662
"SubscriptionManager[5678-dogs]: Sending \`check\` request to router",
663663
"SubscriptionManager[5678-dogs]: \`check\` request successful",
664+
"SubscriptionManager[5678-dogs]: Starting new heartbeat interval for http://mock-router-url-2.com",
664665
"SubscriptionCallback[5678-dogs]: Starting graphql-js subscription",
665666
"SubscriptionCallback[5678-dogs]: graphql-js subscription successful",
666-
"SubscriptionManager[5678-dogs]: Starting new heartbeat interval for http://mock-router-url-2.com",
667667
"SubscriptionManager[5678-dogs]: Listening to graphql-js subscription",
668668
"SubscriptionCallback[5678-dogs]: Responding to original subscription request",
669669
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -850,17 +850,17 @@ describe('SubscriptionCallbackPlugin', () => {
850850
"SubscriptionCallback[1234-cats]: Received new subscription request",
851851
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
852852
"SubscriptionManager[1234-cats]: \`check\` request successful",
853+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
853854
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
854855
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
855-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
856856
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
857857
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
858858
"SubscriptionCallback[5678-dogs]: Received new subscription request",
859859
"SubscriptionManager[5678-dogs]: Sending \`check\` request to router",
860860
"SubscriptionManager[5678-dogs]: \`check\` request successful",
861+
"SubscriptionManager[5678-dogs]: Starting new heartbeat interval for http://mock-router-url.com/5678-dogs",
861862
"SubscriptionCallback[5678-dogs]: Starting graphql-js subscription",
862863
"SubscriptionCallback[5678-dogs]: graphql-js subscription successful",
863-
"SubscriptionManager[5678-dogs]: Starting new heartbeat interval for http://mock-router-url.com/5678-dogs",
864864
"SubscriptionManager[5678-dogs]: Listening to graphql-js subscription",
865865
"SubscriptionCallback[5678-dogs]: Responding to original subscription request",
866866
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1003,9 +1003,9 @@ describe('SubscriptionCallbackPlugin', () => {
10031003
"SubscriptionCallback[1234-cats]: Received new subscription request",
10041004
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
10051005
"SubscriptionManager[1234-cats]: \`check\` request successful",
1006+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
10061007
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
10071008
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1008-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
10091009
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
10101010
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
10111011
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1084,9 +1084,9 @@ describe('SubscriptionCallbackPlugin', () => {
10841084
"SubscriptionCallback[1234-cats]: Received new subscription request",
10851085
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
10861086
"SubscriptionManager[1234-cats]: \`check\` request successful",
1087+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
10871088
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
10881089
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1089-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
10901090
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
10911091
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
10921092
"SubscriptionManager[1234-cats]: Sending \`next\` request to router",
@@ -1139,9 +1139,9 @@ describe('SubscriptionCallbackPlugin', () => {
11391139
"SubscriptionCallback[1234-cats]: Received new subscription request",
11401140
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
11411141
"SubscriptionManager[1234-cats]: \`check\` request successful",
1142+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
11421143
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
11431144
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1144-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
11451145
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
11461146
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
11471147
"ERROR: SubscriptionManager[1234-cats]: Generator threw an error, terminating subscription: The subscription generator didn't catch this!",
@@ -1274,9 +1274,11 @@ describe('SubscriptionCallbackPlugin', () => {
12741274
],
12751275
});
12761276

1277-
// Trigger the heartbeat interval just to make sure it doesn't actually
1278-
// happen in this case (we haven't mocked it, so it'll throw an error if it
1279-
// sends a heartbeat).
1277+
// The heartbeat is initialized just before awaiting the subscription.
1278+
// So in this case where the subscription throws, there will be one
1279+
// heartbeat before the subscription is cleaned up.
1280+
mockRouterCheckResponse();
1281+
12801282
jest.advanceTimersByTime(5000);
12811283

12821284
await completeRequest;
@@ -1285,13 +1287,17 @@ describe('SubscriptionCallbackPlugin', () => {
12851287
[
12861288
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
12871289
"SubscriptionManager[1234-cats]: \`check\` request successful",
1290+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
12881291
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
12891292
"ERROR: SubscriptionCallback[1234-cats]: graphql-js subscription unsuccessful: [
12901293
The subscription field "invalidSubscriptionField" is not defined.
12911294
]",
12921295
"SubscriptionManager[1234-cats]: Sending \`complete\` request to router with errors",
12931296
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
1297+
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
12941298
"SubscriptionManager[1234-cats]: \`complete\` request successful",
1299+
"SubscriptionManager: Heartbeat received response for ID: 1234-cats",
1300+
"SubscriptionManager: Heartbeat request successful, ID: 1234-cats",
12951301
"SubscriptionCallback: Server is shutting down. Cleaning up outstanding subscriptions and heartbeat intervals",
12961302
"SubscriptionCallback: Successfully cleaned up outstanding subscriptions and heartbeat intervals.",
12971303
]
@@ -1353,9 +1359,9 @@ describe('SubscriptionCallbackPlugin', () => {
13531359
"SubscriptionCallback[1234-cats]: Received new subscription request",
13541360
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
13551361
"SubscriptionManager[1234-cats]: \`check\` request successful",
1362+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
13561363
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
13571364
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1358-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
13591365
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
13601366
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
13611367
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1433,9 +1439,9 @@ describe('SubscriptionCallbackPlugin', () => {
14331439
"SubscriptionCallback[1234-cats]: Received new subscription request",
14341440
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
14351441
"SubscriptionManager[1234-cats]: \`check\` request successful",
1442+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
14361443
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
14371444
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1438-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
14391445
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
14401446
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
14411447
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1543,9 +1549,9 @@ describe('SubscriptionCallbackPlugin', () => {
15431549
"WARN: SubscriptionManager[1234-cats]: Retrying \`check\` request (attempt 1) due to error: request to http://mock-router-url.com/ failed, reason: network request error",
15441550
"WARN: SubscriptionManager[1234-cats]: Retrying \`check\` request (attempt 2) due to error: request to http://mock-router-url.com/ failed, reason: network request error",
15451551
"SubscriptionManager[1234-cats]: \`check\` request successful",
1552+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
15461553
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
15471554
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1548-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
15491555
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
15501556
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
15511557
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1664,9 +1670,9 @@ describe('SubscriptionCallbackPlugin', () => {
16641670
"SubscriptionCallback[1234-cats]: Received new subscription request",
16651671
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
16661672
"SubscriptionManager[1234-cats]: \`check\` request successful",
1673+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
16671674
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
16681675
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1669-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
16701676
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
16711677
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
16721678
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1772,9 +1778,9 @@ describe('SubscriptionCallbackPlugin', () => {
17721778
"SubscriptionCallback[1234-cats]: Received new subscription request",
17731779
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
17741780
"SubscriptionManager[1234-cats]: \`check\` request successful",
1781+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
17751782
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
17761783
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1777-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
17781784
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
17791785
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
17801786
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1875,9 +1881,9 @@ describe('SubscriptionCallbackPlugin', () => {
18751881
"SubscriptionCallback[1234-cats]: Received new subscription request",
18761882
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
18771883
"SubscriptionManager[1234-cats]: \`check\` request successful",
1884+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
18781885
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
18791886
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1880-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
18811887
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
18821888
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
18831889
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",
@@ -1981,9 +1987,9 @@ describe('SubscriptionCallbackPlugin', () => {
19811987
"SubscriptionCallback[1234-cats]: Received new subscription request",
19821988
"SubscriptionManager[1234-cats]: Sending \`check\` request to router",
19831989
"SubscriptionManager[1234-cats]: \`check\` request successful",
1990+
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
19841991
"SubscriptionCallback[1234-cats]: Starting graphql-js subscription",
19851992
"SubscriptionCallback[1234-cats]: graphql-js subscription successful",
1986-
"SubscriptionManager[1234-cats]: Starting new heartbeat interval for http://mock-router-url.com",
19871993
"SubscriptionManager[1234-cats]: Listening to graphql-js subscription",
19881994
"SubscriptionCallback[1234-cats]: Responding to original subscription request",
19891995
"SubscriptionManager: Sending \`check\` request to http://mock-router-url.com for ID: 1234-cats",

0 commit comments

Comments
 (0)