All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.terracotta.management.resource.services.events.AllEventsResourceServiceImplV2 Maven / Gradle / Ivy

Go to download

Ehcache is an open source, standards-based cache used to boost performance, offload the database and simplify scalability. Ehcache is robust, proven and full-featured and this has made it the most widely-used Java-based cache.

There is a newer version: 2.10.9.2
Show newest version
/*
 * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
 */
package org.terracotta.management.resource.services.events;

import org.glassfish.jersey.media.sse.OutboundEvent;
import org.glassfish.jersey.media.sse.SseBroadcaster;
import org.glassfish.jersey.media.sse.SseFeature;
import org.glassfish.jersey.server.ChunkedOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terracotta.management.ServiceLocator;
import org.terracotta.management.resource.events.EventEntityV2;

import java.io.IOException;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;

/**
 * A resource service for sending events.
 *
 * Since {@link TerracottaEventOutput} does not flushes events by itself, messages are flushed only once every
 * {@link #BATCH_SIZE} times. To prevent events lingering in the queue waiting for the {@link #BATCH_SIZE} quota to be
 * reached, a timer performs a flush every {@link #TIMER_INTERVAL} ms.
 * Finally, since Jersey does not close event outputs itself (even when the TCP connection was dropped,
 * see JERSEY-2833 for details), event outputs get closed if
 * they've been idle for {@link #MAX_IDLE_KEEPALIVE} ms.
 *
 * This must be marked as @Singleton otherwise Jersey will create a new instance of this class per request,
 * creating as many timer threads.
 *
 * @author Ludovic Orban
 */
@Path("/v2/events")
@Singleton
public class AllEventsResourceServiceImplV2 {

  private static final Logger LOG = LoggerFactory.getLogger(AllEventsResourceServiceImplV2.class);

  public static final int  BATCH_SIZE     = Integer.getInteger("TerracottaEventOutput.batch_size", 32);
  public static final long     TIMER_INTERVAL     = Long.getLong("TerracottaEventOutput.timer_interval", 917L);

  // Making the reaper timer interval a non-round number of seconds to reduce the probability that a race condition
  // occurs causing multiple events to fire
  public static final long     MAX_IDLE_KEEPALIVE = Long.getLong("TerracottaEventOutput.max_idle_keepalive", 57917L);

  private final EventServiceV2 eventService;

  // This broadcaster is only used for book keeping purposes.
  private final Broadcaster broadcaster;

  // TAB-6785 : it's as if @Singleton had no effects, so making sure here to instantiate only 1 timer
  private static final Timer flushTimer = new Timer("sse-flush-timer", true);

  public AllEventsResourceServiceImplV2() {
    this.eventService = ServiceLocator.locate(EventServiceV2.class);
    this.broadcaster = new Broadcaster();
    LOG.debug("sse-flush-timer being used: {}", flushTimer);
    flushTimer.schedule(new TimerTask() {
      @Override
      public void run() {
        LOG.debug("There are {} registered SSE event output(s), checking them", broadcaster.outputs.size());
        for (Map.Entry entry : broadcaster.outputs.entrySet()) {
          TerracottaEventOutput output = entry.getKey();
          TerracottaEventOutputFlushingMetadata metadata = entry.getValue();
          long idleTime = metadata.accumulatedIdleTime.addAndGet(TIMER_INTERVAL);

          int unflushedCount = metadata.unflushedCount.get();
          if (unflushedCount > 0) {
            LOG.debug("A SSE event output accumulated {} unflushed events during max interval, flushing it",
                      unflushedCount);
            try {
              output.flush();
            } catch (Exception e) {
              LOG.warn("Error flushing SSE from timer, closing event output", e);
              broadcaster.close(output);
            } finally {
              metadata.unflushedCount.addAndGet(-unflushedCount);
            }
            continue;
          } else if (idleTime >= MAX_IDLE_KEEPALIVE) {
            LOG.info("A SSE event output has been idle for too long {}, closing it", idleTime);
            broadcaster.close(output);
            continue;
          }

          LOG.debug("A SSE event output accumulated 0 event during flush interval");
        } // for
      }

    }, TIMER_INTERVAL, TIMER_INTERVAL);
  }

  @GET
  @Produces(SseFeature.SERVER_SENT_EVENTS)
  public TerracottaEventOutput getServerSentEvents(@Context UriInfo info, @QueryParam("localOnly") boolean localOnly) {
    LOG.info("Invoking AllEventsResourceServiceImplV2.getServerSentEvents: info={}, localOnly={}", info.getRequestUri(),
             localOnly);

    EventServiceListener eventOutput = new EventServiceListener();

    broadcaster.add(eventOutput);
    eventService.registerEventListener(eventOutput, localOnly);

    return eventOutput;
  }


  private class Broadcaster extends SseBroadcaster {

    private final Map outputs = new ConcurrentHashMap();

    @Override
    public void onException(final ChunkedOutput chunkedOutput, final Exception exception) {
      LOG.debug("Error writing to OutputEvent", exception);
      close(chunkedOutput);
    }

    @Override
    public > boolean add(OUT chunkedOutput) {
      outputs.put((TerracottaEventOutput) chunkedOutput, new TerracottaEventOutputFlushingMetadata());
      return super.add(chunkedOutput);
    }

    @Override
    public void onClose(final ChunkedOutput chunkedOutput) {
      outputs.remove(chunkedOutput);
      eventService.unregisterEventListener((EventServiceListener) chunkedOutput);
    }

    public void close(final ChunkedOutput chunkedOutput) {
      try {
        chunkedOutput.close();
      } catch (Exception e) {
        LOG.warn("Error closing SSE event output from timer", e);
      } finally {
        onClose(chunkedOutput);
        remove(chunkedOutput);
      }
    }
  }


  public class EventServiceListener extends TerracottaEventOutput implements EventServiceV2.EventListener {
    @Override
    public synchronized void write(OutboundEvent chunk) throws IOException {
      if (isClosed()) { throw new IOException("closed"); }

      TerracottaEventOutputFlushingMetadata metadata = broadcaster.outputs.get(this);
      metadata.accumulatedIdleTime.set(0L);
      int unflushedCount = metadata.unflushedCount.incrementAndGet();

      try {
        super.write(chunk);
      } finally {
        if (unflushedCount == BATCH_SIZE) {
          LOG.debug("A SSE event output reached {} unflushed events, flushing it", unflushedCount);
          metadata.unflushedCount.addAndGet(-unflushedCount);
          super.flush();
        } else {
          LOG.debug("A SSE event output accumulating {} unflushed events", unflushedCount);
        }
      }
    }

    @Override
    public void onEvent(EventEntityV2 eventEntity) {
      OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
      eventBuilder.reconnectDelay(100);
      eventBuilder.mediaType(MediaType.APPLICATION_JSON_TYPE);
      eventBuilder.name(EventEntityV2.class.getSimpleName());
      eventBuilder.data(EventEntityV2.class, eventEntity);
      OutboundEvent event = eventBuilder.build();

      try {
        write(event);
      } catch (IOException ioe) {
        onError(ioe);
      }

      if (LOG.isDebugEnabled()) {
        LOG.debug(String.format("Event dispatched: {AgentId: %s, Type: %s, ApiVersion: %s, Representables: %s}",
                                eventEntity.getAgentId(), eventEntity.getType(), eventEntity.getApiVersion(),
                                eventEntity.getRootRepresentables()));
      }
    }

    @Override
    public void onError(Throwable throwable) {
      LOG.debug("Error when waiting for management events.", throwable);
      try {
        broadcaster.close(this);
      } catch (Exception e) {
        LOG.warn("Error closing SSE event output", e);
      }
    }
  }


  private static class TerracottaEventOutputFlushingMetadata {
    final AtomicInteger unflushedCount = new AtomicInteger();
    final AtomicLong accumulatedIdleTime = new AtomicLong();
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy