/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * The contents of this file are subject to the Netscape Public * License Version 1.1 (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.mozilla.org/NPL/ * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 2001 Netscape Communications Corporation. All * Rights Reserved. * * Contributor(s): */ package org.ietf.ldap; import java.util.*; import java.io.*; import java.net.*; /** * Makes a connection to a server from a list using "smart" failover. * Connection attempts can be made serially from the same thread, or * in parallel by creating a separate thread after the specified delay. * Connection setup status is preserved for later attempts, so that servers * that are more likely to be available will be tried first. *
* The total time spent opening a connection can be limited with the
* ConnectTimeout
property.
*
* When a connection is successfully created, a socket is opened. The socket
* is passed to the LDAPConnThread. The LDAPConnThread must call
* invalidateConnection() if the connection is lost due to a network or
* server error, or disconnect() if the connection is deliberately terminated
* by the user.
*/
class LDAPConnSetupMgr implements Cloneable, Serializable {
static final long serialVersionUID = 1519402748245755306L;
/**
* Policy for opening a connection when multiple servers are used
*/
private static final int SERIAL = 0;
private static final int PARALLEL = 1;
/**
* ServerEntry.connSetupStatus possible value. The values also represent
* the likelihood that the connection will be setup to a server. Lower
* values have higher priority. See sortDsList() method
*/
private static final int CONNECTED = 0;
private static final int DISCONNECTED = 1;
private static final int NEVER_USED = 2;
private static final int INTERRUPTED = 3;
private static final int FAILED = 4;
/**
* Representation for a server in the server list.
*/
class ServerEntry {
String host;
int port;
int connSetupStatus;
Thread connSetupThread;
ServerEntry(String host, int port, int status) {
this.host = host;
this.port = port;
connSetupStatus = status;
connSetupThread = null;
}
public String toString() {
return "{" +host+":"+port + " status="+connSetupStatus+"}";
}
}
/**
* Socket to the connected server
*/
private Socket _socket = null;
/**
* Last exception occured during connection setup
*/
private LDAPException _connException = null;
/**
* List of server to use for the connection setup
*/
ServerEntry[] _dsList;
/**
* Index of the last connected server
*/
private int _dsIdx = -1;
/**
* Socket factory for SSL connections
*/
LDAPSocketFactory _factory;
/**
* Connection setup policy (PARALLEL or SERIAL)
*/
int _policy = SERIAL;
/**
* Delay in ms before another connection setup thread is started.
*/
int _connSetupDelay = -1;
/**
* The maximum time to wait to established the connection
*/
int _connectTimeout = 0;
/**
* During connection setup, the current count of servers to which
* connection attmpt has been made
*/
private transient int _attemptCnt = 0;
/**
* Connection IDs for ldap trace messages
*/
private static int _nextId;
private int _id;
/**
* Constructor
* @param host list of host names to which to connect
* @param port list of port numbers corresponding to the host list
* @param factory socket factory for SSL connections
*/
LDAPConnSetupMgr(String[] hosts, int[] ports, LDAPSocketFactory factory) {
_dsList = new ServerEntry[hosts.length];
for (int i=0; i < hosts.length; i++) {
_dsList[i] = new ServerEntry(hosts[i], ports[i], NEVER_USED);
}
_factory = factory;
_id = _nextId++;
}
/**
* Try to open the connection to any of the servers in the list, limiting
* the time waiting for the connection to be established
* @return connection socket
*/
synchronized Socket openConnection() throws LDAPException{
long tcur=0, tmax = Long.MAX_VALUE;
Thread th = null;
reset();
// If reconnecting, sort dsList so that servers more likly to
// be available are tried first
sortDsList();
if (_connectTimeout == 0) {
// No need for a separate thread, connect time not limited
connect();
}
else {
// Wait for connection at most _connectTimeout milliseconds
// Run connection setup in a separate thread to monitor the time
tmax = System.currentTimeMillis() + _connectTimeout;
th = new Thread (new Runnable() {
public void run() {
connect();
}
}, "ConnSetupMgr");
th.setDaemon(true);
th.start();
while (_socket==null && (_attemptCnt < _dsList.length) &&
(tcur = System.currentTimeMillis()) < tmax) {
try {
wait(tmax - tcur);
}
catch (InterruptedException e) {
th.interrupt();
cleanup();
throw new LDAPInterruptedException("Interrupted connect operation");
}
}
}
if (_socket != null) {
return _socket;
}
if ( th != null && (tcur = System.currentTimeMillis()) >= tmax) {
// We have timed out
th.interrupt();
cleanup();
throw new LDAPException(
"connect timeout, " + getServerList() + " might be unreachable",
LDAPException.CONNECT_ERROR);
}
if (_connException != null && _dsList.length == 1) {
throw _connException;
}
throw new LDAPException(
"failed to connect to server " + getServerList(),
LDAPException.CONNECT_ERROR);
}
private void reset() {
_socket = null;
_connException = null;
_attemptCnt = 0;
for (int i=0; i < _dsList.length; i++) {
_dsList[i].connSetupThread = null;
}
}
private String getServerList() {
StringBuffer sb = new StringBuffer();
for (int i=0; i < _dsList.length; i++) {
sb.append(i==0 ? "" : " ");
sb.append(_dsList[i].host);
sb.append(":");
sb.append(_dsList[i].port);
}
return sb.toString();
}
private void connect() {
if (_policy == SERIAL || _dsList.length == 1) {
openSerial();
}
else {
openParallel();
}
}
/**
* Called when the current connection is lost.
* Put the connected server at the end of the server list for
* the next connect attempt.
*/
synchronized void invalidateConnection() {
if (_socket != null) {
_dsList[_dsIdx].connSetupStatus = FAILED;
// Move the entry to the end of the list
int srvCnt = _dsList.length, j=0;
ServerEntry[] newDsList = new ServerEntry[_dsList.length];
for (int i=0; i < srvCnt; i++) {
if (i != _dsIdx) {
newDsList[j++] = _dsList[i];
}
}
newDsList[j] = _dsList[_dsIdx];
_dsList = newDsList;
_dsIdx = j;
}
_socket = null;
}
/**
* Called when the current connection is terminated by the user.
* Mark the connected server status as DISCONNECTED. This will
* put it at top of the server list for the next connect attempt.
*/
void disconnect() {
if (_socket != null) {
_dsList[_dsIdx].connSetupStatus = DISCONNECTED;
}
_socket = null;
}
Socket getSocket() {
return _socket;
}
String getHost() {
if (_dsIdx >= 0) {
return _dsList[_dsIdx].host;
}
return _dsList[0].host;
}
int getPort() {
if (_dsIdx >= 0) {
return _dsList[_dsIdx].port;
}
return _dsList[0].port;
}
int getConnSetupDelay() {
return _connSetupDelay/1000;
}
/**
* Selects the connection failover policy
* @param delay in seconds for the parallel connection setup policy.
* Possible values are:
(delay=-1) use serial policy,
* (delay=0) start immediately concurrent threads to each specified server
*
(delay>0) create a new connection setup thread after delay seconds
*/
void setConnSetupDelay(int delay) {
_policy = (delay < 0) ? SERIAL : PARALLEL;
_connSetupDelay = delay*1000;
}
int getConnectTimeout() {
return _connectTimeout/1000;
}
/**
* Sets the maximum time to spend in the openConnection() call
* @param timeout in seconds to wait for the connection to be established
*/
void setConnectTimeout(int timeout) {
_connectTimeout = timeout*1000;
}
/**
* Check if the user has voluntarily closed the connection
*/
boolean isUserDisconnected() {
return (_dsIdx >=0 &&
_dsList[_dsIdx].connSetupStatus == DISCONNECTED);
}
/**
* Try sequentially to open a new connection to a server.
*/
private void openSerial() {
for (int i=0; i < _dsList.length; i++) {
_dsList[i].connSetupThread = Thread.currentThread();
connectServer(i);
if (_socket != null) {
return;
}
}
}
/**
* Try concurrently to open a new connection a server. Create a separate
* thread for each connection attempt.
*/
private synchronized void openParallel() {
for (int i=0; _socket==null && i < _dsList.length; i++) {
//Create a Thread to execute connectSetver()
final int dsIdx = i;
String threadName = "ConnSetupMgr " +
_dsList[dsIdx].host + ":" + _dsList[dsIdx].port;
Thread t = new Thread(new Runnable() {
public void run() {
connectServer(dsIdx);
}
}, threadName);
_dsList[dsIdx].connSetupThread = t;
t.setDaemon(true);
t.start();
// Wait before starting another thread if the delay is not zero
if (_connSetupDelay != 0 && i < (_dsList.length-1)) {
try {
wait(_connSetupDelay);
}
catch (InterruptedException e) {
return;
}
}
}
// At this point all threads are started. Wait until first thread
// succeeds to connect or all threads terminate
while (_socket == null && (_attemptCnt < _dsList.length)) {
// Wait for a thread to terminate
try {
wait();
}
catch (InterruptedException e) {}
}
}
/**
* Connect to the server at the given index
*/
void connectServer(int idx) {
ServerEntry entry = _dsList[idx];
Thread currThread = Thread.currentThread();
Socket sock = null;
LDAPException conex = null;
try {
/* If we are to create a socket ourselves, make sure it has
sufficient privileges to connect to the desired host */
if (_factory == null) {
sock = new Socket (entry.host, entry.port);
//s.setSoLinger( false, -1 );
} else {
sock = _factory.createSocket(entry.host, entry.port);
}
}
catch (IOException e) {
conex = new LDAPException("failed to connect to server "
+ entry.host+":"+entry.port, LDAPException.CONNECT_ERROR);
}
catch (LDAPException e) {
conex = e;
}
if (currThread.isInterrupted()) {
return;
}
synchronized (this) {
if (_socket == null && entry.connSetupThread == currThread) {
entry.connSetupThread = null;
if (sock != null) {
entry.connSetupStatus = CONNECTED;
_socket = sock;
_dsIdx = idx;
cleanup(); // Signal other concurrent threads to terminate
}
else {
entry.connSetupStatus = FAILED;
_connException = conex;
}
_attemptCnt++;
notifyAll();
}
}
}
/**
* Terminate all concurrently running connection setup threads
*/
private synchronized void cleanup() {
Thread currThread = Thread.currentThread();
for (int i=0; i < _dsList.length; i++) {
ServerEntry entry = _dsList[i];
if (entry.connSetupThread != null && entry.connSetupThread != currThread) {
entry.connSetupStatus = INTERRUPTED;
//Thread.stop() is considered to be dangerous, use Thread.interrupt().
//interrupt() will however not work if the thread is blocked in the
//socket library native connect() call, but the connect() will
//eventually timeout and the thread will die.
entry.connSetupThread.interrupt();
entry.connSetupThread = null;
}
}
}
/**
* Sorts Server List so that servers which are more likely to be available
* are tried first. The likelihood of making a successful connection
* is determined by the connSetupStatus. Lower values have higher
* likelihood. Thus, the order of server access is (1) disconnected by
* the user (2) never used (3) interrupted connection attempt
* (4) connection setup failed/connection lost
*/
private void sortDsList() {
int srvCnt = _dsList.length;
for (int i=1; i < srvCnt; i++) {
for (int j=0; j < i; j++) {
if (_dsList[i].connSetupStatus < _dsList[j].connSetupStatus) {
// swap entries
ServerEntry entry = _dsList[j];
_dsList[j] = _dsList[i];
_dsList[i] = entry;
}
}
}
}
/**
* This is used only by the ldapjdk test libaray to simulate a
* server problem and to test fail-over and rebind
* @return A flag whether the connection was closed
*/
boolean breakConnection() {
try {
_socket.close();
return true;
}
catch (Exception e) {
return false;
}
}
public String toString() {
String str = "dsIdx="+_dsIdx+ " dsList=";
for (int i=0; i < _dsList.length; i++) {
str += _dsList[i]+ " ";
}
return str;
}
int getID() {
return _id;
}
String getLDAPUrl() {
return ((_factory == null) ? "ldap" : "ldaps") +
"://" + getHost() + ":" + getPort();
}
public Object clone() {
try {
LDAPConnSetupMgr cloneMgr = (LDAPConnSetupMgr) super.clone();
cloneMgr._dsList = new ServerEntry[_dsList.length];
for (int i=0; i<_dsList.length; i++) {
ServerEntry e = _dsList[i];
cloneMgr._dsList[i] = new ServerEntry(e.host, e.port, e.connSetupStatus);
}
return cloneMgr;
}
catch (CloneNotSupportedException ex) {
return null;
}
}
}