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

com.symphony.bdk.bot.sdk.sse.SseSubscriber Maven / Gradle / Ivy

package com.symphony.bdk.bot.sdk.sse;

import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;

import com.symphony.bdk.bot.sdk.sse.config.SseSubscriberProps;
import com.symphony.bdk.bot.sdk.sse.model.SseEvent;

import lombok.AccessLevel;
import lombok.Getter;

/**
 * Represents a client application subscribing for real-time events
 *
 * @author Marcus Secato
 */
@Getter
public class SseSubscriber {
  private static final Logger LOGGER = LoggerFactory.getLogger(SseSubscriber.class);
  private static final String COMPLETION_EVENT = "_publisher_completion";
  private static final String COMPLETION_WITH_ERROR_EVENT = "_publisher_completion_error";
  private static final String KEEPALIVE_EVENT = "keep-alive";

  private List eventTypes;
  private Map metadata;
  private String lastEventId;
  private Long userId;

  @Getter(AccessLevel.NONE)
  private SseEmitter sseEmitter;
  @Getter(AccessLevel.NONE)
  private List> publishers;
  @Getter(AccessLevel.NONE)
  private BlockingQueue eventQueue;
  @Getter(AccessLevel.NONE)
  private Throwable lastPublisherError;
  @Getter(AccessLevel.NONE)
  private boolean listening = false;
  @Getter(AccessLevel.NONE)
  private SseSubscriberProps subscriberConfig;

  public SseSubscriber(SseEmitter sseEmitter, List eventTypes, Map metadata,
      String lastEventId, Long userId, SseSubscriberProps subscriberConfig) {
    this.sseEmitter = sseEmitter;
    this.eventTypes = eventTypes;
    this.metadata = metadata;
    this.lastEventId = lastEventId;
    this.userId = userId;
    this.subscriberConfig = subscriberConfig;

    this.eventQueue = new LinkedBlockingQueue<>(
        subscriberConfig.getQueueCapacity());
    this.sseEmitter.onCompletion(() -> forceComplete());
  }

  void bindPublishers(List> publishers) {
    this.publishers = publishers;
    this.publishers.stream().forEach(pub -> pub.addSubscriber(this));
  }

  void startListening() {
    listening = true;
    listen();
  }

  private void listen() {
    while (listening) {
      try {
        SseEvent event = eventQueue.poll(
            subscriberConfig.getQueueTimeout(), TimeUnit.MILLISECONDS);

        if (event == null) {
          sseEmitter.send(SseEmitter.event()
              .name(KEEPALIVE_EVENT));
        } else {
          switch (event.getEvent()) {
            case COMPLETION_EVENT:
              sseEmitter.complete();
              terminate();
              break;
            case COMPLETION_WITH_ERROR_EVENT:
              sseEmitter.completeWithError(lastPublisherError);
              terminate();
              break;
            default:
              sseEmitter.send(buildEvent(event));
          }
        }
      } catch (Exception e) {
        LOGGER.info("Error handling event for user {}: {}", userId, e.getMessage());
        terminate();
      }
    }
  }

  /**
   * Sends event back to client application. Called by {@link SsePublisher} when new event is
   * available.
   *
   * @param sseEvent the SSE event to be sent
   */
  public void sendEvent(SseEvent sseEvent) {
    try {
      eventQueue.put(sseEvent);
    } catch (InterruptedException ie) {
      LOGGER.debug("Queue interrupted error when adding event");
    }
  }

  /**
   * Notifies subscriber that the given publisher is done sending event
   *
   * @param publisher the publisher 
   */
  void complete(SsePublisher publisher) {
    internalComplete(publisher);
  }

  /**
   * Notifies subscriber that the given publisher is done sending event due to error
   *
   * @param publisher the publisher
   * @param ex the error
   */
  void completeWithError(SsePublisher publisher, Throwable ex) {
    lastPublisherError = ex;
    internalComplete(publisher);
  }

  private void internalComplete(SsePublisher publisher) {
    LOGGER.debug("Handling publisher completion");
    publishers = publishers.stream()
        .filter(pub -> !pub.equals(publisher))
        .collect(Collectors.toList());

    if (publishers.size() == 0) {
      LOGGER.debug("No more publishers. Informing client that server is done.");
      if (lastPublisherError != null) {
        sendEvent(SseEvent.builder()
            .event(COMPLETION_WITH_ERROR_EVENT)
            .build());
      } else {
        sendEvent(SseEvent.builder()
            .event(COMPLETION_EVENT)
            .build());
      }
    }
  }

  private void terminate() {
    LOGGER.debug("Terminating SSE subscription for user {}", userId);
    listening = false;
    publishers.stream().forEach(pub -> pub.removeSubscriber(this));
  }

  private void forceComplete() {
    sendEvent(SseEvent.builder()
        .event(COMPLETION_EVENT)
        .build());
  }

  private SseEventBuilder buildEvent(SseEvent event) {
    SseEventBuilder builder = SseEmitter.event();
    if (event.getId() != null) {
      builder.id(event.getId());
    }

    if (event.getEvent() != null) {
      builder.name(event.getEvent());
    }

    if (event.getData() != null) {
      builder.data(event.getData());
    }

    if (event.getRetry() != null) {
      builder.reconnectTime(event.getRetry());
    }

    return builder;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy