public LifeCycleFilter() {
   totalConnectionNumber = new AtomicInteger();
   activeConnectionsMap = DataStructures.<Connection, Integer>getConcurrentMap();
 }
Пример #2
0
/**
 * 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();
    }
  }
Пример #4
0
/**
 * 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();
    }
  }