Class SourceImagePrewarmService

java.lang.Object
io.goobi.viewer.controller.imaging.SourceImagePrewarmService

public final class SourceImagePrewarmService extends Object
Asynchronously triggers the ContentServer's source-image cache for the page the user just navigated to, so the OpenSeadragon tile burst that follows finds the master image already decoded and resident in the SourceImageCache.

Without prewarm, the first viewer of a cold-cache page pays the full source-decode latency spread across the parallel tile requests: every tile thread either ROI-decodes the source itself (cache disabled) or waits for the burst-coalesce decoder (cache enabled but cold — the first tile triggers the decode, every subsequent tile waits). With prewarm, the decoder runs once during HTML render — by the time the browser starts requesting tiles, the cache already holds the decoded BufferedImage and every tile is served via cheap BufferedImage.getSubimage(int, int, int, int).

The service is a JVM-wide singleton because the underlying SourceImageCache is itself a singleton and a single bounded executor avoids one Tomcat session monopolising decode threads at the expense of others. Tasks are submitted to a small pool (4 threads) with a bounded queue; overflow is dropped silently — prewarm is best-effort, never load-bearing.

The decode runs on a worker thread that calls getRenderedImage(null) on a fresh ImageManager. That call hits AbstractImageInterpreter's cache hook which — after the prewarm thread's own registerRequestAndIsBurst primer — takes the burst-coalesce path and lands in SourceImageCache.getOrDecodeFull(java.net.URI, de.unigoettingen.sub.commons.cache.SourceImageCache.ImageDecoder). The first thread (us) wins the race, decodes the master, and populates the cache. Tile threads arriving concurrently see the in-flight CompletableFuture and wait, which is exactly the desired coalescing behavior.

  • Method Details

    • getInstance

      public static SourceImagePrewarmService getInstance()
    • prewarm

      public void prewarm(PhysicalElement page)
      Resolves the source-image URI of the given page and submits an async prewarm. Silently returns when the page is null, has no usable file path, or fails to resolve — prewarm is best-effort and never throws to the caller.
      Parameters:
      page - page whose master image should be pre-decoded; may be null
    • prewarmByPiAndPage

      public void prewarmByPiAndPage(String pi, int pageOrder)
      Submits an async prewarm for the page identified by record PI and 1-based page order. Returns immediately. The Solr lookup that resolves PI+order to the master file path runs on a worker thread, never on the caller's thread (typically a servlet request thread that must hand the response back to the user as fast as possible).

      Used by PrewarmRequestFilter which fires before JSF is even involved — earliest possible point in the request lifecycle to start an async master-image decode.

      Parameters:
      pi - record PI; null/blank short-circuits
      pageOrder - 1-based page order; values < 1 short-circuit
    • prewarm

      public void prewarm(URI sourceUri)
      Submits an async prewarm for the given source-image URI. Returns immediately. The actual decode happens on a worker thread some time later. Safe to call repeatedly with the same URI: a duplicate submission while a previous prewarm is still in flight short-circuits in SourceImageCache.getOrDecodeFull(java.net.URI, de.unigoettingen.sub.commons.cache.SourceImageCache.ImageDecoder) (one decoder runs, the other waits and serves the same image), and a submission for an already-cached URI returns without doing work.
      Parameters:
      sourceUri - URI of the master image file (typically file://...). May be null — null is silently ignored to keep callers free of preconditions.
    • shutdown

      public void shutdown()
      Stops the prewarm executor cleanly so Tomcat does not report a memory-leak warning at webapp undeploy. Discards any queued tasks (prewarm is best-effort, so dropping pending decodes is fine), interrupts in-flight worker threads and waits up to 5 seconds for them to terminate.

      Called from ContextListener#contextDestroyed BEFORE the Solr client is closed, because resolveAndPrewarm(java.lang.String, int) performs a Solr lookup; shutting down the executor first guarantees no worker is still running when Solr disappears.

    • getSubmittedCount

      public long getSubmittedCount()
      Returns:
      number of prewarm tasks ever submitted to the executor
    • getCompletedCount

      public long getCompletedCount()
      Returns:
      number of prewarm tasks that ran to a successful decode
    • getDroppedCount

      public long getDroppedCount()
      Returns:
      number of prewarm submissions rejected by the executor (e.g. saturated queue)
    • getSkippedAlreadyCachedCount

      public long getSkippedAlreadyCachedCount()
      Returns:
      number of prewarm calls short-circuited because the URI was already cached
    • getFailedCount

      public long getFailedCount()
      Returns:
      number of prewarm tasks that failed (file missing, decode error, Solr lookup)