fix(osgi): restart on live-export state, not the shared upload folder (#36434)#36503
fix(osgi): restart on live-export state, not the shared upload folder (#36434)#36503dsolistorres wants to merge 6 commits into
Conversation
|
Tick the box to add this pull request to the merge queue (same as
|
6202023 to
3c73093
Compare
|
Claude finished @dsolistorres's task in 5m 41s —— View job 🔍 dotCMS Backend ReviewReviewed the full diff vs New Issues
Resolved
Notes (verified, no action needed)
No Critical or High severity issues. The one Medium above is non-blocking. |
✅ Addressed review observation — log-level config keys documentedThanks for catching the mismatch between the PR description and the code. The two
The system framework already namespaces all of its felix configuration under No code change needed — the PR description has been updated with the table above so the two keys are explicit for reviewers and QA. |
3c73093 to
6764bf6
Compare
Addressed backend-review feedback (commit
|
🔍 dotCMS Backend Review[🟠 High]
return Response.ok(new ResponseEntityView<>(
files.stream().map(File::getName).collect(Collectors.toSet())))
.build();💡 [🟠 High]
// OSGIUtil.java:230 — no lock acquired
this.persistOsgiExtras(extraPackages, testDryRun);
//restart OSGI after delay (manual extra-packages edit path keeps the debounce)
debouncer.debounce("restartOsgi", this::restartOsgi, delay, TimeUnit.MILLISECONDS);💡 Wrap [🟡 Medium]
final String errorMsg = "Cluster-wide OSGI restart failed after bundle "
+ "upload [" + uploadedJarNames + "]: " + getExceptionMessage(re);
Logger.error(this, errorMsg, re);
pushBundleUploadError(errorMsg);💡 Scope this notification to Next steps
|
Addressed backend-review observations (commit
|
8959815 to
2f9b90d
Compare
Addressed @Schema observation (commit
|
…#36434) On a cluster the plugin-deploy pipeline decided "already exported -> skip restart" by reading the shared osgi-extra.conf, and triggered the restart only when the upload folder was still non-empty. A peer node writing the export and moving the jar to the shared load folder could cancel the restart on every node, leaving the package in the file but never live in any running framework -> the bundle failed to resolve with "osgi.wiring.package=...;(version>=0.0.0)" persistently (Mode A). Stage 1 fix: - Decide "needs restart" against each node's live export snapshot (captured at framework start), not the shared osgi-extra.conf file. - Capture that decision under the osgi_restart_lock and move the jars to load under the same lock, so a concurrent node cannot race it away. - Submit the cluster-wide restart off-thread (single submitter) so the upload endpoint still returns immediately and the restart starts ahead of fileinstall's next poll. - Remove the fragile upload-folder guard from the debounced restart path. - Also make felix.log.level Config-driven (FELIX_LOG_LEVEL / system.felix.log.level) for field diagnostics, and add Tier-A regression tests proving version=0 is equivalent to 0.0.0. Verified on a 2-node Docker cluster sharing /data/shared: the bundle resolves to ACTIVE on both nodes with no wiring.package error. Mode B (remote-node transient from the shared load folder) is tracked separately for Stage 2. Refs: #36434 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Document the known pre-restart fileinstall window (origin AND remote nodes) at the jar-move site as a transient, self-healing condition deferred to Stage 2 (#36504), so it is not mistaken for an oversight. - Surface async cluster-wide restart failures to the admin via pushBundleUploadError (system event + error toast), covering pub/sub publish failures that restartOsgiOnlyLocal does not handle itself. - Bind the reload task to a typed ReturnableDelegate<Boolean> local instead of an inline cast to disambiguate the tryClusterLock overload. - Add regression test test_zzz_04: with the package already written to the shared osgi-extra.conf but absent from this node's live snapshot, processOsgiPackages still returns needsRestart=true (the Mode A fix), and returns false once the package is live in the snapshot. Refs: #36434 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mes in error (#36434) Addresses backend-review observability/documentation feedback on the new fire-and-forget cluster-restart path: - OSGIResource.uploadBundles: the @Operation/@apiresponse docs and Javadoc now state that a 200 confirms the upload was accepted/scheduled, NOT that the plugin is live. Restart completion is signalled by OSGI_FRAMEWORK_RESTART, successful deployment by OSGI_BUNDLES_LOADED, and async failure by OSGI_BUNDLES_UPLOAD_FAILED. Regenerated openapi.yaml from the annotations. - checkUploadFolder: the async restart-failure message now includes the uploaded jar names so an admin can correlate the standalone error toast back to the upload that triggered it, matching the other pushBundleUploadError call sites in this file. Refs: #36434 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gView (#36434) uploadBundles returns ResponseEntityView<Set<String>> (the set of uploaded jar names), but the 200 @apiresponse declared @Schema(implementation = ResponseEntityStringView.class). Point it at ResponseEntitySetStringView so the OpenAPI contract matches the actual return type. Regenerated openapi.yaml. Refs: #36434 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79fe5e0 to
e6f7cc4
Compare
…#36434) The async upload-restart path pushes OSGI_FRAMEWORK_RESTART before the (off-thread, blocking) restartOsgiClusterWide call, unlike the synchronous restartOsgi()/_restart paths that push after. Document that this is intentional — it shows the UI "restarting" spinner while the restart is in progress, and a restart failure is surfaced via OSGI_BUNDLES_UPLOAD_FAILED (not as a false success). Comment-only; no behavior change. Refs: #36434 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re: OSGI_FRAMEWORK_RESTART event ordering (commit
|
…scope error broadcast (#36434) Addresses backend-review findings: - HIGH: uploadBundles built a raw ResponseEntityView<> while the 200 @Schema declares ResponseEntitySetStringView; construct the declared subclass so the object and the OpenAPI contract agree. - HIGH: the manual PUT /extra-packages writer (writeOsgiExtras, incl. RESET) mutated osgi-extra.conf with no lock, while the upload pipeline mutates it under the OSGI_RESTART_LOCK_KEY cluster lock -> the two writers could race and lose updates cluster-wide. Serialize the manual writer under the same lock (dry-run validation stays outside the lock to preserve the REST 400 behavior). - MEDIUM: the async restart-failure broadcast (pushBundleUploadError, GLOBAL visibility) leaked jar names + exception text to every logged-in backend user; keep that detail in the server log and broadcast a generic message instead. Refs: #36434 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addressed the latest review round (commit
|
|
@claude the changes you requested have been addressed in commit
|

Problem (#36434 — Stage 1 / Mode A)
On a clustered dotCMS (2+ nodes sharing
/data/shared), uploading an OSGi plugin that imports a dotCMS package not yet in the exported-packages list could leave the plugin permanently unresolved on every node:…even though
osgi-extra.confalready contained<pkg>. This is not a version-string problem (version=0≡0.0.0); it's a restart that was skipped on the cluster.Root cause (Mode A)
Two flaws combined, both in the upload pipeline of
OSGIUtil:osgi-extra.conffile rather than this node's running framework. When a peer node had just written the new package to that shared file, this node read it back, concluded "no restart needed," and only moved the jar.uploadfolder still non-empty?" The real restart ran debounced, after the cluster lock was released, so a peer's jar-move could emptyuploadand silently cancel it.Net: the export was written to the shared file and the jar deployed to
load, but no node ever restarted → the package was never published to any running framework → persistent resolve failure.Fix (Stage 1)
liveExportedPackages) and compares the plugin's packages against that, so a peer writing the shared file can't trick a node into skipping its restart.load+ capturingneedsRestartall happen underosgi_restart_lock; the boolean can't be raced away.PUT /extra-packagesedit, which must restart even with no jar inupload).initializeFramework, fixing a latent boot-with-preloaded-jars gap.Also included
felix.log.levelis nowConfig-driven (default3) so4can capture the Felix resolver's candidate evaluation in the field — per the issue's Diagnostics ask. dotCMS runs two separate Felix frameworks, so there are two independent config keys, one per framework (the system framework namespaces all its felix config undersystem., matching the existingsystem.felix.base.dirpattern):OSGIUtil.java:201felix.log.levelDOT_FELIX_LOG_LEVEL3OSGISystem.java:96system.felix.log.levelDOT_SYSTEM_FELIX_LOG_LEVEL3Levels:
1=ERROR,2=WARNING,3=INFO,4=DEBUG. Set the relevant key to4to enable resolver DEBUG on that framework only.Tier-A regression tests in
OSGIUtilTestprovingversion=0≡0.0.0and that the error only appears when the package is absent from the running framework.Verification
./mvnw compile -pl :dotcms-core— BUILD SUCCESS.OSGIUtilTest— 8 run, 0 failures/errors/skipped./data/shared, package absent from exports, plugin dropped into the shareduploadfolder (both pollers racing — the Mode A trigger): the origin node took the newRestarting OSGI Cluster Widepath, the peer restarted via pub/sub, andcom.dotcms.osgi.repro-37894dreachedACTIVEon both nodes with zerowiring.packageerrors.Scope / follow-up
This is Stage 1: it eliminates the persistent failure (Mode A). A transient, self-healing window remains: because the jar is moved into the shared, fileinstall-watched
loadfolder before the async restart republishes the new exports, a still-stale framework can briefly auto-start the bundle and log awiring.package=…(version>=0.0.0)error that clears on the pending restart. This window can occur on both the origin node and remote nodes — it is intentionally deferred to Stage 2 (node-localload+ verify-exports-before-start / version-gated apply), tracked in #36504. It is documented in a code comment at the jar-move site so it isn't mistaken for an oversight. In practice the origin node's restart is submitted immediately and typically outruns fileinstall's poll (the 2-node test below saw zero errors), but the window is not formally closed until Stage 2.Refs: #36434