Skip to content

Commit d75205c

Browse files
committed
fix(otlp-exporter-base): cover compression with the export timeout
- Start the abort timer before compressing, so compression time counts toward the timeout - Pass the timeout signal to the compression pipe, so a stalled compressor fails the export instead of hanging
1 parent c9decf5 commit d75205c

2 files changed

Lines changed: 88 additions & 22 deletions

File tree

‎experimental/packages/otlp-exporter-base/src/platform/browser/transport/fetch-transport.ts‎

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,16 @@ class FetchTransport implements IExporterTransport {
7777
// would not survive.
7878
const suppressedContext = suppressTracing(context.active());
7979

80+
const abortController = new AbortController();
81+
const timeout = setTimeout(() => abortController.abort(), timeoutMillis);
82+
8083
// Compressed first so that the keepalive budget counts the bytes sent.
8184
const compressed =
8285
this._compression === 'none'
8386
? undefined
84-
: await this._compress(data, this._compression);
87+
: await this._compress(data, this._compression, abortController.signal);
8588
const body = compressed ?? data;
8689

87-
const abortController = new AbortController();
88-
const timeout = setTimeout(() => abortController.abort(), timeoutMillis);
8990
// Fetch API may be wrapped by an instrumentation like `@opentelemetry/instrumentation-fetch`.
9091
// In that case the instrumentation would create a new Span for this request,
9192
// creating an indirect endless loop Export -> Span -> Export.
@@ -212,14 +213,19 @@ class FetchTransport implements IExporterTransport {
212213

213214
private async _compress(
214215
data: Uint8Array,
215-
format: CompressionFormat
216+
format: CompressionFormat,
217+
signal: AbortSignal
216218
): Promise<Blob | undefined> {
217219
try {
218-
return await compressWithCompressionStream(data, format);
220+
return await compressWithCompressionStream(data, format, signal);
219221
} catch (error) {
220-
diag.warn(
221-
`${format} compression failed, sending export uncompressed: ${error}`
222-
);
222+
// Not warned on timeout: `fetch` rejects on the aborted signal, so nothing
223+
// is sent.
224+
if (!signal.aborted) {
225+
diag.warn(
226+
`${format} compression failed, sending export uncompressed: ${error}`
227+
);
228+
}
223229
return undefined;
224230
}
225231
}
@@ -318,25 +324,26 @@ function isCompressionStreamFormatSupported(
318324

319325
async function compressWithCompressionStream(
320326
data: Uint8Array,
321-
format: CompressionFormat
327+
format: CompressionFormat,
328+
signal: AbortSignal
322329
): Promise<Blob> {
323-
// Read with a stream reader: `Response.arrayBuffer()` waits for a task in
324-
// some engines, which never runs once the page unloads.
325-
const reader = new ReadableStream<Uint8Array>({
330+
// Read with `pipeTo`: `Response.arrayBuffer()` waits for a task in some
331+
// engines, which never runs once the page unloads.
332+
const chunks: Uint8Array[] = [];
333+
await new ReadableStream<Uint8Array>({
326334
start(controller) {
327335
controller.enqueue(data);
328336
controller.close();
329337
},
330338
})
331339
.pipeThrough(new globalThis.CompressionStream(format))
332-
.getReader();
333-
334-
const chunks: Uint8Array[] = [];
335-
for (;;) {
336-
const { done, value } = await reader.read();
337-
if (done) {
338-
return new Blob(chunks);
339-
}
340-
chunks.push(value);
341-
}
340+
.pipeTo(
341+
new WritableStream({
342+
write(chunk) {
343+
chunks.push(chunk);
344+
},
345+
}),
346+
{ signal }
347+
);
348+
return new Blob(chunks);
342349
}

‎experimental/packages/otlp-exporter-base/test/browser/fetch-transport.test.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,65 @@ describe('FetchTransport', function () {
13341334
sinon.assert.alwaysCalledWithMatch(warn, 'gzip compression failed');
13351335
});
13361336

1337+
it('fails when compression takes longer than the timeout', async function () {
1338+
// arrange
1339+
// Barely compressible, so gzip tasks far longer than the timeout.
1340+
const slowPayload = new Uint8Array(4 * 1024 * 1024); // 4MB
1341+
for (let i = 0; i < slowPayload.length; i += 65536) {
1342+
crypto.getRandomValues(slowPayload.subarray(i, i + 65536));
1343+
}
1344+
// Settles after a task, as a real request does, so a due timer runs first.
1345+
sinon.stub(globalThis, 'fetch').callsFake(
1346+
(_input, init) =>
1347+
new Promise((resolve, reject) => {
1348+
init?.signal?.addEventListener('abort', () => {
1349+
reject(init.signal?.reason);
1350+
});
1351+
setTimeout(() => resolve(new Response('', { status: 200 })), 0);
1352+
})
1353+
);
1354+
const transport = createFetchTransport(gzipTransportParameters);
1355+
const shortTimeout = 10;
1356+
1357+
// act
1358+
const result = await transport.send(slowPayload, shortTimeout);
1359+
1360+
// assert
1361+
assert.strictEqual(result.status, 'failure');
1362+
});
1363+
1364+
it('aborts a stalled compression and fails without sending when the timeout fires', async function () {
1365+
// arrange
1366+
const cancel = sinon.spy();
1367+
sinon.stub(globalThis, 'CompressionStream').value(
1368+
class {
1369+
readonly writable = new WritableStream<Uint8Array>();
1370+
readonly readable = new ReadableStream<Uint8Array>({ cancel });
1371+
}
1372+
);
1373+
const fetchStub = sinon
1374+
.stub(globalThis, 'fetch')
1375+
.callsFake(async (_input, init) => {
1376+
init?.signal?.throwIfAborted();
1377+
return new Response('', { status: 200 });
1378+
});
1379+
const { warn } = registerMockDiagLogger();
1380+
const transport = createFetchTransport(gzipTransportParameters);
1381+
const shortTimeout = 10;
1382+
1383+
// act
1384+
const result = await transport.send(testPayload, shortTimeout);
1385+
1386+
// assert
1387+
assert.strictEqual(result.status, 'failure');
1388+
assert.strictEqual(
1389+
(fetchStub.firstCall.args[1] as RequestInit).signal?.aborted,
1390+
true
1391+
);
1392+
sinon.assert.calledOnce(cancel);
1393+
sinon.assert.notCalled(warn);
1394+
});
1395+
13371396
it('compresses the body again on retry', async function () {
13381397
// arrange
13391398
const fetchStub = sinon.stub(globalThis, 'fetch');

0 commit comments

Comments
 (0)