package com.ausregistry.jtoolkit2.session;
import java.io.IOException;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.ausregistry.jtoolkit2.ConfigurationError;
import com.ausregistry.jtoolkit2.ErrorPkg;
import com.ausregistry.jtoolkit2.se.CommandType;
import com.ausregistry.jtoolkit2.se.Greeting;
/**
* This defines the operations or actions for managing a SessionPool and a StatsViewer.
*
* Uses the debug, support and user level loggers.
*/
public class SessionPoolImpl implements SessionPool, StatsViewer {
private static final int MAX_ACCEPTABLE_FAIL_COUNT = 3;
// / ratio of pollInterval to server timeout - must be < 0.5.
private static final float PI_STO_FRACT = 0.4f;
// / Minimum acceptable poll interval is 2 minutes. Reduce for testing.
private static final long MIN_ACCEPTABLE_POLL_INTERVAL = 120000;
private static final String[] CMD_LIMIT_PAIR = new String[] {"<<command>>", "<<limit>>" };
private static final String[] SESSION_LIMIT_EXCEDED_ERROR_MSG_ARGS = new String[] {"<<total>>", "<<cutoff>>" };
private static final String[] CMD_COUNT_LIMIT = new String[] {"<<command>>", "<<count>>", "<<limit>>" };
private String pname;
private Set<Session> pool;
private Session lastSession;
private int sessionsInUse;
private int maximumSize;
private long pollInterval;
private final long waitTimeout;
private long limitExceededWaitTimeout;
// / Minimum duration to keep sessions alive.
private final long clientTimeout;
private final SessionProperties sessionProperties;
private Logger debugLogger;
private Logger userLogger;
{
pname = getClass().getPackage().getName();
pool = new CopyOnWriteArraySet<Session>();
sessionsInUse = 0;
debugLogger = Logger.getLogger(pname + ".debug");
userLogger = Logger.getLogger(pname + ".user");
}
SessionPoolImpl(SessionPoolProperties poolProps, SessionProperties sessionProps) {
this.sessionProperties = sessionProps;
maximumSize = poolProps.getMaximumPoolSize();
pollInterval = (long) (poolProps.getServerTimeout() * PI_STO_FRACT);
long minPollInterval = Long.valueOf(System.getProperty("client.pollinterval.min",
String.valueOf(MIN_ACCEPTABLE_POLL_INTERVAL)));
if (pollInterval < minPollInterval) {
pollInterval = minPollInterval;
}
clientTimeout = poolProps.getClientTimeout();
waitTimeout = poolProps.getWaitTimeout();
// wait for lesser of 200ms and 10% command limit interval,
// but no less than 50ms if command limit exceeded
limitExceededWaitTimeout = Math.min(sessionProps.getCommandLimitInterval() / 10, 200L);
limitExceededWaitTimeout = Math.max(limitExceededWaitTimeout, 50L);
}
@Override
public void setMaxSize(int size) {
maximumSize = size;
}
/**
* Pull a greeting from the pool, forcing initialisation of the pool if a greeting has not yet been received.
*/
@Override
public Greeting getLastGreeting() throws SessionConfigurationException, SessionOpenException,
InterruptedException {
if (lastSession == null) {
init();
}
return lastSession.getGreeting();
}
private void init() throws SessionConfigurationException, SessionOpenException, InterruptedException {
releaseSession(getSession());
}
@Override
public long keepAlive() throws IOException {
debugLogger.finest("enter");
debugLogger.fine("Pool size = " + pool.size());
for (Session s : pool) {
long mruInterval = s.getStatsManager().getMruInterval();
if (pollInterval < mruInterval && mruInterval < clientTimeout) {
s.keepAlive();
}
}
debugLogger.finest("exit");
return pollInterval;
}
@Override
public synchronized void empty() {
if (pool == null || pool.isEmpty()) {
return;
}
for (Session session : pool) {
if (session != null) {
try {
try {
session.acquire();
} catch (TimeoutException te) {
userLogger.warning(ErrorPkg.getMessage("epp.session.pool.empty.acquire.timeout"));
}
session.close();
} catch (InterruptedException ie) {
userLogger.info(ie.getMessage());
}
}
}
pool.clear();
}
@Override
public synchronized void clean() throws SessionConfigurationException, SessionOpenException {
int count = pool.size();
empty();
for (int i = 0; i < count; i++) {
try {
openSession();
} catch (SessionOpenException e) {
Throwable cause = e.getCause();
if (cause instanceof SessionLimitExceededException) {
// It is possible that another client application will
// close some sessions while this is running, thereby
// allowing further sessions to be opened.
continue;
} else {
throw e;
}
}
}
}
@Override
public Session getSession() throws SessionConfigurationException, SessionOpenException, InterruptedException {
return getSessionImpl(null);
}
@Override
public Session getSession(CommandType commandType) throws SessionConfigurationException, SessionOpenException,
InterruptedException {
return getSessionImpl(commandType);
}
Session getSession(Transaction[] txs) throws SessionConfigurationException, SessionOpenException,
InterruptedException {
return getSessionImpl(txs);
}
private Session getSessionImpl(Object obj) throws SessionConfigurationException, SessionOpenException,
InterruptedException {
debugLogger.finest("enter");
Session acquiredSession = null;
int failCount = 0;
do {
acquiredSession = getBestAvailableSession(obj);
if (acquiredSession == null) {
if (poolCanGrow()) {
try {
openSession();
} catch (SessionOpenException soe) {
waitOrMaybeFail(soe, failCount);
failCount++;
}
} else {
waitForRelease();
}
}
} while (acquiredSession == null);
debugLogger.finest("exit");
return acquiredSession;
}
private void waitOrMaybeFail(SessionOpenException soe, int failCount) throws SessionOpenException {
Throwable cause = soe.getCause();
if (cause instanceof SessionLimitExceededException) {
userLogger.warning(ErrorPkg.getMessage("epp.session.open.fail.limit_exceeded",
SESSION_LIMIT_EXCEDED_ERROR_MSG_ARGS, new int[] {maximumSize, pool.size() }));
// The server is not allowing me to open any more sessions,
// therefore the actual maximum allowed concurrently open
// sessions is probably the current number of open sessions.
// Hence, I am constraining the pool to the current size.
//
constrainPoolToCurrentSize();
if (pool.isEmpty()) {
// No sessions are open, but the server is rejecting connections
// from this client. Therefore, there is a configuration error.
throw new ConfigurationError(cause);
} else {
waitForRelease();
}
} else {
if (isRepeatableFailedLogin(cause, failCount)) {
userLogger.warning(soe.getMessage());
userLogger.warning(ErrorPkg.getMessage("epp.server.failure.retry", "<<count>>",
String.valueOf(failCount)));
} else {
throw soe;
}
}
}
/**
* This should be used when the pool has been misconfigured to grow to a greater number of sessions than the EPP
* server supports. The intention is to prevent an infinite loop in waiting for the release of a session.
*/
private void constrainPoolToCurrentSize() {
maximumSize = pool.size();
}
private boolean poolCanGrow() {
return pool.size() < maximumSize;
}
private boolean isRepeatableFailedLogin(Throwable throwable, int count) {
return (throwable instanceof LoginException && throwable.getCause() instanceof CommandFailedException
&& count < MAX_ACCEPTABLE_FAIL_COUNT);
}
private synchronized Session getBestAvailableSession(Object obj) throws InterruptedException {
debugLogger.finest("enter");
Session bestSession = null;
int cc; // command-specific command count
int tc; // total command count
int min = Integer.MAX_VALUE;
int cmdCutoff, totCutoff;
Iterator<Session> iter = pool.iterator();
CommandType type;
if (obj instanceof Transaction[]) {
Transaction[] txs = (Transaction[]) obj;
sessionSearch: while (iter.hasNext()) {
Session session = iter.next();
if (session.isAvailable()) {
cmdCutoff = Integer.MAX_VALUE;
totCutoff = Integer.MAX_VALUE;
for (Transaction tx : txs) {
CommandType t = tx.getCommand().getCommandType();
StatsManager m = session.getStatsManager();
if (SessionPoolImpl.isCutoff(m.getRecentCommandCount(t), m.getRecentCommandCount(),
sessionProperties.getCommandLimit(t), sessionProperties.getCommandLimit())) {
continue sessionSearch;
}
}
// Only set this if session command counts are all under
// cutoff values
bestSession = session;
}
}
} else {
if (obj instanceof CommandType) {
type = (CommandType) obj;
} else {
type = null;
}
for (Session session : pool) {
if (session.isAvailable()) {
cmdCutoff = Integer.MAX_VALUE;
totCutoff = Integer.MAX_VALUE;
if (type == null) {
tc = session.getStatsManager().getRecentCommandCount();
cc = tc;
totCutoff = sessionProperties.getCommandLimit();
cmdCutoff = totCutoff;
} else {
cc = session.getStatsManager().getRecentCommandCount(type);
cmdCutoff = sessionProperties.getCommandLimit(type);
tc = session.getStatsManager().getRecentCommandCount();
totCutoff = sessionProperties.getCommandLimit();
}
if (cc < cmdCutoff && cc < min && tc < totCutoff) {
min = cc;
bestSession = session;
if (debugLogger.isLoggable(Level.FINE)) {
debugLogger.fine(ErrorPkg.getMessage("epp.session.rate.limit.notexceeded",
CMD_COUNT_LIMIT, new String[] {type == null ? "all commands" : type.toString(),
String.valueOf(cc), String.valueOf(cmdCutoff) }));
}
} else {
userLogger.info(ErrorPkg.getMessage("epp.session.rate.limit.exceeded", CMD_LIMIT_PAIR,
new String[] {type.toString(), String.valueOf(cmdCutoff) }));
}
}
}
}
if (bestSession != null) {
try {
bestSession.acquire();
sessionsInUse++;
} catch (TimeoutException te) {
userLogger.info(ErrorPkg.getMessage("epp.session.pool.acquire.timeout"));
bestSession = null;
}
}
debugLogger.finest("exit");
return bestSession;
}
private void openSession() throws SessionConfigurationException, SessionOpenException {
Session newSession = SessionFactory.newInstance(sessionProperties);
newSession.open();
synchronized (this) {
lastSession = newSession;
pool.add(newSession);
notify();
}
}
private synchronized void waitForRelease() {
if (sessionsInUse < pool.size()) {
// There are only inappropriate sessions available.
try {
wait(limitExceededWaitTimeout);
} catch (InterruptedException ie) {
userLogger.severe("Interrupt in wait: rate limit reset wait");
}
}
while (sessionsInUse >= pool.size() && pool.size() >= maximumSize) {
try {
wait(waitTimeout);
} catch (InterruptedException ie) {
userLogger.severe("Interrupted waiting for session release");
}
}
}
@Override
public void releaseSession(Session session) {
if (session == null) {
return;
}
if (session.isInvalid()) {
session.close();
session.release();
synchronized (this) {
pool.remove(session);
sessionsInUse--;
debugLogger.fine(ErrorPkg.getMessage("epp.session.pool.release.notify.invalid", "<<thread>>",
String.valueOf(Thread.currentThread().getId())));
notify();
}
} else if (!session.isOpen()) {
session.release();
synchronized (this) {
pool.remove(session);
sessionsInUse--;
debugLogger.fine(ErrorPkg.getMessage("epp.session.pool.release.notify.closed", "<<thread>>",
String.valueOf(Thread.currentThread().getId())));
notify();
}
} else {
session.release();
synchronized (this) {
sessionsInUse--;
debugLogger.fine(ErrorPkg.getMessage("epp.session.pool.release.notify.normal", "<<thread>>",
String.valueOf(Thread.currentThread().getId())));
notify();
}
}
}
@Override
public StatsViewer getStatsViewer() {
return this;
}
@Override
public int getRecentCommandCount() {
return getRecentCommandCount(null);
}
@Override
public long getCommandCount() {
return getCommandCount(null);
}
@Override
public int getRecentCommandCount(CommandType type) {
int retval = 0;
for (Session s : pool) {
if (s == null) {
continue;
}
StatsViewer v = s.getStatsManager();
if (type == null) {
retval += v.getRecentCommandCount();
} else {
retval += v.getRecentCommandCount(type);
}
}
return retval;
}
@Override
public long getCommandCount(CommandType type) {
long retval = 0L;
for (Session s : pool) {
if (s == null) {
continue;
}
StatsViewer v = s.getStatsManager();
if (type == null) {
retval += v.getCommandCount();
} else {
retval += v.getCommandCount(type);
}
}
return retval;
}
@Override
public long getAverageResponseTime(CommandType type) {
long t = 0L;
long c = 0L;
for (Session s : pool) {
if (s == null) {
continue;
}
StatsViewer v = s.getStatsManager();
if (type == null) {
c += v.getCommandCount();
t += v.getCommandCount() * v.getAverageResponseTime();
} else {
c += v.getCommandCount(type);
t += v.getCommandCount(type) * v.getAverageResponseTime(type);
}
}
return t / c;
}
@Override
public long getAverageResponseTime() {
return getAverageResponseTime(null);
}
@Override
public long getMruInterval() {
long minInterval = Long.MAX_VALUE;
for (Session s : pool) {
if (s == null) {
continue;
}
long mruInterval = s.getStatsManager().getMruInterval();
minInterval = mruInterval < minInterval ? mruInterval : minInterval;
}
return minInterval;
}
@Override
public long getResultCodeCount(int resultCode) {
long retval = 0L;
for (Session s : pool) {
if (s == null) {
continue;
}
retval += s.getStatsManager().getResultCodeCount(resultCode);
}
return retval;
}
private static boolean isCutoff(int ccount, int tcount, int clim, int tlim) {
return ccount >= clim || tcount >= tlim;
}
}