dotCMS / dotCMS/core

OSGi cluster plugin deploy — Stage 2: eliminate the shared-load-folder restart race (Mode B)

Open
#36,504 1 comment 0 reactions 0 assignees View on GitHub

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)

  1. 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.)
  2. Shared monotonic "exports version". Persist a cluster-wide version that increments whenever osgi-extra.conf changes. Each node records the version it has applied.
  3. 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.
  4. 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.)
  5. 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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.