Skip to content

Commit 7219465

Browse files
authored
Merge pull request #27408 from backstage/patch-release-pr-27407
Patch release of #27407
2 parents ed98ca6 + b7ea044 commit 7219465

File tree

5 files changed

+116
-25
lines changed

5 files changed

+116
-25
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "root",
3-
"version": "1.32.4",
3+
"version": "1.32.5",
44
"private": true,
55
"repository": {
66
"type": "git",

plugins/events-node/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @backstage/plugin-events-node
22

3+
## 0.4.4
4+
5+
### Patch Changes
6+
7+
- f7ca00b: Fixed an issue where the event bus polling would duplicate and increase exponentially over time.
8+
39
## 0.4.3
410

511
### Patch Changes

plugins/events-node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@backstage/plugin-events-node",
3-
"version": "0.4.3",
3+
"version": "0.4.4",
44
"description": "The plugin-events-node module for @backstage/plugin-events-backend",
55
"backstage": {
66
"role": "node-library",

plugins/events-node/src/api/DefaultEventsService.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,94 @@ describe('DefaultEventsService', () => {
160160
await (service as any).shutdown();
161161
});
162162

163+
it('should wait an poll on timeout', async () => {
164+
const logger = mockServices.logger.mock();
165+
const service = DefaultEventsService.create({ logger }).forPlugin('a', {
166+
auth: mockServices.auth(),
167+
logger,
168+
discovery: mockServices.discovery(),
169+
lifecycle: mockServices.lifecycle.mock(),
170+
});
171+
172+
let callCount = 0;
173+
174+
let blockingController: ReadableStreamDefaultController;
175+
const blockingStream = new ReadableStream({
176+
start(controller) {
177+
blockingController = controller;
178+
},
179+
});
180+
181+
mswServer.use(
182+
rest.put(
183+
'http://localhost:0/api/events/bus/v1/subscriptions/a.tester',
184+
(_req, res, ctx) => res(ctx.status(200)),
185+
),
186+
// The first and third calls result in a blocking 202 that is resolved after 100ms
187+
// The second and fourth calls result in a 200 with an event
188+
// The fifth call blocks until the end of the test
189+
// No more than 5 calls should be made
190+
rest.get(
191+
'http://localhost:0/api/events/bus/v1/subscriptions/a.tester/events',
192+
(_req, res, ctx) => {
193+
callCount += 1;
194+
if (callCount === 1 || callCount === 3) {
195+
return res(
196+
ctx.status(202),
197+
ctx.body(
198+
new ReadableStream({
199+
start(controller) {
200+
setTimeout(() => controller.close(), 100);
201+
},
202+
}),
203+
),
204+
);
205+
} else if (callCount === 2 || callCount === 4) {
206+
return res(
207+
ctx.status(200),
208+
ctx.json({
209+
events: [{ topic: 'test', payload: { callCount } }],
210+
}),
211+
);
212+
} else if (callCount === 5) {
213+
return res(ctx.status(202), ctx.body(blockingStream));
214+
}
215+
throw new Error(`events endpoint called too many times`);
216+
},
217+
),
218+
);
219+
220+
const event = await new Promise(resolve => {
221+
const events = new Array<EventParams>();
222+
service.subscribe({
223+
id: 'tester',
224+
topics: ['test'],
225+
async onEvent(newEvent) {
226+
events.push(newEvent);
227+
if (events.length === 2) {
228+
resolve(events);
229+
}
230+
},
231+
});
232+
});
233+
234+
expect(event).toEqual([
235+
{ topic: 'test', eventPayload: { callCount: 2 } },
236+
{ topic: 'test', eventPayload: { callCount: 4 } },
237+
]);
238+
239+
// Wait to make sure no additional calls happen
240+
await new Promise(resolve => setTimeout(resolve, 100));
241+
242+
expect(callCount).toBe(5);
243+
244+
// Internal call to clean up subscriptions
245+
await (service as any).shutdown();
246+
247+
// Close the stream for the 5th call so that we don't leave the request hanging
248+
blockingController!.close();
249+
});
250+
163251
it('should not read events from bus if disabled', async () => {
164252
const logger = mockServices.logger.mock();
165253
const service = DefaultEventsService.create({

plugins/events-node/src/api/DefaultEventsService.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -207,28 +207,17 @@ class PluginEventsService implements EventsService {
207207
{ token },
208208
);
209209

210-
if (!res.ok) {
211-
if (res.status === 404) {
212-
this.logger.info(
213-
`Polling event subscription resulted in a 404, recreating subscription`,
214-
);
215-
hasSubscription = false;
216-
} else {
217-
throw await ResponseError.fromResponse(res);
218-
}
219-
}
220-
221-
// Successful response, reset backoff
222-
backoffMs = POLL_BACKOFF_START_MS;
223-
224-
// 202 means there were no immediately available events, but the
225-
// response will block until either new events are available or the
226-
// request times out. In both cases we should should try to read events
227-
// immediately again
228210
if (res.status === 202) {
211+
// 202 means there were no immediately available events, but the
212+
// response will block until either new events are available or the
213+
// request times out. In both cases we should should try to read events
214+
// immediately again
215+
229216
lock.release();
230-
await res.body?.getReader()?.closed;
231-
process.nextTick(poll);
217+
// We don't actually expect any response body here, but waiting for
218+
// an empty body to be returned has been more reliable that waiting
219+
// for the response body stream to close.
220+
await res.text();
232221
} else if (res.status === 200) {
233222
const data = await res.json();
234223
if (data) {
@@ -245,10 +234,15 @@ class PluginEventsService implements EventsService {
245234
);
246235
}
247236
}
248-
} else {
249-
this.logger.warn(
250-
`Unexpected response status ${res.status} from events backend for subscription "${subscriptionId}"`,
237+
}
238+
} else {
239+
if (res.status === 404) {
240+
this.logger.info(
241+
`Polling event subscription resulted in a 404, recreating subscription`,
251242
);
243+
hasSubscription = false;
244+
} else {
245+
throw await ResponseError.fromResponse(res);
252246
}
253247
}
254248
}
@@ -276,6 +270,9 @@ class PluginEventsService implements EventsService {
276270
}
277271
}
278272

273+
// No errors, reset backoff
274+
backoffMs = POLL_BACKOFF_START_MS;
275+
279276
process.nextTick(poll);
280277
} catch (error) {
281278
this.logger.warn(

0 commit comments

Comments
 (0)