Skip to content

Commit bac7f81

Browse files
authored
fix: Disable event processor background flush timers for edge clients (#1797)
1 parent 5c12f19 commit bac7f81

6 files changed

Lines changed: 58 additions & 3 deletions

File tree

packages/sdk/fastly/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,18 @@ yarn add @launchdarkly/fastly-server-sdk
2222

2323
- The SDK must be initialized and used when processing requests, not during build-time initialization.
2424
- The SDK caches all KV data during initialization to reduce the number of backend requests needed to fetch KV data. This means changes to feature flags or segments will not be picked up during the lifecycle of a single request instance.
25-
- Events should flushed using the [`waitUntil()` method](https://js-compute-reference-docs.edgecompute.app/docs/globals/FetchEvent/prototype/waitUntil).
25+
26+
### Events and the client lifecycle
27+
28+
The SDK is designed to be created per request: call `init()` inside your request handler, and flush events at the end of each request using the [`waitUntil()` method](https://js-compute-reference-docs.edgecompute.app/docs/globals/FetchEvent/prototype/waitUntil):
29+
30+
```js
31+
event.waitUntil(ldClient.flush());
32+
```
33+
34+
The SDK does not run a periodic background flush. On [Fastly Compute](https://www.fastly.com/documentation/guides/compute/developer-guides/sandbox-lifecycle/), outbound requests must be made while a request is being handled, and by default each request runs in a fresh sandbox that is torn down afterward, so a background timer cannot reliably deliver events. Flushing explicitly with `waitUntil()` on every request that evaluates flags is the supported pattern -- it keeps the sandbox alive just long enough to send the events after the response is returned.
35+
36+
If you enable Fastly's opt-in [reusable sandboxes](https://www.fastly.com/documentation/guides/compute/developer-guides/sandbox-lifecycle/), a single sandbox may serve multiple requests, so avoid holding a single client at module scope and relying on it to flush on its own: buffered events would accumulate and, once the event queue capacity is reached, be dropped. Creating a client per request and flushing via `waitUntil()` avoids this regardless of sandbox mode.
2637

2738
## Quickstart
2839

packages/sdk/fastly/__tests__/api/LDClient.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ describe('Edge LDClient', () => {
4444
},
4545
});
4646
});
47+
it('constructs the event processor without background flush timers', async () => {
48+
const client = new LDClient('client-side-id', createBasicPlatform().info, {
49+
sendEvents: true,
50+
eventsBackendName: 'launchdarkly',
51+
});
52+
await client.waitForInitialization({ timeout: 10 });
53+
54+
// The 6th positional argument to EventProcessor is `start`. Edge clients run per
55+
// request and flush explicitly, so they must not start the background flush timers.
56+
expect(mockEventProcessor).toHaveBeenCalled();
57+
expect(mockEventProcessor.mock.calls[0][5]).toBe(false);
58+
});
59+
4760
it('uses custom eventsUri when specified', async () => {
4861
const client = new LDClient('client-side-id', createBasicPlatform().info, {
4962
sendEvents: true,

packages/sdk/fastly/src/api/LDClient.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Info, internal, LDClientImpl } from '@launchdarkly/js-server-sdk-common';
1+
import { Info, LDClientImpl, ServerInternalOptions } from '@launchdarkly/js-server-sdk-common';
22

33
import EdgePlatform from '../platform';
44
import { FastlySDKOptions } from '../utils/validateOptions';
@@ -18,10 +18,16 @@ export default class LDClient extends LDClientImpl {
1818
platformInfo,
1919
eventsBackendName || DEFAULT_EVENTS_BACKEND_NAME,
2020
);
21-
const internalOptions: internal.LDInternalOptions = {
21+
const internalOptions: ServerInternalOptions = {
2222
analyticsEventPath: `/events/bulk/${clientSideID}`,
2323
diagnosticEventPath: `/events/diagnostic/${clientSideID}`,
2424
includeAuthorizationHeader: false,
25+
// Fastly Compute runs a fresh instance per request and flushes events explicitly
26+
// via the request handler (event.waitUntil(client.flush())). Disable the event
27+
// processor's background flush timers so a per-request client does not leave
28+
// interval timers running that would keep the runtime alive and pin the client
29+
// graph in memory.
30+
disableBackgroundEventFlush: true,
2531
};
2632

2733
const finalOptions = createOptions(ldOptions);

packages/shared/sdk-server/src/LDClientImpl.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function constructFDv1(
116116
dataSourceErrorHandler: (e: any) => void,
117117
hooks: Hook[],
118118
instanceId: string | undefined,
119+
startEventProcessor: boolean,
119120
): {
120121
config: Configuration;
121122
logger: LDLogger | undefined;
@@ -170,6 +171,7 @@ function constructFDv1(
170171
baseHeaders,
171172
new ContextDeduplicator(config),
172173
diagnosticsManager,
174+
startEventProcessor,
173175
);
174176
}
175177

@@ -253,6 +255,7 @@ function constructFDv2(
253255
initSuccess: () => void,
254256
hooks: Hook[],
255257
instanceId: string | undefined,
258+
startEventProcessor: boolean,
256259
): {
257260
config: Configuration;
258261
logger: LDLogger | undefined;
@@ -314,6 +317,7 @@ function constructFDv2(
314317
baseHeaders,
315318
new ContextDeduplicator(config),
316319
diagnosticsManager,
320+
startEventProcessor,
317321
);
318322
}
319323

@@ -587,6 +591,10 @@ export default class LDClientImpl implements LDClient {
587591
) {
588592
const config = new Configuration(options, internalOptions);
589593

594+
// Edge SDKs run per request and flush events explicitly, so they opt out of the
595+
// event processor's background flush timers to avoid leaving interval timers running.
596+
const startEventProcessor = !internalOptions?.disableBackgroundEventFlush;
597+
590598
this.environmentMetadata = createPluginEnvironmentMetadata(_platform, _sdkKey, config);
591599

592600
const hooks: Hook[] = [];
@@ -620,6 +628,7 @@ export default class LDClientImpl implements LDClient {
620628
(e) => this._dataSourceErrorHandler(e),
621629
hooks,
622630
internalOptions?.instanceId,
631+
startEventProcessor,
623632
));
624633

625634
this.bigSegmentStatusProviderInternal = this._bigSegmentsManager
@@ -657,6 +666,7 @@ export default class LDClientImpl implements LDClient {
657666
() => this._initSuccess(),
658667
hooks,
659668
internalOptions?.instanceId,
669+
startEventProcessor,
660670
));
661671
this._featureStore = transactionalStore;
662672
this.bigSegmentStatusProviderInternal = this._bigSegmentsManager

packages/shared/sdk-server/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export * from './events';
1111
export * from '@launchdarkly/js-sdk-common';
1212
export * as internalServer from './internal';
1313

14+
export type { ServerInternalOptions } from './options/ServerInternalOptions';
15+
1416
export {
1517
LDClientImpl,
1618
BigSegmentStoreStatusProviderImpl,

packages/shared/sdk-server/src/options/ServerInternalOptions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,17 @@ export interface ServerInternalOptions extends internal.LDInternalOptions {
1212
* undefined and the header is omitted.
1313
*/
1414
instanceId?: string;
15+
16+
/**
17+
* When true, the event processor is constructed without starting its periodic
18+
* background work (the flush and context-deduplication interval timers, and the
19+
* diagnostic timer when diagnostics are enabled). Events are still recorded and
20+
* are delivered only via explicit flush() calls.
21+
*
22+
* This is intended for per-request edge SDKs that flush explicitly (for example
23+
* via a runtime's waitUntil) and must not leave interval timers running. A live
24+
* interval timer keeps the runtime event loop alive and roots the whole client
25+
* graph in memory, which for a per-request client is a leak.
26+
*/
27+
disableBackgroundEventFlush?: boolean;
1528
}

0 commit comments

Comments
 (0)