/*
* 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.vysper.xmpp.extension.xep0124;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Periodically checks the BOSH sessions to see if there are inactive sessions,
* in this case it will close the inactive sessions.
* <p>
* This class efficiently checks the inactive sessions.
* Supposing that at a specific moment there are N total sessions connected and from these sessions
* there are M sessions that are inactive (M is lower than or equal to N) then this class will detect the inactive sessions with
* O(M) time complexity.
* <p>
* <b>Note:</b> A modification of the expire (when becoming inactive) time of a sessions has approximatively
* O(log N) time complexity.
* <p>
* This class is thread safe.
*
* @author The Apache MINA Project (dev@mina.apache.org)
*/
public class InactivityChecker extends Thread {
private final static Logger LOGGER = LoggerFactory.getLogger(InactivityChecker.class);
/*
* The interval in milliseconds between two consecutive inactivity checks.
*/
private final int CHECKING_INTERVAL = 1000;
/*
* Keeps the BOSH sessions sorted according to the time they expire (the key of the map).
*
* The value associated with a key in the map can be a BoshBackedSessionContext
* or a Set<BoshBackedSessionContext> (in case more than one session expires at the same time).
*/
private final SortedMap<Long, Object> sessions;
public InactivityChecker() {
setName(InactivityChecker.class.getSimpleName());
setDaemon(true);
sessions = new TreeMap<Long, Object>();
}
/**
* Updates (or removes) a session expire time.
* <p>
* If it is a new session then oldExpireTime will be null and newExpireTime will be the expire time,
* if the session is removed from the inactivity checker then the newExpireTime will be null and the oldExpireTime
* will be the latest expire time the session had. If it is an update for an old expire time then oldExpireTime will be
* the latest expire time and newExpireTime will be the updated expire time.
* <p>
* <b>Note:</b> The session should be added to the inactivity checker only when BoshBackedSessionContext#requestsWindow.isEmpty()
* returns true (as stated in the specification). Also when the BoshBackedSessionContext#requestsWindow.isEmpty() returns
* false the session needs to be removed from the inactivity checker.
*
* @param session the {@link BoshBackedSessionContext} that the expire time is modified for
* @param oldExpireTime the latest expire time the session had
* @param newExpireTime the new updated expire time, (if this is null and the session is watched by the inactivity checker
* then the session will be removed from the inactivity checker)
* @return true if the inactivity checker is watching the session (to detect inactivity), false otherwise
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public boolean updateExpireTime(BoshBackedSessionContext session, Long oldExpireTime, Long newExpireTime) {
boolean ret = session.isWatchedByInactivityChecker();
if ((oldExpireTime == null && newExpireTime == null)
|| (newExpireTime != null && newExpireTime.equals(oldExpireTime))) {
return ret;
}
synchronized (sessions) {
if (oldExpireTime != null) {
Object oldValue = sessions.get(oldExpireTime);
if (oldValue instanceof Set) {
((Set) oldValue).remove(session);
if (((Set) oldValue).isEmpty()) {
sessions.remove(oldExpireTime);
}
} else if (oldValue != null) {
sessions.remove(oldExpireTime);
}
ret = false;
}
if (newExpireTime != null) {
Object value = sessions.get(newExpireTime);
if (value instanceof Set) {
((Set) value).add(session);
} else if (value == null) {
sessions.put(newExpireTime, session);
} else {
Set<BoshBackedSessionContext> set = new HashSet<BoshBackedSessionContext>();
sessions.put(newExpireTime, set);
set.add((BoshBackedSessionContext) value);
set.add(session);
}
ret = true;
}
}
return ret;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
for (;;) {
if (Thread.interrupted()) {
break;
}
synchronized (this) {
try {
wait(CHECKING_INTERVAL);
} catch (InterruptedException e) {
break;
}
}
long time = System.currentTimeMillis();
// the inactive sessions are saved in a list to close them after the synchronized block to prevent a deadlock
// that could happen when trying to close the sessions inside the synchronized block
List<BoshBackedSessionContext> inactiveSessions = null;
synchronized (sessions) {
// get the oldest key
Long expireTime = sessions.isEmpty() ? null : sessions.firstKey();
while (expireTime != null) {
// as long as we find expired sessions we save them in the list and remove them from our sorted map
if (time >= expireTime) {
if (inactiveSessions == null) {
inactiveSessions = new ArrayList<BoshBackedSessionContext>();
}
Object value = sessions.get(expireTime);
if (value instanceof Set) {
inactiveSessions.addAll((Set<BoshBackedSessionContext>) value);
} else if (value != null) {
inactiveSessions.add((BoshBackedSessionContext) value);
}
sessions.remove(expireTime);
expireTime = sessions.isEmpty() ? null : sessions.firstKey();
} else {
// at the first non-expired session, we know that all the next sessions are more recent and cannot
// be expired if the current session is Ok, so we break the loop
break;
}
}
}
if (inactiveSessions != null) {
for (BoshBackedSessionContext session : inactiveSessions) {
LOGGER.error("BOSH session {} reached maximum inactivity period, closing session...", session);
session.close();
}
}
}
}
}