Skip to content

Fix UDP socket leak in CentralDogma-to-CentralDogma mirroring - #1328

Merged
minwoox merged 5 commits into
line:mainfrom
minwoox:fix_leak
Jul 10, 2026
Merged

Fix UDP socket leak in CentralDogma-to-CentralDogma mirroring#1328
minwoox merged 5 commits into
line:mainfrom
minwoox:fix_leak

Conversation

@minwoox

@minwoox minwoox commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Motivation:

CentralDogma-to-CentralDogma mirrors were creating a new
ArmeriaCentralDogmaClient on every scheduler tick, allocating a fresh
Armeria WebClient, connection pool, and UDP socket each time. These
sockets were not reclaimed promptly, leading to too many open
files.

Modifications:

  • Add CentralDogma.withAccessToken(String) as a default interface method
    that returns a derived client sharing the same underlying WebClient.
    Calling close() on the derived client is a no-op; the base client owns
    the connection resources.
  • Add Mirror.setBaseClientPool(ConcurrentHashMap<String, Object>) as a
    default no-op method so MirrorSchedulingService can inject the pool
    without coupling the server module to client types.

Result:

  • Mirrors pointing at the same remote host share one Armeria WebClient,
    eliminating per-run DNS/TLS/connection overhead and stopping UDP socket
    accumulation.

Motivation:

CentralDogma-to-CentralDogma mirrors were creating and closing a new
CentralDogmaClient on every mirror run which is wasteful.

Modifications:

- Add CentralDogmaMirror.getOrCreateRemoteClient() that lazily creates
  and caches a single CentralDogmaClient per mirror instance, reusing
  it across all subsequent runs.
- Disable health checks (healthCheckIntervalMillis(0)) in the remote
  client: mirrors run on a fixed schedule and a failed run retries on
  the next tick, so persistent health-check requests serve no purpose.
- Add MirrorSchedulingService.activeMirrors (ConcurrentHashMap) to
  cache Mirror instances across scheduler ticks

Result:

- Each CentralDogmaMirror holds at most one remote client for its
  lifetime, eliminating per-run DNS/TLS/connection overhead and
  preventing UDP socket accumulation.
@minwoox minwoox added this to the 0.85.0 milestone Jul 8, 2026
@minwoox
minwoox requested review from ikhoon and jrhee17 as code owners July 8, 2026 07:15
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds derived-client access-token support, introduces shared client-pool injection in mirror scheduling, and updates CentralDogmaMirror to reuse pooled base clients while deriving authenticated clients. It also removes the runMigration constructor parameter from mirror scheduling.

Changes

Mirror and client flow

Layer / File(s) Summary
Client access-token derivation
client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogma.java, client/java/src/main/java/com/linecorp/centraldogma/client/AbstractCentralDogma.java, client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java, client/java/src/main/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogma.java
Adds withAccessToken(String) support across the client API and wrappers, and exposes the configured meter registry from the base client.
Mirror scheduling and pool injection
server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java, server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java, server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServicePlugin.java, it/mirror-listener/.../CustomMirrorListenerTest.java, server-mirror-git/.../MirrorSchedulingServiceTest.java
Adds mirror pool injection and lifecycle hooks, removes runMigration from scheduler construction, closes pooled resources on shutdown, and updates caller/test constructor invocations.
CentralDogmaMirror remote client reuse
server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java
Caches a remote CentralDogma client, derives authenticated per-run clients from the current credential, disables health-check traffic in the base client, and closes the cached client on shutdown.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: new feature

Suggested reviewers: trustin, ikhoon

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: reducing UDP socket leaks by reusing CentralDogma clients in mirroring.
Description check ✅ Passed The description accurately describes the client reuse, access-token support, and shared pool injection in the diff.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java`:
- Around line 290-302: The mirror replacement logic in MirrorSchedulingService
currently closes the previous mirror immediately inside activeMirrors.compute,
which can interrupt an in-flight sync in the worker. Update the active mirror
swap flow so replaced or deleted mirrors are not closed until any active runs
using them have finished, or introduce reference counting for the shared client
in CentralDogmaMirror to avoid shutting down the cached CentralDogma client
mid-run. Keep the existing identity check in the compute block, but defer the
existing.close() call to the point where the mirror is no longer in use.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 992e2c01-e94a-4945-961e-11a3ff3b0cc8

📥 Commits

Reviewing files that changed from the base of the PR and between e89d1d4 and 1841273.

📒 Files selected for processing (6)
  • it/mirror-listener/src/test/java/com/linecorp/centraldogma/it/mirror/listener/CustomMirrorListenerTest.java
  • server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java
  • server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingServiceTest.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServicePlugin.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java
  • server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java

}

final String key = mirrorCacheKey(freshMirror);
currentMirrorKeys.add(key);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of adding a new API that derives a Central Dogma with a new access token, and caching CentralDogma clients per endpoint URL?

Map<String, CentralDogma> clients = ...;

String key = dogmaUrl;
CentralDogma defaultClient = clients.get(key);
CentralDogma derivedClient = defaultClient.withAccessToken(credential.accessToken());
// Perform CD-to-CD mirroring with the derivedClient

Even if we cache clients for active mirrors, I am concerned that the number of DnsEndpointGroups will increase as CD-to-CD mirroring increases. It could lead to leaks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, addressed!
Please note that there are some limitations though:

@jrhee17 jrhee17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍

* Closes the resources held by this {@link Mirror}, such as cached connections to the remote.
* The default implementation is a no-op.
*/
default void close() {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Will mirrors created by MirrorServiceV1 also be closed/cleaned up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be closed. 😓 Thanks!
By the way, close method is gone now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java (1)

180-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding unit tests for the new withAccessToken derived-client behavior.

No test changes are included for this override (or the ReplicationLagTolerantCentralDogma counterpart). Given the derived client changes close()/resource-ownership semantics, a test asserting the derived client shares the underlying WebClient/connection pool and that close() is a no-op would guard against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java`
around lines 180 - 186, Add unit tests for the new derived-client behavior in
ArmeriaCentralDogma.withAccessToken and the ReplicationLagTolerantCentralDogma
counterpart. Verify that the returned client reuses the same underlying
WebClient/connection pool as the base instance and that calling close() on the
derived client does not release shared resources or affect the original client.
Add coverage for the no-op SafeCloseable ownership semantics so future changes
to derived-client construction or close handling do not regress.
server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java (1)

108-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pool key comment says "maxBytes" but the actual key excludes it.

The field comment states keys are "host:port:tls:maxBytes," but CentralDogmaMirror.baseClientPoolKey produces host:port:tls only. This is functionally safe because maxNumBytesPerMirror is a single global value, but the comment is inaccurate and could mislead future maintainers. Align the comment with the actual key format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java`
around lines 108 - 110, The shared base-client pool comment is inaccurate
because it says the key includes maxBytes, while
CentralDogmaMirror.baseClientPoolKey actually uses only host:port:tls. Update
the comment near baseClientPool to match the real key format and keep the
explanation aligned with how MirrorSchedulingService and CentralDogmaMirror
share the pool.
server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java (1)

136-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Javadoc mentions maxResponseLength in the key but the actual key doesn't include it.

The Javadoc on lines 137-139 states the pool key encodes "host, port, TLS flag, and maxResponseLength," but CentralDogmaMirror.baseClientPoolKey only includes host, port, and TLS flag. Since maxNumBytesPerMirror is a global setting in MirrorSchedulingService, this doesn't cause a functional issue today, but the documentation is misleading. Consider correcting the Javadoc to match the actual key composition, or adding maxResponseLength to the key if per-mirror values are planned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java`
around lines 136 - 142, The Javadoc for setBaseClientPool in Mirror is
inconsistent with the actual cache key composition used by
CentralDogmaMirror.baseClientPoolKey. Update the documentation to describe only
the fields actually included in the shared base-client pool key (host, port, and
TLS flag), or, if per-mirror sizing is intended, adjust the key generation logic
accordingly so the documented and implemented behavior match.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java`:
- Around line 141-144: The no-pool branch in CentralDogmaMirror creates a base
client via createBaseClient(uri, maxNumBytes) but never retains or closes it, so
the close() path only releases remoteClient and leaks resources. Update
CentralDogmaMirror to store the base client created in the no-pool path,
distinguish whether it is pool-owned, and make close() explicitly close that
retained base client when no scheduler pool was injected.

---

Nitpick comments:
In
`@client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java`:
- Around line 180-186: Add unit tests for the new derived-client behavior in
ArmeriaCentralDogma.withAccessToken and the ReplicationLagTolerantCentralDogma
counterpart. Verify that the returned client reuses the same underlying
WebClient/connection pool as the base instance and that calling close() on the
derived client does not release shared resources or affect the original client.
Add coverage for the no-op SafeCloseable ownership semantics so future changes
to derived-client construction or close handling do not regress.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java`:
- Around line 108-110: The shared base-client pool comment is inaccurate because
it says the key includes maxBytes, while CentralDogmaMirror.baseClientPoolKey
actually uses only host:port:tls. Update the comment near baseClientPool to
match the real key format and keep the explanation aligned with how
MirrorSchedulingService and CentralDogmaMirror share the pool.

In `@server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java`:
- Around line 136-142: The Javadoc for setBaseClientPool in Mirror is
inconsistent with the actual cache key composition used by
CentralDogmaMirror.baseClientPoolKey. Update the documentation to describe only
the fields actually included in the shared base-client pool key (host, port, and
TLS flag), or, if per-mirror sizing is intended, adjust the key generation logic
accordingly so the documented and implemented behavior match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b30a1736-766e-4a43-95a6-1234be3bb7e7

📥 Commits

Reviewing files that changed from the base of the PR and between 1841273 and 0151969.

📒 Files selected for processing (7)
  • client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java
  • client/java/src/main/java/com/linecorp/centraldogma/client/AbstractCentralDogma.java
  • client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogma.java
  • client/java/src/main/java/com/linecorp/centraldogma/internal/client/ReplicationLagTolerantCentralDogma.java
  • server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java
  • server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java (1)

224-241: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Always attempt to terminate the worker before closing pooled clients.

terminate(scheduler) || terminate(worker) short-circuits. If scheduler termination reports interruption, the worker is not terminated, but the newly added baseClientPool close/clear still runs and can close clients under an active worker.

Proposed fix
-            final boolean interrupted = terminate(scheduler) || terminate(worker);
+            final boolean schedulerInterrupted = terminate(scheduler);
+            final boolean workerInterrupted = terminate(worker);
+            final boolean interrupted = schedulerInterrupted || workerInterrupted;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java`
around lines 224 - 241, The shutdown logic in MirrorSchedulingService currently
short-circuits on terminate(scheduler) || terminate(worker), so the worker may
never be terminated before baseClientPool is closed. Update the termination flow
in the shutdown/finally path to always call terminate on both scheduler and
worker first, then combine their interruption results afterward, before closing
and clearing baseClientPool. Keep the change localized around terminate,
scheduler, worker, and the pooled-client cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java`:
- Around line 224-241: The shutdown logic in MirrorSchedulingService currently
short-circuits on terminate(scheduler) || terminate(worker), so the worker may
never be terminated before baseClientPool is closed. Update the termination flow
in the shutdown/finally path to always call terminate on both scheduler and
worker first, then combine their interruption results afterward, before closing
and clearing baseClientPool. Keep the change localized around terminate,
scheduler, worker, and the pooled-client cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 664514ec-dc2f-4565-9e6a-163e2c3dc8ab

📥 Commits

Reviewing files that changed from the base of the PR and between 0151969 and 87e8af3.

📒 Files selected for processing (3)
  • server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java
  • server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java
  • server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java
💤 Files with no reviewable changes (1)
  • server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java

@minwoox minwoox changed the title Cache remote CentralDogma clients in CentralDogmaMirror Fix UDP socket leak in CentralDogma-to-CentralDogma mirroring Jul 9, 2026

@ikhoon ikhoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 👍

@minwoox
minwoox merged commit 99db9ec into line:main Jul 10, 2026
14 checks passed
@minwoox
minwoox deleted the fix_leak branch July 10, 2026 06:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants