Class FcSampleSseServlet

java.lang.Object
jakarta.servlet.GenericServlet
jakarta.servlet.http.HttpServlet
org.frontcache.resilience.stream.FcSampleSseServlet
All Implemented Interfaces:
jakarta.servlet.Servlet, jakarta.servlet.ServletConfig, Serializable
Direct Known Subclasses:
FcDashboardStreamServlet

public abstract class FcSampleSseServlet extends jakarta.servlet.http.HttpServlet
Serves the SSE dashboard stream at /fc-dashboard.stream and at the pre-2.7 /hystrix.stream, from Frontcache's own metrics instead of Hystrix's. The SSE framing, the 500 ms cadence and the maxConcurrentConnections throttle are exactly what they were. What changed, in two steps:

PR 3 removed RxJava

Observable.concatMap(...).observeOn(Schedulers.io()) became one scheduled publisher plus one queue per subscriber. Frames are built ONCE per tick rather than once per subscriber, so fifteen dashboards cost one serialization rather than fifteen.

2.7 removed the parked thread

Until now each connection held a container thread for its entire life - originally asleep, then blocked on its own queue. At the default cap that is five threads, at a tuned fifteen it is fifteen, doing nothing but waiting 500 ms at a time. On a node whose thread pool also serves traffic, leaving a dashboard tab open all afternoon costs a request-serving thread all afternoon. This is now Servlet 3.1 async with non-blocking output: doGet validates, calls startAsync(), registers a WriteListener and returns, handing the container thread straight back. No thread is dedicated to a viewer at all - writes happen either on the publisher thread (when the connection is ready to take a frame) or on a container thread the container hands us via onWritePossible() (when it was not, and now is).

Why the writes are non-blocking rather than "publisher writes and blocks"

Because that is the whole difficulty of the change. A single publisher thread doing blocking writes would be simple and would reintroduce precisely what PR 3 fixed: one reader behind TCP backpressure stalls every other dashboard. isReady() is what avoids it - a write is attempted only when the container promises it will not block, so a stalled reader just stops being written to. Its own queue fills, its oldest frames are dropped, and no other connection notices.

The idle handoff

onWritePossible() is only called on a not-ready -> ready transition, so a subscriber that drains its queue while still ready will never be called again on its own. That is why FcSampleSseServlet.Subscriber.offer(byte[]) also drains: whichever of the two threads finds work to do does it, under one lock per subscriber so that only one of them is inside isReady()/write() at a time. The lock is held only across non-blocking writes, so a container thread can never be parked on it for longer than it takes to fill a socket buffer.

The connection count

The cap is now released from FcSampleSseServlet.Subscriber.close(), not from a finally around the request, because the request ends immediately and the connection does not. close() is compare-and-set guarded so the decrement happens exactly once however the connection ends - client disconnect, write error, container timeout, or shutdown. Getting this wrong would not fail visibly at once: the endpoint would simply start answering 503 forever after N lifetime connections, which is why StandaloneDashboardStreamTests asserts a closed connection frees its slot.

Two behavior changes, both from proposal section 14.6

- A wrong or absent API key gets 401, where it used to get 200 and an endless stream of empty frames. The header is this endpoint's only authentication, so an unauthenticated read should be refused rather than answered with an empty success. - 503 now means only what it says: the service is shutting down, or the connection cap is reached. The old code answered every case, including "your key is wrong", the same way.
See Also:
  • Field Details

    • DEFAULT_PAUSE_POLLER_THREAD_DELAY_IN_MS

      protected static final int DEFAULT_PAUSE_POLLER_THREAD_DELAY_IN_MS
      Publish cadence in ms. Named for the field it replaced, which was a poll timeout.
      See Also:
  • Constructor Details

    • FcSampleSseServlet

      protected FcSampleSseServlet(int pausePollerThreadDelayInMs)
  • Method Details

    • getMaxNumberConcurrentConnectionsAllowed

      protected abstract int getMaxNumberConcurrentConnectionsAllowed()
    • getNumberCurrentConnections

      protected abstract int getNumberCurrentConnections()
    • incrementAndGetCurrentConcurrentConnections

      protected abstract int incrementAndGetCurrentConcurrentConnections()
    • decrementCurrentConcurrentConnections

      protected abstract void decrementCurrentConcurrentConnections()
    • doGet

      protected void doGet(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response) throws jakarta.servlet.ServletException, IOException
      Overrides:
      doGet in class jakarta.servlet.http.HttpServlet
      Throws:
      jakarta.servlet.ServletException
      IOException
    • shutdown

      public static void shutdown()
      WebSphere won't shutdown a servlet until after a 60 second timeout if there is an instance of the servlet executing a request. Add this method to enable a hook to notify Frontcache to shutdown. You must invoke this method at shutdown, perhaps from some other servlet's destroy() method.
    • init

      public void init() throws jakarta.servlet.ServletException
      Overrides:
      init in class jakarta.servlet.GenericServlet
      Throws:
      jakarta.servlet.ServletException
    • destroy

      public void destroy()
      Specified by:
      destroy in interface jakarta.servlet.Servlet
      Overrides:
      destroy in class jakarta.servlet.GenericServlet