Cluster-wide Locking with the LockManager Service
Starting with Hippo CMS versions 12.1.0, 12.0.3, 11.2.4, and 10.2.8, the LockManager service provides a scalable and resilient mechanism for sequential process execution across a Hippo CMS cluster. The LockManager offers a more efficient alternative to native JCR-based locking and supports cluster-wide primary selection. For the rationale behind introducing the LockManager, see Rationale behind the new LockManager.
When to Use the LockManager
Cluster-wide locking is required in several scenarios, all of which can be addressed with the LockManager:
- Execute a task or job once across the entire cluster.
- Execute a task or job on every cluster node, but prevent concurrent execution.
- Leader Election: Run a long-lived job on a single cluster node, and allow another node to take over if the original node fails.
To implement scenario 1, obtain a cluster-wide lock as described in Cluster-wide locking with the LockManager. In the code's "Do work" section, check if the task still needs to run or if another node has already completed it.
For scenario 2, use a cluster-wide lock with waitForLock from LockManagerUtils. See Waiting for a Lock.
For scenario 3, use the approach from scenario 1 for leader election. See Leader Election with the LockManager.
Using LockManager with JCR
When you acquire a cluster-wide lock and then execute JCR-related operations, you must call:
session.refresh(true|false)
Replace session with your JCR session instance. This call ensures that your local cluster node is synchronized with the latest global changes. Without refreshing the session, you risk working with outdated data, which can result in errors or InvalidItemStateException when persisting changes.
Hint: Always start a cluster-wide synchronized JCR block with
session.refresh(true|false).
Accessing the LockManager
LockManager lockManager = HippoServiceRegistry.getService(LockManager.class)
Cluster-wide Locking with the LockManager
A Lock is associated with the thread that acquires it and can only be released by that thread. Each call to lock(String) increases the hold count; the lock is released only when the hold count reaches zero. The following example demonstrates cluster-wide locking on a given key:
public void run() { try (LockResource ignore = lockManager.lock(key)){ // session.refresh(true|false) if JCR nodes are involved // Do work } catch (AlreadyLockedException e) { log.info("'{}' is already locked", key, e); } catch (LockException e) { log.error("Exception while trying to obtain lock", e); } }
This example uses a try-with-resources statement. The LockResource implements AutoCloseable, so the lock is released when close() is called.
The same logic without AutoCloseable:
public void run() { boolean locked = false; try { LockResource resource = lockManager.lock(key); locked = true; // session.refresh(true|false) if JCR nodes are involved // Do work } catch (AlreadyLockedException e) { log.info("'{}' is already locked", key, e); } catch (LockException e) { log.error("Exception while trying to obtain lock, e); } finally { if (locked) { lockManager.unlock(key); } } }
If the key is already locked by another thread or cluster node, lock(key) throws an AlreadyLockedException immediately. This differs from ReentrantLock.lock(), which blocks until the lock is available. For blocking behavior, use LockManagerUtils.waitForLock(LockManager, String, long). For timeout-based attempts, use LockManagerUtils.waitForLock(LockManager, String, long, long).
Unlocking from Another Thread
A lock is bound to the thread that acquired it, and only that thread can release it using lockManager.unlock(key). However, another thread can release the lock by calling LockResource.close() on the returned LockResource. The lock remains associated with the original thread, so ensure the thread that acquired the lock is not terminated before the other thread completes its work. Otherwise, the lock may expire prematurely.
Waiting for a Lock
To execute a task on every cluster node without concurrent execution, use the following pattern:
LockManager lockManager = HippoServiceRegistry.getService(LockManager.class)) try { LockManagerUtils.waitForLock(lockManager, key, 500); // session.refresh(true|false) if JCR nodes are involved // Do stuff } catch(LockException | InterruptedException e) { // handle exception }
LockManagerUtils.waitForLock retries every 500 milliseconds until it acquires the lock for the specified key. To set a maximum wait time, use:
LockManagerUtils.waitForLock(lockManager, key, 500, 1000 * 60);
This waits up to one minute. These methods are cluster-wide equivalents of ReentrantLock.lock() and ReentrantLock.tryLock(timeout, unit).
Leader Election with the LockManager
The LockManager can be used for leader election in a cluster. Each node runs a process that attempts to acquire a cluster-wide lock for the same key. The node that acquires the lock becomes the leader. Other nodes continue to attempt to acquire the lock in case the leader fails. If the leader shuts down without releasing the lock, another node can become leader only after the lock expires (up to one minute). This scenario triggers a warning log about the unreleased lock. Future releases may provide a dedicated service for leader election via the HippoServiceRegistry.
Rationale Behind the New LockManager
JCR (Apache Jackrabbit) locking works for short-lived locks, but has limitations for long-lived locks, especially under concurrent and long-running sessions. These limitations can cause lock timeouts.
To address these issues and improve scalability, the LockManager service was introduced. It does not depend on JCR and is designed to be lightweight, simple to use, and easy to manage. All native JCR lock and HippoLock API usage in the product core has been replaced with the LockManager. The HippoLock API is deprecated and will be removed in a future major release (v13 or later).
Lock management is critical for core features such as workflow, schedulers, replication, and relevance. For this reason, the LockManager service has been backported to all supported releases, including 12.0.3, 11.2.4, and 10.2.8.
The LockManager uses a dedicated database table, which is created automatically during deployment or upgrade. If your database credentials do not allow schema changes, review the Upgrade 12.0.2 to 12.1.0 instructions.