Skip to content

Commit

Permalink
Merge branch 'master' into feature/ATS-12
Browse files Browse the repository at this point in the history
  • Loading branch information
madness-inc committed Aug 16, 2019
2 parents bc60bf7 + 04410e1 commit 7f72e5b
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 55 deletions.
13 changes: 7 additions & 6 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ image::https://www.aiticon.com/assets/images/appng_logo_760px.jpg[]

:version: 0.2.0-SNAPSHOT
:mongo-version: 3.10.2
:jedis-version: 3.1.0
:pool2-version: 2.6.2
:hazelcast-version: 3.12.1

== appNG Tomcat Session
Expand All @@ -27,7 +29,8 @@ log4j.category.org.appng.tomcat.session = trace
----

== Using Redis
The implementation is based on the great work of James Coleman's https://github.com/jcoleman/tomcat-redis-session-manager[tomcat-redis-session-manager^].
This implementation uses https://redis.io/[Redis^] to store sessions.
It is based on the great work of James Coleman's https://github.com/jcoleman/tomcat-redis-session-manager[tomcat-redis-session-manager^].

A few changes were made to support Tomcat 8 and the latest version of Redis.

Expand All @@ -48,16 +51,14 @@ Add the following into your Tomcat `context.xml` (or the context block of the `s
Copy the following files into the `TOMCAT_BASE/lib` directory:

* appng-tomcat-session-{version}.jar
* http://repo1.maven.org/maven2/redis/clients/jedis/2.9.0/jedis-2.9.0.jar[jedis-2.9.0.jar^]
* http://repo1.maven.org/maven2/org/apache/commons/commons-pool2/2.4.2/commons-pool2-2.4.2.jar[commons-pool2-2.4.2.jar^]
* http://repo1.maven.org/maven2/redis/clients/jedis/{jedis-version}/jedis-{jedis-version}.jar[jedis-{jedis-version}.jar^]
* http://repo1.maven.org/maven2/org/apache/commons/commons-pool2/{pool2-version}/commons-pool2-{pool2-version}.jar[commons-pool2-{pool2-version}.jar^]

Reboot the server, and sessions should now be stored in Redis.


== Using MongoDB
The implementation is based on the great work of Kevin Davis' https://github.com/HBRGTech/mongo-session-manager[mongo-session-manager^].

A few changes were made to support Tomcat 8 and the latest version of MongoDB.
This implementation uses https://www.mongodb.com[MongoDB^] to store sessions.

=== Configuration
Add the following into your Tomcat `context.xml` (or the context block of the `server.xml` if applicable.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ public MongoStore getStore() {
return (MongoStore) super.getStore();
}

@Override
public Session createSession(String sessionId) {
Session session = super.createSession(sessionId);
try {
save(session);
} catch (IOException e) {
log.warn(String.format("Error creating session: %s", session.getIdInternal()));
session = null;
}
getStore().setThreadLocalSession(session);
return session;
}

public void afterRequest() {
getStore().removeThreadLocalSession();
}

public void save(Session session) throws IOException {
getStore().save(session);
}

@Override
public void add(Session session) {
// do nothing, we don't want to use Map<String,Session> sessions!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ public void invoke(Request request, Response response) throws IOException, Servl
getNext().invoke(request, response);
} finally {
long start = System.currentTimeMillis();
if (!Utils.isTemplateRequest(request)) {
storeSession(request, response);
}
storeSession(request, response);
long duration = System.currentTimeMillis() - start;
if (log.isDebugEnabled() && duration > 0) {
log.debug(String.format("handling session for %s took %sms", request.getServletPath(), duration));
Expand All @@ -50,16 +48,20 @@ public void invoke(Request request, Response response) throws IOException, Servl
}

private void storeSession(Request request, Response response) throws IOException {
Session session = request.getSessionInternal(false);
if (session != null) {
MongoPersistentManager manager = (MongoPersistentManager) request.getContext().getManager();
if (session.isValid()) {
log.debug(String.format("Request with session completed, saving session %s", session.getId()));
manager.getStore().save(session);
} else {
log.debug(String.format("HTTP Session has been invalidated, removing %s", session.getId()));
manager.remove(session);
MongoPersistentManager manager = (MongoPersistentManager) request.getContext().getManager();
try {
Session session = request.getSessionInternal(false);
if (session != null) {
if (session.isValid()) {
log.debug(String.format("Request with session completed, saving session %s", session.getId()));
manager.save(session);
} else {
log.debug(String.format("HTTP Session has been invalidated, removing %s", session.getId()));
manager.remove(session);
}
}
} finally {
manager.afterRequest();
}
}
}
109 changes: 72 additions & 37 deletions src/main/java/org/appng/tomcat/session/mongo/MongoStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@
*/
public class MongoStore extends StoreBase {

protected ThreadLocal<Session> currentSession = new ThreadLocal<>();

private final Log log = Utils.getLog(MongoStore.class);

/** Property used to store the Session's ID */
private static final String idProperty = "_id";
private static final String idProperty = "session_id";

/** Property used to store the Session's context name */
protected static final String appContextProperty = "app";
Expand Down Expand Up @@ -110,7 +112,6 @@ public class MongoStore extends StoreBase {
* <pre>
* 127.0.0.1:27001,127.0.0.1:27002
* </pre>
*
* </p>
*/
protected String hosts;
Expand Down Expand Up @@ -169,12 +170,12 @@ public class MongoStore extends StoreBase {
protected long maxWaitTime = 5000;

/** The time to wait in one iteration when reading a session that is still used by another thread */
protected long waitTime = 50;
protected long waitTime = 100;

/** The {@link ReadPreference}, using {@link ReadPreference#primary()} for maximum consistency by default */
protected ReadPreference readPreference = ReadPreference.primary();

/** The {@link ReadConcern}, using {@link ReadConcern#MAJORITY} for maximum consistency by default */
/** The {@link ReadConcern}, using {@link ReadConcern#DEFAULT} for maximum consistency by default */
protected ReadConcern readConcern = ReadConcern.DEFAULT;

/** Should a TTL index be used to expire sessions ? */
Expand Down Expand Up @@ -268,12 +269,22 @@ public void processExpires() {
* {@inheritDoc}
*/
public StandardSession load(String id) throws ClassNotFoundException, IOException {
long start = System.currentTimeMillis();

BasicDBObject sessionQuery = new BasicDBObject();
sessionQuery.put(idProperty, id);
sessionQuery.put(appContextProperty, this.getName());
StandardSession session = (StandardSession) currentSession.get();
if (null != session) {
if (session.getIdInternal().equals(id)) {
debug("Session from ThreadLocal: %s", id);
return session;
} else {
warn("Session from ThreadLocal differed! Requested: %s, found: %s", id, session.getIdInternal());
removeThreadLocalSession();
session = null;
}
}

long start = System.currentTimeMillis();

BasicDBObject sessionQuery = sessionQuery(id);
DBObject mongoSession = this.collection.findOne(sessionQuery);
if (null == mongoSession) {
return null;
Expand All @@ -290,8 +301,12 @@ public StandardSession load(String id) throws ClassNotFoundException, IOExceptio
}
mongoSession = this.collection.findOne(sessionQuery);
}

StandardSession session = null;
if (waited >= maxWaitTime) {
info("waited more than %sms, proceeding!", maxWaitTime);
}
if (null != mongoSession.get(THREAD_PROPERTY)) {
debug("Session %s is still used by Thread %s", id, mongoSession.get(THREAD_PROPERTY));
}

Container container = manager.getContext();
Context context = (Context) container;
Expand All @@ -306,28 +321,32 @@ public StandardSession load(String id) throws ClassNotFoundException, IOExceptio
session.readObjectData(ois);
session.setManager(this.manager);

this.collection.update(sessionQuery, new BasicDBObject("$set",
new BasicDBObject(THREAD_PROPERTY, Thread.currentThread().getName())));
String threadName = Thread.currentThread().getName();
BasicDBObject setThread = new BasicDBObject("$set", new BasicDBObject(THREAD_PROPERTY, threadName));
this.collection.update(sessionQuery, setThread);

debug("Loaded session %s with query %s in %s ms (lastModified %s), owned by thread [%s]", id,
sessionQuery, System.currentTimeMillis() - start, new Date(session.getLastAccessedTime()),
Thread.currentThread().getName());
} catch (ReflectiveOperationException e1) {
throw new ClassNotFoundException("error loading session " + id, e1);
threadName);

setThreadLocalSession(session);
} catch (ReflectiveOperationException roe) {
warn("Error loading session: %s", id);
throw roe;
}
}

return session;
}

private BasicDBObject sessionQuery(String id) {
return new BasicDBObject(idProperty, id).append(appContextProperty, this.getName());
}

/**
* {@inheritDoc}
*/
public void remove(String id) throws IOException {
BasicDBObject sessionQuery = new BasicDBObject();
sessionQuery.put(idProperty, id);
sessionQuery.put(appContextProperty, this.getName());

BasicDBObject sessionQuery = sessionQuery(id);
try {
this.collection.remove(sessionQuery);
debug("removed session %s (query: %s)", id, sessionQuery);
Expand Down Expand Up @@ -366,22 +385,18 @@ public void save(Session session) throws IOException {

byte[] data = bos.toByteArray();

BasicDBObject mongoSession = new BasicDBObject();
mongoSession.put(idProperty, session.getIdInternal());
mongoSession.put(appContextProperty, this.getName());
BasicDBObject sessionQuery = sessionQuery(session.getIdInternal());
BasicDBObject mongoSession = (BasicDBObject) sessionQuery.copy();
mongoSession.put(creationTimeProperty, session.getCreationTime());
mongoSession.put(sessionDataProperty, data);
mongoSession.put(lastModifiedProperty, Calendar.getInstance().getTime());

BasicDBObject sessionQuery = new BasicDBObject();
sessionQuery.put(idProperty, session.getId());

WriteResult updated = this.collection.update(sessionQuery, mongoSession, true, false);
debug("Saved session %s with query %s in %s ms (lastModified %s) acknowledged: %s", session.getId(),
sessionQuery, System.currentTimeMillis() - start, mongoSession.getDate(lastModifiedProperty),
updated.wasAcknowledged());
} catch (MongoException e) {
getLog().error("Unable to save session to MongoDB", e);
warn("Error saving session: %s", session.getIdInternal());
throw e;
}
}
Expand All @@ -392,12 +407,24 @@ private void debug(String message, Object... args) {
}
}

private void trace(String message, Object... args) {
if (getLog().isTraceEnabled()) {
getLog().trace(String.format(message, args));
}
}

private void info(String message, Object... args) {
if (getLog().isInfoEnabled()) {
getLog().info(String.format(message, args));
}
}

private void warn(String message, Object... args) {
if (getLog().isWarnEnabled()) {
getLog().warn(String.format(message, args));
}
}

/**
* Initialize this Store by connecting to the MongoDB using the configuration parameters supplied.
*/
Expand Down Expand Up @@ -461,26 +488,25 @@ private void getConnection() throws LifecycleException {
}
MongoClientOptions options = MongoClientOptions.builder().connectTimeout(connectionTimeoutMs)
.maxWaitTime(connectionWaitTimeoutMs).connectionsPerHost(maxPoolSize).writeConcern(writeConcern)
.readPreference(readPreference).readConcern(readConcern).build();
.readPreference(readPreference).readConcern(readConcern).requiredReplicaSetName(replicaSet)
.build();

List<ServerAddress> hosts = new ArrayList<ServerAddress>();
String[] dbHosts = this.hosts.split(",");
for (String dbHost : dbHosts) {
for (String dbHost : this.hosts.split(",")) {
String[] hostInfo = dbHost.split(":");
ServerAddress address = new ServerAddress(hostInfo[0], Integer.parseInt(hostInfo[1]));
hosts.add(address);
hosts.add(new ServerAddress(hostInfo[0], Integer.parseInt(hostInfo[1])));
}

info("%s [%s]: Connecting to MongoDB [%s]", getStoreName(), this.getName(), this.hosts);

List<MongoCredential> credentials = new ArrayList<MongoCredential>();
if (this.username != null || this.password != null) {
info("%s [%s]: Authenticating using [%s]", getStoreName(), this.getName(), this.username);
for (int i = 0; i < hosts.size(); i++) {
credentials.add(MongoCredential.createCredential(username, dbName, password.toCharArray()));
}
MongoCredential credential = MongoCredential.createCredential(username, dbName,
password.toCharArray());
this.mongoClient = new MongoClient(hosts, credential, options);
} else {
this.mongoClient = new MongoClient(hosts, options);
}
this.mongoClient = new MongoClient(hosts, credentials, options);
}

info("%s [%s]: Using Database [%s]", getStoreName(), this.getName(), this.dbName);
Expand Down Expand Up @@ -527,6 +553,15 @@ private Log getLog() {
return log;
}

void removeThreadLocalSession() {
currentSession.remove();
}

void setThreadLocalSession(Session session) {
currentSession.set(session);
}

// Setters
public void setConnectionUri(String connectionUri) {
this.connectionUri = connectionUri;
}
Expand Down

0 comments on commit 7f72e5b

Please sign in to comment.