Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 123 additions & 16 deletions java/org/apache/catalina/session/FileStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import jakarta.servlet.ServletContext;

Expand Down Expand Up @@ -71,6 +74,12 @@ public final class FileStore extends StoreBase {
private File directoryFile = null;


/**
* A map of potential locks to control concurrent read/writes of a session's persistence file.
*/
private final ConcurrentHashMap<String, UsageCountingReadWriteLock> idLocks = new ConcurrentHashMap();


/**
* Name to register for this Store, used for logging.
*/
Expand Down Expand Up @@ -183,7 +192,7 @@ public String[] keys() throws IOException {
public Session load(String id) throws ClassNotFoundException, IOException {
// Open an input stream to the specified pathname, if any
File file = file(id);
if (file == null || !file.exists()) {
if (file == null) {
return null;
}

Expand All @@ -196,19 +205,26 @@ public Session load(String id) throws ClassNotFoundException, IOException {

ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null);

try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
ObjectInputStream ois = getObjectInputStream(fis)) {
try {
acquireIdReadLock(id);
if (!file.exists()) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we've confirmed the file exists for us to read, then we don't want it modified or deleted from this point through to the read completion. So we grab the lock before file.exists and hold to read completion. If we checked file.exists before the lock in save, then it's not truly protected from a delete/modification from another thread right after before the save then acquires the lock and completes its read.

return null;
}

StandardSession session = (StandardSession) manager.createEmptySession();
session.readObjectData(ois);
session.setManager(manager);
return session;
} catch (FileNotFoundException e) {
if (contextLog.isDebugEnabled()) {
contextLog.debug(sm.getString("fileStore.noFile", id, file.getAbsolutePath()));
try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
ObjectInputStream ois = getObjectInputStream(fis)) {
StandardSession session = (StandardSession) manager.createEmptySession();
session.readObjectData(ois);
session.setManager(manager);
return session;
} catch (FileNotFoundException e) {
if (contextLog.isDebugEnabled()) {
contextLog.debug(sm.getString("fileStore.noFile", id, file.getAbsolutePath()));
}
return null;
}
return null;
} finally {
releaseIdReadLock(id);
context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL);
}
}
Expand All @@ -225,8 +241,13 @@ public void remove(String id) throws IOException {
.trace(sm.getString(getStoreName() + ".removing", id, file.getAbsolutePath()));
}

if (file.exists() && !file.delete()) {
throw new IOException(sm.getString("fileStore.deleteSessionFailed", file));
try{
acquireIdWriteLock(id);
if (file.exists() && !file.delete()) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll actually modify (remove) the session file here so we should acquire the lock to protect from modifying the file during any potential load that may be reading it.

throw new IOException(sm.getString("fileStore.deleteSessionFailed", file));
}
} finally {
releaseIdWriteLock(id);
}
}

Expand All @@ -243,9 +264,14 @@ public void save(Session session) throws IOException {
.trace(sm.getString(getStoreName() + ".saving", session.getIdInternal(), file.getAbsolutePath()));
}

try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(fos))) {
((StandardSession) session).writeObjectData(oos);
try {
acquireIdWriteLock(session.getIdInternal());
try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The save is not consuming or removing existing data so it does not care or need to check if the file exists or not already. We just need to acquire the lock before writing any actual data to ensure we don't modify the data during a load operations data read.

ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(fos))) {
((StandardSession) session).writeObjectData(oos);
}
} finally {
releaseIdWriteLock(session.getIdInternal());
}
}

Expand Down Expand Up @@ -307,4 +333,85 @@ private File file(String id) throws IOException {

return canonicalFile;
}


/**
* Acquire and create if necessary a readlock for a given session id.
*
* @param id The ID of the Session.
*/
private void acquireIdReadLock(String id) {
idLocks.compute(id,
(k, v) -> v == null ? new UsageCountingReadWriteLock() : v).lockRead();
}


/**
* Release a readlock for a given session id.
*
* @param id The ID of the Session.
*/
private void releaseIdReadLock(String id) {
idLocks.computeIfPresent(id,
(k, v) -> v.releaseRead() == 0 ? null : v);
}


/**
* Acquire and create if necessary a writelock for a given session id.
*
* @param id The ID of the Session.
*/
private void acquireIdWriteLock(String id) {
idLocks.compute(id,
(k, v) -> v == null ? new UsageCountingReadWriteLock() : v).lockWrite();
}


/**
* Release a writelock for a given session id.
*
* @param id The ID of the Session.
*/
private void releaseIdWriteLock(String id) {
idLocks.computeIfPresent(id,
(k, v) -> v.releaseWrite() == 0 ? null : v);
}


/*
* The FileStore uses a per session ReentrantReadWriteLock to ensure that only one write (from a remove or save)
* occurs to a session persistence file at a time and not during any reads of the file (from a load call). This
* is to protect from concurrency issues that may arise, particularly if using a PersistentValve. To limit the
* size of the session ID to lock map, the locks are created when required and destroyed (made eligible for GC)
* as soon as they are not required.
*/
private static class UsageCountingReadWriteLock {
private final AtomicLong usageCount = new AtomicLong(0);
private final ReentrantReadWriteLock lock;

private UsageCountingReadWriteLock() {
lock = new ReentrantReadWriteLock();
}

private void lockRead() {
usageCount.incrementAndGet();
lock.readLock().lock();
}

private long releaseRead() {
lock.readLock().unlock();
return usageCount.decrementAndGet();
}

private void lockWrite() {
usageCount.incrementAndGet();
lock.writeLock().lock();
}

private long releaseWrite() {
lock.writeLock().unlock();
return usageCount.decrementAndGet();
}
}
}