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

com.bazaarvoice.emodb.databus.core.DefaultDatabus Maven / Gradle / Ivy

There is a newer version: 6.5.190
Show newest version
package com.bazaarvoice.emodb.databus.core;

import com.bazaarvoice.emodb.common.dropwizard.lifecycle.LifeCycleRegistry;
import com.bazaarvoice.emodb.common.dropwizard.time.ClockTicker;
import com.bazaarvoice.emodb.common.uuid.TimeUUIDs;
import com.bazaarvoice.emodb.databus.ChannelNames;
import com.bazaarvoice.emodb.databus.DefaultJoinFilter;
import com.bazaarvoice.emodb.databus.api.Databus;
import com.bazaarvoice.emodb.databus.api.Event;
import com.bazaarvoice.emodb.databus.api.MoveSubscriptionStatus;
import com.bazaarvoice.emodb.databus.api.Names;
import com.bazaarvoice.emodb.databus.api.ReplaySubscriptionStatus;
import com.bazaarvoice.emodb.databus.api.Subscription;
import com.bazaarvoice.emodb.databus.api.UnknownMoveException;
import com.bazaarvoice.emodb.databus.api.UnknownReplayException;
import com.bazaarvoice.emodb.databus.api.UnknownSubscriptionException;
import com.bazaarvoice.emodb.databus.db.SubscriptionDAO;
import com.bazaarvoice.emodb.event.api.EventData;
import com.bazaarvoice.emodb.event.api.EventSink;
import com.bazaarvoice.emodb.event.core.SizeCacheKey;
import com.bazaarvoice.emodb.job.api.JobHandler;
import com.bazaarvoice.emodb.job.api.JobHandlerRegistry;
import com.bazaarvoice.emodb.job.api.JobIdentifier;
import com.bazaarvoice.emodb.job.api.JobRequest;
import com.bazaarvoice.emodb.job.api.JobService;
import com.bazaarvoice.emodb.job.api.JobStatus;
import com.bazaarvoice.emodb.sor.api.Coordinate;
import com.bazaarvoice.emodb.sor.api.Intrinsic;
import com.bazaarvoice.emodb.sor.api.ReadConsistency;
import com.bazaarvoice.emodb.sor.api.UnknownTableException;
import com.bazaarvoice.emodb.sor.condition.Condition;
import com.bazaarvoice.emodb.sor.condition.Conditions;
import com.bazaarvoice.emodb.sor.core.DataProvider;
import com.bazaarvoice.emodb.sor.core.UpdateIntentEvent;
import com.bazaarvoice.emodb.sor.core.UpdateRef;
import com.bazaarvoice.emodb.sortedq.core.ReadOnlyQueueException;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.google.common.base.Predicate;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.common.base.Ticker;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.primitives.Ints;
import com.google.inject.Inject;
import io.dropwizard.lifecycle.Managed;
import org.joda.time.DateTime;
import org.joda.time.Duration;

import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.time.Clock;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

public class DefaultDatabus implements Databus, Managed {
    /** How long should poll loop, searching for events before giving up and returning. */
    private static final Duration MAX_POLL_TIME = Duration.millis(100);

    /** How long should the app wait before querying the data store again when it finds an unknown change? */
    private static final Duration RECENT_UNKNOWN_RETRY = Duration.millis(400);

    /** How old does an event need to be before we stop aggressively looking for it? */
    private static final Duration STALE_UNKNOWN_AGE = Duration.standardSeconds(2);

    /** Don't merge too many duplicate events together to avoid event keys getting unreasonably long. */
    private static final int MAX_EVENTS_TO_CONSOLIDATE = 1000;

    private final EventBus _eventBus;
    private final SubscriptionDAO _subscriptionDao;
    private final DatabusEventStore _eventStore;
    private final DataProvider _dataProvider;
    private final SubscriptionEvaluator _subscriptionEvaluator;
    private final JobService _jobService;
    private final Meter _peekedMeter;
    private final Meter _polledMeter;
    private final Meter _renewedMeter;
    private final Meter _ackedMeter;
    private final Meter _recentUnknownMeter;
    private final Meter _staleUnknownMeter;
    private final Meter _redundantMeter;
    private final Meter _discardedMeter;
    private final Meter _consolidatedMeter;
    private final LoadingCache> _eventSizeCache;
    private final Supplier _defaultJoinFilterCondition;
    private final Ticker _ticker;
    private final Clock _clock;

