Follow-up to #36434 / PR #36503 (Stage 1).
Context
Stage 1 (#36434, PR #36503) fixed Mode A — the persistent failure where a cluster plugin upload wrote the new package to the shared osgi-extra.conf but no node ever restarted, so the bundle stayed unresolved forever. The restart decision is now taken under the osgi_restart_lock against each node's live export snapshot and can no longer be raced away.
Stage 1 deliberately left Mode B open. This issue covers it.
Problem still present after Stage 1 (Mode B — transient)
The user (felix) and system frameworks share /data/shared/felix/load, and fileinstall on every node watches that shared folder. When the origin node moves a freshly-uploaded plugin jar into the shared load folder, a remote node's fileinstall can auto-start that jar against its still-stale framework (whose system.packages.extra doesn't yet include the newly-exported package) before that node processes the pub/sub restart request — producing, transiently:
BundleException: Unable to resolve <bundle>: missing requirement
osgi.wiring.package; (&(osgi.wiring.package=<pkg>)(version>=0.0.0))
It is self-healing (the node recovers once it restarts), but it still emits the exact error the customer reports, so it reads as a failure in the logs. Root cause: a shared load folder combined with independent per-node framework state and no version gate on when a node may start a bundle.
Goal
Separate persisting a config/plugin change from applying it on each node, so deployment is an idempotent, crash-recoverable operation that scales to N nodes and does not depend on timing:
- A node that crashes after the config is persisted but before it restarts should recover automatically on next boot.
- No node should start a bundle whose required exports are not yet live on that node.
Proposed design (for discussion)
- Node-local
load folder. Keep fileinstall, but make each node's load (fileinstall watch dir) node-local rather than shared. A node only populates its own load after it has applied the required exports (restarted). This removes the shared-folder race entirely. (Decision from Stage 1 planning: keep fileinstall, make load node-local.)
- Shared monotonic "exports version". Persist a cluster-wide version that increments whenever
osgi-extra.conf changes. Each node records the version it has applied.
- Poll the version, not the upload folder. A node that sees the shared version ahead of its applied version restarts, marks the version applied, then installs the pending bundle(s) locally. Replaces the fragile upload-folder polling.
- Verify exports are live before auto-start. Before starting a bundle from
load, confirm the required exports are present in the current framework; otherwise defer until after the pending restart. (Directly addresses Mode B even before the folder is fully node-local.)
- Consider cleaning the felix bundle cache on restart when exports changed (
org.osgi.framework.storage.clean), or otherwise forcing re-resolution.
The liveExportedPackages snapshot introduced in Stage 1 is a natural stepping stone toward this apply-model.
Acceptance criteria
- On a 2+ node cluster sharing
/data/shared, uploading a plugin that imports a not-yet-exported dotCMS package resolves to ACTIVE on all nodes with no wiring.package error in any node's log (not even transiently).
- Killing a node between "config persisted" and "restart" and bringing it back leaves it consistent (bundle ACTIVE, exports live) with no manual intervention.
- No dependency on debounce timing or poller interleaving for correctness.
Test plan
- Extend the 2-node Docker harness used to validate Stage 1 (reintroduce the
OSGI_RESTART_REMOTE_DELAY_MILLIS fault-injection seam locally to force the remote node to lag) and assert no transient resolve error on the delayed node.
- Crash-recovery scenario as above.
Refs: #36434, #36503
Follow-up to #36434 / PR #36503 (Stage 1).
Context
Stage 1 (#36434, PR #36503) fixed Mode A — the persistent failure where a cluster plugin upload wrote the new package to the shared
osgi-extra.confbut no node ever restarted, so the bundle stayed unresolved forever. The restart decision is now taken under theosgi_restart_lockagainst each node's live export snapshot and can no longer be raced away.Stage 1 deliberately left Mode B open. This issue covers it.
Problem still present after Stage 1 (Mode B — transient)
The user (
felix) and system frameworks share/data/shared/felix/load, and fileinstall on every node watches that shared folder. When the origin node moves a freshly-uploaded plugin jar into the sharedloadfolder, a remote node's fileinstall can auto-start that jar against its still-stale framework (whosesystem.packages.extradoesn't yet include the newly-exported package) before that node processes the pub/sub restart request — producing, transiently:It is self-healing (the node recovers once it restarts), but it still emits the exact error the customer reports, so it reads as a failure in the logs. Root cause: a shared
loadfolder combined with independent per-node framework state and no version gate on when a node may start a bundle.Goal
Separate persisting a config/plugin change from applying it on each node, so deployment is an idempotent, crash-recoverable operation that scales to N nodes and does not depend on timing:
Proposed design (for discussion)
loadfolder. Keep fileinstall, but make each node'sload(fileinstall watch dir) node-local rather than shared. A node only populates its ownloadafter it has applied the required exports (restarted). This removes the shared-folder race entirely. (Decision from Stage 1 planning: keep fileinstall, make load node-local.)osgi-extra.confchanges. Each node records the version it has applied.load, confirm the required exports are present in the current framework; otherwise defer until after the pending restart. (Directly addresses Mode B even before the folder is fully node-local.)org.osgi.framework.storage.clean), or otherwise forcing re-resolution.The
liveExportedPackagessnapshot introduced in Stage 1 is a natural stepping stone toward this apply-model.Acceptance criteria
/data/shared, uploading a plugin that imports a not-yet-exported dotCMS package resolves toACTIVEon all nodes with nowiring.packageerror in any node's log (not even transiently).Test plan
OSGI_RESTART_REMOTE_DELAY_MILLISfault-injection seam locally to force the remote node to lag) and assert no transient resolve error on the delayed node.Refs: #36434, #36503