/**
* 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.zookeeper.server;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.SessionExpiredException;
import org.apache.zookeeper.common.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a full featured SessionTracker. It tracks session in grouped by tick
* interval. It always rounds up the tick interval to provide a sort of grace
* period. Sessions are thus expired in batches made up of sessions that expire
* in a given interval.
*/
public class SessionTrackerImpl extends ZooKeeperCriticalThread implements
SessionTracker {
private static final Logger LOG = LoggerFactory.getLogger(SessionTrackerImpl.class);
protected final ConcurrentHashMap<Long, SessionImpl> sessionsById =
new ConcurrentHashMap<Long, SessionImpl>();
private final ExpiryQueue<SessionImpl> sessionExpiryQueue;
private final ConcurrentMap<Long, Integer> sessionsWithTimeout;
private final AtomicLong nextSessionId = new AtomicLong();
public static class SessionImpl implements Session {
SessionImpl(long sessionId, int timeout) {
this.sessionId = sessionId;
this.timeout = timeout;
isClosing = false;
}
final long sessionId;
final int timeout;
boolean isClosing;
Object owner;
public long getSessionId() { return sessionId; }
public int getTimeout() { return timeout; }
public boolean isClosing() { return isClosing; }
public String toString() {
return "0x" + Long.toHexString(sessionId);
}
}
/**
* Generates an initial sessionId. High order byte is serverId, next 5
* 5 bytes are from timestamp, and low order 2 bytes are 0s.
*/
public static long initializeNextSession(long id) {
long nextSid;
nextSid = (Time.currentElapsedTime() << 24) >>> 8;
nextSid = nextSid | (id <<56);
return nextSid;
}
private final SessionExpirer expirer;
public SessionTrackerImpl(SessionExpirer expirer,
ConcurrentMap<Long, Integer> sessionsWithTimeout, int tickTime,
long serverId, ZooKeeperServerListener listener)
{
super("SessionTracker", listener);
this.expirer = expirer;
this.sessionExpiryQueue = new ExpiryQueue<SessionImpl>(tickTime);
this.sessionsWithTimeout = sessionsWithTimeout;
this.nextSessionId.set(initializeNextSession(serverId));
for (Entry<Long, Integer> e : sessionsWithTimeout.entrySet()) {
addSession(e.getKey(), e.getValue());
}
}
volatile boolean running = true;
public void dumpSessions(PrintWriter pwriter) {
pwriter.print("Session ");
sessionExpiryQueue.dump(pwriter);
}
/**
* Returns a mapping from time to session IDs of sessions expiring at that time.
*/
synchronized public Map<Long, Set<Long>> getSessionExpiryMap() {
// Convert time -> sessions map to time -> session IDs map
Map<Long, Set<SessionImpl>> expiryMap = sessionExpiryQueue.getExpiryMap();
Map<Long, Set<Long>> sessionExpiryMap = new TreeMap<Long, Set<Long>>();
for (Entry<Long, Set<SessionImpl>> e : expiryMap.entrySet()) {
Set<Long> ids = new HashSet<Long>();
sessionExpiryMap.put(e.getKey(), ids);
for (SessionImpl s : e.getValue()) {
ids.add(s.sessionId);
}
}
return sessionExpiryMap;
}
@Override
public String toString() {
StringWriter sw = new StringWriter();
PrintWriter pwriter = new PrintWriter(sw);
dumpSessions(pwriter);
pwriter.flush();
pwriter.close();
return sw.toString();
}
@Override
public void run() {
try {
while (running) {
long waitTime = sessionExpiryQueue.getWaitTime();
if (waitTime > 0) {
Thread.sleep(waitTime);
continue;
}
for (SessionImpl s : sessionExpiryQueue.poll()) {
setSessionClosing(s.sessionId);
expirer.expire(s);
}
}
} catch (InterruptedException e) {
handleException(this.getName(), e);
}
LOG.info("SessionTrackerImpl exited loop!");
}
synchronized public boolean touchSession(long sessionId, int timeout) {
SessionImpl s = sessionsById.get(sessionId);
if (s == null) {
logTraceTouchInvalidSession(sessionId, timeout);
return false;
}
if (s.isClosing()) {
logTraceTouchClosingSession(sessionId, timeout);
return false;
}
updateSessionExpiry(s, timeout);
return true;
}
private void updateSessionExpiry(SessionImpl s, int timeout) {
logTraceTouchSession(s.sessionId, timeout, "");
sessionExpiryQueue.update(s, timeout);
}
private void logTraceTouchSession(long sessionId, int timeout, String sessionStatus){
if (!LOG.isTraceEnabled())
return;
String msg = MessageFormat.format(
"SessionTrackerImpl --- Touch {0}session: 0x{1} with timeout {2}",
sessionStatus, Long.toHexString(sessionId), Integer.toString(timeout));
ZooTrace.logTraceMessage(LOG, ZooTrace.CLIENT_PING_TRACE_MASK, msg);
}
private void logTraceTouchInvalidSession(long sessionId, int timeout) {
logTraceTouchSession(sessionId, timeout, "invalid ");
}
private void logTraceTouchClosingSession(long sessionId, int timeout) {
logTraceTouchSession(sessionId, timeout, "closing ");
}
public int getSessionTimeout(long sessionId) {
return sessionsWithTimeout.get(sessionId);
}
synchronized public void setSessionClosing(long sessionId) {
if (LOG.isTraceEnabled()) {
LOG.trace("Session closing: 0x" + Long.toHexString(sessionId));
}
SessionImpl s = sessionsById.get(sessionId);
if (s == null) {
return;
}
s.isClosing = true;
}
synchronized public void removeSession(long sessionId) {
LOG.debug("Removing session 0x" + Long.toHexString(sessionId));
SessionImpl s = sessionsById.remove(sessionId);
sessionsWithTimeout.remove(sessionId);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,
"SessionTrackerImpl --- Removing session 0x"
+ Long.toHexString(sessionId));
}
if (s != null) {
sessionExpiryQueue.remove(s);
}
}
public void shutdown() {
LOG.info("Shutting down");
running = false;
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.getTextTraceLevel(),
"Shutdown SessionTrackerImpl!");
}
}
public long createSession(int sessionTimeout) {
long sessionId = nextSessionId.getAndIncrement();
addSession(sessionId, sessionTimeout);
return sessionId;
}
public boolean addGlobalSession(long id, int sessionTimeout) {
return addSession(id, sessionTimeout);
}
public synchronized boolean addSession(long id, int sessionTimeout) {
sessionsWithTimeout.put(id, sessionTimeout);
boolean added = false;
SessionImpl session = sessionsById.get(id);
if (session == null){
session = new SessionImpl(id, sessionTimeout);
}
// findbugs2.0.3 complains about get after put.
// long term strategy would be use computeIfAbsent after JDK 1.8
SessionImpl existedSession = sessionsById.putIfAbsent(id, session);
if (existedSession != null) {
session = existedSession;
} else {
added = true;
LOG.debug("Adding session 0x" + Long.toHexString(id));
}
if (LOG.isTraceEnabled()) {
String actionStr = added ? "Adding" : "Existing";
ZooTrace.logTraceMessage(LOG, ZooTrace.SESSION_TRACE_MASK,
"SessionTrackerImpl --- " + actionStr + " session 0x"
+ Long.toHexString(id) + " " + sessionTimeout);
}
updateSessionExpiry(session, sessionTimeout);
return added;
}
public boolean isTrackingSession(long sessionId) {
return sessionsById.containsKey(sessionId);
}
public synchronized void checkSession(long sessionId, Object owner)
throws KeeperException.SessionExpiredException,
KeeperException.SessionMovedException,
KeeperException.UnknownSessionException {
LOG.debug("Checking session 0x" + Long.toHexString(sessionId));
SessionImpl session = sessionsById.get(sessionId);
if (session == null) {
throw new KeeperException.UnknownSessionException();
}
if (session.isClosing()) {
throw new KeeperException.SessionExpiredException();
}
if (session.owner == null) {
session.owner = owner;
} else if (session.owner != owner) {
throw new KeeperException.SessionMovedException();
}
}
synchronized public void setOwner(long id, Object owner) throws SessionExpiredException {
SessionImpl session = sessionsById.get(id);
if (session == null || session.isClosing()) {
throw new KeeperException.SessionExpiredException();
}
session.owner = owner;
}
public void checkGlobalSession(long sessionId, Object owner)
throws KeeperException.SessionExpiredException,
KeeperException.SessionMovedException {
try {
checkSession(sessionId, owner);
} catch (KeeperException.UnknownSessionException e) {
throw new KeeperException.SessionExpiredException();
}
}
}