Commit d7e16b2c authored by Ate Douma's avatar Ate Douma

CMS-10974 [Backport 11.2] Add Lock Service through which you can acquire a...

CMS-10974 [Backport 11.2] Add Lock Service through which you can acquire a (cluster wide) lock (without using JCR)

This is a single squashed commit backport from CMS-10897 and CMS-10970:

  CMS-10897 Added Lock Manager Service interface.

    (cherry picked from commit c896f52c)

  CMS-10897 Also store the lock thread in Lock object

    (cherry picked from commit 5eb8911b)

  CMS-10897 Add refreshRateSeconds concept

    (cherry picked from commit 5993979a)

  CMS-10897 Improve the javadoc

    (cherry picked from commit a2b2c5b8)

  CMS-10897 Don't throw LockException on a couple of methods

    Otherwise typically end projects have to try / catch again in finally
    block where they invoke #unlock

    (cherry picked from commit 669db680)

  CMS-10897 remove pointless RuntimeException javadoc mention

    The lock manager impl shouldn't throw a runtime exception but
    just log a warning or error in case something completely unexpected
    happens (like failing database connection)

    (cherry picked from commit 7fafe01a)

  CMS-10897 introduce LockManagerException for some methods

    For a method like #isLocked or #getLocks, in case of an exception, we
    need to throw an exception: Otherwise we need to return true|false or
    an empty list: Client code can't handle this right. Then we can choose
    between a runtime or checked exception. Since something like #getLocks
    is something a client can react on (for example show a failed message in
    UI for lock overview or a retry), a checked exception makes most sense

    (cherry picked from commit b13f6b91)

  CMS-10897 Remove the #lock with refreshRateSeconds

    In practice, it is never needed. The LockManager will just refresh the
    locks within 60 seconds. There is no real added value in having this
    method apart from an extremely tiny optimization (that refresh can
    be invoked less frequently)

    (cherry picked from commit af558c5a)

  CMS-10897 Introduce LockResource such that we can use try-with-resource

    (cherry picked from commit 401c0fb2)

  CMS-10897 Add a reference to the Lock and Thread in the LockResource

    These fields make sense to expose in the LockResource

    (cherry picked from commit e752a434)

  CMS-10897 adding LockManagerUtils to support waiting for a lock

    Unit test will be provided through hippo-repository/test module

    (cherry picked from commit f1e8869a)

  CMS-10897 Make javadoc more explicit

   (cherry picked from commit d56dcc01)

  CMS-10897 Javadoc fix and throw IllegalStateException in error scenario

    Although it can't happen, returning null is odd imho. There is obviously
    an illegal state, so let's throw one

    (cherry picked from commit de44f55d)

  CMS-10897 Describe the LockManagerUtils in the javadoc as well

    Explain the difference between LockManager and ReentrantLock and
    explain how the cluster wide ReentrantLock behavior can be achieved
    with the LockManagerUtils

    (cherry picked from commit f4062c4b)

  CMS-10897 improve javadoc

    (cherry picked from commit b0a3ac98)

  CMS-10970 Support closing a LockResource by a different thread

    (cherry picked from commit 9c40d9da)

