├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── doc └── images │ ├── channels-1.png │ ├── channels-2.png │ ├── channels-3.png │ ├── clustering-1.png │ ├── clustering-2.png │ ├── firststeps-1.png │ ├── firststeps-2.png │ ├── legend.png │ ├── multicast-1.png │ ├── ordermgnt-1.png │ ├── senderrefs-1.png │ ├── senderrefs-2.png │ ├── stackabletraits-1.png │ ├── stackabletraits-2.png │ ├── stackabletraits-3.png │ ├── stackabletraits-4.png │ ├── stackabletraits-5.png │ └── statemachines-1.png ├── es-core-test ├── build.sbt └── src │ └── test │ ├── java │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── core │ │ └── BehaviorSpecProcessor.java │ ├── resources │ └── application.conf │ └── scala │ └── org │ └── eligosource │ └── eventsourced │ └── core │ ├── AggregatorSpec.scala │ ├── BehaviorSpec.scala │ ├── DefaultChannelSpec.scala │ ├── DependentStateRecoverySpec.scala │ ├── EventsourcingSpec.scala │ ├── FsmSpec.scala │ ├── GraphRecoverySpec.scala │ ├── MulticastSpec.scala │ ├── ProcessorSpec.scala │ ├── ReliableChannelSpec.scala │ └── StressSpec.scala ├── es-core ├── build.sbt └── src │ └── main │ └── scala │ └── org │ └── eligosource │ └── eventsourced │ ├── core │ ├── Behavior.scala │ ├── Channel.scala │ ├── ChannelProps.scala │ ├── Confirm.scala │ ├── Emitter.scala │ ├── Eventsourced.scala │ ├── EventsourcingExtension.scala │ ├── Japi.scala │ ├── Journal.scala │ ├── JournalProtocol.scala │ ├── Message.scala │ ├── Multicast.scala │ ├── ProcessorProps.scala │ ├── Receiver.scala │ ├── ReplayParams.scala │ ├── Sequencer.scala │ ├── Snapshot.scala │ └── package.scala │ └── patterns │ └── reliable │ └── requestreply │ └── ReliableRequestReply.scala ├── es-examples ├── build.sbt └── src │ └── main │ ├── java │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ ├── example │ │ └── japi │ │ │ └── SnapshotExample.java │ │ └── guide │ │ └── japi │ │ ├── FirstSteps.java │ │ ├── SenderReferences.java │ │ └── StackableTraits.java │ ├── resources │ ├── cluster.conf │ ├── journal.conf │ ├── order.conf │ └── reliable.conf │ └── scala │ └── org │ └── eligosource │ └── eventsourced │ ├── example │ ├── BasicExample.scala │ ├── ClusterExample.scala │ ├── OrderExample.scala │ ├── OrderExampleReliable.scala │ ├── ReliableChannelExample.scala │ ├── ReliableRequestReplyExample.scala │ ├── SnapshotExample.scala │ └── StandaloneChannelExample.scala │ └── guide │ ├── FirstSteps.scala │ ├── SenderReferences.scala │ └── StackableTraits.scala ├── es-journal ├── es-journal-common │ ├── build.sbt │ └── src │ │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── eligosource │ │ │ │ └── eventsourced │ │ │ │ └── journal │ │ │ │ └── common │ │ │ │ └── serialization │ │ │ │ └── Protocol.java │ │ ├── protocol │ │ │ └── Protocol.proto │ │ ├── resources │ │ │ └── reference.conf │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── common │ │ │ ├── JournalProps.scala │ │ │ ├── ReadOnlyFacade.scala │ │ │ ├── serialization │ │ │ ├── MessageSerialization.scala │ │ │ ├── SnapshotSerialization.scala │ │ │ └── package.scala │ │ │ ├── snapshot │ │ │ ├── HadoopFilesystemSnapshotting.scala │ │ │ └── LocalFilesystemSnapshotting.scala │ │ │ ├── support │ │ │ ├── AsynchronousWriteReplaySupport.scala │ │ │ ├── SynchronousWriteReplaySupport.scala │ │ │ └── package.scala │ │ │ └── util │ │ │ ├── Key.scala │ │ │ ├── WriteInMsgQueue.scala │ │ │ ├── WriteOutMsgCache.scala │ │ │ └── package.scala │ │ └── test │ │ ├── resources │ │ ├── application.conf │ │ ├── persist.conf │ │ └── serializer.conf │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── common │ │ ├── JournalSpec.scala │ │ ├── ReplaySpec.scala │ │ └── serialization │ │ └── SerializerSpec.scala ├── es-journal-dynamodb │ ├── build.sbt │ ├── readme.md │ └── src │ │ ├── it │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── dynamodb │ │ │ ├── DynamoDBJournalSpec.scala │ │ │ └── DynamoDBJournalSupport.scala │ │ └── main │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── dynamodb │ │ ├── DynamoDBJournal.scala │ │ └── DynamoDBJournalProps.scala ├── es-journal-hbase │ ├── build.sbt │ ├── readme.md │ └── src │ │ ├── it │ │ ├── resources │ │ │ └── log4j.properties │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── hbase │ │ │ ├── HBaseCleanup.scala │ │ │ ├── HBaseSpec.scala │ │ │ └── HBaseSupport.scala │ │ └── main │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── hbase │ │ ├── HBaseJournal.scala │ │ ├── HBaseJournalProps.scala │ │ ├── HBaseTable.scala │ │ └── package.scala ├── es-journal-inmem │ ├── build.sbt │ └── src │ │ ├── main │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── inmem │ │ │ ├── InmemJournal.scala │ │ │ └── InmemJournalProps.scala │ │ └── test │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── inmem │ │ ├── InmemJournalLifecycle.scala │ │ ├── InmemJournalSpec.scala │ │ └── InmemReplaySpec.scala ├── es-journal-journalio │ ├── build.sbt │ └── src │ │ ├── main │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── journalio │ │ │ ├── JournalioJournal.scala │ │ │ └── JournalioJournalProps.scala │ │ └── test │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── journalio │ │ ├── JournalioJournalSpec.scala │ │ └── JournalioReplaySpec.scala ├── es-journal-leveldb │ ├── build.sbt │ └── src │ │ ├── main │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── leveldb │ │ │ ├── LeveldbJournal.scala │ │ │ ├── LeveldbJournalPS.scala │ │ │ ├── LeveldbJournalProps.scala │ │ │ └── LeveldbJournalSS.scala │ │ └── test │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── leveldb │ │ ├── LeveldbJournalSpec.scala │ │ ├── LeveldbReplaySpec.scala │ │ └── LeveldbSupport.scala ├── es-journal-mongodb-casbah │ ├── build.sbt │ ├── readme.md │ └── src │ │ ├── main │ │ └── scala │ │ │ └── org │ │ │ └── eligosource │ │ │ └── eventsourced │ │ │ └── journal │ │ │ └── mongodb │ │ │ └── casbah │ │ │ ├── MongodbCasbahJournal.scala │ │ │ └── MongodbCasbahJournalProps.scala │ │ └── test │ │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── mongodb │ │ └── casbah │ │ ├── MongodbCasbahFixtureSupport.scala │ │ ├── MongodbCasbahJournalSpec.scala │ │ ├── MongodbCasbahReplaySpec.scala │ │ ├── MongodbSpecSupport.scala │ │ └── package.scala └── es-journal-mongodb-reactive │ ├── build.sbt │ ├── readme.md │ └── src │ ├── it │ ├── resources │ │ └── log4j.properties │ └── scala │ │ └── org │ │ └── eligosource │ │ └── eventsourced │ │ └── journal │ │ └── mongodb │ │ └── reactive │ │ ├── MongodbReactiveFixtureSupport.scala │ │ ├── MongodbReactiveSpec.scala │ │ └── MongodbReactiveSpecSupport.scala │ └── main │ └── scala │ └── org │ └── eligosource │ └── eventsourced │ └── journal │ └── mongodb │ └── reactive │ ├── MongodbReactiveJournal.scala │ ├── MongodbReactiveJournalProps.scala │ └── package.scala └── project ├── Build.scala ├── build.properties └── plugins.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea* 2 | *.log 3 | *.iml 4 | target/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - "2.10.1" 4 | jdk: 5 | - oraclejdk7 6 | - openjdk7 7 | branches: 8 | except: 9 | - /^wip-.*$/ 10 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization in ThisBuild := "org.eligosource" 2 | 3 | version in ThisBuild := "0.7-SNAPSHOT" 4 | 5 | scalaVersion in ThisBuild := Version.Scala 6 | -------------------------------------------------------------------------------- /doc/images/channels-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/channels-1.png -------------------------------------------------------------------------------- /doc/images/channels-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/channels-2.png -------------------------------------------------------------------------------- /doc/images/channels-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/channels-3.png -------------------------------------------------------------------------------- /doc/images/clustering-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/clustering-1.png -------------------------------------------------------------------------------- /doc/images/clustering-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/clustering-2.png -------------------------------------------------------------------------------- /doc/images/firststeps-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/firststeps-1.png -------------------------------------------------------------------------------- /doc/images/firststeps-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/firststeps-2.png -------------------------------------------------------------------------------- /doc/images/legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/legend.png -------------------------------------------------------------------------------- /doc/images/multicast-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/multicast-1.png -------------------------------------------------------------------------------- /doc/images/ordermgnt-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/ordermgnt-1.png -------------------------------------------------------------------------------- /doc/images/senderrefs-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/senderrefs-1.png -------------------------------------------------------------------------------- /doc/images/senderrefs-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/senderrefs-2.png -------------------------------------------------------------------------------- /doc/images/stackabletraits-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/stackabletraits-1.png -------------------------------------------------------------------------------- /doc/images/stackabletraits-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/stackabletraits-2.png -------------------------------------------------------------------------------- /doc/images/stackabletraits-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/stackabletraits-3.png -------------------------------------------------------------------------------- /doc/images/stackabletraits-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/stackabletraits-4.png -------------------------------------------------------------------------------- /doc/images/stackabletraits-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/stackabletraits-5.png -------------------------------------------------------------------------------- /doc/images/statemachines-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eligosource/eventsourced/7a3439e64d739311820a8eb30cf8acc815cc7a40/doc/images/statemachines-1.png -------------------------------------------------------------------------------- /es-core-test/build.sbt: -------------------------------------------------------------------------------- 1 | Nobootcp.settings 2 | 3 | libraryDependencies ++= Seq( 4 | "commons-io" % "commons-io" % "2.3" % "test" 5 | ) 6 | -------------------------------------------------------------------------------- /es-core-test/src/test/java/org/eligosource/eventsourced/core/BehaviorSpecProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core; 17 | 18 | import akka.actor.ActorRef; 19 | import akka.japi.Procedure; 20 | 21 | public class BehaviorSpecProcessor extends UntypedEventsourcedReceiver { 22 | 23 | private ActorRef destination; 24 | 25 | private Procedure changed = new Procedure() { 26 | @Override 27 | public void apply(Object message) { 28 | if (message.equals("bar")) { 29 | destination.tell(String.format("bar (%d)", sequenceNr()), getSelf()); 30 | unbecome(); 31 | } 32 | } 33 | }; 34 | 35 | public BehaviorSpecProcessor(ActorRef destination) { 36 | this.destination = destination; 37 | } 38 | 39 | @Override 40 | public int id() { 41 | return 1; 42 | } 43 | 44 | @Override 45 | public void onReceive(Object message) throws Exception { 46 | if (message.equals("foo")) { 47 | destination.tell(String.format("foo (%d)", sequenceNr()), getSelf()); 48 | become(changed); 49 | } else if (message.equals("baz")) { 50 | destination.tell(String.format("baz (%d)", sequenceNr()), getSelf()); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /es-core-test/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | log-dead-letters = 0 3 | log-dead-letters-during-shutdown = off 4 | } 5 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/BehaviorSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | import BehaviorSpec._ 21 | 22 | class BehaviorSpec extends EventsourcingSpec[Fixture] { 23 | "A eventsourced processor" must { 24 | "be able to change behavior without loosing event-sourcing functionality (Scala API)" in { fixture => 25 | import fixture._ 26 | 27 | val p = scalaProcessor 28 | 29 | p ! Message("foo") 30 | p ! Message("bar") 31 | p ! Message("baz") 32 | 33 | dequeue() must be ("foo (1)") 34 | dequeue() must be ("bar (2)") 35 | dequeue() must be ("baz (3)") 36 | } 37 | "be able to change behavior without loosing event-sourcing functionality (Java API)" in { fixture => 38 | import fixture._ 39 | 40 | val p = javaProcessor 41 | 42 | p ! Message("foo") 43 | p ! Message("bar") 44 | p ! Message("baz") 45 | 46 | dequeue() must be ("foo (1)") 47 | dequeue() must be ("bar (2)") 48 | dequeue() must be ("baz (3)") 49 | } 50 | "call unhandled() for messages for which receive() is not defined" in { fixture => 51 | import fixture._ 52 | 53 | val p = scalaProcessor 54 | 55 | p ! Message("xyz") 56 | 57 | dequeue() must be ("unhandled (1)") 58 | } 59 | } 60 | } 61 | 62 | object BehaviorSpec { 63 | class Fixture extends EventsourcingFixture[Any] { 64 | val destination = system.actorOf(Props(new Destination(queue) with Receiver with Confirm)) 65 | 66 | def scalaProcessor = extension.processorOf(Props(new Processor(destination) with Receiver with Eventsourced { val id = 1 } )) 67 | def javaProcessor = extension.processorOf(Props(new BehaviorSpecProcessor(destination))) 68 | } 69 | 70 | class Processor(destination: ActorRef) extends Actor { this: Receiver => 71 | val changed: Receive = { 72 | case "bar" => { destination ! ("bar (%d)" format sequenceNr); unbecome() } 73 | } 74 | 75 | def receive: Receive = { 76 | case "foo" => { destination ! ("foo (%d)" format sequenceNr); become(changed) } 77 | case "baz" => { destination ! ("baz (%d)" format sequenceNr) } 78 | } 79 | 80 | override def unhandled(msg: Any) = msg match { 81 | case s: String => { destination ! ("unhandled (%d)" format sequenceNr) } 82 | case _ => super.unhandled(msg) 83 | } 84 | } 85 | 86 | class Destination(queue: java.util.Queue[Any]) extends Actor { 87 | def receive: Receive = { 88 | case msg => queue.add(msg) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/DependentStateRecoverySpec.scala: -------------------------------------------------------------------------------- 1 | package org.eligosource.eventsourced.core 2 | 3 | import akka.actor._ 4 | 5 | import DependentStateRecoverySpec._ 6 | 7 | class DependentStateRecoverySpec extends EventsourcingSpec[Fixture]{ 8 | "Two processors exchanging state-dependent messages" must { 9 | "be recovered consistently in" in { fixture => 10 | import fixture._ 11 | 12 | val a1 = processorA() 13 | val b1 = processorB() 14 | 15 | a1 tell (Message(Increment), b1) 16 | dequeue() must be ("done") 17 | 18 | // replace registered processor with new ones 19 | processorA() 20 | processorB() 21 | 22 | extension.recover() 23 | dequeue() must be ("done") 24 | } 25 | } 26 | } 27 | 28 | object DependentStateRecoverySpec { 29 | class Fixture extends EventsourcingFixture[Any] { 30 | val target = system.actorOf(Props(new Target(queue))) 31 | 32 | def processorA() = extension.processorOf(Props(new A(target) with Receiver with Eventsourced { val id = 1 })) 33 | def processorB() = extension.processorOf(Props(new B with Receiver with Eventsourced { val id = 2 })) 34 | } 35 | 36 | case object Increment 37 | case object GetCounter 38 | case class Counter(value: Int) 39 | 40 | class A(target: ActorRef) extends Actor { this: Receiver => 41 | var counter = 0 42 | 43 | def receive = { 44 | case Increment => { 45 | counter += 1 46 | sender ! message.copy(event = GetCounter) 47 | } 48 | case Counter(value) => { 49 | if (counter != (value + 1)) target ! "error" 50 | if (counter == 5) target ! "done" else sender ! message.copy(event = Increment) 51 | } 52 | } 53 | } 54 | 55 | class B extends Actor { this: Receiver => 56 | var counter = 0 57 | 58 | def receive = { 59 | case Increment => { 60 | counter += 1 61 | sender ! message.copy(event = Increment) 62 | } 63 | case GetCounter => { 64 | sender ! message.copy(event = Counter(counter)) 65 | } 66 | } 67 | } 68 | 69 | class Target(queue: java.util.Queue[Any]) extends Actor { 70 | def receive = { case event => queue.add(event) } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/EventsourcingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import java.util.concurrent.{CountDownLatch, LinkedBlockingQueue, TimeUnit} 19 | 20 | import scala.concurrent.Await 21 | import scala.concurrent.duration._ 22 | import scala.reflect.ClassTag 23 | 24 | import akka.actor._ 25 | import akka.pattern.ask 26 | import akka.util.Timeout 27 | 28 | import org.scalatest.fixture._ 29 | import org.scalatest.matchers.MustMatchers 30 | 31 | import org.eligosource.eventsourced.journal.common.JournalProps 32 | import org.eligosource.eventsourced.journal.leveldb._ 33 | 34 | abstract class EventsourcingSpec[T <: EventsourcingFixture[_] : ClassTag] extends WordSpec with MustMatchers { 35 | type FixtureParam = T 36 | 37 | def createFixture = 38 | implicitly[ClassTag[T]].runtimeClass.newInstance().asInstanceOf[T] 39 | 40 | def withFixture(test: OneArgTest) { 41 | val fixture = createFixture 42 | try { 43 | test(fixture) 44 | } finally { 45 | fixture.shutdown() 46 | } 47 | } 48 | } 49 | 50 | trait EventsourcingFixtureOps[A] { self: EventsourcingFixture[A] => 51 | val queue = new LinkedBlockingQueue[A] 52 | 53 | def cleanup() 54 | def journalProps: JournalProps 55 | 56 | def dequeue[A](queue: LinkedBlockingQueue[A]): A = { 57 | queue.poll(timeout.duration.toMillis, TimeUnit.MILLISECONDS) 58 | } 59 | 60 | def dequeue(): A = { 61 | dequeue(queue) 62 | } 63 | 64 | def dequeue(p: A => Unit) { 65 | p(dequeue()) 66 | } 67 | 68 | def result[A : ClassTag](actor: ActorRef)(r: Any): A = { 69 | Await.result(actor.ask(r)(timeout).mapTo[A], timeout.duration) 70 | } 71 | } 72 | 73 | class EventsourcingFixture[A] extends EventsourcingFixtureOps[A] with LeveldbSupport { 74 | implicit val timeout = Timeout(10 seconds) 75 | implicit val system = ActorSystem("test") 76 | 77 | val journal = journalProps.createJournal 78 | val extension = EventsourcingExtension(system, journal) 79 | 80 | def shutdown() { 81 | system.shutdown() 82 | system.awaitTermination(timeout.duration) 83 | cleanup() 84 | } 85 | } 86 | 87 | trait FutureCommands { 88 | def await() 89 | } 90 | 91 | class CommandListener(latch: CountDownLatch, predicate: PartialFunction[Any, Boolean]) extends Actor { 92 | def receive = { 93 | case msg => if (predicate.isDefinedAt(msg) && predicate(msg)) latch.countDown() 94 | } 95 | } 96 | 97 | object CommandListener { 98 | def apply(journal: ActorRef, count: Int)(predicate: PartialFunction[Any, Boolean])(implicit system: ActorSystem): FutureCommands = { 99 | val latch = new CountDownLatch(count) 100 | journal ! JournalProtocol.SetCommandListener(Some(system.actorOf(Props(new CommandListener(latch, predicate))))) 101 | new FutureCommands { 102 | def await() = latch.await(5, TimeUnit.SECONDS) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/FsmSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | import org.eligosource.eventsourced.core.JournalProtocol._ 21 | 22 | import FsmSpec._ 23 | 24 | class FsmSpec extends EventsourcingSpec[Fixture] { 25 | "An event-sourced FSM" must { 26 | "recover its state from stored event messages" in { fixture => 27 | import fixture._ 28 | 29 | val door = configure() 30 | val completion = CommandListener(journal, 3) { case cmd: WriteAck => true } 31 | extension.recover() 32 | 33 | door ! Message("open") 34 | door ! Message("close") 35 | door ! Message("close") 36 | 37 | dequeue must be(DoorMoved(Open, 1)) 38 | dequeue must be(DoorMoved(Closed, 2)) 39 | dequeue must be(DoorNotMoved(Closed, "cannot close door")) 40 | 41 | completion.await() 42 | 43 | val recoveredDoor = configure() 44 | extension.recover() 45 | 46 | recoveredDoor ! Message("open") 47 | recoveredDoor ! Message("close") 48 | recoveredDoor ! Message("blah") 49 | 50 | dequeue must be(DoorMoved(Open, 3)) 51 | dequeue must be(DoorMoved(Closed, 4)) 52 | dequeue must be(NotSupported("blah")) 53 | } 54 | } 55 | } 56 | 57 | object FsmSpec { 58 | class Fixture extends EventsourcingFixture[Any] { 59 | val destination = system.actorOf(Props(new Destination(queue) with Receiver with Confirm)) 60 | 61 | def configure(): ActorRef = { 62 | extension.channelOf(DefaultChannelProps(1, destination)) 63 | extension.processorOf(Props(new Door with Emitter with Eventsourced { val id = 1 } )) 64 | } 65 | } 66 | 67 | sealed trait DoorState 68 | 69 | case object Open extends DoorState 70 | case object Closed extends DoorState 71 | 72 | case class DoorMoved(state: DoorState, times: Int) 73 | case class DoorNotMoved(state: DoorState, cmd: String) 74 | case class NotSupported(cmd: Any) 75 | 76 | class Door extends Actor with FSM[DoorState, Int] { this: Emitter => 77 | startWith(Closed, 0) 78 | 79 | when(Closed) { 80 | case Event("open", counter) => { 81 | emit(DoorMoved(Open, counter + 1)) 82 | goto(Open) using(counter + 1) 83 | } 84 | } 85 | 86 | when(Open) { 87 | case Event("close", counter) => { 88 | emit(DoorMoved(Closed, counter + 1)) 89 | goto(Closed) using(counter + 1) 90 | } 91 | } 92 | 93 | whenUnhandled { 94 | case Event(cmd @ ("open" | "close"), counter) => { 95 | emit(DoorNotMoved(stateName, "cannot %s door" format cmd)) 96 | stay 97 | } 98 | case Event(cmd, counter) => { 99 | emit(NotSupported(cmd)) 100 | stay 101 | } 102 | } 103 | 104 | def emit(event: Any) = emitter(1) forwardEvent event 105 | } 106 | 107 | class Destination(queue: java.util.Queue[Any]) extends Actor { 108 | def receive = { 109 | case event => queue.add(event) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/MulticastSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | import MulticastSpec._ 21 | 22 | class MulticastSpec extends EventsourcingSpec[Fixture] { 23 | "A multicast processor" must { 24 | "forward received event messages to its targets" in { fixture => 25 | import fixture._ 26 | 27 | val m = multicast() 28 | 29 | m ! Message("test") 30 | 31 | dequeue() must be ("test") 32 | dequeue() must be ("test") 33 | } 34 | "forward received non-event messages to its targets" in { fixture => 35 | import fixture._ 36 | 37 | val m = multicast() 38 | m ! "blah" 39 | 40 | dequeue() must be ("blah") 41 | dequeue() must be ("blah") 42 | } 43 | "support message transformation" in { fixture => 44 | import fixture._ 45 | 46 | val m = multicast(msg => msg.copy(event = "mod: %s" format msg.event)) 47 | 48 | m ! Message("test") 49 | 50 | dequeue() must be ("mod: test") 51 | dequeue() must be ("mod: test") 52 | } 53 | "preserve sender" in { fixture => 54 | import fixture._ 55 | 56 | val d = decorator() 57 | result[String](d)(Message("test")) must be("re: test") 58 | result[String](d)("blah") must be("re: blah") 59 | } 60 | } 61 | } 62 | 63 | object MulticastSpec { 64 | class Fixture extends EventsourcingFixture[Any] { 65 | val destination = system.actorOf(Props(new Destination(queue) with Receiver with Confirm)) 66 | 67 | val channel1 = extension.channelOf(DefaultChannelProps(1, destination)) 68 | val channel2 = extension.channelOf(DefaultChannelProps(2, destination)) 69 | 70 | def multicast(transformer: Message => Any = identity) = 71 | extension.processorOf(Props(org.eligosource.eventsourced.core.multicast(1, List( 72 | system.actorOf(Props(new Target(channel1) with Receiver)), 73 | system.actorOf(Props(new Target(channel2) with Receiver))), transformer))) 74 | 75 | def decorator(transformer: Message => Any = identity) = 76 | extension.processorOf(Props(org.eligosource.eventsourced.core.decorator(2, 77 | system.actorOf(Props(new Target(channel1) with Receiver)), transformer))) 78 | 79 | extension.recover() 80 | } 81 | 82 | class Target(channel: ActorRef) extends Actor { this: Receiver => 83 | def receive = { 84 | case "blah" => channel forward Message("blah", ack = false) 85 | case event => channel forward message 86 | } 87 | } 88 | 89 | class Destination(queue: java.util.Queue[Any]) extends Actor { this: Receiver => 90 | def receive = { 91 | case event => { 92 | queue.add(event) 93 | sender ! ("re: %s" format event) 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/ProcessorSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | import ProcessorSpec._ 21 | 22 | class ProcessorSpec extends EventsourcingSpec[Fixture] { 23 | "A eventsourced processor" must { 24 | "receive a timestamp message" in { fixture => 25 | import fixture._ 26 | result[Long](processor(1))(Message("foo")) must be > (0L) 27 | } 28 | "not have an id < 1" in { fixture => 29 | import fixture._ 30 | intercept[InvalidProcessorIdException](processor(0)) 31 | intercept[InvalidProcessorIdException](processor(-1)) 32 | } 33 | } 34 | } 35 | 36 | object ProcessorSpec { 37 | class Fixture extends EventsourcingFixture[Long] { 38 | def processor(pid: Int = 1) = extension.processorOf(Props(new Processor with Receiver with Eventsourced { val id = pid } )) 39 | } 40 | 41 | class Processor extends Actor { this: Receiver => 42 | def receive = { 43 | case "foo" => sender ! message.timestamp 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /es-core-test/src/test/scala/org/eligosource/eventsourced/core/StressSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import java.util.concurrent.TimeUnit 19 | 20 | import akka.actor._ 21 | import akka.pattern.ask 22 | import akka.util.Timeout 23 | 24 | import org.eligosource.eventsourced.core.StressSpec._ 25 | 26 | class StressSpec extends EventsourcingSpec[Fixture] { 27 | "An event-sourced application" when { 28 | "using default channels" should { 29 | "be able to deal with reasonable load" ignore { fixture => 30 | import fixture._ 31 | 32 | val processor = configure(reliable = false) 33 | extension.recover() 34 | 35 | warmup(processor) 36 | queue.poll(10, TimeUnit.SECONDS) 37 | println("warmup done") 38 | 39 | stress(processor, throttle = 4) 40 | queue.poll(100, TimeUnit.SECONDS) must be(stressCycles) 41 | println("stress done") 42 | } 43 | } 44 | "using reliable channels" should { 45 | "be able to deal with reasonable load" ignore { fixture => 46 | import fixture._ 47 | 48 | val processor = configure(reliable = true) 49 | extension.recover() 50 | 51 | warmup(processor) 52 | queue.poll(10, TimeUnit.SECONDS) 53 | println("warmup done") 54 | 55 | stress(processor, throttle = 7) 56 | queue.poll(100, TimeUnit.SECONDS) must be(stressCycles) 57 | println("stress done") 58 | } 59 | } 60 | } 61 | } 62 | 63 | object StressSpec { 64 | val warmupCycles = 1000 65 | val stressCycles = 100000 66 | 67 | class Fixture extends EventsourcingFixture[Any] { 68 | val destination = system.actorOf(Props(new Destination(queue) with Receiver with Confirm)) 69 | 70 | val reliableChannel = extension.channelOf(ReliableChannelProps(1, destination)) 71 | val defaultChannel = extension.channelOf(DefaultChannelProps(2, destination)) 72 | 73 | def configure(reliable: Boolean): ActorRef = { 74 | val channel = if (reliable) reliableChannel else defaultChannel 75 | extension.processorOf(Props(new Processor(channel) with Eventsourced { val id = 1 } )) 76 | } 77 | } 78 | 79 | def warmup(processor: ActorRef)(implicit timeout: Timeout, system: ActorSystem) { 80 | -warmupCycles to 0 foreach { i => processor ! Message(i) } 81 | } 82 | 83 | def stress(processor: ActorRef, throttle: Long)(implicit timeout: Timeout, system: ActorSystem) { 84 | import system.dispatcher 85 | 86 | val start = System.nanoTime() 87 | 1 to stressCycles foreach { i => 88 | if (i % 100 == 0) Thread.sleep(throttle) 89 | val nanos = System.nanoTime() 90 | processor ? Message(i) onSuccess { 91 | case r: Int => if (r % 5000 == 0) { 92 | val now = System.nanoTime() 93 | 94 | val latency = (now - nanos) / 1e6 95 | val throughput = r * 1e9 / (now - start) 96 | 97 | // print some statistics ... 98 | println("throughput = %.0f msgs/sec, latency of response %d = %.2f ms" format (throughput, r, latency)) 99 | } 100 | } 101 | } 102 | } 103 | 104 | class Processor(channel: ActorRef) extends Actor { 105 | def receive = { 106 | case msg: Message => channel forward msg 107 | } 108 | } 109 | 110 | class Destination(queue: java.util.Queue[Any]) extends Actor { this: Receiver => 111 | def receive = { 112 | case ctr: Int => { 113 | sender ! ctr 114 | if (ctr == 0 || ctr == stressCycles) queue.add(ctr) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /es-core/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.osgi.SbtOsgi.OsgiKeys 2 | 3 | libraryDependencies in ThisBuild ++= Seq( 4 | "org.scalatest" %% "scalatest" % Version.ScalaTest % "test" 5 | ) 6 | 7 | libraryDependencies ++= Seq( 8 | "com.typesafe.akka" %% "akka-actor" % Version.Akka % "compile" 9 | ) 10 | 11 | OsgiKeys.importPackage := Seq( 12 | "scala*;version=\"[2.10.0,2.11.0)\"", 13 | "akka*;version=\"[2.1.1,2.2.0)\"" 14 | ) 15 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Behavior.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | import akka.japi.Procedure 20 | 21 | /** 22 | * Allows actors with a stackable [[org.eligosource.eventsourced.core.Receiver]], 23 | * [[org.eligosource.eventsourced.core.Emitter]] and/or 24 | * [[org.eligosource.eventsourced.core.Eventsourced]] modification to change their behavior 25 | * with `become()` and `unbecome()` without loosing the functionality implemented by these 26 | * traits. 27 | * 28 | * On the other hand, actors that use `context.become()` to change their behavior will loose 29 | * their `Receiver`, `Emitter` and/or `Eventsourced` functionality. 30 | */ 31 | trait Behavior extends Actor { 32 | private val emptyBehaviorStack: List[Receive] = Nil 33 | private var behaviorStack: List[Receive] = super.receive :: emptyBehaviorStack 34 | 35 | abstract override def receive: Receive = { 36 | case msg => invoke(msg) 37 | } 38 | 39 | final def invoke(msg: Any) { 40 | val behavior = behaviorStack.head 41 | if (behavior.isDefinedAt(msg)) behavior(msg) else unhandled(msg) 42 | } 43 | 44 | /** 45 | * Puts `behavior` on the hotswap stack. This will preserve the behavior of this stackable 46 | * trait. Actors that additionally want to replace the behavior of this stackable trait should 47 | * call `context.become(...)`. 48 | * 49 | * @param behavior new behavior 50 | * @param discardOld if `true`, `unbecome()` will be called prior to pushing `behavior`. 51 | */ 52 | def become(behavior: Actor.Receive, discardOld: Boolean = true) { 53 | behaviorStack = behavior :: (if (discardOld && behaviorStack.nonEmpty) behaviorStack.tail else behaviorStack) 54 | } 55 | 56 | /** 57 | * Java API. 58 | * 59 | * Puts `behavior` on the hotswap stack. This will preserve the behavior of this stackable 60 | * trait. Actors that additionally want to replace the behavior of this stackable trait should 61 | * call `getContext().become(...)`. The existing (old) behavior will be discarded. 62 | * 63 | * @param behavior new behavior 64 | */ 65 | def become(behavior: Procedure[Any]): Unit = become(behavior, true) 66 | 67 | /** 68 | * Java API. 69 | * 70 | * Puts `behavior` on the hotswap stack. This will preserve the behavior of this stackable 71 | * trait. Actors that additionally want to replace the behavior of this stackable trait should 72 | * call `getContext().become(...)`. 73 | * 74 | * @param behavior new behavior 75 | * @param discardOld if `true`, `unbecome()` will be called prior to pushing `behavior`. 76 | */ 77 | def become(behavior: Procedure[Any], discardOld: Boolean): Unit = 78 | become({ case msg => behavior.apply(msg) }: Actor.Receive, discardOld) 79 | 80 | /** 81 | * Reverts the behavior to the previous one on the hotswap stack. This will preserve the behavior 82 | * of this stackable trait. 83 | */ 84 | def unbecome() { 85 | behaviorStack = if (behaviorStack.isEmpty || behaviorStack.tail.isEmpty) super.receive :: emptyBehaviorStack else behaviorStack.tail 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Confirm.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | /** 21 | * Stackable modification for actors that want to automatically confirm the 22 | * receipt of an event [[org.eligosource.eventsourced.core.Message]] from a 23 | * [[org.eligosource.eventsourced.core.Channel]]. If the modified actor's 24 | * `receive` method successfully returns, this trait calls `confirm(true)` on 25 | * the received event message. If `receive` throws an exception, this trait 26 | * calls `confirm(false)` on the received event message and re-throws the 27 | * exception. Usage example: 28 | * 29 | * {{{ 30 | * val myActor = system.actorOf(Props(new MyActor with Confirm)) 31 | * 32 | * class MyActor extends Actor { 33 | * def receive = { 34 | * case msg: Message => // ... 35 | * } 36 | * } 37 | * }}} 38 | * 39 | * This trait can also be used in combination with other stackable traits of the 40 | * library (such as [[org.eligosource.eventsourced.core.Receiver]], 41 | * [[org.eligosource.eventsourced.core.Emitter]] or 42 | * [[org.eligosource.eventsourced.core.Eventsourced]]), for example: 43 | * 44 | * {{{ 45 | * val myActor = system.actorOf(Props(new MyActor with Receiver with Confirm with Eventsourced { val id = ... } )) 46 | * 47 | * class MyActor extends Actor { 48 | * def receive = { 49 | * case event => // ... 50 | * } 51 | * } 52 | * }}} 53 | */ 54 | trait Confirm extends Actor { 55 | abstract override def receive = { 56 | case msg: Message => { 57 | try { 58 | super.receive(msg) 59 | msg.confirm(true) 60 | } catch { 61 | case e: Throwable => { msg.confirm(false); throw e } 62 | } 63 | } 64 | case msg => { 65 | super.receive(msg) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Eventsourced.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | import org.eligosource.eventsourced.core.JournalProtocol._ 21 | 22 | /** 23 | * Stackable modification for making an actor persistent via event-sourcing (or command-sourcing). 24 | * It writes any input [[org.eligosource.eventsourced.core.Message]] to a journal. Input messages 25 | * of any other type are not journaled. Example: 26 | * 27 | * {{{ 28 | * val system: ActorSystem = ... 29 | * val journal: ActorRef = ... 30 | * val extension = EventsourcingExtension(system, journal) 31 | * 32 | * class MyActor extends Actor { 33 | * def receive = { 34 | * case msg: Message => // journaled event message 35 | * case msg => // non-journaled message 36 | * } 37 | * } 38 | * 39 | * // create and register and event-sourced actor (processor) 40 | * val myActor = extension.processorOf(Props(new MyActor with Eventsourced { val id = 1 } )) 41 | * 42 | * // replay journaled messages from previous application runs 43 | * extension.recover() 44 | * 45 | * myActor ! Message("foo event") // message will be journaled 46 | * myActor ! "whatever" // message will not be journaled 47 | * }}} 48 | * 49 | * If the `Eventsourced` trait is used in combination with [[org.eligosource.eventsourced.core.Receiver]] 50 | * or [[org.eligosource.eventsourced.core.Emitter]], `Eventsourced` must be the last modification: 51 | * 52 | * {{{ 53 | * new Actor with Receiver with Eventsourced { ... } // ok 54 | * new Actor with Emitter with Eventsourced { ... } // ok 55 | * new Actor with Eventsourced with Receiver { ... } // won't work 56 | * new Actor with Eventsourced with Emitter { ... } // won't work 57 | * }}} 58 | * 59 | * The `Eventsourced` trait can additionally be combined with the stackable 60 | * [[org.eligosource.eventsourced.core.Confirm]] trait. 61 | * 62 | * @see [[org.eligosource.eventsourced.core.EventsourcingExtension]] 63 | */ 64 | trait Eventsourced extends Behavior { 65 | import Eventsourced._ 66 | 67 | protected val extension = EventsourcingExtension(context.system) 68 | protected val journal = extension.journal 69 | 70 | /** 71 | * Processor id. 72 | */ 73 | def id: Int 74 | 75 | abstract override def receive = { 76 | case GetId => { 77 | sender ! id 78 | } 79 | case msg: Message => { 80 | journal forward WriteInMsg(id, msg.copy(processorId = id), self) 81 | } 82 | case Written(msg) => { 83 | // optionally set processorId for backwards compatibility with existing stores 84 | super.receive(if (msg.processorId == 0L) msg.copy(processorId = id) else msg) 85 | } 86 | case Looped(CompleteProcessing) => { 87 | sender ! Completed 88 | } 89 | case Looped(msg) => { 90 | super.receive(msg) 91 | } 92 | case SnapshotRequest => { 93 | journal forward RequestSnapshot(id, self) 94 | } 95 | case sr: SnapshotRequest => { 96 | super.receive(sr) 97 | } 98 | case so: SnapshotOffer => { 99 | super.receive(so) 100 | } 101 | case msg => { 102 | // won't be written to journal but must be looped through 103 | // journal actor in order to to preserve order of event 104 | // messages and non-event messages sent to this actor 105 | journal forward Loop(msg, self) 106 | } 107 | } 108 | 109 | /** 110 | * Calls `super.postStop` and then de-registers this processor from 111 | * [[org.eligosource.eventsourced.core.EventsourcingExtension]]. 112 | */ 113 | abstract override def postStop() { 114 | super.postStop() 115 | extension.deregisterProcessor(id) 116 | } 117 | } 118 | 119 | private [eventsourced] object Eventsourced { 120 | case object GetId 121 | 122 | case object CompleteProcessing 123 | case object Completed 124 | } 125 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Japi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | /** 21 | * Java API. 22 | * 23 | * Do not extend this class directly. It is identical to [[akka.actor.UntypedActor]] 24 | * except that `receive` is non-final which allows for decoration with stackable traits. 25 | */ 26 | abstract class UntypedActorSupport extends Actor { 27 | 28 | // ---------------------------------------------------------- 29 | // Temporary copy-paste of akka.actor.UntypedActor because UntypedActor.receive is 30 | // final and cannot be decorated with stackable traits. 31 | // ---------------------------------------------------------- 32 | 33 | override def supervisorStrategy: SupervisorStrategy = super.supervisorStrategy 34 | 35 | def getContext(): UntypedActorContext = context.asInstanceOf[UntypedActorContext] 36 | def getSelf(): ActorRef = self 37 | def getSender(): ActorRef = sender 38 | 39 | @throws(classOf[Exception]) def onReceive(message: Any): Unit 40 | 41 | @throws(classOf[Exception]) override def preStart(): Unit = super.preStart() 42 | @throws(classOf[Exception]) override def postStop(): Unit = super.postStop() 43 | 44 | @throws(classOf[Exception]) override def preRestart(reason: Throwable, message: Option[Any]): Unit = 45 | super.preRestart(reason, message) 46 | @throws(classOf[Exception]) override def postRestart(reason: Throwable): Unit = 47 | super.postRestart(reason) 48 | 49 | @throws(classOf[Exception]) def receive = { case msg => onReceive(msg) } 50 | } 51 | 52 | /** 53 | * Java API. 54 | * 55 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Receiver]]. 56 | */ 57 | abstract class UntypedReceiver extends UntypedActorSupport with Receiver 58 | 59 | /** 60 | * Java API. 61 | * 62 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Emitter]]. 63 | */ 64 | abstract class UntypedEmitter extends UntypedActorSupport with Emitter 65 | 66 | /** 67 | * Java API. 68 | * 69 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Confirm]]. 70 | */ 71 | abstract class UntypedConfirmingActor extends UntypedActorSupport with Confirm 72 | 73 | /** 74 | * Java API. 75 | * 76 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Eventsourced]]. 77 | */ 78 | abstract class UntypedEventsourcedActor extends UntypedActorSupport with Eventsourced 79 | 80 | /** 81 | * Java API. 82 | * 83 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Receiver]] and 84 | * [[org.eligosource.eventsourced.core.Confirm]]Base class for untyped actors, modified with . 85 | */ 86 | abstract class UntypedConfirmingReceiver extends UntypedReceiver with Confirm 87 | 88 | /** 89 | * Java API. 90 | * 91 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Receiver]] and 92 | * [[org.eligosource.eventsourced.core.Eventsourced]]. 93 | */ 94 | abstract class UntypedEventsourcedReceiver extends UntypedReceiver with Eventsourced 95 | 96 | /** 97 | * Java API. 98 | * 99 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Emitter]] and 100 | * [[org.eligosource.eventsourced.core.Eventsourced]]. 101 | */ 102 | abstract class UntypedEventsourcedEmitter extends UntypedEmitter with Eventsourced 103 | 104 | /** 105 | * Java API. 106 | * 107 | * Base class for untyped actors, modified with [[org.eligosource.eventsourced.core.Receiver]], 108 | * [[org.eligosource.eventsourced.core.Confirm]] and 109 | * [[org.eligosource.eventsourced.core.Eventsourced]]. 110 | */ 111 | abstract class UntypedEventsourcedConfirmingReceiver extends UntypedConfirmingReceiver with Eventsourced 112 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Journal.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import scala.language.reflectiveCalls 19 | 20 | import akka.actor._ 21 | 22 | /** 23 | * Only exists for backwards compatibility. 24 | */ 25 | object Journal { 26 | /** 27 | * Creates a journal actor from the specified journal configuration object. 28 | * 29 | * @param props journal configuration object. 30 | * @return journal actor. 31 | */ 32 | @deprecated("use JournalProps.createJournal instead", "0.6") 33 | def apply(props: { 34 | def name: Option[String] 35 | def dispatcherName: Option[String] 36 | def createJournalActor: Actor 37 | })(implicit actorRefFactory: ActorRefFactory): ActorRef = 38 | actor(props.createJournalActor, props.name, props.dispatcherName) 39 | } 40 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Message.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor.ActorRef 19 | 20 | /** 21 | * A message for communicating application events. Application events are not interpreted 22 | * by the [[https://github.com/eligosource/eventsourced eventsourced library]] and can have 23 | * any type. Since the library doesn't make any assumptions about the structure and semantics 24 | * of `event`, applications may also choose to send ''commands'' with [[org.eligosource.eventsourced.core.Message]]s. 25 | * In other words, the library can be used for both, event-sourcing and command-sourcing. 26 | * 27 | * Messages sent to an [[org.eligosource.eventsourced.core.Eventsourced]] processor 28 | * are called ''input'' messages. Processors process input messages and optionally 29 | * ''emit'' (or send) ''output'' messages to one or more destinations, usually via 30 | * [[org.eligosource.eventsourced.core.Channel]]s. Output messages should be derived 31 | * from input messages using the `copy(...)` method. Processors may also reply to 32 | * initial senders using the actor's current `sender` reference. 33 | * 34 | * @param event Application event (or command). 35 | * @param sequenceNr Sequence number that is generated when messages are written 36 | * to the journal. Can also be used for detecting duplicates, in special cases. 37 | * @param timestamp time the input message was added to the event log. 38 | * @param processorId Id of the event processor that stored (and emitted) this message. 39 | * @param ack Whether or not an ''acknowledgement'' should be written to the journal during 40 | * (or after) delivery of this message by a [[org.eligosource.eventsourced.core.Channel]]. 41 | * Used by event processors to indicate a series of output messages (that are derived 42 | * from a single input message). In this case, output messages 1 to n-1 should have `ack` 43 | * set to `false` and only output message n should have `ack` set to `true` (default). 44 | * If an acknowledgement has been written for a series, all messages of that series will 45 | * be ignored by the corresponding channel during a replay, otherwise all of them will 46 | * be delivered again. 47 | */ 48 | case class Message( 49 | event: Any, 50 | sequenceNr: Long = 0L, 51 | timestamp: Long = 0L, 52 | processorId: Int = 0, 53 | acks: Seq[Int] = Nil, 54 | ack: Boolean = true, 55 | senderRef: ActorRef = null, 56 | confirmationTarget: ActorRef = null, 57 | confirmationPrototype: Confirmation = null) { 58 | 59 | /** 60 | * Should be called by [[org.eligosource.eventsourced.core.Channel]] destinations to 61 | * (positively or negatively) confirm the receipt of this event message. Destinations 62 | * may also delegate this call other actors or threads. 63 | * 64 | * @param pos `true` for a positive receipt confirmation, `false` for a negative one. 65 | */ 66 | def confirm(pos: Boolean = true) { 67 | if (confirmationTarget != null && confirmationPrototype != null) confirmationTarget ! confirmationPrototype.copy(positive = pos) 68 | } 69 | 70 | /** 71 | * Returns a copy of this message with an updated `event` value. 72 | */ 73 | def withEvent(event: Any): Message = 74 | copy(event = event) 75 | 76 | /** 77 | * Returns a copy of this message with an updated `ack` value. 78 | */ 79 | def withAck(ack: Boolean): Message = 80 | copy(ack = ack) 81 | 82 | private [eventsourced] def withTimestamp: Message = withTimestamp(System.currentTimeMillis) 83 | private [eventsourced] def withTimestamp(timestamp: Long): Message = copy(timestamp = timestamp) 84 | private [eventsourced] def clearConfirmationSettings = copy( 85 | confirmationTarget = null, 86 | confirmationPrototype = null) 87 | } 88 | 89 | object Message { 90 | /** 91 | * Creates a new message from specified `event`. 92 | * 93 | * @param event an event object. 94 | */ 95 | def create(event: Any): Message = 96 | Message(event) 97 | } 98 | 99 | /** 100 | * Confirmation message, generated by channel destinations when confirming a message receipt 101 | * (by calling `Message.confirm(Boolean)`). System message, not used by applications. 102 | * 103 | * @param processorId Sending processor id. 104 | * @param channelId Sending channel id. 105 | * @param sequenceNr Message sequence number. 106 | * @param positive `true` for a positive confirmation, `false` otherwise. 107 | * 108 | */ 109 | case class Confirmation(processorId: Int, channelId: Int, sequenceNr: Long, positive: Boolean) 110 | 111 | /** 112 | * Confirmation timeout message. System message, not used by applications. 113 | * 114 | * @param sequenceNr Sequence number of message for which a delivery confirmation timeout 115 | * occurred. 116 | */ 117 | case class ConfirmationTimeout(sequenceNr: Long) 118 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Multicast.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | /** 21 | * An [[org.eligosource.eventsourced.core.Eventsourced]] processor that forwards 22 | * received event [[org.eligosource.eventsourced.core.Message]]s to `targets` 23 | * (together with the sender reference). Messsages of type other than `Message` 24 | * are forwarded as well. 25 | * 26 | * Using `Multicast` is useful in situtations where mutliple processors should 27 | * receive the same event messages but an application doesn't want them to journal 28 | * these messages redundantly. 29 | * 30 | * @param targets multicast targets. 31 | * @param transformer function applied to received event 32 | * [[org.eligosource.eventsourced.core.Message]]s before they are forwarded 33 | * to `targets`. 34 | */ 35 | class Multicast(targets: Seq[ActorRef], transformer: Message => Any) extends Actor { this: Eventsourced => 36 | def receive = { 37 | case msg: Message => targets.foreach(_ forward transformer(msg)) 38 | case msg => targets.foreach(_ forward msg) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/ProcessorProps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | import akka.japi.{Function => JFunction} 20 | 21 | /** 22 | * [[org.eligosource.eventsourced.core.Eventsourced]] processor configuration object. 23 | * 24 | * @param id Processor id. 25 | * @param processorFactory Processor factory. 26 | * @param name Optional processor name. 27 | * @param dispatcherName Optional dispatcher name. 28 | */ 29 | case class ProcessorProps( 30 | id: Int, 31 | processorFactory: Int => Actor with Eventsourced, 32 | name: Option[String] = None, 33 | dispatcherName: Option[String] = None) { 34 | 35 | /** 36 | * Returns a new `ProcessorProps` with the specified name. 37 | */ 38 | def withName(name: String) = 39 | copy(name = Some(name)) 40 | 41 | /** 42 | * Returns a new `ProcessorProps` with the specified dispatcher name. 43 | */ 44 | def withDispatcherName(dispatcherName: String) = 45 | copy(dispatcherName = Some(dispatcherName)) 46 | 47 | /** 48 | * Creates a processor with the settings defined by this configuration object. 49 | * 50 | * @param actorRefFactory [[org.eligosource.eventsourced.core.Eventsourced]] 51 | * ref factory. 52 | * @return a processor ref. 53 | * @throws InvalidActorNameException if `name` is defined and already in use 54 | * in the underlying actor system. 55 | */ 56 | def createProcessor()(implicit actorRefFactory: ActorRefFactory): ActorRef = { 57 | var props = Props(processorFactory(id)) 58 | 59 | if (dispatcherName.isDefined) 60 | props = props.withDispatcher(dispatcherName.get) 61 | 62 | if (name.isDefined) 63 | actorRefFactory.actorOf(props, name.get) else 64 | actorRefFactory.actorOf(props) 65 | } 66 | } 67 | 68 | object ProcessorProps { 69 | /** 70 | * Java API. 71 | * 72 | * Creates a new `ProcessorProps` object with specified `id` and `processorFactory`. 73 | * 74 | * @param id processor id. 75 | * @param processorFactory processor factory. 76 | */ 77 | def create(id: Int, processorFactory: JFunction[Integer, Actor with Eventsourced]) = 78 | new ProcessorProps(id, pid => processorFactory(pid)) 79 | } -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Receiver.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | /** 21 | * Stackable modification for actors to extract the `event` from a received 22 | * event [[org.eligosource.eventsourced.core.Message]] and calling the modified 23 | * actor's `receive` method with that `event`. Example: 24 | * 25 | * {{{ 26 | * val myReceiver = system.actorOf(Props(new MyReceiver with Receiver)) 27 | * 28 | * myReceiver ! Message("foo event") 29 | * 30 | * class MyReceiver extends Actor { this: Receiver => 31 | * def receive = { 32 | * case "foo event" => { 33 | * val msg = message // current message 34 | * val snr = sequenceNr // sequence number of message 35 | * 36 | * assert(snr > 0L) 37 | * // ... 38 | * } 39 | * } 40 | * } 41 | * }}} 42 | * 43 | * Event messages received by concrete `Receiver`s are stored in a private field 44 | * and can be obtained via the `message` or `messageOption` method. 45 | * 46 | * The `Receiver` trait can also be used in combination with other stackable traits of the 47 | * library (such as [[org.eligosource.eventsourced.core.Confirm]] or 48 | * [[org.eligosource.eventsourced.core.Eventsourced]]), for example: 49 | * 50 | * {{{ 51 | * val myReceiver = system.actorOf(Props(new MyReceiver with Receiver with Confirm with Eventsourced { val id = ... } )) 52 | * 53 | * class MyReceiver extends Actor { this: Receiver => 54 | * def receive = { 55 | * // ... 56 | * } 57 | * } 58 | * }}} 59 | * 60 | */ 61 | trait Receiver extends Behavior { 62 | private var _message: Option[Message] = None 63 | 64 | /** 65 | * Current event message option. `None` if the last message received by this receiver 66 | * is not of type [[org.eligosource.eventsourced.core.Message]]. 67 | */ 68 | def messageOption: Option[Message] = _message 69 | 70 | /** 71 | * Current event message. 72 | * 73 | * @throws IllegalStateException if the the last message received by this receiver 74 | * is not of type [[org.eligosource.eventsourced.core.Message]] 75 | * 76 | * @see `messageOption` 77 | */ 78 | def message: Message = messageOption.getOrElse(throw new IllegalStateException("no current event or command message")) 79 | 80 | /** 81 | * Sequence number of current event message 82 | * 83 | * @throws IllegalStateException if the the last message received by this receiver 84 | * is not of type [[org.eligosource.eventsourced.core.Message]] 85 | */ 86 | def sequenceNr: Long = message.sequenceNr 87 | 88 | /** 89 | * Positively or negatively confirms the receipt of the current event message. 90 | * 91 | * @param pos `true` for a positive receipt confirmation, `false` for a negative one. 92 | * 93 | * @throws IllegalStateException if the the last message received by this receiver 94 | * is not of type [[org.eligosource.eventsourced.core.Message]] 95 | */ 96 | def confirm(pos: Boolean = true) = message.confirm(pos) 97 | 98 | abstract override def receive = { 99 | case msg: Message => { 100 | _message = Some(msg) 101 | super.receive(msg.event) 102 | } 103 | case msg => { 104 | _message = None 105 | super.receive(msg) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Sequencer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor.Actor 19 | 20 | /** 21 | * Stackable modification for actors that need to receive a re-sequenced message stream. 22 | * `(sequence number, message)` tuples will be resequenced by this trait according to 23 | * `sequence number` where the modified actor's `receive` method is called with the re- 24 | * sequenced `message`s. Messages with types other than `(Long, Any)` by-pass the re- 25 | * sequencing algorithm. 26 | */ 27 | trait Sequencer extends Actor { 28 | import scala.collection.mutable.Map 29 | 30 | private val delayed = Map.empty[Long, Any] 31 | private var delivered = 0L 32 | 33 | abstract override def receive = { 34 | case (seqnr: Long, msg) => { 35 | resequence(seqnr, msg) 36 | } 37 | case msg => { 38 | super.receive(msg) 39 | } 40 | } 41 | 42 | @scala.annotation.tailrec 43 | private def resequence(seqnr: Long, msg: Any) { 44 | if (seqnr == delivered + 1) { 45 | delivered = seqnr 46 | super.receive(msg) 47 | } else { 48 | delayed += (seqnr -> msg) 49 | } 50 | val eo = delayed.remove(delivered + 1) 51 | if (eo.isDefined) resequence(delivered + 1, eo.get) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/Snapshot.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.core 17 | 18 | import akka.actor._ 19 | 20 | /** 21 | * Snapshot metadata. 22 | */ 23 | trait SnapshotMetadata { 24 | /** Processor id */ 25 | def processorId: Int 26 | /** Sequence number at which the snapshot was taken by a processor */ 27 | def sequenceNr: Long 28 | /** Time at which the snapshot was saved by a journal */ 29 | def timestamp: Long 30 | } 31 | 32 | /** 33 | * Snapshot of processor state. 34 | * 35 | * @param state Processor-specific state. 36 | */ 37 | case class Snapshot(processorId: Int, sequenceNr: Long, timestamp: Long, state: Any) extends SnapshotMetadata { 38 | private [eventsourced] def withTimestamp(timestamp: Long): Snapshot = copy(timestamp = timestamp) 39 | private [eventsourced] def withTimestamp: Snapshot = withTimestamp(System.currentTimeMillis) 40 | } 41 | 42 | /** 43 | * Requests a snapshot capturing action from an [[org.eligosource.eventsourced.core.Eventsourced]] 44 | * processor. Received by processor. 45 | */ 46 | case class SnapshotRequest(processorId: Int, sequenceNr: Long, requestor: ActorRef) { 47 | /** 48 | * Captures a snapshot of the receiving processor's state. The processor must call this method 49 | * during execution of its `receive` method. 50 | * 51 | * @param state current processor state. 52 | */ 53 | def process(state: Any)(implicit context: ActorContext) = { 54 | context.sender.tell(JournalProtocol.SaveSnapshot(Snapshot(processorId, sequenceNr, 0L, state)), requestor) 55 | } 56 | } 57 | 58 | /** 59 | * Command for requesting a snapshot capturing action from a processor. Once a processor 60 | * processed the received [[org.eligosource.eventsourced.core.SnapshotRequest]] message 61 | * (by calling the message's `process` method with its current state) the captured snapshot 62 | * will be saved. The sender of this command will receive a [[org.eligosource.eventsourced.core.SnapshotSaved]] 63 | * reply when saving successfully completed. 64 | */ 65 | object SnapshotRequest { 66 | /** 67 | * Java API. 68 | * 69 | * Returns this object. 70 | */ 71 | def get = this 72 | } 73 | 74 | /** 75 | * Offers a snapshot to a processor during replay. 76 | * 77 | * @see [[org.eligosource.eventsourced.core.ReplayParams]] 78 | */ 79 | case class SnapshotOffer(snapshot: Snapshot) 80 | 81 | /** 82 | * Success reply to a snapshot capturing request. 83 | * 84 | * @see [[org.eligosource.eventsourced.core.SnapshotRequest$]] 85 | */ 86 | case class SnapshotSaved(processorId: Int, sequenceNr: Long, timestamp: Long) extends SnapshotMetadata 87 | 88 | /** 89 | * Failure reply to a snapshot capturing request. 90 | */ 91 | class SnapshotNotSupportedException(message: String) extends RuntimeException(message) with Serializable 92 | -------------------------------------------------------------------------------- /es-core/src/main/scala/org/eligosource/eventsourced/core/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced 17 | 18 | import akka.actor._ 19 | 20 | package object core { 21 | 22 | /** 23 | * Instantiates, configures and returns a actor. 24 | * 25 | * @param actor actor factory. 26 | * @param name optional name of the actor in the underlying actor system. 27 | * @param dispatcherName optional dispatcher name. 28 | * @throws InvalidActorNameException if `name` is defined and already in use 29 | * in the underlying actor system. 30 | */ 31 | def actor(actor: => Actor, name: Option[String] = None, dispatcherName: Option[String] = None)(implicit actorRefFactory: ActorRefFactory): ActorRef = { 32 | var props = Props(actor) 33 | 34 | dispatcherName.foreach { name => 35 | props = props.withDispatcher(name) 36 | } 37 | 38 | if (name.isDefined) 39 | actorRefFactory.actorOf(props, name.get) else 40 | actorRefFactory.actorOf(props) 41 | } 42 | 43 | // ------------------------------------------------------------ 44 | // Factories for special-purpose processors 45 | // ------------------------------------------------------------ 46 | 47 | /** 48 | * Returns a [[org.eligosource.eventsourced.core.Multicast]] processor with a 49 | * single `target`. Useful in situations are actors cannot be modified with 50 | * the stackable [[org.eligosource.eventsourced.core.Eventsourced]] trait 51 | * e.g. because the actor's `receive` method is declared `final`. 52 | * 53 | * @param processorId processor id. 54 | * @param target single multicast target. 55 | * @param transformer function applied to received event 56 | * [[org.eligosource.eventsourced.core.Message]]s before they are forwarded 57 | * to `target`. 58 | */ 59 | def decorator(processorId: Int, target: ActorRef, transformer: Message => Any = identity): Actor with Eventsourced = 60 | multicast(processorId, List(target), transformer) 61 | 62 | /** 63 | * Returns a [[org.eligosource.eventsourced.core.Multicast]] processor. 64 | * 65 | * @param processorId processor id. 66 | * @param targets multicast targets. 67 | * @param transformer function applied to received event 68 | * [[org.eligosource.eventsourced.core.Message]]s before they are forwarded 69 | * to `targets`. 70 | */ 71 | def multicast(processorId: Int, targets: Seq[ActorRef], transformer: Message => Any = identity): Actor with Eventsourced = 72 | new Multicast(targets, transformer) with Eventsourced { val id = processorId } 73 | } -------------------------------------------------------------------------------- /es-examples/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies ++= Seq( 2 | "com.typesafe.akka" %% "akka-cluster" % Version.Akka % "compile" 3 | ) 4 | -------------------------------------------------------------------------------- /es-examples/src/main/java/org/eligosource/eventsourced/example/japi/SnapshotExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.example.japi; 17 | 18 | import static akka.pattern.Patterns.ask; 19 | 20 | import java.io.File; 21 | import java.io.Serializable; 22 | 23 | import akka.actor.*; 24 | import akka.dispatch.OnComplete; 25 | import akka.japi.Util; 26 | 27 | import org.apache.hadoop.conf.Configuration; 28 | import org.apache.hadoop.fs.FileSystem; 29 | import org.eligosource.eventsourced.core.*; 30 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps; 31 | 32 | public class SnapshotExample { 33 | public static class Processor extends UntypedEventsourcedReceiver { 34 | private int counter = 0; 35 | 36 | @Override 37 | public int id() { 38 | return 1; 39 | } 40 | 41 | @Override 42 | public void onReceive(Object message) throws Exception { 43 | if (message instanceof Increment) { 44 | Increment inc = (Increment)message; 45 | counter += inc.by; 46 | System.out.println(String.format("incremented counter by %d to %d (snr = %d)", 47 | inc.by, counter, sequenceNr())); 48 | } else if (message instanceof SnapshotRequest) { 49 | SnapshotRequest sr = (SnapshotRequest)message; 50 | sr.process(counter, getContext()); 51 | System.out.println(String.format("processed snapshot request for ctr = %d (snr = %d)", 52 | counter, sr.sequenceNr())); 53 | } else if (message instanceof SnapshotOffer) { 54 | SnapshotOffer so = (SnapshotOffer)message; 55 | counter = (Integer)so.snapshot().state(); 56 | System.out.println(String.format("accepted snapshot offer for ctr = %d (snr = %d time = %d)", 57 | counter, so.snapshot().sequenceNr(), so.snapshot().timestamp())); 58 | } 59 | } 60 | } 61 | 62 | public static class Increment implements Serializable { 63 | private int by; 64 | 65 | public Increment(int by) { 66 | this.by = by; 67 | } 68 | } 69 | 70 | public static void main(String... args) throws Exception { 71 | final ActorSystem system = ActorSystem.create("guide"); 72 | 73 | FileSystem fs = FileSystem.getLocal(new Configuration()); 74 | 75 | final ActorRef journal = LeveldbJournalProps.create(new File("target/snapshots-java")).withNative(false).withSnapshotFilesystem(fs).createJournal(system); 76 | final EventsourcingExtension extension = EventsourcingExtension.create(system, journal); 77 | final ActorRef processor = extension.processorOf(Props.create(Processor.class), system); 78 | 79 | extension.recover(extension.getReplayParams().allWithSnapshot()); 80 | 81 | processor.tell(Message.create(new Increment(1)), null); 82 | processor.tell(Message.create(new Increment(2)), null); 83 | 84 | ask(processor, SnapshotRequest.get(), 5000L).mapTo(Util.classTag(SnapshotSaved.class)).onComplete(new OnComplete() { 85 | public void onComplete(Throwable failure, SnapshotSaved result) { 86 | if (failure != null) { 87 | System.out.println(String.format("snapshotting failed: %s", failure.getMessage())); 88 | } else { 89 | System.out.println(String.format("snapshotting succeeded: pid = %d snr = %d time = %d", 90 | result.processorId(), result.sequenceNr(), result.timestamp())); 91 | } 92 | } 93 | }, system.dispatcher()); 94 | 95 | processor.tell(Message.create(new Increment(3)), null); 96 | 97 | Thread.sleep(1000); 98 | system.shutdown(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /es-examples/src/main/java/org/eligosource/eventsourced/guide/japi/FirstSteps.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.guide.japi; 17 | 18 | import java.io.File; 19 | 20 | import akka.actor.*; 21 | 22 | import org.eligosource.eventsourced.core.*; 23 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps; 24 | 25 | public class FirstSteps { 26 | public static class Processor extends UntypedEventsourcedActor { 27 | private ActorRef destination; 28 | private int counter = 0; 29 | 30 | public Processor(ActorRef destination) { 31 | this.destination = destination; 32 | } 33 | 34 | @Override 35 | public int id() { 36 | return 1; 37 | } 38 | 39 | @Override 40 | public void onReceive(Object message) throws Exception { 41 | if (message instanceof Message) { 42 | Message msg = (Message)message; 43 | counter = counter + 1; 44 | System.out.println(String.format("[processor] event = %s (%d)", msg.event(), counter)); 45 | destination.tell(msg.withEvent(String.format("processed %d event messages so far", counter)), getSelf()); 46 | } 47 | } 48 | } 49 | 50 | public static class Destination extends UntypedActor { 51 | @Override 52 | public void onReceive(Object message) throws Exception { 53 | if (message instanceof Message) { 54 | Message msg = (Message)message; 55 | System.out.println(String.format("[destination] event = %s", msg.event())); 56 | msg.confirm(true); 57 | } 58 | } 59 | } 60 | 61 | public static void main(String... args) throws Exception { 62 | final ActorSystem system = ActorSystem.create("guide"); 63 | 64 | final ActorRef journal = LeveldbJournalProps.create(new File("target/guide-1-java")).withNative(false).createJournal(system); 65 | final EventsourcingExtension extension = EventsourcingExtension.create(system, journal); 66 | 67 | final ActorRef destination = system.actorOf(Props.create(Destination.class)); 68 | final ActorRef channel = extension.channelOf(DefaultChannelProps.create(1, destination), system); 69 | final ActorRef processor = extension.processorOf(Props.create(Processor.class, channel), system); 70 | 71 | extension.recover(); 72 | 73 | processor.tell(Message.create("foo"), null); 74 | 75 | Thread.sleep(1000); 76 | system.shutdown(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /es-examples/src/main/java/org/eligosource/eventsourced/guide/japi/SenderReferences.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.guide.japi; 17 | 18 | import static akka.pattern.Patterns.ask; 19 | 20 | import java.io.File; 21 | 22 | import scala.concurrent.Future; 23 | 24 | import akka.actor.*; 25 | import akka.dispatch.OnSuccess; 26 | import akka.japi.Util; 27 | 28 | import org.eligosource.eventsourced.core.*; 29 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps; 30 | 31 | public class SenderReferences { 32 | public static class Processor extends UntypedEventsourcedActor { 33 | private ActorRef destination; 34 | private int counter = 0; 35 | 36 | public Processor(ActorRef destination) { 37 | this.destination = destination; 38 | } 39 | 40 | @Override 41 | public int id() { 42 | return 1; 43 | } 44 | 45 | @Override 46 | public void onReceive(Object message) throws Exception { 47 | if (message instanceof Message) { 48 | Message msg = (Message)message; 49 | counter = counter + 1; 50 | System.out.println(String.format("[processor] event = %s (%d)", msg.event(), counter)); 51 | destination.forward(msg.withEvent(String.format("processed %d event messages so far", counter)), getContext()); 52 | } 53 | } 54 | } 55 | 56 | public static class Destination extends UntypedActor { 57 | @Override 58 | public void onReceive(Object message) throws Exception { 59 | if (message instanceof Message) { 60 | Message msg = (Message)message; 61 | System.out.println(String.format("[destination] event = %s", msg.event())); 62 | msg.confirm(true); 63 | getSender().tell(String.format("done processing event = %s", msg.event()), getSelf()); 64 | } 65 | } 66 | } 67 | 68 | public final static class PrintResult extends OnSuccess { 69 | @Override 70 | public final void onSuccess(T t) { 71 | System.out.println(t); 72 | } 73 | } 74 | 75 | public static void main(String... args) throws Exception { 76 | final ActorSystem system = ActorSystem.create("guide"); 77 | 78 | final ActorRef journal = LeveldbJournalProps.create(new File("target/guide-3-java")).withNative(false).createJournal(system); 79 | final EventsourcingExtension extension = EventsourcingExtension.create(system, journal); 80 | 81 | final ActorRef destination = system.actorOf(Props.create(Destination.class)); 82 | final ActorRef channel = extension.channelOf(DefaultChannelProps.create(1, destination), system); 83 | final ActorRef processor = extension.processorOf(Props.create(Processor.class, channel), system); 84 | 85 | extension.recover(); 86 | 87 | Future futureResult = ask(processor, Message.create("foo"), 5000L).mapTo(Util.classTag(String.class)); 88 | futureResult.onSuccess(new PrintResult(), system.dispatcher()); 89 | 90 | Thread.sleep(1000); 91 | system.shutdown(); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /es-examples/src/main/java/org/eligosource/eventsourced/guide/japi/StackableTraits.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.guide.japi; 17 | 18 | import java.io.File; 19 | 20 | import akka.actor.*; 21 | 22 | import org.eligosource.eventsourced.core.*; 23 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps; 24 | 25 | public class StackableTraits { 26 | public static class Processor extends UntypedEventsourcedEmitter { 27 | private int counter = 0; 28 | 29 | @Override 30 | public int id() { 31 | return 1; 32 | } 33 | 34 | @Override 35 | public void onReceive(Object event) throws Exception { 36 | counter = counter + 1; 37 | System.out.println(String.format("[processor] event = %s (%d)", event, counter)); 38 | emitter(1).sendEvent(String.format("processed %d event messages so far", counter), getSelf()); 39 | } 40 | } 41 | 42 | public static class Destination extends UntypedConfirmingReceiver { 43 | @Override 44 | public void onReceive(Object event) throws Exception { 45 | System.out.println(String.format("[destination] event = %s", event)); 46 | } 47 | } 48 | 49 | public static void main(String... args) throws Exception { 50 | final ActorSystem system = ActorSystem.create("guide"); 51 | 52 | final ActorRef journal = LeveldbJournalProps.create(new File("target/guide-2-java")).withNative(false).createJournal(system); 53 | final EventsourcingExtension extension = EventsourcingExtension.create(system, journal); 54 | 55 | final ActorRef destination = system.actorOf(Props.create(Destination.class)); 56 | final ActorRef channel = extension.channelOf(DefaultChannelProps.create(1, destination), system); 57 | final ActorRef processor = extension.processorOf(Props.create(Processor.class), system); 58 | 59 | extension.recover(); 60 | 61 | processor.tell(Message.create("foo"), null); 62 | 63 | Thread.sleep(1000); 64 | system.shutdown(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /es-examples/src/main/resources/cluster.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | provider = "akka.cluster.ClusterActorRefProvider" 4 | } 5 | remote { 6 | log-remote-lifecycle-events = off 7 | netty.tcp { 8 | hostname = "127.0.0.1" 9 | port = 0 10 | } 11 | } 12 | cluster { 13 | seed-nodes = [ 14 | "akka.tcp://node@127.0.0.1:2561", 15 | "akka.tcp://node@127.0.0.1:2562" 16 | ] 17 | auto-down = on 18 | } 19 | loglevel = ERROR 20 | } 21 | -------------------------------------------------------------------------------- /es-examples/src/main/resources/journal.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | provider = "akka.remote.RemoteActorRefProvider" 4 | } 5 | remote { 6 | enabled-transports = ["akka.remote.netty.tcp"] 7 | netty.tcp { 8 | hostname = "127.0.0.1" 9 | port = 2553 10 | } 11 | } 12 | loglevel = INFO 13 | } 14 | -------------------------------------------------------------------------------- /es-examples/src/main/resources/order.conf: -------------------------------------------------------------------------------- 1 | common { 2 | akka { 3 | actor { 4 | provider = "akka.remote.RemoteActorRefProvider" 5 | } 6 | remote { 7 | enabled-transports = ["akka.remote.netty.tcp"] 8 | netty.tcp { 9 | hostname = "127.0.0.1" 10 | } 11 | } 12 | loglevel = INFO 13 | } 14 | } 15 | 16 | processor { 17 | akka { 18 | remote { 19 | netty.tcp { 20 | port = 2853 21 | } 22 | } 23 | } 24 | } 25 | 26 | validator { 27 | akka { 28 | remote { 29 | netty.tcp { 30 | port = 2852 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /es-examples/src/main/resources/reliable.conf: -------------------------------------------------------------------------------- 1 | common { 2 | akka { 3 | actor { 4 | provider = "akka.remote.RemoteActorRefProvider" 5 | } 6 | remote { 7 | enabled-transports = ["akka.remote.netty.tcp"] 8 | netty.tcp { 9 | hostname = "127.0.0.1" 10 | } 11 | } 12 | loglevel = INFO 13 | } 14 | } 15 | 16 | sender { 17 | akka { 18 | remote { 19 | netty.tcp { 20 | port = 2853 21 | } 22 | } 23 | } 24 | } 25 | 26 | destination { 27 | akka { 28 | remote { 29 | netty.tcp { 30 | port = 2852 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/example/BasicExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.example 17 | 18 | import java.io.File 19 | 20 | import scala.concurrent.duration._ 21 | 22 | import akka.actor._ 23 | import akka.pattern.ask 24 | import akka.util.Timeout 25 | 26 | import org.eligosource.eventsourced.core._ 27 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps 28 | 29 | object BasicExample extends App { 30 | implicit val system = ActorSystem("example") 31 | implicit val timeout = Timeout(5 seconds) 32 | 33 | import system.dispatcher 34 | 35 | // Event sourcing extension 36 | val extension = EventsourcingExtension(system, LeveldbJournalProps(new File("target/example-2"), native = false).createJournal) 37 | 38 | // Register event-sourced processors 39 | val processorA = extension.processorOf(ProcessorProps(1, pid => new ProcessorA with Receiver with Confirm with Eventsourced { val id = pid } )) 40 | val processorB = extension.processorOf(ProcessorProps(2, pid => new ProcessorB with Emitter with Eventsourced { val id = pid })) 41 | 42 | // Modification 'with Receiver' makes actor an acknowledging event/command message receiver 43 | val destination = system.actorOf(Props(new Destination with Receiver with Confirm)) 44 | 45 | // Configure and register channels 46 | val channelA = extension.channelOf(DefaultChannelProps(1, processorA).withName("channelA")) 47 | val channelB = extension.channelOf(ReliableChannelProps(2, destination).withName("channelB")) 48 | 49 | // Register another event-sourced processors (which isn't modified with Receiver or Emitter) 50 | val processorC = extension.processorOf(ProcessorProps(3, pid => new ProcessorC(channelA, channelB) with Eventsourced { val id = pid } )) 51 | 52 | // recover all registered processors 53 | extension.recover() 54 | 55 | val p: ActorRef = processorC // can be replaced with processorB 56 | 57 | // send event message to p 58 | p ! Message("some event") 59 | 60 | // send event message to p and receive response from processor 61 | p ? Message("some event") onSuccess { case resp => println("received response %s" format resp) } 62 | 63 | // send message to p (bypasses journaling because it's not an instance of Message) 64 | p ! "blah" 65 | 66 | // wait for all messages to arrive (graceful shutdown coming soon) 67 | Thread.sleep(1000) 68 | 69 | // then shutdown 70 | system.shutdown() 71 | 72 | // ----------------------------------------------------------- 73 | // Actor definitions 74 | // ----------------------------------------------------------- 75 | 76 | class ProcessorA extends Actor { 77 | def receive = { 78 | case event => { 79 | // do something with event 80 | println("received event = %s" format event) 81 | } 82 | } 83 | } 84 | 85 | class ProcessorB extends Actor { this: Emitter with Eventsourced => 86 | def receive = { 87 | case "blah" => { 88 | println("received non-journaled message") 89 | } 90 | case event => { 91 | // Receiver actors have access to: 92 | val msg = message // current message 93 | val snr = sequenceNr // sequence number of message 94 | // ... 95 | 96 | // Eventsourced actors have access to processor id 97 | val pid = id 98 | 99 | // do something with event 100 | println("received event = %s (processor id = %d, sequence nr = %d)" format(event, pid, snr)) 101 | 102 | // Emitter actors can emit events to named channels 103 | emitter("channelA") sendEvent "out-a" 104 | emitter("channelB") sendEvent "out-b" 105 | 106 | // optionally respond to initial sender 107 | sender ! "done" 108 | } 109 | } 110 | } 111 | 112 | // does the same as ProcessorB but doesn't use any attributes of traits Emitter and Eventsourced 113 | class ProcessorC(channelA: ActorRef, channelB: ActorRef) extends Actor { 114 | def receive = { 115 | case "blah" => { 116 | println("received non-journaled message") 117 | } 118 | case msg: Message => { 119 | println("received event = %s (processor id = 3, sequence nr = %d)" format(msg.event, msg.sequenceNr)) 120 | 121 | channelA ! msg.copy(event = "out-a") 122 | channelB ! msg.copy(event = "out-b") 123 | 124 | sender ! "done" 125 | } 126 | } 127 | } 128 | 129 | class Destination extends Actor { this: Receiver => 130 | def receive = { 131 | case event => { 132 | // Receiver actors have access to: 133 | val msg = message // current message 134 | val snr = sequenceNr // sequence number of message 135 | // ... 136 | 137 | // do something with event 138 | println("received event = %s" format event) 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/example/OrderExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.example 17 | 18 | import scala.concurrent._ 19 | import scala.concurrent.duration._ 20 | 21 | import akka.actor._ 22 | import akka.pattern.ask 23 | import akka.util.Timeout 24 | 25 | import org.eligosource.eventsourced.core._ 26 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps 27 | 28 | object OrderExample extends App { 29 | implicit val system = ActorSystem("example") 30 | implicit val timeout = Timeout(5 seconds) 31 | 32 | import system.dispatcher 33 | 34 | val journalDir = new java.io.File("target/example-1") 35 | val journal = LeveldbJournalProps(journalDir, native = false).createJournal 36 | 37 | val extension = EventsourcingExtension(system, journal) 38 | 39 | val processor = extension.processorOf(Props(new OrderProcessor with Emitter with Confirm with Eventsourced { val id = 1 })) 40 | val validator = system.actorOf(Props(new CreditCardValidator(processor) with Receiver)) 41 | val destination = system.actorOf(Props(new Destination with Receiver with Confirm)) 42 | 43 | extension.channelOf(ReliableChannelProps(1, validator).withName("validation_requests")) 44 | extension.channelOf(DefaultChannelProps(2, destination).withName("accepted_orders")) 45 | extension.recover() 46 | 47 | processor ? Message(OrderSubmitted(Order(details = "jelly beans", creditCardNumber = "1234-5678-1234-5678"))) onSuccess { 48 | case order: Order => println("received response %s" format order) 49 | } 50 | 51 | Thread.sleep(1000) // wait for output events to arrive (graceful shutdown coming soon) 52 | system.shutdown() 53 | 54 | // ------------------------------------ 55 | // domain object 56 | // ------------------------------------ 57 | 58 | case class Order(id: Int = -1, details: String, validated: Boolean = false, creditCardNumber: String) 59 | 60 | // ------------------------------------ 61 | // domain events 62 | // ------------------------------------ 63 | 64 | case class OrderSubmitted(order: Order) 65 | case class OrderAccepted(order: Order) 66 | 67 | case class CreditCardValidationRequested(order: Order) 68 | case class CreditCardValidated(orderId: Int) 69 | 70 | // ------------------------------------ 71 | // event-sourced order processor 72 | // ------------------------------------ 73 | 74 | class OrderProcessor extends Actor { this: Emitter => 75 | var orders = Map.empty[Int, Order] // processor state 76 | 77 | def receive = { 78 | case OrderSubmitted(order) => { 79 | val id = orders.size 80 | val upd = order.copy(id = id) 81 | orders = orders + (id -> upd) 82 | emitter("validation_requests") forwardEvent CreditCardValidationRequested(upd) 83 | } 84 | case CreditCardValidated(orderId) => { 85 | orders.get(orderId).foreach { order => 86 | val upd = order.copy(validated = true) 87 | orders = orders + (orderId -> upd) 88 | sender ! upd 89 | emitter("accepted_orders") sendEvent OrderAccepted(upd) 90 | } 91 | } 92 | } 93 | } 94 | 95 | // ------------------------------------ 96 | // channel destinations 97 | // ------------------------------------ 98 | 99 | class CreditCardValidator(orderProcessor: ActorRef) extends Actor { this: Receiver => 100 | def receive = { 101 | case CreditCardValidationRequested(order) => { 102 | val sdr = sender // initial sender 103 | val msg = message // current message 104 | Future { 105 | // do some credit card validation asynchronously 106 | // ... 107 | 108 | // and send back a successful validation result (preserving the initial sender) 109 | orderProcessor tell (msg.copy(event = CreditCardValidated(order.id)), sdr) 110 | 111 | // please note that this receiver does NOT confirm message receipt. The confirmation 112 | // is done by the order processor when it receives the CreditCardValidated event. 113 | } 114 | } 115 | } 116 | } 117 | 118 | class Destination extends Actor { 119 | def receive = { 120 | case event => println("received event %s" format event) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/example/ReliableChannelExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.example 17 | 18 | import java.io.File 19 | 20 | import scala.concurrent.Await._ 21 | import scala.concurrent.duration._ 22 | 23 | import akka.actor._ 24 | import akka.pattern.ask 25 | import akka.util.Timeout 26 | 27 | import com.typesafe.config.ConfigFactory 28 | 29 | import org.eligosource.eventsourced.core._ 30 | import org.eligosource.eventsourced.journal.leveldb._ 31 | 32 | class Sender(receiverPath: String) extends Actor { 33 | val id = 1 34 | val ext = EventsourcingExtension(context.system) 35 | val proxy = context.actorOf(Props(new DestinationProxy(receiverPath))) 36 | val channel = ext.channelOf(ReliableChannelProps(1, proxy) 37 | .withRedeliveryMax(1000) 38 | .withRedeliveryDelay(1 second) 39 | .withConfirmationTimeout(2 seconds)) 40 | 41 | def receive = { 42 | case msg: Message => { 43 | sender ! s"accepted ${msg.event}" 44 | channel forward msg 45 | } 46 | } 47 | } 48 | 49 | class DestinationProxy(destinationPath: String) extends Actor { 50 | val destinationSelection: ActorSelection = context.actorSelection(destinationPath) 51 | 52 | def receive = { 53 | case msg => destinationSelection tell (msg, sender) // forward 54 | } 55 | } 56 | 57 | class DestinationEndpoint(destination: ActorRef) extends Actor { 58 | def receive = { 59 | case msg => destination forward msg 60 | } 61 | } 62 | 63 | class Destination extends Actor { 64 | val id = 2 65 | 66 | def receive = { 67 | case msg: Message => { 68 | println(s"received ${msg.event}") 69 | msg.confirm() 70 | } 71 | } 72 | } 73 | 74 | object Sender extends App { 75 | val config = ConfigFactory.load("reliable") 76 | val common = config.getConfig("common") 77 | 78 | implicit val system = ActorSystem("example", config.getConfig("sender").withFallback(common)) 79 | implicit val timeout = Timeout(5 seconds) 80 | 81 | val journal = LeveldbJournalProps(new File("target/example-sender"), native = false).createJournal 82 | val extension = EventsourcingExtension(system, journal) 83 | 84 | val sender = extension.processorOf(Props(new Sender("akka.tcp://example@127.0.0.1:2852/user/destination") with Eventsourced)) 85 | 86 | extension.recover() 87 | 88 | while (true) { 89 | print("enter a message: ") 90 | Console.readLine() match { 91 | case "exit" => System.exit(0) 92 | case msg => println(result(sender ? Message(msg), timeout.duration)) 93 | } 94 | } 95 | } 96 | 97 | object Destination extends App { 98 | val config = ConfigFactory.load("reliable") 99 | val common = config.getConfig("common") 100 | 101 | implicit val system = ActorSystem("example", config.getConfig("destination").withFallback(common)) 102 | 103 | val journal = LeveldbJournalProps(new File("target/example-destination"), native = false).createJournal 104 | val extension = EventsourcingExtension(system, journal) 105 | 106 | val destination = extension.processorOf(Props(new Destination with Eventsourced)) 107 | 108 | // wait for destination recovery to complete 109 | extension.recover() 110 | 111 | // make destination remotely accessible after recovery 112 | system.actorOf(Props(new DestinationEndpoint(destination)), "destination") 113 | } 114 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/example/SnapshotExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.example 17 | 18 | import java.io.File 19 | 20 | import scala.concurrent.duration._ 21 | import scala.util._ 22 | 23 | import akka.actor._ 24 | import akka.pattern.ask 25 | import akka.util._ 26 | 27 | import org.eligosource.eventsourced.core._ 28 | import org.eligosource.eventsourced.journal.leveldb._ 29 | 30 | object SnapshotExample extends App { 31 | implicit val system = ActorSystem("example") 32 | implicit val timeout = Timeout(5 seconds) 33 | 34 | val journalDir: File = new File("target/snapshots") 35 | val journal: ActorRef = LeveldbJournalProps(journalDir, native = false).createJournal 36 | val extension = EventsourcingExtension(system, journal) 37 | 38 | case class Increment(by: Int) 39 | 40 | class Processor extends Actor { this: Receiver => 41 | var counter = 0 42 | 43 | def receive = { 44 | case Increment(by) => { 45 | counter += by 46 | println(s"incremented counter by ${by} to ${counter} (snr = ${sequenceNr})") 47 | } 48 | case sr @ SnapshotRequest(pid, snr, _) => { 49 | sr.process(counter) 50 | println(s"processed snapshot request for ctr = ${counter} (snr = ${snr})") 51 | 52 | } 53 | case so @ SnapshotOffer(Snapshot(_, snr, time, ctr: Int)) => { 54 | counter = ctr 55 | println(s"accepted snapshot offer for ctr = ${counter} (snr = ${snr} time = ${time}})") 56 | } 57 | } 58 | } 59 | 60 | import system.dispatcher 61 | import extension._ 62 | 63 | val processor = processorOf(Props(new Processor with Receiver with Eventsourced { val id = 1 } )) 64 | 65 | extension.recover(replayParams.allWithSnapshot) 66 | processor ! Message(Increment(1)) 67 | processor ! Message(Increment(2)) 68 | 69 | (processor ? SnapshotRequest).mapTo[SnapshotSaved].onComplete { 70 | case Success(SnapshotSaved(pid, snr, time)) => println(s"snapshotting succeeded: pid = ${pid} snr = ${snr} time = ${time}") 71 | case Failure(e) => println(s"snapshotting failed: ${e.getMessage}") 72 | } 73 | 74 | processor ! Message(Increment(3)) 75 | 76 | Thread.sleep(1000) 77 | system.shutdown() 78 | } 79 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/example/StandaloneChannelExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.example 17 | 18 | import java.io.File 19 | 20 | import scala.concurrent.duration._ 21 | import scala.util._ 22 | 23 | import akka.actor._ 24 | import akka.pattern.ask 25 | import akka.util._ 26 | 27 | import org.eligosource.eventsourced.core._ 28 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps 29 | 30 | object StandaloneChannelExample extends App { 31 | implicit val system = ActorSystem("example") 32 | implicit val timeout = Timeout(50 seconds) 33 | 34 | import system.dispatcher 35 | 36 | val journal: ActorRef = LeveldbJournalProps(new File("target/standalone"), native = false).createJournal 37 | val extension = EventsourcingExtension(system, journal) 38 | 39 | class Destination extends Actor { this: Receiver => 40 | var ctr = 0 41 | 42 | def receive = { 43 | case event => { 44 | ctr += 1 45 | println(s"event = ${event}, counter = ${ctr}") 46 | if (ctr > 2) { 47 | sender ! s"received ${event}" 48 | confirm() 49 | ctr = 0 50 | } 51 | } 52 | } 53 | } 54 | 55 | val policy = RedeliveryPolicy().copy(confirmationTimeout = 1 second, restartDelay = 1 second, redeliveryDelay = 0 seconds) 56 | val destination: ActorRef = system.actorOf(Props(new Destination with Receiver)) 57 | val channel1 = extension.channelOf(ReliableChannelProps(1, destination, policy)) 58 | 59 | extension.recover() 60 | 61 | channel1 ? "a" onComplete { 62 | case Success(r) => println(s"reply = ${r}") 63 | case Failure(e) => println(s"error = ${e.getMessage}") 64 | } 65 | 66 | channel1 ? Message("b") onComplete { 67 | case Success(r) => println(s"reply = ${r}") 68 | case Failure(e) => println(s"error = ${e.getMessage}") 69 | } 70 | 71 | Thread.sleep(7000) 72 | system.shutdown() 73 | } 74 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/guide/FirstSteps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.guide 17 | 18 | import java.io.File 19 | 20 | import akka.actor._ 21 | 22 | import org.eligosource.eventsourced.core._ 23 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps 24 | 25 | class MyActor extends Actor { 26 | def receive = { 27 | case _ => 28 | } 29 | } 30 | 31 | class MyERActor extends MyActor with Receiver with Eventsourced { 32 | val id = 1 33 | } 34 | 35 | object FirstSteps extends App { 36 | implicit val system = ActorSystem("guide") 37 | 38 | // create a journal 39 | val journal: ActorRef = LeveldbJournalProps(new File("target/guide-1"), native = false).createJournal 40 | 41 | // create an event-sourcing extension 42 | val extension = EventsourcingExtension(system, journal) 43 | 44 | // event-sourced processor 45 | class Processor(destination: ActorRef) extends Actor { 46 | var counter = 0 47 | 48 | def receive = { 49 | case msg: Message => { 50 | // update internal state 51 | counter = counter + 1 52 | // print received event and number of processed event messages so far 53 | println("[processor] event = %s (%d)" format (msg.event, counter)) 54 | // send modified event message to destination 55 | destination ! msg.copy(event = "processed %d event messages so far" format counter) 56 | } 57 | } 58 | } 59 | 60 | // channel destination 61 | class Destination extends Actor { 62 | def receive = { 63 | case msg: Message => { 64 | // print event received from processor via channel 65 | println("[destination] event = '%s'" format msg.event) 66 | // confirm receipt of event message from channel 67 | msg.confirm() 68 | } 69 | } 70 | } 71 | 72 | // create channel destination 73 | val destination: ActorRef = system.actorOf(Props[Destination]) 74 | 75 | // create and register a channel 76 | val channel: ActorRef = extension.channelOf(DefaultChannelProps(1, destination)) 77 | 78 | // create and register event-sourced processor 79 | val processor: ActorRef = extension.processorOf(Props(new Processor(channel) with Eventsourced { val id = 1 } )) 80 | 81 | // recover registered processors by replaying journaled events 82 | extension.recover() 83 | 84 | // send event message to processor (will be journaled) 85 | processor ! Message("foo") 86 | 87 | // wait for all messages to arrive (graceful shutdown coming soon) 88 | Thread.sleep(1000) 89 | 90 | // then shutdown 91 | system.shutdown() 92 | } 93 | 94 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/guide/SenderReferences.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.guide 17 | 18 | import java.io.File 19 | 20 | import scala.concurrent.duration._ 21 | 22 | import akka.actor._ 23 | import akka.pattern.ask 24 | import akka.util.Timeout 25 | 26 | import org.eligosource.eventsourced.core._ 27 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps 28 | 29 | object SenderReferences extends App { 30 | implicit val system = ActorSystem("guide") 31 | implicit val timeout = Timeout(5 seconds) 32 | 33 | import system.dispatcher 34 | 35 | // create a journal 36 | val journal: ActorRef = LeveldbJournalProps(new File("target/guide-3"), native = false).createJournal 37 | 38 | // create an event-sourcing extension 39 | val extension = EventsourcingExtension(system, journal) 40 | 41 | // event-sourced processor 42 | class Processor(destination: ActorRef) extends Actor { 43 | var counter = 0 44 | 45 | def receive = { 46 | case msg: Message => { 47 | // update internal state 48 | counter = counter + 1 49 | // print received event and number of processed event messages so far 50 | println("[processor] event = %s (%d)" format (msg.event, counter)) 51 | // forward modified event message to destination (together with sender reference) 52 | destination forward msg.copy(event = "processed %d event messages so far" format counter) 53 | } 54 | } 55 | } 56 | 57 | // channel destination 58 | class Destination extends Actor { 59 | def receive = { 60 | case msg: Message => { 61 | // print event received from processor via channel 62 | println("[destination] event = '%s'" format msg.event) 63 | // confirm receipt of event message from channel 64 | msg.confirm() 65 | // reply to sender 66 | sender ! ("done processing event = %s" format msg.event) 67 | } 68 | } 69 | } 70 | 71 | // create channel destination 72 | val destination: ActorRef = system.actorOf(Props[Destination]) 73 | 74 | // create and register a channel 75 | val channel: ActorRef = extension.channelOf(DefaultChannelProps(1, destination)) 76 | 77 | // create and register event-sourced processor 78 | val processor: ActorRef = extension.processorOf(Props(new Processor(channel) with Eventsourced { val id = 1 } )) 79 | 80 | // recover registered processors by replaying journaled events 81 | extension.recover() 82 | 83 | // send event message to processor (will be journaled) 84 | // and asynchronously receive response (will not be journaled) 85 | processor ? Message("foo") onSuccess { 86 | case response => println(response) 87 | } 88 | 89 | // wait for all messages to arrive (graceful shutdown coming soon) 90 | Thread.sleep(1000) 91 | 92 | // then shutdown 93 | system.shutdown() 94 | } 95 | 96 | -------------------------------------------------------------------------------- /es-examples/src/main/scala/org/eligosource/eventsourced/guide/StackableTraits.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.guide 17 | 18 | import java.io.File 19 | 20 | import akka.actor._ 21 | 22 | import org.eligosource.eventsourced.core._ 23 | import org.eligosource.eventsourced.journal.leveldb.LeveldbJournalProps 24 | 25 | object StackableTraits extends App { 26 | implicit val system = ActorSystem("guide") 27 | 28 | // create a journal 29 | val journal: ActorRef = LeveldbJournalProps(new File("target/guide-2"), native = false).createJournal 30 | 31 | // create an event-sourcing extension 32 | val extension = EventsourcingExtension(system, journal) 33 | 34 | // event-sourced processor 35 | class Processor extends Actor { this: Emitter => 36 | var counter = 0 37 | 38 | def receive = { 39 | case event => { 40 | // update internal state 41 | counter = counter + 1 42 | // print received event and number of processed events so far 43 | println("[processor] event = %s (%d)" format (event, counter)) 44 | // send new event to destination channel 45 | emitter("destination") sendEvent ("processed %d events so far" format counter) 46 | } 47 | } 48 | } 49 | 50 | // channel destination 51 | class Destination extends Actor { 52 | def receive = { 53 | case event => { 54 | // print event received from processor via channel 55 | println("[destination] event = '%s'" format event) 56 | } 57 | } 58 | } 59 | 60 | // create and register event-sourced processor 61 | val processor: ActorRef = extension.processorOf(Props(new Processor with Emitter with Eventsourced { val id = 1 } )) 62 | 63 | // create channel destination 64 | val destination: ActorRef = system.actorOf(Props(new Destination with Receiver with Confirm)) 65 | 66 | // create and register a named channel 67 | extension.channelOf(DefaultChannelProps(1, destination).withName("destination")) 68 | 69 | // recover registered processors by replaying journaled events 70 | extension.recover() 71 | 72 | // send event message to processor (will be journaled) 73 | processor ! Message("foo") 74 | 75 | // wait for all messages to arrive (graceful shutdown coming soon) 76 | Thread.sleep(1000) 77 | 78 | // then shutdown 79 | system.shutdown() 80 | } 81 | 82 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies ++= Seq( 2 | "com.google.protobuf" % "protobuf-java" % "2.4.1" % "compile", 3 | "org.apache.hadoop" % "hadoop-core" % Version.Hadoop % "compile" 4 | exclude("commons-httpclient", "commons-httpclient") 5 | exclude("commons-beanutils", "commons-beanutils-core") 6 | exclude("commons-collections", "commons-collections") 7 | exclude("org.mortbay.jetty", "jsp-api-2.1") 8 | exclude("org.mortbay.jetty", "jsp-2.1") 9 | exclude("org.mortbay.jetty", "jetty-util") 10 | exclude("org.mortbay.jetty", "jetty") 11 | exclude("tomcat", "jasper-runtime") 12 | exclude("tomcat", "jasper-compiler") 13 | exclude("junit", "junit"), 14 | "com.typesafe.akka" %% "akka-remote" % Version.Akka % "test", 15 | "commons-io" % "commons-io" % "2.3" % "test" 16 | ) 17 | 18 | OsgiKeys.importPackage := Seq( 19 | "scala*;version=\"[2.10.0,2.11.0)\"", 20 | "akka*;version=\"[2.1.1,2.2.0)\"", 21 | "com.google.protobuf*;version=\"[2.4.0,2.5.0)\"" 22 | ) 23 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/protocol/Protocol.proto: -------------------------------------------------------------------------------- 1 | option java_package = "org.eligosource.eventsourced.journal.common.serialization"; 2 | option java_outer_classname = "Protocol"; 3 | 4 | /** 5 | * Journal command type. Only used for persistence. 6 | */ 7 | enum CommandType { 8 | WRITE_IN = 1; 9 | WRITE_OUT = 2; 10 | WRITE_ACK = 3; 11 | } 12 | 13 | /** 14 | * Journal command. Used for persistence only. 15 | */ 16 | message CommandProtocol { 17 | required CommandType commandType = 1; 18 | optional MessageProtocol message = 2; 19 | optional int32 processorId = 3; 20 | optional int32 channelId = 4; 21 | optional int64 sequenceNr = 5; 22 | } 23 | 24 | /** 25 | * Event message. Used for persistence and remoting. 26 | */ 27 | message MessageProtocol { 28 | optional bytes event = 2; 29 | optional bytes eventManifest = 3; 30 | optional int32 eventSerializerId = 4; 31 | optional int32 processorId = 6; 32 | optional int64 sequenceNr = 7; 33 | optional int64 timestamp = 9; 34 | optional string senderRef = 8; 35 | optional string confirmationTarget = 20; 36 | optional ConfirmationProtocol confirmationPrototype = 21; 37 | } 38 | 39 | /** 40 | * Confirmation message. Used for remoting only. 41 | */ 42 | message ConfirmationProtocol { 43 | optional int32 processorId = 1; 44 | optional int32 channelId = 2; 45 | optional int64 sequenceNr = 3; 46 | optional bool positive = 4; 47 | } -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | serializers { 4 | message = "org.eligosource.eventsourced.journal.common.serialization.MessageSerializer" 5 | confirmation = "org.eligosource.eventsourced.journal.common.serialization.ConfirmationSerializer" 6 | } 7 | serialization-bindings { 8 | "org.eligosource.eventsourced.core.Message" = message 9 | "org.eligosource.eventsourced.core.Confirmation" = confirmation 10 | } 11 | } 12 | } 13 | 14 | eventsourced { 15 | journal { 16 | snapshot-dispatcher { 17 | executor = "thread-pool-executor" 18 | type = PinnedDispatcher 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/JournalProps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common 17 | 18 | import akka.actor._ 19 | 20 | import org.eligosource.eventsourced.core.actor 21 | 22 | /** 23 | * Journal configuration object. 24 | */ 25 | trait JournalProps { 26 | /** 27 | * Optional journal name. 28 | */ 29 | def name: Option[String] 30 | 31 | /** 32 | * Optional dispatcher name. 33 | */ 34 | def dispatcherName: Option[String] 35 | 36 | /** 37 | * Creates and starts a new journal using the settings of this configuration object. 38 | */ 39 | def createJournal(implicit actorRefFactory: ActorRefFactory): ActorRef = { 40 | val journalRef = actor(createJournalActor, name, dispatcherName) 41 | if(readOnly) 42 | actor(new ReadOnlyFacade(journalRef)) 43 | else 44 | journalRef 45 | } 46 | 47 | /** 48 | * Creates a journal actor instance. 49 | */ 50 | protected def createJournalActor: Actor 51 | 52 | /** 53 | * Make journal read only (e.g. offline snapshot) 54 | */ 55 | def readOnly: Boolean 56 | } 57 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/ReadOnlyFacade.scala: -------------------------------------------------------------------------------- 1 | package org.eligosource.eventsourced.journal.common 2 | 3 | import akka.actor.{ActorRef, Actor} 4 | import org.eligosource.eventsourced.core.JournalProtocol.ReadCommand 5 | 6 | class ReadOnlyFacade(journal: ActorRef) extends Actor { 7 | def receive = { 8 | case cmd: ReadCommand => journal forward cmd 9 | case msg => //noop 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/serialization/SnapshotSerialization.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common.serialization 17 | 18 | import java.io._ 19 | 20 | import akka.util.ClassLoaderObjectInputStream 21 | 22 | import org.eligosource.eventsourced.core._ 23 | 24 | /** 25 | * Snapshot (de)serialization utility. 26 | */ 27 | trait SnapshotSerialization { 28 | /** Stream manager for snapshot IO */ 29 | def snapshotAccess: SnapshotAccess 30 | /** Snapshot serializer */ 31 | def snapshotSerializer: SnapshotSerializer 32 | 33 | /** 34 | * Serializes a snapshot using `snapshotSerializer` and `snapshotAccess`. 35 | */ 36 | def serializeSnapshot(snapshot: Snapshot): Unit = 37 | snapshotAccess.withOutputStream(snapshot)(snapshotSerializer.serializeSnapshot(_, snapshot, snapshot.state)) 38 | 39 | /** 40 | * Deserializes and returns a snapshot using `snapshotSerializer` and `snapshotAccess`. 41 | */ 42 | def deserializeSnapshot(metadata: SnapshotMetadata): Snapshot = { 43 | val state = snapshotAccess.withInputStream(metadata)(snapshotSerializer.deserializeSnapshot(_, metadata)) 44 | Snapshot(metadata.processorId, metadata.sequenceNr, metadata.timestamp, state) 45 | } 46 | } 47 | 48 | /** 49 | * Input and output stream management for snapshot IO. 50 | */ 51 | trait SnapshotAccess { 52 | /** 53 | * Provides a managed output stream for writing a state object. 54 | * 55 | * @param metadata snapshot metadata needed to create an output stream. 56 | * @param p called with the managed output stream. 57 | * @throws IOException if writing fails. 58 | */ 59 | def withOutputStream(metadata: SnapshotMetadata)(p: OutputStream => Unit) 60 | 61 | /** 62 | * Provides a managed input stream for reading a state object. 63 | * 64 | * @param metadata snapshot metadata needed to create an input stream. 65 | * @param f called with the managed input stream. 66 | * @return read snapshot. 67 | * @throws IOException if reading fails. 68 | */ 69 | def withInputStream(metadata: SnapshotMetadata)(f: InputStream => Any): Any 70 | } 71 | 72 | /** 73 | * State serializer. 74 | */ 75 | trait SnapshotSerializer { 76 | /** 77 | * Serializes a state object to an output stream. 78 | */ 79 | def serializeSnapshot(stream: OutputStream, metadata: SnapshotMetadata, state: Any): Unit 80 | /** 81 | * Deserializes a state object from an input stream. 82 | */ 83 | def deserializeSnapshot(stream: InputStream, metadata: SnapshotMetadata): Any 84 | } 85 | 86 | object SnapshotSerializer { 87 | /** 88 | * State serializer using Java serialization. 89 | */ 90 | val java = new SnapshotSerializer { 91 | def serializeSnapshot(stream: OutputStream, metadata: SnapshotMetadata, state: Any) = 92 | new ObjectOutputStream(stream).writeObject(state) 93 | 94 | def deserializeSnapshot(stream: InputStream, metadata: SnapshotMetadata) = 95 | new ClassLoaderObjectInputStream(getClass.getClassLoader, stream).readObject() 96 | } 97 | } -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/serialization/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common 17 | 18 | import org.eligosource.eventsourced.core.SnapshotMetadata 19 | 20 | package object serialization { 21 | implicit val snapshotMetadataOrdering = new Ordering[SnapshotMetadata] { 22 | def compare(x: SnapshotMetadata, y: SnapshotMetadata) = 23 | if (x.processorId == y.processorId) math.signum(x.sequenceNr - y.sequenceNr).toInt 24 | else x.processorId - y.processorId 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/snapshot/LocalFilesystemSnapshotting.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common.snapshot 17 | 18 | import java.io._ 19 | 20 | import scala.annotation.tailrec 21 | import scala.collection.SortedSet 22 | import scala.concurrent._ 23 | import scala.concurrent.duration._ 24 | import scala.util._ 25 | 26 | import akka.actor._ 27 | import akka.pattern.ask 28 | import akka.util._ 29 | 30 | import org.eligosource.eventsourced.core._ 31 | import org.eligosource.eventsourced.journal.common.serialization._ 32 | 33 | private [journal] trait LocalFilesystemSnapshotting { outer: Actor => 34 | private val FilenamePattern = """^snapshot-(\d+)-(\d+)-(\d+)""".r 35 | private var snapshotMetadata = Map.empty[Int, SortedSet[SnapshotMetadata]] 36 | 37 | lazy val snapshotSerialization = new SnapshotSerialization { 38 | val snapshotAccess = new LocalFilesystemSnapshotAccess(snapshotDir) 39 | val snapshotSerializer = outer.snapshotSerializer 40 | } 41 | 42 | def snapshotDir: File 43 | def snapshotSerializer: SnapshotSerializer 44 | def snapshotSaveTimeout: FiniteDuration 45 | 46 | def loadSnapshotSync(processorId: Int, snapshotFilter: SnapshotMetadata => Boolean): Option[Snapshot] = { 47 | @tailrec 48 | def go(metadata: SortedSet[SnapshotMetadata]): Option[Snapshot] = metadata.lastOption match { 49 | case None => None 50 | case Some(md) => { 51 | Try(snapshotSerialization.deserializeSnapshot(md)) match { 52 | case Success(ss) => Some(ss) 53 | case Failure(_) => go(metadata.init) // try older snapshot 54 | } 55 | } 56 | } 57 | 58 | for { 59 | mds <- snapshotMetadata.get(processorId) 60 | md <- go(mds.filter(snapshotFilter)) 61 | } yield md 62 | } 63 | 64 | def saveSnapshot(snapshot: Snapshot): Future[SnapshotSaved] = { 65 | val snapshotter = context.actorOf(Props(new Snapshotter) 66 | .withDispatcher("eventsourced.journal.snapshot-dispatcher")) 67 | snapshotter.ask(snapshot)(Timeout(snapshotSaveTimeout)).mapTo[SnapshotSaved] 68 | } 69 | 70 | def snapshotSaved(metadata: SnapshotMetadata) { 71 | snapshotMetadata.get(metadata.processorId) match { 72 | case None => snapshotMetadata = snapshotMetadata + (metadata.processorId -> SortedSet(metadata)) 73 | case Some(mds) => snapshotMetadata = snapshotMetadata + (metadata.processorId -> (mds + metadata)) 74 | } 75 | } 76 | 77 | def initSnapshotting() { 78 | if (!snapshotDir.exists) snapshotDir.mkdirs() 79 | 80 | val metadata = snapshotDir.listFiles.map(_.getName).collect { 81 | case FilenamePattern(pid, snr, tms) => SnapshotSaved(pid.toInt, snr.toLong, tms.toLong) 82 | } 83 | 84 | snapshotMetadata = SortedSet.empty[SnapshotMetadata] ++ metadata groupBy(_.processorId) 85 | } 86 | 87 | private class Snapshotter extends Actor { 88 | def receive = { 89 | case s: Snapshot => { 90 | Try(snapshotSerialization.serializeSnapshot(s)) match { 91 | case Success(_) => sender ! SnapshotSaved(s.processorId, s.sequenceNr, s.timestamp) 92 | case Failure(e) => sender ! Status.Failure(e) 93 | } 94 | context.stop(self) 95 | } 96 | } 97 | } 98 | } 99 | 100 | private [journal] class LocalFilesystemSnapshotAccess(snapshotDir: File) extends SnapshotAccess { 101 | def withOutputStream(metadata: SnapshotMetadata)(p: (OutputStream) => Unit) = 102 | withStream(new FileOutputStream(snapshotFile(metadata)), p) 103 | 104 | def withInputStream(metadata: SnapshotMetadata)(p: (InputStream) => Any) = 105 | withStream(new FileInputStream(snapshotFile(metadata)), p) 106 | 107 | private def withStream[A <: Closeable, B](stream: A, p: A => B): B = 108 | try { p(stream) } finally { stream.close() } 109 | 110 | private def snapshotFile(metadata: SnapshotMetadata): File = 111 | new File(snapshotDir, s"snapshot-${metadata.processorId}-${metadata.sequenceNr}-${metadata.timestamp}") 112 | } 113 | 114 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/support/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common 17 | 18 | import org.eligosource.eventsourced.core.Message 19 | 20 | package object support { 21 | /** 22 | * Decorates `p` to reset `Message.senderRef`. The `senderRef` is reset if the event message 23 | * 24 | * - has a `sequenceNr < initialCounter` and 25 | * - has a `senderRef` of type `PromiseActorRef` 26 | * 27 | * Otherwise, the `senderRef` is not changed. This ensures that references to temporary system 28 | * actors from previous application runs are not resolved. 29 | * 30 | * @param initialCounter initial counter value after journal recovery. 31 | * @param p event message handler. 32 | * @return decorated event message handler. 33 | */ 34 | def resetPromiseActorRef(initialCounter: Long)(p: Message => Unit): Message => Unit = msg => 35 | if (msg.senderRef == null) p(msg) 36 | // TODO: change to isInstanceOf[PromiseActorRef] 37 | else if (msg.senderRef.getClass.getSimpleName == "PromiseActorRef" && msg.sequenceNr < initialCounter) { 38 | p(msg.copy(senderRef = null)) 39 | } 40 | else p(msg) 41 | } 42 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/util/Key.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common.util 17 | 18 | /** 19 | * Storage key for some key-value based journal implementations. 20 | */ 21 | private [journal] case class Key( 22 | processorId: Int, 23 | initiatingChannelId: Int, 24 | sequenceNr: Long, 25 | confirmingChannelId: Int) 26 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/util/WriteInMsgQueue.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common.util 17 | 18 | import scala.collection.immutable.Queue 19 | 20 | import org.eligosource.eventsourced.core.JournalProtocol._ 21 | 22 | /** 23 | * Queue for WriteInMsg commands including a mechanism for matching acknowledgements. 24 | */ 25 | private [journal] class WriteInMsgQueue extends Iterable[(WriteInMsg, List[Int])] { 26 | var cmds = Queue.empty[WriteInMsg] 27 | var acks = Map.empty[Key, List[Int]] 28 | 29 | var len = 0 30 | 31 | def enqueue(cmd: WriteInMsg) { 32 | cmds = cmds.enqueue(cmd) 33 | len = len + 1 34 | } 35 | 36 | def dequeue(): (WriteInMsg, List[Int]) = { 37 | val (cmd, q) = cmds.dequeue 38 | val key = Key(cmd.processorId, 0, cmd.message.sequenceNr, 0) 39 | cmds = q 40 | len = len - 1 41 | acks.get(key) match { 42 | case Some(as) => { acks = acks - key; (cmd, as) } 43 | case None => (cmd, Nil) 44 | } 45 | } 46 | 47 | def ack(cmd: WriteAck) { 48 | val key = Key(cmd.processorId, 0, cmd.ackSequenceNr, 0) 49 | acks.get(key) match { 50 | case Some(as) => acks = acks + (key -> (cmd.channelId :: as)) 51 | case None => acks = acks + (key -> List(cmd.channelId)) 52 | } 53 | } 54 | 55 | def iterator = 56 | cmds.iterator.map(c => (c, acks.getOrElse(Key(c.processorId, 0, c.message.sequenceNr, 0), Nil))) 57 | 58 | override def size = 59 | len 60 | 61 | def clear() { 62 | acks = Map.empty 63 | cmds = Queue.empty 64 | len = 0 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/util/WriteOutMsgCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common.util 17 | 18 | import scala.collection.immutable.SortedMap 19 | 20 | import org.eligosource.eventsourced.core.Message 21 | import org.eligosource.eventsourced.core.JournalProtocol._ 22 | 23 | /** 24 | * Cache for WriteOutMsg commands. 25 | */ 26 | private [journal] class WriteOutMsgCache[L] { 27 | var cmds = SortedMap.empty[Key, (L, WriteOutMsg)] 28 | 29 | def update(cmd: WriteOutMsg, loc: L) { 30 | val key = Key(0, cmd.channelId, cmd.message.sequenceNr, 0) 31 | cmds = cmds + (key -> (loc, cmd)) 32 | } 33 | 34 | def update(cmd: DeleteOutMsg): Option[L] = { 35 | val key = Key(0, cmd.channelId, cmd.msgSequenceNr, 0) 36 | cmds.get(key) match { 37 | case Some((loc, msg)) => { cmds = cmds - key; Some(loc) } 38 | case None => None 39 | } 40 | } 41 | 42 | def messages(channelId: Int, fromSequenceNr: Long): Iterable[Message] = { 43 | val from = Key(0, channelId, fromSequenceNr, 0) 44 | val to = Key(0, channelId, Long.MaxValue, 0) 45 | cmds.range(from, to).values.map(_._2.message) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/main/scala/org/eligosource/eventsourced/journal/common/util/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common 17 | 18 | import java.nio.ByteBuffer 19 | 20 | package object util { 21 | private [journal] implicit val ordering = new Ordering[Key] { 22 | def compare(x: Key, y: Key) = 23 | if (x.processorId != y.processorId) 24 | x.processorId - y.processorId 25 | else if (x.initiatingChannelId != y.initiatingChannelId) 26 | x.initiatingChannelId - y.initiatingChannelId 27 | else if (x.sequenceNr != y.sequenceNr) 28 | math.signum(x.sequenceNr - y.sequenceNr).toInt 29 | else if (x.confirmingChannelId != y.confirmingChannelId) 30 | x.confirmingChannelId - y.confirmingChannelId 31 | else 0 32 | } 33 | 34 | implicit def counterToBytes(ctr: Long): Array[Byte] = 35 | ByteBuffer.allocate(8).putLong(ctr).array 36 | 37 | implicit def counterFromBytes(bytes: Array[Byte]): Long = 38 | ByteBuffer.wrap(bytes).getLong 39 | } 40 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | log-dead-letters = 0 3 | log-dead-letters-during-shutdown = off 4 | } 5 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/test/resources/persist.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | serializers { 4 | custom = "org.eligosource.eventsourced.journal.common.JournalSpec$CustomEventSerializer" 5 | } 6 | serialization-bindings { 7 | "org.eligosource.eventsourced.journal.common.JournalSpec$CustomEvent" = custom 8 | } 9 | } 10 | log-dead-letters = 0 11 | log-dead-letters-during-shutdown = off 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/test/resources/serializer.conf: -------------------------------------------------------------------------------- 1 | common { 2 | akka { 3 | actor { 4 | provider = "akka.remote.RemoteActorRefProvider" 5 | } 6 | remote { 7 | enabled-transports = ["akka.remote.netty.tcp"] 8 | netty.tcp { 9 | hostname = "127.0.0.1" 10 | } 11 | } 12 | loglevel = ERROR 13 | log-dead-letters = 0 14 | log-dead-letters-during-shutdown = off 15 | } 16 | } 17 | 18 | client { 19 | akka { 20 | remote { 21 | netty.tcp { 22 | port = 2653 23 | } 24 | } 25 | } 26 | } 27 | 28 | server { 29 | akka { 30 | remote { 31 | netty.tcp { 32 | port = 2652 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /es-journal/es-journal-common/src/test/scala/org/eligosource/eventsourced/journal/common/serialization/SerializerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.common.serialization 17 | 18 | import scala.concurrent.Await 19 | import scala.concurrent.duration._ 20 | 21 | import akka.actor._ 22 | import akka.pattern.ask 23 | import akka.util.Timeout 24 | 25 | import com.typesafe.config.ConfigFactory 26 | 27 | import org.scalatest.fixture._ 28 | import org.scalatest.matchers.MustMatchers 29 | 30 | import org.eligosource.eventsourced.core._ 31 | 32 | class SerializerSpec extends WordSpec with MustMatchers { 33 | import SerializerSpec._ 34 | 35 | type FixtureParam = Fixture 36 | 37 | class Fixture { 38 | implicit val timeout = Timeout(5 seconds) 39 | 40 | val config = ConfigFactory.load("serializer") 41 | val configCommon = config.getConfig("common") 42 | 43 | val server = ActorSystem("server", config.getConfig("server").withFallback(configCommon)) 44 | val client = ActorSystem("client", config.getConfig("client").withFallback(configCommon)) 45 | 46 | server.actorOf(Props[RemoteActor], "remote") 47 | 48 | Thread.sleep(100) 49 | 50 | def shutdown() { 51 | server.shutdown() 52 | client.shutdown() 53 | server.awaitTermination(5 seconds) 54 | client.awaitTermination(5 seconds) 55 | } 56 | } 57 | 58 | def withFixture(test: OneArgTest) { 59 | val fixture = new Fixture 60 | try { test(fixture) } finally { fixture.shutdown() } 61 | } 62 | 63 | "A MessageSerializer" must { 64 | "serialize event messages" in { fixture => 65 | import fixture._ 66 | import client.dispatcher 67 | 68 | val responseFuture = for { 69 | ActorIdentity(1, Some(remote)) <- client.actorSelection("akka.tcp://server@127.0.0.1:2652/user/remote") ? Identify(1) 70 | response <- remote ? Message("a", confirmationTarget = remote, confirmationPrototype = Confirmation(1, 1, 1, true)) 71 | } yield response 72 | 73 | Await.result(responseFuture, timeout.duration) must be(Message("success: a")) 74 | } 75 | } 76 | 77 | "A ConfirmationSerializer" must { 78 | "serialize confirmation messages" in { fixture => 79 | import fixture._ 80 | import client.dispatcher 81 | 82 | val responseFuture = for { 83 | ActorIdentity(1, Some(remote)) <- client.actorSelection("akka.tcp://server@127.0.0.1:2652/user/remote") ? Identify(1) 84 | response <- remote ? Confirmation(1, 1, 1, true) 85 | } yield response 86 | 87 | Await.result(responseFuture, timeout.duration) must be(Confirmation(2, 2, 2, false)) 88 | } 89 | } 90 | } 91 | 92 | object SerializerSpec { 93 | class RemoteActor extends Actor { 94 | def receive = { 95 | case msg: Message => { 96 | if (msg.confirmationTarget == self && msg.confirmationPrototype == Confirmation(1, 1, 1, true)) 97 | sender ! Message("success: %s" format msg.event) 98 | else 99 | sender ! Message("failure: %s" format msg.event) 100 | } 101 | case Confirmation(1, 1, 1, true) => { 102 | sender ! Confirmation(2, 2, 2, false) 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /es-journal/es-journal-dynamodb/build.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq("sonatype" at "https://oss.sonatype.org/content/repositories/snapshots/", 2 | "spray repo" at "http://repo.spray.io", 3 | "spray nightlies" at "http://nightlies.spray.io/") 4 | 5 | libraryDependencies ++= Seq( 6 | "com.sclasen" %% "spray-dynamodb" % "0.2.0-spray-20130712" % "compile", 7 | "com.sclasen" %% "spray-aws" % "0.2.0-spray-20130712" % "compile" 8 | ) 9 | 10 | OsgiKeys.importPackage := Seq( 11 | "scala*;version=\"[2.10.0,2.11.0)\"", 12 | "akka*;version=\"[2.1.0,3.0.0)\"" 13 | ) -------------------------------------------------------------------------------- /es-journal/es-journal-dynamodb/readme.md: -------------------------------------------------------------------------------- 1 | DynamoDB Journal 2 | ================ 3 | 4 | This is the DynamoDB backed Eventsourced Journal implementation. 5 | 6 | To use this journal, you will need to create a DynamoDB table in your AWS account. This can be done manually, or with the 7 | table creation utility included in this implementation. 8 | 9 | ## Throughput 10 | 11 | *Note* please take care to provision ample write throughput for your application. 12 | 13 | A good rule of thumb is to provision one write unit per eventsourced message per second in your app when using processors and default channels, 14 | and two write units per eventsourced message per second in your app when using processors and reliable channels. 15 | 16 | ### EC2 Instance sizing 17 | 18 | Note that for best throughput you should run the journal on an EC2 instance in the same region as your table. 19 | 20 | If you plan on writing more than 1000 messages per second or so, you should use a Cluster Compute instance that has 10 Gig networking, 21 | as this journal implementation is network bound. 22 | 23 | ## Configure DynamoDB 24 | 25 | Here is an example of using the table creation utility to create a DynamoDB table for your application's journal. 26 | It assumes that your AWS key and secret are set as the environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, 27 | and that eventsourced and the dynamodb journal implementation are dependencies in your project. 28 | 29 | ``` 30 | $ sbt console 31 | [info] Starting scala interpreter... 32 | [info] 33 | Welcome to Scala version 2.10.0 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_09). 34 | Type in expressions to have them evaluated. 35 | Type :help for more information. 36 | 37 | scala> import org.eligosource.eventsourced.journal.dynamodb.DynamoDBJournal 38 | import org.eligosource.eventsourced.journal.dynamodb.DynamoDBJournal 39 | 40 | scala> val readThroughput = 100 41 | readThroughput: Int = 100 42 | 43 | scala> val writeThroughput = 100 44 | writeThroughput: Int = 100 45 | 46 | scala> val tableName = "my-journal-table" 47 | tableName: String = my-journal-table 48 | 49 | scala> DynamoDBJournal.createJournal(tableName, readThroughput, writeThroughput) 50 | [INFO] [03/11/2013 11:48:56.566] [dynamo-util-spray.io.io-bridge-dispatcher-6] [akka://dynamo-util/user/io-bridge] akka://dynamo-util/user/io-bridge started 51 | [INFO] [03/11/2013 11:48:56.626] [dynamo-util-akka.actor.default-dispatcher-4] [akka://dynamo-util/user/default-http-client] Starting akka://dynamo-util/user/default-http-client 52 | 53 | scala> waiting for my-journal-table to be ACTIVE 54 | waiting for my-journal-table to be ACTIVE 55 | waiting for my-journal-table to be ACTIVE 56 | waiting for my-journal-table to be ACTIVE 57 | ... 58 | waiting for my-journal-table to be ACTIVE 59 | waiting for my-journal-table to be ACTIVE 60 | my-journal-table created. 61 | 62 | scala> DynamoDBJournal.utilDynamoSystem.shutdown 63 | 64 | [success] Total time: 72 s, completed Mar 11, 2013 11:53:50 AM 65 | 66 | > 67 | 68 | ``` 69 | 70 | The utilities also include a method to scale the throughput of your table up or down. Since you can only increase throughput by a factor of 2 per api call, 71 | the utility takes care of waiting for the table to be active and increasing the throughput again until the target throughput is reached. 72 | 73 | This makes scaling up to do throughput testing straightforward. 74 | 75 | *NOTE* AWS WILL ONLY ALLOW YOU TO SCALE DOWN YOUR THROUGHPUT ONCE PER 24HRS. Please be careful when using lots of throughput for testing. 76 | 77 | 78 | ## Using the DynamoDB Journal 79 | 80 | Once you have DynamoDB configured properly, you can start using the journal. In addition to the table name, you will need to select 81 | an application name to use with the journal, since one DynamoDB table can support multiple journals concurrently. 82 | 83 | Here is a basic example of how you can set up an eventsourced actor system, using the DynamoDB journal. 84 | 85 | ``` 86 | import org.eligosource.eventsourced.core._ 87 | import org.eligosource.eventsourced.journal.dynamodb.DynamoDBJournalProps 88 | import akka.actor._ 89 | import concurrent.duration._ 90 | 91 | implicit val actorSystem = ActorSystem("example") 92 | implicit val timeout = Timeout(5 seconds) 93 | 94 | import system.dispatcher 95 | 96 | val key = sys.env("AWS_ACCESS_KEY_ID") 97 | val secret = sys.env("AWS_SECRET_ACCESS_KEY") 98 | val table = "my-journal-table" 99 | val app = "my-eventsourced-app" 100 | val props = DynamoDBJournalProps(table, app, key, secret, asyncWriterCount = 16, system = actorSystem) 101 | 102 | // Event sourcing extension 103 | val extension = EventsourcingExtension(system, props.createJournal) 104 | 105 | // create processors and channels 106 | val p = extension.processorOf(...) 107 | val c = extension.channelOf(...) 108 | 109 | //recover application state 110 | extension.recover() 111 | 112 | //have at it 113 | 114 | p ! Message("go-go") 115 | 116 | ``` -------------------------------------------------------------------------------- /es-journal/es-journal-dynamodb/src/it/scala/org/eligosource/eventsourced/journal/dynamodb/DynamoDBJournalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.dynamodb 17 | 18 | import akka.actor.ActorSystem 19 | 20 | import org.eligosource.eventsourced.journal.common.{PersistentReplaySpec, JournalProps, PersistentJournalSpec} 21 | 22 | class DynamoDBJournalSpec extends PersistentJournalSpec with DynamoDBJournalSupport 23 | 24 | class DynamoDBReplaySpec extends PersistentReplaySpec with DynamoDBJournalSupport 25 | 26 | 27 | -------------------------------------------------------------------------------- /es-journal/es-journal-dynamodb/src/it/scala/org/eligosource/eventsourced/journal/dynamodb/DynamoDBJournalSupport.scala: -------------------------------------------------------------------------------- 1 | package org.eligosource.eventsourced.journal.dynamodb 2 | 3 | import akka.actor.{DeadLetter, Props, Actor, ActorSystem} 4 | import org.scalatest.{BeforeAndAfterAll, Suite, BeforeAndAfterEach} 5 | import org.eligosource.eventsourced.journal.common.JournalProps 6 | import akka.util.Timeout 7 | import concurrent.duration._ 8 | 9 | trait DynamoDBJournalSupport extends BeforeAndAfterEach with BeforeAndAfterAll { 10 | this: Suite => 11 | 12 | var _app = System.currentTimeMillis().toString 13 | 14 | def journalProps: JournalProps = { 15 | val key = sys.env("AWS_ACCESS_KEY_ID") 16 | val secret = sys.env("AWS_SECRET_ACCESS_KEY") 17 | val table = sys.env("TEST_TABLE") 18 | val app = _app 19 | DynamoDBJournalProps(table, app, key, secret, counterShards = 10, operationTimeout = Timeout(30 seconds)) 20 | } 21 | 22 | override protected def afterEach() { 23 | _app = System.currentTimeMillis().toString 24 | } 25 | 26 | } 27 | 28 | class Listener extends Actor { 29 | def receive = { 30 | case a:DeadLetter ⇒ println(a) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /es-journal/es-journal-dynamodb/src/main/scala/org/eligosource/eventsourced/journal/dynamodb/DynamoDBJournalProps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.dynamodb 17 | 18 | import scala.concurrent.duration._ 19 | 20 | import akka.actor.{ActorRefFactory, ActorSystem} 21 | import akka.util.Timeout 22 | 23 | import com.sclasen.spray.aws.dynamodb.DynamoDBClientProps 24 | 25 | import org.apache.hadoop.fs.{FileSystem, Path} 26 | 27 | import org.eligosource.eventsourced.journal.common.JournalProps 28 | import org.eligosource.eventsourced.journal.common.serialization.SnapshotSerializer 29 | import org.eligosource.eventsourced.journal.common.snapshot.HadoopFilesystemSnapshottingProps 30 | import org.eligosource.eventsourced.journal.common.snapshot.HadoopFilesystemSnapshotting.defaultLocalFilesystem 31 | 32 | /** 33 | * CounterShards should be set to at least a 10x multiple of the expected throughput 34 | * of the journal during the lifetime of a journal the counterShards can only be 35 | * increased. Decreases in counterShards will be ignored. The larger the value of 36 | * counterShards, the longer it takes to recover the storedCounter. 37 | */ 38 | case class DynamoDBJournalProps( 39 | journalTable: String, 40 | eventSourcedApp: String, 41 | key: String, 42 | secret: String, 43 | operationTimeout: Timeout = Timeout(10 seconds), 44 | replayOperationTimeout: Timeout = Timeout(1 minute), 45 | counterShards:Int=10000, 46 | factory: Option[ActorRefFactory] = None, 47 | dynamoEndpoint:String = "dynamodb.us-east-1.amazonaws.com", 48 | name: Option[String] = None, dispatcherName: Option[String] = None, 49 | snapshotPath: Path = new Path("snapshots"), 50 | snapshotSerializer: SnapshotSerializer = SnapshotSerializer.java, 51 | snapshotLoadTimeout: FiniteDuration = 1 hour, 52 | snapshotSaveTimeout: FiniteDuration = 1 hour, 53 | snapshotFilesystem: FileSystem = defaultLocalFilesystem, 54 | readOnly:Boolean = false) 55 | extends JournalProps with HadoopFilesystemSnapshottingProps[DynamoDBJournalProps] { 56 | 57 | /** 58 | * Java API. 59 | */ 60 | def withOperationTimeout(operationTimeout: Timeout) = 61 | copy(operationTimeout = operationTimeout) 62 | 63 | /** 64 | * Java API. 65 | */ 66 | def withReplayOperationTimeout(oeplayOperationTimeout: Timeout) = 67 | copy(replayOperationTimeout = replayOperationTimeout) 68 | 69 | /** 70 | * Java API. 71 | */ 72 | def withFactory(factory: ActorRefFactory) = 73 | copy(factory = Some(factory)) 74 | 75 | /** 76 | * Java API. 77 | */ 78 | def withDynamoEndpoint(dynamoEndpoint: String) = 79 | copy(dynamoEndpoint = dynamoEndpoint) 80 | 81 | /** 82 | * Java API. 83 | */ 84 | def withSnapshotPath(snapshotPath: Path) = 85 | copy(snapshotPath = snapshotPath) 86 | 87 | /** 88 | * Java API. 89 | */ 90 | def withSnapshotSerializer(snapshotSerializer: SnapshotSerializer) = 91 | copy(snapshotSerializer = snapshotSerializer) 92 | 93 | /** 94 | * Java API. 95 | */ 96 | def withSnapshotLoadTimeout(snapshotLoadTimeout: FiniteDuration) = 97 | copy(snapshotLoadTimeout = snapshotLoadTimeout) 98 | 99 | /** 100 | * Java API. 101 | */ 102 | def withSnapshotSaveTimeout(snapshotSaveTimeout: FiniteDuration) = 103 | copy(snapshotSaveTimeout = snapshotSaveTimeout) 104 | 105 | /** 106 | * Java API. 107 | */ 108 | def withSnapshotFilesystem(snapshotFilesystem: FileSystem) = 109 | copy(snapshotFilesystem = snapshotFilesystem) 110 | 111 | /** 112 | * Java API. 113 | */ 114 | def withReadOnly(ro:Boolean) = 115 | copy(readOnly = ro) 116 | 117 | def createJournalActor = 118 | new DynamoDBJournal(this) 119 | 120 | def clientProps(system:ActorSystem) = 121 | DynamoDBClientProps(key, secret, operationTimeout, system, factory.getOrElse(system), dynamoEndpoint) 122 | } 123 | 124 | object DynamoDBJournalProps { 125 | /** 126 | * Java API. 127 | */ 128 | def create(journalTable: String, eventSourcedApp: String, key: String, secret: String) = 129 | DynamoDBJournalProps(journalTable, eventSourcedApp, key, secret) 130 | } -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/build.sbt: -------------------------------------------------------------------------------- 1 | fork := true 2 | 3 | libraryDependencies ++= Seq( 4 | "org.apache.hbase" % "hbase" % "0.94.5" % "compile", 5 | "org.hbase" % "asynchbase" % "1.4.1" 6 | exclude("org.slf4j", "log4j-over-slf4j") 7 | exclude("org.slf4j", "jcl-over-slf4j"), 8 | "org.slf4j" % "slf4j-log4j12" % "1.6.0", 9 | "org.scalatest" %% "scalatest" % Version.ScalaTest % "it", 10 | "org.apache.hadoop" % "hadoop-test" % "1.1.1" % "it", 11 | "org.apache.hbase" % "hbase" % "0.94.5" % "it" classifier "tests" 12 | ) 13 | 14 | OsgiKeys.importPackage := Seq( 15 | "scala*;version=\"[2.10.0,2.11.0)\"", 16 | "akka*;version=\"[2.1.1,2.2.0)\"" 17 | ) 18 | -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/readme.md: -------------------------------------------------------------------------------- 1 | HBase Journal 2 | ============= 3 | 4 | [Eventsourced](https://github.com/eligosource/eventsourced) applications create an [HBase](http://hbase.apache.org) backed journal using the [HBaseJournalProps](http://eligosource.github.com/eventsourced/api/snapshot/#org.eligosource.eventsourced.journal.hbase.HBaseJournalProps) configuration object. 5 | 6 | Properties 7 | ---------- 8 | 9 | An HBase backed journal has the following properties when running on a real HBase cluster: 10 | 11 | - High availability. 12 | - Horizontal scalability of writes by adding nodes. 13 | - Horizontal scalability of reads (replay) by adding nodes. 14 | - Writes are evenly distributed across regions (region servers) 15 | - All reads and writes are asynchronous and non-blocking. 16 | - Efficient per-processor recovery. 17 | - Efficient per-channel recovery (applies to reliable channels). 18 | 19 | Status 20 | ------ 21 | 22 | Experimental but fully functional. 23 | 24 | Getting started 25 | --------------- 26 | 27 | This section shows how to initialize an HBase journal that connects to a local, standalone HBase instance. 28 | 29 | First, download, install and start a standalone HBase instance by following the instructions in the HBase [quick start guide](http://hbase.apache.org/book/quickstart.html). Then start `sbt` from the `eventsourced` project root and enter: 30 | 31 | > project eventsourced-journal-hbase 32 | > org.eligosource.eventsourced.journal.hbase.CreateTable localhost 33 | 34 | Add the required depedencies to your project's `build.sbt` file: 35 | 36 | resolvers += "Eligosource Snapshots" at "http://repo.eligotech.com/nexus/content/repositories/eligosource-snapshots" 37 | 38 | libraryDependencies += "org.eligosource" %% "eventsourced-core" % "0.7-SNAPSHOT" 39 | 40 | libraryDependencies += "org.eligosource" %% "eventsourced-journal-hbase" % "0.7-SNAPSHOT" 41 | 42 | Initialize the HBase journal in your application: 43 | 44 | ```scala 45 | import akka.actor._ 46 | import org.eligosource.eventsourced.core._ 47 | import org.eligosource.eventsourced.journal.hbase.HBaseJournalProps 48 | 49 | implicit val system = ActorSystem("example") 50 | 51 | // create and start the HBase journal 52 | val journal: ActorRef = HBaseJournalProps("localhost").createJournal 53 | 54 | // create an event-sourcing extension that uses the HBase journal 55 | val extension = EventsourcingExtension(system, journal) 56 | 57 | // ... 58 | ``` 59 | 60 | Cluster setup 61 | ------------- 62 | 63 | For storing event messages to a real HBase cluster, a table must be initially created with the [CreateTable](http://eligosource.github.com/eventsourced/api/snapshot/#org.eligosource.eventsourced.journal.hbase.CreateTable$) utility as shown in the following example: 64 | 65 | ```scala 66 | import org.eligosource.eventsourced.journal.hbase.CreateTable 67 | 68 | class Example { 69 | val zookeeperQuorum = "localhost:2181" // comma separated list of servers in the ZooKeeper quorum 70 | val tableName = "event" // name of the event message table to be created 71 | val partitionCount = 16 // number of regions the event message table is pre-split 72 | 73 | CreateTable(zookeeperQuorum, tableName, partitionCount) 74 | } 75 | ``` 76 | 77 | This creates an event message table with the name `event` that is pre-split into 16 regions. The journal actor will evenly distribute (partition) event messages across regions. 78 | -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/src/it/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=ERROR, stdout 2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 3 | log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout 4 | -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/src/it/scala/org/eligosource/eventsourced/journal/hbase/HBaseCleanup.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.hbase 17 | 18 | import org.apache.hadoop.hbase.client._ 19 | 20 | trait HBaseCleanup { 21 | def client: HTable 22 | 23 | def cleanup() { 24 | import scala.collection.mutable.Buffer 25 | import scala.collection.JavaConverters._ 26 | 27 | val deletes = for { 28 | i <- 0 to 3 29 | j <- 0 to 10 30 | k <- 0 to 100 31 | } yield Seq( 32 | new Delete(InMsgKey(i, j, k).toBytes), 33 | new Delete(OutMsgKey(i, j, k).toBytes)) 34 | 35 | client.delete(Buffer(deletes.flatten: _*).asJava) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/src/it/scala/org/eligosource/eventsourced/journal/hbase/HBaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.hbase 17 | 18 | import org.apache.hadoop.hbase._ 19 | import org.apache.hadoop.hbase.client._ 20 | 21 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} 22 | 23 | import org.eligosource.eventsourced.journal.common.{ReadOnlyJournalSpec, PersistentReplaySpec, PersistentJournalSpec} 24 | 25 | trait HBaseSpec extends HBaseCleanup with BeforeAndAfterEach with BeforeAndAfterAll { self: Suite => 26 | var port: Int = 0 27 | var util: HBaseTestingUtility = _ 28 | var admin: HBaseAdmin = _ 29 | var client: HTable = _ 30 | 31 | def zookeeperQuorum = "localhost:%d" format port 32 | lazy val props = { 33 | HBaseJournalProps(zookeeperQuorum) 34 | .withReplayChunkSize(8) 35 | .withSnapshotFilesystem(util.getTestFileSystem) 36 | } 37 | 38 | def journalProps = props 39 | 40 | def readOnlyJournalProps = props.withReadOnly(true) 41 | 42 | override def afterEach() { 43 | cleanup() 44 | } 45 | 46 | override def beforeAll() { 47 | util = new HBaseTestingUtility() 48 | util.startMiniCluster() 49 | port = util.getZkCluster.getClientPort 50 | 51 | CreateTable(util.getConfiguration, DefaultTableName, DefaultPartitionCount) 52 | client = new HTable(util.getConfiguration, DefaultTableName) 53 | } 54 | 55 | override def afterAll() = try { 56 | client.close() 57 | util.shutdownMiniCluster() 58 | util.cleanupTestDir() 59 | } catch { case _: Throwable => /* ignore */ } 60 | } 61 | 62 | class HBaseJournalSpec extends PersistentJournalSpec with ReadOnlyJournalSpec with HBaseSpec 63 | class HBaseReplaySpec extends PersistentReplaySpec with HBaseSpec 64 | -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/src/it/scala/org/eligosource/eventsourced/journal/hbase/HBaseSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.hbase 17 | 18 | import org.apache.hadoop.hbase.HBaseConfiguration 19 | import org.apache.hadoop.hbase.client.HTable 20 | 21 | object HBaseSupport { 22 | val config = HBaseConfiguration.create() 23 | val client = new HTable(config, "event") 24 | } 25 | 26 | trait HBaseSupport extends HBaseCleanup { 27 | val client = HBaseSupport.client 28 | val journalProps = HBaseJournalProps("localhost").withReplayChunkSize(8) 29 | } 30 | -------------------------------------------------------------------------------- /es-journal/es-journal-hbase/src/main/scala/org/eligosource/eventsourced/journal/hbase/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal 17 | 18 | import scala.concurrent._ 19 | 20 | import java.nio.ByteBuffer 21 | 22 | import com.stumbleupon.async.{Callback, Deferred} 23 | 24 | import org.apache.hadoop.hbase.util.Bytes 25 | 26 | package object hbase { 27 | val DefaultTableName = "event" 28 | val DefaultPartitionCount = 4 29 | 30 | val ColumnFamilyName = "ef" 31 | val ColumnFamilyNameBytes = Bytes.toBytes(ColumnFamilyName) 32 | 33 | val MsgColumnName = "msg" 34 | val MsgColumnNameBytes = Bytes.toBytes(MsgColumnName) 35 | 36 | val SequenceNrColumnName = "snr" 37 | val SequenceNrColumnNameBytes = Bytes.toBytes(SequenceNrColumnName) 38 | 39 | val PartitionCountColumnName = "pct" 40 | val PartitionCountColumnNameBytes = Bytes.toBytes(PartitionCountColumnName) 41 | 42 | val TimestampColumnName = "tms" 43 | val TimestampColumnNameBytes = Bytes.toBytes(TimestampColumnName) 44 | 45 | val AckColumnPrefix = "ack" 46 | def ackColumnBytes(channelId: Int) = Bytes.toBytes(AckColumnPrefix + channelId) 47 | 48 | class PartitionCountNotFoundException(table: String) extends Exception( 49 | s"Partition count not stored in table ${table}" 50 | ) 51 | 52 | private [hbase] sealed trait Key { 53 | def partition: Int 54 | def source: Int 55 | def sequenceNumber: Long 56 | def withSequenceNumber(f: Long => Long): Key 57 | def withPartition(p: Int): Key 58 | def toBytes = { 59 | val bb = ByteBuffer.allocate(16) 60 | bb.putInt(partition) 61 | bb.putInt(source) 62 | bb.putLong(sequenceNumber) 63 | bb.array 64 | } 65 | } 66 | 67 | private [hbase] case class InMsgKey(partition: Int, processorId: Int, sequenceNumber: Long) extends Key { 68 | def source = processorId 69 | def withSequenceNumber(f: (Long) => Long) = copy(sequenceNumber = f(sequenceNumber)) 70 | def withPartition(p: Int) = copy(partition = p) 71 | } 72 | 73 | private [hbase] case class OutMsgKey(partition: Int, channelId: Int, sequenceNumber: Long) extends Key { 74 | def source = -channelId 75 | def withSequenceNumber(f: (Long) => Long) = copy(sequenceNumber = f(sequenceNumber)) 76 | def withPartition(p: Int) = copy(partition = p) 77 | } 78 | 79 | private [hbase] case class CounterKey(partition: Int, sequenceNumber: Long) extends Key { 80 | val source = 0 81 | def withSequenceNumber(f: (Long) => Long) = copy(sequenceNumber = f(sequenceNumber)) 82 | def withPartition(p: Int) = copy(partition = p) 83 | } 84 | 85 | private [hbase] case class PartitionCountKey(upper: Boolean = false) extends Key { 86 | val partition = -1 87 | val source = 0 88 | val sequenceNumber = if (upper) 1L else 0L 89 | 90 | def withPartition(p: Int) = 91 | throw new UnsupportedOperationException("withPartition on PartitionCountKey") 92 | 93 | def withSequenceNumber(f: (Long) => Long) = 94 | throw new UnsupportedOperationException("withSequenceNumber on PartitionCountKey") 95 | } 96 | 97 | implicit def deferredToFuture[A](d: Deferred[A]): Future[A] = { 98 | val promise = Promise[A]() 99 | d.addCallback(new Callback[Unit, A] { 100 | def call(a: A) = promise.success(a) 101 | }) 102 | d.addErrback(new Callback[Unit, Throwable] { 103 | def call(t: Throwable) = promise.failure(t) 104 | }) 105 | promise.future 106 | } 107 | 108 | // ------------------------- 109 | // temporary ... 110 | // ------------------------- 111 | 112 | private [hbase] def longToBytes(l: Long): Array[Byte] = 113 | ByteBuffer.allocate(8).putLong(l).array() 114 | 115 | private [hbase] def longFromBytes(b: Array[Byte]): Long = 116 | ByteBuffer.wrap(b).getLong 117 | 118 | private [hbase] def bitString(key: Key): String = { 119 | "%s-%s-%s" format( 120 | bitString(key.partition), 121 | bitString(key.source), 122 | bitString(key.sequenceNumber)) 123 | } 124 | 125 | private [hbase] def bitString(i: Int): String = (for { 126 | p <- 31 to 0 by -1 127 | } yield (i >> p) & 1) mkString("") 128 | 129 | private [hbase] def bitString(l: Long): String = (for { 130 | p <- 64 to 0 by -1 131 | } yield (l >> p) & 1) mkString("") 132 | } 133 | -------------------------------------------------------------------------------- /es-journal/es-journal-inmem/build.sbt: -------------------------------------------------------------------------------- 1 | OsgiKeys.importPackage := Seq( 2 | "scala*;version=\"[2.10.0,2.11.0)\"", 3 | "akka*;version=\"[2.1.1,2.2.0)\"" 4 | ) 5 | -------------------------------------------------------------------------------- /es-journal/es-journal-inmem/src/main/scala/org/eligosource/eventsourced/journal/inmem/InmemJournal.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.inmem 17 | 18 | import scala.collection.immutable.SortedMap 19 | import scala.concurrent.Future 20 | 21 | import akka.actor._ 22 | 23 | import org.eligosource.eventsourced.core._ 24 | import org.eligosource.eventsourced.journal.common.support.SynchronousWriteReplaySupport 25 | import org.eligosource.eventsourced.journal.common.util._ 26 | 27 | /** 28 | * In-memory journal for testing purposes. 29 | */ 30 | private [eventsourced] class InmemJournal extends SynchronousWriteReplaySupport { 31 | import JournalProtocol._ 32 | 33 | var redoMap = SortedMap.empty[Key, Any] 34 | var snapshots = Map.empty[Int, List[Snapshot]] 35 | 36 | def executeWriteInMsg(cmd: WriteInMsg) { 37 | redoMap = redoMap + (Key(cmd.processorId, 0, counter, 0) -> cmd.message.clearConfirmationSettings) 38 | } 39 | 40 | def executeWriteOutMsg(cmd: WriteOutMsg) { 41 | redoMap = redoMap + (Key(Int.MaxValue, cmd.channelId, counter, 0) -> cmd.message.clearConfirmationSettings) 42 | 43 | if (cmd.ackSequenceNr != SkipAck) { 44 | redoMap = redoMap + (Key(cmd.ackProcessorId, 0, cmd.ackSequenceNr, cmd.channelId) -> null) 45 | } 46 | } 47 | 48 | def executeWriteAck(cmd: WriteAck) { 49 | redoMap = redoMap + (Key(cmd.processorId, 0, cmd.ackSequenceNr, cmd.channelId) -> null) 50 | } 51 | 52 | def executeDeleteOutMsg(cmd: DeleteOutMsg) { 53 | redoMap = redoMap - Key(Int.MaxValue, cmd.channelId, cmd.msgSequenceNr, 0) 54 | } 55 | 56 | def executeBatchReplayInMsgs(cmds: Seq[ReplayInMsgs], p: (Message, ActorRef) => Unit) { 57 | cmds.foreach(cmd => replay(cmd.processorId, 0, cmd.fromSequenceNr, cmd.toSequenceNr, msg => p(msg, cmd.target))) 58 | } 59 | 60 | def executeReplayInMsgs(cmd: ReplayInMsgs, p: Message => Unit) { 61 | replay(cmd.processorId, 0, cmd.fromSequenceNr, cmd.toSequenceNr, p) 62 | } 63 | 64 | def executeReplayOutMsgs(cmd: ReplayOutMsgs, p: Message => Unit) { 65 | replay(Int.MaxValue, cmd.channelId, cmd.fromSequenceNr, Long.MaxValue, p) 66 | } 67 | 68 | override def loadSnapshotSync(processorId: Int, snapshotFilter: SnapshotMetadata => Boolean) = for { 69 | ss <- snapshots.get(processorId) 70 | fs <- ss.filter(snapshotFilter).headOption 71 | } yield fs 72 | 73 | override def saveSnapshot(snapshot: Snapshot) = { 74 | snapshots.get(snapshot.processorId) match { 75 | case None => snapshots = snapshots + (snapshot.processorId -> List(snapshot)) 76 | case Some(ss) => snapshots = snapshots + (snapshot.processorId -> (snapshot :: ss)) 77 | } 78 | Future.successful(SnapshotSaved(snapshot.processorId, snapshot.sequenceNr, snapshot.timestamp)) 79 | } 80 | 81 | def snapshotSaved(metadata: SnapshotMetadata) {} 82 | 83 | def storedCounter = counter 84 | 85 | private def replay(processorId: Int, channelId: Int, fromSequenceNr: Long, toSequenceNr: Long, p: Message => Unit) { 86 | val startKey = Key(processorId, channelId, fromSequenceNr, 0) 87 | val stopKey = Key(processorId, channelId, toSequenceNr, 0) 88 | val iter = redoMap.from(startKey).to(stopKey).iterator.buffered 89 | replay(iter, startKey, p) 90 | } 91 | 92 | @scala.annotation.tailrec 93 | private def replay(iter: BufferedIterator[(Key, Any)], key: Key, p: Message => Unit) { 94 | if (iter.hasNext) { 95 | val nextEntry = iter.next() 96 | val nextKey = nextEntry._1 97 | if (nextKey.confirmingChannelId != 0) { 98 | // phantom ack (just advance iterator) 99 | replay(iter, nextKey, p) 100 | } else if (key.processorId == nextKey.processorId && 101 | key.initiatingChannelId == nextKey.initiatingChannelId) { 102 | val msg = nextEntry._2.asInstanceOf[Message] 103 | val channelIds = confirmingChannelIds(iter, nextKey, Nil) 104 | p(msg.copy(acks = channelIds)) 105 | replay(iter, nextKey, p) 106 | } 107 | } 108 | } 109 | 110 | @scala.annotation.tailrec 111 | private def confirmingChannelIds(iter: BufferedIterator[(Key, Any)], key: Key, channelIds: List[Int]): List[Int] = { 112 | if (iter.hasNext) { 113 | val nextEntry = iter.head 114 | val nextKey = nextEntry._1 115 | if (key.processorId == nextKey.processorId && 116 | key.initiatingChannelId == nextKey.initiatingChannelId && 117 | key.sequenceNr == nextKey.sequenceNr) { 118 | iter.next() 119 | confirmingChannelIds(iter, nextKey, nextKey.confirmingChannelId :: channelIds) 120 | } else channelIds 121 | } else channelIds 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /es-journal/es-journal-inmem/src/main/scala/org/eligosource/eventsourced/journal/inmem/InmemJournalProps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.inmem 17 | 18 | import akka.actor.Actor 19 | 20 | import org.eligosource.eventsourced.journal.common.JournalProps 21 | 22 | /** 23 | * Configuration object for an in-memory based journal. For testing purposes only. 24 | * 25 | * Journal actors can be created from a configuration object as follows: 26 | * 27 | * {{{ 28 | * import akka.actor._ 29 | * 30 | * import org.eligosource.eventsourced.core.Journal 31 | * import org.eligosource.eventsourced.journal.inmem.InmemJournalProps 32 | * 33 | * implicit val system: ActorSystem = ... 34 | * 35 | * val journal: ActorRef = Journal(InmemJournalProps()) 36 | * }}} 37 | * 38 | * @param name Optional journal actor name. 39 | * @param dispatcherName Optional journal actor dispatcher name. 40 | */ 41 | case class InmemJournalProps( 42 | name: Option[String] = None, 43 | dispatcherName: Option[String] = None, 44 | readOnly: Boolean = false) extends JournalProps { 45 | 46 | /** 47 | * Java API. 48 | * 49 | * Returns a new `InmemJournalProps` with specified journal actor name. 50 | */ 51 | def withName(name: String) = 52 | copy(name = Some(name)) 53 | 54 | /** 55 | * Java API. 56 | * 57 | * Returns a new `InmemJournalProps` with specified journal actor dispatcher name. 58 | */ 59 | def withDispatcherName(dispatcherName: String) = 60 | copy(dispatcherName = Some(dispatcherName)) 61 | 62 | /** 63 | * Java API. 64 | */ 65 | def withReadOnly(ro:Boolean) = 66 | copy(readOnly = ro) 67 | 68 | def createJournalActor: Actor = 69 | new InmemJournal 70 | } 71 | 72 | object InmemJournalProps { 73 | /** 74 | * Java API. 75 | */ 76 | def create = 77 | new InmemJournalProps() 78 | } -------------------------------------------------------------------------------- /es-journal/es-journal-inmem/src/test/scala/org/eligosource/eventsourced/journal/inmem/InmemJournalLifecycle.scala: -------------------------------------------------------------------------------- 1 | package org.eligosource.eventsourced.journal.inmem 2 | 3 | trait InmemJournalLifecycle { 4 | def journalProps = InmemJournalProps() 5 | } 6 | -------------------------------------------------------------------------------- /es-journal/es-journal-inmem/src/test/scala/org/eligosource/eventsourced/journal/inmem/InmemJournalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.inmem 17 | 18 | import org.eligosource.eventsourced.journal.common.JournalSpec 19 | 20 | class InmemJournalSpec extends JournalSpec with InmemJournalLifecycle 21 | -------------------------------------------------------------------------------- /es-journal/es-journal-inmem/src/test/scala/org/eligosource/eventsourced/journal/inmem/InmemReplaySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.inmem 17 | 18 | import org.eligosource.eventsourced.journal.common.ReplaySpec 19 | 20 | class InmemReplaySpec extends ReplaySpec with InmemJournalLifecycle -------------------------------------------------------------------------------- /es-journal/es-journal-journalio/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.osgi.SbtOsgi 2 | 3 | libraryDependencies ++= Seq( 4 | "com.github.sbtourist" % "journalio" % "1.3" % "compile" 5 | ) 6 | 7 | OsgiKeys.importPackage := Seq( 8 | "scala*;version=\"[2.10.0,2.11.0)\"", 9 | "akka*;version=\"[2.1.1,2.2.0)\"", 10 | "journal.io.api;version=\"[1.3,2.0)\";resolution:=optional" 11 | ) 12 | -------------------------------------------------------------------------------- /es-journal/es-journal-journalio/src/test/scala/org/eligosource/eventsourced/journal/journalio/JournalioJournalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.journalio 17 | 18 | import java.io.File 19 | 20 | import akka.actor.ActorSystem 21 | 22 | import org.apache.commons.io.FileUtils 23 | import org.scalatest.BeforeAndAfterEach 24 | 25 | import akka.actor.ActorRef 26 | 27 | import org.eligosource.eventsourced.core.JournalProtocol.ReplayInMsgs 28 | import org.eligosource.eventsourced.journal.common.PersistentJournalSpec 29 | 30 | class JournalioJournalSpec extends PersistentJournalSpec with BeforeAndAfterEach { 31 | def journalProps = JournalioJournalProps(JournalioJournalSpec.journalDir) 32 | 33 | override def prepareJournal(journal: ActorRef, system: ActorSystem) { 34 | journal ! ReplayInMsgs(1, 0, system.deadLetters) 35 | } 36 | 37 | override def afterEach() { 38 | FileUtils.deleteDirectory(JournalioJournalSpec.journalDir) 39 | } 40 | } 41 | 42 | object JournalioJournalSpec { 43 | val journalDir = new File("es-journal/es-journal-journalio/target/journal") 44 | } -------------------------------------------------------------------------------- /es-journal/es-journal-journalio/src/test/scala/org/eligosource/eventsourced/journal/journalio/JournalioReplaySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.journalio 17 | 18 | import org.apache.commons.io.FileUtils 19 | import org.scalatest.BeforeAndAfterEach 20 | 21 | import org.eligosource.eventsourced.journal.common.PersistentReplaySpec 22 | 23 | class JournalioReplaySpec extends PersistentReplaySpec with BeforeAndAfterEach { 24 | def journalProps = JournalioJournalProps(JournalioJournalSpec.journalDir) 25 | 26 | override def afterEach() { 27 | FileUtils.deleteDirectory(JournalioJournalSpec.journalDir) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /es-journal/es-journal-leveldb/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.osgi.SbtOsgi 2 | 3 | Nobootcp.settings 4 | 5 | libraryDependencies ++= Seq( 6 | "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.6" % "compile", 7 | "org.iq80.leveldb" % "leveldb" % "0.5" % "compile" 8 | ) 9 | 10 | OsgiKeys.importPackage := Seq( 11 | "scala*;version=\"[2.10.0,2.11.0)\"", 12 | "akka*;version=\"[2.1.1,2.2.0)\"", 13 | "org.fusesource.leveldbjni;version=\"[1.5,2.0.0)\";resolution:=optional", 14 | "org.iq80.leveldb;version=\"[1.5,2.0.0)\";resolution:=optional" 15 | ) 16 | -------------------------------------------------------------------------------- /es-journal/es-journal-leveldb/src/main/scala/org/eligosource/eventsourced/journal/leveldb/LeveldbJournal.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.leveldb 17 | 18 | import akka.actor._ 19 | 20 | import org.iq80.leveldb._ 21 | 22 | import org.eligosource.eventsourced.journal.common.serialization.CommandSerialization 23 | 24 | trait LeveldbJournal { this: Actor => 25 | def props: LeveldbJournalProps 26 | 27 | val levelDbReadOptions = new ReadOptions().verifyChecksums(props.checksum) 28 | val levelDbWriteOptions = new WriteOptions().sync(props.fsync) 29 | val leveldb = factory.open(props.dir, leveldbOptions) 30 | 31 | val serialization = CommandSerialization(context.system) 32 | 33 | def factory = { 34 | if (props.native) org.fusesource.leveldbjni.JniDBFactory.factory 35 | else org.iq80.leveldb.impl.Iq80DBFactory.factory 36 | } 37 | 38 | def leveldbOptions = { 39 | val options = new Options().createIfMissing(true) 40 | if (props.native) options else options.compressionType(CompressionType.NONE) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /es-journal/es-journal-leveldb/src/test/scala/org/eligosource/eventsourced/journal/leveldb/LeveldbJournalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.leveldb 17 | 18 | import java.io.File 19 | 20 | import org.eligosource.eventsourced.core._ 21 | import org.eligosource.eventsourced.core.JournalProtocol._ 22 | import org.eligosource.eventsourced.journal.common._ 23 | 24 | abstract class LeveldbJournalSpec extends PersistentJournalSpec { 25 | import JournalSpec._ 26 | 27 | "persist input messages with a custom event serializer" in { fixture => 28 | import fixture._ 29 | 30 | journal ! WriteInMsg(1, Message(CustomEvent("test-1")), writeTarget) 31 | journal ! WriteInMsg(1, Message(CustomEvent("test-2")), writeTarget) 32 | 33 | journal ! ReplayInMsgs(1, 0, replayTarget) 34 | 35 | dequeue(replayQueue) { m => m must be(Message(CustomEvent("TEST-1"), sequenceNr = 1, timestamp = m.timestamp)) } 36 | dequeue(replayQueue) { m => m must be(Message(CustomEvent("TEST-2"), sequenceNr = 2, timestamp = m.timestamp)) } 37 | } 38 | "persist output messages with a custom event serializer" in { fixture => 39 | import fixture._ 40 | 41 | journal ! WriteOutMsg(1, Message(CustomEvent("test-3")), 1, SkipAck, writeTarget) 42 | journal ! WriteOutMsg(1, Message(CustomEvent("test-4")), 1, SkipAck, writeTarget) 43 | 44 | journal ! ReplayOutMsgs(1, 0, replayTarget) 45 | 46 | dequeue(replayQueue) { m => m must be(Message(CustomEvent("TEST-3"), sequenceNr = 1, timestamp = 0L)) } 47 | dequeue(replayQueue) { m => m must be(Message(CustomEvent("TEST-4"), sequenceNr = 2, timestamp = 0L)) } 48 | } 49 | } 50 | 51 | object LeveldbJournalSpec { 52 | val journalDir = new File("es-journal/es-journal-leveldb/target/journal") 53 | } 54 | 55 | class LeveldbJournalPSNativeSpec extends LeveldbJournalSpec with LeveldbCleanup { 56 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir) 57 | } 58 | 59 | class LeveldbJournalSSNativeSpec extends JournalSpec with LeveldbCleanup { 60 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir).withSequenceStructure 61 | } 62 | 63 | class LeveldbJournalPSJavaSpec extends LeveldbJournalSpec with LeveldbCleanup { 64 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir).withNative(false) 65 | } 66 | 67 | class LeveldbJournalSSJavaSpec extends JournalSpec with LeveldbCleanup { 68 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir).withSequenceStructure.withNative(false) 69 | } 70 | -------------------------------------------------------------------------------- /es-journal/es-journal-leveldb/src/test/scala/org/eligosource/eventsourced/journal/leveldb/LeveldbReplaySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.leveldb 17 | 18 | import org.eligosource.eventsourced.journal.common.PersistentReplaySpec 19 | 20 | class LeveldbReplayPSNativeSpec extends PersistentReplaySpec with LeveldbCleanup { 21 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir) 22 | } 23 | 24 | class LeveldbReplaySSNativeSpec extends PersistentReplaySpec with LeveldbCleanup { 25 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir).withSequenceStructure 26 | } 27 | 28 | class LeveldbReplayPSJavaSpec extends PersistentReplaySpec with LeveldbCleanup { 29 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir).withNative(false) 30 | } 31 | 32 | class LeveldbReplaySSJavaSpec extends PersistentReplaySpec with LeveldbCleanup { 33 | def journalProps = LeveldbJournalProps(LeveldbJournalSpec.journalDir).withSequenceStructure.withNative(false) 34 | } 35 | -------------------------------------------------------------------------------- /es-journal/es-journal-leveldb/src/test/scala/org/eligosource/eventsourced/journal/leveldb/LeveldbSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.leveldb 17 | 18 | import java.io.File 19 | 20 | import org.apache.commons.io.FileUtils 21 | import org.scalatest.{Suite, BeforeAndAfterEach} 22 | 23 | trait LeveldbSupport { 24 | val journalDir = new File("es-core-test/target/journal") 25 | val journalProps = LeveldbJournalProps(journalDir) // use native LevelDB 26 | 27 | def cleanup() { 28 | FileUtils.deleteDirectory(journalDir) 29 | } 30 | } 31 | 32 | trait LeveldbCleanup extends BeforeAndAfterEach { this: Suite => 33 | override def afterEach() { 34 | FileUtils.deleteDirectory(LeveldbJournalSpec.journalDir) 35 | } 36 | } -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.osgi.SbtOsgi 2 | 3 | resolvers += "Sonatype OSS" at "https://oss.sonatype.org/content/repositories/releases" 4 | 5 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" 6 | 7 | resolvers += "Typesafe" at "http://repo.typesafe.com/typesafe/releases" 8 | 9 | libraryDependencies ++= Seq( 10 | "org.mongodb" %% "casbah-core" % "2.6.2" % "compile", 11 | "org.mongodb" %% "casbah-commons" % "2.6.2" % "compile", 12 | "de.flapdoodle.embed" % "de.flapdoodle.embed.mongo" % "1.33" % "test" 13 | ) 14 | 15 | OsgiKeys.importPackage := Seq( 16 | "scala*;version=\"[2.10.0,2.11.0)\"", 17 | "akka*;version=\"[2.1.1,2.2.0)\"" 18 | ) 19 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/readme.md: -------------------------------------------------------------------------------- 1 | # MongoDB Casbah Journal 2 | 3 | [Eventsourced](https://github.com/eligosource/eventsourced) applications can create a [mongoDB](http://www.mongodb.org/) Casbah backed journal. 4 | 5 | - Using the [Casbah](http://api.mongodb.org/scala/casbah/2.0/) based [MongodbCasbahJournalProps](http://eligosource.github.com/eventsourced/api/snapshot/#org.eligosource.eventsourced.journal.mongodb.casbah.MongodbCasbahJournalProps) configuration object. 6 | 7 | ## Properties 8 | 9 | A mongoDB backed journal has the following properties when running on a real mongoDB cluster: 10 | 11 | - Highly available. 12 | - Horizontal scalability of writes via sharding. 13 | - Horizontal scalability of reads (replay) via sharding. 14 | - Writes evenly distributed via sharding. 15 | - Efficient per-processor recovery. 16 | - Efficient per-channel recovery (applies to reliable channels). 17 | 18 | ## Status 19 | 20 | Experimental. The Casbah based MongoDB journal is fully functional. 21 | 22 | Casbah Driver Version: 2.6.2 23 | 24 | ## Example 25 | 26 | This section shows how to initialize a journal that connects to a local, standalone mongoDB instance. 27 | 28 | First, download, install and start a standalone mongoDB instance by following the instructions in the mongoDB [Installing MongoDB](http://docs.mongodb.org/manual/installation/). Then add the required dependencies to your project's `build.sbt` file: 29 | 30 | resolvers += "Eligosource Snapshots" at "http://repo.eligotech.com/nexus/content/repositories/eligosource-snapshots" 31 | 32 | libraryDependencies += "org.eligosource" %% "eventsourced-core" % "0.7-SNAPSHOT" 33 | 34 | libraryDependencies += "org.eligosource" %% "eventsourced-journal-mongodb-casbah" % "0.7-SNAPSHOT" 35 | 36 | ### Mongodb Casbah Based Journal Initialization 37 | 38 | ```scala 39 | import akka.actor._ 40 | import com.mongodb.casbah.Imports._ 41 | import org.eligosource.eventsourced.core._ 42 | import org.eligosource.eventsourced.journal.mongodb.casbah.MongodbCasbahJournalProps 43 | 44 | implicit val system = ActorSystem("example") 45 | 46 | // create and start the Casbah based mongoDB journal 47 | val journal: ActorRef = MongodbCasbahJournalProps(MongoClient(), "eventsourced", "event").createJournal 48 | 49 | // create an event-sourcing extension that uses the Casbah based mongoDB journal 50 | val extension = EventsourcingExtension(system, journal) 51 | 52 | // ... 53 | ``` -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/src/main/scala/org/eligosource/eventsourced/journal/mongodb/casbah/MongodbCasbahJournalProps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.casbah 17 | 18 | import scala.concurrent.duration._ 19 | 20 | import akka.actor.Actor 21 | 22 | import com.mongodb.casbah.Imports._ 23 | 24 | import org.apache.hadoop.fs.{FileSystem, Path} 25 | 26 | import org.eligosource.eventsourced.journal.common.JournalProps 27 | import org.eligosource.eventsourced.journal.common.serialization.SnapshotSerializer 28 | import org.eligosource.eventsourced.journal.common.snapshot.HadoopFilesystemSnapshottingProps 29 | import org.eligosource.eventsourced.journal.common.snapshot.HadoopFilesystemSnapshotting.defaultLocalFilesystem 30 | 31 | /** 32 | * Configuration object for an Mongodb/Casbah based journal. 33 | * 34 | * Journal actors can be created from a configuration object as follows: 35 | * 36 | * {{{ 37 | * import akka.actor._ 38 | * 39 | * import org.eligosource.eventsourced.core.Journal 40 | * import org.eligosource.eventsourced.journal.mongodb.casbah.MongodbCasbahJournalProps 41 | * 42 | * implicit val system: ActorSystem = ... 43 | * 44 | * val journal: ActorRef = Journal(MongodbCasbahJournalProps(journalConn)) 45 | * }}} 46 | * 47 | * @param mongoClient Required mongoDB/Casbah client. 48 | * @param dbName Required mongoDB database name. 49 | * @param collName Required mongoDB collection name. 50 | * @param name Optional journal actor name. 51 | * @param dispatcherName Optional journal actor dispatcher name. 52 | */ 53 | case class MongodbCasbahJournalProps( 54 | mongoClient: MongoClient, 55 | dbName: String, 56 | collName: String, 57 | snapshotPath: Path = new Path("snapshots"), 58 | snapshotSerializer: SnapshotSerializer = SnapshotSerializer.java, 59 | snapshotLoadTimeout: FiniteDuration = 1 hour, 60 | snapshotSaveTimeout: FiniteDuration = 1 hour, 61 | snapshotFilesystem: FileSystem = defaultLocalFilesystem, 62 | name: Option[String] = None, dispatcherName: Option[String] = None, 63 | readOnly:Boolean = false) 64 | extends JournalProps with HadoopFilesystemSnapshottingProps[MongodbCasbahJournalProps] { 65 | 66 | /** 67 | * Java API. 68 | * 69 | * Returns a new `MongodbCasbahJournalProps` with specified journal actor name. 70 | */ 71 | def withName(name: String) = copy(name = Some(name)) 72 | 73 | /** 74 | * Java API. 75 | * 76 | * Returns a new `MongodbCasbahJournalProps` with specified journal actor dispatcher name. 77 | */ 78 | def withDispatcherName(dispatcherName: String) = copy(dispatcherName = Some(dispatcherName)) 79 | 80 | /** 81 | * Java API. 82 | */ 83 | def withSnapshotPath(snapshotPath: Path) = 84 | copy(snapshotPath = snapshotPath) 85 | 86 | /** 87 | * Java API. 88 | */ 89 | def withSnapshotSerializer(snapshotSerializer: SnapshotSerializer) = 90 | copy(snapshotSerializer = snapshotSerializer) 91 | 92 | /** 93 | * Java API. 94 | */ 95 | def withSnapshotLoadTimeout(snapshotLoadTimeout: FiniteDuration) = 96 | copy(snapshotLoadTimeout = snapshotLoadTimeout) 97 | 98 | /** 99 | * Java API. 100 | */ 101 | def withSnapshotSaveTimeout(snapshotSaveTimeout: FiniteDuration) = 102 | copy(snapshotSaveTimeout = snapshotSaveTimeout) 103 | 104 | /** 105 | * Java API. 106 | */ 107 | def withSnapshotFilesystem(snapshotFilesystem: FileSystem) = 108 | copy(snapshotFilesystem = snapshotFilesystem) 109 | 110 | /** 111 | * Java API. 112 | */ 113 | def withReadOnly(ro:Boolean) = 114 | copy(readOnly = ro) 115 | 116 | def createJournalActor: Actor = new MongodbCasbahJournal(this) 117 | } 118 | 119 | object MongodbCasbahJournalProps { 120 | /** 121 | * Java API. 122 | */ 123 | def create(mongoClient: MongoClient, dbName: String, collName: String) = 124 | MongodbCasbahJournalProps(mongoClient, dbName, collName) 125 | } -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/src/test/scala/org/eligosource/eventsourced/journal/mongodb/casbah/MongodbCasbahFixtureSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.casbah 17 | 18 | import com.mongodb.casbah.Imports._ 19 | 20 | trait MongodbCasbahFixtureSupport { 21 | 22 | val dbName = "es1" 23 | val collName = "event" 24 | val journalProps = MongodbCasbahJournalProps(MongoClient(mongoLocalHostName, mongoDefaultPort), dbName, collName) 25 | 26 | def cleanup() { 27 | MongoClient(mongoLocalHostName, mongoDefaultPort)(dbName)(collName).dropCollection() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/src/test/scala/org/eligosource/eventsourced/journal/mongodb/casbah/MongodbCasbahJournalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.casbah 17 | 18 | import com.mongodb.casbah.Imports._ 19 | 20 | import org.eligosource.eventsourced.journal.common.PersistentJournalSpec 21 | import org.eligosource.eventsourced.core.JournalProtocol.{ReplayInMsgs, WriteInMsg} 22 | import org.eligosource.eventsourced.core.Message 23 | 24 | import org.scalatest.BeforeAndAfterEach 25 | import akka.actor.{Props, ActorSystem} 26 | import org.eligosource.eventsourced.journal.common.JournalSpec.CommandTarget 27 | 28 | class MongodbCasbahJournalSpec extends PersistentJournalSpec with MongodbSpecSupport with BeforeAndAfterEach { 29 | 30 | val dbName = "es2" 31 | val collName = "event" 32 | 33 | // Since multiple embedded instances will run, each one must have a different port. 34 | override def mongoPort = 54321 35 | 36 | def journalProps = MongodbCasbahJournalProps(MongoClient(mongoLocalHostName, mongoPort), dbName, collName) 37 | 38 | override def afterEach() { 39 | MongoClient(mongoLocalHostName, mongoPort)(dbName)(collName).dropCollection() 40 | } 41 | 42 | "fetch the highest casbah message counter as sequence nbr in correct order after 1000 message insert" in { fixture => 43 | import fixture._ 44 | 45 | val Upper = 1000 46 | val UpperPlus1 = Upper + 1 47 | 48 | for(x <- 1 to Upper) { 49 | journal ! WriteInMsg(1, Message("test-" + x), writeTarget) 50 | } 51 | journal ! ReplayInMsgs(1, 0, replayTarget) 52 | 53 | for(x <- 1 to Upper) { 54 | dequeue(replayQueue) { m => m.sequenceNr must be (x.toLong) } 55 | } 56 | 57 | system.shutdown() 58 | system.awaitTermination(duration) 59 | 60 | val anotherSystem = ActorSystem("test") 61 | val anotherJournal = journalProps.createJournal(anotherSystem) 62 | val anotherReplayTarget = anotherSystem.actorOf(Props(new CommandTarget(replayQueue))) 63 | 64 | anotherJournal ! WriteInMsg(1, Message("test-" + UpperPlus1), writeTarget) 65 | anotherJournal ! ReplayInMsgs(1, 0, anotherReplayTarget) 66 | 67 | for(x <- 1 to UpperPlus1) { 68 | dequeue(replayQueue) { m => m.sequenceNr must be (x.toLong) } 69 | } 70 | 71 | anotherSystem.shutdown() 72 | anotherSystem.awaitTermination(duration) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/src/test/scala/org/eligosource/eventsourced/journal/mongodb/casbah/MongodbCasbahReplaySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.casbah 17 | 18 | import java.io.File 19 | 20 | import com.mongodb.casbah.Imports._ 21 | 22 | import org.apache.commons.io.FileUtils 23 | import org.apache.hadoop.fs.{Path, FileSystem} 24 | import org.apache.hadoop.conf.Configuration 25 | 26 | import org.scalatest.BeforeAndAfterEach 27 | 28 | import org.eligosource.eventsourced.journal.common.PersistentReplaySpec 29 | 30 | class MongodbCasbahReplaySpec extends PersistentReplaySpec with MongodbSpecSupport with BeforeAndAfterEach { 31 | val dbName = "es2" 32 | val collName = "event" 33 | 34 | val snapshotFileSystemRoot = "es-journal/es-journal-mongodb-casbah/target/journal" 35 | val snapshotFilesystem = FileSystem.getLocal(new Configuration) 36 | 37 | snapshotFilesystem.setWorkingDirectory(new Path(snapshotFileSystemRoot)) 38 | 39 | // Since multiple embedded instances will run, each one must have a different port. 40 | override def mongoPort = 54322 41 | 42 | def journalProps = MongodbCasbahJournalProps(MongoClient(mongoLocalHostName, mongoPort), dbName, collName, snapshotFilesystem = snapshotFilesystem) 43 | 44 | override def afterEach() { 45 | MongoClient(mongoLocalHostName, mongoPort)(dbName)(collName).dropCollection() 46 | FileUtils.deleteDirectory(new File(snapshotFileSystemRoot)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/src/test/scala/org/eligosource/eventsourced/journal/mongodb/casbah/MongodbSpecSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.casbah 17 | 18 | import org.scalatest.{Suite, BeforeAndAfterAll} 19 | import de.flapdoodle.embed.mongo.{Command, MongodStarter} 20 | import de.flapdoodle.embed.mongo.config.{RuntimeConfigBuilder, MongodConfig} 21 | import de.flapdoodle.embed.process.io.{NullProcessor, Processors} 22 | import de.flapdoodle.embed.process.config.io.ProcessOutput 23 | 24 | /** 25 | * This class provides test support for starting and stopping the embedded mongo instance. 26 | */ 27 | trait MongodbSpecSupport extends BeforeAndAfterAll { this: Suite => 28 | 29 | def mongoPort = mongoDefaultPort 30 | 31 | override def beforeAll() { 32 | 33 | // Used to filter out console output messages. 34 | val processOutput = new ProcessOutput(Processors.named("[mongod>]", new NullProcessor), 35 | Processors.named("[MONGOD>]", new NullProcessor), Processors.named("[console>]", new NullProcessor)) 36 | 37 | val runtimeConfig = new RuntimeConfigBuilder() 38 | .defaults(Command.MongoD) 39 | .processOutput(processOutput) 40 | .build() 41 | 42 | // Startup embedded mongodb. 43 | mongoStarter = MongodStarter.getInstance(runtimeConfig) 44 | mongoExe = mongoStarter.prepare(new MongodConfig(mongoVer, mongoPort, mongoLocalHostIPV6)) 45 | mongod = mongoExe.start() 46 | } 47 | 48 | override def afterAll() { 49 | mongod.stop() 50 | mongoExe.stop() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-casbah/src/test/scala/org/eligosource/eventsourced/journal/mongodb/casbah/package.scala: -------------------------------------------------------------------------------- 1 | package org.eligosource.eventsourced.journal.mongodb 2 | 3 | import de.flapdoodle.embed.mongo.distribution.Version 4 | import de.flapdoodle.embed.mongo.{MongodProcess, MongodExecutable, MongodStarter} 5 | import de.flapdoodle.embed.process.runtime.Network 6 | 7 | package object casbah { 8 | 9 | val mongoVer = Version.V2_4_3 10 | val mongoLocalHostName = Network.getLocalHost.getCanonicalHostName 11 | val mongoLocalHostIPV6 = Network.localhostIsIPv6() 12 | val mongoDefaultPort = 12345 13 | 14 | var mongoStarter: MongodStarter = _ 15 | var mongoExe: MongodExecutable = _ 16 | var mongod: MongodProcess = _ 17 | } 18 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-reactive/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.osgi.SbtOsgi 2 | 3 | resolvers += "Eligosource Releases Repo" at "http://repo.eligotech.com/nexus/content/repositories/eligosource-releases" 4 | 5 | resolvers += "Eligosource Snapshots Repo" at "http://repo.eligotech.com/nexus/content/repositories/eligosource-snapshots" 6 | 7 | resolvers += "Sonatype OSS" at "https://oss.sonatype.org/content/repositories/releases" 8 | 9 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" 10 | 11 | resolvers += "Typesafe" at "http://repo.typesafe.com/typesafe/releases" 12 | 13 | libraryDependencies ++= Seq( 14 | "org.reactivemongo" %% "reactivemongo" % "0.9-AKKA-2.2.0" % "compile,it" 15 | exclude("ch.qos.logback", "logback-core") 16 | exclude("ch.qos.logback", "logback-classic"), 17 | "de.flapdoodle.embed" % "de.flapdoodle.embed.mongo" % "1.33" % "it", 18 | "org.slf4j" % "slf4j-log4j12" % "1.6.0", 19 | "org.scalatest" %% "scalatest" % Version.ScalaTest % "it" 20 | ) 21 | 22 | OsgiKeys.importPackage := Seq( 23 | "scala*;version=\"[2.10.0,2.11.0)\"", 24 | "akka*;version=\"[2.1.1,2.2.0)\"" 25 | ) 26 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-reactive/readme.md: -------------------------------------------------------------------------------- 1 | # MongoDB Reactive Journal 2 | 3 | [Eventsourced](https://github.com/eligosource/eventsourced) applications can create a [mongoDB](http://www.mongodb.org/) [reactivemongo](http://reactivemongo.org/) backed journal using the [MongodbReactiveJournalProps](http://eligosource.github.com/eventsourced/api/snapshot/#org.eligosource.eventsourced.journal.mongodb.reactive.MongodbReactiveJournalProps) configuration object. 4 | 5 | ## Properties 6 | 7 | A mongoDB reactive journal has the following properties when running on a real mongoDB cluster: 8 | 9 | - Highly available. 10 | - Horizontal scalability of writes via sharding. 11 | - Horizontal scalability of reads (replay) via sharding. 12 | - Writes evenly distributed via sharding. 13 | - All reads and writes are asynchornous and non-blocking. 14 | - Efficient per-processor recovery. 15 | - Efficient per-channel recovery (applies to reliable channels). 16 | 17 | ## Status 18 | 19 | Experimental but fully functional. 20 | 21 | ## Example 22 | 23 | This section shows how to initialize a journal that connects to a local, standalone mongoDB instance. 24 | 25 | First, download, install and start a standalone mongoDB instance by following the instructions in the mongoDB [Installing MongoDB](http://docs.mongodb.org/manual/installation/). Then add the required dependencies to your project's `build.sbt` file: 26 | 27 | resolvers += "Eligosource Snapshots" at "http://repo.eligotech.com/nexus/content/repositories/eligosource-snapshots" 28 | 29 | libraryDependencies += "org.eligosource" %% "eventsourced-core" % "0.7-SNAPSHOT" 30 | 31 | libraryDependencies += "org.eligosource" %% "eventsourced-journal-mongodb-reactive" % "0.7-SNAPSHOT" 32 | 33 | ### Mongodb Reactive Based Journal Initialization 34 | 35 | ```scala 36 | import akka.actor._ 37 | import org.eligosource.eventsourced.core._ 38 | import org.eligosource.eventsourced.journal.mongodb.reactive.MongodbReactiveJournalProps 39 | 40 | implicit val system = ActorSystem("example") 41 | 42 | // create and start the reactive based mongoDB journal 43 | val journal: ActorRef = MongodbReactiveJournalProps(List("localhost:27017").createJournal 44 | 45 | // create an event-sourcing extension that uses the ReactiveMongo based mongoDB journal 46 | val extension = EventsourcingExtension(system, journal) 47 | 48 | // ... 49 | ``` -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-reactive/src/it/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=ERROR, stdout 2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 3 | log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout 4 | 5 | log4j.logger.reactivemongo.api.Cursor=ERROR 6 | log4j.logger.reactivemongo.api.Failover=ERROR 7 | log4j.logger.reactivemongo.core.actors.MongoDBSystem=ERROR 8 | log4j.logger.reactivemongo.core.actors.MonitorActor=ERROR 9 | log4j.logger.de.flapdoodle.embed.mongo.AbstractMongoProcess=ERROR 10 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-reactive/src/it/scala/org/eligosource/eventsourced/journal/mongodb/reactive/MongodbReactiveFixtureSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.reactive 17 | 18 | import reactivemongo.api.{DB, MongoConnection, MongoDriver} 19 | 20 | import scala.concurrent.{Await, ExecutionContext} 21 | import scala.concurrent.duration._ 22 | 23 | trait MongodbReactiveFixtureSupport { 24 | 25 | implicit val ec: ExecutionContext = ExecutionContext.Implicits.global 26 | implicit val duration = 10 seconds 27 | 28 | val connection = MongodbReactiveFixtureSupport.connection 29 | val journalProps = MongodbReactiveJournalProps(List("localhost:34567")).withDbName("journal2").withCollName("event2") 30 | 31 | def cleanup() { 32 | def cleanup() = try { 33 | val db = DB("journal2", connection) 34 | Await.result(db.drop(), duration) 35 | } catch { case _: Throwable => /* ignore */ } 36 | cleanup() 37 | } 38 | } 39 | 40 | object MongodbReactiveFixtureSupport { 41 | val driver = new MongoDriver 42 | val connection = driver.connection(List("localhost:34567")) 43 | } 44 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-reactive/src/it/scala/org/eligosource/eventsourced/journal/mongodb/reactive/MongodbReactiveSpecSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb.reactive 17 | 18 | import org.scalatest.{Suite, BeforeAndAfterAll} 19 | import de.flapdoodle.embed.mongo.{MongodProcess, MongodExecutable, Command, MongodStarter} 20 | import de.flapdoodle.embed.mongo.config.{RuntimeConfigBuilder, MongodConfig} 21 | import de.flapdoodle.embed.process.io.{NullProcessor, Processors} 22 | import de.flapdoodle.embed.process.config.io.ProcessOutput 23 | import de.flapdoodle.embed.mongo.distribution.Version 24 | import de.flapdoodle.embed.process.runtime.Network 25 | 26 | /** 27 | * This class provides test support for starting and stopping the embedded mongo instance. 28 | */ 29 | trait MongodbReactiveSpecSupport extends BeforeAndAfterAll { this: Suite => 30 | 31 | val mongoVer = Version.V2_4_3 32 | val mongoLocalHostName = Network.getLocalHost.getCanonicalHostName 33 | val mongoLocalHostIPV6 = Network.localhostIsIPv6() 34 | val mongoDefaultPort = 34567 35 | 36 | var mongoStarter: MongodStarter = _ 37 | var mongoExe: MongodExecutable = _ 38 | var mongod: MongodProcess = _ 39 | 40 | override def beforeAll() { 41 | 42 | // Used to filter out console output messages. 43 | val processOutput = new ProcessOutput(Processors.named("[mongod>]", new NullProcessor), 44 | Processors.named("[MONGOD>]", new NullProcessor), Processors.named("[console>]", new NullProcessor)) 45 | 46 | val runtimeConfig = new RuntimeConfigBuilder() 47 | .defaults(Command.MongoD) 48 | .processOutput(processOutput) 49 | .build() 50 | 51 | // Startup embedded mongodb. 52 | mongoStarter = MongodStarter.getInstance(runtimeConfig) 53 | mongoExe = mongoStarter.prepare(new MongodConfig(mongoVer, mongoDefaultPort, mongoLocalHostIPV6)) 54 | mongod = mongoExe.start() 55 | } 56 | 57 | override def afterAll() = try { 58 | mongod.stop() 59 | mongoExe.stop() 60 | } catch { case _: Throwable => /* ignore */ } 61 | } 62 | -------------------------------------------------------------------------------- /es-journal/es-journal-mongodb-reactive/src/main/scala/org/eligosource/eventsourced/journal/mongodb/reactive/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 Eligotech BV. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.eligosource.eventsourced.journal.mongodb 17 | 18 | package object reactive { 19 | 20 | val DefaultDatabaseName = "journal" 21 | val DefaultCollectionName = "event" 22 | 23 | private [reactive] case class Key( 24 | processorId: Int = Int.MaxValue, 25 | initiatingChannelId: Int = 0, 26 | sequenceNr: Long, 27 | confirmingChannelId: Int = 0) { 28 | 29 | def withSequenceNr(f: (Long) => Long) = copy(sequenceNr = f(sequenceNr)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.12.2 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" 2 | 3 | resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/" 4 | 5 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.4.0") 6 | 7 | addSbtPlugin("com.orrsella" % "sbt-sublime" % "1.0.5") 8 | 9 | addSbtPlugin("com.typesafe.sbt" % "sbt-osgi" % "0.4.0") 10 | 11 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.1.2") 12 | 13 | addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "0.6.2") 14 | 15 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.0") 16 | 17 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.4") 18 | --------------------------------------------------------------------------------