Skip to content

feat(sdk): add explicit connection release to Python and Go clients - #372

Merged
benbenbang merged 7 commits into
mainfrom
feat/371-enums-close
Sep 30, 2026
Merged

benbenbang merged 7 commits into
mainfrom
feat/371-enums-close

Conversation

@benbenbang

Copy link
Copy Markdown
Member

Users had no way to explicitly release the pooled HTTP connections held by our SDK clients, forcing them to rely on garbage collection or process exit. This adds first-class lifecycle control across both the Python and Go SDKs.

Python client lifecycle:

  • Add FmpClient.close() to release pooled connections and reject all later calls (including through previously fetched namespaces) with FmpConfigError; closing is idempotent and in-flight calls finish normally.
  • Support the context manager protocol so with FmpClient(...) as client: closes the client on block exit while propagating any exception, with __exit__ returning None.
  • Introduce a shared ClientHandle wrapping the builder with an atomic closed flag, checked under the same mutex used to evict and drop the pooled client, and rework the runtime and every namespace to use it.

Go client lifecycle:

  • Add Client.CloseIdleConnections() to close idle keep-alive connections on the SDK-owned transport while keeping the client usable; make it a no-op when the transport is caller-supplied via WithHTTPClient, tracked by a new ownsTransport flag.

Tests and docs:

  • Cover close, context manager semantics, idempotency, exception propagation, and runtime isolation for the Python client, and verify connection reuse and ownership handling for the Go client.
  • Document the new behaviour in the crate and Go SDK READMEs and the Python and Go wikis, clarifying when unclosed clients release pooled connections.
  • Note that generated wire enums are intentionally #[non_exhaustive].

Closes #371

Switch generated namespace structs from `Arc<ClientBuilder>` to
`Arc<ClientHandle>`, updating the emitter to import `ClientHandle`
alongside `block_on` and adjusting the corresponding test expectations.

Also document that every wire enum is intentionally `#[non_exhaustive]`
so the provider can add values without a breaking release.
This change lets users release an `FmpClient`'s pooled HTTP connections
explicitly and use the client as a context manager.

**New client lifecycle:**
* Added `FmpClient.close()`, which releases the client's pooled
  connections and rejects every later call, including calls through a
  namespace fetched before the close, with `FmpConfigError`. Closing is
  idempotent and calls already in flight finish normally.
* Added `__enter__` and `__exit__` so `with FmpClient(...) as client:`
  closes the client when the block exits and lets any exception from the
  block propagate.

**Shared client handle:**
* Introduced `ClientHandle`, wrapping the `ClientBuilder` with an atomic
  closed flag shared by the client and every namespace it hands out. The
  client cache reads the flag under the same mutex `close()` uses to
  evict, so a closed handle is never re-cached and its pooled client is
  dropped to release idle connections.
* Reworked `runtime::block_on` and all namespaces to take `ClientHandle`
  instead of `ClientBuilder`, returning a configuration error when the
  handle is closed.
Add tests verifying that `close()` releases pooled connections, that the
client works as a context manager returning itself, and that closed
clients raise `FmpConfigError("the client is closed")` on any call,
including through namespaces captured before close. Also cover idempotent
close, exception propagation from the `with` block, and isolation between
clients sharing the process-wide runtime.

Document the close and context manager behaviour in the crate README and
the Python wiki.
Add `Client.CloseIdleConnections()`, which closes the idle keep-alive
connections of the transport `NewClient` built and leaves the client
usable for later requests. When `WithHTTPClient` supplies the
`*http.Client`, the call is a no-op so the caller-owned transport and its
lifecycle stay untouched. A `Client` owns no resources beyond that
connection pool, so there is no `Close`.

Track ownership with a new `ownsTransport` flag set when the SDK builds
its own transport, and gate the close on it. Cover both paths with tests:
one asserts the owned connection is closed and the client still works,
the other asserts a caller transport is left alone. Document the method
in the Go SDK README and wiki.

close #371
The context manager `__exit__` previously returned `False`, which is
functionally correct for propagation but relied on returning a boolean.
Change it to return `None` so exceptions from the block propagate cleanly
and the type stub accurately reflects the behaviour.

Added a test asserting `__exit__(None, None, None)` returns `None`.
Strengthen the CloseIdleConnections test by tracking dial counts through
the server's ConnState callback. The test now probes the client twice to
confirm a single connection is reused before CloseIdleConnections, and
verifies that a fresh connection is established afterward.
Update the Python client docs to describe more precisely when an
unclosed client releases its pooled connections: on the next request
made through any client after garbage collection, or at process exit.
This replaces the vaguer "lazily, some time after" wording.
@benbenbang
benbenbang enabled auto-merge (rebase) September 30, 2026 00:03
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request labels Sep 30, 2026
@benbenbang
benbenbang merged commit 1a045a9 into main Sep 30, 2026
9 checks passed
@benbenbang
benbenbang deleted the feat/371-enums-close branch September 30, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat!: non_exhaustive open query enums and FmpClient close/with (1.2.0)

1 participant