/* * Copyright (c) 2010-2016. Axon Framework * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.axonframework.eventsourcing.eventstore; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.axonframework.common.Assert; import java.io.Serializable; import java.util.*; import java.util.stream.LongStream; /** * Implementation of a {@link TrackingToken} that uses the global insertion sequence number of the event to determine * tracking order and additionally stores a set of possible gaps that have been detected while tracking the event * store. * <p> * By storing the sequence numbers of gaps, i.e. sequence numbers of events that may have been inserted but have not * been committed to the store, consumers are able to track the event store uninterruptedly even when there are gaps * in the sequence numbers of events. If a gap is detected the event store can check if meanwhile this gap has been * filled each time a new batch of events is fetched. * * @author Rene de Waele */ public class GapAwareTrackingToken implements TrackingToken, Serializable { private final long index; private final SortedSet<Long> gaps; /** * Returns a new {@link GapAwareTrackingToken} instance based on the given {@code index} and collection of {@code * gaps}. * * @param index the highest global sequence number of events up until (and including) this tracking token * @param gaps global sequence numbers of events that have not been seen yet even though these sequence numbers are * smaller than the current index. These missing sequence numbers may be filled in later when those * events get committed to the store or may never be filled in if those events never get committed. * @return a new tracking token from given index and gaps */ @JsonCreator public static GapAwareTrackingToken newInstance(@JsonProperty("index") long index, @JsonProperty("gaps") Collection<Long> gaps) { if (gaps.isEmpty()) { return new GapAwareTrackingToken(index, Collections.emptySortedSet()); } SortedSet<Long> gapSet = new TreeSet<>(gaps); Assert.isTrue(gapSet.last() < index, () -> String.format("Gap indices [%s] should all be smaller than head index [%d]", gaps, index)); return new GapAwareTrackingToken(index, gapSet); } private GapAwareTrackingToken(long index, SortedSet<Long> gaps) { this.index = index; this.gaps = gaps; } /** * Returns a new {@link GapAwareTrackingToken} instance based on this token but which has advanced to given {@code * index}. Gaps that have fallen behind the index by more than the {@code maxGapOffset} will not be included in the * new token. * <p> * Note that the given {@code index} should be one of the current token's gaps or be higher than the current token's * index. * * @param index the global sequence number of the next event * @param maxGapOffset the maximum distance between a gap and the token's index * @return the new token that has advanced from the current token */ public GapAwareTrackingToken advanceTo(long index, int maxGapOffset) { long newIndex; SortedSet<Long> gaps = new TreeSet<>(this.gaps); if (gaps.remove(index)) { newIndex = this.index; } else if (index > this.index) { newIndex = index; LongStream.range(this.index + 1L, index).forEach(gaps::add); } else { throw new IllegalArgumentException(String.format( "The given index [%d] should be larger than the token index [%d] or be one of the token's gaps [%s]", index, this.index, gaps)); } gaps = gaps.tailSet(newIndex - maxGapOffset); return new GapAwareTrackingToken(newIndex, gaps); } /** * Get the highest global sequence of events seen up until the point of this tracking token. * * @return the highest global event sequence number seen so far */ public long getIndex() { return index; } /** * Get a {@link SortedSet} of this token's gaps. * * @return the gaps of this token */ public SortedSet<Long> getGaps() { return Collections.unmodifiableSortedSet(gaps); } /** * Check if this token contains one ore more gaps. * * @return {@code true} if this token contains gaps, {@code false} otherwise */ public boolean hasGaps() { return !gaps.isEmpty(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } GapAwareTrackingToken that = (GapAwareTrackingToken) o; return index == that.index && Objects.equals(gaps, that.gaps); } @Override public int hashCode() { return Objects.hash(index, gaps); } @Override public String toString() { return "GapAwareTrackingToken{" + "index=" + index + ", gaps=" + gaps + '}'; } }