Skip to content

feat: let flush() stop waiting on an AbortSignal - #41

Draft
bengl wants to merge 2 commits into
nodejs:mainfrom
bengl:bengl/flush-signal
Draft

bengl wants to merge 2 commits into
nodejs:mainfrom
bengl:bengl/flush-signal

Conversation

@bengl

@bengl bengl commented Nov 3, 2026

Copy link
Copy Markdown
Member

Builds on #32, so its first commit is #32's. The second commit is this change.

What changed. flush({ signal }) takes an AbortSignal, such as AbortSignal.timeout(ms), the way Node core APIs take one. When it aborts, the promise rejects with an AbortError shaped like core's: code ABORT_ERR, message The operation was aborted, and the signal's reason as its cause. Core builds that error from an internal class, so microtel builds the same shape. Export failures still resolve the promise, so only an abort the caller asked for rejects.

Aborting stops the waiting, not the export. The export keeps running, bounded by exportTimeout, and its spans still reach the collector if it succeeds. With a signal that has already aborted, the promise rejects right away and no export starts, so the spans stay buffered for the next flush. The listener goes on through addAbortListener(), so it runs even when an earlier listener stops the event's propagation, as core's own abort listeners do. It comes off once the flush settles, so one signal can serve many flushes.

Validation. Options must be an object and signal an AbortSignal, checked the way core checks them. validateAbortSignal is ported from core into lib/validate.js, with parity tests against core's own. The beforeExit handler now calls flush() with no arguments, since Node passes it the exit code.

Not taken. A separate timeout option. AbortSignal.timeout() already covers it.

Tests. In test/test-flush-promise.js:

  • no signal, and empty options, behave as before
  • invalid options and an invalid signal throw ERR_INVALID_ARG_TYPE
  • an already-aborted signal rejects and leaves the spans for a later flush, which exports them
  • aborting while an export is in flight rejects right away, and the export still finishes. The collector holds its answer until after the rejection, so a flush that waited instead would deadlock the child. A flush started after the abort stays pending until the held export is answered.
  • the flush still rejects when an earlier abort listener stops propagation
  • AbortSignal.timeout() rejects with its TimeoutError as the cause
  • the abort listener is gone once flush() settles

bengl added 2 commits November 2, 2026 16:28
flush() started an export and returned nothing, so code had no way to
wait for its spans to be sent. Shutdown handlers and serverless
functions need to, since explicit exits and frozen processes skip the
beforeExit flush.

flush() now returns a promise that resolves once the spans it took
from the buffer and every export already in flight are done. It never
rejects. Failed exports are already reported as OTelExportWarning
warnings, and a rejection would be unhandled in every caller that
ignores the result. An export that fails to send destroys its
request, so neither its socket nor its deadline outlives it.

The export benchmark awaits its flushes instead of counting requests
at the collector, and several tests await flush() instead of waiting
a fixed delay for an export that might not come.

Signed-off-by: Bryan English <bryan@bryanenglish.com>
Previously, a caller could not bound how long flush() waited. Now
flush({ signal }) takes an AbortSignal, such as AbortSignal.timeout(ms),
the way core APIs take one, and rejects with an AbortError shaped like
core's when it aborts. Export failures still resolve the promise.

Aborting stops the waiting, not the export, which keeps running,
bounded by exportTimeout. With a signal that has already aborted,
flush() rejects right away and starts no export, so the spans stay
buffered for the next flush. The abort listener runs even when an
earlier listener stops the event's propagation, as core's do.

validateAbortSignal is ported from core into lib/validate.js, with
parity tests.

Signed-off-by: Bryan English <bryan@bryanenglish.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant