Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

OtelMetrics.registerProducer: readers bound before one that fails to bind are never shut down and keep exporting

Open Beginner friendly
#9,356 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Effect-TS/effect

All issues in Effect-TS/effect

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.