/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.felix.httplite.server; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; import org.apache.felix.httplite.osgi.Logger; /** * This class implements a simple thread pool for servicing HTTP connections. * The thread pool does not create any threads initially, but waits for * connections to be added to create threads. As connections are added, threads * are only created if they are needed up until the thread limit. If threads * are inactive for a period of time, then the threads terminate; the default * is 60000 milliseconds. **/ public class ThreadPool { /** * Default thread timeout */ public static final int DEFAULT_THREAD_TIMEOUT = 60000; private final int m_threadTimeout; private final ThreadGroup m_group = new ThreadGroup("ThreadPoolGroup"); private int m_state; private ThreadGate m_shutdownGate; private int m_threadName = 0; private int m_threadLimit = 0; private int m_threadCount = 0; private int m_threadAvailable = 0; private final List m_connectionList = new ArrayList(); private final Logger m_logger; /** * Constructs a thread pool with the specified thread limit and with * the default inactivity timeout. * @param threadLimit The maximum number of threads in the pool. * @param logger Logger instance. **/ public ThreadPool(final int threadLimit, final Logger logger) { this(threadLimit, DEFAULT_THREAD_TIMEOUT, logger); } /** * Constructs a thread pool with the specified thread limit and inactivity * timeout. * @param threadLimit The maximum number of threads in the pool. * @param threadTimeout The inactivity timeout for threads in milliseconds. * @param logger Logger instance. **/ public ThreadPool(int threadLimit, int threadTimeout, Logger logger) { m_threadLimit = threadLimit; m_threadTimeout = threadTimeout; m_logger = logger; m_state = Server.INACTIVE_STATE; } /** * This method returns the current state of the thread pool, which is one * of the following values: * <ul> * <li><tt>ThreadPool.INACTIVE_STATE</tt> - the thread pool is currently * not active. * </li> * <li><tt>ThreadPool.ACTIVE_STATE</tt> - the thread pool is active and * servicing connections. * </li> * <li><tt>ThreadPool.STOPPING_STATE</tt> - the thread pool is in the * process of shutting down. * </li> * </ul> * @return The current state of the thread pool. **/ public synchronized int getState() { return m_state; } /** * Starts the thread pool if it is not already active, allowing it to * service connections. * @throws java.lang.IllegalStateException If the thread pool is in the * <tt>ThreadPool.STOPPING_STATE</tt> state. **/ public synchronized void start() { if (m_state != Server.STOPPING_STATE) { m_state = Server.ACTIVE_STATE; } else { throw new IllegalStateException("Thread pool is in process of stopping."); } } /** * This method stops the thread pool if it is currently active. This method * will block the calling thread until the thread pool is completely stopped. * This can potentially take a long time, since it allows all existing * connections to be processed before shutting down. Subsequent calls to * this method will also block the caller. If a blocked thread is interrupted, * the method will release the blocked thread by throwing an interrupted * exception. In such a case, the thread pool will still continue its * shutdown process. * @throws java.lang.InterruptedException If the calling thread is interrupted. **/ public void stop() throws InterruptedException { ThreadGate gate = null; synchronized (this) { if (m_state != Server.INACTIVE_STATE) { // If there is no shutdown gate, create one and save its // reference both in the field and locally. All threads // that call stop() while the server is stopping will wait // on this gate. if ((m_shutdownGate == null) && (m_threadCount > 0)) { m_shutdownGate = new ThreadGate(); } gate = m_shutdownGate; m_state = Server.STOPPING_STATE; // Interrupt all threads that have been created by the // thread pool. m_group.interrupt(); } } // Wait on gate for thread pool shutdown to complete. if (gate != null) { gate.await(); } } /** * This method adds an HTTP connection to the thread pool for servicing. * @param connection the HTTP connection. * @throws java.lang.IllegalStateException If the thread pool is not in the * <tt>ThreadPool.ACTIVE_STATE</tt> state. **/ public synchronized void addConnection(final Connection connection) { if (m_state == Server.ACTIVE_STATE) { // Add the new connection to the connection list. m_connectionList.add(connection); notify(); // If there are not enough available threads to handle all outstanding // connections and we still haven't reached our thread limit, then // add another thread. if ((m_threadAvailable < m_connectionList.size()) && (m_threadCount < m_threadLimit)) { // Increase our thread count, but not number of available threads, // since the new thread will be used to service the new connection // and thus is not available. m_threadCount++; // Use simple integer for thread name for logging purposes. if (m_threadName == Integer.MAX_VALUE) { m_threadName = 1; } else { m_threadName++; } // Create and start thread into our thread group. new Thread(m_group, new Runnable() { public void run() { processConnections(); } }, Integer.toString(m_threadName)).start(); m_logger.log(Logger.LOG_DEBUG, "Created new thread for pool; count = " + m_threadCount + ", max = " + m_threadLimit + "."); } } else { throw new IllegalStateException("The thread pool is not active."); } } /** * This method is the main loop for all threads servicing connections. **/ private void processConnections() { Connection connection; while (true) { synchronized (this) { // Any new threads entering this region are now available to // process a connection, so increment the available count. m_threadAvailable++; try { // Keep track of when we start to wait so that we // know if our timeout expires. long start = System.currentTimeMillis(); long current = start; // Wait until there is a connection to service or until // the timeout expires; if the timeout is zero, then there // is no timeout. while (m_state == Server.ACTIVE_STATE && (m_connectionList.size() == 0) && ((m_threadTimeout == 0) || ((current - start) < m_threadTimeout))) { // Try to wait for another connection, but our timeout // expires then commit suicide. wait(m_threadTimeout - (current - start)); current = System.currentTimeMillis(); } } catch (InterruptedException ex) { // This generally happens when we are shutting down. Thread.currentThread().interrupt(); } // Set connection to null if we are going to commit suicide; // otherwise get the first available connection for servicing. if (m_connectionList.size() == 0) { connection = null; } else { connection = (Connection) m_connectionList.remove(0); } // Decrement number of available threads, since we will either // start to service a connection at this point or we will commit // suicide. m_threadAvailable--; // If we do not have a connection, then we are committing // suicide due to inactivity or because we were interrupted // and are stopping the thread pool. if (connection == null) { // One less thread in use. m_threadCount--; if (Thread.interrupted()) { m_logger.log(Logger.LOG_DEBUG, "Pool thread dying due to interrupt."); } else { m_logger.log(Logger.LOG_DEBUG, "Pool thread dying due to inactivity."); } // If we are stopping and the last thread is dead, then // open the shutdown gate to release all threads waiting // for us to stop. if ((m_state == Server.STOPPING_STATE) && (m_threadCount == 0)) { m_shutdownGate.open(); m_shutdownGate = null; m_state = Server.INACTIVE_STATE; m_logger.log(Logger.LOG_DEBUG, "Server shutdown complete."); } // Return to kill the thread by exiting our run method. return; } } // Otherwise, we have a connection so process it. // Note, we might have outstanding connections to // process even if we are stopping, so we cleaning // service those remaining connections before stopping. try { connection.process(); m_logger.log(Logger.LOG_DEBUG, "Connection closed normally."); } catch (SocketTimeoutException ex) { m_logger.log(Logger.LOG_INFO, "Connection closed due to inactivity."); } catch (Exception ex) { m_logger.log(Logger.LOG_ERROR, "Connection close due to unknown reason.", ex); } } } }