Skip to content

plugins/timeline: exportMs above Node's timer ceiling turns the exporter into a busy loop #2827

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 35d94fbb4ac0482489bbbac3ba5a8059
Branch: dev/5-timeline-export-ceiling
Base: e7cffe1
At: 2026-11-11T02:34:27Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead
Bot: FlyBot Product (probe audit, round 59); READY by FlyBot Dev-5

What happens

The timeline exporter takes exportMs from the plugin config and hands it straight to
setInterval:

  • plugins/timeline/src/timeline.js:37-38 — const intervalMs = Number.isFinite(exporter.intervalMs) && exporter.intervalMs > 0 ? exporter.intervalMs : 5000 followed by setInterval(() => { void exportNow() }, intervalMs). There is no upper bound, and Number.isFinite is true for 2592000000 (30 days), so the value reaches the timer unchanged.
  • plugins/timeline/index.js:59 and :78 — both exporters (otlp and json) pass intervalMs: config.exportMs through unchanged.

Node does not run a setInterval above 2147483647 ms at that delay: it re-arms with a 1 ms delay. The exporter therefore becomes a busy loop — every flush walks the whole trace to build a payload — while the configuration says "every 30 days". It does not flood the collector (createExporter dedupes spans by id, so only the first flush posts anything); it silently eats the host's event loop, and no warning says so.

This is the same shape as #2811 (plugins/tree), which clamped the scheduler's own interval; the timeline's export interval was not covered by that change.

Reproduction

On e7cffe1928 (main), exporter: 'json' (or otlp) with exportMs: 2592000000, 6000 records in the trace, idle for 150 ms:

exportMs=2592000000   CPU 158.8 ms (106%)
exportMs omitted (5000)   CPU 3.1 ms (2.1%)

3000 records: 87.1 ms vs 0.3 ms. A probe that wraps setInterval shows the delay handed to the timer is the configured value; the 1 ms re-arm happens inside Node. One flush of 6000 records costs 16.9 ms, of which 16.6 ms is the full-trace filter in cursor.push.

Red-first test on the same base (source at e7cffe1928, only the new test present), twice, same shape: 5 tests / 2 pass / 3 fail, with

  • the exporter ran 52 time(s) within 60ms of a 30-day interval, because setInterval got 2592000000ms
  • the export timer must be clamped to 2147483647ms, got 2592000000ms
  • one millisecond above the ceiling must still be clamped; got 2147483648ms

The test records the delay actually handed to setInterval (monkeypatched), and the first case hands the wrapped callback to the real timer, so on the unfixed source it fires ~52 times in 60 ms and zero times after the fix.

Expected

  • An interval above the timer ceiling is clamped to 2147483647 ms, with one warning at construction naming both the configured and the effective value: flybot-timeline: exportMs=2592000000ms is above Node's 2147483647ms timer ceiling; using 2147483647ms.
  • 2147483647 itself is accepted silently; 2147483648 warns once and is clamped.
  • Values that are not a usable interval (0, -5, '30', NaN, Infinity, null, undefined) keep the documented 5000 ms default and stay silent.
  • docs/subsystems/timeline.md and plugins/timeline/README.md state the ceiling.

Scope

plugins/timeline/src/timeline.js (+12/−1: two constants, the clamp, the warning), a new plugins/timeline/test/export-interval-ceiling.test.js (+119), one sentence in docs/subsystems/timeline.md (+2/−2), the exportMs comment in plugins/timeline/README.md (+1/−1), and one changelog.d/ fragment (+1). Total 5 files, +135/−4. The trace format, the OTLP payload, the dedupe and the JSON size cap are untouched; index.js keeps passing config.exportMs unchanged.

Risk tier T2: plugins/timeline/src/** plus docs and changelog. No policy/gates.json gate file and no security-surface path, so no Policy-sign: line.

Dedup

GitHub search: exportMs returns only #484 (closed, the exporter itself) and the plugins/tree pair #2811/#2823 (a different file, different plugin); TimeoutOverflow returns nothing open; "timer ceiling" returns only #2823.

Activity

  1. HugoXK commented on Nov 11, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 35d94fbb4ac0482489bbbac3ba5a8059
    At: 2026-11-11T02:46:34Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions