
com.composum.sling.core.concurrent.LazyCreationServiceImpl Maven / Gradle / Ivy
Show all versions of composum-nodes-commons Show documentation
package com.composum.sling.core.concurrent;
import com.composum.sling.core.ResourceHandle;
import com.composum.sling.core.util.ResourceUtil;
import org.apache.commons.lang3.Validate;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.*;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.*;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.jetbrains.annotations.NotNull;
import javax.jcr.*;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.lock.LockManager;
import java.util.Calendar;
import java.util.Map;
import java.util.Objects;
import static com.composum.sling.core.util.ResourceUtil.*;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Implementation of the {@link LazyCreationService}. Uses {@link SequencerService} to
* at least locally avoid conflicts by locking the parent of the created resource. If two nodes of a cluster
* try to create the same resource, anyway, one of the transactions is rolled back. It will just be logged and
* ignored.
*/
@Component(
property = {
Constants.SERVICE_DESCRIPTION + "=Composum Nodes Lazy Creation service"
},
immediate = true
)
@Designate(ocd = LazyCreationServiceImpl.Configuration.class)
public class LazyCreationServiceImpl implements LazyCreationService {
private static final Logger LOG = getLogger(LazyCreationServiceImpl.class);
@Reference
protected ResourceResolverFactory resolverFactory;
@Reference
protected SequencerService sequencer;
protected volatile Configuration config;
@Override
public T getOrCreate(ResourceResolver resolver, String path, RetrievalStrategy getter,
CreationStrategy creator, final Map parentProperties)
throws RepositoryException {
ParentCreationStrategy parentCreationStrategy = new ParentCreationStrategy() {
@Override
public Resource createParent(ResourceResolver resolver, Resource parentsParent, String parentName, int
level)
throws RepositoryException, PersistenceException {
return resolver.create(parentsParent, parentName, parentProperties);
}
};
return getOrCreate(resolver, path, getter, creator, parentCreationStrategy);
}
@Override
public T getOrCreate(ResourceResolver resolver, String path, RetrievalStrategy getter, CreationStrategy
creator, InitializationStrategy initializer, final Map parentProperties)
throws RepositoryException, PersistenceException {
ParentCreationStrategy parentCreationStrategy = new ParentCreationStrategy() {
@Override
public Resource createParent(ResourceResolver resolver, Resource parentsParent, String parentName, int
level)
throws RepositoryException, PersistenceException {
return resolver.create(parentsParent, parentName, parentProperties);
}
};
return getOrCreate(resolver, path, getter, creator, initializer, parentCreationStrategy);
}
@Override
public T getOrCreate(ResourceResolver resolver, String path, RetrievalStrategy getter,
CreationStrategy creator, ParentCreationStrategy parentCreationStrategy)
throws RepositoryException {
Validate.notNull(path, "Path must not be null");
Validate.isTrue(path.startsWith("/"), "Path must be absolute: %s", path);
T result = getter.get(resolver, path);
if (null != result) return result;
String parentPath = ResourceUtil.getParent(path);
ResourceResolver adminResolver = null;
SequencerService.Token token = sequencer.acquire(path);
try {
refreshSession(resolver, true);
// check nobody created it during acquiring lock:
if (null != (result = getter.get(resolver, path))) return result;
adminResolver = createAdministrativeResolver();
Resource parentResource = adminResolver.getResource(parentPath);
if (null == parentResource) {
sequencer.release(token); // release lock temporarily to prevent deadlocks
token = null;
parentResource = safeCreateParent(adminResolver, parentPath, 1, parentCreationStrategy);
Validate.notNull(parentResource, "Parent creator didn't create " + parentPath);
token = sequencer.acquire(path);
refreshSession(resolver, true);
// check nobody created it during re-acquiring lock:
if (null != (result = getter.get(resolver, path))) return result;
}
try {
refreshSession(adminResolver, false);
creator.create(adminResolver, parentResource, ResourceUtil.getName(path));
adminResolver.commit();
Resource resourceAsAdmin = adminResolver.getResource(path);
Validate.notNull(resourceAsAdmin, "Bug: could not find %s even after calling creator", path);
LOG.debug("Created {}", path);
} catch (ItemExistsException | PersistenceException e) { // ignore
LOG.info("Creation of {} aborted - probably parallel creation {}", path, e.toString()
+ "/" + String.valueOf(e.getCause()));
} catch (RepositoryException e) { // others seem strange, though, but might be OK.
LOG.warn("Creation error for {}: {}", path, e);
}
refreshSession(resolver, true);
result = getter.get(resolver, path);
} finally {
if (null != token) sequencer.release(token);
if (null != adminResolver) adminResolver.close();
}
if (null == result) LOG.warn("Still not present after trying to create it: {}", path);
return result;
}
/**
* {@inheritDoc}
* We create the item in two steps. First, it is created and JCR-locked. Then it is initialized and JCR-unlocked.
* So, when trying to retrieve the item, we check that it is not locked, too, to verify that it is not in
* construction. The resource is assumed fully initialized if it exists and is not locked. We put the current time
* into lastupdatetime to keep track how long it is locked and whether the lock must be broken. Since we can only
* lock the item after the creation is committed, we set jcr:lastModified only after locking and check this must be
* set when retrieving the item.
*
* If the item exists but is locked, we wait until it is unlocked and then return what's there. If we exceed the
* {@link Configuration#lazycreation_maximumlockwait()} when waiting for the lock, we break the lock and create it ourselves.
*/
@Override
public T getOrCreate(final ResourceResolver resolver, final String path, RetrievalStrategy getter,
final CreationStrategy creator, final InitializationStrategy initializer,
final ParentCreationStrategy parentCreationStrategy) throws
RepositoryException, PersistenceException {
Validate.notNull(path, "Path must not be null");
Validate.isTrue(path.startsWith("/"), "Path must be absolute: %s", path);
if (resourceIsInitialized(resolver, path)) return getter.get(resolver, path);
LOG.debug("Going to create and init {}", path);
String parentPath = ResourceUtil.getParent(path);
ResourceResolver adminResolver = null;
Lock lock = null;
try {
refreshSession(resolver, true);
// check that nobody created it during acquiring lock
if (resourceIsInitialized(resolver, path)) return getter.get(resolver, path);
adminResolver = createAdministrativeResolver();
Resource parentResource = adminResolver.getResource(parentPath);
if (null == parentResource) {
parentResource = safeCreateParent(adminResolver, parentPath, 1, parentCreationStrategy);
Validate.notNull(parentResource, "Parent creator didn't create " + parentPath);
}
refreshSession(adminResolver, false);
Resource resource = adminResolver.getResource(path);
if (null == resource) {
resource = createUninitializedResource(adminResolver, parentResource, path, creator);
}
LockManager lockManager = adminResolver.adaptTo(Session.class).getWorkspace().getLockManager();
lock = tryToLockResource(path, adminResolver);
if (null != lock) {
initializeResource(adminResolver, path, initializer, lockManager);
}
refreshSession(resolver, true);
T result = getter.get(resolver, path);
if (null == result)
LOG.warn("Still not present after trying to create it: {}", path);
return result;
} finally {
if (null != adminResolver) adminResolver.close();
}
}
protected Resource createUninitializedResource(ResourceResolver adminResolver, Resource parentResource, String path,
CreationStrategy creator) {
SequencerService.Token token = sequencer.acquire(path);
try {
refreshSession(adminResolver, false);
Resource resource = adminResolver.getResource(path);
if (null == resource) {
try {
resource = creator.create(adminResolver, parentResource, ResourceUtil.getName(path));
Node node = resource.adaptTo(Node.class);
node.addMixin(TYPE_CREATED);
node.addMixin(TYPE_LAST_MODIFIED);
node.addMixin(TYPE_LOCKABLE);
node.setProperty(PROP_LAST_MODIFIED, (Value) null); // marker that is not initialized yet
adminResolver.commit();
LOG.debug("Created uninitialized {}", path);
} catch (ItemExistsException | PersistenceException e) { // ignore
LOG.info("Creation of uninitialized {} aborted - probably parallel creation: {}", path, e
.toString() + "/" + String.valueOf(e.getCause()));
} catch (RepositoryException e) { // others seem strange, though, but might be OK.
LOG.warn("Creation error for uninitialized {}: {}", path, e);
}
resource = adminResolver.getResource(path);
Validate.notNull(resource, "Bug: could not find %s after trying to create it: %s", path);
}
return resource;
} finally {
sequencer.release(token);
}
}
/**
* If it is not initialized, try to lock it for up to {@link Configuration#lazycreation_maximumlockwait()}.
*
* @return the lock it is locked, null if it is already initialized by someone else
* @throws javax.jcr.lock.LockException if we couldn't get a lock
*/
protected Lock tryToLockResource(String path, ResourceResolver adminResolver)
throws RepositoryException, PersistenceException {
LockManager lockManager = adminResolver.adaptTo(Session.class).getWorkspace().getLockManager();
Calendar resourceLockTime = ResourceHandle.use(adminResolver.getResource(path))
.getProperty(PROP_LAST_MODIFIED, Calendar.getInstance());
long lockTime = Math.max(resourceLockTime.getTimeInMillis(), System.currentTimeMillis());
final long stopPollingTime = lockTime + getConfiguration().lazycreation_maximumlockwait() * 1000;
long waitStep = 0;
long restWait;
Exception lastFail = null;
do {
try {
Thread.sleep(waitStep);
} catch (InterruptedException e) {
// someone interrupted us; we better give up completely.
throw new RepositoryException("Locking was interrupted.", e);
}
// We need sequencer because the JCR locking doesn't seem to distinguish between sessions on one instance, or something. Hard to test.
SequencerService.Token token = sequencer.acquire(path);
try {
refreshSession(adminResolver, false);
if (resourceIsInitialized(adminResolver, path)) return null;
boolean locked = lockManager.holdsLock(path);
LOG.debug("Path {} is locked={}", path, locked);
if (!locked) try {
refreshSession(adminResolver, false);
Lock lock = lockManager.lock(path, true, false, Long.MAX_VALUE, null);
ResourceHandle.use(adminResolver.getResource(path)).setProperty(PROP_LAST_MODIFIED, Calendar
.getInstance());
adminResolver.commit();
LOG.debug("Got lock on {} token {}", path, lock.getLockToken());
return lock;
} catch (LockException | PersistenceException ex) {
LOG.info("Could not lock {} : {}", path, ex.toString());
lastFail = ex;
}
} finally {
sequencer.release(token);
}
restWait = stopPollingTime - System.currentTimeMillis();
waitStep = Math.min(waitStep * 2 + 100, restWait); // iterative doubling to not try too often
} while (restWait > 0);
// we take over the lock from whoever locked it.
SequencerService.Token token = sequencer.acquire(path);
try {
refreshSession(adminResolver, false);
Lock lock;
try {
lock = lockManager.getLock(path);
} catch (LockException le) { // node not locked anymode - race condition?
refreshSession(adminResolver, false);
if (resourceIsInitialized(adminResolver, path)) return null;
LOG.error("Bug: could not lock " + path + " but is now unlocked but not ready: ", lastFail);
throw new LockException("Could not lock " + path + " but is unlocked but not ready", lastFail);
}
try {
ResourceHandle.use(adminResolver.getResource(path)).setProperty(PROP_LAST_MODIFIED, (Calendar) null);
Validate.isTrue(path.equals(lock.getNode().getPath()), "Unexpected lock path %s instead of %s", path,
lock.getNode().getPath());
adminResolver.commit();
refreshSession(adminResolver, false);
lockManager.addLockToken(lock.getLockToken());
lockManager.unlock(path);
refreshSession(adminResolver, false);
lock = lockManager.lock(path, true, false, Long.MAX_VALUE, null);
adminResolver.commit();
ResourceHandle.use(adminResolver.getResource(path)).setProperty(PROP_LAST_MODIFIED, Calendar
.getInstance());
adminResolver.commit();
LOG.info("Took over obsolete lock on {}", path);
return lock;
} catch (LockException le) { // node not locked anymode - race condition?
refreshSession(adminResolver, false);
if (resourceIsInitialized(adminResolver, path)) return null;
LOG.warn("Taking over lock on " + path + " failed; giving up since timeout");
throw le;
}
} finally {
sequencer.release(token);
}
}
protected void initializeResource(ResourceResolver adminResolver, String path, InitializationStrategy
initializer, LockManager lockManager) {
SequencerService.Token token = sequencer.acquire(path);
try {
Resource resource;
try {
refreshSession(adminResolver, false);
resource = adminResolver.getResource(path);
if (!resourceIsInitialized(adminResolver, path)) {
initializer.initialize(adminResolver, resource);
ResourceHandle.use(resource).setProperty(PROP_LAST_MODIFIED, Calendar.getInstance());
adminResolver.commit();
}
// we deliberately do only unlock when the initialization is successful, since otherwise the
// entity would be treated as "initialized." Another request will take over after timeout.
LOG.info("Initialized {}", path);
refreshSession(adminResolver, false);
if (lockManager.holdsLock(path)) {
lockManager.addLockToken(lockManager.getLock(path).getLockToken());
lockManager.unlock(path);
LOG.debug("Unlocking {}", path);
}
adminResolver.commit();
} catch (ItemExistsException | PersistenceException e) { // ignore
LOG.info("Initialization of {} aborted - probably parallel initialization: {}", path, e.toString()
+ "/" + String.valueOf(e.getCause()));
} catch (RepositoryException e) { // others seem strange, though, but might be OK.
LOG.warn("Initialization error for {}: {}", path, e);
}
} finally {
sequencer.release(token);
}
}
protected boolean resourceIsInitialized(ResourceResolver resolver, String path) throws RepositoryException {
return isInitialized(resolver.getResource(path));
}
@Override
public boolean isInitialized(Resource resource) throws RepositoryException {
ResourceHandle handle = ResourceHandle.use(resource);
if (!handle.isValid()) {
return false;
}
if (handle.getProperty(PROP_LAST_MODIFIED) == null) {
return false;
}
refreshSession(handle.getResourceResolver(), true);
LockManager lockManager = handle.getResourceResolver().adaptTo(Session.class).getWorkspace().getLockManager();
boolean locked = lockManager.holdsLock(handle.getPath());
return !locked;
}
@Override
public Resource waitForInitialization(ResourceResolver resolver, String path) throws RepositoryException {
Resource resource = resolver.getResource(path);
if (null == resource) {
// there may be a creation in progress, so we wait for the lock.
SequencerService.Token token = sequencer.acquire(path);
sequencer.release(token);
refreshSession(resolver, true);
resource = resolver.getResource(path);
}
if (null == resource) return null;
if (isInitialized(resource)) return resource;
final long stopPollingTime = System.currentTimeMillis() + getConfiguration().lazycreation_maximumlockwait() * 1000;
long waitStep = 0;
long restWait;
do {
try {
Thread.sleep(waitStep);
} catch (InterruptedException e) {
// Somebody interrupted us, better give up and don't wait anymore.
throw new RepositoryException("Interrupted", e);
}
refreshSession(resolver, true);
resource = resolver.getResource(path);
if (null == resource) {
LOG.warn("Resource unexpectedly vanished during wait: {}", path);
return null; // vanished again - how??
}
if (isInitialized(resource)) return resource;
restWait = stopPollingTime - System.currentTimeMillis();
waitStep = Math.min(waitStep * 2 + 100, restWait); // iterative doubling to not try too often
} while (restWait > 0);
return null;
}
/**
* Tries to create the parent while catching exceptions that could be triggered by someone having created it in
* parallel in the meantime. Includes commit and locks path on this node.
*/
protected Resource safeCreateParent(ResourceResolver adminResolver, String path, int level,
ParentCreationStrategy parentCreationStrategy) throws RepositoryException {
if ("/".equals(path))
return adminResolver.getResource("/");
String[] separated = com.composum.sling.core.util.ResourceUtil.splitPathAndName(path);
String parentPath = separated[0];
SequencerService.Token token = sequencer.acquire(path);
try {
refreshSession(adminResolver, false);
Resource resource = adminResolver.getResource(path);
if (resource == null) {
Resource parent = adminResolver.getResource(parentPath);
if (parent == null) {
sequencer.release(token); // avoid any deadlock conditions by freeing lock temporarily
token = null;
parent = safeCreateParent(adminResolver, parentPath, level + 1, parentCreationStrategy);
token = sequencer.acquire(path);
refreshSession(adminResolver, false);
resource = adminResolver.getResource(path); // could have been created when re-locking
}
if (null == resource) {
try {
resource = parentCreationStrategy.createParent(adminResolver, parent, separated[1], level);
Validate.notNull(resource, "Parent creator didn't create " + path);
adminResolver.commit();
LOG.debug("Created parent {}", path);
} catch (PersistenceException e) { // ignore
LOG.info("Creation of parent {} aborted - probably parallel creation {}", path, e.toString()
+ "/" + String.valueOf(e.getCause()));
refreshSession(adminResolver, false);
resource = adminResolver.getResource(path);
if (null == resource) {
LOG.error("Bug: creation aborted *and* resource is not there!", e);
}
}
}
}
return resource;
} finally {
if (null != token) sequencer.release(token);
}
}
/**
* Resets unmodified resources to the currently saved state.
*/
protected void refreshSession(ResourceResolver resolver, boolean keepChanges) {
try {
resolver.refresh();
Session session = resolver.adaptTo(Session.class);
session.refresh(keepChanges);
} catch (RepositoryException rex) {
LOG.warn(rex.toString(), rex);
}
}
/**
* Make administrative resolver with the necessary permissions to create stuff. Remember to close it!
*/
protected ResourceResolver createAdministrativeResolver() {
// used for maximum backwards compatibility; TODO recheck and decide from time to time
try {
return resolverFactory.getAdministrativeResourceResolver(null);
} catch (LoginException e) {
throw new SlingException("Configuration problem: we cannot get an administrative resolver ", e);
}
}
@Activate @Modified
protected void activate(Configuration config) {
this.config = config;
}
@Deactivate
protected void deactivate() {
this.config = null;
}
@NotNull
protected Configuration getConfiguration() {
return Objects.requireNonNull(this.config, "Method called on deactivated service");
}
@ObjectClassDefinition(name = "Composum Lazy Creation Service", description = "Provides a cluster-safe 'get or create' pattern")
public @interface Configuration {
@AttributeDefinition(name = "Maximum lock wait time", description =
"Maximum time in seconds for which the service waits until it assumes another cluster node \" +\n" +
" \"tried to create a resource and the attempt hangs. The lock is broken after that and another \" +\n" +
" \"attempt is started.")
// naming of this method is due to backwards compatibility
int lazycreation_maximumlockwait() default 30;
}
}