/* * Tigase Jabber/XMPP Server * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. Look for COPYING file in the top folder. * If not, see http://www.gnu.org/licenses/. * * $Rev$ * Last modified by $Author$ * $Date$ */ package tigase.net; //~--- non-JDK imports -------------------------------------------------------- import tigase.annotations.TODO; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Comparator; import java.util.Set; import java.util.concurrent.CompletionService; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; //~--- classes ---------------------------------------------------------------- /** * Describe class SocketThread here. * * * Created: Mon Jan 30 12:01:17 2006 * * @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a> * @version $Rev$ */ public class SocketThread implements Runnable { private static final Logger log = Logger.getLogger(SocketThread.class.getName()); /** Field description */ public static final int DEF_MAX_THREADS_PER_CPU = 8; private static final int MAX_EMPTY_SELECTIONS = 10; private static SocketThread[] socketReadThread = null; private static SocketThread[] socketWriteThread = null; private static int cpus = Runtime.getRuntime().availableProcessors(); private static ThreadPoolExecutor executor = null; /** * Variable <code>completionService</code> keeps reference to server thread pool. * There is only one thread pool used by all server modules. Each module requiring * separate threads for tasks processing must have access to server thread pool. */ private static CompletionService<IOService<?>> completionService = null; //~--- static initializers -------------------------------------------------- //private static int threadNo = 0; //private static final int READ_ONLY = SelectionKey.OP_READ; //private static final int READ_WRITE = SelectionKey.OP_READ | SelectionKey.OP_WRITE; static { if (socketReadThread == null) { int nThreads = (cpus * DEF_MAX_THREADS_PER_CPU) / 2 + 1; executor = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); completionService = new ExecutorCompletionService<IOService<?>>(executor); socketReadThread = new SocketThread[nThreads]; socketWriteThread = new SocketThread[nThreads]; for (int i = 0; i < socketReadThread.length; i++) { socketReadThread[i] = new SocketThread("socketReadThread-" + i); socketReadThread[i].reading = true; Thread thrd = new Thread(socketReadThread[i]); thrd.setName("socketReadThread-" + i); thrd.start(); } log.log(Level.WARNING, "{0} socketReadThreads started.", socketReadThread.length); for (int i = 0; i < socketWriteThread.length; i++) { socketWriteThread[i] = new SocketThread("socketWriteThread-" + i); socketWriteThread[i].writing = true; Thread thrd = new Thread(socketWriteThread[i]); thrd.setName("socketWriteThread-" + i); thrd.start(); } log.log(Level.WARNING, "{0} socketWriteThreads started.", socketWriteThread.length); } // end of if (acceptThread == null) } //~--- fields --------------------------------------------------------------- private Selector clientsSel = null; // private boolean selecting = false; private int empty_selections = 0; private boolean reading = false; private boolean writing = false; private ConcurrentSkipListSet<IOService<?>> waiting = new ConcurrentSkipListSet<IOService<?>>(new IOServiceComparator()); private boolean stopping = false; // IOServices must be added to thread pool after they are removed from // the selector and the selector and key is cleared, otherwise we have // dead-lock somewhere down in the: // java.nio.channels.spi.AbstractSelectableChannel.removeKey(AbstractSelectableChannel.java:111) private ConcurrentSkipListSet<IOService<?>> forCompletion = new ConcurrentSkipListSet<IOService<?>>(new IOServiceComparator()); //~--- constructors --------------------------------------------------------- /** * Creates a new <code>SocketThread</code> instance. * */ private SocketThread(String name) { try { clientsSel = Selector.open(); } catch (Exception e) { log.log(Level.SEVERE, "Server I/O error, can't continue my work.", e); stopping = true; } // end of try-catch new ResultsListener("ResultsListener-" + name).start(); } //~--- methods -------------------------------------------------------------- ///** // * Method description // * // * // * @return // */ //public static SocketThread getInstance() { // return socketReadThread[0]; //} //private static final Object threadNoSync = new Object(); //private int incrementAndGet() { //int result = 0; //synchronized (threadNoSync) { // threadNo = (threadNo + 1) % socketReadThread.length; // result = threadNo; //} //return result; //} /** * Method description * * * @param s */ public static void addSocketService(IOService<?> s) { // Due to a delayed SelectionKey cancelling deregistering // nature this distribution doesn't work well, it leads to // dead-lock. Let's make sure the service is always processed // by the same thread thus the same Selector. // socketReadThread[incrementAndGet()].addSocketServicePriv(s); if (s.waitingToRead()) { socketReadThread[s.hashCode() % socketReadThread.length].addSocketServicePriv(s); } if (s.waitingToSend()) { socketWriteThread[s.hashCode() % socketWriteThread.length].addSocketServicePriv(s); } } /** * Method description * * * @param s */ public static void removeSocketService(IOService<Object> s) { Selector clientsSel = socketReadThread[s.hashCode() % socketReadThread.length].clientsSel; SelectionKey key = s.getSocketChannel().keyFor(clientsSel); if ((key != null) && (key.attachment() == s)) { key.cancel(); } // end of if (key != null) clientsSel = socketWriteThread[s.hashCode() % socketWriteThread.length].clientsSel; key = s.getSocketChannel().keyFor(clientsSel); if ((key != null) && (key.attachment() == s)) { key.cancel(); } // end of if (key != null) } /** * Method description * * * @param s */ @SuppressWarnings("unchecked") public void addSocketServicePriv(IOService<?> s) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Adding to waiting: {0}", s); } waiting.add((IOService<Object>) s); // Calling lazy wakeup to avoid multiple wakeup calls // when lots of new services are added.... clientsSel.wakeup(); // wakeupHelper.wakeup(); } /** * Describe <code>run</code> method here. * */ @SuppressWarnings({ "unchecked" }) @Override public void run() { while ( !stopping) { try { clientsSel.select(); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Selector AWAKE: {0}", clientsSel); } Set<SelectionKey> selected = clientsSel.selectedKeys(); int selectedKeys = selected.size(); if ((selectedKeys == 0) && (waiting.size() == 0)) { if (log.isLoggable(Level.FINEST)) { log.finest("Selected keys = 0!!! a bug again?"); } if ((++empty_selections) > MAX_EMPTY_SELECTIONS) { recreateSelector(); } } else { empty_selections = 0; if (selectedKeys > 0) { // This is dirty but selectNow() causes concurrent modification exception // and the selectNow() is needed because of a bug in JVM mentioned below for (SelectionKey sk : selected) { // According to most guides we should use below code // removing SelectionKey from iterator, however a little later // we do cancel() on this key so removing is somehow redundant // and causes concurrency exception if a few calls are performed // at the same time. // selected_keys.remove(sk); IOService s = (IOService) sk.attachment(); try { if (log.isLoggable(Level.FINEST)) { StringBuilder sb = new StringBuilder("AWAKEN: " + s.getUniqueId()); if (sk.isWritable()) { sb.append(", ready for WRITING"); } if (sk.isReadable()) { sb.append(", ready for READING"); } sb.append(", readyOps() = ").append(sk.readyOps()); log.finest(sb.toString()); } // Set<SelectionKey> selected_keys = clientsSel.selectedKeys(); // for (SelectionKey sk : selected_keys) { // Handling a bug or not a bug described in the // last comment to this issue: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4850373 // and // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933 sk.cancel(); forCompletion.add(s); // IOServices must be added to thread pool after they are removed from // the selector and the selector and key is cleared, otherwise we have // dead-lock somewhere down in the: // java.nio.channels.spi.AbstractSelectableChannel. // removeKey(AbstractSelectableChannel.java:111) // completionService.submit(s); } catch (CancelledKeyException e) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "CancelledKeyException, stopping the connection: {0}", s.getUniqueId()); } try { s.forceStop(); } catch (Exception ex2) { if (log.isLoggable(Level.WARNING)) { log.log(Level.WARNING, "got exception during forceStop: {0}", e); } } } } } // Clean-up cancelled keys... clientsSel.selectNow(); } addAllWaiting(); IOService serv = null; while ((serv = forCompletion.pollFirst()) != null) { completionService.submit(serv); } // clientsSel.selectNow(); } catch (CancelledKeyException brokene) { // According to Java API that should not happen. // I think it happens only on the broken Java implementation // from Apple. log.log(Level.WARNING, "Ups, broken JDK, Apple? ", brokene); try { recreateSelector(); } catch (Exception e) { log.log(Level.SEVERE, "Serious problem, can't recreate selector: ", e); // stopping = true; } } catch (IOException ioe) { // According to Java API that should not happen. // I think it happens only on the broken Java implementation from Apple // and due to a bug: http://bugs.sun.com/view_bug.do?bug_id=6693490 log.log(Level.WARNING, "Problem with the network connection: ", ioe); try { recreateSelector(); } catch (Exception e) { log.log(Level.SEVERE, "Serious problem, can't recreate selector: ", e); // stopping = true; } } catch (Exception exe) { log.log(Level.SEVERE, "Server I/O error: ", exe); try { recreateSelector(); } catch (Exception e) { log.log(Level.SEVERE, "Serious problem, can't recreate selector: ", e); // stopping = true; } // stopping = true; } } } //~--- set methods ---------------------------------------------------------- /** * Method description * * * @param threads */ public void setMaxThread(int threads) { executor.setCorePoolSize(threads); executor.setMaximumPoolSize(threads); } /** * Method description * * * @param threads */ public void setMaxThreadPerCPU(int threads) { setMaxThread(threads * cpus); } //~--- methods -------------------------------------------------------------- private void addAllWaiting() throws IOException { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "waiting.size(): {0}", waiting.size()); } IOService s = null; // boolean added = false; while ((s = waiting.pollFirst()) != null) { SocketChannel sc = s.getSocketChannel(); try { if (sc.isConnected()) { if (reading) { sc.register(clientsSel, SelectionKey.OP_READ, s); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "ADDED OP_READ: {0}", s.getUniqueId()); } } if (writing) { sc.register(clientsSel, SelectionKey.OP_WRITE, s); if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "ADDED OP_WRITE: {0}", s.getUniqueId()); } } // added = true; } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Socket not connected: {0}", s.getUniqueId()); } try { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Forcing stopping the service: {0}", s.getUniqueId()); } s.forceStop(); } catch (Exception e) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Exception while stopping service: " + s.getUniqueId(), e); } } } } catch (Exception e) { if (log.isLoggable(Level.FINER)) { log.log(Level.FINER, "Forcing stopping the service: " + s.getUniqueId(), e); } try { s.forceStop(); } catch (Exception ez) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Exception while stopping service: " + s.getUniqueId(), ez); } } } // end of try-catch } // end of for () // if (added) { // clientsSel.wakeup(); // } } // Implementation of java.lang.Runnable private synchronized void recreateSelector() throws IOException { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "Recreating selector, opened channels: {0}", clientsSel.keys().size()); } empty_selections = 0; // Handling a bug or not a bug described in the // last comment to this issue: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4850373 // and // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933 // Recreating the selector and registering all channles with // the new selector // Selector tempSel = clientsSel; // clientsSel = Selector.open(); boolean cancelled = false; // Sometimes this is just a broken connection which causes // selector spin... this is the cheaper solution.... for (SelectionKey sk : clientsSel.keys()) { IOService<?> serv = (IOService<?>) sk.attachment(); SocketChannel sc = serv.getSocketChannel(); if ((sc == null) ||!sc.isConnected()) { cancelled = true; sk.cancel(); try { log.log(Level.INFO, "Forcing stopping the service: {0}", serv.getUniqueId()); serv.forceStop(); } catch (Exception e) {} } // waiting.offer(serv); } if (cancelled) { clientsSel.selectNow(); } else { // Unfortunately must be something wrong with the selector // itself, now more expensive calculations... Selector tempSel = clientsSel; clientsSel = Selector.open(); for (SelectionKey sk : tempSel.keys()) { IOService<?> serv = (IOService<?>) sk.attachment(); sk.cancel(); waiting.add(serv); } tempSel.close(); } } //~--- inner classes -------------------------------------------------------- private class IOServiceComparator implements Comparator<IOService<?>> { /** * Method description * * * @param o1 * @param o2 * * @return */ @Override public int compare(IOService<?> o1, IOService<?> o2) { return o1.getUniqueId().compareTo(o2.getUniqueId()); } } @TODO(note = "ExecutionException is poorly implemented.") protected class ResultsListener extends Thread { /** * Constructs ... * * * @param name */ public ResultsListener(String name) { super(); setName(name); } //~--- methods ------------------------------------------------------------ /** * Method description * */ @Override public void run() { for (;;) { try { IOService<?> service = completionService.take().get(); if (service != null) { if (service.isConnected()) { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "COMPLETED: {0}", service.getUniqueId()); } addSocketService(service); } else { if (log.isLoggable(Level.FINEST)) { log.log(Level.FINEST, "REMOVED: {0}", service.getUniqueId()); } } // end of else } } catch (ExecutionException e) { log.log(Level.WARNING, "Protocol execution exception.", e); // TODO: Do something with this } // end of catch catch (InterruptedException e) { log.log(Level.WARNING, "Protocol execution interrupted.", e); } // end of try-catch catch (Exception e) { log.log(Level.WARNING, "Protocol execution unknown exception.", e); } // end of catch } // end of for () } } } // SocketThread //~ Formatted in Sun Code Convention //~ Formatted by Jindent --- http://www.jindent.com