Skip to content

Dispose the HttpClient when the target closes or reloads - #24

Merged
PetrHeinz merged 9 commits into
mainfrom
claude/dispose-client
Sep 30, 2026
Merged

PetrHeinz merged 9 commits into
mainfrom
claude/dispose-client

Conversation

@PetrHeinz

Copy link
Copy Markdown
Member

Stacked on #19, which is stacked on #17: the base is claude/max-flush-time, because this builds on its stopDrain. Merge it after #17 and #19.

Every InitializeTarget creates a new Client with its own HttpClient, and neither CloseTarget nor a configuration reload (autoReload) disposes the previous one. Each reload leaks a handler and its pooled connections.

  • Client implements IDisposable and disposes its HttpClient. This adds to the public API; nothing is removed.
  • The target keeps the client it created. stopDrain disposes it when the drain's Stop() task completes, whether or not the bounded wait ran out. It continues the stop task with the disposal instead of disposing right after the wait, so a drain that is still retrying in the background keeps a working client until its loop ends, and the process exit is not held up. Each client is disposed once, also when a closed target is initialized again.
  • Drain is unchanged.

The target-level test points two targets (before and after a reload) at a plain TCP server that answers with 202 and keeps the connections open. It checks that the server sees both connections end after the reload and Shutdown(), with no internal hook in the library.

The first commit adds the tests and is expected to be red: it does not compile without Client.Dispose(). With an empty Dispose() added, the disposed client still sends and both connections stay open. The second commit adds the disposal.

🤖 Generated with Claude Code

PetrHeinz and others added 8 commits September 30, 2026 15:38
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The target now overrides FlushAsync: the drain wakes up, sends what is queued and completes the flush once everything enqueued before the call has been sent or given up on.

Based on the FlushAsync support in #7.

Co-authored-by: Rolf Kristensen <11509660+snakefoot@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tests use the new maxFlushTimeMilliseconds option, so this commit does not compile yet. With only the property added, shutdown takes 45 seconds while the endpoint keeps failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the target waited for queued logs without a limit, so an endpoint that cannot be reached held the application's shutdown for 45 seconds or more per batch. The new maxFlushTimeMilliseconds option (default 30000, 0 = no limit) bounds that wait, like maxFlushTime in the Java client; when it runs out, a warning goes to NLog's internal log.

Based on the bounded CloseTarget wait in #7.

Co-authored-by: Rolf Kristensen <11509660+snakefoot@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stacks this PR on #17. With both, LogFactory.Shutdown() first waits up to NLog's 15 s flush timeout for the drain and then up to maxFlushTimeMilliseconds more on close, so StopsWaitingForDeliveryOnShutdownAfterMaxFlushTime fails (about 15.5 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LogFactory.Shutdown() flushes all targets with NLog's own 15 s timeout and then closes them, so with FlushAsync waiting for the drain, closing added maxFlushTimeMilliseconds on top: 15 s + 30 s with the defaults.

FlushAsync now completes after at most maxFlushTimeMilliseconds, with a warning in the internal log when it gives up. The time spent waiting on a flush the drain has not delivered yet counts against the close that follows, so a shutdown waits maxFlushTimeMilliseconds in total: with the defaults NLog's flush gives up at 15 s and the close waits the remaining 15 s. A delivered flush does not count, and the drain keeps retrying in the background as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client has no Dispose() yet, so this commit does not compile. With an empty Dispose(), the disposed client still sends, and a raw http server sees the connections of a reloaded and of a closed target stay open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client now implements IDisposable. The target keeps the client it created and disposes it when the drain's Stop() task completes, also when that is after the bounded wait ran out, so a drain still retrying in the background keeps a working client and the process exit is not held up. Each client is disposed once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PetrHeinz
PetrHeinz marked this pull request as ready for review September 30, 2026 14:54
@PetrHeinz
PetrHeinz changed the base branch from claude/max-flush-time to main September 30, 2026 17:31
# Conflicts:
#	BetterStack.Logs.NLog/BetterStackLogsTarget.cs
#	tests/BetterStack.Logs.NLog.Tests/BetterStackLogsTargetTests.cs
@PetrHeinz
PetrHeinz merged commit 31bcc3b into main Sep 30, 2026
16 checks passed
@PetrHeinz
PetrHeinz deleted the claude/dispose-client branch September 30, 2026 17:37
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