OSGi cluster plugin deploy — Stage 2: eliminate the shared-load-folder restart race (Mode B)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
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
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.) - Shared monotonic "exports version". Persist a cluster-wide version that increments whenever
osgi-extra.confchanges. 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 toACTIVEon all nodes with nowiring.packageerror 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_MILLISfault-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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading #36434 and PR #36503, then locate the 2-node Docker harness used for Stage 1 and the OSGi restart path. Reintroduce the delayed-node fault injection described in the test plan and trace how shared load-folder polling and export snapshots interact. Done means crash recovery and plugin activation on every node without any transient wiring.package error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, java
- Domain
- backend, distributed-systems, testing-qa
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100