OtelMetrics.registerProducer: readers bound before one that fails to bind are never shut down and keep exporting
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 75/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- observability
Research direction
The acquire step of registerProducer in packages/opentelemetry/src/OtelMetrics.ts (around lines 82-99) binds each reader in one Effect.sync, so a throw partway through leaves earlier readers bound with no finalizer. Read that acquire block and the acquireRelease finalizer, then run the two registerProducer cases in packages/opentelemetry/test/OtelMetrics.test.ts. Done when the test that expects already-bound readers to be shut down after a failed bind passes, and the existing shutdown-on-scope-close test still passes.
Written by the indexing model from the issue text.
Description
What version of Effect is running?
4.0.5 (effect, @effect/opentelemetry; @opentelemetry/sdk-metrics 2.12.0, Node 26.11.1). registerProducer is unchanged on main at c9a1e72.
What steps can reproduce the bug?
When the reader thunk given to OtelMetrics.layer or registerProducer returns several readers and a later one throws from setMetricProducer, the build dies but the earlier readers stay bound and are never shut down. For example, the later reader may already be registered with a MeterProvider, so the SDK throws "MetricReader can not be bound to a MeterProvider again.". A PeriodicExportingMetricReader among the earlier readers keeps exporting Effect metrics after the scope has closed. If the build failure is handled, for example with a Layer.catchCause fallback, it keeps exporting for the life of the process with no owner.
This test, added to packages/opentelemetry/test/OtelMetrics.test.ts (with Cause and Exit imports), fails on main. The first case passes:
describe("registerProducer", () => {
class Reader extends MetricReader {
shutdowns = 0
protected onShutdown(): Promise<void> {
this.shutdowns++
return Promise.resolve()
}
protected onForceFlush(): Promise<void> {
return Promise.resolve()
}
}
it.effect("shuts down every reader when the scope closes", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const producer = new internal.MetricProducerImpl(resourceFromAttributes({}), services)
const first = new Reader()
const second = new Reader()
yield* OtelMetrics.registerProducer(producer, () => [first, second]).pipe(Effect.scoped)
assert.deepStrictEqual([first.shutdowns, second.shutdowns], [1, 1])
}))
it.effect("shuts down readers bound before a reader that fails to bind", () =>
Effect.gen(function*() {
const services = yield* Effect.context<never>()
const producer = new internal.MetricProducerImpl(resourceFromAttributes({}), services)
const fresh = new Reader()
const alreadyBound = new Reader()
alreadyBound.setMetricProducer(producer)
const exit = yield* OtelMetrics.registerProducer(producer, () => [fresh, alreadyBound]).pipe(
Effect.scoped,
Effect.exit
)
assert.isTrue(Exit.isFailure(exit) && Cause.hasDies(exit.cause))
assert.strictEqual(fresh.shutdowns, 1)
}))
})
What is the expected behavior?
Every reader that registerProducer bound is shut down when the acquisition fails, as the release does when the scope closes. Two separate registerProducer calls in one scope already do this: when the second call dies, the first call's finalizer shuts its reader down.
What do you see instead?
expected +0 to equal 1: fresh is bound but never shut down.
The published 4.0.5 packages behave the same way. In that run the readers were PeriodicExportingMetricReaders with a 50 ms interval. In the 300 ms after the failed Effect.provide(OtelMetrics.layer(() => [a, alreadyBound])), reader a exported 5 more times, and its exporter's shutdown was never called.
Additional information
The acquire binds every reader inside one Effect.sync (OtelMetrics.ts#L82-L99). When setMetricProducer throws for a later reader, the earlier ones are already bound. The throw becomes a defect, and acquireRelease adds its finalizer only after acquisition succeeds (effect.ts#L4153-L4155), so nothing shuts them down. main still has the same code. Shutting down the readers bound so far when a later bind throws would cover it. The v3 Metrics.registerProducer (0.64.1) binds readers the same way.
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 808
- Avg merge
- 9h 39m
- Merged PRs (30d)
- 703
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Effect-TS/effect
-
BrowserWorkerRunner: port finalizer throws when the worker global has no close() (Bun)Possibly taken @santiago-ramos-02 claimed this 16 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Effect-TS/effect#8635 · 3 comments ·
Maintainers usually reply within 1 day
-
Support {self: this} for fnUntracedMay be free again @ArjunCodess claimed this 28 days ago, and no pull request is open. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Effect-TS/effect#8101 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 45/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 Half a day Newbie friendliness 52/100
Maintainers usually reply within 1 day
-
RcMap, RcRef and ScopedCache: a dying release is lost on idle expiry, an interrupted lookup, or expiry during `keys`/`values`/`entries`Possibly taken @effect-bot claimed this 1 day ago. Open
Effect-TS/effect#9372 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in Effect-TS/effect
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Chocobozzz/PeerTube#7832 ·
Maintainers usually reply within 3 days
-
Pay to use?Open
Difficulty 1/5 Under an hour Newbie friendliness 66/100
darkreader/darkreader#16108 · 1 comment ·
Maintainers usually reply within 2 days
-
Add: Dak Nong TV SDOpencheck:passed streams:add
Difficulty 1/5 Under an hour Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
bug engine
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
frostney/GocciaScript#1746 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
agilepathway/label-checker#739 · 4 comments ·
Maintainers usually reply within 1 day