+per #16541 add missing java samples for persistence query

This commit is contained in:
Konrad Malawski 2015-06-08 12:26:19 +02:00 committed by Konrad Malawski
parent 3b94108e0c
commit 3314de4cb9
10 changed files with 589 additions and 92 deletions

View file

@ -4,71 +4,373 @@
package docs.persistence;
import static akka.pattern.Patterns.ask;
import akka.actor.*;
import akka.dispatch.Mapper;
import akka.event.EventStreamSpec;
import akka.japi.Function;
import akka.japi.Procedure;
import akka.japi.pf.ReceiveBuilder;
import akka.pattern.BackoffSupervisor;
import akka.persistence.*;
import akka.persistence.query.*;
import akka.persistence.query.javadsl.ReadJournal;
import akka.stream.ActorMaterializer;
import akka.stream.javadsl.Sink;
import akka.stream.javadsl.Source;
import akka.util.Timeout;
import docs.persistence.query.MyEventsByTagPublisher;
import docs.persistence.query.PersistenceQueryDocSpec;
import org.reactivestreams.Subscriber;
import scala.collection.Seq;
import scala.collection.immutable.Vector;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;
import scala.concurrent.duration.FiniteDuration;
import scala.runtime.Boxed;
import scala.runtime.BoxedUnit;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class PersistenceQueryDocTest {
final Timeout timeout = Timeout.durationToTimeout(FiniteDuration.create(3, TimeUnit.SECONDS));
final ActorSystem system = ActorSystem.create();
final ActorMaterializer mat = ActorMaterializer.create(system);
//#my-read-journal
class MyReadJournal implements ReadJournal {
private final ExtendedActorSystem system;
class MyReadJournal implements ReadJournal {
private final ExtendedActorSystem system;
public MyReadJournal(ExtendedActorSystem system) {
this.system = system;
}
final FiniteDuration defaultRefreshInterval = FiniteDuration.create(3, TimeUnit.SECONDS);
final FiniteDuration defaultRefreshInterval = FiniteDuration.create(3, TimeUnit.SECONDS);
@SuppressWarnings("unchecked")
public <T, M> Source<T, M> query(Query<T, M> q, Hint... hints) {
if (q instanceof EventsByTag) {
final EventsByTag eventsByTag = (EventsByTag) q;
final String tag = eventsByTag.tag();
long offset = eventsByTag.offset();
@SuppressWarnings("unchecked")
public <T, M> Source<T, M> query(Query<T, M> q, Hint... hints) {
if (q instanceof EventsByTag) {
final EventsByTag eventsByTag = (EventsByTag) q;
final String tag = eventsByTag.tag();
long offset = eventsByTag.offset();
final Props props = MyEventsByTagPublisher.props(tag, offset, refreshInterval(hints));
final Props props = MyEventsByTagPublisher.props(tag, offset, refreshInterval(hints));
return (Source<T, M>) Source.<EventEnvelope>actorPublisher(props)
.mapMaterializedValue(noMaterializedValue());
} else {
// unsuported
return Source.<T>failed(
new UnsupportedOperationException(
"Query $unsupported not supported by " + getClass().getName()))
.mapMaterializedValue(noMaterializedValue());
}
return (Source<T, M>) Source.<EventEnvelope>actorPublisher(props)
.mapMaterializedValue(noMaterializedValue());
} else {
// unsuported
return Source.<T>failed(
new UnsupportedOperationException(
"Query " + q + " not supported by " + getClass().getName()))
.mapMaterializedValue(noMaterializedValue());
}
}
private FiniteDuration refreshInterval(Hint[] hints) {
FiniteDuration ret = defaultRefreshInterval;
for (Hint hint : hints)
if (hint instanceof RefreshInterval)
ret = ((RefreshInterval) hint).interval();
return ret;
}
private FiniteDuration refreshInterval(Hint[] hints) {
for (Hint hint : hints)
if (hint instanceof RefreshInterval)
return ((RefreshInterval) hint).interval();
private <I, M> akka.japi.function.Function<I, M> noMaterializedValue () {
return param -> (M) null;
}
return defaultRefreshInterval;
}
private <I, M> akka.japi.function.Function<I, M> noMaterializedValue() {
return param -> (M) null;
}
}
//#my-read-journal
//#my-read-journal
void demonstrateBasicUsage() {
final ActorSystem system = ActorSystem.create();
//#basic-usage
// obtain read journal by plugin id
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
// issue query to journal
Source<Object, BoxedUnit> source =
readJournal.query(EventsByPersistenceId.create("user-1337", 0, Long.MAX_VALUE));
// materialize stream, consuming events
ActorMaterializer mat = ActorMaterializer.create(system);
source.runForeach(event -> System.out.println("Event: " + event), mat);
//#basic-usage
}
void demonstrateAllPersistenceIdsLive() {
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#all-persistence-ids-live
readJournal.query(AllPersistenceIds.getInstance());
//#all-persistence-ids-live
}
void demonstrateNoRefresh() {
final ActorSystem system = ActorSystem.create();
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#all-persistence-ids-snap
readJournal.query(AllPersistenceIds.getInstance(), NoRefresh.getInstance());
//#all-persistence-ids-snap
}
void demonstrateRefresh() {
final ActorSystem system = ActorSystem.create();
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#events-by-persistent-id-refresh
final RefreshInterval refresh = RefreshInterval.create(1, TimeUnit.SECONDS);
readJournal.query(EventsByPersistenceId.create("user-us-1337"), refresh);
//#events-by-persistent-id-refresh
}
void demonstrateEventsByTag() {
final ActorSystem system = ActorSystem.create();
final ActorMaterializer mat = ActorMaterializer.create(system);
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#events-by-tag
// assuming journal is able to work with numeric offsets we can:
final Source<EventEnvelope, BoxedUnit> blueThings =
readJournal.query(EventsByTag.create("blue"));
// find top 10 blue things:
final Future<List<Object>> top10BlueThings =
(Future<List<Object>>) blueThings
.map(t -> t.event())
.take(10) // cancels the query stream after pulling 10 elements
.<List<Object>>runFold(new ArrayList<>(10), (acc, e) -> {
acc.add(e);
return acc;
}, mat);
// start another query, from the known offset
Source<EventEnvelope, BoxedUnit> blue = readJournal.query(EventsByTag.create("blue", 10));
//#events-by-tag
}
//#materialized-query-metadata-classes
// a plugin can provide:
//#materialized-query-metadata-classes
static
//#materialized-query-metadata-classes
final class QueryMetadata {
public final boolean deterministicOrder;
public final boolean infinite;
public QueryMetadata(Boolean deterministicOrder, Boolean infinite) {
this.deterministicOrder = deterministicOrder;
this.infinite = infinite;
}
}
//#materialized-query-metadata-classes
static
//#materialized-query-metadata-classes
final class AllEvents implements Query<Object, QueryMetadata> {
private AllEvents() {}
private static AllEvents INSTANCE = new AllEvents();
}
//#materialized-query-metadata-classes
void demonstrateMaterializedQueryValues() {
final ActorSystem system = ActorSystem.create();
final ActorMaterializer mat = ActorMaterializer.create(system);
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#materialized-query-metadata
final Source<Object, QueryMetadata> events = readJournal.query(AllEvents.INSTANCE);
events.mapMaterializedValue(meta -> {
System.out.println("The query is: " +
"ordered deterministically: " + meta.deterministicOrder + " " +
"infinite: " + meta.infinite);
return meta;
});
//#materialized-query-metadata
}
class ReactiveStreamsCompatibleDBDriver {
Subscriber<List<Object>> batchWriter() {
return null;
}
}
void demonstrateWritingIntoDifferentStore() {
final ActorSystem system = ActorSystem.create();
final ActorMaterializer mat = ActorMaterializer.create(system);
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#projection-into-different-store-rs
final ReactiveStreamsCompatibleDBDriver driver = new ReactiveStreamsCompatibleDBDriver();
final Subscriber<List<Object>> dbBatchWriter = driver.batchWriter();
// Using an example (Reactive Streams) Database driver
readJournal
.query(EventsByPersistenceId.create("user-1337"))
.grouped(20) // batch inserts into groups of 20
.runWith(Sink.create(dbBatchWriter), mat); // write batches to read-side database
//#projection-into-different-store-rs
}
//#projection-into-different-store-simple-classes
class ExampleStore {
Future<Void> save(Object any) {
// ...
//#projection-into-different-store-simple-classes
return null;
//#projection-into-different-store-simple-classes
}
}
//#projection-into-different-store-simple-classes
void demonstrateWritingIntoDifferentStoreWithMapAsync() {
final ActorSystem system = ActorSystem.create();
final ActorMaterializer mat = ActorMaterializer.create(system);
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#projection-into-different-store-simple
final ExampleStore store = new ExampleStore();
readJournal
.query(EventsByTag.create("bid"))
.mapAsync(1, store::save)
.runWith(Sink.ignore(), mat);
//#projection-into-different-store-simple
}
//#projection-into-different-store
class MyResumableProjection {
private final String name;
public MyResumableProjection(String name) {
this.name = name;
}
public Future<Long> saveProgress(long offset) {
// ...
//#projection-into-different-store
return null;
//#projection-into-different-store
}
public Future<Long> latestOffset() {
// ...
//#projection-into-different-store
return null;
//#projection-into-different-store
}
}
//#projection-into-different-store
void demonstrateWritingIntoDifferentStoreWithResumableProjections() throws Exception {
final ActorSystem system = ActorSystem.create();
final ActorMaterializer mat = ActorMaterializer.create(system);
final ReadJournal readJournal =
PersistenceQuery.get(system)
.getReadJournalFor("akka.persistence.query.noop-read-journal");
//#projection-into-different-store-actor-run
final Timeout timeout = Timeout.apply(3, TimeUnit.SECONDS);
final MyResumableProjection bidProjection = new MyResumableProjection("bid");
final Props writerProps = Props.create(TheOneWhoWritesToQueryJournal.class, "bid");
final ActorRef writer = system.actorOf(writerProps, "bid-projection-writer");
long startFromOffset = Await.result(bidProjection.latestOffset(), timeout.duration());
readJournal
.query(EventsByTag.create("bid", startFromOffset))
.<Long>mapAsync(8, envelope -> {
final Future<Object> f = ask(writer, envelope.event(), timeout);
return f.<Long>map(new Mapper<Object, Long>() {
@Override public Long apply(Object in) {
return envelope.offset();
}
}, system.dispatcher());
})
.mapAsync(1, offset -> bidProjection.saveProgress(offset))
.runWith(Sink.ignore(), mat);
}
//#projection-into-different-store-actor-run
class ComplexState {
boolean readyToSave() {
return false;
}
}
static class Record {
static Record of(Object any) {
return new Record();
}
}
//#projection-into-different-store-actor
final class TheOneWhoWritesToQueryJournal extends AbstractActor {
private final ExampleStore store;
private ComplexState state = new ComplexState();
public TheOneWhoWritesToQueryJournal() {
store = new ExampleStore();
receive(ReceiveBuilder.matchAny(message -> {
state = updateState(state, message);
// example saving logic that requires state to become ready:
if (state.readyToSave())
store.save(Record.of(state));
}).build());
}
ComplexState updateState(ComplexState state, Object msg) {
// some complicated aggregation logic here ...
return state;
}
}
//#projection-into-different-store-actor
}

View file

@ -5,6 +5,7 @@
package docs.persistence.query;
import akka.actor.Cancellable;
import akka.actor.Scheduler;
import akka.japi.Pair;
import akka.japi.pf.ReceiveBuilder;
import akka.persistence.PersistentRepr;
@ -23,6 +24,7 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import static java.util.stream.Collectors.toList;
@ -39,21 +41,28 @@ class MyEventsByTagJavaPublisher extends AbstractActorPublisher<EventEnvelope> {
private final String CONTINUE = "CONTINUE";
private final int LIMIT = 1000;
private long currentOffset;
private List<EventEnvelope> buf = new ArrayList<>();
private List<EventEnvelope> buf = new LinkedList<>();
private Cancellable continueTask;
public MyEventsByTagJavaPublisher(Connection connection, String tag, Long offset, FiniteDuration refreshInterval) {
public MyEventsByTagJavaPublisher(Connection connection,
String tag,
Long offset,
FiniteDuration refreshInterval) {
this.connection = connection;
this.tag = tag;
this.currentOffset = offset;
this.continueTask = context().system().scheduler().schedule(refreshInterval, refreshInterval, self(), CONTINUE, context().dispatcher(), self());
final Scheduler scheduler = context().system().scheduler();
this.continueTask = scheduler
.schedule(refreshInterval, refreshInterval, self(), CONTINUE,
context().dispatcher(), self());
receive(ReceiveBuilder
.matchEquals(CONTINUE, (in) -> {
query();
deliverBuf();
})
.matchEquals(CONTINUE, (in) -> {
query();
deliverBuf();
})
.match(Cancel.class, (in) -> {
context().stop(self());
})
@ -71,33 +80,33 @@ class MyEventsByTagJavaPublisher extends AbstractActorPublisher<EventEnvelope> {
private void query() {
if (buf.isEmpty()) {
try {
PreparedStatement s = connection.prepareStatement(
"SELECT id, persistent_repr " +
"FROM journal WHERE tag = ? AND id >= ? " +
"ORDER BY id LIMIT ?");
final String query = "SELECT id, persistent_repr " +
"FROM journal WHERE tag = ? AND id >= ? " +
"ORDER BY id LIMIT ?";
try (PreparedStatement s = connection.prepareStatement(query)) {
s.setString(1, tag);
s.setLong(2, currentOffset);
s.setLong(3, LIMIT);
final ResultSet rs = s.executeQuery();
try (ResultSet rs = s.executeQuery()) {
final List<Pair<Long, byte[]>> res = new ArrayList<>(LIMIT);
while (rs.next())
res.add(Pair.create(rs.getLong(1), rs.getBytes(2)));
final List<Pair<Long, byte[]>> res = new ArrayList<>(LIMIT);
while (rs.next())
res.add(Pair.create(rs.getLong(1), rs.getBytes(2)));
if (!res.isEmpty()) {
currentOffset = res.get(res.size() - 1).first();
if (!res.isEmpty()) {
currentOffset = res.get(res.size() - 1).first();
}
buf = res.stream().map(in -> {
final Long id = in.first();
final byte[] bytes = in.second();
final PersistentRepr p = serialization.deserialize(bytes, PersistentRepr.class).get();
return new EventEnvelope(id, p.persistenceId(), p.sequenceNr(), p.payload());
}).collect(toList());
}
buf = res.stream().map(in -> {
final Long id = in.first();
final byte[] bytes = in.second();
final PersistentRepr p = serialization.deserialize(bytes, PersistentRepr.class).get();
return new EventEnvelope(id, p.persistenceId(), p.sequenceNr(), p.payload());
}).collect(toList());
} catch(Exception e) {
onErrorThenStop(e);
}

View file

@ -14,12 +14,6 @@ side of an application, however it can help to migrate data from the write side
simple scenarios Persistence Query may be powerful enough to fulful the query needs of your app, however we highly
recommend (in the spirit of CQRS) of splitting up the write/read sides into separate datastores as the need arrises.
While queries can be performed directly on the same datastore, it is also a very common pattern to use the queries
to create *projections* of the write-side's events and store them into a separate datastore which is optimised for more
complex queries. This architectural pattern of projecting the data into a query optimised datastore, with possibly some
transformation or canculations along the way is the core use-case and recommended style of using Akka Persistence Query
- pulling out of one Journal and storing into another one.
.. warning::
This module is marked as **“experimental”** as of its introduction in Akka 2.4.0. We will continue to
@ -58,7 +52,7 @@ journal is as simple as:
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#basic-usage
Journal implementers are encouraged to put this identified in a variable known to the user, such that one can access it via
Journal implementers are encouraged to put this identifier in a variable known to the user, such that one can access it via
``getJournalFor(NoopJournal.identifier)``, however this is not enforced.
Read journal implementations are available as `Community plugins`_.
@ -90,7 +84,7 @@ If your usage does not require a live stream, you can disable refreshing by usin
``EventsByPersistenceId`` is a query equivalent to replaying a :ref:`PersistentActor <event-sourcing>`,
however, since it is a stream it is possible to keep it alive and watch for additional incoming events persisted by the
persistent actor identified by the given ``persistenceId``. Most journal will have to revert to polling in order to achieve
persistent actor identified by the given ``persistenceId``. Most journals will have to revert to polling in order to achieve
this, which can be configured using the ``RefreshInterval`` query hint:
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#events-by-persistent-id-refresh
@ -120,7 +114,6 @@ including for example taking the first 10 and cancelling the stream. It is worth
query has an optionally supported offset parameter (of type ``Long``) which the journals can use to implement resumable-streams.
For example a journal may be able to use a WHERE clause to begin the read starting from a specific row, or in a datastore
that is able to order events by insertion time it could treat the Long as a timestamp and select only older events.
Again, specific capabilities are specific to the journal you are using, so you have to
Materialized values of queries
@ -133,6 +126,7 @@ stream, for example if it's finite or infinite, strictly ordered or not ordered
is defined as the ``M`` type parameter of a query (``Query[T,M]``), which allows journals to provide users with their
specialised query object, as demonstrated in the sample below:
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#materialized-query-metadata-classes
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#materialized-query-metadata
.. _materialized values: http://doc.akka.io/docs/akka-stream-and-http-experimental/1.0/java/stream-quickstart.html#Materialized_values
@ -152,18 +146,18 @@ means that data stores which are able to scale to accomodate these requirements
On the other hand the same application may have some complex statistics view or we may have analists working with the data
to figure out best bidding strategies and trends this often requires some kind of expressive query capabilities like
for example SQL or writing Spark jobs to analyse the data. Trefore the data stored in the write-side needs to be
for example SQL or writing Spark jobs to analyse the data. Therefore the data stored in the write-side needs to be
projected into the other read-optimised datastore.
.. note::
When refering to **Materialized Views** in Akka Persistence think of it as "some persistent storage of the result of a Query".
In other words, it means that the view is created once, in order to be afterwards queries multiple times, as in this format
In other words, it means that the view is created once, in order to be afterwards queried multiple times, as in this format
it may be more efficient or interesting to query it (instead of the source events directly).
Materialize view to Reactive Streams compatible datastore
---------------------------------------------------------
If the read datastore exposes it an `Reactive Streams`_ interface then implementing a simple projection
If the read datastore exposes an `Reactive Streams`_ interface then implementing a simple projection
is as simple as, using the read-journal and feeding it into the databases driver interface, for example like so:
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#projection-into-different-store-rs
@ -179,6 +173,7 @@ you may have to implement the write logic using plain functions or Actors instea
In case your write logic is state-less and you just need to convert the events from one data data type to another
before writing into the alternative datastore, then the projection is as simple as:
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#projection-into-different-store-simple-classes
.. includecode:: code/docs/persistence/PersistenceQueryDocTest.java#projection-into-different-store-simple
Resumable projections