    @Inject
    public DefaultDatabus(LifeCycleRegistry lifeCycle, EventBus eventBus, DataProvider dataProvider,
                          SubscriptionDAO subscriptionDao, DatabusEventStore eventStore,
                          SubscriptionEvaluator subscriptionEvaluator, JobService jobService,
                          JobHandlerRegistry jobHandlerRegistry, MetricRegistry metricRegistry,
                          @DefaultJoinFilter Supplier defaultJoinFilterCondition, Clock clock) {
        _eventBus = eventBus;
        _subscriptionDao = subscriptionDao;
        _eventStore = eventStore;
        _dataProvider = dataProvider;
        _subscriptionEvaluator = subscriptionEvaluator;
        _jobService = jobService;
        _defaultJoinFilterCondition = defaultJoinFilterCondition;
        _ticker = ClockTicker.getTicker(clock);
        _clock = clock;
        _peekedMeter = newEventMeter("peeked", metricRegistry);
        _polledMeter = newEventMeter("polled", metricRegistry);
        _renewedMeter = newEventMeter("renewed", metricRegistry);
        _ackedMeter = newEventMeter("acked", metricRegistry);
        _recentUnknownMeter = newEventMeter("recent-unknown", metricRegistry);
        _staleUnknownMeter = newEventMeter("stale-unknown", metricRegistry);
        _redundantMeter = newEventMeter("redundant", metricRegistry);
        _discardedMeter = newEventMeter("discarded", metricRegistry);
        _consolidatedMeter = newEventMeter("consolidated", metricRegistry);
        _eventSizeCache = CacheBuilder.newBuilder()
                .expireAfterWrite(15, TimeUnit.SECONDS)
                .maximumSize(2000)
                .ticker(_ticker)
                .build(new CacheLoader>() {
                    @Override
                    public Map.Entry load(SizeCacheKey key)
                            throws Exception {
                        return Maps.immutableEntry(internalEventCountUpTo(key.channelName, key.limitAsked), key.limitAsked);
                    }
                });
        lifeCycle.manage(this);

        checkNotNull(jobHandlerRegistry, "jobHandlerRegistry");
        registerMoveSubscriptionJobHandler(jobHandlerRegistry);
        registerReplaySubscriptionJobHandler(jobHandlerRegistry);
    }

    private void registerMoveSubscriptionJobHandler(JobHandlerRegistry jobHandlerRegistry) {
        jobHandlerRegistry.addHandler(
                MoveSubscriptionJob.INSTANCE,
                new Supplier>() {
                    @Override
                    public JobHandler get() {
                        return new JobHandler() {
                            @Override
                            public MoveSubscriptionResult run(MoveSubscriptionRequest request)
                                    throws Exception {
                                try {
                                    _eventStore.move(request.getFrom(), request.getTo());
                                } catch (ReadOnlyQueueException e) {
                                    // The from queue is not owned by this server.
                                    return notOwner();
                                }
                                return new MoveSubscriptionResult(new Date());
                            }
                        };
                    }
                });
    }

    private void registerReplaySubscriptionJobHandler(JobHandlerRegistry jobHandlerRegistry) {
        jobHandlerRegistry.addHandler(
                ReplaySubscriptionJob.INSTANCE,
                new Supplier>() {
                    @Override
                    public JobHandler get() {
                        return new JobHandler() {
                            @Override
                            public ReplaySubscriptionResult run(ReplaySubscriptionRequest request)
                                    throws Exception {
                                try {
                                    replay(request.getSubscription(), request.getSince());
                                } catch (ReadOnlyQueueException e) {
                                    // The subscription is not owned by this server.
                                    return notOwner();
                                }
                                // Make sure that we have completed the replay request in time.
                                // If replay took more than the replay TTL time, we should fail it as
                                // there could be some events that were expired before we could replay it
                                if (request.getSince() != null && new DateTime(request.getSince())
                                        .plus(DatabusChannelConfiguration.REPLAY_TTL)
                                        .isBeforeNow()) {
                                    // Uh-oh we were too late in replaying, and could have lost some events
                                    throw new ReplayTooLateException();
                                }
                                return new ReplaySubscriptionResult(new Date());
                            }
                        };
                    }
                });
    }

