code.onehippo.org is currently readonly. We are migrating to code.bloomreach.com, please continue working there on Monday 14/12. See: https://docs.bloomreach.com/display/engineering/GitLab

Commit dfa6f279 authored by Ard Schrijvers's avatar Ard Schrijvers

REPO-1811 Implement in-memory lock manager

parent 7bc5effb
......@@ -36,6 +36,7 @@ import org.onehippo.cm.ConfigurationService;
import org.onehippo.cm.engine.ConfigurationServiceImpl;
import org.onehippo.cm.engine.InternalConfigurationService;
import org.onehippo.cms7.services.HippoServiceRegistry;
import org.onehippo.cms7.services.lock.LockManager;
import org.onehippo.repository.bootstrap.InitializationProcessor;
import org.hippoecm.repository.api.ReferenceWorkspace;
import org.hippoecm.repository.impl.DecoratorFactoryImpl;
......@@ -45,6 +46,7 @@ import org.hippoecm.repository.jackrabbit.RepositoryImpl;
import org.hippoecm.repository.security.HippoSecurityManager;
import org.hippoecm.repository.util.RepoUtils;
import org.onehippo.repository.modules.ModuleManager;
import org.onehippo.services.lock.LockManagerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -80,6 +82,7 @@ public class LocalHippoRepository extends HippoRepositoryImpl {
private String repoConfig;
private ConfigurationServiceImpl configurationService;
private LockManager lockManager;
private ModuleManager moduleManager;
......@@ -254,6 +257,10 @@ public class LocalHippoRepository extends HippoRepositoryImpl {
Modules.setModules(new Modules(Thread.currentThread().getContextClassLoader()));
jackrabbitRepository = new LocalRepositoryImpl(createRepositoryConfig());
lockManager = new LockManagerFactory(jackrabbitRepository).create();
HippoServiceRegistry.registerService(lockManager, LockManager.class);
repository = new DecoratorFactoryImpl().getRepositoryDecorator(jackrabbitRepository);
final Session rootSession = jackrabbitRepository.getRootSession(null);
......@@ -303,6 +310,10 @@ public class LocalHippoRepository extends HippoRepositoryImpl {
nodeTypesChangeTracker.stop();
nodeTypesChangeTracker = null;
}
if (lockManager != null) {
HippoServiceRegistry.unregisterService(lockManager, LockManager.class);
lockManager.destroy();
}
if (configurationService != null) {
HippoServiceRegistry.unregisterService(configurationService, ConfigurationService.class);
configurationService.stop();
......
package org.onehippo.services.lock;
import javax.jcr.RepositoryException;
import org.hippoecm.repository.jackrabbit.RepositoryImpl;
import org.onehippo.cms7.services.lock.LockManager;
public class LockManagerFactory {
private RepositoryImpl repositoryImpl;
public LockManagerFactory(final RepositoryImpl repositoryImpl) {
this.repositoryImpl = repositoryImpl;
}
/**
* Creates the {@link LockManager} which can be used for general purpose locking *not* using JCR at all
*
* @throws RuntimeException if the lock manager cannot be created, resulting the repository startup to
* short-circuit
* @throws RepositoryException if a repository exception happened while creating the lock manager
*/
public LockManager create() throws RuntimeException, RepositoryException {
return new MemoryLockManager();
}
}
/*
* 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.services.lock;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.onehippo.cms7.services.lock.Lock;
import org.onehippo.cms7.services.lock.LockException;
import org.onehippo.cms7.services.lock.LockManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MemoryLockManager implements LockManager {
private static final Logger log = LoggerFactory.getLogger(MemoryLockManager.class);
private final Map<String, MemoryLock> locks = new HashMap();
@Override
public synchronized void lock(final String key) throws LockException {
final MemoryLock memoryLock = locks.get(key);
if (memoryLock == null) {
log.debug("Create lock '{}' for thread '{}'", key, Thread.currentThread().getName());
locks.put(key, new MemoryLock(key));
return;
}
final Thread lockThread = memoryLock.thread.get();
if (lockThread == null) {
log.warn("Thread '{}' that created lock for '{}' has stopped without releasing the lock. Thread '{}' " +
"now gets the lock", memoryLock.getLockOwner(), key, Thread.currentThread().getName());
memoryLock.thread = new WeakReference<>(Thread.currentThread());
return;
}
if (lockThread == Thread.currentThread()) {
log.debug("Thread '{}' already contains lock '{}', increase hold count", Thread.currentThread().getName(), key);
memoryLock.increment();
return;
}
throw new LockException(String.format("This thread '%s' cannot lock '%s' : already locked by thread '%s'",
Thread.currentThread().getName(), key, lockThread.getName()));
}
@Override
public synchronized void unlock(final String key) throws LockException {
final MemoryLock memoryLock = locks.get(key);
if (memoryLock == null) {
log.debug("No lock present for '{}'", key);
return;
}
final Thread lockThread = memoryLock.thread.get();
if (lockThread == null) {
log.warn("Thread '{}' that created lock for '{}' has stopped without releasing the lock. Removing lock now",
memoryLock.getLockOwner(), key, Thread.currentThread().getName());
locks.remove(key);
}
if (lockThread != Thread.currentThread()) {
throw new LockException(String.format("Thread '%s' cannot unlock '%s' because lock owned by '%s'", Thread.currentThread().getName(), key,
lockThread.getName()));
}
memoryLock.decrement();
if (memoryLock.holdCount < 0) {
log.error("Hold count of lock should never be able to be less than 0. Core implementation issue in {}. Remove " +
"lock for {} nonetheless.",
this.getClass().getName(), key);
locks.remove(key);
} else if (memoryLock.holdCount == 0) {
log.debug("Remove lock '{}'", key);
locks.remove(key);
} else {
log.debug("Lock '{}' will not be removed since hold count is '{}'", key, memoryLock.holdCount);
}
}
@Override
public synchronized boolean isLocked(final String key) throws LockException {
expungeNeverUnlockedLocksFromGCedThreads();
return locks.containsKey(key);
}
@Override
public synchronized List<Lock> getLocks() {
expungeNeverUnlockedLocksFromGCedThreads();
return new ArrayList<>(locks.values());
}
@Override
public void destroy() {
Iterator<Map.Entry<String, MemoryLock>> iterator = locks.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, MemoryLock> next = iterator.next();
log.warn("Lock '{}' owned by '{}' was never unlocked. Removing the lock now.", next.getKey(), next.getValue().getLockOwner());
}
}
private void expungeNeverUnlockedLocksFromGCedThreads() {
Iterator<Map.Entry<String, MemoryLock>> iterator = locks.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, MemoryLock> next = iterator.next();
if (next.getValue().thread.get() == null) {
log.warn("Lock '{}' with lockOwner '{}' was present but the Thread that created the lock does not exist any more. " +
"Removing the lock now", next.getKey(), next.getValue().getLockOwner());
iterator.remove();
}
}
}
class MemoryLock extends Lock {
private WeakReference<Thread> thread;
int holdCount;
public MemoryLock(final String lockKey) {
super(lockKey, Thread.currentThread().getName(), System.currentTimeMillis());
thread = new WeakReference<>(Thread.currentThread());
holdCount = 1;
}
public void increment() {
holdCount++;
}
public void decrement() {
holdCount--;
}
}
}
/*
* 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.services.lock;
import java.util.concurrent.ExecutionException;
import org.junit.Before;
import org.junit.Test;
import org.onehippo.cms7.services.lock.LockException;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class MemoryLockManagerTest {
private MemoryLockManager memoryLockManager;
@Before
public void setUp() {
memoryLockManager = new MemoryLockManager();
}
@Test
public void same_thread_can_lock_same_key_multiple_times() throws Exception {
memoryLockManager.lock("123");
memoryLockManager.lock("123");
assertEquals(1, memoryLockManager.getLocks().size());
assertEquals("123", memoryLockManager.getLocks().iterator().next().getLockKey());
assertEquals(Thread.currentThread().getName(), memoryLockManager.getLocks().iterator().next().getLockOwner());
assertEquals(2, ((MemoryLockManager.MemoryLock)memoryLockManager.getLocks().iterator().next()).holdCount);
memoryLockManager.unlock("123");
assertEquals(1, memoryLockManager.getLocks().size());
assertEquals(1, ((MemoryLockManager.MemoryLock)memoryLockManager.getLocks().iterator().next()).holdCount);
memoryLockManager.unlock("123");
assertEquals(0, memoryLockManager.getLocks().size());
}
@Test
public void same_thread_can_unlock_() throws Exception {
memoryLockManager.lock("123");
memoryLockManager.unlock("123");
assertEquals(0, memoryLockManager.getLocks().size());
}
@Test
public void other_thread_cannot_unlock_() throws Exception {
memoryLockManager.lock("123");
Thread lockThread = new Thread(() -> {
try {
memoryLockManager.unlock("123");
} catch (LockException e) {
// expected
}
});
lockThread.start();
lockThread.join();
assertEquals(1, memoryLockManager.getLocks().size());
}
@Test
public void when_other_thread_contains_lock_a_lock_exception_is_thrown_on_lock_attempt() throws Exception {
memoryLockManager.lock("123");
try {
newSingleThreadExecutor().submit(() -> {
memoryLockManager.lock("123");
return true;
}).get();
fail("ExecutionException excpected");
} catch (ExecutionException e) {
Throwable cause = e.getCause();
assertTrue(cause instanceof LockException);
}
}
@Test
public void when_other_thread_contains_lock_it_cannot_be_unlocked_by_other_thread() throws Exception {
Thread lockThread = new Thread(() -> {
try {
memoryLockManager.lock("123");
} catch (LockException e) {
e.printStackTrace();
}
});
lockThread.start();
lockThread.join();
try {
memoryLockManager.unlock("123");
fail("Main thread should not be able to unlock");
} catch (LockException e) {
// expected
}
}
@Test
public void when_thread_containing_lock_has_ended_without_unlocking_the_lock_can_be_reclaimed_by_another_thread() throws Exception {
Thread lockThread = new Thread(() -> {
try {
memoryLockManager.lock("123");
} catch (LockException e) {
e.printStackTrace();
}
});
lockThread.start();
lockThread.join();
try {
memoryLockManager.lock("123");
fail("Other thread should have the lock");
} catch (LockException e) {
// expected
}
assertEquals("123", memoryLockManager.getLocks().iterator().next().getLockKey());
assertEquals(lockThread.getName(), memoryLockManager.getLocks().iterator().next().getLockOwner());
// set the lock thread to null and make it eligible for GC ....however since the lock has not been unlocked, we
// do expect a warning
lockThread = null;
long l = System.currentTimeMillis();
while (tryFor10Seconds(l)) {
System.gc();
if (memoryLockManager.getLocks().size() == 0) {
break;
}
}
assertEquals(0, memoryLockManager.getLocks().size());
// main thread can lock again
memoryLockManager.lock("123");
assertEquals("123", memoryLockManager.getLocks().iterator().next().getLockKey());
assertEquals(Thread.currentThread().getName(), memoryLockManager.getLocks().iterator().next().getLockOwner());
}
private boolean tryFor10Seconds(final long l) {
return System.currentTimeMillis() - l < 10000;
}
}
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