Repository navigation
fix(client): make register publisher startup idempotent - #7082
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Failed startup can leak heartbeat schedulers and cause duplicate traffic on retry.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Makes client register publisher startup idempotent and prevents duplicate runtime resources.
Changes:
- Serializes startup and reuses the initialized provider manager.
- Removes redundant Tars startup.
- Adds repeated-start regression coverage.
File summaries
| File | Description |
|---|---|
ShenyuClientRegisterEventPublisher.java |
Adds synchronized, delayed publication. |
ShenyuClientRegisterEventPublisherTest.java |
Tests manager reuse. |
TarsServiceBeanEventListener.java |
Removes duplicate startup. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Aias00
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES (PMC Aias00): the main build job is failing — this PR does not compile/pass tests, so it cannot be merged. Please fix the compilation/test failures and re-run CI, then request a re-review.
| * @param repository register repository | ||
| * @return URI subscriber | ||
| */ | ||
| protected ShenyuClientURIExecutorSubscriber createUriSubscriber(final ShenyuClientRegisterRepository repository) { |
There was a problem hiding this comment.
Non-blocking design nit: these two protected factories exist only so ShenyuClientRegisterEventPublisherTest can inject mocks. They widen the API surface of a public singleton (any subclass anywhere can now override disruptor construction).
Options if you want to keep production API tight:
- make them package-private and put the test in the same package (
org.apache.shenyu.client.core.disruptor), or - drop them and use
Mockito.mockConstruction(DisruptorProviderManage.class, ...)in the test.
Not a blocker — the behaviour is correct either way.
| try { | ||
| manage.startup(); | ||
| uriSubscriber.start(); | ||
| providerManage = manage; |
There was a problem hiding this comment.
Good — assigning providerManage only after both startup() and uriSubscriber.start() succeed is what makes the failure path retryable (the field stays null, so the next start() builds a fresh manager).
One thing worth a line of Javadoc: once this field is set, a later start() with a different ShenyuClientRegisterRepository is silently ignored. That is the intended idempotency, but it is surprising to a reader who passes a repository and sees it discarded.
Aias00
left a comment
There was a problem hiding this comment.
Review (re-review, PMC Aias00) — fix(client): make register publisher startup idempotent
Decision: APPROVE
Correction to my earlier review
My previous REQUEST_CHANGES said "this PR does not compile". That was wrong, and I apologise for the noise: I read the red CI at face value. Pulling the actual job log for build (21, ubuntu-latest) shows it ends with ##[error]The operation was canceled. — every build/build-docker-images/k8s-examples-http job in that run was cancelled, not failed, and no module reported a compilation or test failure (check-license-header and changes in the same run are green). I have re-triggered the cancelled jobs so we get a clean signal. The code is fine.
Why the change is correct
- The redundant Tars startup is genuinely redundant.
TarsServiceBeanEventListener extends AbstractContextRefreshedEventListener, andAbstractContextRefreshedEventListener's constructor already callspublisher.start(shenyuClientRegisterRepository)(line 139). The extra call in the Tars subclass constructor invokedstart()a second time on the same singleton, which built a secondDisruptorProviderManageand overwroteproviderManage— leaking the first ring buffer and itsshenyu_disruptor_consumer_*/shenyu_disruptor_provider_*threads. That is exactly #6541. Removing it is right. - Idempotency.
start()is nowsynchronizedand returns early whenproviderManage != null, so any number of client listeners (tars + http + dubbo in one app, etc.) share one disruptor.providerManageisvolatile, sopublishEvent()/getProviderManage()readers see a safely published, fully started instance. - Publish-after-startup + rollback.
providerManageis assigned only aftermanage.startup()anduriSubscriber.start()succeed. On aRuntimeExceptionthe URI subscriber and theDisruptorProviderare shut down and the exception is rethrown, leavingproviderManagenull so a retry can actually succeed. This closes the "failed startup leaks heartbeat schedulers" gap. - Heartbeat moved out of the constructor. Scheduling the 30s/10s heartbeat from
ShenyuClientURIExecutorSubscriber's constructor meant a scheduler thread was created even when startup subsequently failed. Moving it tostart()behind anAtomicBooleanguard fixes that, with no behaviour change on the happy path. The existingShenyuClientURIExecutorSubscriberTestconstructs the subscriber directly and only exercisesgetType()/executor(...), so it is unaffected. executor()now callsshutdown()instead ofif (!executor.isTerminated()) { executor.shutdown(); }—ScheduledThreadPoolExecutor#shutdown()is idempotent, so this is equivalent.
Non-blocking suggestions
createUriSubscriber(...)/createProviderManage(...)are test-only seams in production code. They widen the API of a public singleton purely so the test can inject mocks. Consider keeping them package-private with a@VisibleForTesting-style comment, or use Mockito'smockConstruction(...), so the extension point does not become part of the supported API surface.- Document the silent no-op. After the first successful
start(), a laterstart()with a differentShenyuClientRegisterRepositoryis silently ignored. That is the intended idempotency, but a line of Javadoc onstart()would save the next reader the same question. start()catchesRuntimeExceptiononly; anErrorfromstartup()would leave the disruptor running withproviderManagenull. Practically never happens — mentioning it only for completeness.
CI status
I re-triggered the cancelled jobs; they are still sitting in queued (Apache runner pool is saturated), so the status may stay yellow/red for a while through no fault of this PR. From the log evidence and the symbol checks above I am satisfied the code compiles and the tests are sound, so I am approving — but please make sure a clean run lands before merge.
Thanks for the fix, and sorry again for the earlier mis-diagnosis.
Aias00
left a comment
There was a problem hiding this comment.
Re-reviewed: making register publisher startup idempotent (and dropping the redundant Tars listener call) removes a double-start hazard. Approving.
Aias00
left a comment
There was a problem hiding this comment.
Reviewed the current head. The publisher startup lifecycle is idempotent and recovers cleanly after partial startup failures. No blocking issues found.
Summary
Testing
JAVA_TOOL_OPTIONS=-javaagent:<byte-buddy-agent> mvn -pl shenyu-client/shenyu-client-core,shenyu-client/shenyu-client-tars -am -DskipTests=false -Dcheckstyle.skip=false -Dtest=ShenyuClientRegisterEventPublisherTest -DfailIfNoTests=false testFixes #6541