// SocketClientFactory.java
// $Id: SocketClientFactory.java,v 1.17 1999/06/04 16:17:20 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.http.socket ;

import java.io.* ;
import java.net.* ;

import org.w3c.jigsaw.http.*;
import org.w3c.jigsaw.daemon.*;
import org.w3c.jigsaw.config.*;
import org.w3c.util.*;

class DebugThread extends Thread {
    SocketClientFactory pool = null;

    public void run() {
	while ( true ) {
	    try {
		sleep(1000*10);
		// Display some client statistics:
		SocketClientState cs = null;
		cs = (SocketClientState) pool.freeList.getHead();
		while (cs != null) {
		    System.out.println(cs.client
				       + " reqcount="
                                       + cs.client.getRequestCount()
				       + ", bindcount="
                                       + cs.client.getBindCount());
		    cs = (SocketClientState)pool.freeList.getNext((LRUAble)cs);
		}
		System.out.println("freeCount ="+pool.freeCount);
		System.out.println("idleCount ="+pool.idleCount);
		System.out.println("totalCount="+pool.clientCount);
		System.out.println("estimCount="+pool.clientEstim);
		System.out.println("Average: "+pool.loadavg);
	    } catch (Exception ex) {
		ex.printStackTrace();
	    }
	}
    }

    DebugThread(SocketClientFactory pool) {
	this.pool = pool;
	setPriority(Thread.MAX_PRIORITY);
    }

    
}


/**
 * The client pool is a kind of client factory.
 * Each time the server gets a new connection, it calls the client pool
 * to bound a client object (newly created or spared) to handle it.
 */

public class SocketClientFactory implements ClientFactory, PropertyMonitoring {
    private static final boolean debug       = false;
    private static final boolean debugthread = false;

    public static final int MINSPARE_FREE = 5;
    public static final int MAXSPARE_FREE = 10;
    public static final int MAXSPARE_IDLE = 20;
    public static final int MAXTHREADS    = 40;
    public static final int MAXCLIENTS    = 32;
    public static final int IDLETO        = 10000;

    public static final int AVG_LIGHT = 1;
    public static final int AVG_NORMAL = 2;
    public static final int AVG_HIGH = 3;
    public static final int AVG_DEAD = 4;
    
    // FIXME doc
    public final static String 
    MINSPARE_FREE_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.minFree";
    // FIXME doc
    public final static String 
    MAXSPARE_FREE_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.maxFree";
    // FIXME doc
    public final static String 
    MAXSPARE_IDLE_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.maxIdle";
    // FIXME doc
    public final static String 
    MAXTHREADS_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.maxThreads";
    // FIXME doc
    public final static String
    MAXCLIENTS_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.maxClients";
    // FIXME doc
    public final static String
    IDLETO_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.idleTimeout";
    // FIXME doc
    public final static String
    BINDADDR_P = "org.w3c.jigsaw.http.socket.SocketClientFactory.bindAddress";
    
    int minFree    = 0;
    int maxFree    = 0;
    int maxIdle    = 0;
    int maxClients = 0;
    InetAddress bindAddr = null;

    int                  count     = 0 ;	// number of created clients.
    httpd                server    = null ;
    ObservableProperties props     = null ;
    int                  busyCount = 0 ;	// number of busy clients.

    LRUList idleList = null;
    LRUList freeList = null;

    SocketClientState csList = null;

    int idleCount   = 0 ;
    int freeCount   = 0 ;
    int clientCount = 0 ;
    int clientEstim = 0 ;

    ThreadCache threadcache = null;

    int loadavg = AVG_LIGHT;

    boolean alive = true;

    /**
     * Some property have changed, update our setting.
     * @param name The name of the property that has changed.
     * @return A boolean, <strong>true</strong> if we updated ourself 
     *    successfully.
     */

    public boolean propertyChanged (String name) {
	httpd s = server;
	if ( name.equals(MINSPARE_FREE_P) ) {
	    minFree = props.getInteger(MINSPARE_FREE_P, minFree);
	} else if ( name.equals(MAXSPARE_FREE_P) ) {
	    maxFree = props.getInteger(MAXSPARE_FREE_P, maxFree);
	} else if ( name.equals(MAXSPARE_IDLE_P) ) {
	    maxIdle = props.getInteger(MAXSPARE_IDLE_P, maxIdle);
	} else if ( name.equals(MAXTHREADS_P) ) {
	    int maxThreads = props.getInteger(MAXTHREADS_P, -1);
	    if ( maxThreads > 0 )
		threadcache.setCachesize(maxThreads);
	} else if ( name.equals(IDLETO_P) ) {
	    int idleto = props.getInteger(IDLETO_P, -1);
	    if ( idleto > 0 )
		threadcache.setIdleTimeout(idleto);
	} else if ( name.equals(MAXCLIENTS_P) ) {
	    int newmax = props.getInteger(MAXCLIENTS_P, -1);
	    if ( newmax > maxClients ) {
		for (int i = maxClients-newmax; --i >= 0; )
		    addClient(true);
	    } else if ( newmax > 0 ) {
		maxClients = newmax;
	    }
	} else if (name.equals(BINDADDR_P)) {
	    try {
		bindAddr = InetAddress.getByName(props.getString(BINDADDR_P,
								 null));
	    } catch (Exception ex) {
		// nothing
	    }
	}
	return true ;
    }

    /**
     * Remove this client state from the glohbal client list.
     * @param cs The client state to remove from the list.
     */

    protected synchronized void deleteClient(SocketClientState cs) {
	if ( cs.csprev == null ) {
	    csList = cs.csnext;
	} else if ( cs.csnext == null ) {
	    cs.csprev.csnext = null;
	} else {
	    cs.csprev.csnext = cs.csnext;
	    cs.csnext.csprev = cs.csprev;
	}
    }

    /**
     * Create a new client for this pool.
     * @param free A boolean, if <strong>true</strong> the client is inserted
     * straight into the free list, otherwise, it is not plugged into any
     * list.
     * @return A SocketClientState instance, if creation of a new client was
     * allowed, <strong>null</strong> if no more clients could be created.
     */

    protected synchronized SocketClientState addClient (boolean free) {
	// Create a new client. 
	csList                   = new SocketClientState(csList);
	SocketClientState cs     = csList;
	SocketClient      client = new SocketClient(server, this, cs);
	cs.client = client;
	clientCount++;
	clientEstim++;
	// Plug into free LRU if required:
	if ( free ) {
	    cs.status = SocketClientState.C_FREE;
	    freeList.toHead(cs);
	    freeCount++;
	}
	return cs ;
    }

    /**
     * Update our idea of the current load.
     * The one important invariant here, is that whenever the free list
     * becomes empty, then the load average should be equals to
     * <strong>AVG_DEAD</strong>. This ensures that the server will start 
     * dropping connections.
     *
     */

    private final void updateLoadAverage() {
	int oldavg = loadavg;
	if ( freeCount >= maxFree ) {
	    loadavg = AVG_LIGHT;
	} else if ((freeCount >= minFree) || (idleCount >= maxIdle)) {
	    if ((loadavg = AVG_NORMAL) < oldavg)
		server.thread.setPriority(Thread.MAX_PRIORITY);
	} else if ( freeCount > 0 ) {
	    /* idleCount < MINSPARE_IDLE */
	    if ((loadavg = AVG_HIGH) > oldavg)
		server.thread.setPriority(server.getClientThreadPriority()-2);
	} else {
	    loadavg = AVG_DEAD;
	}
    }
	    
    private final synchronized void incrClientCount() {
	++clientCount;
	++clientEstim;
	updateLoadAverage();
    }

    private final synchronized void decrClientCount() {
	--clientCount;
	updateLoadAverage();
    }

    private final synchronized boolean incrFreeCount() {
	if ( clientEstim > maxClients ) {
	    clientEstim--;
	    return false;
	}
	++freeCount;
	updateLoadAverage();
	return true;
    }

    private final synchronized boolean decrFreeCount() {
	if ( freeCount > 0 ) {
	    --freeCount;
	    updateLoadAverage();
	    return true;
	} else {
	    return false;
	}
    }

    private final synchronized boolean incrIdleCount() {
	if ((loadavg > AVG_HIGH) || (idleCount + 1 >= maxIdle))
	    return false;
	++idleCount;
	updateLoadAverage();
	return true;
    }
    
    private final synchronized boolean decrIdleCount() {
	if ( idleCount > 0 ) {
	    --idleCount;
	    updateLoadAverage();
	    return true;
	} else {
	    return false;
	}
    }

    /**
     * Notify that this client has finished with its connection.
     * If the pool wants the client to be freed (because it has too many of 
     * them), it makes the client kill itself (which will trigger a call to
     * the clientFinished method, were enventual cleanup is performed).
     * @param client The client that is done with its connection.
     */

    protected boolean clientConnectionFinished (SocketClient client) {
	// If the client pool has shut down, exit straight:
	if ( ! alive )
	    return false;
	SocketClientState cs = client.state;
	switch(cs.status) {
	  case SocketClientState.C_IDLE:
	      decrIdleCount();
	      idleList.remove(cs);
	      break;
	  case SocketClientState.C_BUSY:
	  case SocketClientState.C_KILL:
	      break;
	  default:
	    break;
	}
	if (incrFreeCount() ) {
	    if ( debug )
		System.out.println(client+": now free.");
	    freeList.toTail(cs);
	    return true;
	} else {
	    if ( debug )
		System.out.println(client+": terminate.");
	    return false;
	}
    }

    /**
     * Notify that this client has been killed.
     * @param client The client that has terminate.
     */

    protected void clientFinished (SocketClient client) {
	SocketClientState cs = client.state;
	if ( debug )
	    System.out.println(client+": finished "+cs.status);
	// If we're not alive any more, skip:
	if ( ! alive )
	    return;
	// Otherwise, perform the job:
	switch(cs.status) {
	  case SocketClientState.C_IDLE:
	      decrIdleCount();
	      idleList.remove(cs);
	      break;
	  case SocketClientState.C_BUSY:
	  case SocketClientState.C_FREE:
	      break;
	  default:
	      String msg = (client 
			    + ": finished with unknown status "
			    + cs.status);
	      server.errlog(msg);
	      break;
	}
	cs.status = SocketClientState.C_FIN;
	decrClientCount();
    }

    /**
     * The client notifies the pool that is has been activated.
     * The client state object is updated to unmark the client as idle.
     * <p>This method needs not be synchronized, as it affect only the client
     * state, <em>not</em> the client list.
     * @param client The activated client.
     */

    protected void notifyUse(SocketClient client) {
	if ( debug )
	    System.out.println(client+": used.");
	SocketClientState cs = client.state;
	decrIdleCount();
	idleList.remove(cs);
	cs.status = SocketClientState.C_BUSY;
    }

    /**
     * The client notifies the pool that it enters idle state.
     * <p>This method needs not be synchronized, as it affect only the client
     * state, <em>not</em> the client list.
     * @param client The client that is going to be idle.
     */

    protected boolean notifyIdle(SocketClient client) {
	SocketClientState cs = client.state;
	if ( alive ) {
	    if ( incrIdleCount() ) {
		if ( debug ) 
		    System.out.println(client+": idle, keep-alive.");
		idleList.toHead(cs);
		cs.status = SocketClientState.C_IDLE;
		return true;
	    } else {
		if ( debug )
		    System.out.println(client+": idle, closed.");
		// Kill some old idle connections, give a chance for next:
		int killsome = (maxFree-minFree) >> 1;
		killSomeClients((killsome > 0) ? killsome : 1);
		// And give it a change if the load is not too high
		if ( incrIdleCount() ) {
		    if ( debug ) 
			System.out.println(client+": idle, keep-alive.");
		    idleList.toHead(cs);
		    cs.status = SocketClientState.C_IDLE;
		    return true;	
		} 
		return false;
	    }
	} else {
	    if ( debug )
		System.out.println(client+": idle (dead), closed.");
	    // Kill some old idle connections, give a chance for next:
	    int killsome = (maxFree-minFree) >> 1;
	    killSomeClients((killsome > 0) ? killsome : 1);
	    return false;
	}
    }

    protected void killSomeClients(int howmany) {
	int count = (howmany > 0) ? howmany : (maxFree - freeCount);
	if (debug) {
	    System.out.println("Killing :" + howmany);
	}
	while ( --count >= 0 ) {
	    SocketClientState cs = (SocketClientState) idleList.removeTail();
	    if ( cs != null ) {
		if ( debug )
		    System.out.println(cs.client+": kill (some-client).");
		decrIdleCount();
		cs.status = SocketClientState.C_KILL;
		cs.client.unbind();
	    } else {
		break;
	    }
	}
    }

    final protected void killSomeClients() {
	killSomeClients(-1);
    }

    protected void run(SocketClient client) {
	if ( debug )
	    System.out.println(client+": warming up...");
	boolean threaded = threadcache.getThread(client, true);
	if ( debug )
	    System.out.println(client+": threaded="+threaded);
    }

    /**
     * Handle the given connection.
     * Find a free client, bind it to the given socket, and run it. If we
     * have reached our maximum allowed number of clients, kill some
     * connections.
     * <p>A client enters the LRU list (and become a candidate for kill) only
     * after it has handle one request (or if some timeout expires). This is
     * performed by the first call to <code>notifyUse</code> which will
     * silently insert the client into the LRU if it was not there already.
     * <p>This client pool does a lot of nice thinigs, but could probably be
     * implemented in a much better way (while keeping the features it has).
     * Contention arond the pool is probably concern number 1 of performances.
     * @param socket The connection to handle.
     */

    public void handleConnection (Socket socket) {
	if ( debug )
	    System.out.println("new connection.");
	SocketClientState cs = null;
	switch(loadavg) {
	  case AVG_LIGHT:
	      // Free list is non empty, be fast:
	      if ( decrFreeCount() )
		  cs = (SocketClientState) freeList.removeTail();
	      break;
	  case AVG_NORMAL:
	  case AVG_HIGH:
	      // Free list is non empty, but we try killing a client:
	      killSomeClients();
	      if ( decrFreeCount() )
		  cs = (SocketClientState) freeList.removeTail();
	      break;
	  case AVG_DEAD:
	      break;
	}
	if ( debug )
	    System.out.println("load "+loadavg
			       + ", client="
			       + ((cs != null) 
				  ? cs.client.toString()
				  : "unbound"));
	// At this point, we do have a free client, bind it:
	if ( cs != null ) {
	    if ( debug ) 
		System.out.println(cs.client+": bound.");
	    cs.status = SocketClientState.C_BUSY;
	    cs.client.bind(socket);
	} else {
	    if ( debug )
		System.out.println("*** connection refused (overloaded).");
	    try {
		socket.close();
	    } catch (IOException ex) {
	    }
	    server.errlog(socket.getInetAddress()+" refused (overloaded).");
	}
	return;
    }

    protected synchronized void killClients(boolean force) {
	alive = false;
	// Kill all clients (first shot):
	SocketClientState cs = csList;
	while ((cs != null) && (cs.client != null)) {
	    // Only if a client is idely read'ing its socket, do we close it
	    cs.client.kill(cs.status==SocketClientState.C_IDLE);
	    cs = cs.csnext;
	}
	// Kill all clients (second shot):
	// Some client may be in transition during first shot, second shot
	// really kills everything.
	try {
	    Thread.sleep(5000);
	} catch (Exception ex) {
	}
	cs = csList;
	while ((cs != null) && (cs.client != null)) {
	    // Only if a client is idely read'ing its socket, do we close it
	    cs.client.kill(true);
	    cs = cs.csnext;
	}
    }

    /**
     * Shutdown the client pool. 
     * If force is <strong>true</strong>, kill all running clients right
     * now, otherwise, wait for them to terminate gracefully, and return
     * when done.
     * @param force Should we interrupt running clients.
     */

    public void shutdown (boolean force) {
	// First stage: kill all clients (synchronized)
	killClients(force) ;
	// Second stage (unsynchronized), join all client threads
	SocketClientState cs = csList;
	while ((cs != null) && (cs.client != null)) {
	    if ( debug )
		System.out.println(cs.client+": join."); 
	    cs.client.join();
	    cs = cs.csnext;
	}
	// Some cleanup (helps the GC, and make sure everything is free)
	props.unregisterObserver(this);
	props    = null;
	csList   = null;
	freeList = null;
	idleList = null;
	server   = null;
    }

    /**
     * Create the master socket for this client factory.
     * @exception IOException If some IO error occurs while creating the
     * server socket.
     * @return A ServerSocket instance.
     */

    public ServerSocket createServerSocket() 
	throws IOException
    {
	// using maxCLient in the backlog is safe, but an overkill :)
	if (bindAddr != null) {
	    return new ServerSocket (server.getPort(), maxClients);
	} else {
	    return new ServerSocket (server.getPort(), 
				     Math.max(128, maxClients),
				     bindAddr);
	}
    }

    /**
     * Initialize the raw, client socket factory.
     * @param server The server context we are attached to.
     */

    public void initialize(httpd server) {
	// Initialize instance variables:
	this.server = server ;
	this.props  = server.getProperties() ;
	this.props.registerObserver (this) ;
	// Register our property sheet:
	PropertySet set = new SocketConnectionProp("SocketConnectionProp"
						   , server);
	server.registerPropertySet(set);
	// Initialize parameters from properties:
	this.minFree    = props.getInteger(MINSPARE_FREE_P, MINSPARE_FREE);
	this.maxFree    = props.getInteger(MAXSPARE_FREE_P, MAXSPARE_FREE);
	this.maxIdle    = props.getInteger(MAXSPARE_IDLE_P, MAXSPARE_IDLE);
	this.maxClients = props.getInteger(MAXCLIENTS_P, MAXCLIENTS);
	String bindAddrName = props.getString(BINDADDR_P, null);
	if (bindAddrName != null) {
	    try {
		bindAddr = InetAddress.getByName(bindAddrName);
	    } catch (Exception ex) {
		// nothing, fallback to default
	    }
	}
	// Create the LRU lists:
	idleList = new AsyncLRUList();
	freeList = new AsyncLRUList();
	// Create the full client list:
	csList = new SocketClientState();
	// Create all our clients:
	for (int i = 0 ; i < maxClients ; i++) {
	    if ( addClient(true) == null )
		throw new RuntimeException (this.getClass().getName()
					    + "[construstructor]"
					    + ": unable to create clients.");
	}
	// Create the thread cache:
	threadcache = new ThreadCache("socket-clients");
	threadcache.setCachesize(props.getInteger(MAXTHREADS_P, MAXTHREADS));
	threadcache.setThreadPriority(server.getClientThreadPriority());
	threadcache.setIdleTimeout(props.getInteger(IDLETO_P,IDLETO));
	threadcache.setGrowAsNeeded(true);
	threadcache.initialize();
	// Start the debugging thread, if needed:
	if ( debugthread )
	    new DebugThread(this).start();
    }

    /**
     * Empty constructor for dynamic class instantiation.
     */

    public SocketClientFactory() {
    }

}