    private void createDatabusReplaySubscription() {
        // Create a master databus replay subscription where the events expire every 50 hours (2 days + 2 hours)
        subscribe(ChannelNames.getMasterReplayChannel(), Conditions.alwaysTrue(),
                Duration.standardDays(3650), DatabusChannelConfiguration.REPLAY_TTL, false);
    }

    private Meter newEventMeter(String name, MetricRegistry metricRegistry) {
        return metricRegistry.meter(getMetricName(name));
    }

    private String getMetricName(String name) {
        return MetricRegistry.name("bv.emodb.databus", "DefaultDatabus", name);
    }

    @Override
    public void start() throws Exception {
        // Create a databus replay subscription
        createDatabusReplaySubscription();
        _eventBus.register(this);
    }

    @Override
    public void stop() throws Exception {
        _eventBus.unregister(this);
    }

    @Override
    public Iterator listSubscriptions(@Nullable String fromSubscriptionExclusive, long limit) {
        checkArgument(limit > 0, "Limit must be >0");

        // We always have all the subscriptions cached in memory so fetch them all.
        Collection subscriptions = _subscriptionDao.getAllSubscriptions();

        // Ignore internal subscriptions (eg. "__system_bus:canary").
        subscriptions = Collections2.filter(subscriptions, new Predicate() {
            @Override
            public boolean apply(Subscription subscription) {
                return !subscription.getName().startsWith("__");
            }
        });

        // Sort them by name.  They're stored sorted in Cassandra so this should be a no-op, but
        // do the sort anyway so we're not depending on internals of the subscription DAO.
        List sorted = new Ordering() {
            @Override
            public int compare(Subscription left, Subscription right) {
                return left.getName().compareTo(right.getName());
            }
        }.immutableSortedCopy(subscriptions);

        // Apply the "from" parameter.
        if (fromSubscriptionExclusive != null) {
            int start = 0;
            for (; start < sorted.size(); start++) {
                if (fromSubscriptionExclusive.compareTo(sorted.get(start).getName()) < 0) {
                    break;
                }
            }
            sorted = sorted.subList(start, sorted.size());
        }

        // Apply the "limit" parameter (be careful to avoid overflow when limit == Long.MAX_VALUE).
        if (sorted.size() > limit) {
            sorted = sorted.subList(0, (int) limit);
        }

        return sorted.iterator();
    }

    @Override
    public void subscribe(String subscription, Condition tableFilter, Duration subscriptionTtl, Duration eventTtl) {
        subscribe(subscription, tableFilter, subscriptionTtl, eventTtl, true);
    }

    @Override
    public void subscribe(String subscription, Condition tableFilter, Duration subscriptionTtl, Duration eventTtl,
                          boolean includeDefaultJoinFilter) {
        // This call should be depracated soon.
        checkLegalSubscriptionName(subscription);
        checkNotNull(tableFilter, "tableFilter");
        checkArgument(subscriptionTtl.isLongerThan(Duration.ZERO), "SubscriptionTtl must be >0");
        checkArgument(eventTtl.isLongerThan(Duration.ZERO), "EventTtl must be >0");
        TableFilterValidator.checkAllowed(tableFilter);
        if (includeDefaultJoinFilter) {
            // If the default join filter condition is set (that is, isn't "alwaysTrue()") then add it to the filter
            Condition defaultJoinFilterCondition = _defaultJoinFilterCondition.get();
            if (!Conditions.alwaysTrue().equals(defaultJoinFilterCondition)) {
                if (tableFilter.equals(Conditions.alwaysTrue())) {
                    tableFilter = defaultJoinFilterCondition;
                } else {
                    tableFilter = Conditions.and(tableFilter, defaultJoinFilterCondition);
                }
            }
        }

        // except for resetting the ttl, recreating a subscription that already exists has no effect.
        // assume that multiple servers that manage the same subscriptions can each attempt to create
        // the subscription at startup.
        _subscriptionDao.insertSubscription(subscription, tableFilter, subscriptionTtl, eventTtl);
    }

    @Override
    public void unsubscribe(String subscription) {
        checkLegalSubscriptionName(subscription);

        _subscriptionDao.deleteSubscription(subscription);
        _eventStore.purge(subscription);
    }

    @Override
    public Subscription getSubscription(String name) throws UnknownSubscriptionException {
        checkLegalSubscriptionName(name);

        Subscription subscription = _subscriptionDao.getSubscription(name);
        if (subscription == null) {
            throw new UnknownSubscriptionException(name);
        }
        return subscription;
    }

    @Subscribe
    public void onUpdateIntent(UpdateIntentEvent event) {
        List eventIds = Lists.newArrayListWithCapacity(event.getUpdateRefs().size());
        for (UpdateRef ref : event.getUpdateRefs()) {
            eventIds.add(UpdateRefSerializer.toByteBuffer(ref));
        }
        _eventStore.addAll(ChannelNames.getMasterFanoutChannel(), eventIds);
    }

    @Override
    public long getEventCount(String subscription) {
        return getEventCountUpTo(subscription, Long.MAX_VALUE);
    }

    @Override
    public long getEventCountUpTo(String subscription, long limit) {
        // We get the size from cache as a tuple of size, and the limit used to estimate that size
        // So, the key is the size, and value is the limit used to estimate the size
        SizeCacheKey sizeCacheKey = new SizeCacheKey(subscription, limit);
        Map.Entry size = _eventSizeCache.getUnchecked(sizeCacheKey);
        if (size.getValue() >= limit) { // We have the same or better estimate
            return size.getKey();
        }
        // User wants a better estimate than what our cache has, so we need to invalidate this key and load a new size
        _eventSizeCache.invalidate(sizeCacheKey);
        return _eventSizeCache.getUnchecked(sizeCacheKey).getKey();
    }

    private long internalEventCountUpTo(String subscription, long limit) {
        checkLegalSubscriptionName(subscription);
        checkArgument(limit > 0, "Limit must be >0");

        return _eventStore.getSizeEstimate(subscription, limit);
    }

    @Override
    public long getClaimCount(String subscription) {
        checkLegalSubscriptionName(subscription);

        return _eventStore.getClaimCount(subscription);
    }

    @Override
    public List peek(final String subscription, int limit) {
        checkLegalSubscriptionName(subscription);
        checkArgument(limit > 0, "Limit must be >0");

        List events = peekOrPoll(subscription, null, limit);
        _peekedMeter.mark(events.size());
        return events;
    }

    @Override
    public List poll(final String subscription, final Duration claimTtl, int limit) {
        checkLegalSubscriptionName(subscription);
        checkArgument(claimTtl.getMillis() >= 0, "ClaimTtl must be >=0");
        checkArgument(limit > 0, "Limit must be >0");

        List events = peekOrPoll(subscription, claimTtl, limit);
        _polledMeter.mark(events.size());
        return events;
    }

    /** Implements peek() or poll() based on whether claimTtl is null or non-null. */
    private List peekOrPoll(String subscription, @Nullable Duration claimTtl, int limit) {
        List items = Lists.newArrayList();
        Map uniqueItems = Maps.newHashMap();
        Map eventOrder = Maps.newHashMap();
        boolean repeatable = claimTtl != null && claimTtl.getMillis() > 0;

        Stopwatch stopwatch = Stopwatch.createStarted(_ticker);
        int padding = 0;
        do {
            int remaining = limit - items.size();
            if (remaining == 0) {
                break;  // Don't need any more events.
            }

            // Query the databus event store.  Consolidate multiple events that refer to the same item.
            ConsolidatingEventSink sink = new ConsolidatingEventSink(remaining + padding);
            boolean more = (claimTtl == null) ?
                    _eventStore.peek(subscription, sink) :
                    _eventStore.poll(subscription, claimTtl, sink);
            Map rawEvents = sink.getEvents();

            if (rawEvents.isEmpty()) {
                break;  // No events to be had.
            }

            List eventIdsToDiscard = Lists.newArrayList();
            List recentUnknownEventIds = Lists.newArrayList();
            List eventIdsToUnclaim = Lists.newArrayList();

            // Query the events from the data store in batch to reduce latency.
            DataProvider.AnnotatedGet annotatedGet = _dataProvider.prepareGetAnnotated(ReadConsistency.STRONG);
            for (Map.Entry entry : rawEvents.entrySet()) {
                Coordinate coord = entry.getKey();

                // Query the table/key pair.
                try {
                    annotatedGet.add(coord.getTable(), coord.getId());
                } catch (UnknownTableException e) {
                    // It's likely the table was dropped since the event was queued.  Discard the events.
                    EventList list = entry.getValue();
                    for (Pair pair : list.getEventAndChangeIds()) {
                        eventIdsToDiscard.add(pair.first());
                    }
                    _discardedMeter.mark(list.size());
                }

                // Keep track of the order in which we received the events from the EventStore.
                if (!eventOrder.containsKey(coord)) {
                    eventOrder.put(coord, eventOrder.size());
                }
            }
            Iterator readResultIter = annotatedGet.execute();

            // Loop through the results of the data store query.
            while (readResultIter.hasNext()) {
                DataProvider.AnnotatedContent readResult = readResultIter.next();

                // Get the JSON System of Record entity for this piece of content
                Map content = readResult.getContent();

                // Find the original event IDs that correspond to this piece of content
                Coordinate coord = Coordinate.fromJson(content);
                EventList eventList = rawEvents.get(coord);

                // Get all databus event tags for the original event(s) for this coordinate
                List> tags = eventList.getTags();

                // Loop over the Databus events for this piece of content.  Usually there's just one, but not always...
                for (Pair eventData : eventList.getEventAndChangeIds()) {
                    String eventId = eventData.first();
                    UUID changeId = eventData.second();

                    // Has the content replicated yet?  If not, abandon the event and we'll try again when the claim expires.
                    if (readResult.isChangeDeltaPending(changeId)) {
                        if (isRecent(changeId)) {
                            recentUnknownEventIds.add(eventId);
                            _recentUnknownMeter.mark();
                        } else {
                            _staleUnknownMeter.mark();
                        }
                        continue;
                    }

                    // Is the change redundant?  If so, no need to fire databus events for it.  Ack it now.
                    if (readResult.isChangeDeltaRedundant(changeId)) {
                        eventIdsToDiscard.add(eventId);
                        _redundantMeter.mark();
                        continue;
                    }

                    // Check whether we've already added this piece of content to the poll result.  If so, consolidate
                    // the two together to reduce the amount of work a client must do.  Note that the previous item
                    // might be from a previous batch of events and it's possible that we have read two different
                    // versions of the same item of content.  This will prefer the most recent.
                    Item previousItem = uniqueItems.get(coord);
                    if (previousItem != null && previousItem.consolidateWith(eventId, content, tags)) {
                        _consolidatedMeter.mark();
                        continue;
                    }

                    // If, due to "padding" we asked for too many events, release the claims for the next poll().
                    if (items.size() == limit) {
                        eventIdsToUnclaim.add(eventId);
                        continue;
                    }

                    // We have found a new item of content to return!
                    Item item = new Item(eventId, eventOrder.get(coord), content, tags);
                    items.add(item);
                    uniqueItems.put(coord, item);
                }
            }

            // Abandon claims when we claimed more items than necessary to satisfy the specified limit.
            if (!eventIdsToUnclaim.isEmpty()) {
                _eventStore.renew(subscription, eventIdsToUnclaim, Duration.ZERO, false);
            }
            // Reduce the claim length on recent unknown IDs so we look for them again soon.
            if (!recentUnknownEventIds.isEmpty()) {
                _eventStore.renew(subscription, recentUnknownEventIds, RECENT_UNKNOWN_RETRY, false);
            }
            // Ack events we never again want to see.
            if (!eventIdsToDiscard.isEmpty()) {
                _eventStore.delete(subscription, eventIdsToDiscard, true);
            }
            if (!more) {
                break;  // Didn't get a full batch, that means there are no more events to be had.
            }

            // Note: Due to redundant/unknown events, it's possible that the 'events' list is empty even though, if we
            // tried again, we'd find more events.  Try again a few times, but not for more than 250ms so clients don't
            // timeout the request.  This helps move through large amounts of redundant deltas relatively quickly while
            // also putting a bound on the total amount of work done by a single call to poll().
            padding = 10;
        } while (repeatable && stopwatch.elapsed(TimeUnit.MILLISECONDS) <  MAX_POLL_TIME.getMillis());

        // Sort the items to match the order of their events in an attempt to get first-in-first-out.
        Collections.sort(items);

        // Return the final list of events.
        List events = Lists.newArrayListWithCapacity(items.size());
        for (Item item : items) {
            events.add(item.toEvent());
        }
        return events;
    }

    private boolean isRecent(UUID changeId) {
        return _clock.millis() - TimeUUIDs.getTimeMillis(changeId) < STALE_UNKNOWN_AGE.getMillis();
    }

    @Override
    public void renew(String subscription, Collection eventKeys, Duration claimTtl) {
        checkLegalSubscriptionName(subscription);
        checkNotNull(eventKeys, "eventKeys");
        checkArgument(claimTtl.getMillis() >= 0, "ClaimTtl must be >=0");

        _eventStore.renew(subscription, EventKeyFormat.decodeAll(eventKeys), claimTtl, true);
        _renewedMeter.mark(eventKeys.size());
    }

    @Override
    public void acknowledge(String subscription, Collection eventKeys) {
        checkLegalSubscriptionName(subscription);
        checkNotNull(eventKeys, "eventKeys");

        _eventStore.delete(subscription, EventKeyFormat.decodeAll(eventKeys), true);
        _ackedMeter.mark(eventKeys.size());
    }

    @Override
    public String replayAsync(String subscription) {
        return replayAsyncSince(subscription, null);
    }

    @Override
    public String replayAsyncSince(String subscription, Date since) {
        checkLegalSubscriptionName(subscription);

        JobIdentifier jobId =
                _jobService.submitJob(
                        new JobRequest<>(ReplaySubscriptionJob.INSTANCE, new ReplaySubscriptionRequest(subscription, since)));

        return jobId.toString();
    }

    public void replay(String subscription, Date since) {
        // Make sure since is within Replay TTL
        checkState(since == null || new DateTime(since).plus(DatabusChannelConfiguration.REPLAY_TTL).isAfterNow(),
                "Since timestamp is outside the replay TTL.");
        String source = ChannelNames.getMasterReplayChannel();
        final Subscription destination = getSubscription(subscription);

        _eventStore.copy(source, subscription, new Predicate() {
            @Override
            public boolean apply(ByteBuffer eventData) {
                return _subscriptionEvaluator.matches(destination, eventData);
            }
        }, since);
    }

    @Override
    public ReplaySubscriptionStatus getReplayStatus(String reference) {
        checkNotNull(reference, "reference");

        JobIdentifier jobId;
        try {
            jobId = JobIdentifier.fromString(reference, ReplaySubscriptionJob.INSTANCE);
        } catch (IllegalArgumentException e) {
            // The reference is illegal and therefore cannot match any replay jobs.
            throw new UnknownReplayException(reference);
        }

        JobStatus status = _jobService.getJobStatus(jobId);

        if (status == null) {
            throw new UnknownReplayException(reference);
        }

        ReplaySubscriptionRequest request = status.getRequest();
        if (request == null) {
            throw new IllegalStateException("Replay request details not found: " + jobId);
        }

        switch (status.getStatus()) {
            case FINISHED:
                return new ReplaySubscriptionStatus(request.getSubscription(), ReplaySubscriptionStatus.Status.COMPLETE);

            case FAILED:
                return new ReplaySubscriptionStatus(request.getSubscription(), ReplaySubscriptionStatus.Status.ERROR);

            default:
                return new ReplaySubscriptionStatus(request.getSubscription(), ReplaySubscriptionStatus.Status.IN_PROGRESS);
        }
    }

    @Override
    public String moveAsync(String from, String to) {
        checkLegalSubscriptionName(from);
        checkLegalSubscriptionName(to);

        JobIdentifier jobId =
                _jobService.submitJob(new JobRequest<>(MoveSubscriptionJob.INSTANCE, new MoveSubscriptionRequest(from, to)));

        return jobId.toString();
    }

    @Override
    public MoveSubscriptionStatus getMoveStatus(String reference) {
        checkNotNull(reference, "reference");

        JobIdentifier jobId;
        try {
            jobId = JobIdentifier.fromString(reference, MoveSubscriptionJob.INSTANCE);
        } catch (IllegalArgumentException e) {
            // The reference is illegal and therefore cannot match any move jobs.
            throw new UnknownMoveException(reference);
        }

        JobStatus status = _jobService.getJobStatus(jobId);

        if (status == null) {
            throw new UnknownMoveException(reference);
        }

        MoveSubscriptionRequest request = status.getRequest();
        if (request == null) {
            throw new IllegalStateException("Move request details not found: " + jobId);
        }

        switch (status.getStatus()) {
            case FINISHED:
                return new MoveSubscriptionStatus(request.getFrom(), request.getTo(), MoveSubscriptionStatus.Status.COMPLETE);

            case FAILED:
                return new MoveSubscriptionStatus(request.getFrom(), request.getTo(), MoveSubscriptionStatus.Status.ERROR);

            default:
                return new MoveSubscriptionStatus(request.getFrom(), request.getTo(), MoveSubscriptionStatus.Status.IN_PROGRESS);
        }
    }

    @Override
    public void injectEvent(String subscription, String table, String key) {
        // Pick a changeId UUID that's guaranteed to be older than the compaction cutoff so poll()'s calls to
        // AnnotatedContent.isChangeDeltaPending() and isChangeDeltaRedundant() will always return false.
        UpdateRef ref = new UpdateRef(table, key, TimeUUIDs.minimumUuid(), ImmutableSet.of());
        _eventStore.add(subscription, UpdateRefSerializer.toByteBuffer(ref));
    }

    @Override
    public void unclaimAll(String subscription) {
        checkLegalSubscriptionName(subscription);

        _eventStore.unclaimAll(subscription);
    }

    @Override
    public void purge(String subscription) {
        checkLegalSubscriptionName(subscription);

        _eventStore.purge(subscription);
    }

    private void checkLegalSubscriptionName(String subscription) {
        checkArgument(Names.isLegalSubscriptionName(subscription),
                "Subscription name must be a lowercase ASCII string between 1 and 255 characters in length. " +
                        "Allowed punctuation characters are -.:@_ and the subscription name may not start with a single underscore character. " +
                        "An example of a valid subscription name would be 'polloi:review'.");
    }

    /** EventStore sink that doesn't count adjacent events for the same table/key against the peek/poll limit. */
    private class ConsolidatingEventSink implements EventSink {
        private final Map _eventMap = Maps.newLinkedHashMap();
        private final int _limit;

        ConsolidatingEventSink(int limit) {
            _limit = limit;
        }

        @Override
        public int remaining() {
            // Go a bit past the desired limit to maximize opportunity to consolidate events.  Otherwise, for example,
            // if limit was 1, we'd stop after the first event and not consolidate anything.
            return _limit - _eventMap.size() + 1;
        }

        @Override
        public Status accept(EventData rawEvent) {
            // Parse the raw event data into table/key/changeId/tags.
            UpdateRef ref = UpdateRefSerializer.fromByteBuffer(rawEvent.getData());

            // Consolidate events that refer to the same item.
            Coordinate contentKey = Coordinate.of(ref.getTable(), ref.getKey());
            EventList eventList = _eventMap.get(contentKey);
            if (eventList == null) {
                if (_eventMap.size() == _limit) {
                    return Status.REJECTED_STOP;
                }
                _eventMap.put(contentKey, eventList = new EventList());
            }

            eventList.add(rawEvent.getId(), ref.getChangeId(), ref.getTags());

            if (eventList.size() == MAX_EVENTS_TO_CONSOLIDATE) {
                return Status.ACCEPTED_STOP;
            }

            return Status.ACCEPTED_CONTINUE;
        }

        Map getEvents() {
            return _eventMap;
        }
    }

    private static class EventList {
        private final List> _eventAndChangeIds = Lists.newArrayList();
        private List> _tags;

        void add(String eventId, UUID changeId, Set tags) {
            _eventAndChangeIds.add(Pair.of(eventId, changeId));
            _tags = sortedTagUnion(_tags, tags);
        }

        List> getEventAndChangeIds() {
            return _eventAndChangeIds;
        }

        List> getTags() {
            return _tags;
        }

        int size() {
            return _eventAndChangeIds.size();
        }
    }

    private static class Item implements Comparable {
        private final List _consolidatedEventIds;
        private final int _sortIndex;
        private Map _content;
        private List> _tags;

        Item(String eventId, int sortIndex, Map content, List> tags) {
            _consolidatedEventIds = Lists.newArrayList(eventId);
            _sortIndex = sortIndex;
            _content = content;
            _tags = tags;
        }

        boolean consolidateWith(String eventId, Map content, List> tags) {
            if (_consolidatedEventIds.size() >= MAX_EVENTS_TO_CONSOLIDATE) {
                return false;
            }

            // We'll construct an event key that combines all the duplicate events.  Unfortunately we can't discard/ack
            // the extra events at this time, subtle race conditions result that can cause clients to miss events.
            _consolidatedEventIds.add(eventId);

            // Pick the newest version of the content.  There's no reason to return stale stuff.
            if (Intrinsic.getVersion(_content) < Intrinsic.getVersion(content)) {
                _content = content;
            }

            // Combine tags from the other event
            for (List tagList : tags) {
                // The contents of "tagList" are already sorted, no need to sort again.
                _tags = sortedTagUnion(_tags, tagList);
            }

            return true;
        }

        Event toEvent() {
            Collections.sort(_consolidatedEventIds);
            // Tags are already sorted

            return new Event(EventKeyFormat.encode(_consolidatedEventIds), _content, _tags);
        }

        @Override
        public int compareTo(Item item) {
            return Ints.compare(_sortIndex, item._sortIndex);
        }
    }

    // Conserve memory by having a singleton set to represent a set containing a single empty set of tags
    private final static List> EMPTY_TAGS = ImmutableList.>of(ImmutableList.of());

    // Simple comparator for comparing lists of pre-sorted lists
    private final static Comparator> TAG_LIST_COMPARATOR = new Comparator>() {
        @Override
        public int compare(List o1, List o2) {
            int l1 = o1.size();
            int l2 = o2.size();
            int l = Math.min(l1, l2);
            for (int i=0; i < l; i++) {
                int c = o1.get(i).compareTo(o2.get(i));
                if (c != 0) {
                    return c;
                }
            }

            // Shorter list sorts first
            return Integer.compare(l1, l2);
        }
    };

    /**
     * Produces a list-of-lists for all unique tag sets for a given databus event.  existingTags must either
     * be null or the result of a previous call to {@link #sortedTagUnion(java.util.List, java.util.List)} or
     * {@link #sortedTagUnion(java.util.List, java.util.Set)}.
     */
    private static List> sortedTagUnion(@Nullable List> existingTags, Set newTagSet) {
       return sortedTagUnion(existingTags, asSortedList(newTagSet));
    }

    /**
     * Produces a list-of-lists for all unique tag sets for a given databus event.  existingTags must either
     * be null or the result of a previous call to {@link #sortedTagUnion(java.util.List, java.util.List)} or
     * {@link #sortedTagUnion(java.util.List, java.util.Set)}.  Additionally the contents of sortedNewTagList
     * must already be sorted prior to this method call.
     */
    private static List> sortedTagUnion(@Nullable List> existingTags, List sortedNewTagList) {

        // Optimize for the common case where for each coordinate there is exactly one event or that all events
        // use the same tags (typically the empty set)
        if (existingTags == null) {
            if (sortedNewTagList.isEmpty()) {
                return EMPTY_TAGS;
            }
            existingTags = Lists.newArrayListWithCapacity(3);
            existingTags.add(sortedNewTagList);
            return existingTags;
        }

        int insertionPoint = Collections.binarySearch(existingTags, sortedNewTagList, TAG_LIST_COMPARATOR);
        if (insertionPoint >= 0) {
            // Existing tags already includes this set of tags
            return existingTags;
        }

        if (existingTags == EMPTY_TAGS) {
            // Can't update the default empty tag set.  Make a copy.
            existingTags = Lists.newArrayListWithCapacity(3);
            existingTags.addAll(EMPTY_TAGS);
        }

        insertionPoint = -insertionPoint - 1;
        existingTags.add(insertionPoint, sortedNewTagList);
        return existingTags;
    }

    private static List asSortedList(Set tags) {
        switch (tags.size()) {
            case 0:
                return ImmutableList.of();

            case 1:
                return ImmutableList.of(tags.iterator().next());

            default:
                return Ordering.natural().immutableSortedCopy(tags);
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy