package com.ausregistry.jtoolkit2.session; import java.io.IOException; import java.util.logging.Logger; import org.xml.sax.SAXException; import com.ausregistry.jtoolkit2.ErrorPkg; import com.ausregistry.jtoolkit2.Timer; import com.ausregistry.jtoolkit2.se.Command; import com.ausregistry.jtoolkit2.se.Greeting; import com.ausregistry.jtoolkit2.se.Response; import com.ausregistry.jtoolkit2.se.Result; import com.ausregistry.jtoolkit2.se.ResultCode; import com.ausregistry.jtoolkit2.xml.EPPSchemaProvider; import com.ausregistry.jtoolkit2.xml.ParsingException; /** * <p> * AusRegistry’s basic implementation of the SessionManager interface. Upon successful configuration, it * guarantees that a pool of {@link com.ausregistry.jtoolkit2.session.Session}s will be available for processing * {@link com.ausregistry.jtoolkit2.session.Transaction}s. A SessionManager is configured from a * {@link com.ausregistry.jtoolkit2.session.SessionManagerProperties} object. This implementation provides only a * blocking implementation of the <code>execute</code> method. It will create a * {@link com.ausregistry.jtoolkit2.session.SessionPool}. By default, the * {@link com.ausregistry.jtoolkit2.session.SessionPool} will use the * {@link com.ausregistry.jtoolkit2.session.TLSSession} implementation of the * {@link com.ausregistry.jtoolkit2.session.Session} interface. It also implements its own * {@link com.ausregistry.jtoolkit2.session.StatsViewer}. * </p> * * <p> * Uses the debug and user level loggers. * </p> */ public class SessionManagerImpl implements SessionManager { private static final int MAX_SLEEP_INTERRUPTS_TO_FAIL = 3; private static final int MAX_ACCEPTABLE_FAIL_COUNT = 5; private enum SMState { STOPPED, STARTED, RUNNING; } private Thread runThread; private SMState state; private SessionPoolImpl sessionPool; private SessionManagerProperties properties; private Logger debugLogger; private Logger userLogger; private String pname; { pname = getClass().getPackage().getName(); state = SMState.STOPPED; } // / For use by factory methods. SessionManagerImpl() { } SessionManagerImpl(SessionManagerProperties props) throws ConfigurationException { configure(props); } @Override public SessionManagerProperties getProperties() { return properties; } /** * Configure the SessionManager from the given set of properties. This can also be used to reconfigure the * SessionManager dynamically at any time, including changes to the managed * {@link com.ausregistry.jtoolkit2.session.SessionPool}. A change of password should be performed using {@link * #changePassword(String, String) changePassword}. Following a change of password, * the properties source should be updated to reflect the change. If this is not done, then the next invocation of * configure will fail due to session login failures. A successful call to configure indicates that subsequent * invocations of <a href="#execute(com.ausregistry.jtoolkit2.session.Transaction)">execute</a> may succeed * (dependent on the semantics of the command and the context). If the configuration fails and a previous invocation * of configure succeeded, then the previous configuration remains in effect, but the causal exception is thrown. If * the configuration fails and there was no previous valid configuration, then an exception is raised and the * SessionManager instance remains unusable (invocations of execute are guaranteed to fail, raising one of * FatalSessionException, IOException or InvalidStateException). */ @Override public void configure(SessionManagerProperties properties) throws ConfigurationException { boolean cfgSucceeded = false; SessionManagerProperties prevProperties = this.properties; this.properties = properties; try { Exception exception = null; try { doConfigure(); cfgSucceeded = true; } catch (Exception e) { cfgSucceeded = false; exception = e; } if (!cfgSucceeded) { if (prevProperties != null) { this.properties = prevProperties; try { doConfigure(); } catch (Exception e) { this.properties = null; throw e; } } else { this.properties = null; } throw exception; } } catch (Exception e) { throw new ConfigurationException(e); } } private void doConfigure() throws Exception { debugLogger = Logger.getLogger(pname + ".debug"); userLogger = Logger.getLogger(pname + ".user"); debugLogger.info("Logging configured from file: " + System.getProperty("java.util.logging.config.file")); EPPSchemaProvider.init(); EPPSchemaProvider.setValidating(properties.getSessionProperties().enforceStrictValidation()); // Need to empty and re-initialise pool if already in use. if (sessionPool != null) { sessionPool.empty(); } sessionPool = new SessionPoolImpl(properties.getSessionPoolProperties(), properties.getSessionProperties()); } /** * Get the service discovery information embodied in the most recent Greeting received from the EPP server. * * * @throws SessionConfigurationException * No prior connection to the EPP server had been established and the SessionPoolProperties provided to * the SessionPool for the purpose of establishing a new Session were invalid. * * @throws SessionOpenException * No prior connection to the EPP server had been established and the attempt to establish a connection * for the purpose of retrieving service information failed. */ @Override public Greeting getLastGreeting() throws SessionConfigurationException, SessionOpenException { try { return sessionPool.getLastGreeting(); } catch (InterruptedException ie) { return null; } } /** * Prepare the SessionManager for providing Transaction processing services. This initialises the * {@link com.ausregistry.jtoolkit2.session.SessionPool} managed by the SessionManager, guaranteeing that any * requirements defined by SessionPool properties are met, providing the pool is initialised successfully. * * @throws SessionConfigurationException * The pool was unable configure a session due to a configuration issue. Such problems include invalid * server location specification and missing or invalid authentication resources. * * @throws SessionOpenException * The pool was unable to open a session due to a configuration issue. Such problems include incorrect * server location, failed authentication and network failure. */ @Override public void startup() throws SessionConfigurationException, SessionOpenException { userLogger.info("Startup"); int failCount = 0; boolean initialised = false; while (!initialised) { try { sessionPool.getLastGreeting(); initialised = true; state = SMState.STARTED; } catch (InterruptedException ie) { failCount++; if (failCount < MAX_ACCEPTABLE_FAIL_COUNT) { userLogger.warning(ErrorPkg.getMessage("startup.interrupted")); } else { userLogger.severe(ErrorPkg.getMessage("startup.interrupted")); throw new SessionOpenException(ie); } } catch (SessionOpenException soe) { Throwable t = soe.getCause(); if (t instanceof LoginException && t.getCause() instanceof CommandFailedException && failCount < MAX_ACCEPTABLE_FAIL_COUNT) { failCount++; } else { throw soe; } } } } /** * Shutdown the SessionManager, making it unavailable for further transaction processing. */ @Override public void shutdown() { debugLogger.finest("enter"); userLogger.info("Initiating shutdown"); if (state == SMState.RUNNING) { debugLogger.info("state == RUNNING"); state = SMState.STARTED; interruptThread(runThread); } if (state == SMState.STARTED) { debugLogger.info("state == STARTED"); sessionPool.empty(); state = SMState.STOPPED; } debugLogger.info("state == STOPPED"); userLogger.info("Shutdown complete"); debugLogger.finest("exit"); } /** * Initiate the SessionPool's keep-alive system. This will run until <a href=#shutdown()>shutdown</a> is invoked on * the SessionManager. */ @Override public void run() { debugLogger.finest("enter"); if (state != SMState.STARTED) { return; } runThread = Thread.currentThread(); state = SMState.RUNNING; try { while (state == SMState.RUNNING) { long sleepInterval = sessionPool.keepAlive(); boolean interrupted = false; int retry = 0; int maxRetries = MAX_SLEEP_INTERRUPTS_TO_FAIL; long sleepTime, awakenTime; do { sleepTime = Timer.now(); try { retry++; if (sleepInterval > 0) { Thread.sleep(sleepInterval); } interrupted = false; } catch (InterruptedException ie) { // reduce the remaining sleep interval awakenTime = Timer.now(); sleepInterval += sleepTime - awakenTime; interrupted = true; } } while (interrupted && retry < maxRetries && state == SMState.RUNNING); } } catch (IOException ioe) { userLogger.severe(ioe.getMessage()); userLogger.severe(ioe.getCause().getMessage()); userLogger.severe(ErrorPkg.getMessage("epp.session.poll.cfg.fail")); } debugLogger.finest("exit"); } /** * Change the maximum size that the managed session pool will grow to. Note that this setting will not be saved to * the configuration source. */ @Override public void changeMaxPoolSize(int size) { sessionPool.setMaxSize(size); } /** * Change the EPP client password from oldPassword to newPassword. Note that this does not update the configuration * source to reflect the change - that must be done separately before any future attempts to (re-)configure the * system. */ @Override public void changePassword(String oldPassword, String newPassword) { debugLogger.finest("enter"); userLogger.info(ErrorPkg.getMessage("reconfigure.pw.change.init", new String[] {"<<old>>", "<<new>>"}, new String[] {oldPassword, newPassword})); sessionPool.empty(); try { Session session = SessionFactory.newInstance(properties.getSessionProperties()); session.changePassword(newPassword); // Attempts to get a session between changePassword and setClientPW // will fail if the password was successfully changed. It is the // application's responsibility to handle transaction failures // during a change of password. This is expected to occur very // infrequently. properties.getSessionProperties().setClientPW(newPassword); } catch (Exception ex) { userLogger.severe(ex.getMessage()); userLogger.severe(ErrorPkg.getMessage("reconfigure.pw.change.fail")); } debugLogger.finest("exit"); } /** * Try to process a single transaction. Up to {@code MAX_ACCEPTABLE_FAIL_COUNT} attempts will be made to process the * transaction in cases where I/O errors or non-protocol server errors occur during processing. Use of the * underlying session is protected against concurrent use by other threads by using the getSession/releaseSession * features of this SessionManager's {@link com.ausregistry.jtoolkit2.session.SessionPool}. This method guarantees * that the session used will be returned to the pool before the method returns. * * @throws FatalSessionException * No session could be acquired to process the transaction. Check the exception message and log records * for details. * * @throws IOException * Every attempt to execute the transaction command failed due to an IOException. This is the last such * IOException. * * @throws ParsingException * Parsing of the response failed. Check the exception message for the cause. * * @throws CommandFailedException * The acceptable limit on the number of failed commands due to server error was exceeded in trying to * process the command. This probably indicates a server limitation related to the command being * processed. * * @throws IllegalStateException * The SessionManager had been shutdown or not started up prior to invoking this method. */ @Override public void execute(Transaction tx) throws FatalSessionException, IOException, ParsingException, CommandFailedException, IllegalStateException { debugLogger.finest("enter"); if (state == SMState.STOPPED) { throw new IllegalStateException(); } Command cmd = tx.getCommand(); Response response = tx.getResponse(); int failCount = 0; boolean isExecuted = false; Session session = null; // if only processing one transaction, get a new session for each // attempt in case the session fails mid-transaction. while (!isExecuted && state != SMState.STOPPED) { try { session = sessionPool.getSession(cmd.getCommandType()); StatsManager statsManager = session.getStatsManager(); statsManager.incCommandCounter(cmd.getCommandType()); tx.start(); session.write(cmd); isExecuted = true; session.read(response); tx.setState(TransactionState.PROCESSED); statsManager.recordResponseTime(cmd.getCommandType(), tx.getResponseTime()); Result[] results = response.getResults(); assert results != null; if (results != null) { for (Result result : results) { assert result != null; statsManager.incResultCounter(result.getResultCode()); int code = result.getResultCode(); switch (code) { case ResultCode.CMD_FAILED: throw new CommandFailedException(); case ResultCode.CMD_FAILED_CLOSING: default: throw new CommandFailedException(); } } } } catch (CommandFailedException cfe) { userLogger.warning(cfe.getMessage()); if (state != SMState.STOPPED && failCount < MAX_ACCEPTABLE_FAIL_COUNT) { failCount++; } else { throw cfe; } } catch (IOException ioe) { userLogger.warning(ioe.getMessage()); userLogger.warning("net.socket.closed"); session.close(); if (state != SMState.STOPPED && failCount < MAX_ACCEPTABLE_FAIL_COUNT) { failCount++; } else { throw ioe; } } catch (InterruptedException ie) { // if interrupted by shutdown, then started will be false // Note: this still enters the finally block continue; } catch (SessionConfigurationException sce) { throw new FatalSessionException(sce); } catch (SessionOpenException soe) { throw new FatalSessionException(soe); } finally { sessionPool.releaseSession(session); } } if (!isExecuted && state == SMState.STOPPED) { throw new IllegalStateException(); } debugLogger.finest("exit"); } /** * Pipeline execute a sequence of commands over a single session. A single * {@link com.ausregistry.jtoolkit2.session.Session} is used in order to guarantee ordering of command effects. Only * those transactions in the UNPROCESSED or RETRY states are executed. The number of transactions in the PROCESSED * state following attempted execution of all eligible transactions is returned. If the number returned is less than * the number of transactions supplied, then the state of each transaction should be checked prior to getting * information from the contained response object. If the state is PROCESSED, then it is safe to use any of the * methods on the response. If the state is RETRY, then it is possible that a further attempt to execute the * transaction will succeed. If the state is FATAL_ERROR, then the transaction should not be re-attempted. In either * of these cases, the getCause method can be used to determine the reason the transaction failed. If the state * remains UNPROCESSED, it indicates that an error which occurred in processing an earlier transaction would have * prevented the successful processing of the UNPROCESSED transaction. These transactions should be retried once the * underlying cause of the first error has been resolved. * * @throws FatalSessionException * No session was available to process any transactions (possibly due to misconfiguration or service * unavailability - check the exception message and log records). See the description in * {@link com.ausregistry.jtoolkit2.session.SessionManager#execute(Transaction[])} for recommended * action. * * @throws IOException * An I/O error occurred while trying to send some command in the given Transaction array. * * @throws IllegalStateException * The SessionManager had been shutdown or not started up prior to invoking this method. See * {@link com.ausregistry.jtoolkit2.session.SessionManager#startup} and * {@link com.ausregistry.jtoolkit2.session.SessionManager#shutdown}. * * @return On success, the length of the transaction array; on failure, the index of the first failed transaction. */ @Override public int execute(Transaction[] txs) throws FatalSessionException, IllegalStateException, IOException { debugLogger.finest("enter"); if (state == SMState.STOPPED) { throw new IllegalStateException(); } Session session = null; StatsManager statsManager = null; // when pipielining commands, a single session must be used for all // transactions in order to guarantee correct command sequence. // MUST ensure that this session is released before exit from this // method. session = sendCommandAndGetSession(txs); statsManager = session.getStatsManager(); int successCount; int lastTxIdx = send(txs, session, statsManager); successCount = receive(txs, session, statsManager, lastTxIdx); sessionPool.releaseSession(session); debugLogger.finest("exit"); return successCount; } /** * Return the index of the last transaction considered for sending. */ private int send(Transaction[] txs, Session session, StatsManager statsManager) throws IOException { for (int i = 1; i < txs.length; i++) { switch (txs[i].getState()) { case PROCESSED: case FATAL_ERROR: continue; default: } Command command = txs[i].getCommand(); txs[i].start(); try { session.write(command); statsManager.incCommandCounter(command.getCommandType()); } catch (ParsingException pe) { txs[i].setState(TransactionState.FATAL_ERROR); if (pe.getCause() instanceof SAXException) { SAXException saxe = (SAXException) pe.getCause(); userLogger.warning(saxe.getMessage()); txs[i].setCause(saxe); } else { userLogger.warning(pe.getMessage()); txs[i].setCause(pe); } } catch (IOException ioe) { userLogger.severe(ioe.getMessage()); txs[i].setState(TransactionState.RETRY); txs[i].setCause(ioe); throw ioe; } } return txs.length - 1; } private int receive(Transaction[] txs, Session session, StatsManager statsManager, int lastTxIdx) { boolean isSessionReadable = true; int firstFailedIndex = Integer.MAX_VALUE; for (int j = 0; j <= lastTxIdx && isSessionReadable; j++) { switch (txs[j].getState()) { case PROCESSED: continue; case FATAL_ERROR: firstFailedIndex = Math.min(j, firstFailedIndex); continue; default: } Response response = txs[j].getResponse(); try { session.read(response); txs[j].setState(TransactionState.PROCESSED); statsManager.recordResponseTime(txs[j].getCommand().getCommandType(), txs[j].getResponseTime()); } catch (ParsingException pe) { txs[j].setState(TransactionState.FATAL_ERROR); if (pe.getCause() instanceof SAXException) { SAXException saxe = (SAXException) pe.getCause(); userLogger.warning(saxe.getMessage()); txs[j].setCause(pe.getCause()); } else { userLogger.warning(pe.getMessage()); txs[j].setCause(pe); } // We can't trust that the message boundaries will be correct // following a parsing error, so the session must be closed in // order to prevent incorrect interpretation of further service // elements received via this session. session.close(); firstFailedIndex = Math.min(j, firstFailedIndex); isSessionReadable = false; } catch (IOException ioe) { userLogger.warning(ioe.getMessage()); txs[j].setState(TransactionState.RETRY); txs[j].setCause(ioe); firstFailedIndex = Math.min(j, firstFailedIndex); isSessionReadable = false; } } return Math.min(firstFailedIndex, txs.length); } private Session sendCommandAndGetSession(Transaction[] txs) throws FatalSessionException { Session session = null; Command command = txs[0].getCommand(); int failCount = 0; while (state != SMState.STOPPED) { try { session = sessionPool.getSession(txs); txs[0].start(); session.write(command); session.getStatsManager().incCommandCounter(command.getCommandType()); return session; } catch (ParsingException pe) { txs[0].setState(TransactionState.FATAL_ERROR); txs[0].setCause(pe.getCause()); return session; } catch (IOException ioe) { sessionPool.releaseSession(session); if (failCount < MAX_ACCEPTABLE_FAIL_COUNT) { failCount++; } else { throw new FatalSessionException(ioe); } } catch (InterruptedException ie) { sessionPool.releaseSession(session); userLogger.warning(ie.getMessage()); } catch (SessionConfigurationException sce) { sessionPool.releaseSession(session); throw new FatalSessionException(sce); } catch (SessionOpenException soe) { sessionPool.releaseSession(session); throw new FatalSessionException(soe); } } throw new IllegalStateException(); } private void interruptThread(Thread thread) { if (thread == null) { return; } try { thread.interrupt(); } catch (SecurityException se) { userLogger.warning(ErrorPkg.getMessage("thread.interrupt.secex")); } } /** * Get the StatsViewer responsible for providing operating statistics about the SessionManager. */ @Override public StatsViewer getStatsViewer() { return sessionPool.getStatsViewer(); } }