public LifeCycleFilter() { totalConnectionNumber = new AtomicInteger(); activeConnectionsMap = DataStructures.<Connection, Integer>getConcurrentMap(); }
/** * This class implements a file caching mechanism used to cache static resources. * * @author Jeanfrancois Arcand * @author Scott Oaks */ public class FileCache implements MonitoringAware<FileCacheProbe> { private static final File TMP_DIR = new File(System.getProperty("java.io.tmpdir")); static final String[] COMPRESSION_ALIASES = {"gzip"}; public enum CacheType { HEAP, MAPPED, FILE, TIMESTAMP } public enum CacheResult { OK_CACHED, OK_CACHED_TIMESTAMP, FAILED_CACHE_FULL, FAILED_ENTRY_EXISTS, FAILED } private static final Logger LOGGER = Grizzly.logger(FileCache.class); /** Cache size. */ private final AtomicInteger cacheSize = new AtomicInteger(); /** A {@link ByteBuffer} cache of static pages. */ private final ConcurrentMap<FileCacheKey, FileCacheEntry> fileCacheMap = DataStructures.<FileCacheKey, FileCacheEntry>getConcurrentMap(); private final FileCacheEntry NULL_CACHE_ENTRY = new FileCacheEntry(this); /** Specifies the maximum time in seconds a resource may be cached. */ private int secondsMaxAge = -1; /** The maximum entries in the {@link FileCache} */ private volatile int maxCacheEntries = 1024; /** The maximum size of a cached resource. */ private long minEntrySize = Long.MIN_VALUE; /** The maximum size of a cached resource. */ private long maxEntrySize = Long.MAX_VALUE; /** The maximum memory mapped bytes. */ private volatile long maxLargeFileCacheSize = Long.MAX_VALUE; /** The maximum cached bytes */ private volatile long maxSmallFileCacheSize = 1048576; /** The current cache size in bytes */ private AtomicLong mappedMemorySize = new AtomicLong(); /** The current cache size in bytes */ private AtomicLong heapSize = new AtomicLong(); /** Is the file cache enabled. */ private boolean enabled = true; private DelayedExecutor.DelayQueue<FileCacheEntry> delayQueue; /** Folder to store compressed cached files */ private volatile File compressedFilesFolder = TMP_DIR; /** Compression configuration, used to decide if cached resource has to be compressed or not */ private final CompressionConfig compressionConfig = new CompressionConfig(); /** <tt>true</tt>, if zero-copy file-send feature could be used, or <tt>false</tt> otherwise. */ private boolean fileSendEnabled; /** File cache probes */ protected final DefaultMonitoringConfig<FileCacheProbe> monitoringConfig = new DefaultMonitoringConfig<FileCacheProbe>(FileCacheProbe.class) { @Override public Object createManagementObject() { return createJmxManagementObject(); } }; // ---------------------------------------------------- Methods ----------// public void initialize(final DelayedExecutor delayedExecutor) { delayQueue = delayedExecutor.createDelayQueue(new EntryWorker(), new EntryResolver()); } /** * Add a resource to the cache. Unlike the {@link * #add(org.glassfish.grizzly.http.HttpRequestPacket, java.io.File)} this method adds a resource * to a cache but is not able to send the resource content to a client if client doesn't have the * latest version of this resource. */ public CacheResult add(final HttpRequestPacket request, final long lastModified) { return add(request, null, lastModified); } /** * Add a {@link File} resource to the cache. If a client comes with not the latest version of this * resource - the {@link FileCache} will return it the latest resource version. */ public CacheResult add(final HttpRequestPacket request, final File cacheFile) { return add(request, cacheFile, cacheFile.lastModified()); } /** Add a resource to the cache. */ protected CacheResult add( final HttpRequestPacket request, final File cacheFile, final long lastModified) { final String requestURI = request.getRequestURI(); if (requestURI == null) { return CacheResult.FAILED; } final String host = request.getHeader(Header.Host); final FileCacheKey key = new FileCacheKey(host, requestURI); if (fileCacheMap.putIfAbsent(key, NULL_CACHE_ENTRY) != null) { key.recycle(); return CacheResult.FAILED_ENTRY_EXISTS; } final int size = cacheSize.incrementAndGet(); // cache is full. if (size > getMaxCacheEntries()) { cacheSize.decrementAndGet(); fileCacheMap.remove(key); key.recycle(); return CacheResult.FAILED_CACHE_FULL; } final HttpResponsePacket response = request.getResponse(); final MimeHeaders headers = response.getHeaders(); final String contentType = response.getContentType(); final FileCacheEntry entry; if (cacheFile != null) { // If we have a file - try to create File-aware cache resource entry = createEntry(cacheFile); entry.setCanBeCompressed(canBeCompressed(cacheFile, contentType)); } else { entry = new FileCacheEntry(this); entry.type = CacheType.TIMESTAMP; } entry.key = key; entry.requestURI = requestURI; entry.lastModified = lastModified; entry.contentType = ContentType.newContentType(contentType); entry.xPoweredBy = headers.getHeader(Header.XPoweredBy); entry.date = headers.getHeader(Header.Date); entry.lastModifiedHeader = headers.getHeader(Header.LastModified); entry.host = host; entry.Etag = headers.getHeader(Header.ETag); entry.server = headers.getHeader(Header.Server); fileCacheMap.put(key, entry); notifyProbesEntryAdded(this, entry); final int secondsMaxAgeLocal = getSecondsMaxAge(); if (secondsMaxAgeLocal > 0) { delayQueue.add(entry, secondsMaxAgeLocal, TimeUnit.SECONDS); } return ((entry.type == CacheType.TIMESTAMP) ? CacheResult.OK_CACHED_TIMESTAMP : CacheResult.OK_CACHED); } /** * Returns {@link FileCacheEntry}. If {@link FileCacheEntry} has been found - this method also * sets correspondent {@link HttpResponsePacket} status code and reason phrase. */ public FileCacheEntry get(final HttpRequestPacket request) { // It should be faster than calculating the key hash code if (cacheSize.get() == 0) return null; final LazyFileCacheKey key = LazyFileCacheKey.create(request); final FileCacheEntry entry = fileCacheMap.get(key); key.recycle(); try { if (entry != null && entry != NULL_CACHE_ENTRY) { // determine if we need to send the cache entry bytes // to the user-agent final HttpStatus httpStatus = checkIfHeaders(entry, request); final boolean flushBody = (httpStatus == null); if (flushBody && entry.type == CacheType.TIMESTAMP) { return null; // this will cause control to be passed to the static handler } request.getResponse().setStatus(httpStatus != null ? httpStatus : HttpStatus.OK_200); notifyProbesEntryHit(this, entry); return entry; } notifyProbesEntryMissed(this, request); } catch (Exception e) { notifyProbesError(this, e); // If an unexpected exception occurs, try to serve the page // as if it wasn't in a cache. LOGGER.log( Level.WARNING, LogMessages.WARNING_GRIZZLY_HTTP_SERVER_FILECACHE_GENERAL_ERROR(), e); } return null; } protected void remove(final FileCacheEntry entry) { if (fileCacheMap.remove(entry.key) != null) { cacheSize.decrementAndGet(); } if (entry.type == FileCache.CacheType.MAPPED) { subMappedMemorySize(entry.bb.remaining()); } else if (entry.type == FileCache.CacheType.HEAP) { subHeapSize(entry.bb.remaining()); } notifyProbesEntryRemoved(this, entry); } protected Object createJmxManagementObject() { return MonitoringUtils.loadJmxObject( "org.glassfish.grizzly.http.server.filecache.jmx.FileCache", this, FileCache.class); } /** Creates {@link FileCacheEntry}. */ private FileCacheEntry createEntry(final File file) { FileCacheEntry entry = tryMapFileToBuffer(file); if (entry == null) { entry = new FileCacheEntry(this); entry.type = CacheType.FILE; } entry.plainFile = file; entry.plainFileSize = file.length(); return entry; } /** * Map the file to a {@link ByteBuffer} * * @return the preinitialized {@link FileCacheEntry} */ private FileCacheEntry tryMapFileToBuffer(final File file) { final long size = file.length(); if (size > getMaxEntrySize()) { return null; } final CacheType type; final ByteBuffer bb; FileChannel fileChannel = null; FileInputStream stream = null; try { if (size > getMinEntrySize()) { if (addMappedMemorySize(size) > getMaxLargeFileCacheSize()) { // Cache full subMappedMemorySize(size); return null; } type = CacheType.MAPPED; } else { if (addHeapSize(size) > getMaxSmallFileCacheSize()) { // Cache full subHeapSize(size); return null; } type = CacheType.HEAP; } stream = new FileInputStream(file); fileChannel = stream.getChannel(); bb = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size); if (type == CacheType.HEAP) { ((MappedByteBuffer) bb).load(); } } catch (Exception e) { notifyProbesError(this, e); return null; } finally { if (stream != null) { try { stream.close(); } catch (IOException ignored) { notifyProbesError(this, ignored); } } if (fileChannel != null) { try { fileChannel.close(); } catch (IOException ignored) { notifyProbesError(this, ignored); } } } final FileCacheEntry entry = new FileCacheEntry(this); entry.type = type; entry.plainFileSize = size; entry.bb = bb; return entry; } /** Checks if the {@link File} with the given content-type could be compressed. */ private boolean canBeCompressed(final File cacheFile, final String contentType) { switch (compressionConfig.getCompressionMode()) { case FORCE: return true; case OFF: return false; case ON: { if (cacheFile.length() < compressionConfig.getCompressionMinSize()) { return false; } return compressionConfig.checkMimeType(contentType); } default: throw new IllegalStateException("Unknown mode"); } } // ------------------------------------------------ Configuration Properties /** @return the maximum time, in seconds, a file may be cached. */ public int getSecondsMaxAge() { return secondsMaxAge; } /** * Sets the maximum time, in seconds, a file may be cached. * * @param secondsMaxAge max age of a cached file, in seconds. */ public void setSecondsMaxAge(int secondsMaxAge) { this.secondsMaxAge = secondsMaxAge; } /** @return the maximum number of files that may be cached. */ public int getMaxCacheEntries() { return maxCacheEntries; } /** * Sets the maximum number of files that may be cached. * * @param maxCacheEntries the maximum number of files that may be cached. */ public void setMaxCacheEntries(int maxCacheEntries) { this.maxCacheEntries = maxCacheEntries; } /** @return the minimum size, in bytes, a file must be in order to be cached in the heap cache. */ public long getMinEntrySize() { return minEntrySize; } /** * The maximum size, in bytes, a file must be in order to be cached in the heap cache. * * @param minEntrySize the maximum size, in bytes, a file must be in order to be cached in the * heap cache. */ public void setMinEntrySize(long minEntrySize) { this.minEntrySize = minEntrySize; } /** * @return the maximum size, in bytes, a resource may be before it can no longer be considered * cacheable. */ public long getMaxEntrySize() { return maxEntrySize; } /** * The maximum size, in bytes, a resource may be before it can no longer be considered cacheable. * * @param maxEntrySize the maximum size, in bytes, a resource may be before it can no longer be * considered cacheable. */ public void setMaxEntrySize(long maxEntrySize) { this.maxEntrySize = maxEntrySize; } /** @return the maximum size of the memory mapped cache for large files. */ public long getMaxLargeFileCacheSize() { return maxLargeFileCacheSize; } /** * Sets the maximum size, in bytes, of the memory mapped cache for large files. * * @param maxLargeFileCacheSize the maximum size, in bytes, of the memory mapped cache for large * files. */ public void setMaxLargeFileCacheSize(long maxLargeFileCacheSize) { this.maxLargeFileCacheSize = maxLargeFileCacheSize; } /** * @return the maximum size, in bytes, of the heap cache for files below the water mark set by * {@link #getMinEntrySize()}. */ public long getMaxSmallFileCacheSize() { return maxSmallFileCacheSize; } /** * The maximum size, in bytes, of the heap cache for files below the water mark set by {@link * #getMinEntrySize()}. * * @param maxSmallFileCacheSize the maximum size, in bytes, of the heap cache for files below the * water mark set by {@link #getMinEntrySize()}. */ public void setMaxSmallFileCacheSize(long maxSmallFileCacheSize) { this.maxSmallFileCacheSize = maxSmallFileCacheSize; } /** @return <code>true</code> if the {@link FileCache} is enabled, otherwise <code>false</code> */ public boolean isEnabled() { return enabled; } /** * Enables/disables the {@link FileCache}. By default, the {@link FileCache} is disabled. * * @param enabled <code>true</code> to enable the {@link FileCache}. */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** Returns the <tt>FileCache</tt> compression configuration settings. */ public CompressionConfig getCompressionConfig() { return compressionConfig; } /** Returns the folder to be used to store temporary compressed files. */ public File getCompressedFilesFolder() { return compressedFilesFolder; } /** Sets the folder to be used to store temporary compressed files. */ public void setCompressedFilesFolder(final File compressedFilesFolder) { this.compressedFilesFolder = compressedFilesFolder != null ? compressedFilesFolder : TMP_DIR; } /** * Returns <code>true</code> if File resources may be be sent using {@link * java.nio.channels.FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)}. * * <p> * * <p>By default, this property will be true, except in the following cases: * * <p> * * <ul> * <li>JVM OS is HP-UX * <li>JVM OS is Linux, and the Oracle JVM in use is 1.6.0_17 or older * </ul> * * <p> * * <p> * * <p>Finally, if the connection between endpoints is secure, send file functionality will be * disabled regardless of configuration. * * @return <code>true</code> if resources will be sent using {@link * java.nio.channels.FileChannel#transferTo(long, long, * java.nio.channels.WritableByteChannel)}. * @since 2.3.5 */ public boolean isFileSendEnabled() { return fileSendEnabled; } /** * Configure whether or send-file support will enabled which allows sending {@link java.io.File} * resources via {@link java.nio.channels.FileChannel#transferTo(long, long, * java.nio.channels.WritableByteChannel)}. If disabled, the more traditional byte[] copy will be * used to send content. * * @param fileSendEnabled <code>true</code> to enable {@link * java.nio.channels.FileChannel#transferTo(long, long, * java.nio.channels.WritableByteChannel)} support. * @since 2.3.5 */ public void setFileSendEnabled(boolean fileSendEnabled) { this.fileSendEnabled = fileSendEnabled; } /** Creates a temporary compressed representation of the given cache entry. */ protected void compressFile(final FileCacheEntry entry) { try { final File tmpCompressedFile = File.createTempFile( String.valueOf(entry.plainFile.hashCode()), ".tmpzip", compressedFilesFolder); tmpCompressedFile.deleteOnExit(); InputStream in = null; OutputStream out = null; try { in = new FileInputStream(entry.plainFile); out = new GZIPOutputStream(new FileOutputStream(tmpCompressedFile)); final byte[] tmp = new byte[1024]; do { final int readNow = in.read(tmp); if (readNow == -1) { break; } out.write(tmp, 0, readNow); } while (true); } finally { if (in != null) { try { in.close(); } catch (IOException ignored) { } } if (out != null) { try { out.close(); } catch (IOException ignored) { } } } final long size = tmpCompressedFile.length(); switch (entry.type) { case HEAP: case MAPPED: { final FileInputStream cFis = new FileInputStream(tmpCompressedFile); try { final FileChannel cFileChannel = cFis.getChannel(); final MappedByteBuffer compressedBb = cFileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size); if (entry.type == CacheType.HEAP) { compressedBb.load(); } entry.compressedBb = compressedBb; } finally { cFis.close(); } break; } case FILE: { break; } default: throw new IllegalStateException("The type is not supported: " + entry.type); } entry.compressedFileSize = size; entry.compressedFile = tmpCompressedFile; } catch (IOException e) { LOGGER.log(Level.FINE, "Can not compress file: " + entry.plainFile, e); } } // ---------------------------------------------------- Monitoring --------// protected final long addHeapSize(long size) { return heapSize.addAndGet(size); } protected final long subHeapSize(long size) { return heapSize.addAndGet(-size); } /** * Return the heap space used for cache * * @return heap size */ public long getHeapCacheSize() { return heapSize.get(); } protected final long addMappedMemorySize(long size) { return mappedMemorySize.addAndGet(size); } protected final long subMappedMemorySize(long size) { return mappedMemorySize.addAndGet(-size); } /** * Return the size of Mapped memory used for caching * * @return Mapped memory size */ public long getMappedCacheSize() { return mappedMemorySize.get(); } /** * Check if the conditions specified in the optional If headers are satisfied. * * @return {@link HttpStatus} if the decision has been made and the response status has been * defined, or <tt>null</tt> otherwise */ private HttpStatus checkIfHeaders(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException { HttpStatus httpStatus = checkIfMatch(entry, request); if (httpStatus == null) { httpStatus = checkIfModifiedSince(entry, request); if (httpStatus == null) { httpStatus = checkIfNoneMatch(entry, request); if (httpStatus == null) { httpStatus = checkIfUnmodifiedSince(entry, request); } } } return httpStatus; } /** * Check if the if-modified-since condition is satisfied. * * @return {@link HttpStatus} if the decision has been made and the response status has been * defined, or <tt>null</tt> otherwise */ private HttpStatus checkIfModifiedSince( final FileCacheEntry entry, final HttpRequestPacket request) throws IOException { try { final String reqModified = request.getHeader(Header.IfModifiedSince); if (reqModified != null) { // optimization - assume the String value sent in the // client's If-Modified-Since header is the same as what // was originally sent if (reqModified.equals(entry.lastModifiedHeader)) { return HttpStatus.NOT_MODIFIED_304; } long headerValue = convertToLong(reqModified); if (headerValue != -1) { long lastModified = entry.lastModified; // If an If-None-Match header has been specified, // If-Modified-Since is ignored. if ((request.getHeader(Header.IfNoneMatch) == null) && (lastModified - headerValue <= 1000)) { // The entity has not been modified since the date // specified by the client. This is not an error case. return HttpStatus.NOT_MODIFIED_304; } } } } catch (IllegalArgumentException illegalArgument) { notifyProbesError(this, illegalArgument); } return null; } /** * Check if the if-none-match condition is satisfied. * * @return {@link HttpStatus} if the decision has been made and the response status has been * defined, or <tt>null</tt> otherwise */ private HttpStatus checkIfNoneMatch(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException { String headerValue = request.getHeader(Header.IfNoneMatch); if (headerValue != null) { String eTag = entry.Etag; boolean conditionSatisfied = false; if (!headerValue.equals("*")) { StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ","); while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) { String currentToken = commaTokenizer.nextToken(); if (currentToken.trim().equals(eTag)) { conditionSatisfied = true; } } } else { conditionSatisfied = true; } if (conditionSatisfied) { // For GET and HEAD, we should respond with // 304 Not Modified. // For every other method, 412 Precondition Failed is sent // back. final Method method = request.getMethod(); if (Method.GET.equals(method) || Method.HEAD.equals(method)) { return HttpStatus.NOT_MODIFIED_304; } else { return HttpStatus.PRECONDITION_FAILED_412; } } } return null; } /** * Check if the if-unmodified-since condition is satisfied. * * @return {@link HttpStatus} if the decision has been made and the response status has been * defined, or <tt>null</tt> otherwise */ private HttpStatus checkIfUnmodifiedSince( final FileCacheEntry entry, final HttpRequestPacket request) throws IOException { try { long lastModified = entry.lastModified; String h = request.getHeader(Header.IfUnmodifiedSince); if (h != null) { // optimization - assume the String value sent in the // client's If-Unmodified-Since header is the same as what // was originally sent if (h.equals(entry.lastModifiedHeader)) { // The entity has not been modified since the date // specified by the client. This is not an error case. return HttpStatus.PRECONDITION_FAILED_412; } long headerValue = convertToLong(h); if (headerValue != -1) { if (headerValue - lastModified <= 1000) { // The entity has not been modified since the date // specified by the client. This is not an error case. return HttpStatus.PRECONDITION_FAILED_412; } } } } catch (IllegalArgumentException illegalArgument) { notifyProbesError(this, illegalArgument); } return null; } /** * Check if the if-match condition is satisfied. * * @param request The servlet request we are processing * @param entry the FileCacheEntry to validate * @return {@link HttpStatus} if the decision has been made and the response status has been * defined, or <tt>null</tt> otherwise */ private HttpStatus checkIfMatch(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException { String headerValue = request.getHeader(Header.IfMatch); if (headerValue != null) { if (headerValue.indexOf('*') == -1) { String eTag = entry.Etag; StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ","); boolean conditionSatisfied = false; while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) { String currentToken = commaTokenizer.nextToken(); if (currentToken.trim().equals(eTag)) { conditionSatisfied = true; } } // If none of the given ETags match, 412 Precondition failed is // sent back if (!conditionSatisfied) { return HttpStatus.PRECONDITION_FAILED_412; } } } return null; } /** {@inheritDoc} */ @Override public MonitoringConfig<FileCacheProbe> getMonitoringConfig() { return monitoringConfig; } /** * Notify registered {@link FileCacheProbe}s about the "entry added" event. * * @param fileCache the <tt>FileCache</tt> event occurred on. * @param entry entry been added */ protected static void notifyProbesEntryAdded( final FileCache fileCache, final FileCacheEntry entry) { final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (FileCacheProbe probe : probes) { probe.onEntryAddedEvent(fileCache, entry); } } } /** * Notify registered {@link FileCacheProbe}s about the "entry removed" event. * * @param fileCache the <tt>FileCache</tt> event occurred on. * @param entry entry been removed */ protected static void notifyProbesEntryRemoved( final FileCache fileCache, final FileCacheEntry entry) { final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (FileCacheProbe probe : probes) { probe.onEntryRemovedEvent(fileCache, entry); } } } /** * Notify registered {@link FileCacheProbe}s about the "entry hit event. * * @param fileCache the <tt>FileCache</tt> event occurred on. * @param entry entry been hit. */ protected static void notifyProbesEntryHit( final FileCache fileCache, final FileCacheEntry entry) { final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (FileCacheProbe probe : probes) { probe.onEntryHitEvent(fileCache, entry); } } } /** * Notify registered {@link FileCacheProbe}s about the "entry missed" event. * * @param fileCache the <tt>FileCache</tt> event occurred on. * @param request HTTP request. */ protected static void notifyProbesEntryMissed( final FileCache fileCache, final HttpRequestPacket request) { final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe(); if (probes != null && probes.length > 0) { for (FileCacheProbe probe : probes) { probe.onEntryMissedEvent( fileCache, request.getHeader(Header.Host), request.getRequestURI()); } } } /** * Notify registered {@link FileCacheProbe}s about the error. * * @param fileCache the <tt>FileCache</tt> event occurred on. */ protected static void notifyProbesError(final FileCache fileCache, final Throwable error) { final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (FileCacheProbe probe : probes) { probe.onErrorEvent(fileCache, error); } } } protected static long convertToLong(final String dateHeader) { if (dateHeader == null) return (-1L); final SimpleDateFormats formats = SimpleDateFormats.create(); try { // Attempt to convert the date header in a variety of formats long result = FastHttpDateFormat.parseDate(dateHeader, formats.getFormats()); if (result != (-1L)) { return result; } throw new IllegalArgumentException(dateHeader); } finally { formats.recycle(); } } private static class EntryWorker implements DelayedExecutor.Worker<FileCacheEntry> { @Override public boolean doWork(final FileCacheEntry element) { element.run(); return true; } } private static class EntryResolver implements DelayedExecutor.Resolver<FileCacheEntry> { @Override public boolean removeTimeout(FileCacheEntry element) { if (element.timeoutMillis != -1) { element.timeoutMillis = -1; return true; } return false; } @Override public long getTimeoutMillis(FileCacheEntry element) { return element.timeoutMillis; } @Override public void setTimeoutMillis(FileCacheEntry element, long timeoutMillis) { element.timeoutMillis = timeoutMillis; } } }
public void testBlockingRead() throws Exception { final String[] clientMsgs = {"XXXXX", "Hello", "from", "client"}; Connection connection = null; int messageNum = 3; final BlockingQueue<String> intermResultQueue = DataStructures.getLTQInstance(String.class); final PUFilter puFilter = new PUFilter(); FilterChain subProtocolChain = puFilter .getPUFilterChainBuilder() .add(new MergeFilter(clientMsgs.length, intermResultQueue)) .add(new EchoFilter()) .build(); puFilter.register(new SimpleProtocolFinder(clientMsgs[0]), subProtocolChain); FilterChainBuilder filterChainBuilder = FilterChainBuilder.newInstance(); filterChainBuilder.add(new TransportFilter()); filterChainBuilder.add(new StringFilter()); filterChainBuilder.add(puFilter); final TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build(); transport.setFilterChain(filterChainBuilder.build()); try { transport.bind(PORT); transport.start(); final BlockingQueue<String> resultQueue = DataStructures.getLTQInstance(String.class); Future<Connection> future = transport.connect("localhost", PORT); connection = future.get(10, TimeUnit.SECONDS); assertTrue(connection != null); FilterChainBuilder clientFilterChainBuilder = FilterChainBuilder.newInstance(); clientFilterChainBuilder.add(new TransportFilter()); clientFilterChainBuilder.add(new StringFilter()); clientFilterChainBuilder.add( new BaseFilter() { @Override public NextAction handleRead(FilterChainContext ctx) throws IOException { resultQueue.add((String) ctx.getMessage()); return ctx.getStopAction(); } }); final FilterChain clientFilterChain = clientFilterChainBuilder.build(); connection.setFilterChain(clientFilterChain); for (int i = 0; i < messageNum; i++) { String clientMessage = ""; for (int j = 0; j < clientMsgs.length; j++) { String msg = clientMsgs[j] + "-" + i; Future<WriteResult> writeFuture = connection.write(msg); assertTrue("Write timeout loop: " + i, writeFuture.get(10, TimeUnit.SECONDS) != null); final String srvInterm = intermResultQueue.poll(10, TimeUnit.SECONDS); assertEquals("Unexpected interm. response (" + i + ", " + j + ")", msg, srvInterm); clientMessage += msg; } final String message = resultQueue.poll(10, TimeUnit.SECONDS); assertEquals("Unexpected response (" + i + ")", clientMessage, message); } } finally { if (connection != null) { connection.closeSilently(); } transport.shutdownNow(); } }
/** * Common {@link Connection} implementation for Java NIO <tt>Connection</tt>s. * * @author Alexey Stashok */ public abstract class AIOConnection implements Connection<SocketAddress> { private static final Logger logger = Grizzly.logger(AIOConnection.class); protected final AIOTransport transport; protected volatile int readBufferSize; protected volatile int writeBufferSize; protected volatile long readTimeoutMillis = 30000; protected volatile long writeTimeoutMillis = 30000; protected volatile AsynchronousChannel channel; protected volatile Processor processor; protected volatile ProcessorSelector processorSelector; protected final AttributeHolder attributes; protected final TaskQueue<AsyncReadQueueRecord> asyncReadQueue; protected final TaskQueue<AsyncWriteQueueRecord> asyncWriteQueue; protected final AtomicBoolean isClosed = new AtomicBoolean(false); protected volatile boolean isBlocking; protected volatile boolean isStandalone; private final Queue<CloseListener> closeListeners = DataStructures.getLTQinstance(CloseListener.class); /** Connection probes */ protected final MonitoringConfigImpl<ConnectionProbe> monitoringConfig = new MonitoringConfigImpl<>(ConnectionProbe.class); public AIOConnection(AIOTransport transport) { this.transport = transport; asyncReadQueue = TaskQueue.createSafeTaskQueue(); asyncWriteQueue = TaskQueue.createSafeTaskQueue(); attributes = new IndexedAttributeHolder(transport.getAttributeBuilder()); } @Override public void configureBlocking(boolean isBlocking) { this.isBlocking = isBlocking; } @Override public boolean isBlocking() { return isBlocking; } @Override public synchronized void configureStandalone(boolean isStandalone) { if (this.isStandalone != isStandalone) { this.isStandalone = isStandalone; if (isStandalone) { processor = StandaloneProcessor.INSTANCE; processorSelector = StandaloneProcessorSelector.INSTANCE; } else { processor = transport.getProcessor(); processorSelector = transport.getProcessorSelector(); } } } @Override public boolean isStandalone() { return isStandalone; } @Override public Transport getTransport() { return transport; } @Override public int getReadBufferSize() { return readBufferSize; } @Override public void setReadBufferSize(int readBufferSize) { this.readBufferSize = readBufferSize; } @Override public int getWriteBufferSize() { return writeBufferSize; } @Override public void setWriteBufferSize(int writeBufferSize) { this.writeBufferSize = writeBufferSize; } @Override public long getReadTimeout(TimeUnit timeUnit) { return timeUnit.convert(readTimeoutMillis, TimeUnit.MILLISECONDS); } @Override public void setReadTimeout(long timeout, TimeUnit timeUnit) { readTimeoutMillis = TimeUnit.MILLISECONDS.convert(timeout, timeUnit); } @Override public long getWriteTimeout(TimeUnit timeUnit) { return timeUnit.convert(writeTimeoutMillis, TimeUnit.MILLISECONDS); } @Override public void setWriteTimeout(long timeout, TimeUnit timeUnit) { writeTimeoutMillis = TimeUnit.MILLISECONDS.convert(timeout, timeUnit); } public AsynchronousChannel getChannel() { return channel; } protected void setChannel(AsynchronousChannel channel) { this.channel = channel; } @Override public Processor obtainProcessor(IOEvent ioEvent) { if (processor == null && processorSelector == null) { return transport.obtainProcessor(ioEvent, this); } if (processor != null && processor.isInterested(ioEvent)) { return processor; } else if (processorSelector != null) { final Processor selectedProcessor = processorSelector.select(ioEvent, this); if (selectedProcessor != null) { return selectedProcessor; } } return null; } @Override public Processor getProcessor() { return processor; } @Override public void setProcessor(Processor preferableProcessor) { this.processor = preferableProcessor; } @Override public ProcessorSelector getProcessorSelector() { return processorSelector; } @Override public void setProcessorSelector(ProcessorSelector preferableProcessorSelector) { this.processorSelector = preferableProcessorSelector; } public TaskQueue<AsyncReadQueueRecord> getAsyncReadQueue() { return asyncReadQueue; } public TaskQueue<AsyncWriteQueueRecord> getAsyncWriteQueue() { return asyncWriteQueue; } @Override public AttributeHolder getAttributes() { return attributes; } @Override public <M> GrizzlyFuture<ReadResult<M, SocketAddress>> read() throws IOException { return read(null); } @Override public <M> GrizzlyFuture<ReadResult<M, SocketAddress>> read( CompletionHandler<ReadResult<M, SocketAddress>> completionHandler) throws IOException { final Processor obtainedProcessor = obtainProcessor(IOEvent.READ); return obtainedProcessor.read(this, completionHandler); } @Override public <M> GrizzlyFuture<WriteResult<M, SocketAddress>> write(M message) throws IOException { return write(null, message, null); } @Override public <M> GrizzlyFuture<WriteResult<M, SocketAddress>> write( M message, CompletionHandler<WriteResult<M, SocketAddress>> completionHandler) throws IOException { return write(null, message, completionHandler); } @Override public <M> GrizzlyFuture<WriteResult<M, SocketAddress>> write( SocketAddress dstAddress, M message, CompletionHandler<WriteResult<M, SocketAddress>> completionHandler) throws IOException { final Processor obtainedProcessor = obtainProcessor(IOEvent.WRITE); return obtainedProcessor.write(this, dstAddress, message, completionHandler); } @Override public boolean isOpen() { return channel != null && channel.isOpen() && !isClosed.get(); } @Override public GrizzlyFuture close() throws IOException { if (!isClosed.getAndSet(true)) { preClose(); notifyCloseListeners(); notifyProbesClose(this); channel.close(); // return transport.getSelectorHandler().executeInSelectorThread( // selectorRunner, new Runnable() { // // @Override // public void run() { // try { // ((AIOTransport) transport).closeConnection(AIOConnection.this); // } catch (IOException e) { // logger.log(Level.FINE, "Error during connection close", e); // } // } // }, null); } return ReadyFutureImpl.create(this); } /** {@inheritDoc} */ @Override public void addCloseListener(CloseListener closeListener) { // check if connection is still open if (!isClosed.get()) { // add close listener closeListeners.add(closeListener); // check the connection state again if (isClosed.get() && closeListeners.remove(closeListener)) { // if connection was closed during the method call - notify the listener try { closeListener.onClosed(this); } catch (IOException ignored) { } } } else { // if connection is closed - notify the listener try { closeListener.onClosed(this); } catch (IOException ignored) { } } } /** {@inheritDoc} */ @Override public boolean removeCloseListener(CloseListener closeListener) { return closeListeners.remove(closeListener); } /** {@inheritDoc} */ @Override public void notifyConnectionError(Throwable error) { notifyProbesError(this, error); } /** {@inheritDoc} */ @Override public final MonitoringConfig<ConnectionProbe> getMonitoringConfig() { return monitoringConfig; } /** * Notify registered {@link ConnectionProbe}s about the bind event. * * @param connection the <tt>Connection</tt> event occurred on. */ protected static void notifyProbesBind(AIOConnection connection) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onBindEvent(connection); } } } /** * Notify registered {@link ConnectionProbe}s about the accept event. * * @param connection the <tt>Connection</tt> event occurred on. */ protected static void notifyProbesAccept(AIOConnection connection) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onAcceptEvent(connection); } } } /** * Notify registered {@link ConnectionProbe}s about the connect event. * * @param connection the <tt>Connection</tt> event occurred on. */ protected static void notifyProbesConnect(final AIOConnection connection) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onConnectEvent(connection); } } } /** Notify registered {@link ConnectionProbe}s about the read event. */ protected static void notifyProbesRead(AIOConnection connection, Buffer data, int size) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onReadEvent(connection, data, size); } } } /** Notify registered {@link ConnectionProbe}s about the write event. */ protected static void notifyProbesWrite(AIOConnection connection, Buffer data, int size) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onWriteEvent(connection, data, size); } } } /** * Notify registered {@link ConnectionProbe}s about the IO Event ready event. * * @param connection the <tt>Connection</tt> event occurred on. * @param ioEvent the {@link IOEvent}. */ protected static void notifyIOEventReady(AIOConnection connection, IOEvent ioEvent) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onIOEventReadyEvent(connection, ioEvent); } } } /** * Notify registered {@link ConnectionProbe}s about the IO Event enabled event. * * @param connection the <tt>Connection</tt> event occurred on. * @param ioEvent the {@link IOEvent}. */ protected static void notifyIOEventEnabled(AIOConnection connection, IOEvent ioEvent) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onIOEventEnableEvent(connection, ioEvent); } } } /** * Notify registered {@link ConnectionProbe}s about the IO Event disabled event. * * @param connection the <tt>Connection</tt> event occurred on. * @param ioEvent the {@link IOEvent}. */ protected static void notifyIOEventDisabled(AIOConnection connection, IOEvent ioEvent) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onIOEventDisableEvent(connection, ioEvent); } } } /** * Notify registered {@link ConnectionProbe}s about the close event. * * @param connection the <tt>Connection</tt> event occurred on. */ protected static void notifyProbesClose(AIOConnection connection) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onCloseEvent(connection); } } } /** * Notify registered {@link ConnectionProbe}s about the error. * * @param connection the <tt>Connection</tt> event occurred on. */ protected static void notifyProbesError(AIOConnection connection, Throwable error) { final ConnectionProbe[] probes = connection.monitoringConfig.getProbesUnsafe(); if (probes != null) { for (ConnectionProbe probe : probes) { probe.onErrorEvent(connection, error); } } } /** Notify all close listeners */ private void notifyCloseListeners() { CloseListener closeListener; while ((closeListener = closeListeners.poll()) != null) { try { closeListener.onClosed(this); } catch (IOException ignored) { } } } protected abstract void preClose(); }
public void testBlockingReadError() throws Exception { final String[] clientMsgs = {"ZZZZZ", "Hello", "from", "client"}; Connection connection = null; final BlockingQueue intermResultQueue = DataStructures.getLTQInstance(); final PUFilter puFilter = new PUFilter(); FilterChain subProtocolChain = puFilter .getPUFilterChainBuilder() .add(new MergeFilter(clientMsgs.length, intermResultQueue)) .add(new EchoFilter()) .build(); puFilter.register(new SimpleProtocolFinder(clientMsgs[0]), subProtocolChain); FilterChainBuilder filterChainBuilder = FilterChainBuilder.newInstance(); filterChainBuilder.add(new TransportFilter()); filterChainBuilder.add(new StringFilter()); filterChainBuilder.add(puFilter); TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build(); transport.setFilterChain(filterChainBuilder.build()); try { transport.bind(PORT); transport.start(); Future<Connection> future = transport.connect("localhost", PORT); connection = future.get(10, TimeUnit.SECONDS); assertTrue(connection != null); FilterChainBuilder clientFilterChainBuilder = FilterChainBuilder.newInstance(); clientFilterChainBuilder.add(new TransportFilter()); clientFilterChainBuilder.add(new StringFilter()); final FilterChain clientFilterChain = clientFilterChainBuilder.build(); connection.setFilterChain(clientFilterChain); String msg = clientMsgs[0]; Future<WriteResult> writeFuture = connection.write(msg); assertTrue("Write timeout", writeFuture.get(10, TimeUnit.SECONDS) != null); final String srvInterm = (String) intermResultQueue.poll(10, TimeUnit.SECONDS); assertEquals("Unexpected interm. response", msg, srvInterm); connection.closeSilently(); connection = null; final Exception e = (Exception) intermResultQueue.poll(10, TimeUnit.SECONDS); assertTrue( "Unexpected response. Exception: " + e.getClass() + ": " + e.getMessage(), e instanceof IOException); } finally { if (connection != null) { connection.closeSilently(); } transport.shutdownNow(); } }