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

io.serialized.client.feed.FeedClient Maven / Gradle / Ivy

The newest version!
package io.serialized.client.feed;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.serialized.client.SerializedClientConfig;
import io.serialized.client.SerializedOkHttpClient;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import org.apache.commons.lang3.Validate;

import java.io.Closeable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Logger;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS;
import static io.serialized.client.feed.FeedRequests.getSequenceNumber;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;

public class FeedClient implements Closeable {

  private static final String SEQUENCE_NUMBER_HEADER = "Serialized-SequenceNumber-Current";

  private final Logger logger = Logger.getLogger(getClass().getName());

  private final SerializedOkHttpClient client;
  private final HttpUrl apiRoot;
  private final Map executors = new ConcurrentHashMap<>();

  private FeedClient(Builder builder) {
    this.client = new SerializedOkHttpClient(builder.httpClient, builder.objectMapper);
    this.apiRoot = builder.apiRoot;
  }

  public static Builder feedClient(SerializedClientConfig config) {
    return new Builder(config);
  }

  @Override
  public void close() {
    executors.values().forEach(ExecutorService::shutdown);
  }

  /**
   * Terminates a subscription by shutting down its executor.
   *
   * @param subscriptionId ID of subscription to terminate.
   */
  public void close(UUID subscriptionId) {
    Optional.ofNullable(executors.remove(subscriptionId)).ifPresent(ExecutorService::shutdown);
  }

  /**
   * Executes a poll starting at given sequence number.
   *
   * @param since Sequence number to start feeding from. Zero (0) starts from the beginning.
   */
  public FeedResponse execute(GetFeedRequest request, long since) {
    HttpUrl.Builder urlBuilder = url(request.feedName);
    Optional.ofNullable(request.limit).ifPresent(limit -> urlBuilder.addQueryParameter("limit", String.valueOf(limit)));
    Optional.ofNullable(request.partitionCount).ifPresent(pCount -> urlBuilder.addQueryParameter("partitionCount", String.valueOf(pCount)));
    Optional.ofNullable(request.partitionNumber).ifPresent(pNumber -> urlBuilder.addQueryParameter("partitionNumber", String.valueOf(pNumber)));
    Optional.ofNullable(request.waitTime).ifPresent(waitTime -> urlBuilder.addQueryParameter("waitTime", String.valueOf(waitTime.toMillis())));

    for (String type : request.types) {
      urlBuilder.addQueryParameter("filterType", type);
    }

    HttpUrl url = urlBuilder.addQueryParameter("since", String.valueOf(since)).build();

    if (request.tenantId().isPresent()) {
      return client.get(url, FeedResponse.class, request.tenantId);
    } else {
      return client.get(url, FeedResponse.class);
    }
  }

  /**
   * Starts subscribing to the feed.
   * The default in-memory sequence number tracker will be used.
   *
   * @param feedEntryHandler Handler invoked for each received entry
   * @return The subscription ID
   * @see SequenceNumberTracker
   * @see InMemorySequenceNumberTracker
   */
  public UUID subscribe(GetFeedRequest request, FeedEntryHandler feedEntryHandler) {
    return subscribe(request, new InMemorySequenceNumberTracker(), feedEntryHandler);
  }

  /**
   * Starts subscribing to the feed.
   * The default in-memory sequence number tracker will be used.
   *
   * @param feedEntryBatchHandler Handler invoked for each received batch
   * @return The subscription ID
   * @see SequenceNumberTracker
   * @see InMemorySequenceNumberTracker
   */
  public UUID subscribe(GetFeedRequest request, FeedEntryBatchHandler feedEntryBatchHandler) {
    return subscribe(request, new InMemorySequenceNumberTracker(), feedEntryBatchHandler);
  }

  /**
   * Starts subscribing to the feed.
   *
   * @param feedEntryHandler Handler invoked for each received entry
   * @return The subscription ID
   */
  public UUID subscribe(GetFeedRequest request, SequenceNumberTracker sequenceNumberTracker, FeedEntryHandler feedEntryHandler) {
    Validate.isTrue(request.waitTime.getSeconds() > 0, "'waitTime' in request cannot be zero when subscribing to a feed");
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    if (request.startFromHead) {
      long sequenceNumber = execute(getSequenceNumber().withFeed(request.feedName).build());
      sequenceNumberTracker.updateLastConsumedSequenceNumber(sequenceNumber);
    }

    executor.scheduleWithFixedDelay(() -> {

      FeedResponse response;

      try {
        do {
          long sequenceNumber = sequenceNumberTracker.lastConsumedSequenceNumber();

          response = execute(request, sequenceNumber);

          if (sequenceNumber > sequenceNumberTracker.lastConsumedSequenceNumber()) {
            return; // Tracker was reset during poll - return to poll again.
          }

          if (response.entries().isEmpty() && response.currentSequenceNumber() > sequenceNumber) {
            sequenceNumberTracker.updateLastConsumedSequenceNumber(response.currentSequenceNumber());

          } else {

            for (FeedEntry feedEntry : response.entries()) {
              try {
                feedEntryHandler.handle(feedEntry);

                try {
                  sequenceNumberTracker.updateLastConsumedSequenceNumber(feedEntry.sequenceNumber());
                } catch (RuntimeException re) {
                  logger.log(WARNING, format("Error updating sequence number after processing: %s - last polled number was [%d]", feedEntry, sequenceNumber), re);
                  throw re;
                }

              } catch (RetryException e) {
                // Retry requested
              }
            }
          }
        } while (request.eagerFetching && response.hasMore());
      } catch (Exception exception) {
        logger.log(WARNING, format("Error polling event feed [%s]: %s", request.feedName, exception.getMessage()), exception);

        try {
          Thread.sleep(1000); // sleep before retrying
        } catch (InterruptedException io) {
          // ignore
        }
      }

    }, 1, 1, TimeUnit.MILLISECONDS);

    UUID subscriptionId = UUID.randomUUID();
    executors.put(subscriptionId, executor);
    return subscriptionId;
  }

  /**
   * Starts subscribing to the feed.
   *
   * @param feedEntryBatchHandler Handler invoked for each received batch
   * @return The subscription ID
   */
  public UUID subscribe(GetFeedRequest request, SequenceNumberTracker sequenceNumberTracker, FeedEntryBatchHandler feedEntryBatchHandler) {
    Validate.isTrue(request.waitTime.getSeconds() > 0, "'waitTime' in request cannot be zero when subscribing to a feed");
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    if (request.startFromHead) {
      long sequenceNumber = execute(getSequenceNumber().withFeed(request.feedName).build());
      sequenceNumberTracker.updateLastConsumedSequenceNumber(sequenceNumber);
    }

    executor.scheduleWithFixedDelay(() -> {

      FeedResponse response;

      try {
        do {
          long sequenceNumber = sequenceNumberTracker.lastConsumedSequenceNumber();

          response = execute(request, sequenceNumber);

          if (sequenceNumber > sequenceNumberTracker.lastConsumedSequenceNumber()) {
            return; // Tracker was reset during poll - return to poll again.
          }

          List entries = response.entries();

          if (entries.isEmpty()) {
            if (response.currentSequenceNumber() > sequenceNumber) {
              sequenceNumberTracker.updateLastConsumedSequenceNumber(response.currentSequenceNumber());
            }
          } else {
            feedEntryBatchHandler.handle(entries);
            sequenceNumberTracker.updateLastConsumedSequenceNumber(entries.get(entries.size() - 1).sequenceNumber());
          }

        } while (request.eagerFetching && response.hasMore());
      } catch (Exception exception) {
        logger.log(WARNING, format("Error polling event feed [%s]: %s", request.feedName, exception.getMessage()), exception);

        try {
          Thread.sleep(1000); // sleep before retrying
        } catch (InterruptedException io) {
          // ignore
        }
      }

    }, 1, 1, TimeUnit.MILLISECONDS);

    UUID subscriptionId = UUID.randomUUID();
    executors.put(subscriptionId, executor);
    return subscriptionId;
  }

  /**
   * @return Feed names and details.
   */
  public List execute(ListFeedsRequest request) {
    HttpUrl url = apiRoot.newBuilder().addPathSegment("feeds").build();

    if (request.tenantId().isPresent()) {
      return client.get(url, FeedsResponse.class, request.tenantId).feeds();
    } else {
      return client.get(url, FeedsResponse.class).feeds();
    }
  }

  /**
   * Gets the current sequence number for current feed.
   * 

* Note that the 'all' feed has it's own global sequence. * * @return The current sequence number, i.e the sequence number of the most recently stored event batch. */ public long execute(GetSequenceNumberRequest request) { HttpUrl url = url(request.feedName).build(); Function func = response -> Long.parseLong(requireNonNull(response.header(SEQUENCE_NUMBER_HEADER))); if (request.tenantId().isPresent()) { return client.head(url, func, request.tenantId); } else { return client.head(url, func); } } private HttpUrl.Builder url(String feedName) { Validate.notBlank(feedName, "No feed specified"); return apiRoot.newBuilder().addPathSegment("feeds").addPathSegment(feedName); } public static class Builder { private final ObjectMapper objectMapper = new ObjectMapper() .disable(FAIL_ON_UNKNOWN_PROPERTIES) .disable(FAIL_ON_EMPTY_BEANS) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) .setSerializationInclusion(NON_NULL); private final OkHttpClient httpClient; private final HttpUrl apiRoot; public Builder(SerializedClientConfig config) { this.httpClient = config.newHttpClient(); this.apiRoot = config.apiRoot(); } /** * Allows object mapper customization. */ public Builder configureObjectMapper(Consumer consumer) { consumer.accept(objectMapper); return this; } public FeedClient build() { return new FeedClient(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy