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

io.split.client.events.EventsTask Maven / Gradle / Ivy

package io.split.client.events;

import com.google.common.annotations.VisibleForTesting;
import io.split.client.dtos.Event;
import io.split.client.utils.Utils;
import io.split.telemetry.storage.TelemetryRuntimeProducer;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static java.lang.Thread.MIN_PRIORITY;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Responsible for sending events added via .track() to Split collection services
 */
public class EventsTask{

    public static final Long MAX_SIZE_BYTES = 5 * 1024 * 1024L;

    private final EventsStorageConsumer _eventsStorageConsumer;
    private final EventsStorageProducer _eventsStorageProducer;
    private final EventsSender _eventsSender;
    private final int _maxQueueSize;
    private final long _flushIntervalMillis;

    private final ExecutorService _senderExecutor;
    private final ExecutorService _consumerExecutor;

    private final ScheduledExecutorService _flushScheduler;

    static final Event SENTINEL = new Event();
    private static final Logger _log = LoggerFactory.getLogger(EventsTask.class);
    private final CloseableHttpClient _httpclient;
    private final URI _target;
    private final int _waitBeforeShutdown;
    private final TelemetryRuntimeProducer _telemetryRuntimeProducer;

    ThreadFactory eventClientThreadFactory(final String name) {
        return r -> new Thread(() -> {
            Thread.currentThread().setPriority(MIN_PRIORITY);
            r.run();
        }, name);
    }


    public static EventsTask create(CloseableHttpClient httpclient, URI eventsRootTarget, int maxQueueSize,
                                    long flushIntervalMillis, int waitBeforeShutdown, TelemetryRuntimeProducer telemetryRuntimeProducer, EventsStorageConsumer eventsStorageConsumer, EventsStorageProducer _eventsStorageProducer) throws URISyntaxException {
        return new EventsTask(eventsStorageConsumer,
                _eventsStorageProducer,
                httpclient,
                Utils.appendPath(eventsRootTarget, "api/events/bulk"),
                maxQueueSize,
                flushIntervalMillis,
                waitBeforeShutdown,
                telemetryRuntimeProducer);
    }

    EventsTask(EventsStorageConsumer eventsStorageConsumer, EventsStorageProducer eventsStorageProducer, CloseableHttpClient httpclient, URI target, int maxQueueSize,
               long flushIntervalMillis, int waitBeforeShutdown, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException {

        _httpclient = checkNotNull(httpclient);

        _target = checkNotNull(target);

        _eventsStorageConsumer = checkNotNull(eventsStorageConsumer);
        _eventsStorageProducer = checkNotNull(eventsStorageProducer);
        _waitBeforeShutdown = waitBeforeShutdown;

        _maxQueueSize = maxQueueSize;
        _flushIntervalMillis = flushIntervalMillis;
        _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);

        _eventsSender = EventsSender.create(_httpclient, _target, _telemetryRuntimeProducer);
        _senderExecutor = new ThreadPoolExecutor(
                1,
                1,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue(50),
                eventClientThreadFactory("eventclient-sender"),
                (r, executor) -> _log.warn("Executor queue full. Dropping events."));

        _consumerExecutor = Executors.newSingleThreadExecutor(eventClientThreadFactory("eventclient-consumer"));
        _consumerExecutor.submit(runConsumer());

        _flushScheduler = Executors.newScheduledThreadPool(1, eventClientThreadFactory("eventclient-flush"));
        _flushScheduler.scheduleAtFixedRate(() -> flush(), _flushIntervalMillis, _flushIntervalMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * the existence of this message in the queue triggers a send event in the consumer thread.
     */
    public void flush() {
        _eventsStorageProducer.track(SENTINEL, 0);
    }  // SENTINEL event won't be queued, so no size needed.

    public void close() {
        try {
            _consumerExecutor.shutdownNow();
            _flushScheduler.shutdownNow();
            _senderExecutor.awaitTermination(_waitBeforeShutdown, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            _log.warn("Error when shutting down EventClientImpl", e);
        }
    }

    /**
     * Infinite loop that listens to event from the event queue, dequeue them and send them over once:
     *  - a CENTINEL message has arrived, or
     *  - the queue reached a specific size
     *
     */

    private Runnable runConsumer() {
        Runnable runnable = () -> {
            List events = new ArrayList<>();
            long accumulated = 0;
            while (!Thread.currentThread().isInterrupted()) {
                WrappedEvent data = _eventsStorageConsumer.pop();
                Event event = data.event();
                Long size = data.size();

                if (event != SENTINEL) {
                    events.add(event);
                    accumulated += size;
                } else if (events.size() < 1) {

                    if (_log.isDebugEnabled()) {
                        _log.debug("No messages to publish.");
                    }

                    continue;
                }
                if (events.size() >= _maxQueueSize ||  accumulated >= MAX_SIZE_BYTES || event == SENTINEL) {

                    // Send over the network
                    if (_log.isDebugEnabled()) {
                        _log.debug(String.format("Sending %d events", events.size()));
                    }

                    // Dispatch
                    List finalEvents = events; //This is to be able to handle events on Runnable.
                    Runnable r = () -> _eventsSender.sendEvents(finalEvents);
                    _senderExecutor.submit(r);

                    // Clear the queue of events for the next batch.
                    events = new ArrayList<>();
                    accumulated = 0;
                }
            }
        };
        return runnable;
    }

    @VisibleForTesting
    URI getTarget() {
        return _target  ;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy