/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.jersey.jdk.connector; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousChannelGroup; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Queue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; /** * Writes and reads data to and from a socket. Only one {@link #write(java.nio.ByteBuffer, * org.glassfish.jersey.jdk.connector.CompletionHandler)} * method call can be processed at a time. Only one {@link #_read(java.nio.ByteBuffer)} operation is supported at a time, * another one is started only after the previous one has completed. Blocking in {@link #onRead(Object)} * or {@link #onConnect()} method will result in data not being read from a socket until these methods have completed. * * @author Petr Janouch (petr.janouch at oracle.com) */ class TransportFilter extends Filter<ByteBuffer, ByteBuffer, Void, ByteBuffer> { private static final Logger LOGGER = Logger.getLogger(TransportFilter.class.getName()); private static final AtomicInteger openedConnections = new AtomicInteger(0); private static final ScheduledExecutorService connectionCloseScheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread thread = new Thread(r); thread.setName("jdk-connector-container-idle-timeout"); thread.setDaemon(true); return thread; }); private static volatile AsynchronousChannelGroup channelGroup; private static volatile ScheduledFuture<?> closeWaitTask; /** * {@link ThreadPoolConfig} current {@link #channelGroup} has been created with. */ private static volatile ThreadPoolConfig currentThreadPoolConfig; /** * Idle timeout that will be used when closing current {@link #channelGroup} */ private static volatile Integer currentContainerIdleTimeout; private final int inputBufferSize; private final ThreadPoolConfig threadPoolConfig; private final int containerIdleTimeout; private volatile AsynchronousSocketChannel socketChannel; /** * Constructor. * <p/> * If the channel group is not active (all connections have been closed and the shutdown timeout is running) and a new * transport is created with tread pool configuration different from the one of the current thread pool, the current * thread pool will be shut down and a new one created with the new configuration. * * @param inputBufferSize size of buffer to be allocated for reading data from a socket. * @param threadPoolConfig thread pool configuration used for creating thread pool. * @param containerIdleTimeout idle time after which the shared thread pool will be destroyed. */ TransportFilter(int inputBufferSize, ThreadPoolConfig threadPoolConfig, int containerIdleTimeout) { super(null); this.inputBufferSize = inputBufferSize; this.threadPoolConfig = threadPoolConfig; this.containerIdleTimeout = containerIdleTimeout; } @Override void write(ByteBuffer data, final org.glassfish.jersey.jdk.connector.CompletionHandler<ByteBuffer> completionHandler) { socketChannel.isOpen(); socketChannel.write(data, data, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer buffer) { if (buffer.hasRemaining()) { write(buffer, completionHandler); return; } completionHandler.completed(buffer); } @Override public void failed(Throwable exc, ByteBuffer buffer) { completionHandler.failed(exc); } }); } @Override void close() { if (socketChannel == null || !socketChannel.isOpen()) { return; } try { socketChannel.close(); } catch (IOException e) { LOGGER.log(Level.INFO, LocalizationMessages.TRANSPORT_CONNECTION_NOT_CLOSED(), e); } synchronized (TransportFilter.class) { openedConnections.decrementAndGet(); if (openedConnections.get() == 0 && channelGroup != null) { scheduleClose(); } } } @Override void startSsl() { onSslHandshakeCompleted(); } @Override public void handleConnect(SocketAddress serverAddress, Filter upstreamFilter) { this.upstreamFilter = upstreamFilter; try { synchronized (TransportFilter.class) { updateThreadPoolConfig(); initializeChannelGroup(); socketChannel = AsynchronousSocketChannel.open(channelGroup); openedConnections.incrementAndGet(); } } catch (IOException e) { onError(e); return; } socketChannel.connect(serverAddress, null, new CompletionHandler<Void, Void>() { @Override public void completed(Void result, Void nothing) { final ByteBuffer inputBuffer = ByteBuffer.allocate(inputBufferSize); onConnect(); _read(inputBuffer); } @Override public void failed(Throwable exc, Void nothing) { onError(exc); try { socketChannel.close(); } catch (IOException e) { LOGGER.log(Level.FINE, LocalizationMessages.TRANSPORT_CONNECTION_NOT_CLOSED(), exc.getMessage()); } } }); } private void updateThreadPoolConfig() { // the channel group is active, no change in configuration if (openedConnections.get() != 0) { return; } // check if the new configuration is different from the one of the current container if (!threadPoolConfig.equals(currentThreadPoolConfig) || containerIdleTimeout != currentContainerIdleTimeout) { currentThreadPoolConfig = threadPoolConfig; currentContainerIdleTimeout = containerIdleTimeout; if (channelGroup == null) { // the channel group has not been initialized (this is a first client) - no need to shut it down return; } closeWaitTask.cancel(true); closeWaitTask = null; channelGroup.shutdown(); channelGroup = null; } } private void initializeChannelGroup() throws IOException { if (closeWaitTask != null) { closeWaitTask.cancel(true); closeWaitTask = null; } if (channelGroup == null) { ThreadFactory threadFactory = threadPoolConfig.getThreadFactory(); if (threadFactory == null) { threadFactory = new TransportThreadFactory(threadPoolConfig); } ExecutorService executor; if (threadPoolConfig.getQueue() != null) { executor = new QueuingExecutor(threadPoolConfig.getCorePoolSize(), threadPoolConfig.getMaxPoolSize(), threadPoolConfig.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, threadPoolConfig.getQueue(), false, threadFactory); } else { int taskQueueLimit = threadPoolConfig.getQueueLimit(); if (taskQueueLimit == -1) { taskQueueLimit = Integer.MAX_VALUE; } executor = new QueuingExecutor(threadPoolConfig.getCorePoolSize(), threadPoolConfig.getMaxPoolSize(), threadPoolConfig.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(taskQueueLimit), true, threadFactory); } // Thread pool is owned by the channel group and will be shut down when channel group is shut down channelGroup = AsynchronousChannelGroup.withCachedThreadPool(executor, threadPoolConfig.getCorePoolSize()); } } private void _read(final ByteBuffer inputBuffer) { /** * It must be checked that the channel has not been closed by {@link #close()} method. */ if (!socketChannel.isOpen()) { return; } socketChannel.read(inputBuffer, null, new CompletionHandler<Integer, Void>() { @Override public void completed(Integer bytesRead, Void result) { // connection closed by the server if (bytesRead == -1) { // close will set TransportFilter.this.upstreamFilter to null Filter upstreamFilter = TransportFilter.this.upstreamFilter; close(); upstreamFilter.onConnectionClosed(); return; } inputBuffer.flip(); onRead(inputBuffer); inputBuffer.compact(); _read(inputBuffer); } @Override public void failed(Throwable exc, Void result) { /** * Reading from the channel will fail if it is closing. In such cases {@link java.nio.channels * .AsynchronousCloseException} * is thrown. This should not be logged and no action undertaken. */ if (exc instanceof AsynchronousCloseException) { return; } onError(exc); } }); } private void scheduleClose() { closeWaitTask = connectionCloseScheduler.schedule(() -> { synchronized (TransportFilter.class) { if (closeWaitTask == null) { return; } channelGroup.shutdown(); channelGroup = null; closeWaitTask = null; } }, currentContainerIdleTimeout, TimeUnit.MILLISECONDS); } /** * A default thread factory that gets used if {@link ThreadPoolConfig#getThreadFactory()} * is not specified. */ private static class TransportThreadFactory implements ThreadFactory { private static final String THREAD_NAME_BASE = " jdk-connector-"; private static final AtomicInteger threadCounter = new AtomicInteger(0); private final ThreadPoolConfig threadPoolConfig; TransportThreadFactory(ThreadPoolConfig threadPoolConfig) { this.threadPoolConfig = threadPoolConfig; } @Override public Thread newThread(Runnable r) { final Thread thread = new Thread(r); thread.setName(THREAD_NAME_BASE + threadCounter.incrementAndGet()); thread.setPriority(threadPoolConfig.getPriority()); thread.setDaemon(threadPoolConfig.isDaemon()); try { AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { if (threadPoolConfig.getInitialClassLoader() == null) { thread.setContextClassLoader(this.getClass().getClassLoader()); } else { thread.setContextClassLoader(threadPoolConfig.getInitialClassLoader()); } return null; } }); } catch (Throwable t) { // just log - client still can work without setting context class loader LOGGER.log(Level.WARNING, LocalizationMessages.TRANSPORT_SET_CLASS_LOADER_FAILED(), t); } return thread; } } /** * A thread pool executor that prefers creating new worker threads over queueing tasks until the maximum poll size * has been reached, after which it will start queueing tasks. */ private static class QueuingExecutor extends ThreadPoolExecutor { private final Queue<Runnable> taskQueue; private final boolean threadSafeQueue; /** * Constructor. * * @param threadSafeQueue indicates if {@link #taskQueue} is thread safe or not. */ QueuingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, Queue<Runnable> taskQueue, boolean threadSafeQueue, ThreadFactory threadFactory) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, new HandOffQueue(taskQueue, threadSafeQueue), threadFactory); this.taskQueue = taskQueue; this.threadSafeQueue = threadSafeQueue; } /** * Submit a task for execution, if the maximum thread limit has been reached and all the threads are occupied, * enqueue the task. The task is not executed by the current thread, but by a thread from the thread pool. * * @param task to be executed. */ @Override public void execute(Runnable task) { try { super.execute(task); } catch (RejectedExecutionException e) { /* execution has been rejected either because the executor has been shut down or all worker threads are * busy - check the former one */ if (isShutdown()) { throw new RejectedExecutionException(LocalizationMessages.TRANSPORT_EXECUTOR_CLOSED(), e); } /* All threads are occupied, try enqueuing the task. * Each worker thread checks the queue after it has finished executing its task. */ if (threadSafeQueue) { if (!taskQueue.offer(task)) { throw new RejectedExecutionException(LocalizationMessages.TRANSPORT_EXECUTOR_QUEUE_LIMIT_REACHED(), e); } } else { synchronized (taskQueue) { if (!taskQueue.offer(task)) { throw new RejectedExecutionException(LocalizationMessages.TRANSPORT_EXECUTOR_QUEUE_LIMIT_REACHED(), e); } } } /** * There is a small time interval between a worker thread checks {@link #taskQueue} and it starts to block * waiting for a new tasks to be submitted (Ideally checking that the {@link #taskQueue} is empty and starting to * block at the task hand off queue would be atomic). This can be detected by the situation when a thread * submitting * a new tasks has been rejected, but not all worker threads are active (However this does not indicate * exclusively * the problematic situation). */ if (getActiveCount() < getMaximumPoolSize()) { /* * There is no guarantee that the same tasks that has been enqueued above will be dequeued, * but trying to execute one arbitrary task by everyone in this situation is enough to clear the queue. */ Runnable dequeuedTask; if (threadSafeQueue) { dequeuedTask = taskQueue.poll(); } else { synchronized (taskQueue) { dequeuedTask = taskQueue.poll(); } } // check if the task has not been consumed by a worker thread after all if (dequeuedTask != null) { execute(dequeuedTask); } } } } /** * Synchronous queue that tries to empty {@link #taskQueue} before it blocks waiting for new tasks to be submitted. * It is passed to {@link ThreadPoolExecutor}, where it is used used to hand off tasks from task-submitting * thread to worker threads. */ private static class HandOffQueue extends SynchronousQueue<Runnable> { private static final long serialVersionUID = -1607064661828834847L; private final Queue<Runnable> taskQueue; private final boolean threadSafeQueue; private HandOffQueue(Queue<Runnable> taskQueue, boolean threadSafeQueue) { this.taskQueue = taskQueue; this.threadSafeQueue = threadSafeQueue; } @Override public Runnable take() throws InterruptedException { // try to empty the task queue Runnable task; if (threadSafeQueue) { task = taskQueue.poll(); } else { synchronized (taskQueue) { task = taskQueue.poll(); } } if (task != null) { return task; } // block and wait for a task to be submitted return super.take(); } @Override public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { // try to empty the task queue Runnable task; if (threadSafeQueue) { task = taskQueue.poll(); } else { synchronized (taskQueue) { task = taskQueue.poll(); } } if (task != null) { return task; } // block and wait for a task to be submitted return super.poll(timeout, unit); } } } }