(cherry picked from commit b00702b1)
parent 8d63fbcf
......@@ -4,3 +4,4 @@
/.project
/.settings
/target
log4j.log
\ No newline at end of file
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
public class AlreadyLockedException extends LockException {
public AlreadyLockedException() {
super();
}
public AlreadyLockedException(final String message) {
super(message);
}
public AlreadyLockedException(final String message, final Throwable cause) {
super(message, cause);
}
public AlreadyLockedException(final Throwable cause) {
super(cause);
}
public AlreadyLockedException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
public class Lock {
private final String lockKey;
private final String lockOwner;
private final String lockThread;
private final long lockTime;
private final String status;
public Lock(final String lockKey, final String lockOwner, final String lockThread, final long lockTime,
final String status) {
this.lockKey = lockKey;
this.lockOwner = lockOwner;
this.lockThread = lockThread;
this.lockTime = lockTime;
this.status = status;
}
public String getLockKey() {
return lockKey;
}
/**
* @return the cluster node id and in case of no clustering, it will be 'default'
*/
public String getLockOwner() {
return lockOwner;
}
/**
* @return the name of the thread that holds the lock
*/
public String getLockThread() {
return lockThread;
}
public long getLockTime() {
return lockTime;
}
public String getStatus() {
return status;
}
@Override
public String toString() {
return "Lock{" +
"lockKey='" + lockKey + '\'' +
", lockOwner='" + lockOwner + '\'' +
", lockThread='" + lockThread + '\'' +
", lockTime=" + lockTime +
", status='" + status + '\'' +
'}';
}
}
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
public class LockException extends Exception {
public LockException() {
super();
}
public LockException(final String message) {
super(message);
}
public LockException(final String message, final Throwable cause) {
super(message, cause);
}
public LockException(final Throwable cause) {
super(cause);
}
protected LockException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.onehippo.cms7.services.SingletonService;
/**
* <p>
* This LockManager is a manager to obtain cluster wide locks. To obtain the {@link LockManager} you can use
* <code>
* <pre>
* LockManager lockManager = HippoServiceRegistry.getService(LockManager.class))
* </pre>
* </code>
* When a {@link Lock} is obtained, that lock is tied to the {@link Thread} that obtained the
* {@link Lock} and can only be unlocked by the same {@link Thread}. The number of invocations on {@link #lock(String)}
* must be balanced with {@link #unlock(String)} since calling {@link Lock} multiple times increases the hold count: Only
* when the hold count is 0, the lock is really freed.
* </p>
* <p>
* An example usage is as follows:
* <code>
* <pre>
* public void run() {
* boolean locked = false;
* try {
* try (LockResource lock = lockManager.lock(key)){
* // Do work
* } catch (LockException e) {
* log.info("Failed to obtain lock, most likely obtained by other cluster node already", e);
* }
* }
* </pre>
* </code>
* or without using {@link AutoCloseable} concept of {@link LockResource}:
* <code>
* <pre>
* public void run() {
* boolean locked = false;
* try {
* lockManager.lock(key);
* locked = true;
* // Do work
* } catch (LockException e) {
* log.info("Failed to obtain lock, most likely obtained by other cluster node already", e);
* } finally {
* if (locked) {
* lockManager.unlock(key);
* }
* }
* }
* </pre>
* </code>
* </p>
* <p>
* Note that when a {@code key} is already locked, the invocation of {@link #lock(String) #lock(key)} directly results
* in an {@link AlreadyLockedException} : This is thus <strong>different</strong> than
* {@link java.util.concurrent.locks.ReentrantLock} behavior. If you need similar behavior to {@link ReentrantLock#lock()}
* but then <strong>cluster wide</strong>, you can use {@link LockManagerUtils#waitForLock(LockManager, String, long)}
* and if you need the cluster wide equivalent of {@link java.util.concurrent.locks.ReentrantLock#tryLock(long, TimeUnit)}
* you can use {@link LockManagerUtils#waitForLock(LockManager, String, long, long)}.
* </p>
*
*/
@SingletonService
public interface LockManager {
/**
* Maximum number of chars for a {@link #lock(String)} key.
*/
int LOCK_KEY_MAX_LENGTH = 256;
/**
* <p>
* Tries to create a {@link Lock} for {@code key}. The {@code key} is not allowed to exceed 256 chars. If there
* is already a {@code Lock} for {@code key} then in case the current {@link Thread} has the lock, void is
* returned, otherwise a {@link LockException} is thrown.
* </p>
* <p>
* Invoking this method multiple times with the same {@code key} and the same {@link Thread} results in the hold count
* being incremented. To unlock the lock, {@link #unlock(String)} or {@link LockResource#close()} must be invoked
* an equal amount of times as {@link #lock(String)} was invoked and the unlock must be invoked with the
* same {@link Thread} as the one that obtained the {@link Lock}. Note that the {@link LockResource#close()} may
* be invoked by a different {@link Thread}!
* </p>
* <p>
* A lock is released when a successful {@link #unlock(String)} or {@link LockResource#close()} is invoked as
* many times as {@link #lock(String)}. Alternatively, when the {@link LockManager} implementation detects that
* the Thread that held the lock is not live any more, the {@link LockManager} implementation can also release the lock.
* </p>
* <p>
* In a clustered setup, a lock will be released (in the database) when it has not been refreshed for more than
* 60 seconds.
* This is a safeguard in case of a clustered setup where a cluster node has an ungraceful shutdown (crash) : In that
* case some database lock might still be present for the crashed node.
* A graceful shutdown should release all locks, implying that every Thread that holds a lock calls {@link #unlock}
* </p>
* <p>
* A persisted {@link Lock} can be marked to be aborted: In this case, the {@link Thread} that holds the lock
* gets interrupted ({@link Thread#interrupt()}). Threads that hold a lock should invoke {@link #unlock(String)}
* when interrupted (in general by just stopping their work and make sure the finally in the try block is hit
* which in general should contain the {@link #unlock(String)} logic.
* </p>
* @param key the key for the {@link Lock} where {@code key} is now allowed to exceed 256 chars
* @return {@link LockResource} such that this {@link #lock(String)} method can be used in a try-with-resources statement
* where the {@link LockResource#close()} results in the lock being freed.
* @throws LockException in case there is already a {@link Lock} for {@code key} (throwing a {@link AlreadyLockedException})
* or the lock could not be created due to some other exception (resulting in a {@link LockManagerException})
* @throws IllegalArgumentException if the {@code key} exceeds 256 chars
*/
LockResource lock(String key) throws LockException;
/**
* @param key the key to unlock where {@code key} is at most 256 chars. If the {@link Thread} that invokes
* {@link #unlock(String) unlock(key)} does not hold the {@link Lock}, nothing happens (apart from
* the {@link LockManager} implementation most likely logging a warning or error, because it is an
* implementation issue if {@code unlock(key)} is invoked by a thread that does not hold the lock.
* @throws IllegalArgumentException if the {@code key} exceeds 256 chars
*/
void unlock(String key);
/**
* <p>
* Returns {@code true} if there is a lock for {@code key}. Note that this method returns {@code true} or {@code false}
* regardless whether the {@link Thread} that invokes {@link #isLocked(String)} contains the lock or whether another
* {@link Thread} contains the lock
* </p>
* @param key the {@code key} to check whether there is a lock for
* @return {@code true} when locked
* @throws IllegalArgumentException if the {@code key} exceeds 256 chars
* @throws LockManagerException if some irrecoverable error occurs, for example a database request timeout
*/
boolean isLocked(String key) throws LockManagerException;
/**
* @return all the {@link Lock}s that are currently active (including locks that are marked to be aborted but not
* yet aborted)
* @throws LockManagerException if some irrecoverable error occurs, for example a database request timeout
*/
List<Lock> getLocks() throws LockManagerException;
/**
* <p>
* Indicates the {@link LockManager} that the {@link Thread} containing the {@link Lock} for {@code key} should
* be interrupted.
* This method can be invoked by another thread than the one that holds the {@link Lock}. In clustered setups
* it can be requested by other cluster nodes that do not contain a {@link Thread} that holds the {@link Lock}.
* </p>
* <p>
* When the {@link LockManager} finds a lock marked to be aborted contained in its own JVM,
* it must interrupt the {@link Thread} that holds the {@link Lock}. As a result, the process should stop
* and the {@link Thread} to abort should invoke {@link #unlock(String)}
* </p>
* <p>
* If there is no {@link Lock} for {@code key}, nothing happens and void is returned.
* </p>
* @param key the {@code key} to check whether there is a lock for
* @throws IllegalArgumentException if the {@code key} exceeds 256 chars
* @throws LockManagerException if some irrecoverable error occurs, for example a database request timeout
*/
void abort(String key) throws LockManagerException;
}
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
public class LockManagerException extends LockException {
public LockManagerException() {
super();
}
public LockManagerException(final String message) {
super(message);
}
public LockManagerException(final String message, final Throwable cause) {
super(message, cause);
}
public LockManagerException(final Throwable cause) {
super(cause);
}
protected LockManagerException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
import java.util.concurrent.TimeoutException;
public class LockManagerUtils {
/**
* Utility method to create and if needed wait indefinitely (unless interrupted) for a {@link LockManager#lock(String)}
* @param lockManager lockManager
* @param key the key for the {@link Lock} where {@code key} is now allowed to exceed 256 chars
* @param waitInterval time in milliseconds to wait before retrying creating the lock
* @return {@link LockResource} such that this {@link ##waitForLock(LockManager, String, long)} method can be used
* in a try-with-resources statement where the {@link LockResource#close()} results in the lock being freed.
* @throws LockException if the lock could not be created (other then {@link AlreadyLockedException})
* @throws InterruptedException when the thread is interrupted while waiting before retrying to create the lock
*/
public static LockResource waitForLock(final LockManager lockManager, final String key, final long waitInterval)
throws LockException, InterruptedException {
try {
return waitForLock(lockManager, key, waitInterval, 0);
} catch (TimeoutException ignore) {
throw new IllegalStateException("LockManagerUtils implementation error");
}
}
/**
* Utility method to create and if needed wait for a maximum amount of time (unless interrupted) for a {@link LockManager#lock(String)}
* @param lockManager lockManager
* @param key the key for the {@link Lock} where {@code key} is now allowed to exceed 256 chars
* @param waitInterval time in milliseconds to wait before retrying creating the lock
* @param maxWait maximum time in milliseconds for trying to create the lock, will throw TimeoutException when exceeded.
* @return {@link LockResource} such that this {@link ##waitForLock(LockManager, String, long)} method can be used
* in a try-with-resources statement where the {@link LockResource#close()} results in the lock being freed.
* @throws LockException if the lock could not be created (other then {@link AlreadyLockedException})
* @throws TimeoutException when the maxWait time has exceeded while trying to create the lock
* @throws InterruptedException when the thread is interrupted while waiting before retrying to create the lock
*/
public static LockResource waitForLock(final LockManager lockManager, final String key, final long waitInterval, final long maxWait)
throws LockException, TimeoutException, InterruptedException {
final long timeoutTime = maxWait > 0 ? System.currentTimeMillis() + maxWait : 0;
while (true) {
try {
return lockManager.lock(key);
} catch (AlreadyLockedException e) {
if (timeoutTime > 0 &&
(System.currentTimeMillis() > timeoutTime ||
(System.currentTimeMillis() + waitInterval) > timeoutTime)) {
throw new TimeoutException();
}
Thread.sleep(waitInterval);
}
}
}
}
/*
* Copyright 2017 Hippo B.V. (http://www.onehippo.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onehippo.cms7.services.lock;
public interface LockResource extends AutoCloseable {
/**
* @return true if this {@link LockResource} has been closed
*/
boolean isClosed();
/**
* Close the {@link LockResource} and unlocks (removes) the lock.
* <p>Note: unlike {@link LockManager#unlock(String)} this may be invoked by another thread, allowing
* delegation of unlocking the lock to another thread</p>
* <p>Warning: while the LockResource may be closed by another thread, the lock itself remains tied to the thread
* creating it!<br/>
* Therefore the thread creating the lock must <em>NOT</em> be terminated before the other thread completes the
* process requiring the lock, as the lock then <em>may</em> expire prematurely!</p>
*/
@Override
void close();
/**
* @return the {@link Lock} for ths {@link LockResource}
*/
Lock getLock();
/**
* @return true if this {@link #getLock()} was created together with this {@link LockResource} instance;
* false if this {@link #getLock()} already was created earlier by the same thread creating this {@link LockResource}.
*/
boolean isNewLock();
/**
* @return the {@link Thread} that holds this {@link LockResource} or {@code null} in case the {@link Thread}
* that created this lock has already stopped and been GC-ed
*/
Thread getHolder();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment