/*
 * Copyright (c) 1995-1997 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of Sun
 * Microsystems, Inc. ("Confidential Information").  You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Sun.
 * 
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES
 * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
 * THIS SOFTWARE OR ITS DERIVATIVES.
 * 
 * CopyrightVersion 1.0
 */


/*
 * This servlet balances the load for some portion of the namespace of your
 * server.   Its setup is described at the bottom of this comment.
 *
 * OVERVIEW
 * This servlet works by maintaining a list of servers (host:port pairs).
 * When given a URI to service, the servlet sends a redirect to the client to
 * <scheme>://<server>/<requested-URI>.  <server> is chosen in round-robin
 * fashion from the list of servers.  <scheme> will be replaced by http or
 * https as appropriate.  
 *
 * The servlet also detects if host:port servers are up by asking them for /.
 * If the server makes any response it is assumed to be up.  Down servers are
 * not returned in a redirect to a client.  This check is made approximately
 * once per minute.  When the server comes back up, it will be added to the
 * list of hosts to return.
 *
 * The ramification of sending a redirect is that the client has to make two
 * requests, doubling the latency.  It also means that clients could bookmark
 * the redirected-to URL.  However, it keeps this servlet simple and keeps the
 * load on the machine running this servlet low.  These problems could be
 * solved by writing a servlet that proxies requests to a set of load balanced
 * servers, but proxying requires more resources than simple redirects.
 * 
 * You could also use a load balancing name server, such as lbnamed or a
 * recent version of BIND.  These servers balance the load by returning
 * different IP addresses for the same host name.  However, since many clients
 * do not respect TTLs on DNS responses, the information that clients use can
 * become stale.  So clients may continue to think a host is down when other
 * hosts in the same family of servers are up.  In some sense this servlet has
 * the same problem since a client could have bookmarks referencing a down
 * host.
 *
 * The implementations of these other servers are better able to detect the
 * load on balanced machines.  (This servlet has no such mechanism -- it just
 * uses round-robin.)
 *
 * One advantage of load balancing at a URI namespace level is that you can
 * direct different portions of your namespace to different groups of servers.
 * With DNS-based solutions, you don't know what URI the client is requesting
 * when the DNS lookup is made, so you have to use a single pool of machines
 * (or expose the client to multiple hostnames). You can use multiple
 * instances of the RedirectLoadBalServlet to manage different parts of the
 * server's namespace with different target hosts.
 *
 * SETUP
 * Make sure the servlet is in the servlets directory, and add it with the
 * Servlets panel of the admin applet.  You can give it whatever name you
 * want.  The class name should be "RedirectLoadBalServlet".  The Arguments:
 * field should have the following syntax:
 * 
 * hosts=<host1:port1> [ <space> <host2:port2>  .... <hostN:portN> ]
 * 
 * so for example, to load balance across dogbert.Eng.Sun.COM:8080 and
 * dilbert.Eng.Sun.COM:80 you would enter:
 *
 * hosts=dogbert.Eng.Sun.COM:8080 dilbert.Eng.Sun.COM:80
 *
 * NOTE: you must enter the fully-qualified hostname (i.e., include your DNS
 * domain).  The servlet does not try to determine the domain of your machine.
 * 
 * Switch to the Setup panel and select "Servlet Aliases".  Map the part of
 * the name space you want load balanced to the name you gave the
 * RedirectLoadBalServlet.  That's it!
 *
 * If you want to have different parts of the name space going to different
 * sets of servers, then repeat this procedure but give the servlet a new name
 * in the Servlets panel.  (Of course, you'll enter a different set of hosts
 * in the Arguments field.) And then use that new servlet name for the other
 * part of the name space in the aliases section of the Setup panel.
 *     
 */

import java.io.*;
import java.net.*;
import java.util.StringTokenizer;

import javax.servlet.*;
import javax.servlet.http.*;

public class RedirectLoadBalServlet extends HttpServlet 
{ 
    /* 
     * The monitor thread checks to make sure that the hosts this servlet
     * knows about are up.  There is one monitor thread per instance of load 
     * balancing servlet.
     */
    class MonitorThread extends Thread {
	private RedirectLoadBalServlet rlbs;
	private final int CHECK_INTERVAL = 60000;
	private final int TIMEOUT_INTERVAL = 10000;
	private byte read_buf[] = new byte[8192];
	
	MonitorThread(RedirectLoadBalServlet rlbs) {
	    this.rlbs = rlbs;
	}
	
	/*
	 * The monitor thread checks all hosts to see if they're up.  Then it 
	 * sleeps for CHECK_INTERVAL ms, and repeats.
	 */
	public void run() 
	{
	    HostData data[] = rlbs.data;
	    int num_hosts = rlbs.data.length;
	    boolean tmp_up[] = new boolean[num_hosts];

	    while (true) {
		// check every host we know about
		for (int i = 0; i < num_hosts; i++) {
		    tmp_up[i] = true;
		    Socket s = null;
		    try {
			// connect to the target and get /
			s = new Socket(data[i].name, data[i].port);
			s.setSoTimeout(TIMEOUT_INTERVAL);
			OutputStream out = s.getOutputStream();
			InputStream in = s.getInputStream();
			String req = "GET /\r\n";
			out.write(req.getBytes(), 0, req.length());
			while (in.read(read_buf, 0, read_buf.length) != -1) ;
		    }
		    catch (Exception e) {
			// on error, assume host is down
			tmp_up[i] = false;
		    }
		    try { if (s != null) s.close(); } catch (Exception e) { ; }
		}
		
		// report results back to servlet
		for (int i = 0; i < num_hosts; i++)
		    rlbs.data[i].up = tmp_up[i];

		// sleep for some interval
		try { Thread.sleep(CHECK_INTERVAL); } catch (Exception e) { ; }
	    }
	}
    }

    /*
     * Bookkeeping class about hosts that are being load balanced 
     */
    class HostData { 
	String location;	// <hostname>[:<port>]
	String name;		// <hostname>
	int port;		// <port> -- will be set to 80 if not specified
	boolean up;		// host is known to be up (true) or down (false)

	HostData(String location) 
	{
	    this.location = location;
	    int colon = location.indexOf(":", 0);
	    if (colon == -1) {
		name = location;
		port = 80;
	    }
	    else { 
		name = location.substring(0, colon);
		String p = location.substring(colon + 1, location.length());
		port = (Integer.valueOf(p)).intValue();
	    }
	    up = true;
	}
    }

    protected int round_robin = 0;  	// points to the first host to check
    protected HostData data[];		// info about all the hosts
    protected MonitorThread mon_thread;	// the thread doing the monitoring

    /**
     * Select the location (host/port) to send the current request to.
     * This implementation does round-robin.
     * @return the host to send the request to.  returns null if there
     * is no host known to be up.
     */
    protected synchronized String selectLocation()
    {
	int i;

	// do a linear scan of the hosts
	for (i = round_robin; i < data.length; i++) {
	    if (data[i].up) { 
		round_robin = (i + 1) % data.length;
		return data[i].location;
	    }
	}
	for (i = 0; i < round_robin; i++) {
	    if (data[i].up) { 
		round_robin = (i + 1) % data.length;
		return data[i].location;
	    }
	}

	// no hosts are known to be up, so return null.
	return null;
    }

    /**
     * Service the request by sending a redirect.
     */
    public void service(HttpServletRequest req,
			HttpServletResponse res)
	 throws IOException
    {
	String location = selectLocation();
	if (location == null)
	    res.sendError(503, "Load balancer cannot find host.");
	else
	    res.sendRedirect(req.getScheme() + "://" + location + req.getRequestURI());
    }

    protected void startMonitorThread()
    {
	mon_thread = new MonitorThread(this);
	mon_thread.start();
    }


    public void destroy() 
    {
	round_robin = 0;
	data = null;
	mon_thread.stop();
    }

    public void init(ServletConfig config) 
	 throws ServletException
    {
	super.init(config);
	
	// get the list of hosts to load balance
	String host_list = config.getInitParameter("hosts");
	if (host_list == null) 
	    throw new ServletException("hosts parameter not set");
	StringTokenizer st = new StringTokenizer(host_list, " ");
	int tokens = st.countTokens();
	data = new HostData[tokens];
	for (int i = 0; i < tokens; i++) 
	    data[i] = new HostData(st.nextToken());

	// start up the thread that will monitor the hosts' states
	startMonitorThread();
    }

    
}
