![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.oak.jcr.delegate;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.jackrabbit.api.stats.RepositoryStatistics.Type.SESSION_READ_COUNTER;
import static org.apache.jackrabbit.api.stats.RepositoryStatistics.Type.SESSION_READ_DURATION;
import static org.apache.jackrabbit.api.stats.RepositoryStatistics.Type.SESSION_WRITE_COUNTER;
import static org.apache.jackrabbit.api.stats.RepositoryStatistics.Type.SESSION_WRITE_DURATION;
import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.jcr.ItemExistsException;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import org.apache.jackrabbit.guava.common.collect.ImmutableMap;
import org.apache.jackrabbit.oak.api.AuthInfo;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.ContentSession;
import org.apache.jackrabbit.oak.api.QueryEngine;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.properties.SystemPropertySupplier;
import org.apache.jackrabbit.oak.jcr.observation.EventFactory;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy;
import org.apache.jackrabbit.oak.jcr.session.RefreshStrategy.Composite;
import org.apache.jackrabbit.oak.jcr.session.SessionNamespaces;
import org.apache.jackrabbit.oak.jcr.session.SessionStats;
import org.apache.jackrabbit.oak.jcr.session.SessionStats.Counters;
import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager;
import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionAware;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
import org.apache.jackrabbit.oak.spi.state.ReadyOnlyBuilderException;
import org.apache.jackrabbit.oak.stats.Clock;
import org.apache.jackrabbit.oak.stats.StatisticManager;
import org.apache.jackrabbit.oak.stats.MeterStats;
import org.apache.jackrabbit.oak.stats.TimerStats;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TODO document
*/
public class SessionDelegate {
static final Logger log = LoggerFactory.getLogger(SessionDelegate.class);
static final Logger auditLogger = LoggerFactory.getLogger("org.apache.jackrabbit.oak.audit");
static final Logger readOperationLogger = LoggerFactory.getLogger("org.apache.jackrabbit.oak.jcr.operations.reads");
static final Logger writeOperationLogger = LoggerFactory.getLogger("org.apache.jackrabbit.oak.jcr.operations.writes");
// the bitmask used for trace level logging
// we use a bitmask instead of a counter to avoid the slow modulo operation:
// https://stackoverflow.com/questions/27977834/why-is-modulus-operator-slow
// so we use "if ((counter & LOG_TRACE_BIT_MASK) == 0) log(...)"
// instead of the slower "if ((counter % LOG_TRACE) == 0) log(...)"
// that means the values need to be some power of two, minus one
// log every 128th call by default
private static final long LOG_TRACE_BIT_MASK = SystemPropertySupplier.create(
"org.apache.jackrabbit.oak.jcr.operations.bitMask",
128L - 1).loggingTo(log).get();
// log a stack trace every ~1 million calls by default
private static final long LOG_TRACE_STACK_BIT_MASK = SystemPropertySupplier.create(
"org.apache.jackrabbit.oak.jcr.operations.stack.bitMask",
1024L * 1024 - 1).loggingTo(log).get();
// the counter used for logging
private static final AtomicLong LOG_COUNTER = new AtomicLong();
private final ContentSession contentSession;
private final SecurityProvider securityProvider;
private final RefreshAtNextAccess refreshAtNextAccess = new RefreshAtNextAccess();
private final SaveCountRefresh saveCountRefresh;
private final RefreshStrategy refreshStrategy;
private final Root root;
private final IdentifierManager idManager;
private final SessionStats sessionStats;
private final Clock clock;
// access time stamps and counters for statistics about this session
private final Counters sessionCounters;
// repository-wide counters for statistics about all sessions
private final MeterStats readCounter;
private final TimerStats readDuration;
private final MeterStats writeCounter;
private final TimerStats writeDuration;
private boolean isAlive = true;
private int sessionOpCount;
private long updateCount = 0;
private String userData = null;
private PermissionProvider permissionProvider;
private boolean refreshPermissionProvider = false;
/**
* The lock used to guarantee synchronized execution of repository
* operations. An explicit lock is used instead of normal Java
* synchronization in order to be able to log attempts to concurrently
* use a session.
*/
private final WarningLock lock = new WarningLock(new ReentrantLock());
private final SessionNamespaces namespaces;
/**
* Create a new session delegate for a {@code ContentSession}. The refresh behaviour of the
* session is governed by the value of the {@code refreshInterval} argument: if the session
* has been idle longer than that value, an implicit refresh will take place.
* In addition a refresh can always be scheduled from the next access by an explicit call
* to {@link #refreshAtNextAccess()}. This is typically done from within the observation event
* dispatcher in order.
*
* @param contentSession the content session
* @param securityProvider the security provider
* @param refreshStrategy the refresh strategy used for auto refreshing this session
* @param statisticManager the statistics manager for tracking session operations
*/
public SessionDelegate(
@NotNull ContentSession contentSession,
@NotNull SecurityProvider securityProvider,
@NotNull RefreshStrategy refreshStrategy,
@NotNull ThreadLocal threadSaveCount,
@NotNull StatisticManager statisticManager,
@NotNull Clock clock) {
this.contentSession = requireNonNull(contentSession);
this.securityProvider = requireNonNull(securityProvider);
this.root = contentSession.getLatestRoot();
this.namespaces = new SessionNamespaces(this.root);
this.saveCountRefresh = new SaveCountRefresh(requireNonNull(threadSaveCount));
this.refreshStrategy = Composite.create(requireNonNull(refreshStrategy),
refreshAtNextAccess, saveCountRefresh, new RefreshNamespaces(
namespaces));
this.idManager = new IdentifierManager(root);
this.clock = requireNonNull(clock);
requireNonNull(statisticManager);
this.sessionStats = new SessionStats(contentSession.toString(),
contentSession.getAuthInfo(), clock, refreshStrategy, this, statisticManager);
this.sessionCounters = sessionStats.getCounters();
readCounter = statisticManager.getMeter(SESSION_READ_COUNTER);
readDuration = statisticManager.getTimer(SESSION_READ_DURATION);
writeCounter = statisticManager.getMeter(SESSION_WRITE_COUNTER);
writeDuration = statisticManager.getTimer(SESSION_WRITE_DURATION);
}
@NotNull
public SessionStats getSessionStats() {
return sessionStats;
}
public void refreshAtNextAccess() {
lock.lock();
try {
refreshAtNextAccess.refreshAtNextAccess(true);
} finally {
lock.unlock();
}
}
/**
* Wrap the passed {@code iterator} in an iterator that synchronizes
* all access to the underlying session.
* @param iterator iterator to synchronized
* @param
* @return synchronized iterator
*/
public Iterator sync(Iterator iterator) {
return new SynchronizedIterator<>(iterator, lock);
}
/**
* Performs the passed {@code SessionOperation} in a safe execution context. This
* context ensures that the session is refreshed if necessary and that refreshing
* occurs before the session operation is performed and the refreshing is done only
* once.
*
* @param sessionOperation the {@code SessionOperation} to perform
* @param return type of {@code sessionOperation}
* @return the result of {@code sessionOperation.perform()}
* @throws RepositoryException
* @see #getRoot()
*/
@NotNull
public T perform(@NotNull SessionOperation sessionOperation) throws RepositoryException {
long t0 = clock.getTime();
// Acquire the exclusive lock for accessing session internals.
// No other session should be holding the lock, so we log a
// message to let the user know of such cases.
lock.lock(sessionOperation);
try {
prePerform(sessionOperation, t0);
try {
sessionOpCount++;
T result = sessionOperation.perform();
logOperationDetails(contentSession, sessionOperation);
return result;
} finally {
postPerform(sessionOperation, t0);
}
} catch (ReadyOnlyBuilderException e) {
throw new ConstraintViolationException(e);
} finally {
lock.unlock();
}
}
/**
* Same as {@link #perform(org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation)}
* but with the option to return {@code null}; thus calling
* {@link org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation#performNullable()}
*
* @param sessionOperation the {@code SessionOperation} to perform
* @param return type of {@code sessionOperation}
* @return the result of {@code sessionOperation.performNullable()}, which
* might also be {@code null}.
* @throws RepositoryException
* @see #perform(org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation)
*/
@Nullable
public T performNullable(@NotNull SessionOperation sessionOperation) throws RepositoryException {
long t0 = clock.getTime();
// Acquire the exclusive lock for accessing session internals.
// No other session should be holding the lock, so we log a
// message to let the user know of such cases.
lock.lock(sessionOperation);
try {
prePerform(sessionOperation, t0);
try {
sessionOpCount++;
T result = sessionOperation.performNullable();
logOperationDetails(contentSession, sessionOperation);
return result;
} finally {
postPerform(sessionOperation, t0);
}
} catch (ReadyOnlyBuilderException e) {
throw new ConstraintViolationException(e);
} finally {
lock.unlock();
}
}
/**
* Same as {@link #perform(org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation)}
* for calls that don't expect any return value; thus calling
* {@link org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation#performVoid()}.
*
* @param sessionOperation the {@code SessionOperation} to perform.
* @throws RepositoryException
* @see #perform(org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation)
*/
public void performVoid(SessionOperation sessionOperation) throws RepositoryException {
long t0 = clock.getTime();
// Acquire the exclusive lock for accessing session internals.
// No other session should be holding the lock, so we log a
// message to let the user know of such cases.
lock.lock(sessionOperation);
try {
prePerform(sessionOperation, t0);
try {
sessionOpCount++;
sessionOperation.performVoid();
logOperationDetails(contentSession, sessionOperation);
} finally {
postPerform(sessionOperation, t0);
}
} catch (ReadyOnlyBuilderException e) {
throw new ConstraintViolationException(e);
} finally {
lock.unlock();
}
}
/**
* Same as {@link #perform(SessionOperation)} unless this method expects
* {@link SessionOperation#perform} not to throw a {@code RepositoryException}.
* Such exceptions will be wrapped into a {@code RuntimeException} and rethrown as they
* are considered an internal error.
*
* @param sessionOperation the {@code SessionOperation} to perform
* @param return type of {@code sessionOperation}
* @return the result of {@code sessionOperation.perform()}
* @see #getRoot()
*/
@NotNull
public T safePerform(SessionOperation sessionOperation) {
try {
return perform(sessionOperation);
} catch (RepositoryException e) {
throw new RuntimeException("Unexpected exception thrown by operation " + sessionOperation, e);
}
}
/**
* Same as {@link #performNullable(SessionOperation)} unless this method expects
* {@link SessionOperation#performNullable} not to throw a {@code RepositoryException}.
* Such exceptions will be wrapped into a {@code RuntimeException} and rethrown as they
* are considered an internal error.
*
* @param sessionOperation the {@code SessionOperation} to perform
* @param return type of {@code sessionOperation}
* @return the result of {@code sessionOperation.performNullable()}
*/
@Nullable
public T safePerformNullable(SessionOperation sessionOperation) {
try {
return performNullable(sessionOperation);
} catch (RepositoryException e) {
throw new RuntimeException("Unexpected exception thrown by operation " + sessionOperation, e);
}
}
@NotNull
public ContentSession getContentSession() {
return contentSession;
}
/**
* Determine whether this session is alive and has not been logged
* out or become stale by other means.
* @return {@code true} if this session is alive, {@code false} otherwise.
*/
public boolean isAlive() {
return isAlive;
}
/**
* Check that this session is alive.
* @throws RepositoryException if this session is not alive
* @see #isAlive()
*/
public void checkAlive() throws RepositoryException {
if (!isAlive()) {
throw new RepositoryException("This session has been closed.");
}
}
/**
* @return session update counter
*/
public long getUpdateCount() {
return updateCount;
}
public void setUserData(String userData) {
this.userData = userData;
}
private void commit(Root root, String path) throws CommitFailedException {
ImmutableMap.Builder info = ImmutableMap.builder();
if (path != null && !denotesRoot(path)) {
info.put(Root.COMMIT_PATH, path);
}
if (userData != null) {
info.put(EventFactory.USER_DATA, userData);
}
root.commit(info.build());
if (permissionProvider != null && refreshPermissionProvider) {
permissionProvider.refresh();
}
}
/**
* Commits the changes currently in the transient space.
* TODO: Consolidate with save().
*
* @throws CommitFailedException if the commit failed
*/
public void commit() throws CommitFailedException {
commit(root, null);
}
/**
* Commits the changes applied to the given root. The user data (if any)
* currently attached to this session is passed as the commit message.
* Used both for normal save() calls and for the various
* direct-to-workspace operations.
*
* @throws CommitFailedException if the commit failed
*/
public void commit(Root root) throws CommitFailedException {
commit(root, null);
}
public void checkProtectedNode(String path) throws RepositoryException {
NodeDelegate node = getNode(path);
if (node == null) {
throw new PathNotFoundException(
"Node " + path + " does not exist.");
} else if (node.isProtected()) {
throw new ConstraintViolationException(
"Node " + path + " is protected.");
}
}
@NotNull
public AuthInfo getAuthInfo() {
return contentSession.getAuthInfo();
}
public void logout() {
if (!isAlive) {
// ignore
return;
}
isAlive = false;
// TODO
sessionStats.close();
try {
contentSession.close();
} catch (IOException e) {
log.warn("Error while closing connection", e);
}
}
@NotNull
public IdentifierManager getIdManager() {
return idManager;
}
@Nullable
public NodeDelegate getRootNode() {
return getNode("/");
}
/**
* {@code NodeDelegate} at the given path
* @param path Oak path
* @return The {@code NodeDelegate} at {@code path} or {@code null} if
* none exists or not accessible.
*/
@Nullable
public NodeDelegate getNode(String path) {
Tree tree = root.getTree(path);
return tree.exists() ? new NodeDelegate(this, tree) : null;
}
/**
* Returns the node or property delegate at the given path.
*
* @param path Oak path
* @return node or property delegate, or {@code null} if none exists
*/
@Nullable
public ItemDelegate getItem(String path) {
String name = PathUtils.getName(path);
if (name.isEmpty()) {
return getRootNode();
} else {
Tree parent = root.getTree(PathUtils.getParentPath(path));
Tree child = parent.getChild(name);
if (child.exists()) {
return new NodeDelegate(this, child);
} else if (parent.hasProperty(name)) {
return new PropertyDelegate(this, parent, name);
} else {
return null;
}
}
}
@Nullable
public NodeDelegate getNodeByIdentifier(String id) {
Tree tree = idManager.getTree(id);
if (tree == null || !tree.exists()) {
return null;
} else {
treeLookedUpByIdentifier(tree);
return new NodeDelegate(this, tree);
}
}
protected void treeLookedUpByIdentifier(@NotNull Tree tree) {
}
/**
* {@code PropertyDelegate} at the given path
* @param path Oak path
* @return The {@code PropertyDelegate} at {@code path} or {@code null} if
* none exists or not accessible.
*/
@Nullable
public PropertyDelegate getProperty(String path) {
Tree parent = root.getTree(PathUtils.getParentPath(path));
String name = PathUtils.getName(path);
return parent.hasProperty(name) ? new PropertyDelegate(this, parent,
name) : null;
}
public boolean hasPendingChanges() {
return root.hasPendingChanges();
}
/**
* Save the subtree rooted at the given {@code path}, or the entire
* transient space if given the root path or {@code null}.
*
* This implementation only performs the save if the subtree rooted
* at {@code path} contains all transient changes and will throw an
* {@link javax.jcr.UnsupportedRepositoryOperationException} otherwise.
*
* @param path
* @throws RepositoryException
*/
public void save(String path) throws RepositoryException {
sessionCounters.saveTime = clock.getTime();
sessionCounters.saveCount++;
try {
commit(root, path);
} catch (CommitFailedException e) {
RepositoryException repositoryException = newRepositoryException(e);
sessionStats.failedSave(repositoryException);
throw repositoryException;
}
}
public void refresh(boolean keepChanges) {
sessionCounters.refreshTime = clock.getTime();
sessionCounters.refreshCount++;
if (keepChanges && hasPendingChanges()) {
root.rebase();
} else {
root.refresh();
}
if (permissionProvider != null && refreshPermissionProvider) {
permissionProvider.refresh();
}
}
//----------------------------------------------------------< Workspace >---
@NotNull
public String getWorkspaceName() {
return contentSession.getWorkspaceName();
}
/**
* Move a node
*
* @param srcPath oak path to the source node to copy
* @param destPath oak path to the destination
* @param transientOp whether or not to perform the move in transient space
* @throws RepositoryException
*/
public void move(String srcPath, String destPath, boolean transientOp)
throws RepositoryException {
Root moveRoot = transientOp ? root : contentSession.getLatestRoot();
// check destination
Tree dest = moveRoot.getTree(destPath);
if (dest.exists()) {
throw new ItemExistsException(destPath);
}
// check parent of destination
String destParentPath = PathUtils.getParentPath(destPath);
Tree destParent = moveRoot.getTree(destParentPath);
if (!destParent.exists()) {
throw new PathNotFoundException(PathUtils.getParentPath(destPath));
}
// check source exists
Tree src = moveRoot.getTree(srcPath);
if (!src.exists()) {
throw new PathNotFoundException(srcPath);
}
try {
if (!moveRoot.move(srcPath, destPath)) {
throw new RepositoryException("Cannot move node at " + srcPath + " to " + destPath);
}
if (!transientOp) {
sessionCounters.saveTime = clock.getTime();
sessionCounters.saveCount++;
commit(moveRoot);
refresh(true);
}
} catch (CommitFailedException e) {
throw newRepositoryException(e);
}
}
@NotNull
public QueryEngine getQueryEngine() {
return root.getQueryEngine();
}
@NotNull
public PermissionProvider getPermissionProvider() {
if (permissionProvider == null) {
if (root instanceof PermissionAware) {
permissionProvider = ((PermissionAware) root).getPermissionProvider();
} else {
permissionProvider = requireNonNull(securityProvider)
.getConfiguration(AuthorizationConfiguration.class)
.getPermissionProvider(root, getWorkspaceName(), getAuthInfo().getPrincipals());
refreshPermissionProvider = true;
}
}
return permissionProvider;
}
/**
* The current {@code Root} instance this session delegate instance operates on.
* To ensure the returned root reflects the correct repository revision access
* should only be done from within a {@link SessionOperation} closure through
* {@link #perform(SessionOperation)}.
*
* @return current root
*/
@NotNull
public Root getRoot() {
return root;
}
@Override
public String toString() {
return contentSession.toString();
}
//-----------------------------------------------------------< internal >---
private void prePerform(@NotNull SessionOperation> op, long t0) throws RepositoryException {
if (sessionOpCount == 0) {
// Refresh and precondition checks only for non re-entrant
// session operations. Don't refresh if this operation is a
// refresh operation itself or a save operation, which does an
// implicit refresh, or logout for obvious reasons.
if (!op.isRefresh() && !op.isSave() && !op.isLogout() &&
refreshStrategy.needsRefresh(SECONDS.convert(t0 - sessionCounters.accessTime, MILLISECONDS))) {
refresh(true);
refreshStrategy.refreshed();
updateCount++;
}
op.checkPreconditions();
}
}
private void postPerform(@NotNull SessionOperation> op, long t0) {
sessionCounters.accessTime = t0;
long dt = NANOSECONDS.convert(clock.getTime() - t0, MILLISECONDS);
sessionOpCount--;
if (op.isUpdate()) {
sessionCounters.writeTime = t0;
sessionCounters.writeCount++;
writeCounter.mark();
writeDuration.update(dt, TimeUnit.NANOSECONDS);
updateCount++;
} else {
sessionCounters.readTime = t0;
sessionCounters.readCount++;
readCounter.mark();
readDuration.update(dt, TimeUnit.NANOSECONDS);
}
if (op.isSave()) {
refreshAtNextAccess.refreshAtNextAccess(false);
// Force refreshing on access through other sessions on the same thread
saveCountRefresh.forceRefresh();
} else if (op.isRefresh()) {
refreshAtNextAccess.refreshAtNextAccess(false);
saveCountRefresh.refreshed();
}
}
private static void logOperationDetails(ContentSession session, SessionOperation ops) {
if (readOperationLogger.isTraceEnabled()
|| writeOperationLogger.isTraceEnabled()
|| auditLogger.isDebugEnabled()) {
Logger log = ops.isUpdate() ? writeOperationLogger : readOperationLogger;
long logId = LOG_COUNTER.incrementAndGet();
if ((logId & LOG_TRACE_BIT_MASK) == 0) {
if ((logId & LOG_TRACE_STACK_BIT_MASK) == 0) {
Exception e = new Exception("count: " + LOG_COUNTER);
log.trace("[{}] {}", session, ops, e);
} else {
log.trace("[{}] {}", session, ops);
}
}
//For a logout operation the auth info is not accessible
if (!ops.isLogout() && !ops.isRefresh() && !ops.isSave() && ops.isUpdate()) {
auditLogger.debug("[{}] [{}] {}", session.getAuthInfo().getUserID(), session, ops);
}
}
}
/**
* Wraps the given {@link CommitFailedException} instance using the
* appropriate {@link RepositoryException} subclass based on the
* {@link CommitFailedException#getType() type} of the given exception.
*
* @param exception typed commit failure exception
* @return matching repository exception
*/
private static RepositoryException newRepositoryException(CommitFailedException exception) {
return exception.asRepositoryException();
}
//------------------------------------------------------------< SynchronizedIterator >---
/**
* This iterator delegates to a backing iterator and synchronises
* all calls wrt. the lock passed to its constructor.
* @param
*/
private static final class SynchronizedIterator implements Iterator {
private final Iterator iterator;
private final WarningLock lock;
SynchronizedIterator(Iterator iterator, WarningLock lock) {
this.iterator = iterator;
this.lock = lock;
}
@Override
public boolean hasNext() {
lock.lock(false, "hasNext()");
try {
return iterator.hasNext();
} finally {
lock.unlock();
}
}
@Override
public T next() {
lock.lock(false, "next()");
try {
return iterator.next();
} finally {
lock.unlock();
}
}
@Override
public void remove() {
lock.lock(true, "remove()");
try {
iterator.remove();
} finally {
lock.unlock();
}
}
}
/**
* A {@link Lock} implementation that has additional methods
* for acquiring the lock, which log a warning if the lock is
* already held by another thread and was also acquired through
* such a method.
*/
private static final class WarningLock implements Lock {
private final Lock lock;
// All access to members only *after* the lock has been acquired
private boolean isUpdate;
private Exception holderTrace;
private String holderThread;
private WarningLock(Lock lock) {
this.lock = lock;
}
public void lock(boolean isUpdate, Object operation) {
if (!lock.tryLock()) {
// Acquire the lock before logging the warnings. As otherwise race conditions
// on the involved fields might lead to wrong warnings.
lock.lock();
if (holderThread != null) {
if (this.isUpdate) {
warn(log, "Attempted to perform " + operation.toString() + " while thread " + holderThread +
" was concurrently writing to this session. Blocked until the " +
"other thread finished using this session. Please review your code " +
"to avoid concurrent use of a session.", holderTrace);
} else if (log.isDebugEnabled()) {
log.debug("Attempted to perform " + operation.toString() + " while thread " + holderThread +
" was concurrently reading from this session. Blocked until the " +
"other thread finished using this session. Please review your code " +
"to avoid concurrent use of a session.", holderTrace);
}
}
}
this.isUpdate = isUpdate;
if (log.isDebugEnabled()) {
holderTrace = new Exception("Stack trace of concurrent access to session");
}
holderThread = Thread.currentThread().getName();
}
private static void warn(Logger logger, String message, Exception stackTrace) {
if (stackTrace != null) {
logger.warn(message, stackTrace);
} else {
logger. warn(message);
}
}
public void lock(SessionOperation> sessionOperation) {
lock(sessionOperation.isUpdate(), sessionOperation);
}
@Override
public void lock() {
lock.lock();
holderTrace = null;
holderThread = null;
}
@Override
public void lockInterruptibly() throws InterruptedException {
lock.lockInterruptibly();
holderTrace = null;
holderThread = null;
}
@Override
public boolean tryLock() {
if (lock.tryLock()) {
holderTrace = null;
holderThread = null;
return true;
} else {
return false;
}
}
@Override
public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
if (lock.tryLock(time, unit)) {
holderTrace = null;
holderThread = null;
return true;
} else {
return false;
}
}
@Override
public void unlock() {
lock.unlock();
}
@NotNull
@Override
public Condition newCondition() {
return lock.newCondition();
}
}
private static class RefreshAtNextAccess implements RefreshStrategy {
private boolean refreshAtNextAccess;
public void refreshAtNextAccess(boolean refreshAtNextAccess) {
this.refreshAtNextAccess = refreshAtNextAccess;
}
@Override
public boolean needsRefresh(long secondsSinceLastAccess) {
return refreshAtNextAccess;
}
@Override
public void refreshed() {
refreshAtNextAccess = false;
}
@Override
public String toString() {
return "Refresh on observation event";
}
}
private static class SaveCountRefresh implements RefreshStrategy {
/**
* The repository-wide {@link ThreadLocal} that keeps track of the number
* of saves performed in each thread.
*/
private final ThreadLocal threadSaveCount;
/**
* Local copy of the {@link #threadSaveCount} for the current thread.
* If the repository-wide counter differs from our local copy, then
* some other session would have done a commit or this session is
* being accessed from some other thread. In either case it's best to
* refresh this session to avoid unexpected behaviour.
*/
private long sessionSaveCount;
public SaveCountRefresh(ThreadLocal threadSaveCount) {
this.threadSaveCount = threadSaveCount;
this.sessionSaveCount = getThreadSaveCount();
}
public void forceRefresh() {
threadSaveCount.set(sessionSaveCount = (getThreadSaveCount() + 1));
}
@Override
public boolean needsRefresh(long secondsSinceLastAccess) {
return sessionSaveCount != getThreadSaveCount();
}
@Override
public void refreshed() {
sessionSaveCount = getThreadSaveCount();
}
private long getThreadSaveCount() {
Long c = threadSaveCount.get();
return c == null ? 0 : c;
}
@Override
public String toString() {
return "Refresh after a save on the same thread from a different session";
}
}
/**
* Read-only RefreshStrategy responsible for notifying the SessionNamespaces
* instance that a refresh was called
*/
private static class RefreshNamespaces implements RefreshStrategy {
private final SessionNamespaces namespaces;
public RefreshNamespaces(SessionNamespaces namespaces) {
this.namespaces = namespaces;
}
@Override
public boolean needsRefresh(long secondsSinceLastAccess) {
return false;
}
@Override
public void refreshed() {
this.namespaces.onSessionRefresh();
}
}
public SessionNamespaces getNamespaces() {
return namespaces;
}
}