com.github.rahulsom.grooves.queries.TemporalQuerySupport Maven / Gradle / Ivy
package com.github.rahulsom.grooves.queries;
import com.github.rahulsom.grooves.api.AggregateType;
import com.github.rahulsom.grooves.api.GroovesException;
import com.github.rahulsom.grooves.api.events.BaseEvent;
import com.github.rahulsom.grooves.api.events.Deprecates;
import com.github.rahulsom.grooves.api.events.RevertEvent;
import com.github.rahulsom.grooves.api.snapshots.TemporalSnapshot;
import com.github.rahulsom.grooves.queries.internal.*;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static com.github.rahulsom.grooves.queries.internal.Utils.returnOrRedirect;
import static com.github.rahulsom.grooves.queries.internal.Utils.stringify;
import static io.reactivex.Flowable.*;
import static java.util.stream.Collectors.toList;
/**
* Default interface to help in building temporal snapshots.
*
* @param The type of {@link AggregateT}'s id
* @param The aggregate over which the query executes
* @param The type of the {@link EventT}'s id field
* @param The type of the Event
* @param The type of the {@link SnapshotT}'s id field
* @param The type of the Snapshot
* @param A reference to the query type. Typically a self reference.
*
* @author Rahul Somasunderam
*/
public interface TemporalQuerySupport<
AggregateIdT,
AggregateT extends AggregateType,
EventIdT,
EventT extends BaseEvent,
SnapshotIdT,
SnapshotT extends TemporalSnapshot,
QueryT extends BaseQuery>
extends
BaseQuery {
/**
* Finds the last usable snapshot. For a given maxTimestamp, finds a snapshot whose last event
* is older than timestamp so a new one can be incrementally computed if possible.
*
* @param aggregate The aggregate for which the latest snapshot is desired
* @param maxTimestamp The max last event timestamp allowed for the snapshot
*
* @return An Flowable that returns at most one snapshot
*/
default Flowable getLastUsableSnapshot(
final AggregateT aggregate, Date maxTimestamp) {
return fromPublisher(getSnapshot(maxTimestamp, aggregate))
.defaultIfEmpty(createEmptySnapshot())
.doOnNext(it -> {
getLog().debug(" -> Last Usable Snapshot: {}",
it.getLastEventTimestamp() == null ? "" : it.toString());
it.setAggregate(aggregate);
});
}
/**
* Given a timestamp, finds the latest snapshot older than that timestamp, and events between
* the snapshot and the desired timestamp.
*
* @param aggregate The aggregate for which such data is desired
* @param moment The maximum timestamp of the last event
*
* @return A Tuple containing the snapshot and the events
*/
default Flowable>> getSnapshotAndEventsSince(
AggregateT aggregate, Date moment) {
return getSnapshotAndEventsSince(aggregate, moment, true);
}
/**
* Given a timestamp, finds the latest snapshot older than that timestamp, and events between
* the snapshot and the desired timestamp.
*
* @param aggregate The aggregate for which such data is desired
* @param moment The moment for the desired snapshot
* @param reuseEarlierSnapshot Whether earlier snapshots can be reused for this computation. It
* is generally a good idea to set this to true unless there are
* known reverts that demand this be set to false.
*
* @return A Tuple containing the snapshot and the events
*/
default Flowable>> getSnapshotAndEventsSince(
AggregateT aggregate, Date moment, boolean reuseEarlierSnapshot) {
if (reuseEarlierSnapshot) {
return getLastUsableSnapshot(aggregate, moment).flatMap(lastSnapshot ->
fromPublisher(getUncomputedEvents(aggregate, lastSnapshot, moment))
.toList()
.toFlowable()
.flatMap(events -> {
if (events.stream().anyMatch(it -> it instanceof RevertEvent)) {
List reverts = events.stream()
.filter(it -> it instanceof RevertEvent)
.collect(toList());
getLog().info(" Uncomputed reverts exist: {}",
stringify(reverts));
return getSnapshotAndEventsSince(
aggregate, moment, false);
} else {
getLog().debug(" Events since last snapshot: {}",
stringify(events));
return just(new Pair<>(lastSnapshot, events));
}
}));
} else {
SnapshotT lastSnapshot = createEmptySnapshot();
final Flowable> uncomputedEvents =
fromPublisher(getUncomputedEvents(aggregate, lastSnapshot, moment))
.toList()
.toFlowable();
return uncomputedEvents
.doOnNext(ue ->
getLog().debug(" Events since origin: {}", stringify(ue)))
.map(ue -> new Pair<>(lastSnapshot, ue));
}
}
/**
* Computes a snapshot for specified version of an aggregate.
*
* @param aggregate The aggregate
* @param moment The moment at which the snapshot is desired
*
* @return An Flowable that returns at most one Snapshot
*/
default Publisher computeSnapshot(AggregateT aggregate, Date moment) {
return computeSnapshot(aggregate, moment, true);
}
/**
* Computes a snapshot for specified version of an aggregate.
*
* @param aggregate The aggregate
* @param moment The moment at which the snapshot is desired
* @param redirect If there has been a deprecation, redirect to the current aggregate's
* snapshot. Defaults to true.
*
* @return An Optional SnapshotType. Empty if cannot be computed.
*/
default Publisher computeSnapshot(
AggregateT aggregate, Date moment, boolean redirect) {
if (getLog().isInfoEnabled()) {
getLog().info("Computing snapshot for {} at {}", aggregate,
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(moment));
}
return getSnapshotAndEventsSince(aggregate, moment).flatMap(seTuple2 -> {
List events = seTuple2.getSecond();
SnapshotT snapshot = seTuple2.getFirst();
getLog().info("Events: {}", events);
if (events.stream().anyMatch(it -> it instanceof RevertEvent)) {
return fromPublisher(snapshot.getAggregateObservable())
.flatMap(aggregate1 ->
aggregate1 == null ?
computeSnapshotAndEvents(
aggregate, moment, redirect, events, snapshot) :
empty())
.map(Flowable::just)
.defaultIfEmpty(computeSnapshotAndEvents(
aggregate, moment, redirect, events, snapshot))
.flatMap(it -> it)
;
} else {
return computeSnapshotAndEvents(aggregate, moment, redirect, events, snapshot)
;
}
});
}
/**
* Computes snapshot and events based on the last usable snapshot.
*
* @param aggregate The aggregate on which we are working
* @param moment The moment for which we desire a snapshot
* @param redirect Whether a redirect should be performed if the aggregate has been
* deprecated by another aggregate
* @param events The list of events
* @param lastUsableSnapshot The last known usable snapshot
*
* @return An observable of the snapshot
*/
default Flowable computeSnapshotAndEvents(
AggregateT aggregate,
Date moment,
boolean redirect,
List events,
SnapshotT lastUsableSnapshot) {
lastUsableSnapshot.setAggregate(aggregate);
Flowable forwardOnlyEvents = Utils.getForwardOnlyEvents(events, getExecutor(),
() -> getSnapshotAndEventsSince(aggregate, moment, false));
Flowable applicableEvents = forwardOnlyEvents
.filter(e -> e instanceof Deprecates)
.toList()
.toFlowable()
.flatMap(list -> {
if (list.isEmpty()) {
return forwardOnlyEvents;
} else {
Flowable>> snapshotAndEventsSince =
getSnapshotAndEventsSince(aggregate, moment, false);
return snapshotAndEventsSince.flatMap(p -> Utils.getForwardOnlyEvents(
p.getSecond(),
getExecutor(),
() -> error(new GroovesException(
"Couldn't apply deprecates events"))));
}
});
final Flowable snapshotTypeObservable =
getExecutor().applyEvents((QueryT) this, lastUsableSnapshot, applicableEvents,
new ArrayList<>(), aggregate);
return snapshotTypeObservable
.doOnNext(snapshot -> {
if (!events.isEmpty()) {
snapshot.setLastEvent(events.get(events.size() - 1));
}
getLog().info(" --> Computed: {}", snapshot);
})
.flatMap(it -> returnOrRedirect(redirect, events, it,
() -> fromPublisher(it.getDeprecatedByObservable())
.flatMap(x -> fromPublisher(computeSnapshot(x, moment)))
));
}
default Executor getExecutor() {
return new QueryExecutor<>();
}
@Override
default Publisher findEventsBefore(EventT event) {
return fromPublisher(event.getAggregateObservable())
.flatMap(aggregate ->
fromPublisher(getUncomputedEvents(aggregate, null, event.getTimestamp())))
;
}
Publisher getUncomputedEvents(
AggregateT aggregate, SnapshotT lastSnapshot, Date snapshotTime);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy