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 rx.Observable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;
import static com.github.rahulsom.grooves.queries.internal.Utils.returnOrRedirect;
import static com.github.rahulsom.grooves.queries.internal.Utils.stringify;
import static java.util.stream.Collectors.toList;
import static rx.Observable.empty;
import static rx.Observable.error;
import static rx.Observable.just;
/**
* 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 Observable that returns at most one snapshot
*/
default Observable getLastUsableSnapshot(
final AggregateT aggregate, Date maxTimestamp) {
return 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 Observable>> 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 Observable>> getSnapshotAndEventsSince(
AggregateT aggregate, Date moment, boolean reuseEarlierSnapshot) {
if (reuseEarlierSnapshot) {
return getLastUsableSnapshot(aggregate, moment).flatMap(lastSnapshot ->
getUncomputedEvents(aggregate, lastSnapshot, moment).toList()
.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 Observable> uncomputedEvents =
getUncomputedEvents(aggregate, lastSnapshot, moment)
.toList();
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 Observable that returns at most one Snapshot
*/
default Observable 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 Observable 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 snapshot
.getAggregateObservable().flatMap(aggregate1 ->
aggregate1 == null ?
computeSnapshotAndEvents(
aggregate, moment, redirect, events, snapshot) :
empty())
.map(Observable::just)
.defaultIfEmpty(computeSnapshotAndEvents(
aggregate, moment, redirect, events, snapshot))
.flatMap(it -> it);
}
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 Observable computeSnapshotAndEvents(
AggregateT aggregate,
Date moment,
boolean redirect,
List events,
SnapshotT lastUsableSnapshot) {
lastUsableSnapshot.setAggregate(aggregate);
Observable forwardOnlyEvents = Utils.getForwardOnlyEvents(events, getExecutor(),
() -> getSnapshotAndEventsSince(aggregate, moment, false));
Observable applicableEvents = forwardOnlyEvents
.filter(e -> e instanceof Deprecates)
.toList()
.flatMap(list -> {
if (list.isEmpty()) {
return forwardOnlyEvents;
} else {
Observable>> snapshotAndEventsSince =
getSnapshotAndEventsSince(aggregate, moment, false);
return snapshotAndEventsSince.flatMap(p -> Utils.getForwardOnlyEvents(
p.getSecond(), getExecutor(), () -> error(
new GroovesException("Couldn't apply deprecates events"))));
}
});
final Observable 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,
() -> it.getDeprecatedByObservable()
.flatMap(x -> computeSnapshot(x, moment))
));
}
default Executor getExecutor() {
return new QueryExecutor<>();
}
@Override
default Observable findEventsBefore(EventT event) {
return event.getAggregateObservable().flatMap(aggregate ->
getUncomputedEvents(aggregate, null, event.getTimestamp())
);
}
Observable getUncomputedEvents(
AggregateT aggregate, SnapshotT lastSnapshot, Date snapshotTime);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy