├── .gitignore ├── README.md ├── build.sbt └── src └── co └── vaughnvernon └── reactiveenterprise ├── CompletableApp.scala ├── aggregator └── Aggregator.scala ├── claimcheck └── ClaimCheck.scala ├── competingconsumer └── CompetingConsumer.scala ├── contentbasedrouter └── ContentBasedRouter.scala ├── contentenricher └── ContentEnricher.scala ├── contentfilter └── ContentFilter.scala ├── datatypechannel └── DatatypeChannel.scala ├── domainmodel └── DomainModelPrototype.scala ├── dynamicrouter └── DynamicRouter.scala ├── envelopewrapper └── EnvelopeWrapper.scala ├── guaranteeddelivery └── GuaranteedDelivery.scala ├── idempotentreceiver ├── Account.scala └── RiskAssessment.scala ├── messagebus └── MessageBus.scala ├── messageexpiration └── MessageExpiration.scala ├── messagefilter └── MessageFilter.scala ├── messagemetadata └── MessageMetadataDriver.scala ├── messagerouter └── MessageRouter.scala ├── messagingbridge ├── MessagingBridge.scala └── RabbitMQBridge.scala ├── pipesandfilters └── PipesAndFilters.scala ├── pointtopointchannel └── PointToPointChannel.scala ├── pollingconsumer ├── DevicePollingConsumer.scala └── PollingConsumer.scala ├── processmanager └── ProcessManager.scala ├── publishsubscribe └── SubClassification.scala ├── recipientlist └── RecipientList.scala ├── requestreply └── RequestReply.scala ├── resequencer └── Resequencer.scala ├── returnaddress ├── ReturnAddress.scala └── ReturnAddressInMessage.scala ├── routingslip └── RoutingSlip.scala ├── scattergather └── ScatterGather.scala ├── selectiveconsumer └── SelectiveConsumer.scala ├── smartproxy └── SmartProxy.scala ├── splitter └── Splitter.scala ├── transactionalactor └── EventSourced.scala └── wiretap └── WireTap.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .project 3 | .classpath 4 | /.settings/ 5 | /bin/ 6 | /journal/ 7 | /snapshots/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactiveMessagingPatterns_ActorModel 2 | The examples for the book "Reactive Messaging Patterns with the Actor Model" 3 | 4 | See: http://www.amazon.com/Reactive-Messaging-Patterns-Actor-Model/dp/0133846830/ 5 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "ReactiveMessagingPatterns_ActorModel" 2 | 3 | version := "1.0" 4 | 5 | scalaVersion := "2.11.7" 6 | 7 | scalaSource in Compile <<= (baseDirectory in Compile)(_ / "src") 8 | 9 | // can't use akka 2.4.x because code use some deprecated classes like EventSourcedProcessor 10 | lazy val akkaVersion = "2.3.4" 11 | 12 | libraryDependencies ++= Seq( 13 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 14 | // akka-persistence was experimental in older versions 15 | "com.typesafe.akka" %% "akka-persistence-experimental" % akkaVersion, 16 | // scala.reflect.runtime is used in examples so we need to add following dependency 17 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 18 | "com.rabbitmq" % "amqp-client" % "3.5.6" 19 | ) 20 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/CompletableApp.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise 16 | 17 | import akka.actor._ 18 | 19 | class CompletableApp(val steps:Int) extends App { 20 | val canComplete = new java.util.concurrent.CountDownLatch(1); 21 | val canStart = new java.util.concurrent.CountDownLatch(1); 22 | val completion = new java.util.concurrent.CountDownLatch(steps); 23 | 24 | val system = ActorSystem("eaipatterns") 25 | 26 | def awaitCanCompleteNow = canComplete.await 27 | 28 | def awaitCanStartNow = canStart.await 29 | 30 | def awaitCompletion = { 31 | completion.await 32 | system.shutdown() 33 | } 34 | 35 | def canCompleteNow() = canComplete.countDown() 36 | 37 | def canStartNow() = canStart.countDown() 38 | 39 | def completeAll() = { 40 | while (completion.getCount > 0) { 41 | completion.countDown() 42 | } 43 | } 44 | 45 | def completedStep() = completion.countDown() 46 | } -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/aggregator/Aggregator.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.aggregator 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class RequestForQuotation(rfqId: String, retailItems: Seq[RetailItem]) { 21 | val totalRetailPrice: Double = retailItems.map(retailItem => retailItem.retailPrice).sum 22 | } 23 | 24 | case class RetailItem(itemId: String, retailPrice: Double) 25 | 26 | case class PriceQuoteInterest(quoterId: String, quoteProcessor: ActorRef, lowTotalRetail: Double, highTotalRetail: Double) 27 | 28 | case class RequestPriceQuote(rfqId: String, itemId: String, retailPrice: Double, orderTotalRetailPrice: Double) 29 | 30 | case class PriceQuote(quoterId: String, rfqId: String, itemId: String, retailPrice: Double, discountPrice: Double) 31 | 32 | case class PriceQuoteFulfilled(priceQuote: PriceQuote) 33 | 34 | case class RequiredPriceQuotesForFulfillment(rfqId: String, quotesRequested: Int) 35 | 36 | case class QuotationFulfillment(rfqId: String, quotesRequested: Int, priceQuotes: Seq[PriceQuote], requester: ActorRef) 37 | 38 | object AggregatorDriver extends CompletableApp(5) { 39 | val priceQuoteAggregator = system.actorOf(Props[PriceQuoteAggregator], "priceQuoteAggregator") 40 | 41 | val orderProcessor = system.actorOf(Props(classOf[MountaineeringSuppliesOrderProcessor], priceQuoteAggregator), "orderProcessor") 42 | 43 | system.actorOf(Props(classOf[BudgetHikersPriceQuotes], orderProcessor), "budgetHikers") 44 | system.actorOf(Props(classOf[HighSierraPriceQuotes], orderProcessor), "highSierra") 45 | system.actorOf(Props(classOf[MountainAscentPriceQuotes], orderProcessor), "mountainAscent") 46 | system.actorOf(Props(classOf[PinnacleGearPriceQuotes], orderProcessor), "pinnacleGear") 47 | system.actorOf(Props(classOf[RockBottomOuterwearPriceQuotes], orderProcessor), "rockBottomOuterwear") 48 | 49 | orderProcessor ! RequestForQuotation("123", 50 | Vector(RetailItem("1", 29.95), 51 | RetailItem("2", 99.95), 52 | RetailItem("3", 14.95))) 53 | 54 | orderProcessor ! RequestForQuotation("125", 55 | Vector(RetailItem("4", 39.99), 56 | RetailItem("5", 199.95), 57 | RetailItem("6", 149.95), 58 | RetailItem("7", 724.99))) 59 | 60 | orderProcessor ! RequestForQuotation("129", 61 | Vector(RetailItem("8", 119.99), 62 | RetailItem("9", 499.95), 63 | RetailItem("10", 519.00), 64 | RetailItem("11", 209.50))) 65 | 66 | orderProcessor ! RequestForQuotation("135", 67 | Vector(RetailItem("12", 0.97), 68 | RetailItem("13", 9.50), 69 | RetailItem("14", 1.99))) 70 | 71 | orderProcessor ! RequestForQuotation("140", 72 | Vector(RetailItem("15", 107.50), 73 | RetailItem("16", 9.50), 74 | RetailItem("17", 599.99), 75 | RetailItem("18", 249.95), 76 | RetailItem("19", 789.99))) 77 | 78 | awaitCompletion 79 | println("Aggregator: is completed.") 80 | } 81 | 82 | class MountaineeringSuppliesOrderProcessor(priceQuoteAggregator: ActorRef) extends Actor { 83 | val interestRegistry = scala.collection.mutable.Map[String, PriceQuoteInterest]() 84 | 85 | def calculateRecipientList(rfq: RequestForQuotation): Iterable[ActorRef] = { 86 | for { 87 | interest <- interestRegistry.values 88 | if (rfq.totalRetailPrice >= interest.lowTotalRetail) 89 | if (rfq.totalRetailPrice <= interest.highTotalRetail) 90 | } yield interest.quoteProcessor 91 | } 92 | 93 | def dispatchTo(rfq: RequestForQuotation, recipientList: Iterable[ActorRef]) = { 94 | var totalRequestedQuotes = 0 95 | recipientList.foreach { recipient => 96 | rfq.retailItems.foreach { retailItem => 97 | println("OrderProcessor: " + rfq.rfqId + " item: " + retailItem.itemId + " to: " + recipient.path.toString) 98 | recipient ! RequestPriceQuote(rfq.rfqId, retailItem.itemId, retailItem.retailPrice, rfq.totalRetailPrice) 99 | } 100 | } 101 | } 102 | 103 | def receive = { 104 | case interest: PriceQuoteInterest => 105 | interestRegistry(interest.quoterId) = interest 106 | case priceQuote: PriceQuote => 107 | priceQuoteAggregator ! PriceQuoteFulfilled(priceQuote) 108 | println(s"OrderProcessor: received: $priceQuote") 109 | case rfq: RequestForQuotation => 110 | val recipientList = calculateRecipientList(rfq) 111 | priceQuoteAggregator ! RequiredPriceQuotesForFulfillment(rfq.rfqId, recipientList.size * rfq.retailItems.size) 112 | dispatchTo(rfq, recipientList) 113 | case fulfillment: QuotationFulfillment => 114 | println(s"OrderProcessor: received: $fulfillment") 115 | AggregatorDriver.completedStep() 116 | case message: Any => 117 | println(s"OrderProcessor: received unexpected message: $message") 118 | } 119 | } 120 | 121 | class PriceQuoteAggregator extends Actor { 122 | val fulfilledPriceQuotes = scala.collection.mutable.Map[String, QuotationFulfillment]() 123 | 124 | def receive = { 125 | case required: RequiredPriceQuotesForFulfillment => 126 | fulfilledPriceQuotes(required.rfqId) = QuotationFulfillment(required.rfqId, required.quotesRequested, Vector(), sender) 127 | case priceQuoteFulFilled: PriceQuoteFulfilled => 128 | val previousFulfillment = fulfilledPriceQuotes(priceQuoteFulFilled.priceQuote.rfqId) 129 | val currentPriceQuotes = previousFulfillment.priceQuotes :+ priceQuoteFulFilled.priceQuote 130 | val currentFulfillment = 131 | QuotationFulfillment( 132 | previousFulfillment.rfqId, 133 | previousFulfillment.quotesRequested, 134 | currentPriceQuotes, 135 | previousFulfillment.requester) 136 | 137 | if (currentPriceQuotes.size >= currentFulfillment.quotesRequested) { 138 | currentFulfillment.requester ! currentFulfillment 139 | fulfilledPriceQuotes.remove(priceQuoteFulFilled.priceQuote.rfqId) 140 | } else { 141 | fulfilledPriceQuotes(priceQuoteFulFilled.priceQuote.rfqId) = currentFulfillment 142 | } 143 | 144 | println(s"PriceQuoteAggregator: fulfilled price quote: $priceQuoteFulFilled") 145 | case message: Any => 146 | println(s"PriceQuoteAggregator: received unexpected message: $message") 147 | } 148 | } 149 | 150 | class BudgetHikersPriceQuotes(interestRegistrar: ActorRef) extends Actor { 151 | val quoterId = self.path.toString.split("/").last 152 | interestRegistrar ! PriceQuoteInterest(quoterId, self, 1.00, 1000.00) 153 | 154 | def receive = { 155 | case rpq: RequestPriceQuote => 156 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 157 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 158 | 159 | case message: Any => 160 | println(s"BudgetHikersPriceQuotes: received unexpected message: $message") 161 | } 162 | 163 | def discountPercentage(orderTotalRetailPrice: Double) = { 164 | if (orderTotalRetailPrice <= 100.00) 0.02 165 | else if (orderTotalRetailPrice <= 399.99) 0.03 166 | else if (orderTotalRetailPrice <= 499.99) 0.05 167 | else if (orderTotalRetailPrice <= 799.99) 0.07 168 | else 0.075 169 | } 170 | } 171 | 172 | class HighSierraPriceQuotes(interestRegistrar: ActorRef) extends Actor { 173 | val quoterId = self.path.toString.split("/").last 174 | interestRegistrar ! PriceQuoteInterest(quoterId, self, 100.00, 10000.00) 175 | 176 | def receive = { 177 | case rpq: RequestPriceQuote => 178 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 179 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 180 | 181 | case message: Any => 182 | println(s"HighSierraPriceQuotes: received unexpected message: $message") 183 | } 184 | 185 | def discountPercentage(orderTotalRetailPrice: Double): Double = { 186 | if (orderTotalRetailPrice <= 150.00) 0.015 187 | else if (orderTotalRetailPrice <= 499.99) 0.02 188 | else if (orderTotalRetailPrice <= 999.99) 0.03 189 | else if (orderTotalRetailPrice <= 4999.99) 0.04 190 | else 0.05 191 | } 192 | } 193 | 194 | class MountainAscentPriceQuotes(interestRegistrar: ActorRef) extends Actor { 195 | val quoterId = self.path.toString.split("/").last 196 | interestRegistrar ! PriceQuoteInterest(quoterId, self, 70.00, 5000.00) 197 | 198 | def receive = { 199 | case rpq: RequestPriceQuote => 200 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 201 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 202 | 203 | case message: Any => 204 | println(s"MountainAscentPriceQuotes: received unexpected message: $message") 205 | } 206 | 207 | def discountPercentage(orderTotalRetailPrice: Double) = { 208 | if (orderTotalRetailPrice <= 99.99) 0.01 209 | else if (orderTotalRetailPrice <= 199.99) 0.02 210 | else if (orderTotalRetailPrice <= 499.99) 0.03 211 | else if (orderTotalRetailPrice <= 799.99) 0.04 212 | else if (orderTotalRetailPrice <= 999.99) 0.045 213 | else if (orderTotalRetailPrice <= 2999.99) 0.0475 214 | else 0.05 215 | } 216 | } 217 | 218 | class PinnacleGearPriceQuotes(interestRegistrar: ActorRef) extends Actor { 219 | val quoterId = self.path.toString.split("/").last 220 | interestRegistrar ! PriceQuoteInterest(quoterId, self, 250.00, 500000.00) 221 | 222 | def receive = { 223 | case rpq: RequestPriceQuote => 224 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 225 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 226 | 227 | case message: Any => 228 | println(s"PinnacleGearPriceQuotes: received unexpected message: $message") 229 | } 230 | 231 | def discountPercentage(orderTotalRetailPrice: Double) = { 232 | if (orderTotalRetailPrice <= 299.99) 0.015 233 | else if (orderTotalRetailPrice <= 399.99) 0.0175 234 | else if (orderTotalRetailPrice <= 499.99) 0.02 235 | else if (orderTotalRetailPrice <= 999.99) 0.03 236 | else if (orderTotalRetailPrice <= 1199.99) 0.035 237 | else if (orderTotalRetailPrice <= 4999.99) 0.04 238 | else if (orderTotalRetailPrice <= 7999.99) 0.05 239 | else 0.06 240 | } 241 | } 242 | 243 | class RockBottomOuterwearPriceQuotes(interestRegistrar: ActorRef) extends Actor { 244 | val quoterId = self.path.toString.split("/").last 245 | interestRegistrar ! PriceQuoteInterest(quoterId, self, 0.50, 7500.00) 246 | 247 | def receive = { 248 | case rpq: RequestPriceQuote => 249 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 250 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 251 | 252 | case message: Any => 253 | println(s"RockBottomOuterwearPriceQuotes: received unexpected message: $message") 254 | } 255 | 256 | def discountPercentage(orderTotalRetailPrice: Double) = { 257 | if (orderTotalRetailPrice <= 100.00) 0.015 258 | else if (orderTotalRetailPrice <= 399.99) 0.02 259 | else if (orderTotalRetailPrice <= 499.99) 0.03 260 | else if (orderTotalRetailPrice <= 799.99) 0.04 261 | else if (orderTotalRetailPrice <= 999.99) 0.05 262 | else if (orderTotalRetailPrice <= 2999.99) 0.06 263 | else if (orderTotalRetailPrice <= 4999.99) 0.07 264 | else if (orderTotalRetailPrice <= 5999.99) 0.075 265 | else 0.08 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/claimcheck/ClaimCheck.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.claimcheck 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | import java.util.UUID 20 | 21 | case class Part(name: String) 22 | 23 | case class CompositeMessage(id: String, part1: Part, part2: Part, part3: Part) 24 | 25 | case class ProcessStep(id: String, claimCheck: ClaimCheck) 26 | 27 | case class StepCompleted(id: String, claimCheck: ClaimCheck, stepName: String) 28 | 29 | object ClaimCheckDriver extends CompletableApp(3) { 30 | val itemChecker = new ItemChecker() 31 | 32 | val step1 = system.actorOf(Props(classOf[Step1], itemChecker), "step1") 33 | val step2 = system.actorOf(Props(classOf[Step2], itemChecker), "step2") 34 | val step3 = system.actorOf(Props(classOf[Step3], itemChecker), "step3") 35 | 36 | val process = system.actorOf(Props(classOf[Process], Vector(step1, step2, step3), itemChecker), "process") 37 | 38 | process ! CompositeMessage("ABC", Part("partA1"), Part("partB2"), Part("partC3")) 39 | 40 | awaitCompletion 41 | println("ClaimCheck: is completed.") 42 | } 43 | 44 | case class ClaimCheck() { 45 | val number = UUID.randomUUID().toString 46 | 47 | override def toString = { 48 | "ClaimCheck(" + number + ")" 49 | } 50 | } 51 | 52 | case class CheckedItem(claimCheck: ClaimCheck, businessId: String, parts: Map[String, Any]) 53 | 54 | case class CheckedPart(claimCheck: ClaimCheck, part: Any) 55 | 56 | class ItemChecker { 57 | val checkedItems = scala.collection.mutable.Map[ClaimCheck, CheckedItem]() 58 | 59 | def checkedItemFor(businessId: String, parts: Map[String, Any]) = { 60 | CheckedItem(ClaimCheck(), businessId, parts) 61 | } 62 | 63 | def checkItem(item: CheckedItem) = { 64 | checkedItems.update(item.claimCheck, item) 65 | } 66 | 67 | def claimItem(claimCheck: ClaimCheck): CheckedItem = { 68 | checkedItems(claimCheck) 69 | } 70 | 71 | def claimPart(claimCheck: ClaimCheck, partName: String): CheckedPart = { 72 | val checkedItem = checkedItems(claimCheck) 73 | 74 | CheckedPart(claimCheck, checkedItem.parts(partName)) 75 | } 76 | 77 | def removeItem(claimCheck: ClaimCheck) = { 78 | if (checkedItems.contains(claimCheck)) { 79 | checkedItems.remove(claimCheck) 80 | } 81 | } 82 | } 83 | 84 | class Process(steps: Vector[ActorRef], itemChecker: ItemChecker) extends Actor { 85 | var stepIndex = 0 86 | 87 | def receive = { 88 | case message: CompositeMessage => 89 | val parts = 90 | Map( 91 | message.part1.name -> message.part1, 92 | message.part2.name -> message.part2, 93 | message.part3.name -> message.part3) 94 | 95 | val checkedItem = itemChecker.checkedItemFor(message.id, parts) 96 | 97 | itemChecker.checkItem(checkedItem) 98 | 99 | steps(stepIndex) ! ProcessStep(message.id, checkedItem.claimCheck) 100 | 101 | case message: StepCompleted => 102 | stepIndex += 1 103 | 104 | if (stepIndex < steps.size) { 105 | steps(stepIndex) ! ProcessStep(message.id, message.claimCheck) 106 | } else { 107 | itemChecker.removeItem(message.claimCheck) 108 | } 109 | 110 | ClaimCheckDriver.completedStep() 111 | 112 | case message: Any => 113 | println(s"Process: received unexpected: $message") 114 | } 115 | } 116 | 117 | class Step1(itemChecker: ItemChecker) extends Actor { 118 | def receive = { 119 | case processStep: ProcessStep => 120 | val claimedPart = itemChecker.claimPart(processStep.claimCheck, "partA1") 121 | 122 | println(s"Step1: processing $processStep\n with $claimedPart") 123 | 124 | sender ! StepCompleted(processStep.id, processStep.claimCheck, "step1") 125 | 126 | case message: Any => 127 | println(s"Step1: received unexpected: $message") 128 | } 129 | } 130 | 131 | class Step2(itemChecker: ItemChecker) extends Actor { 132 | def receive = { 133 | case processStep: ProcessStep => 134 | val claimedPart = itemChecker.claimPart(processStep.claimCheck, "partB2") 135 | 136 | println(s"Step2: processing $processStep\n with $claimedPart") 137 | 138 | sender ! StepCompleted(processStep.id, processStep.claimCheck, "step2") 139 | 140 | case message: Any => 141 | println(s"Step2: received unexpected: $message") 142 | } 143 | } 144 | 145 | class Step3(itemChecker: ItemChecker) extends Actor { 146 | def receive = { 147 | case processStep: ProcessStep => 148 | val claimedPart = itemChecker.claimPart(processStep.claimCheck, "partC3") 149 | 150 | println(s"Step3: processing $processStep\n with $claimedPart") 151 | 152 | sender ! StepCompleted(processStep.id, processStep.claimCheck, "step3") 153 | 154 | case message: Any => 155 | println(s"Step3: received unexpected: $message") 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/competingconsumer/CompetingConsumer.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.competingconsumer 16 | 17 | import co.vaughnvernon.reactiveenterprise.CompletableApp 18 | import akka.actor._ 19 | import akka.routing.SmallestMailboxPool 20 | 21 | object CompetingConsumerDriver extends CompletableApp(100) { 22 | val workItemsProvider = system.actorOf( 23 | Props[WorkConsumer] 24 | .withRouter(SmallestMailboxPool(nrOfInstances = 5))) 25 | 26 | for (itemCount <- 1 to 100) { 27 | workItemsProvider ! WorkItem("WorkItem" + itemCount) 28 | } 29 | 30 | awaitCompletion 31 | } 32 | 33 | case class WorkItem(name: String) 34 | 35 | class WorkConsumer extends Actor { 36 | def receive = { 37 | case workItem: WorkItem => 38 | println(s"${self.path.name} for: ${workItem.name}") 39 | 40 | CompetingConsumerDriver.completedStep 41 | } 42 | } -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/contentbasedrouter/ContentBasedRouter.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.contentbasedrouter 16 | 17 | import scala.collection.Map 18 | import akka.actor._ 19 | import co.vaughnvernon.reactiveenterprise._ 20 | 21 | case class Order(id: String, orderType: String, orderItems: Map[String, OrderItem]) { 22 | val grandTotal: Double = orderItems.values.map(orderItem => orderItem.price).sum 23 | 24 | override def toString = { 25 | s"Order($id, $orderType, $orderItems, Totaling: $grandTotal)" 26 | } 27 | } 28 | 29 | case class OrderItem(id: String, itemType: String, description: String, price: Double) { 30 | override def toString = { 31 | s"OrderItem($id, $itemType, '$description', $price)" 32 | } 33 | } 34 | 35 | case class OrderPlaced(order: Order) 36 | 37 | object ContentBasedRouterDriver extends CompletableApp(3) { 38 | val orderRouter = system.actorOf(Props[OrderRouter], "orderRouter") 39 | val orderItem1 = OrderItem("1", "TypeABC.4", "An item of type ABC.4.", 29.95) 40 | val orderItem2 = OrderItem("2", "TypeABC.1", "An item of type ABC.1.", 99.95) 41 | val orderItem3 = OrderItem("3", "TypeABC.9", "An item of type ABC.9.", 14.95) 42 | val orderItemsOfTypeA = Map(orderItem1.itemType -> orderItem1, orderItem2.itemType -> orderItem2, orderItem3.itemType -> orderItem3) 43 | orderRouter ! OrderPlaced(Order("123", "TypeABC", orderItemsOfTypeA)) 44 | 45 | val orderItem4 = OrderItem("4", "TypeXYZ.2", "An item of type XYZ.2.", 74.95) 46 | val orderItem5 = OrderItem("5", "TypeXYZ.1", "An item of type XYZ.1.", 59.95) 47 | val orderItem6 = OrderItem("6", "TypeXYZ.7", "An item of type XYZ.7.", 29.95) 48 | val orderItem7 = OrderItem("7", "TypeXYZ.5", "An item of type XYZ.5.", 9.95) 49 | val orderItemsOfTypeX = Map(orderItem4.itemType -> orderItem4, orderItem5.itemType -> orderItem5, orderItem6.itemType -> orderItem6, orderItem7.itemType -> orderItem7) 50 | orderRouter ! OrderPlaced(Order("124", "TypeXYZ", orderItemsOfTypeX)) 51 | 52 | awaitCompletion 53 | println("ContentBasedRouter: is completed.") 54 | } 55 | 56 | class OrderRouter extends Actor { 57 | val inventorySystemA = context.actorOf(Props[InventorySystemA], "inventorySystemA") 58 | val inventorySystemX = context.actorOf(Props[InventorySystemX], "inventorySystemX") 59 | 60 | def receive = { 61 | case orderPlaced: OrderPlaced => 62 | orderPlaced.order.orderType match { 63 | case "TypeABC" => 64 | println(s"OrderRouter: routing $orderPlaced") 65 | inventorySystemA ! orderPlaced 66 | case "TypeXYZ" => 67 | println(s"OrderRouter: routing $orderPlaced") 68 | inventorySystemX ! orderPlaced 69 | } 70 | 71 | ContentBasedRouterDriver.completedStep() 72 | case _ => 73 | println("OrderRouter: received unexpected message") 74 | } 75 | } 76 | 77 | class InventorySystemA extends Actor { 78 | def receive = { 79 | case OrderPlaced(order) => 80 | println(s"InventorySystemA: handling $order") 81 | ContentBasedRouterDriver.completedStep() 82 | case _ => 83 | println("InventorySystemA: received unexpected message") 84 | } 85 | } 86 | 87 | class InventorySystemX extends Actor { 88 | def receive = { 89 | case OrderPlaced(order) => 90 | println(s"InventorySystemX: handling $order") 91 | ContentBasedRouterDriver.completedStep() 92 | case _ => 93 | println("InventorySystemX: received unexpected message") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/contentenricher/ContentEnricher.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.contentenricher 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | import java.util.Date 20 | 21 | case class DoctorVisitCompleted( 22 | val patientId: String, 23 | val firstName: String, 24 | val date: Date, 25 | val patientDetails: PatientDetails) { 26 | def this(patientId: String, firstName: String, date: Date) = { 27 | this(patientId, firstName, date, PatientDetails(null, null, null)) 28 | } 29 | 30 | def carrier = patientDetails.carrier 31 | def lastName = patientDetails.lastName 32 | def socialSecurityNumber = patientDetails.socialSecurityNumber 33 | } 34 | 35 | case class PatientDetails(val lastName: String, val socialSecurityNumber: String, val carrier: String) 36 | 37 | case class VisitCompleted(dispatcher: ActorRef) 38 | 39 | object ContentEnricherDriver extends CompletableApp(3) { 40 | val accountingSystemDispatcher = system.actorOf(Props[AccountingSystemDispatcher], "accountingSystem") 41 | val accountingEnricherDispatcher = system.actorOf(Props(classOf[AccountingEnricherDispatcher], accountingSystemDispatcher), "accountingDispatcher") 42 | val scheduledDoctorVisit = system.actorOf(Props(classOf[ScheduledDoctorVisit], "123456789", "John"), "scheduledVisit") 43 | scheduledDoctorVisit ! VisitCompleted(accountingEnricherDispatcher) 44 | 45 | awaitCompletion 46 | println("ContentEnricher: is completed.") 47 | } 48 | 49 | class AccountingEnricherDispatcher(val accountingSystemDispatcher: ActorRef) extends Actor { 50 | def receive = { 51 | case doctorVisitCompleted: DoctorVisitCompleted => 52 | println("AccountingEnricherDispatcher: querying and forwarding.") 53 | // query the enriching patient information... 54 | // ... 55 | val lastName = "Doe" 56 | val carrier = "Kaiser" 57 | val socialSecurityNumber = "111-22-3333" 58 | val enrichedDoctorVisitCompleted = DoctorVisitCompleted( 59 | doctorVisitCompleted.patientId, 60 | doctorVisitCompleted.firstName, 61 | doctorVisitCompleted.date, 62 | PatientDetails(lastName, socialSecurityNumber, carrier)) 63 | accountingSystemDispatcher forward enrichedDoctorVisitCompleted 64 | ContentEnricherDriver.completedStep() 65 | case _ => 66 | println("AccountingEnricherDispatcher: received unexpected message") 67 | } 68 | } 69 | 70 | class AccountingSystemDispatcher extends Actor { 71 | def receive = { 72 | case doctorVisitCompleted: DoctorVisitCompleted => 73 | println("AccountingSystemDispatcher: sending to Accounting System...") 74 | ContentEnricherDriver.completedStep() 75 | case _ => 76 | println("AccountingSystemDispatcher: received unexpected message") 77 | } 78 | } 79 | 80 | class ScheduledDoctorVisit(val patientId: String, val firstName: String) extends Actor { 81 | var completedOn: Date = _ 82 | 83 | def receive = { 84 | case visitCompleted: VisitCompleted => 85 | println("ScheduledDoctorVisit: completing visit.") 86 | completedOn = new Date() 87 | visitCompleted.dispatcher ! new DoctorVisitCompleted(patientId, firstName, completedOn) 88 | ContentEnricherDriver.completedStep() 89 | case _ => 90 | println("ScheduledDoctorVisit: received unexpected message") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/contentfilter/ContentFilter.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.contentfilter 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise.CompletableApp 19 | 20 | case class FilteredMessage(light: String, and: String, fluffy: String, message: String) { 21 | override def toString = { 22 | s"FilteredMessage(" + light + " " + and + " " + fluffy + " " + message + ")" 23 | } 24 | } 25 | 26 | case class UnfilteredPayload(largePayload: String) 27 | 28 | object ContentFilterDriver extends CompletableApp(3) { 29 | val messageExchangeDispatcher = system.actorOf(Props[MessageExchangeDispatcher], "messageExchangeDispatcher") 30 | 31 | messageExchangeDispatcher ! UnfilteredPayload("A very large message with complex structure...") 32 | 33 | awaitCompletion 34 | println("RequestReply: is completed.") 35 | } 36 | 37 | class MessageExchangeDispatcher extends Actor { 38 | val messageContentFilter = context.actorOf(Props[MessageContentFilter], "messageContentFilter") 39 | 40 | def receive = { 41 | case message: UnfilteredPayload => 42 | println("MessageExchangeDispatcher: received unfiltered message: " + message.largePayload) 43 | messageContentFilter ! message 44 | ContentFilterDriver.completedStep() 45 | case message: FilteredMessage => 46 | println("MessageExchangeDispatcher: dispatching: " + message) 47 | ContentFilterDriver.completedStep() 48 | case _ => 49 | println("MessageExchangeDispatcher: received unexpected message") 50 | } 51 | } 52 | 53 | class MessageContentFilter extends Actor { 54 | def receive = { 55 | case message: UnfilteredPayload => 56 | println("MessageContentFilter: received unfiltered message: " + message.largePayload) 57 | // filtering occurs... 58 | sender ! FilteredMessage("this", "feels", "so", "right") 59 | ContentFilterDriver.completedStep() 60 | case _ => 61 | println("MessageContentFilter: received unexpected message") 62 | } 63 | } -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/datatypechannel/DatatypeChannel.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.datatypechannel 16 | 17 | import akka.actor._ 18 | 19 | abstract class RabbitMQReceiver extends Actor 20 | 21 | object DatatypeChannel { 22 | 23 | } 24 | 25 | class ProductQueriesChannel extends RabbitMQReceiver { 26 | def receive = { 27 | case message: Array[Byte] => 28 | val productQuery = translateToProductQuery(message) 29 | //... 30 | } 31 | 32 | def translateToProductQuery(message: Array[Byte]) = { 33 | null 34 | } 35 | } 36 | 37 | class PriceQuoteChannel extends RabbitMQReceiver { 38 | def receive = { 39 | case message: Array[Byte] => 40 | val priceQuote = translateToPriceQuote(message) 41 | //... 42 | } 43 | 44 | def translateToPriceQuote(message: Array[Byte]) = { 45 | null 46 | } 47 | } 48 | 49 | class PurchaseOrderChannel extends RabbitMQReceiver { 50 | def receive = { 51 | case message: Array[Byte] => 52 | val purchaseOrder = translateToPurchaseOrder(message) 53 | //... 54 | } 55 | 56 | def translateToPurchaseOrder(message: Array[Byte]) = { 57 | null 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/domainmodel/DomainModelPrototype.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.domainmodel 16 | 17 | import scala.concurrent.Await 18 | import scala.concurrent.duration._ 19 | import akka.actor._ 20 | import akka.pattern.ask 21 | import akka.util.Timeout 22 | 23 | import co.vaughnvernon.reactiveenterprise.CompletableApp 24 | 25 | class Order extends Actor { 26 | var amount: Double = _ 27 | 28 | def receive = { 29 | case init: InitializeOrder => 30 | println(s"Initializing Order with $init") 31 | this.amount = init.amount 32 | case processOrder: ProcessOrder => 33 | println(s"Processing Order is $processOrder") 34 | DomainModelPrototype.completedStep() 35 | } 36 | } 37 | 38 | case class InitializeOrder(amount: Double) 39 | case class ProcessOrder() 40 | 41 | object DomainModelPrototype extends CompletableApp(1) { 42 | 43 | val orderType = "co.vaughnvernon.reactiveenterprise.domainmodel.Order" 44 | 45 | val model = DomainModel("OrderProcessing") 46 | 47 | model.registerAggregateType(orderType) 48 | 49 | val order = model.aggregateOf(orderType, "123") 50 | 51 | order ! InitializeOrder(249.95) 52 | 53 | order ! ProcessOrder() 54 | 55 | awaitCompletion 56 | 57 | model.shutdown() 58 | 59 | println("DomainModelPrototype: is completed.") 60 | } 61 | 62 | object DomainModel { 63 | def apply(name: String): DomainModel = { 64 | new DomainModel(name) 65 | } 66 | } 67 | 68 | class DomainModel(name: String) { 69 | val aggregateTypeRegistry = scala.collection.mutable.Map[String, AggregateType]() 70 | val system = ActorSystem(name) 71 | 72 | def aggregateOf(typeName: String, id: String): AggregateRef = { 73 | if (aggregateTypeRegistry.contains(typeName)) { 74 | val aggregateType = aggregateTypeRegistry(typeName) 75 | aggregateType.cacheActor ! RegisterAggregateId(id) 76 | AggregateRef(id, aggregateType.cacheActor) 77 | } else { 78 | throw new IllegalStateException(s"DomainModel type registry does not have a $typeName") 79 | } 80 | } 81 | 82 | def registerAggregateType(typeName: String): Unit = { 83 | if (!aggregateTypeRegistry.contains(typeName)) { 84 | val actorRef = system.actorOf(Props(classOf[AggregateCache], typeName), typeName) 85 | aggregateTypeRegistry(typeName) = AggregateType(actorRef) 86 | } 87 | } 88 | 89 | def shutdown() = { 90 | system.shutdown() 91 | } 92 | } 93 | 94 | class AggregateCache(typeName: String) extends Actor { 95 | val aggregateClass: Class[Actor] = Class.forName(typeName).asInstanceOf[Class[Actor]] 96 | val aggregateIds = scala.collection.mutable.Set[String]() 97 | 98 | def receive = { 99 | case message: CacheMessage => 100 | val aggregate = context.child(message.id).getOrElse { 101 | if (!aggregateIds.contains(message.id)) { 102 | throw new IllegalStateException(s"No aggregate of type $typeName and id ${message.id}") 103 | } else { 104 | context.actorOf(Props(aggregateClass), message.id) 105 | } 106 | } 107 | aggregate.tell(message.actualMessage, message.sender) 108 | 109 | case register: RegisterAggregateId => 110 | this.aggregateIds.add(register.id) 111 | } 112 | } 113 | 114 | case class AggregateRef(id: String, cache: ActorRef) { 115 | def tell(message: Any)(implicit sender: ActorRef = null): Unit = { 116 | cache ! CacheMessage(id, message, sender) 117 | } 118 | 119 | def !(message: Any)(implicit sender: ActorRef = null): Unit = { 120 | cache ! CacheMessage(id, message, sender) 121 | } 122 | } 123 | 124 | case class AggregateType(cacheActor: ActorRef) 125 | 126 | case class CacheMessage(id: String, actualMessage: Any, sender: ActorRef) 127 | 128 | case class RegisterAggregateId(id: String) 129 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/dynamicrouter/DynamicRouter.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.dynamicrouter 16 | 17 | import reflect.runtime.currentMirror 18 | import akka.actor._ 19 | import co.vaughnvernon.reactiveenterprise._ 20 | 21 | case class InterestedIn(messageType: String) 22 | case class NoLongerInterestedIn(messageType: String) 23 | 24 | case class TypeAMessage(description: String) 25 | case class TypeBMessage(description: String) 26 | case class TypeCMessage(description: String) 27 | case class TypeDMessage(description: String) 28 | 29 | object DynamicRouterDriver extends CompletableApp(5) { 30 | val dunnoInterested = system.actorOf(Props[DunnoInterested], "dunnoInterested") 31 | 32 | val typedMessageInterestRouter = 33 | system.actorOf(Props( 34 | new TypedMessageInterestRouter(dunnoInterested, 4, 1)), 35 | "typedMessageInterestRouter") 36 | 37 | val typeAInterest = system.actorOf(Props(classOf[TypeAInterested], typedMessageInterestRouter), "typeAInterest") 38 | val typeBInterest = system.actorOf(Props(classOf[TypeBInterested], typedMessageInterestRouter), "typeBInterest") 39 | val typeCInterest = system.actorOf(Props(classOf[TypeCInterested], typedMessageInterestRouter), "typeCInterest") 40 | val typeCAlsoInterested = system.actorOf(Props(classOf[TypeCAlsoInterested], typedMessageInterestRouter), "typeCAlsoInterested") 41 | 42 | awaitCanStartNow 43 | 44 | typedMessageInterestRouter ! TypeAMessage("Message of TypeA.") 45 | typedMessageInterestRouter ! TypeBMessage("Message of TypeB.") 46 | typedMessageInterestRouter ! TypeCMessage("Message of TypeC.") 47 | 48 | awaitCanCompleteNow 49 | 50 | typedMessageInterestRouter ! TypeCMessage("Another message of TypeC.") 51 | typedMessageInterestRouter ! TypeDMessage("Message of TypeD.") 52 | 53 | awaitCompletion 54 | println("DynamicRouter: is completed.") 55 | } 56 | 57 | class TypedMessageInterestRouter( 58 | dunnoInterested: ActorRef, 59 | canStartAfterRegistered: Int, 60 | canCompleteAfterUnregistered: Int) extends Actor { 61 | 62 | val interestRegistry = scala.collection.mutable.Map[String, ActorRef]() 63 | val secondaryInterestRegistry = scala.collection.mutable.Map[String, ActorRef]() 64 | 65 | def receive = { 66 | case interestedIn: InterestedIn => 67 | registerInterest(interestedIn) 68 | case noLongerInterestedIn: NoLongerInterestedIn => 69 | unregisterInterest(noLongerInterestedIn) 70 | case message: Any => 71 | sendFor(message) 72 | } 73 | 74 | def registerInterest(interestedIn: InterestedIn) = { 75 | val messageType = typeOfMessage(interestedIn.messageType) 76 | if (!interestRegistry.contains(messageType)) { 77 | interestRegistry(messageType) = sender 78 | } else { 79 | secondaryInterestRegistry(messageType) = sender 80 | } 81 | 82 | if (interestRegistry.size + secondaryInterestRegistry.size >= canStartAfterRegistered) { 83 | DynamicRouterDriver.canStartNow() 84 | } 85 | } 86 | 87 | def sendFor(message: Any) = { 88 | val messageType = typeOfMessage(currentMirror.reflect(message).symbol.toString) 89 | 90 | if (interestRegistry.contains(messageType)) { 91 | interestRegistry(messageType) forward message 92 | } else { 93 | dunnoInterested ! message 94 | } 95 | } 96 | 97 | def typeOfMessage(rawMessageType: String): String = { 98 | rawMessageType.replace('$', ' ').replace('.', ' ').split(' ').last.trim 99 | } 100 | 101 | var unregisterCount: Int = 0 102 | 103 | def unregisterInterest(noLongerInterestedIn: NoLongerInterestedIn) = { 104 | val messageType = typeOfMessage(noLongerInterestedIn.messageType) 105 | 106 | if (interestRegistry.contains(messageType)) { 107 | val wasInterested = interestRegistry(messageType) 108 | 109 | if (wasInterested.compareTo(sender) == 0) { 110 | if (secondaryInterestRegistry.contains(messageType)) { 111 | val nowInterested = secondaryInterestRegistry.remove(messageType) 112 | 113 | interestRegistry(messageType) = nowInterested.get 114 | } else { 115 | interestRegistry.remove(messageType) 116 | } 117 | 118 | unregisterCount = unregisterCount + 1; 119 | if (unregisterCount >= this.canCompleteAfterUnregistered) { 120 | DynamicRouterDriver.canCompleteNow() 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | class DunnoInterested extends Actor { 128 | def receive = { 129 | case message: Any => 130 | println(s"DunnoInterest: received undeliverable message: $message") 131 | DynamicRouterDriver.completedStep() 132 | } 133 | } 134 | 135 | class TypeAInterested(interestRouter: ActorRef) extends Actor { 136 | interestRouter ! InterestedIn(TypeAMessage.getClass.getName) 137 | 138 | def receive = { 139 | case message: TypeAMessage => 140 | println(s"TypeAInterested: received: $message") 141 | DynamicRouterDriver.completedStep() 142 | case message: Any => 143 | println(s"TypeAInterested: received unexpected message: $message") 144 | } 145 | } 146 | 147 | class TypeBInterested(interestRouter: ActorRef) extends Actor { 148 | interestRouter ! InterestedIn(TypeBMessage.getClass.getName) 149 | 150 | def receive = { 151 | case message: TypeBMessage => 152 | println(s"TypeBInterested: received: $message") 153 | DynamicRouterDriver.completedStep() 154 | case message: Any => 155 | println(s"TypeBInterested: received unexpected message: $message") 156 | } 157 | } 158 | 159 | class TypeCInterested(interestRouter: ActorRef) extends Actor { 160 | interestRouter ! InterestedIn(TypeCMessage.getClass.getName) 161 | 162 | def receive = { 163 | case message: TypeCMessage => 164 | println(s"TypeCInterested: received: $message") 165 | 166 | interestRouter ! NoLongerInterestedIn(TypeCMessage.getClass.getName) 167 | 168 | DynamicRouterDriver.completedStep() 169 | 170 | case message: Any => 171 | println(s"TypeCInterested: received unexpected message: $message") 172 | } 173 | } 174 | 175 | class TypeCAlsoInterested(interestRouter: ActorRef) extends Actor { 176 | interestRouter ! InterestedIn(TypeCMessage.getClass.getName) 177 | 178 | def receive = { 179 | case message: TypeCMessage => 180 | println(s"TypeCAlsoInterested: received: $message") 181 | 182 | interestRouter ! NoLongerInterestedIn(TypeCMessage.getClass.getName) 183 | 184 | DynamicRouterDriver.completedStep() 185 | case message: Any => 186 | println(s"TypeCAlsoInterested: received unexpected message: $message") 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/envelopewrapper/EnvelopeWrapper.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.envelopewrapper 16 | 17 | import scala.collection.immutable.Map 18 | 19 | trait ReplyToSupport { 20 | def reply(message: Any) = { } 21 | def setUpReplyToSupport(returnAddress: String) = { } 22 | } 23 | 24 | trait RegisterCustomer { 25 | 26 | } 27 | 28 | trait RabbitMQReplyToSupport extends ReplyToSupport { 29 | override def reply(message: Any) = { 30 | } 31 | 32 | override def setUpReplyToSupport(returnAddress: String) = { 33 | } 34 | } 35 | 36 | case class RegisterCustomerRabbitMQReplyToMapEnvelope( 37 | mapMessage: Map[String, String]) 38 | extends RegisterCustomer with RabbitMQReplyToSupport { 39 | 40 | this.setUpReplyToSupport(mapMessage("returnAddress")) 41 | } 42 | 43 | object EnvelopeWrapperDriver { 44 | 45 | } -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/guaranteeddelivery/GuaranteedDelivery.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.guaranteeddelivery 16 | 17 | import akka.actor._ 18 | import akka.persistence._ 19 | import co.vaughnvernon.reactiveenterprise.CompletableApp 20 | import java.util.UUID 21 | import java.util.Date 22 | 23 | import akka.persistence.ConfirmablePersistent 24 | import akka.persistence.Deliver 25 | 26 | object GuaranteedDeliveryDriver extends CompletableApp(2) { 27 | val analyzer1 = 28 | system.actorOf(Props[OrderAnalyzer], "orderAnalyzer1") 29 | 30 | val orderProcessor1 = 31 | system.actorOf(Props(classOf[OrderProcessor], analyzer1.path), "orderProcessor1") 32 | 33 | val orderId = new Date().getTime.toString 34 | println(s"Processing: $orderId") 35 | orderProcessor1 ! Persistent(ProcessOrder(orderId, "Details...")) 36 | 37 | awaitCompletion 38 | } 39 | 40 | case class ProcessOrder(orderId: String, details: String) 41 | 42 | class OrderProcessor(orderAnalyzer: ActorPath) extends Processor { 43 | val channel = 44 | context.actorOf( 45 | Channel.props(), 46 | s"${self.path.name}-channel") 47 | 48 | override def preStart() = { 49 | self ! Recover(replayMax=0L) 50 | } 51 | 52 | def receive = { 53 | case message @ Persistent(actualMessage, sequenceNumber) => 54 | print(s"Handling persisted: $sequenceNumber: ") 55 | actualMessage match { 56 | case processOrder: ProcessOrder => 57 | println(s"ProcessOrder: $processOrder") 58 | channel ! Deliver(message, orderAnalyzer) 59 | GuaranteedDeliveryDriver.completedStep 60 | case unknown: Any => 61 | println(s"Unknown: $unknown") 62 | } 63 | case PersistenceFailure(actualMessage, sequenceNumber, cause) => 64 | println(s"Handling failed persistent: actualMessage") 65 | GuaranteedDeliveryDriver.completedStep 66 | case non_persisted: Any => 67 | println(s"Handling non-persistent: $non_persisted") 68 | GuaranteedDeliveryDriver.completedStep 69 | } 70 | } 71 | 72 | class OrderAnalyzer extends Actor { 73 | def receive = { 74 | case confirmable @ ConfirmablePersistent( 75 | actualMessage, sequenceNumber, redeliveries) => 76 | 77 | println(s"OrderAnalyzer: $actualMessage") 78 | confirmable.confirm 79 | GuaranteedDeliveryDriver.completedStep 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/idempotentreceiver/Account.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.idempotentreceiver 16 | 17 | import scala.collection.mutable.Map 18 | import akka.actor._ 19 | import java.util.UUID 20 | import co.vaughnvernon.reactiveenterprise.CompletableApp 21 | 22 | object AccountDriver extends CompletableApp(17) { 23 | val account = system.actorOf(Props(classOf[Account], AccountId()), "account") 24 | 25 | val deposit1 = Deposit(TransactionId(), Money(100)) 26 | account ! deposit1 27 | account ! QueryBalance() 28 | account ! deposit1 29 | account ! Deposit(TransactionId(), Money(20)) 30 | account ! QueryBalance() 31 | account ! deposit1 32 | account ! Withdraw(TransactionId(), Money(50)) 33 | account ! QueryBalance() 34 | account ! deposit1 35 | account ! Deposit(TransactionId(), Money(70)) 36 | account ! QueryBalance() 37 | account ! deposit1 38 | account ! Withdraw(TransactionId(), Money(100)) 39 | account ! QueryBalance() 40 | account ! deposit1 41 | account ! Deposit(TransactionId(), Money(10)) 42 | account ! QueryBalance() 43 | 44 | awaitCompletion 45 | 46 | println("Completed.") 47 | } 48 | 49 | case class Money(value: Double) { 50 | def +(money: Money): Money = Money(value + money.value) 51 | def -(money: Money): Money = Money(value - money.value) 52 | def negative(): Money = Money(0 - value) 53 | } 54 | 55 | object AccountId { 56 | var currentId = 0 57 | def apply(): AccountId = { 58 | currentId = currentId + 1 59 | AccountId(currentId.toString) 60 | } 61 | } 62 | case class AccountId(id: String) 63 | 64 | object TransactionId { 65 | var currentId = 0 66 | def apply(): TransactionId = { 67 | currentId = currentId + 1 68 | TransactionId(currentId.toString) 69 | } 70 | } 71 | case class TransactionId(id: String) 72 | 73 | case class Transaction(transactionId: TransactionId, amount: Money) 74 | 75 | case class AccountBalance(accountId: AccountId, amount: Money) 76 | case class Deposit(transactionId: TransactionId, amount: Money) 77 | case class QueryBalance() 78 | case class Withdraw(transactionId: TransactionId, amount: Money) 79 | 80 | class Account(accountId: AccountId) extends Actor { 81 | val transactions = Map.empty[TransactionId, Transaction] 82 | 83 | def receive = { 84 | case deposit: Deposit => 85 | val transaction = Transaction(deposit.transactionId, deposit.amount) 86 | println(s"Deposit: $transaction") 87 | transactions += (deposit.transactionId -> transaction) 88 | AccountDriver.completedStep 89 | case withdraw: Withdraw => 90 | val transaction = Transaction(withdraw.transactionId, withdraw.amount.negative) 91 | println(s"Withdraw: $transaction") 92 | transactions += (withdraw.transactionId -> transaction) 93 | AccountDriver.completedStep 94 | case query: QueryBalance => 95 | sender ! calculateBalance() 96 | AccountDriver.completedStep 97 | } 98 | 99 | def calculateBalance(): AccountBalance = { 100 | var amount = Money(0) 101 | 102 | transactions.values map { transaction => 103 | amount = amount + transaction.amount 104 | } 105 | 106 | println(s"Balance: $amount") 107 | 108 | AccountBalance(accountId, amount) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/idempotentreceiver/RiskAssessment.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.idempotentreceiver 16 | 17 | import scala.concurrent.Await 18 | import scala.concurrent.Future 19 | import scala.concurrent.duration._ 20 | import scala.language.postfixOps 21 | import akka.util.Timeout 22 | import akka.pattern.ask 23 | import co.vaughnvernon.reactiveenterprise.CompletableApp 24 | import akka.actor._ 25 | 26 | object RiskAssessmentDriver extends CompletableApp(2) { 27 | implicit val timeout = Timeout(5 seconds) 28 | 29 | val riskAssessment = system.actorOf(Props[RiskAssessment], "riskAssessment") 30 | 31 | val futureAssessment1 = riskAssessment ? ClassifyRisk() 32 | printRisk(futureAssessment1) 33 | 34 | riskAssessment ! AttachDocument("This is a HIGH risk.") 35 | 36 | val futureAssessment2 = riskAssessment ? ClassifyRisk() 37 | printRisk(futureAssessment2) 38 | 39 | awaitCompletion 40 | 41 | def printRisk(futureAssessment: Future[Any]): Unit = { 42 | val classification = 43 | Await.result(futureAssessment, timeout.duration) 44 | .asInstanceOf[RiskClassified] 45 | println(s"$classification") 46 | 47 | completedStep 48 | } 49 | } 50 | 51 | case class AttachDocument(documentText: String) 52 | case class ClassifyRisk() 53 | case class RiskClassified(classification: String) 54 | 55 | case class Document(documentText: Option[String]) { 56 | if (documentText.isDefined) { 57 | val text = documentText.get 58 | if (text == null || text.trim.isEmpty) { 59 | throw new IllegalStateException("Document must have text.") 60 | } 61 | } 62 | 63 | def determineClassification = { 64 | val text = documentText.get.toLowerCase 65 | 66 | if (text.contains("low")) "Low" 67 | else if (text.contains("medium")) "Medium" 68 | else if (text.contains("high")) "High" 69 | else "Unknown" 70 | } 71 | 72 | def isNotAttached = documentText.isEmpty 73 | def isAttached = documentText.isDefined 74 | } 75 | 76 | class RiskAssessment extends Actor { 77 | var document = Document(None) 78 | 79 | def documented: Receive = { 80 | case attachment: AttachDocument => 81 | // already received; ignore 82 | 83 | case classify: ClassifyRisk => 84 | sender ! RiskClassified(document.determineClassification) 85 | } 86 | 87 | def undocumented: Receive = { 88 | case attachment: AttachDocument => 89 | document = Document(Some(attachment.documentText)) 90 | context.become(documented) 91 | case classify: ClassifyRisk => 92 | sender ! RiskClassified("Unknown") 93 | } 94 | 95 | def receive = undocumented 96 | } 97 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messagebus/MessageBus.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messagebus 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | object MessageBusDriver extends CompletableApp(9) { 21 | val tradingBus = system.actorOf(Props(classOf[TradingBus], 6), "tradingBus") 22 | val marketAnalysisTools = system.actorOf(Props(classOf[MarketAnalysisTools], tradingBus), "marketAnalysisTools") 23 | val portfolioManager = system.actorOf(Props(classOf[PortfolioManager], tradingBus), "portfolioManager") 24 | val stockTrader = system.actorOf(Props(classOf[StockTrader], tradingBus), "stockTrader") 25 | 26 | awaitCanStartNow 27 | 28 | tradingBus ! Status() 29 | 30 | tradingBus ! TradingCommand("ExecuteBuyOrder", ExecuteBuyOrder("p123", "MSFT", 100, 31.85)) 31 | tradingBus ! TradingCommand("ExecuteSellOrder", ExecuteSellOrder("p456", "MSFT", 200, 31.80)) 32 | tradingBus ! TradingCommand("ExecuteBuyOrder", ExecuteBuyOrder("p789", "MSFT", 100, 31.83)) 33 | 34 | awaitCompletion 35 | println("MessageBus: is completed.") 36 | } 37 | 38 | case class CommandHandler(applicationId: String, handler: ActorRef) 39 | case class ExecuteBuyOrder(portfolioId: String, symbol: String, quantity: Int, price: Double) 40 | case class BuyOrderExecuted(portfolioId: String, symbol: String, quantity: Int, price: Double) 41 | case class ExecuteSellOrder(portfolioId: String, symbol: String, quantity: Int, price: Double) 42 | case class SellOrderExecuted(portfolioId: String, symbol: String, quantity: Int, price: Double) 43 | case class NotificationInterest(applicationId: String, interested: ActorRef) 44 | case class RegisterCommandHandler(applicationId: String, commandId: String, handler: ActorRef) 45 | case class RegisterNotificationInterest(applicationId: String, notificationId: String, interested: ActorRef) 46 | case class TradingCommand(commandId: String, command: Any) 47 | case class TradingNotification(notificationId: String, notification: Any) 48 | 49 | case class Status() 50 | 51 | class TradingBus(canStartAfterRegistered: Int) extends Actor { 52 | val commandHandlers = scala.collection.mutable.Map[String, Vector[CommandHandler]]() 53 | val notificationInterests = scala.collection.mutable.Map[String, Vector[NotificationInterest]]() 54 | 55 | var totalRegistered = 0 56 | 57 | def dispatchCommand(command: TradingCommand) = { 58 | if (commandHandlers.contains(command.commandId)) { 59 | commandHandlers(command.commandId) map { commandHandler => 60 | commandHandler.handler ! command.command 61 | } 62 | } 63 | } 64 | 65 | def dispatchNotification(notification: TradingNotification) = { 66 | if (notificationInterests.contains(notification.notificationId)) { 67 | notificationInterests(notification.notificationId) map { notificationInterest => 68 | notificationInterest.interested ! notification.notification 69 | } 70 | } 71 | } 72 | 73 | def notifyStartWhenReady() = { 74 | totalRegistered += 1 75 | 76 | if (totalRegistered == this.canStartAfterRegistered) { 77 | println(s"TradingBus: is ready with registered actors: $totalRegistered") 78 | MessageBusDriver.canStartNow() 79 | } 80 | } 81 | 82 | def receive = { 83 | case register: RegisterCommandHandler => 84 | println(s"TradingBus: registering: $register") 85 | registerCommandHandler(register.commandId, register.applicationId, register.handler) 86 | notifyStartWhenReady() 87 | 88 | case register: RegisterNotificationInterest => 89 | println(s"TradingBus: registering: $register") 90 | registerNotificationInterest(register.notificationId, register.applicationId, register.interested) 91 | notifyStartWhenReady() 92 | 93 | case command: TradingCommand => 94 | println(s"TradingBus: dispatching command: $command") 95 | dispatchCommand(command) 96 | 97 | case notification: TradingNotification => 98 | println(s"TradingBus: dispatching notification: $notification") 99 | dispatchNotification(notification) 100 | 101 | case status: Status => 102 | println(s"TradingBus: STATUS: has commandHandlers: $commandHandlers") 103 | println(s"TradingBus: STATUS: has notificationInterests: $notificationInterests") 104 | 105 | case message: Any => 106 | println(s"TradingBus: received unexpected: $message") 107 | } 108 | 109 | def registerCommandHandler(commandId: String, applicationId: String, handler: ActorRef) = { 110 | if (!commandHandlers.contains(commandId)) { 111 | commandHandlers(commandId) = Vector[CommandHandler]() 112 | } 113 | 114 | commandHandlers(commandId) = 115 | commandHandlers(commandId) :+ CommandHandler(applicationId, handler) 116 | } 117 | 118 | def registerNotificationInterest(notificationId: String, applicationId: String, interested: ActorRef) = { 119 | if (!notificationInterests.contains(notificationId)) { 120 | notificationInterests(notificationId) = Vector[NotificationInterest]() 121 | } 122 | 123 | notificationInterests(notificationId) = 124 | notificationInterests(notificationId) :+ NotificationInterest(applicationId, interested) 125 | } 126 | } 127 | 128 | class MarketAnalysisTools(tradingBus: ActorRef) extends Actor { 129 | val applicationId = self.path.name 130 | 131 | tradingBus ! RegisterNotificationInterest(applicationId, "BuyOrderExecuted", self) 132 | tradingBus ! RegisterNotificationInterest(applicationId, "SellOrderExecuted", self) 133 | 134 | def receive = { 135 | case executed: BuyOrderExecuted => 136 | println(s"MarketAnalysisTools: adding analysis for: $executed") 137 | MessageBusDriver.completedStep() 138 | 139 | case executed: SellOrderExecuted => 140 | println(s"MarketAnalysisTools: adjusting analysis for: $executed") 141 | MessageBusDriver.completedStep() 142 | 143 | case message: Any => 144 | println(s"MarketAnalysisTools: received unexpected: $message") 145 | } 146 | } 147 | 148 | class PortfolioManager(tradingBus: ActorRef) extends Actor { 149 | val applicationId = self.path.name 150 | 151 | tradingBus ! RegisterNotificationInterest(applicationId, "BuyOrderExecuted", self) 152 | tradingBus ! RegisterNotificationInterest(applicationId, "SellOrderExecuted", self) 153 | 154 | def receive = { 155 | case executed: BuyOrderExecuted => 156 | println(s"PortfolioManager: adding holding to portfolio for: $executed") 157 | MessageBusDriver.completedStep() 158 | 159 | case executed: SellOrderExecuted => 160 | println(s"PortfolioManager: adjusting holding in portfolio for: $executed") 161 | MessageBusDriver.completedStep() 162 | 163 | case message: Any => 164 | println(s"PortfolioManager: received unexpected: $message") 165 | } 166 | } 167 | 168 | class StockTrader(tradingBus: ActorRef) extends Actor { 169 | val applicationId = self.path.name 170 | 171 | tradingBus ! RegisterCommandHandler(applicationId, "ExecuteBuyOrder", self) 172 | tradingBus ! RegisterCommandHandler(applicationId, "ExecuteSellOrder", self) 173 | 174 | def receive = { 175 | case buy: ExecuteBuyOrder => 176 | println(s"StockTrader: buying for: $buy") 177 | tradingBus ! TradingNotification("BuyOrderExecuted", BuyOrderExecuted(buy.portfolioId, buy.symbol, buy.quantity, buy.price)) 178 | MessageBusDriver.completedStep() 179 | 180 | case sell: ExecuteSellOrder => 181 | println(s"StockTrader: selling for: $sell") 182 | tradingBus ! TradingNotification("SellOrderExecuted", SellOrderExecuted(sell.portfolioId, sell.symbol, sell.quantity, sell.price)) 183 | MessageBusDriver.completedStep() 184 | 185 | case message: Any => 186 | println(s"StockTrader: received unexpected: $message") 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messageexpiration/MessageExpiration.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messageexpiration 16 | 17 | import java.util.concurrent.TimeUnit 18 | import java.util.Date 19 | import scala.concurrent._ 20 | import scala.concurrent.duration._ 21 | import scala.util._ 22 | import ExecutionContext.Implicits.global 23 | import akka.actor._ 24 | import co.vaughnvernon.reactiveenterprise._ 25 | 26 | trait ExpiringMessage { 27 | val occurredOn = System.currentTimeMillis() 28 | val timeToLive: Long 29 | 30 | def isExpired: Boolean = { 31 | val elapsed = System.currentTimeMillis() - occurredOn 32 | 33 | elapsed > timeToLive 34 | } 35 | } 36 | 37 | case class PlaceOrder(id: String, itemId: String, price: Double, timeToLive: Long) extends ExpiringMessage 38 | 39 | object MessageExpirationDriver extends CompletableApp(3) { 40 | val purchaseAgent = system.actorOf(Props[PurchaseAgent], "purchaseAgent") 41 | val purchaseRouter = system.actorOf(Props(classOf[PurchaseRouter], purchaseAgent), "purchaseRouter") 42 | 43 | purchaseRouter ! PlaceOrder("1", "11", 50.00, 1000) 44 | purchaseRouter ! PlaceOrder("2", "22", 250.00, 100) 45 | purchaseRouter ! PlaceOrder("3", "33", 32.95, 10) 46 | 47 | awaitCompletion 48 | println("MessageExpiration: is completed.") 49 | } 50 | 51 | class PurchaseRouter(purchaseAgent: ActorRef) extends Actor { 52 | val random = new Random((new Date()).getTime) 53 | 54 | def receive = { 55 | case message: Any => 56 | val millis = random.nextInt(100) + 1 57 | println(s"PurchaseRouter: delaying delivery of $message for $millis milliseconds") 58 | val duration = Duration.create(millis, TimeUnit.MILLISECONDS) 59 | context.system.scheduler.scheduleOnce(duration, purchaseAgent, message) 60 | } 61 | } 62 | 63 | class PurchaseAgent extends Actor { 64 | def receive = { 65 | case placeOrder: PlaceOrder => 66 | if (placeOrder.isExpired) { 67 | context.system.deadLetters ! placeOrder 68 | println(s"PurchaseAgent: delivered expired $placeOrder to dead letters") 69 | } else { 70 | println(s"PurchaseAgent: placing order for $placeOrder") 71 | } 72 | 73 | MessageExpirationDriver.completedStep() 74 | 75 | case message: Any => 76 | println(s"PurchaseAgent: received unexpected: $message") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messagefilter/MessageFilter.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messagefilter 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise.CompletableApp 19 | 20 | case class Order(id: String, orderType: String, orderItems: Map[String, OrderItem]) { 21 | val grandTotal: Double = orderItems.values.map(orderItem => orderItem.price).sum 22 | 23 | def isType(prefix: String): Boolean = { 24 | this.orderType.startsWith(prefix) 25 | } 26 | 27 | override def toString = { 28 | s"Order($id, $orderType, $orderItems, Totaling: $grandTotal)" 29 | } 30 | } 31 | 32 | case class OrderItem(id: String, itemType: String, description: String, price: Double) { 33 | override def toString = { 34 | s"OrderItem($id, $itemType, '$description', $price)" 35 | } 36 | } 37 | 38 | case class OrderPlaced(order: Order) 39 | 40 | object MessageFilterDriver extends CompletableApp (4) { 41 | val inventorySystemA = system.actorOf(Props[InventorySystemA], "inventorySystemA") 42 | val actualInventorySystemX = system.actorOf(Props[InventorySystemX], "inventorySystemX") 43 | val inventorySystemX = system.actorOf(Props(classOf[InventorySystemXMessageFilter], actualInventorySystemX), "inventorySystemXMessageFilter") 44 | 45 | val orderItem1 = OrderItem("1", "TypeABC.4", "An item of type ABC.4.", 29.95) 46 | val orderItem2 = OrderItem("2", "TypeABC.1", "An item of type ABC.1.", 99.95) 47 | val orderItem3 = OrderItem("3", "TypeABC.9", "An item of type ABC.9.", 14.95) 48 | val orderItemsOfTypeA = Map(orderItem1.itemType -> orderItem1, orderItem2.itemType -> orderItem2, orderItem3.itemType -> orderItem3) 49 | inventorySystemA ! OrderPlaced(Order("123", "TypeABC", orderItemsOfTypeA)) 50 | inventorySystemX ! OrderPlaced(Order("123", "TypeABC", orderItemsOfTypeA)) 51 | 52 | val orderItem4 = OrderItem("4", "TypeXYZ.2", "An item of type XYZ.2.", 74.95) 53 | val orderItem5 = OrderItem("5", "TypeXYZ.1", "An item of type XYZ.1.", 59.95) 54 | val orderItem6 = OrderItem("6", "TypeXYZ.7", "An item of type XYZ.7.", 29.95) 55 | val orderItem7 = OrderItem("7", "TypeXYZ.5", "An item of type XYZ.5.", 9.95) 56 | val orderItemsOfTypeX = Map(orderItem4.itemType -> orderItem4, orderItem5.itemType -> orderItem5, orderItem6.itemType -> orderItem6, orderItem7.itemType -> orderItem7) 57 | inventorySystemA ! OrderPlaced(Order("124", "TypeXYZ", orderItemsOfTypeX)) 58 | inventorySystemX ! OrderPlaced(Order("124", "TypeXYZ", orderItemsOfTypeX)) 59 | 60 | awaitCompletion 61 | println("MessageFilter: is completed.") 62 | } 63 | 64 | class InventorySystemA extends Actor { 65 | def receive = { 66 | case OrderPlaced(order) if (order.isType("TypeABC")) => 67 | println(s"InventorySystemA: handling $order") 68 | MessageFilterDriver.completedStep() 69 | case incompatibleOrder => 70 | println(s"InventorySystemA: filtering out: $incompatibleOrder") 71 | MessageFilterDriver.completedStep() 72 | } 73 | } 74 | 75 | class InventorySystemX extends Actor { 76 | def receive = { 77 | case OrderPlaced(order) => 78 | println(s"InventorySystemX: handling $order") 79 | MessageFilterDriver.completedStep() 80 | case _ => 81 | println("InventorySystemX: received unexpected message") 82 | MessageFilterDriver.completedStep() 83 | } 84 | } 85 | 86 | class InventorySystemXMessageFilter(actualInventorySystemX: ActorRef) extends Actor { 87 | def receive = { 88 | case orderPlaced: OrderPlaced if (orderPlaced.order.isType("TypeXYZ")) => 89 | actualInventorySystemX forward orderPlaced 90 | MessageFilterDriver.completedStep() 91 | case incompatibleOrder => 92 | println(s"InventorySystemXMessageFilter: filtering out: $incompatibleOrder") 93 | MessageFilterDriver.completedStep() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messagemetadata/MessageMetadataDriver.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messagemetadata 16 | 17 | import java.util.{Date, Random} 18 | import akka.actor._ 19 | import co.vaughnvernon.reactiveenterprise.CompletableApp 20 | 21 | object MessageMetadataDriver extends CompletableApp(3) { 22 | import Metadata._ 23 | 24 | val processor3 = system.actorOf(Props(classOf[Processor], None), "processor3") 25 | val processor2 = system.actorOf(Props(classOf[Processor], Some(processor3)), "processor2") 26 | val processor1 = system.actorOf(Props(classOf[Processor], Some(processor2)), "processor1") 27 | 28 | val entry = Entry( 29 | Who("driver"), 30 | What("Started"), 31 | Where(this.getClass.getSimpleName, "driver"), 32 | new Date(), 33 | Why("Running processors")) 34 | 35 | processor1 ! SomeMessage("Data...", entry.asMetadata) 36 | 37 | awaitCompletion 38 | 39 | println("Completed.") 40 | } 41 | 42 | object Metadata { 43 | def apply() = new Metadata() 44 | 45 | case class Who(name: String) 46 | case class What(happened: String) 47 | case class Where(actorType: String, actorName: String) 48 | case class Why(explanation: String) 49 | 50 | case class Entry(who: Who, what: What, where: Where, when: Date, why: Why) { 51 | def this(who: Who, what: What, where: Where, why: Why) = 52 | this(who, what, where, new Date(), why) 53 | 54 | def this(who: String, what: String, actorType: String, actorName: String, why: String) = 55 | this(Who(who), What(what), Where(actorType, actorName), new Date(), Why(why)) 56 | 57 | def asMetadata = (new Metadata(List[Entry](this))) 58 | } 59 | } 60 | 61 | import Metadata._ 62 | 63 | case class Metadata(entries: List[Entry]) { 64 | def this() = this(List.empty[Entry]) 65 | def this(entry: Entry) = this(List[Entry](entry)) 66 | 67 | def including(entry: Entry): Metadata = { 68 | Metadata(entries :+ entry) 69 | } 70 | } 71 | 72 | case class SomeMessage( 73 | payload: String, 74 | metadata: Metadata = new Metadata()) { 75 | 76 | def including(entry: Entry): SomeMessage = { 77 | SomeMessage(payload, metadata.including(entry)) 78 | } 79 | } 80 | 81 | class Processor(next: Option[ActorRef]) extends Actor { 82 | import Metadata._ 83 | 84 | val random = new Random() 85 | 86 | def receive = { 87 | case message: SomeMessage => 88 | report(message) 89 | 90 | val nextMessage = message.including(entry) 91 | 92 | if (next.isDefined) { 93 | next.get ! nextMessage 94 | } else { 95 | report(nextMessage, "complete") 96 | } 97 | 98 | MessageMetadataDriver.completedStep 99 | } 100 | 101 | def because = s"Because: ${random.nextInt(10)}" 102 | 103 | def entry = 104 | Entry(Who(user), 105 | What(wasProcessed), 106 | Where(this.getClass.getSimpleName, self.path.name), 107 | new Date(), 108 | Why(because)) 109 | 110 | def report(message: SomeMessage, heading: String = "received") = 111 | println(s"${self.path.name} $heading: $message") 112 | 113 | def user = s"user${random.nextInt(100)}" 114 | 115 | def wasProcessed = s"Processed: ${random.nextInt(5)}" 116 | } 117 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messagerouter/MessageRouter.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messagerouter 16 | 17 | import co.vaughnvernon.reactiveenterprise._ 18 | import akka.actor._ 19 | 20 | object MessageRouterDriver extends CompletableApp(20) { 21 | val processor1 = system.actorOf(Props[Processor], "processor1") 22 | val processor2 = system.actorOf(Props[Processor], "processor2") 23 | 24 | val alternatingRouter = system.actorOf(Props(classOf[AlternatingRouter], processor1, processor2), "alternatingRouter") 25 | 26 | for (count <- 1 to 10) { 27 | alternatingRouter ! "Message #" + count 28 | } 29 | 30 | awaitCompletion 31 | 32 | println("MessageRouter: is completed.") 33 | } 34 | 35 | class AlternatingRouter(processor1: ActorRef, processor2: ActorRef) extends Actor { 36 | var alternate = 1; 37 | 38 | def alternateProcessor() = { 39 | if (alternate == 1) { 40 | alternate = 2 41 | processor1 42 | } else { 43 | alternate = 1 44 | processor2 45 | } 46 | } 47 | 48 | def receive = { 49 | case message: Any => 50 | val processor = alternateProcessor 51 | println(s"AlternatingRouter: routing $message to ${processor.path.name}") 52 | processor ! message 53 | MessageRouterDriver.completedStep() 54 | } 55 | } 56 | 57 | class Processor extends Actor { 58 | def receive = { 59 | case message: Any => 60 | println(s"Processor: ${self.path.name} received $message") 61 | MessageRouterDriver.completedStep() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messagingbridge/MessagingBridge.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messagingbridge 16 | 17 | import akka.event.Logging 18 | import akka.event.LoggingAdapter 19 | import co.vaughnvernon.reactiveenterprise.CompletableApp 20 | 21 | class MessageBridgeException( 22 | message: String, 23 | cause: Throwable, 24 | retry: Boolean) 25 | extends RuntimeException(message, cause) { 26 | 27 | def this(message: String, cause: Throwable) = 28 | this(message, cause, false) 29 | 30 | def retry(): Boolean = retry 31 | } 32 | 33 | object RabbitMQMessagingBridgeRunner extends CompletableApp(1) { 34 | } 35 | 36 | class InventoryProductAllocationBridge(config: RabbitMQBridgeConfig) 37 | extends RabbitMQBridgeActor(config) { 38 | 39 | private val log: LoggingAdapter = Logging.getLogger(context.system, self) 40 | 41 | def receive = { 42 | case message: RabbitMQBinaryMessage => 43 | log.error("Binary messages not supported.") 44 | case message: RabbitMQTextMessage => 45 | log.error(s"Received text message: ${message.textMessage}") 46 | case invalid: Any => 47 | log.error(s"Don't understand message: $invalid") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/messagingbridge/RabbitMQBridge.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.messagingbridge 16 | 17 | import java.io.IOException 18 | import java.util.Date 19 | import akka.actor._ 20 | import com.rabbitmq.client._ 21 | import com.rabbitmq.client.AMQP.BasicProperties 22 | import com.rabbitmq.client.AMQP.Queue.DeclareOk 23 | import com.rabbitmq.client.QueueingConsumer.Delivery 24 | 25 | object RabbitMQMessageType extends Enumeration { 26 | type RabbitMQMessageType = Value 27 | val Binary, Text = Value 28 | } 29 | 30 | import RabbitMQMessageType._ 31 | 32 | case class RabbitMQBridgeConfig( 33 | messageTypes: Array[String], 34 | settings: RabbitMQConnectionSettings, 35 | name: String, 36 | messageType: RabbitMQMessageType, 37 | durable: Boolean, 38 | exclusive: Boolean, 39 | autoAcknowledged: Boolean, 40 | autoDeleted: Boolean) { 41 | 42 | if (messageTypes == null) 43 | throw new IllegalArgumentException("Must provide empty messageTypes.") 44 | if (settings == null) 45 | throw new IllegalArgumentException("Must provide settings.") 46 | if (name == null || name.isEmpty) 47 | throw new IllegalArgumentException("Must provide name.") 48 | } 49 | 50 | case class RabbitMQConnectionSettings( 51 | hostName: String, 52 | port: Int, 53 | virtualHost: String, 54 | username: String, 55 | password: String) { 56 | 57 | def this() = 58 | this("localhost", -1, "/", null, null) 59 | 60 | def this(hostName: String, virtualHost: String) = 61 | this(hostName, -1, virtualHost, null, null) 62 | 63 | def hasPort(): Boolean = 64 | port > 0 65 | 66 | def hasUserCredentials(): Boolean = 67 | username != null && password != null 68 | } 69 | 70 | case class RabbitMQBinaryMessage( 71 | messageType: String, 72 | messageId: String, 73 | timestamp: Date, 74 | binaryMessage: Array[Byte], 75 | deliveryTag: Long, 76 | redelivery: Boolean) 77 | 78 | case class RabbitMQTextMessage( 79 | messageType: String, 80 | messageId: String, 81 | timestamp: Date, 82 | textMessage: String, 83 | deliveryTag: Long, 84 | redelivery: Boolean) 85 | 86 | abstract class RabbitMQBridgeActor(config: RabbitMQBridgeConfig) extends Actor { 87 | private val queueChannel = new QueueChannel(self, config) 88 | 89 | /*----------------------------------------------- 90 | def receive = { 91 | case message: RabbitMQBinaryMessage => 92 | ... 93 | case message: RabbitMQTextMessage => 94 | ... 95 | case invalid: Any => 96 | ... 97 | } 98 | -----------------------------------------------*/ 99 | 100 | } 101 | 102 | private class QueueChannel( 103 | bridge: ActorRef, 104 | config: RabbitMQBridgeConfig) { 105 | 106 | var connection = factory.newConnection 107 | var channel = connection.createChannel 108 | var queueName = openQueue 109 | val consumer = new DispatchingConsumer(this, bridge) 110 | 111 | def autoAcknowledged(): Boolean = 112 | config.autoAcknowledged 113 | 114 | def close(): Unit = { 115 | try { 116 | if (channel != null && channel.isOpen) channel.close 117 | } catch { 118 | case t: Throwable => // fall through 119 | } 120 | 121 | try { 122 | if (connection != null && connection.isOpen) connection.close 123 | } catch { 124 | case t: Throwable => // fall through 125 | } 126 | 127 | channel = null 128 | connection = null 129 | } 130 | 131 | def closed(): Boolean = 132 | channel == null && connection == null 133 | 134 | def config(): RabbitMQBridgeConfig = 135 | config 136 | 137 | def hostName(): String = 138 | config.settings.hostName 139 | 140 | def messageTypes(): Array[String] = 141 | config.messageTypes 142 | 143 | private def factory() = { 144 | val factory = new ConnectionFactory() 145 | 146 | factory.setHost(config.settings.hostName) 147 | 148 | if (config.settings.hasPort()) factory.setPort(config.settings.port) 149 | 150 | factory.setVirtualHost(config.settings.virtualHost) 151 | 152 | if (config.settings.hasUserCredentials) { 153 | factory.setUsername(config.settings.username) 154 | factory.setPassword(config.settings.password) 155 | } 156 | 157 | factory 158 | } 159 | 160 | private def openQueue(): String = { 161 | val result = 162 | channel.queueDeclare( 163 | config.name, 164 | config.durable, 165 | config.exclusive, 166 | config.autoDeleted, 167 | null) 168 | 169 | result.getQueue 170 | } 171 | } 172 | 173 | private class DispatchingConsumer( 174 | queueChannel: QueueChannel, 175 | bridge: ActorRef) 176 | extends DefaultConsumer(queueChannel.channel) { 177 | 178 | override def handleDelivery( 179 | consumerTag: String, 180 | envelope: Envelope, 181 | properties: BasicProperties, 182 | body: Array[Byte]): Unit = { 183 | 184 | if (!queueChannel.closed) { 185 | handle(bridge, new Delivery(envelope, properties, body)); 186 | } 187 | } 188 | 189 | override def handleShutdownSignal( 190 | consumerTag: String, 191 | signal: ShutdownSignalException): Unit = { 192 | queueChannel.close 193 | } 194 | 195 | private def handle( 196 | bridge: ActorRef, 197 | delivery: Delivery): Unit = { 198 | try { 199 | if (this.filteredMessageType(delivery)) { 200 | ; 201 | } else if (queueChannel.config.messageType == Binary) { 202 | bridge ! 203 | RabbitMQBinaryMessage( 204 | delivery.getProperties.getType, 205 | delivery.getProperties.getMessageId, 206 | delivery.getProperties.getTimestamp, 207 | delivery.getBody, 208 | delivery.getEnvelope.getDeliveryTag, 209 | delivery.getEnvelope.isRedeliver) 210 | } else if (queueChannel.config.messageType == Text) { 211 | bridge ! 212 | RabbitMQTextMessage( 213 | delivery.getProperties.getType, 214 | delivery.getProperties.getMessageId, 215 | delivery.getProperties.getTimestamp, 216 | new String(delivery.getBody), 217 | delivery.getEnvelope.getDeliveryTag, 218 | delivery.getEnvelope.isRedeliver) 219 | } 220 | 221 | this.ack(delivery) 222 | 223 | } catch { 224 | case e: MessageBridgeException => 225 | // System.out.println("MESSAGE EXCEPTION (MessageConsumer): " + e.getMessage()); 226 | nack(delivery, e.retry) 227 | case t: Throwable => 228 | // System.out.println("EXCEPTION (MessageConsumer): " + t.getMessage()); 229 | this.nack(delivery, false) 230 | } 231 | } 232 | 233 | private def ack(delivery: Delivery): Unit = { 234 | try { 235 | if (!queueChannel.autoAcknowledged) { 236 | this.getChannel().basicAck( 237 | delivery.getEnvelope.getDeliveryTag, 238 | false); 239 | } 240 | } catch { 241 | case e: IOException => // fall through 242 | } 243 | } 244 | 245 | private def nack(delivery: Delivery, retry: Boolean): Unit = { 246 | try { 247 | if (!queueChannel.autoAcknowledged) { 248 | this.getChannel().basicNack( 249 | delivery.getEnvelope.getDeliveryTag, 250 | false, 251 | retry) 252 | } 253 | } catch { 254 | case ioe: IOException => // fall through 255 | } 256 | } 257 | 258 | private def filteredMessageType(delivery: Delivery): Boolean = { 259 | var filtered = false 260 | 261 | if (!queueChannel.messageTypes.isEmpty) { 262 | val messageType = delivery.getProperties.getType 263 | 264 | if (messageType == null || !queueChannel.messageTypes.contains(messageType)) { 265 | filtered = true 266 | } 267 | } 268 | 269 | filtered 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/pipesandfilters/PipesAndFilters.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.pipesandfilters 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class ProcessIncomingOrder(orderInfo: Array[Byte]) 21 | 22 | object PipesAndFiltersDriver extends CompletableApp(9) { 23 | val orderText = "(encryption)(certificate)..." 24 | val rawOrderBytes = orderText.toCharArray.map(_.toByte) 25 | 26 | val filter5 = system.actorOf(Props[OrderManagementSystem], "orderManagementSystem") 27 | val filter4 = system.actorOf(Props(classOf[Deduplicator], filter5), "deduplicator") 28 | val filter3 = system.actorOf(Props(classOf[Authenticator], filter4), "authenticator") 29 | val filter2 = system.actorOf(Props(classOf[Decrypter], filter3), "decrypter") 30 | val filter1 = system.actorOf(Props(classOf[OrderAcceptanceEndpoint], filter2), "orderAcceptanceEndpoint") 31 | 32 | filter1 ! rawOrderBytes 33 | filter1 ! rawOrderBytes 34 | 35 | awaitCompletion 36 | 37 | println("PipesAndFilters: is completed.") 38 | } 39 | 40 | class Authenticator(nextFilter: ActorRef) extends Actor { 41 | def receive = { 42 | case message: ProcessIncomingOrder => 43 | val text = new String(message.orderInfo) 44 | println(s"Authenticator: processing $text") 45 | val orderText = text.replace("(certificate)", "") 46 | nextFilter ! ProcessIncomingOrder(orderText.toCharArray.map(_.toByte)) 47 | PipesAndFiltersDriver.completedStep() 48 | } 49 | } 50 | 51 | class Decrypter(nextFilter: ActorRef) extends Actor { 52 | def receive = { 53 | case message: ProcessIncomingOrder => 54 | val text = new String(message.orderInfo) 55 | println(s"Decrypter: processing $text") 56 | val orderText = text.replace("(encryption)", "") 57 | nextFilter ! ProcessIncomingOrder(orderText.toCharArray.map(_.toByte)) 58 | PipesAndFiltersDriver.completedStep() 59 | } 60 | } 61 | 62 | class Deduplicator(nextFilter: ActorRef) extends Actor { 63 | val processedOrderIds = scala.collection.mutable.Set[String]() 64 | 65 | def orderIdFrom(orderText: String): String = { 66 | val orderIdIndex = orderText.indexOf("id='") + 4 67 | val orderIdLastIndex = orderText.indexOf("'", orderIdIndex) 68 | orderText.substring(orderIdIndex, orderIdLastIndex) 69 | } 70 | 71 | def receive = { 72 | case message: ProcessIncomingOrder => 73 | val text = new String(message.orderInfo) 74 | println(s"Deduplicator: processing $text") 75 | val orderId = orderIdFrom(text) 76 | if (processedOrderIds.add(orderId)) { 77 | nextFilter ! message 78 | } else { 79 | println(s"Deduplicator: found duplicate order $orderId") 80 | } 81 | PipesAndFiltersDriver.completedStep() 82 | } 83 | } 84 | 85 | class OrderAcceptanceEndpoint(nextFilter: ActorRef) extends Actor { 86 | def receive = { 87 | case rawOrder: Array[Byte] => 88 | val text = new String(rawOrder) 89 | println(s"OrderAcceptanceEndpoint: processing $text") 90 | nextFilter ! ProcessIncomingOrder(rawOrder) 91 | PipesAndFiltersDriver.completedStep() 92 | } 93 | } 94 | 95 | class OrderManagementSystem extends Actor { 96 | def receive = { 97 | case message: ProcessIncomingOrder => 98 | val text = new String(message.orderInfo) 99 | println(s"OrderManagementSystem: processing unique order: $text") 100 | PipesAndFiltersDriver.completedStep() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/pointtopointchannel/PointToPointChannel.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.pointtopointchannel 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | object PointToPointChannelDriver extends CompletableApp(4) { 21 | val actorB = system.actorOf(Props[ActorB]) 22 | 23 | actorB ! "Goodbye, from actor C!" 24 | actorB ! "Hello, from actor A!" 25 | actorB ! "Goodbye again, from actor C!" 26 | actorB ! "Hello again, from actor A!" 27 | 28 | awaitCompletion 29 | 30 | println("PointToPointChannel: completed.") 31 | } 32 | 33 | class ActorB extends Actor { 34 | var goodbye = 0 35 | var goodbyeAgain = 0 36 | var hello = 0 37 | var helloAgain = 0 38 | 39 | def receive = { 40 | case message: String => 41 | hello = hello + 42 | (if (message.contains("Hello")) 1 else 0) 43 | helloAgain = helloAgain + 44 | (if (message.startsWith("Hello again")) 1 else 0) 45 | assert(hello == 0 || hello > helloAgain) 46 | 47 | goodbye = goodbye + 48 | (if (message.contains("Goodbye")) 1 else 0) 49 | goodbyeAgain = goodbyeAgain + 50 | (if (message.startsWith("Goodbye again")) 1 else 0) 51 | assert(goodbye == 0 || goodbye > goodbyeAgain) 52 | 53 | PointToPointChannelDriver.completedStep 54 | } 55 | } -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/pollingconsumer/DevicePollingConsumer.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.pollingconsumer 16 | 17 | import scala.util.Random 18 | import scala.concurrent._ 19 | import scala.concurrent.duration._ 20 | import scala.util._ 21 | import ExecutionContext.Implicits.global 22 | import java.util.Date 23 | import java.util.concurrent.TimeUnit 24 | import akka.actor._ 25 | import co.vaughnvernon.reactiveenterprise._ 26 | 27 | object DevicePollingConsumerDriver extends CompletableApp(10) { 28 | val evenNumberDevice = new EvenNumberDevice() 29 | val monitor = system.actorOf(Props(classOf[EvenNumberMonitor], evenNumberDevice), "evenNumberMonitor") 30 | 31 | monitor ! Monitor() 32 | 33 | awaitCompletion 34 | 35 | println("DevicePollingConsumerDriver: completed.") 36 | } 37 | 38 | case class Monitor() 39 | 40 | class EvenNumberMonitor(evenNumberDevice: EvenNumberDevice) extends Actor { 41 | val scheduler = 42 | new CappedBackOffScheduler( 43 | 500, 44 | 15000, 45 | context.system, 46 | self, 47 | Monitor()) 48 | 49 | def monitor = { 50 | val evenNumber = evenNumberDevice.nextEvenNumber(3) 51 | if (evenNumber.isDefined) { 52 | println(s"EVEN: ${evenNumber.get}") 53 | scheduler.reset 54 | DevicePollingConsumerDriver.completedStep 55 | } else { 56 | println(s"MISS") 57 | scheduler.backOff 58 | } 59 | } 60 | 61 | def receive = { 62 | case request: Monitor => 63 | monitor 64 | } 65 | } 66 | 67 | class CappedBackOffScheduler( 68 | minimumInterval: Int, 69 | maximumInterval: Int, 70 | system: ActorSystem, 71 | receiver: ActorRef, 72 | message: Any) { 73 | 74 | var interval = minimumInterval 75 | 76 | def backOff = { 77 | interval = interval * 2 78 | if (interval > maximumInterval) interval = maximumInterval 79 | schedule 80 | } 81 | 82 | def reset = { 83 | interval = minimumInterval 84 | schedule 85 | } 86 | 87 | private def schedule = { 88 | val duration = Duration.create(interval, TimeUnit.MILLISECONDS) 89 | system.scheduler.scheduleOnce(duration, receiver, message) 90 | } 91 | } 92 | 93 | class EvenNumberDevice() { 94 | val random = new Random(99999) 95 | 96 | def nextEvenNumber(waitFor: Int): Option[Int] = { 97 | val timeout = new Timeout(waitFor) 98 | var nextEvenNumber: Option[Int] = None 99 | 100 | while (!timeout.isTimedOut && nextEvenNumber.isEmpty) { 101 | Thread.sleep(waitFor / 2) 102 | 103 | val number = random.nextInt(100000) 104 | 105 | if (number % 2 == 0) nextEvenNumber = Option(number) 106 | } 107 | 108 | nextEvenNumber 109 | } 110 | 111 | def nextEvenNumber(): Option[Int] = { 112 | nextEvenNumber(-1) 113 | } 114 | } 115 | 116 | class Timeout(withinMillis: Int) { 117 | val mark = currentTime 118 | 119 | def isTimedOut(): Boolean = { 120 | if (withinMillis == -1) false 121 | else currentTime - mark >= withinMillis 122 | } 123 | 124 | private def currentTime(): Long = { 125 | (new Date()).getTime 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/pollingconsumer/PollingConsumer.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.pollingconsumer 16 | 17 | import scala.collection.immutable.List 18 | import akka.actor._ 19 | import co.vaughnvernon.reactiveenterprise._ 20 | 21 | object PollingConsumerDriver extends CompletableApp(1) { 22 | val workItemsProvider = system.actorOf(Props[WorkItemsProvider], "workItemsProvider") 23 | val workConsumer = system.actorOf(Props(classOf[WorkConsumer], workItemsProvider), "workConsumer") 24 | 25 | workConsumer ! WorkNeeded() 26 | 27 | awaitCompletion 28 | 29 | println("PollingConsumerDriver: completed.") 30 | } 31 | 32 | case class WorkNeeded() 33 | case class WorkOnItem(workItem: WorkItem) 34 | 35 | class WorkConsumer(workItemsProvider: ActorRef) extends Actor { 36 | var totalItemsWorkedOn = 0 37 | 38 | def performWorkOn(workItem: WorkItem) = { 39 | totalItemsWorkedOn = totalItemsWorkedOn + 1 40 | if (totalItemsWorkedOn >= 15) { 41 | context.stop(self) 42 | PollingConsumerDriver.completeAll 43 | } 44 | } 45 | 46 | override def postStop() = { 47 | context.stop(workItemsProvider) 48 | } 49 | 50 | def receive = { 51 | case allocated: WorkItemsAllocated => 52 | println("WorkItemsAllocated...") 53 | allocated.workItems map { workItem => 54 | self ! WorkOnItem(workItem) 55 | } 56 | self ! WorkNeeded() 57 | case workNeeded: WorkNeeded => 58 | println("WorkNeeded...") 59 | workItemsProvider ! AllocateWorkItems(5) 60 | case workOnItem: WorkOnItem => 61 | println(s"Performed work on: ${workOnItem.workItem.name}") 62 | performWorkOn(workOnItem.workItem) 63 | } 64 | } 65 | 66 | case class AllocateWorkItems(numberOfItems: Int) 67 | case class WorkItemsAllocated(workItems: List[WorkItem]) 68 | case class WorkItem(name: String) 69 | 70 | class WorkItemsProvider extends Actor { 71 | var workItemsNamed: Int = 0 72 | 73 | def allocateWorkItems(numberOfItems: Int): List[WorkItem] = { 74 | var allocatedWorkItems = List[WorkItem]() 75 | for (itemCount <- 1 to numberOfItems) { 76 | val nameIndex = workItemsNamed + itemCount 77 | allocatedWorkItems = allocatedWorkItems :+ WorkItem("WorkItem" + nameIndex) 78 | } 79 | workItemsNamed = workItemsNamed + numberOfItems 80 | allocatedWorkItems 81 | } 82 | 83 | def receive = { 84 | case request: AllocateWorkItems => 85 | sender ! WorkItemsAllocated(allocateWorkItems(request.numberOfItems)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/processmanager/ProcessManager.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.processmanager 16 | 17 | import co.vaughnvernon.reactiveenterprise.CompletableApp 18 | import akka.actor._ 19 | import akka.event.Logging 20 | import akka.event.LoggingAdapter 21 | import java.util.Random 22 | 23 | object ProcessManagerDriver extends CompletableApp(5) { 24 | val creditBureau = 25 | system.actorOf( 26 | Props[CreditBureau], 27 | "creditBureau") 28 | 29 | val bank1 = 30 | system.actorOf( 31 | Props(classOf[Bank], "bank1", 2.75, 0.30), 32 | "bank1") 33 | val bank2 = 34 | system.actorOf( 35 | Props(classOf[Bank], "bank2", 2.73, 0.31), 36 | "bank2") 37 | val bank3 = 38 | system.actorOf( 39 | Props(classOf[Bank], "bank3", 2.80, 0.29), 40 | "bank3") 41 | 42 | val loanBroker = system.actorOf( 43 | Props(classOf[LoanBroker], 44 | creditBureau, 45 | Vector(bank1, bank2, bank3)), 46 | "loanBroker") 47 | 48 | loanBroker ! QuoteBestLoanRate("111-11-1111", 100000, 84) 49 | 50 | awaitCompletion 51 | } 52 | 53 | //=========== ProcessManager 54 | 55 | case class ProcessStarted( 56 | processId: String, 57 | process: ActorRef) 58 | 59 | case class ProcessStopped( 60 | processId: String, 61 | process: ActorRef) 62 | 63 | abstract class ProcessManager extends Actor { 64 | private var processes = Map[String, ActorRef]() 65 | val log: LoggingAdapter = Logging.getLogger(context.system, self) 66 | 67 | def processOf(processId: String): ActorRef = { 68 | if (processes.contains(processId)) { 69 | processes(processId) 70 | } else { 71 | null 72 | } 73 | } 74 | 75 | def startProcess(processId: String, process: ActorRef) = { 76 | if (!processes.contains(processId)) { 77 | processes = processes + (processId -> process) 78 | self ! ProcessStarted(processId, process) 79 | } 80 | } 81 | 82 | def stopProcess(processId: String) = { 83 | if (processes.contains(processId)) { 84 | val process = processes(processId) 85 | processes = processes - processId 86 | self ! ProcessStopped(processId, process) 87 | } 88 | } 89 | } 90 | 91 | //=========== LoanBroker 92 | 93 | case class QuoteBestLoanRate( 94 | taxId: String, 95 | amount: Integer, 96 | termInMonths: Integer) 97 | 98 | case class BestLoanRateQuoted( 99 | bankId: String, 100 | loanRateQuoteId: String, 101 | taxId: String, 102 | amount: Integer, 103 | termInMonths: Integer, 104 | creditScore: Integer, 105 | interestRate: Double) 106 | 107 | case class BestLoanRateDenied( 108 | loanRateQuoteId: String, 109 | taxId: String, 110 | amount: Integer, 111 | termInMonths: Integer, 112 | creditScore: Integer) 113 | 114 | class LoanBroker( 115 | creditBureau: ActorRef, 116 | banks: Seq[ActorRef]) 117 | extends ProcessManager { 118 | 119 | def receive = { 120 | case message: BankLoanRateQuoted => 121 | log.info(s"$message") 122 | 123 | processOf(message.loadQuoteReferenceId) ! 124 | RecordLoanRateQuote( 125 | message.bankId, 126 | message.bankLoanRateQuoteId, 127 | message.interestRate) 128 | 129 | case message: CreditChecked => 130 | log.info(s"$message") 131 | 132 | processOf(message.creditProcessingReferenceId) ! 133 | EstablishCreditScoreForLoanRateQuote( 134 | message.creditProcessingReferenceId, 135 | message.taxId, 136 | message.score) 137 | 138 | case message: CreditScoreForLoanRateQuoteDenied => 139 | log.info(s"$message") 140 | 141 | processOf(message.loanRateQuoteId) ! 142 | TerminateLoanRateQuote() 143 | 144 | ProcessManagerDriver.completeAll 145 | 146 | val denied = 147 | BestLoanRateDenied( 148 | message.loanRateQuoteId, 149 | message.taxId, 150 | message.amount, 151 | message.termInMonths, 152 | message.score) 153 | 154 | log.info(s"Would be sent to original requester: $denied") 155 | 156 | case message: CreditScoreForLoanRateQuoteEstablished => 157 | log.info(s"$message") 158 | 159 | banks map { bank => 160 | bank ! QuoteLoanRate( 161 | message.loanRateQuoteId, 162 | message.taxId, 163 | message.score, 164 | message.amount, 165 | message.termInMonths) 166 | } 167 | 168 | ProcessManagerDriver.completedStep 169 | 170 | case message: LoanRateBestQuoteFilled => 171 | log.info(s"$message") 172 | 173 | ProcessManagerDriver.completedStep 174 | 175 | stopProcess(message.loanRateQuoteId) 176 | 177 | val best = BestLoanRateQuoted( 178 | message.bestBankLoanRateQuote.bankId, 179 | message.loanRateQuoteId, 180 | message.taxId, 181 | message.amount, 182 | message.termInMonths, 183 | message.creditScore, 184 | message.bestBankLoanRateQuote.interestRate) 185 | 186 | log.info(s"Would be sent to original requester: $best") 187 | 188 | case message: LoanRateQuoteRecorded => 189 | log.info(s"$message") 190 | 191 | ProcessManagerDriver.completedStep 192 | 193 | case message: LoanRateQuoteStarted => 194 | log.info(s"$message") 195 | 196 | creditBureau ! CheckCredit( 197 | message.loanRateQuoteId, 198 | message.taxId) 199 | 200 | case message: LoanRateQuoteTerminated => 201 | log.info(s"$message") 202 | 203 | stopProcess(message.loanRateQuoteId) 204 | 205 | case message: ProcessStarted => 206 | log.info(s"$message") 207 | 208 | message.process ! StartLoanRateQuote(banks.size) 209 | 210 | case message: ProcessStopped => 211 | log.info(s"$message") 212 | 213 | context.stop(message.process) 214 | 215 | case message: QuoteBestLoanRate => 216 | val loanRateQuoteId = LoanRateQuote.id 217 | 218 | log.info(s"$message for: $loanRateQuoteId") 219 | 220 | val loanRateQuote = 221 | LoanRateQuote( 222 | context.system, 223 | loanRateQuoteId, 224 | message.taxId, 225 | message.amount, 226 | message.termInMonths, 227 | self) 228 | 229 | startProcess(loanRateQuoteId, loanRateQuote) 230 | } 231 | } 232 | 233 | //=========== LoanRateQuote 234 | 235 | case class StartLoanRateQuote( 236 | expectedLoanRateQuotes: Integer) 237 | 238 | case class LoanRateQuoteStarted( 239 | loanRateQuoteId: String, 240 | taxId: String) 241 | 242 | case class TerminateLoanRateQuote() 243 | 244 | case class LoanRateQuoteTerminated( 245 | loanRateQuoteId: String, 246 | taxId: String) 247 | 248 | case class EstablishCreditScoreForLoanRateQuote( 249 | loanRateQuoteId: String, 250 | taxId: String, 251 | score: Integer) 252 | 253 | case class CreditScoreForLoanRateQuoteEstablished( 254 | loanRateQuoteId: String, 255 | taxId: String, 256 | score: Integer, 257 | amount: Integer, 258 | termInMonths: Integer) 259 | 260 | case class CreditScoreForLoanRateQuoteDenied( 261 | loanRateQuoteId: String, 262 | taxId: String, 263 | amount: Integer, 264 | termInMonths: Integer, 265 | score: Integer) 266 | 267 | case class RecordLoanRateQuote( 268 | bankId: String, 269 | bankLoanRateQuoteId: String, 270 | interestRate: Double) 271 | 272 | case class LoanRateQuoteRecorded( 273 | loanRateQuoteId: String, 274 | taxId: String, 275 | bankLoanRateQuote: BankLoanRateQuote) 276 | 277 | case class LoanRateBestQuoteFilled( 278 | loanRateQuoteId: String, 279 | taxId: String, 280 | amount: Integer, 281 | termInMonths: Integer, 282 | creditScore: Integer, 283 | bestBankLoanRateQuote: BankLoanRateQuote) 284 | 285 | case class BankLoanRateQuote( 286 | bankId: String, 287 | bankLoanRateQuoteId: String, 288 | interestRate: Double) 289 | 290 | object LoanRateQuote { 291 | val randomLoanRateQuoteId = new Random() 292 | 293 | def apply( 294 | system: ActorSystem, 295 | loanRateQuoteId: String, 296 | taxId: String, 297 | amount: Integer, 298 | termInMonths: Integer, 299 | loanBroker: ActorRef): ActorRef = { 300 | 301 | val loanRateQuote = 302 | system.actorOf( 303 | Props( 304 | classOf[LoanRateQuote], 305 | loanRateQuoteId, taxId, 306 | amount, termInMonths, loanBroker), 307 | "loanRateQuote-" + loanRateQuoteId) 308 | 309 | loanRateQuote 310 | } 311 | 312 | def id() = { 313 | randomLoanRateQuoteId.nextInt(1000).toString 314 | } 315 | } 316 | 317 | class LoanRateQuote( 318 | loanRateQuoteId: String, 319 | taxId: String, 320 | amount: Integer, 321 | termInMonths: Integer, 322 | loanBroker: ActorRef) 323 | extends Actor { 324 | 325 | var bankLoanRateQuotes = Vector[BankLoanRateQuote]() 326 | var creditRatingScore: Int = _ 327 | var expectedLoanRateQuotes: Int = _ 328 | 329 | private def bestBankLoanRateQuote() = { 330 | var best = bankLoanRateQuotes(0) 331 | 332 | bankLoanRateQuotes map { bankLoanRateQuote => 333 | if (best.interestRate > 334 | bankLoanRateQuote.interestRate) { 335 | best = bankLoanRateQuote 336 | } 337 | } 338 | 339 | best 340 | } 341 | 342 | private def quotableCreditScore( 343 | score: Integer): Boolean = { 344 | score > 399 345 | } 346 | 347 | def receive = { 348 | case message: StartLoanRateQuote => 349 | expectedLoanRateQuotes = 350 | message.expectedLoanRateQuotes 351 | loanBroker ! 352 | LoanRateQuoteStarted( 353 | loanRateQuoteId, 354 | taxId) 355 | 356 | case message: EstablishCreditScoreForLoanRateQuote => 357 | creditRatingScore = message.score 358 | if (quotableCreditScore(creditRatingScore)) 359 | loanBroker ! 360 | CreditScoreForLoanRateQuoteEstablished( 361 | loanRateQuoteId, 362 | taxId, 363 | creditRatingScore, 364 | amount, 365 | termInMonths) 366 | else 367 | loanBroker ! 368 | CreditScoreForLoanRateQuoteDenied( 369 | loanRateQuoteId, 370 | taxId, 371 | amount, 372 | termInMonths, 373 | creditRatingScore) 374 | 375 | case message: RecordLoanRateQuote => 376 | val bankLoanRateQuote = 377 | BankLoanRateQuote( 378 | message.bankId, 379 | message.bankLoanRateQuoteId, 380 | message.interestRate) 381 | bankLoanRateQuotes = 382 | bankLoanRateQuotes :+ bankLoanRateQuote 383 | loanBroker ! 384 | LoanRateQuoteRecorded( 385 | loanRateQuoteId, 386 | taxId, 387 | bankLoanRateQuote) 388 | 389 | if (bankLoanRateQuotes.size >= 390 | expectedLoanRateQuotes) 391 | loanBroker ! 392 | LoanRateBestQuoteFilled( 393 | loanRateQuoteId, 394 | taxId, 395 | amount, 396 | termInMonths, 397 | creditRatingScore, 398 | bestBankLoanRateQuote) 399 | 400 | case message: TerminateLoanRateQuote => 401 | loanBroker ! 402 | LoanRateQuoteTerminated( 403 | loanRateQuoteId, 404 | taxId) 405 | } 406 | } 407 | 408 | //=========== CreditBureau 409 | 410 | case class CheckCredit( 411 | creditProcessingReferenceId: String, 412 | taxId: String) 413 | 414 | case class CreditChecked( 415 | creditProcessingReferenceId: String, 416 | taxId: String, 417 | score: Integer) 418 | 419 | class CreditBureau extends Actor { 420 | val creditRanges = Vector(300, 400, 500, 600, 700) 421 | val randomCreditRangeGenerator = new Random() 422 | val randomCreditScoreGenerator = new Random() 423 | 424 | def receive = { 425 | case message: CheckCredit => 426 | val range = 427 | creditRanges( 428 | randomCreditRangeGenerator.nextInt(5)) 429 | val score = 430 | range 431 | + randomCreditScoreGenerator.nextInt(20) 432 | 433 | sender ! 434 | CreditChecked( 435 | message.creditProcessingReferenceId, 436 | message.taxId, 437 | score) 438 | } 439 | } 440 | 441 | //=========== Bank 442 | 443 | case class QuoteLoanRate( 444 | loadQuoteReferenceId: String, 445 | taxId: String, 446 | creditScore: Integer, 447 | amount: Integer, 448 | termInMonths: Integer) 449 | 450 | case class BankLoanRateQuoted( 451 | bankId: String, 452 | bankLoanRateQuoteId: String, 453 | loadQuoteReferenceId: String, 454 | taxId: String, 455 | interestRate: Double) 456 | 457 | class Bank( 458 | bankId: String, 459 | primeRate: Double, 460 | ratePremium: Double) 461 | extends Actor { 462 | 463 | val randomDiscount = new Random() 464 | val randomQuoteId = new Random() 465 | 466 | private def calculateInterestRate( 467 | amount: Double, 468 | months: Double, 469 | creditScore: Double): Double = { 470 | 471 | val creditScoreDiscount = creditScore / 100.0 / 10.0 - 472 | (randomDiscount.nextInt(5) * 0.05) 473 | 474 | primeRate + ratePremium + ((months / 12.0) / 10.0) - 475 | creditScoreDiscount 476 | } 477 | 478 | def receive = { 479 | case message: QuoteLoanRate => 480 | val interestRate = 481 | calculateInterestRate( 482 | message.amount.toDouble, 483 | message.termInMonths.toDouble, 484 | message.creditScore.toDouble) 485 | 486 | sender ! BankLoanRateQuoted( 487 | bankId, randomQuoteId.nextInt(1000).toString, 488 | message.loadQuoteReferenceId, message.taxId, interestRate) 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/publishsubscribe/SubClassification.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.publishsubscribe 16 | 17 | import akka.actor._ 18 | import akka.event._ 19 | import akka.util._ 20 | import co.vaughnvernon.reactiveenterprise._ 21 | import java.math.RoundingMode 22 | 23 | object SubClassificationDriver extends CompletableApp(6) { 24 | val allSubscriber = system.actorOf(Props[AllMarketsSubscriber], "AllMarketsSubscriber") 25 | val nasdaqSubscriber = system.actorOf(Props[NASDAQSubscriber], "NASDAQSubscriber") 26 | val nyseSubscriber = system.actorOf(Props[NYSESubscriber], "NYSESubscriber") 27 | 28 | val quotesBus = new QuotesEventBus 29 | 30 | quotesBus.subscribe(allSubscriber, Market("quotes")) 31 | quotesBus.subscribe(nasdaqSubscriber, Market("quotes/NASDAQ")) 32 | quotesBus.subscribe(nyseSubscriber, Market("quotes/NYSE")) 33 | 34 | quotesBus.publish(PriceQuoted(Market("quotes/NYSE"), Symbol("ORCL"), new Money("37.84"))) 35 | quotesBus.publish(PriceQuoted(Market("quotes/NASDAQ"), Symbol("MSFT"), new Money("37.16"))) 36 | quotesBus.publish(PriceQuoted(Market("quotes/DAX"), Symbol("SAP:GR"), new Money("61.95"))) 37 | quotesBus.publish(PriceQuoted(Market("quotes/NKY"), Symbol("6701:JP"), new Money("237"))) 38 | 39 | awaitCompletion 40 | } 41 | 42 | case class Money(amount: BigDecimal) { 43 | def this(amount: String) = this(new java.math.BigDecimal(amount)) 44 | 45 | amount.setScale(4, BigDecimal.RoundingMode.HALF_UP) 46 | } 47 | 48 | case class Market(name: String) 49 | 50 | case class PriceQuoted(market: Market, ticker: Symbol, price: Money) 51 | 52 | class QuotesEventBus extends EventBus with SubchannelClassification { 53 | type Classifier = Market 54 | type Event = PriceQuoted 55 | type Subscriber = ActorRef 56 | 57 | protected def classify(event: Event): Classifier = { 58 | event.market 59 | } 60 | 61 | protected def publish(event: Event, subscriber: Subscriber): Unit = { 62 | subscriber ! event 63 | } 64 | 65 | protected def subclassification = new Subclassification[Classifier] { 66 | def isEqual( 67 | subscribedToClassifier: Classifier, 68 | eventClassifier: Classifier): Boolean = { 69 | 70 | subscribedToClassifier.equals(eventClassifier) 71 | } 72 | 73 | def isSubclass( 74 | subscribedToClassifier: Classifier, 75 | eventClassifier: Classifier): Boolean = { 76 | 77 | subscribedToClassifier.name.startsWith(eventClassifier.name) 78 | } 79 | } 80 | } 81 | 82 | class AllMarketsSubscriber extends Actor { 83 | def receive = { 84 | case quote: PriceQuoted => 85 | println(s"AllMarketsSubscriber received: $quote") 86 | SubClassificationDriver.completedStep 87 | } 88 | } 89 | 90 | class NASDAQSubscriber extends Actor { 91 | def receive = { 92 | case quote: PriceQuoted => 93 | println(s"NASDAQSubscriber received: $quote") 94 | SubClassificationDriver.completedStep 95 | } 96 | } 97 | 98 | class NYSESubscriber extends Actor { 99 | def receive = { 100 | case quote: PriceQuoted => 101 | println(s"NYSESubscriber received: $quote") 102 | SubClassificationDriver.completedStep 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/recipientlist/RecipientList.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.recipientlist 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class RequestForQuotation(rfqId: String, retailItems: Seq[RetailItem]) { 21 | val totalRetailPrice: Double = retailItems.map(retailItem => retailItem.retailPrice).sum 22 | } 23 | 24 | case class RetailItem(itemId: String, retailPrice: Double) 25 | 26 | case class PriceQuoteInterest(path: String, quoteProcessor: ActorRef, lowTotalRetail: Double, highTotalRetail: Double) 27 | 28 | case class RequestPriceQuote(rfqId: String, itemId: String, retailPrice: Double, orderTotalRetailPrice: Double) 29 | 30 | case class PriceQuote(rfqId: String, itemId: String, retailPrice: Double, discountPrice: Double) 31 | 32 | object RecipientListDriver extends CompletableApp(5) { 33 | val orderProcessor = system.actorOf(Props[MountaineeringSuppliesOrderProcessor], "orderProcessor") 34 | 35 | system.actorOf(Props(classOf[BudgetHikersPriceQuotes], orderProcessor), "budgetHikers") 36 | system.actorOf(Props(classOf[HighSierraPriceQuotes], orderProcessor), "highSierra") 37 | system.actorOf(Props(classOf[MountainAscentPriceQuotes], orderProcessor), "mountainAscent") 38 | system.actorOf(Props(classOf[PinnacleGearPriceQuotes], orderProcessor), "pinnacleGear") 39 | system.actorOf(Props(classOf[RockBottomOuterwearPriceQuotes], orderProcessor), "rockBottomOuterwear") 40 | 41 | orderProcessor ! RequestForQuotation("123", 42 | Vector(RetailItem("1", 29.95), 43 | RetailItem("2", 99.95), 44 | RetailItem("3", 14.95))) 45 | 46 | orderProcessor ! RequestForQuotation("125", 47 | Vector(RetailItem("4", 39.99), 48 | RetailItem("5", 199.95), 49 | RetailItem("6", 149.95), 50 | RetailItem("7", 724.99))) 51 | 52 | orderProcessor ! RequestForQuotation("129", 53 | Vector(RetailItem("8", 119.99), 54 | RetailItem("9", 499.95), 55 | RetailItem("10", 519.00), 56 | RetailItem("11", 209.50))) 57 | 58 | orderProcessor ! RequestForQuotation("135", 59 | Vector(RetailItem("12", 0.97), 60 | RetailItem("13", 9.50), 61 | RetailItem("14", 1.99))) 62 | 63 | orderProcessor ! RequestForQuotation("140", 64 | Vector(RetailItem("15", 107.50), 65 | RetailItem("16", 9.50), 66 | RetailItem("17", 599.99), 67 | RetailItem("18", 249.95), 68 | RetailItem("19", 789.99))) 69 | } 70 | 71 | class MountaineeringSuppliesOrderProcessor extends Actor { 72 | val interestRegistry = scala.collection.mutable.Map[String, PriceQuoteInterest]() 73 | 74 | def calculateRecipientList(rfq: RequestForQuotation): Iterable[ActorRef] = { 75 | for { 76 | interest <- interestRegistry.values 77 | if (rfq.totalRetailPrice >= interest.lowTotalRetail) 78 | if (rfq.totalRetailPrice <= interest.highTotalRetail) 79 | } yield interest.quoteProcessor 80 | } 81 | 82 | def dispatchTo(rfq: RequestForQuotation, recipientList: Iterable[ActorRef]) = { 83 | recipientList.foreach { recipient => 84 | rfq.retailItems.foreach { retailItem => 85 | println("OrderProcessor: " + rfq.rfqId + " item: " + retailItem.itemId + " to: " + recipient.path.toString) 86 | recipient ! RequestPriceQuote(rfq.rfqId, retailItem.itemId, retailItem.retailPrice, rfq.totalRetailPrice) 87 | } 88 | } 89 | } 90 | 91 | def receive = { 92 | case interest: PriceQuoteInterest => 93 | interestRegistry(interest.path) = interest 94 | case priceQuote: PriceQuote => 95 | println(s"OrderProcessor: received: $priceQuote") 96 | case rfq: RequestForQuotation => 97 | val recipientList = calculateRecipientList(rfq) 98 | dispatchTo(rfq, recipientList) 99 | case message: Any => 100 | println(s"OrderProcessor: received unexpected message: $message") 101 | } 102 | } 103 | 104 | class BudgetHikersPriceQuotes(interestRegistrar: ActorRef) extends Actor { 105 | interestRegistrar ! PriceQuoteInterest(self.path.toString, self, 1.00, 1000.00) 106 | 107 | def receive = { 108 | case rpq: RequestPriceQuote => 109 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 110 | sender ! PriceQuote(rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 111 | 112 | case message: Any => 113 | println(s"BudgetHikersPriceQuotes: received unexpected message: $message") 114 | } 115 | 116 | def discountPercentage(orderTotalRetailPrice: Double) = { 117 | if (orderTotalRetailPrice <= 100.00) 0.02 118 | else if (orderTotalRetailPrice <= 399.99) 0.03 119 | else if (orderTotalRetailPrice <= 499.99) 0.05 120 | else if (orderTotalRetailPrice <= 799.99) 0.07 121 | else 0.075 122 | } 123 | } 124 | 125 | class HighSierraPriceQuotes(interestRegistrar: ActorRef) extends Actor { 126 | interestRegistrar ! PriceQuoteInterest(self.path.toString, self, 100.00, 10000.00) 127 | 128 | def receive = { 129 | case rpq: RequestPriceQuote => 130 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 131 | sender ! PriceQuote(rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 132 | 133 | case message: Any => 134 | println(s"HighSierraPriceQuotes: received unexpected message: $message") 135 | } 136 | 137 | def discountPercentage(orderTotalRetailPrice: Double): Double = { 138 | if (orderTotalRetailPrice <= 150.00) 0.015 139 | else if (orderTotalRetailPrice <= 499.99) 0.02 140 | else if (orderTotalRetailPrice <= 999.99) 0.03 141 | else if (orderTotalRetailPrice <= 4999.99) 0.04 142 | else 0.05 143 | } 144 | } 145 | 146 | class MountainAscentPriceQuotes(interestRegistrar: ActorRef) extends Actor { 147 | interestRegistrar ! PriceQuoteInterest(self.path.toString, self, 70.00, 5000.00) 148 | 149 | def receive = { 150 | case rpq: RequestPriceQuote => 151 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 152 | sender ! PriceQuote(rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 153 | 154 | case message: Any => 155 | println(s"MountainAscentPriceQuotes: received unexpected message: $message") 156 | } 157 | 158 | def discountPercentage(orderTotalRetailPrice: Double) = { 159 | if (orderTotalRetailPrice <= 99.99) 0.01 160 | else if (orderTotalRetailPrice <= 199.99) 0.02 161 | else if (orderTotalRetailPrice <= 499.99) 0.03 162 | else if (orderTotalRetailPrice <= 799.99) 0.04 163 | else if (orderTotalRetailPrice <= 999.99) 0.045 164 | else if (orderTotalRetailPrice <= 2999.99) 0.0475 165 | else 0.05 166 | } 167 | } 168 | 169 | class PinnacleGearPriceQuotes(interestRegistrar: ActorRef) extends Actor { 170 | interestRegistrar ! PriceQuoteInterest(self.path.toString, self, 250.00, 500000.00) 171 | 172 | def receive = { 173 | case rpq: RequestPriceQuote => 174 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 175 | sender ! PriceQuote(rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 176 | 177 | case message: Any => 178 | println(s"PinnacleGearPriceQuotes: received unexpected message: $message") 179 | } 180 | 181 | def discountPercentage(orderTotalRetailPrice: Double) = { 182 | if (orderTotalRetailPrice <= 299.99) 0.015 183 | else if (orderTotalRetailPrice <= 399.99) 0.0175 184 | else if (orderTotalRetailPrice <= 499.99) 0.02 185 | else if (orderTotalRetailPrice <= 999.99) 0.03 186 | else if (orderTotalRetailPrice <= 1199.99) 0.035 187 | else if (orderTotalRetailPrice <= 4999.99) 0.04 188 | else if (orderTotalRetailPrice <= 7999.99) 0.05 189 | else 0.06 190 | } 191 | } 192 | 193 | class RockBottomOuterwearPriceQuotes(interestRegistrar: ActorRef) extends Actor { 194 | interestRegistrar ! PriceQuoteInterest(self.path.toString, self, 0.50, 7500.00) 195 | 196 | def receive = { 197 | case rpq: RequestPriceQuote => 198 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 199 | sender ! PriceQuote(rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 200 | 201 | case message: Any => 202 | println(s"RockBottomOuterwearPriceQuotes: received unexpected message: $message") 203 | } 204 | 205 | def discountPercentage(orderTotalRetailPrice: Double) = { 206 | if (orderTotalRetailPrice <= 100.00) 0.015 207 | else if (orderTotalRetailPrice <= 399.99) 0.02 208 | else if (orderTotalRetailPrice <= 499.99) 0.03 209 | else if (orderTotalRetailPrice <= 799.99) 0.04 210 | else if (orderTotalRetailPrice <= 999.99) 0.05 211 | else if (orderTotalRetailPrice <= 2999.99) 0.06 212 | else if (orderTotalRetailPrice <= 4999.99) 0.07 213 | else if (orderTotalRetailPrice <= 5999.99) 0.075 214 | else 0.08 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/requestreply/RequestReply.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.requestreply 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class Request(what: String) 21 | case class Reply(what: String) 22 | case class StartWith(server: ActorRef) 23 | 24 | object RequestReplyDriver extends CompletableApp(1) { 25 | val client = system.actorOf(Props[Client], "client") 26 | val server = system.actorOf(Props[Server], "server") 27 | client ! StartWith(server) 28 | 29 | awaitCompletion 30 | println("RequestReply: is completed.") 31 | } 32 | 33 | class Client extends Actor { 34 | def receive = { 35 | case StartWith(server) => 36 | println("Client: is starting...") 37 | server ! Request("REQ-1") 38 | case Reply(what) => 39 | println("Client: received response: " + what) 40 | RequestReplyDriver.completedStep() 41 | case _ => 42 | println("Client: received unexpected message") 43 | } 44 | } 45 | 46 | class Server extends Actor { 47 | def receive = { 48 | case Request(what) => 49 | println("Server: received request value: " + what) 50 | sender ! Reply("RESP-1 for " + what) 51 | case _ => 52 | println("Server: received unexpected message") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/resequencer/Resequencer.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.resequencer 16 | 17 | import java.util.concurrent.TimeUnit 18 | import java.util.Date 19 | import scala.concurrent._ 20 | import scala.concurrent.duration._ 21 | import scala.util._ 22 | import ExecutionContext.Implicits.global 23 | import akka.actor._ 24 | import co.vaughnvernon.reactiveenterprise._ 25 | 26 | case class SequencedMessage(correlationId: String, index: Int, total: Int) 27 | 28 | case class ResequencedMessages(dispatchableIndex: Int, sequencedMessages: Array[SequencedMessage]) { 29 | def advancedTo(dispatchableIndex: Int) = { 30 | ResequencedMessages(dispatchableIndex, sequencedMessages) 31 | } 32 | } 33 | 34 | object ResequencerDriver extends CompletableApp(10) { 35 | val sequencedMessageConsumer = system.actorOf(Props[SequencedMessageConsumer], "sequencedMessageConsumer") 36 | val resequencerConsumer = system.actorOf(Props(classOf[ResequencerConsumer], sequencedMessageConsumer), "resequencerConsumer") 37 | val chaosRouter = system.actorOf(Props(classOf[ChaosRouter], resequencerConsumer), "chaosRouter") 38 | 39 | for (index <- 1 to 5) chaosRouter ! SequencedMessage("ABC", index, 5) 40 | for (index <- 1 to 5) chaosRouter ! SequencedMessage("XYZ", index, 5) 41 | 42 | awaitCompletion 43 | println("Resequencer: is completed.") 44 | } 45 | 46 | class ChaosRouter(consumer: ActorRef) extends Actor { 47 | val random = new Random((new Date()).getTime) 48 | 49 | def receive = { 50 | case sequencedMessage: SequencedMessage => 51 | val millis = random.nextInt(100) + 1 52 | println(s"ChaosRouter: delaying delivery of $sequencedMessage for $millis milliseconds") 53 | val duration = Duration.create(millis, TimeUnit.MILLISECONDS) 54 | context.system.scheduler.scheduleOnce(duration, consumer, sequencedMessage) 55 | case message: Any => 56 | println(s"ChaosRouter: received unexpected: $message") 57 | } 58 | } 59 | 60 | class ResequencerConsumer(actualConsumer: ActorRef) extends Actor { 61 | val resequenced = scala.collection.mutable.Map[String, ResequencedMessages]() 62 | 63 | def dispatchAllSequenced(correlationId: String) = { 64 | val resequencedMessages = resequenced(correlationId) 65 | var dispatchableIndex = resequencedMessages.dispatchableIndex 66 | 67 | resequencedMessages.sequencedMessages.foreach { sequencedMessage => 68 | if (sequencedMessage.index == dispatchableIndex) { 69 | actualConsumer ! sequencedMessage 70 | 71 | dispatchableIndex += 1 72 | } 73 | } 74 | 75 | resequenced(correlationId) = 76 | resequencedMessages.advancedTo(dispatchableIndex) 77 | } 78 | 79 | def dummySequencedMessages(count: Int): Seq[SequencedMessage] = { 80 | for { 81 | index <- 1 to count 82 | } yield { 83 | SequencedMessage("", -1, count) 84 | } 85 | } 86 | 87 | def receive = { 88 | case unsequencedMessage: SequencedMessage => 89 | println(s"ResequencerConsumer: received: $unsequencedMessage") 90 | resequence(unsequencedMessage) 91 | dispatchAllSequenced(unsequencedMessage.correlationId) 92 | removeCompleted(unsequencedMessage.correlationId) 93 | case message: Any => 94 | println(s"ResequencerConsumer: received unexpected: $message") 95 | } 96 | 97 | def removeCompleted(correlationId: String) = { 98 | val resequencedMessages = resequenced(correlationId) 99 | 100 | if (resequencedMessages.dispatchableIndex > resequencedMessages.sequencedMessages(0).total) { 101 | resequenced.remove(correlationId) 102 | println(s"ResequencerConsumer: removed completed: $correlationId") 103 | } 104 | } 105 | 106 | def resequence(sequencedMessage: SequencedMessage) = { 107 | if (!resequenced.contains(sequencedMessage.correlationId)) { 108 | resequenced(sequencedMessage.correlationId) = 109 | ResequencedMessages(1, dummySequencedMessages(sequencedMessage.total).toArray) 110 | } 111 | 112 | resequenced(sequencedMessage.correlationId) 113 | .sequencedMessages 114 | .update(sequencedMessage.index - 1, sequencedMessage) 115 | } 116 | } 117 | 118 | class SequencedMessageConsumer extends Actor { 119 | def receive = { 120 | case sequencedMessage: SequencedMessage => 121 | println(s"SequencedMessageConsumer: received: $sequencedMessage") 122 | ResequencerDriver.completedStep() 123 | case message: Any => 124 | println(s"SequencedMessageConsumer: received unexpected: $message") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/returnaddress/ReturnAddress.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.returnaddress 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class Request(what: String) 21 | case class RequestComplex(what: String) 22 | case class Reply(what: String) 23 | case class ReplyToComplex(what: String) 24 | case class StartWith(server: ActorRef) 25 | 26 | object ReturnAddressDriver extends CompletableApp(2) { 27 | val client = system.actorOf(Props[Client], "client") 28 | val server = system.actorOf(Props[Server], "server") 29 | client ! StartWith(server) 30 | 31 | awaitCompletion 32 | println("ReturnAddress: is completed.") 33 | } 34 | 35 | class Client extends Actor { 36 | def receive = { 37 | case StartWith(server) => 38 | println("Client: is starting...") 39 | server ! Request("REQ-1") 40 | server ! RequestComplex("REQ-20") 41 | case Reply(what) => 42 | println("Client: received reply: " + what) 43 | ReturnAddressDriver.completedStep() 44 | case ReplyToComplex(what) => 45 | println("Client: received reply to complex: " + what) 46 | ReturnAddressDriver.completedStep() 47 | case _ => 48 | println("Client: received unexpected message") 49 | } 50 | } 51 | 52 | class Server extends Actor { 53 | val worker = context.actorOf(Props[Worker], "worker") 54 | 55 | def receive = { 56 | case request: Request => 57 | println("Server: received request value: " + request.what) 58 | sender ! Reply("RESP-1 for " + request.what) 59 | case request: RequestComplex => 60 | println("Server: received request value: " + request.what) 61 | worker forward request 62 | case _ => 63 | println("Server: received unexpected message") 64 | } 65 | } 66 | 67 | class Worker extends Actor { 68 | def receive = { 69 | case RequestComplex(what) => 70 | println("Worker: received complex request value: " + what) 71 | sender ! ReplyToComplex("RESP-2000 for " + what) 72 | case _ => 73 | println("Worker: received unexpected message") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/returnaddress/ReturnAddressInMessage.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.returnaddress 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class RequestWithReplyTo(what: String, replyTo: ActorRef) 21 | 22 | object ReturnAddressInMessageDriver extends CompletableApp(1) { 23 | val client = system.actorOf(Props[ClientSendsReplyTo], "client") 24 | val server = system.actorOf(Props[ServerRepliesTo], "server") 25 | client ! StartWith(server) 26 | 27 | awaitCompletion 28 | println("ReturnAddressInMessage: is completed.") 29 | } 30 | 31 | class ClientSendsReplyTo extends Actor { 32 | def receive = { 33 | case StartWith(server) => 34 | println("Client: is starting...") 35 | server ! RequestWithReplyTo("REQ-1", self) 36 | case Reply(what) => 37 | println("Client: received response: " + what) 38 | ReturnAddressInMessageDriver.completedStep() 39 | case _ => 40 | println("Client: received unexpected message") 41 | } 42 | } 43 | 44 | class ServerRepliesTo extends Actor { 45 | def receive = { 46 | case RequestWithReplyTo(what, replyTo) => 47 | println("Server: received request value: " + what) 48 | replyTo ! Reply("RESP-1 for " + what) 49 | case _ => 50 | println("Server: received unexpected message") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/routingslip/RoutingSlip.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.routingslip 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise._ 19 | 20 | case class CustomerInformation(val name: String, val federalTaxId: String) 21 | 22 | case class ContactInformation(val postalAddress: PostalAddress, val telephone: Telephone) 23 | 24 | case class PostalAddress( 25 | val address1: String, val address2: String, 26 | val city: String, val state: String, val zipCode: String) 27 | 28 | case class Telephone(val number: String) 29 | 30 | case class ServiceOption(val id: String, val description: String) 31 | 32 | case class RegistrationData(val customerInformation: CustomerInformation, val contactInformation: ContactInformation, val serviceOption: ServiceOption) 33 | 34 | case class ProcessStep(val name: String, val processor: ActorRef) 35 | 36 | case class RegistrationProcess(val processId: String, val processSteps: Seq[ProcessStep], val currentStep: Int) { 37 | def this(processId: String, processSteps: Seq[ProcessStep]) { 38 | this(processId, processSteps, 0) 39 | } 40 | 41 | def isCompleted: Boolean = { 42 | currentStep >= processSteps.size 43 | } 44 | 45 | def nextStep(): ProcessStep = { 46 | if (isCompleted) { 47 | throw new IllegalStateException("Process had already completed.") 48 | } 49 | 50 | processSteps(currentStep) 51 | } 52 | 53 | def stepCompleted(): RegistrationProcess = { 54 | new RegistrationProcess(processId, processSteps, currentStep + 1) 55 | } 56 | } 57 | 58 | case class RegisterCustomer(val registrationData: RegistrationData, val registrationProcess: RegistrationProcess) { 59 | def advance():Unit = { 60 | val advancedProcess = registrationProcess.stepCompleted 61 | if (!advancedProcess.isCompleted) { 62 | advancedProcess.nextStep().processor ! RegisterCustomer(registrationData, advancedProcess) 63 | } 64 | RoutingSlipDriver.completedStep() 65 | } 66 | } 67 | 68 | object RoutingSlipDriver extends CompletableApp(4) { 69 | val processId = java.util.UUID.randomUUID().toString 70 | 71 | val step1 = ProcessStep("create_customer", ServiceRegistry.customerVault(system, processId)) 72 | val step2 = ProcessStep("set_up_contact_info", ServiceRegistry.contactKeeper(system, processId)) 73 | val step3 = ProcessStep("select_service_plan", ServiceRegistry.servicePlanner(system, processId)) 74 | val step4 = ProcessStep("check_credit", ServiceRegistry.creditChecker(system, processId)) 75 | 76 | val registrationProcess = new RegistrationProcess(processId, Vector(step1, step2, step3, step4)) 77 | 78 | val registrationData = 79 | new RegistrationData( 80 | CustomerInformation("ABC, Inc.", "123-45-6789"), 81 | ContactInformation( 82 | PostalAddress("123 Main Street", "Suite 100", "Boulder", "CO", "80301"), 83 | Telephone("303-555-1212")), 84 | ServiceOption("99-1203", "A description of 99-1203.")) 85 | 86 | val registerCustomer = RegisterCustomer(registrationData, registrationProcess) 87 | 88 | registrationProcess.nextStep().processor ! registerCustomer 89 | 90 | awaitCompletion 91 | println("RoutingSlip: is completed.") 92 | } 93 | 94 | class CreditChecker extends Actor { 95 | def receive = { 96 | case registerCustomer: RegisterCustomer => 97 | val federalTaxId = registerCustomer.registrationData.customerInformation.federalTaxId 98 | println(s"CreditChecker: handling register customer to perform credit check: $federalTaxId") 99 | registerCustomer.advance() 100 | context.stop(self) 101 | case message: Any => 102 | println(s"CreditChecker: received unexpected message: $message") 103 | } 104 | } 105 | 106 | class ContactKeeper extends Actor { 107 | def receive = { 108 | case registerCustomer: RegisterCustomer => 109 | val contactInfo = registerCustomer.registrationData.contactInformation 110 | println(s"ContactKeeper: handling register customer to keep contact information: $contactInfo") 111 | registerCustomer.advance() 112 | context.stop(self) 113 | case message: Any => 114 | println(s"ContactKeeper: received unexpected message: $message") 115 | } 116 | } 117 | 118 | class CustomerVault extends Actor { 119 | def receive = { 120 | case registerCustomer: RegisterCustomer => 121 | val customerInformation = registerCustomer.registrationData.customerInformation 122 | println(s"CustomerVault: handling register customer to create a new custoner: $customerInformation") 123 | registerCustomer.advance() 124 | context.stop(self) 125 | case message: Any => 126 | println(s"CustomerVault: received unexpected message: $message") 127 | } 128 | } 129 | 130 | class ServicePlanner extends Actor { 131 | def receive = { 132 | case registerCustomer: RegisterCustomer => 133 | val serviceOption = registerCustomer.registrationData.serviceOption 134 | println(s"ServicePlanner: handling register customer to plan a new customer service: $serviceOption") 135 | registerCustomer.advance() 136 | context.stop(self) 137 | case message: Any => 138 | println(s"ServicePlanner: received unexpected message: $message") 139 | } 140 | } 141 | 142 | object ServiceRegistry { 143 | def contactKeeper(system: ActorSystem, id: String) = { 144 | system.actorOf(Props[ContactKeeper], "contactKeeper-" + id) 145 | } 146 | 147 | def creditChecker(system: ActorSystem, id: String) = { 148 | system.actorOf(Props[CreditChecker], "creditChecker-" + id) 149 | } 150 | 151 | def customerVault(system: ActorSystem, id: String) = { 152 | system.actorOf(Props[CustomerVault], "customerVault-" + id) 153 | } 154 | 155 | def servicePlanner(system: ActorSystem, id: String) = { 156 | system.actorOf(Props[ServicePlanner], "servicePlanner-" + id) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/scattergather/ScatterGather.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.scattergather 16 | 17 | import java.util.concurrent.TimeUnit 18 | import scala.concurrent._ 19 | import scala.concurrent.duration._ 20 | import ExecutionContext.Implicits.global 21 | import akka.actor._ 22 | import co.vaughnvernon.reactiveenterprise._ 23 | 24 | case class RequestForQuotation(rfqId: String, retailItems: Seq[RetailItem]) { 25 | val totalRetailPrice: Double = retailItems.map(retailItem => retailItem.retailPrice).sum 26 | } 27 | 28 | case class RetailItem(itemId: String, retailPrice: Double) 29 | 30 | case class RequestPriceQuote(rfqId: String, itemId: String, retailPrice: Double, orderTotalRetailPrice: Double) 31 | 32 | case class PriceQuote(quoterId: String, rfqId: String, itemId: String, retailPrice: Double, discountPrice: Double) 33 | 34 | case class PriceQuoteFulfilled(priceQuote: PriceQuote) 35 | 36 | case class PriceQuoteTimedOut(rfqId: String) 37 | 38 | case class RequiredPriceQuotesForFulfillment(rfqId: String, quotesRequested: Int) 39 | 40 | case class QuotationFulfillment(rfqId: String, quotesRequested: Int, priceQuotes: Seq[PriceQuote], requester: ActorRef) 41 | 42 | case class BestPriceQuotation(rfqId: String, priceQuotes: Seq[PriceQuote]) 43 | 44 | case class SubscribeToPriceQuoteRequests(quoterId: String, quoteProcessor: ActorRef) 45 | 46 | object ScatterGatherDriver extends CompletableApp(5) { 47 | val priceQuoteAggregator = system.actorOf(Props[PriceQuoteAggregator], "priceQuoteAggregator") 48 | 49 | val orderProcessor = system.actorOf(Props(classOf[MountaineeringSuppliesOrderProcessor], priceQuoteAggregator), "orderProcessor") 50 | 51 | system.actorOf(Props(classOf[BudgetHikersPriceQuotes], orderProcessor), "budgetHikers") 52 | system.actorOf(Props(classOf[HighSierraPriceQuotes], orderProcessor), "highSierra") 53 | system.actorOf(Props(classOf[MountainAscentPriceQuotes], orderProcessor), "mountainAscent") 54 | system.actorOf(Props(classOf[PinnacleGearPriceQuotes], orderProcessor), "pinnacleGear") 55 | system.actorOf(Props(classOf[RockBottomOuterwearPriceQuotes], orderProcessor), "rockBottomOuterwear") 56 | 57 | orderProcessor ! RequestForQuotation("123", 58 | Vector(RetailItem("1", 29.95), 59 | RetailItem("2", 99.95), 60 | RetailItem("3", 14.95))) 61 | 62 | orderProcessor ! RequestForQuotation("125", 63 | Vector(RetailItem("4", 39.99), 64 | RetailItem("5", 199.95), 65 | RetailItem("6", 149.95), 66 | RetailItem("7", 724.99))) 67 | 68 | orderProcessor ! RequestForQuotation("129", 69 | Vector(RetailItem("8", 119.99), 70 | RetailItem("9", 499.95), 71 | RetailItem("10", 519.00), 72 | RetailItem("11", 209.50))) 73 | 74 | orderProcessor ! RequestForQuotation("135", 75 | Vector(RetailItem("12", 0.97), 76 | RetailItem("13", 9.50), 77 | RetailItem("14", 1.99))) 78 | 79 | orderProcessor ! RequestForQuotation("140", 80 | Vector(RetailItem("15", 1295.50), 81 | RetailItem("16", 9.50), 82 | RetailItem("17", 599.99), 83 | RetailItem("18", 249.95), 84 | RetailItem("19", 789.99))) 85 | 86 | awaitCompletion 87 | println("Scatter-Gather: is completed.") 88 | } 89 | 90 | class MountaineeringSuppliesOrderProcessor(priceQuoteAggregator: ActorRef) extends Actor { 91 | val subscribers = scala.collection.mutable.Map[String, SubscribeToPriceQuoteRequests]() 92 | 93 | def dispatch(rfq: RequestForQuotation) = { 94 | subscribers.values.foreach { subscriber => 95 | val quoteProcessor = subscriber.quoteProcessor 96 | rfq.retailItems.foreach { retailItem => 97 | println("OrderProcessor: " + rfq.rfqId + " item: " + retailItem.itemId + " to: " + subscriber.quoterId) 98 | quoteProcessor ! RequestPriceQuote(rfq.rfqId, retailItem.itemId, retailItem.retailPrice, rfq.totalRetailPrice) 99 | } 100 | } 101 | } 102 | 103 | def receive = { 104 | case subscriber: SubscribeToPriceQuoteRequests => 105 | subscribers(subscriber.quoterId) = subscriber 106 | case priceQuote: PriceQuote => 107 | priceQuoteAggregator ! PriceQuoteFulfilled(priceQuote) 108 | println(s"OrderProcessor: received: $priceQuote") 109 | case rfq: RequestForQuotation => 110 | priceQuoteAggregator ! RequiredPriceQuotesForFulfillment(rfq.rfqId, subscribers.size * rfq.retailItems.size) 111 | dispatch(rfq) 112 | case bestPriceQuotation: BestPriceQuotation => 113 | println(s"OrderProcessor: received: $bestPriceQuotation") 114 | ScatterGatherDriver.completedStep() 115 | case message: Any => 116 | println(s"OrderProcessor: received unexpected message: $message") 117 | } 118 | } 119 | 120 | class PriceQuoteAggregator extends Actor { 121 | val fulfilledPriceQuotes = scala.collection.mutable.Map[String, QuotationFulfillment]() 122 | 123 | def bestPriceQuotationFrom(quotationFulfillment: QuotationFulfillment): BestPriceQuotation = { 124 | val bestPrices = scala.collection.mutable.Map[String, PriceQuote]() 125 | 126 | quotationFulfillment.priceQuotes.foreach { priceQuote => 127 | if (bestPrices.contains(priceQuote.itemId)) { 128 | if (bestPrices(priceQuote.itemId).discountPrice > priceQuote.discountPrice) { 129 | bestPrices(priceQuote.itemId) = priceQuote 130 | } 131 | } else { 132 | bestPrices(priceQuote.itemId) = priceQuote 133 | } 134 | } 135 | 136 | BestPriceQuotation(quotationFulfillment.rfqId, bestPrices.values.toVector) 137 | } 138 | 139 | def receive = { 140 | case required: RequiredPriceQuotesForFulfillment => 141 | fulfilledPriceQuotes(required.rfqId) = QuotationFulfillment(required.rfqId, required.quotesRequested, Vector(), sender) 142 | val duration = Duration.create(2, TimeUnit.SECONDS) 143 | context.system.scheduler.scheduleOnce(duration, self, PriceQuoteTimedOut(required.rfqId)) 144 | case priceQuoteFulfilled: PriceQuoteFulfilled => 145 | priceQuoteRequestFulfilled(priceQuoteFulfilled) 146 | println(s"PriceQuoteAggregator: fulfilled price quote: $PriceQuoteFulfilled") 147 | case priceQuoteTimedOut: PriceQuoteTimedOut => 148 | priceQuoteRequestTimedOut(priceQuoteTimedOut.rfqId) 149 | case message: Any => 150 | println(s"PriceQuoteAggregator: received unexpected message: $message") 151 | } 152 | 153 | def priceQuoteRequestFulfilled(priceQuoteFulfilled: PriceQuoteFulfilled) = { 154 | if (fulfilledPriceQuotes.contains(priceQuoteFulfilled.priceQuote.rfqId)) { 155 | val previousFulfillment = fulfilledPriceQuotes(priceQuoteFulfilled.priceQuote.rfqId) 156 | val currentPriceQuotes = previousFulfillment.priceQuotes :+ priceQuoteFulfilled.priceQuote 157 | val currentFulfillment = 158 | QuotationFulfillment( 159 | previousFulfillment.rfqId, 160 | previousFulfillment.quotesRequested, 161 | currentPriceQuotes, 162 | previousFulfillment.requester) 163 | 164 | if (currentPriceQuotes.size >= currentFulfillment.quotesRequested) { 165 | quoteBestPrice(currentFulfillment) 166 | } else { 167 | fulfilledPriceQuotes(priceQuoteFulfilled.priceQuote.rfqId) = currentFulfillment 168 | } 169 | } 170 | } 171 | 172 | def priceQuoteRequestTimedOut(rfqId: String) = { 173 | if (fulfilledPriceQuotes.contains(rfqId)) { 174 | quoteBestPrice(fulfilledPriceQuotes(rfqId)) 175 | } 176 | } 177 | 178 | def quoteBestPrice(quotationFulfillment: QuotationFulfillment) = { 179 | if (fulfilledPriceQuotes.contains(quotationFulfillment.rfqId)) { 180 | quotationFulfillment.requester ! bestPriceQuotationFrom(quotationFulfillment) 181 | fulfilledPriceQuotes.remove(quotationFulfillment.rfqId) 182 | } 183 | } 184 | } 185 | 186 | class BudgetHikersPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor { 187 | val quoterId = self.path.name 188 | priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self) 189 | 190 | def receive = { 191 | case rpq: RequestPriceQuote => 192 | if (rpq.orderTotalRetailPrice < 1000.00) { 193 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 194 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 195 | } else { 196 | println(s"BudgetHikersPriceQuotes: ignoring: $rpq") 197 | } 198 | 199 | case message: Any => 200 | println(s"BudgetHikersPriceQuotes: received unexpected message: $message") 201 | } 202 | 203 | def discountPercentage(orderTotalRetailPrice: Double) = { 204 | if (orderTotalRetailPrice <= 100.00) 0.02 205 | else if (orderTotalRetailPrice <= 399.99) 0.03 206 | else if (orderTotalRetailPrice <= 499.99) 0.05 207 | else if (orderTotalRetailPrice <= 799.99) 0.07 208 | else 0.075 209 | } 210 | } 211 | 212 | class HighSierraPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor { 213 | val quoterId = self.path.name 214 | priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self) 215 | 216 | def receive = { 217 | case rpq: RequestPriceQuote => 218 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 219 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 220 | 221 | case message: Any => 222 | println(s"HighSierraPriceQuotes: received unexpected message: $message") 223 | } 224 | 225 | def discountPercentage(orderTotalRetailPrice: Double): Double = { 226 | if (orderTotalRetailPrice <= 150.00) 0.015 227 | else if (orderTotalRetailPrice <= 499.99) 0.02 228 | else if (orderTotalRetailPrice <= 999.99) 0.03 229 | else if (orderTotalRetailPrice <= 4999.99) 0.04 230 | else 0.05 231 | } 232 | } 233 | 234 | class MountainAscentPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor { 235 | val quoterId = self.path.name 236 | priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self) 237 | 238 | def receive = { 239 | case rpq: RequestPriceQuote => 240 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 241 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 242 | 243 | case message: Any => 244 | println(s"MountainAscentPriceQuotes: received unexpected message: $message") 245 | } 246 | 247 | def discountPercentage(orderTotalRetailPrice: Double) = { 248 | if (orderTotalRetailPrice <= 99.99) 0.01 249 | else if (orderTotalRetailPrice <= 199.99) 0.02 250 | else if (orderTotalRetailPrice <= 499.99) 0.03 251 | else if (orderTotalRetailPrice <= 799.99) 0.04 252 | else if (orderTotalRetailPrice <= 999.99) 0.045 253 | else if (orderTotalRetailPrice <= 2999.99) 0.0475 254 | else 0.05 255 | } 256 | } 257 | 258 | class PinnacleGearPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor { 259 | val quoterId = self.path.name 260 | priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self) 261 | 262 | def receive = { 263 | case rpq: RequestPriceQuote => 264 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 265 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 266 | 267 | case message: Any => 268 | println(s"PinnacleGearPriceQuotes: received unexpected message: $message") 269 | } 270 | 271 | def discountPercentage(orderTotalRetailPrice: Double) = { 272 | if (orderTotalRetailPrice <= 299.99) 0.015 273 | else if (orderTotalRetailPrice <= 399.99) 0.0175 274 | else if (orderTotalRetailPrice <= 499.99) 0.02 275 | else if (orderTotalRetailPrice <= 999.99) 0.03 276 | else if (orderTotalRetailPrice <= 1199.99) 0.035 277 | else if (orderTotalRetailPrice <= 4999.99) 0.04 278 | else if (orderTotalRetailPrice <= 7999.99) 0.05 279 | else 0.06 280 | } 281 | } 282 | 283 | class RockBottomOuterwearPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor { 284 | val quoterId = self.path.name 285 | priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self) 286 | 287 | def receive = { 288 | case rpq: RequestPriceQuote => 289 | if (rpq.orderTotalRetailPrice < 2000.00) { 290 | val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice 291 | sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount) 292 | } else { 293 | println(s"RockBottomOuterwearPriceQuotes: ignoring: $rpq") 294 | } 295 | 296 | case message: Any => 297 | println(s"RockBottomOuterwearPriceQuotes: received unexpected message: $message") 298 | } 299 | 300 | def discountPercentage(orderTotalRetailPrice: Double) = { 301 | if (orderTotalRetailPrice <= 100.00) 0.015 302 | else if (orderTotalRetailPrice <= 399.99) 0.02 303 | else if (orderTotalRetailPrice <= 499.99) 0.03 304 | else if (orderTotalRetailPrice <= 799.99) 0.04 305 | else if (orderTotalRetailPrice <= 999.99) 0.05 306 | else if (orderTotalRetailPrice <= 2999.99) 0.06 307 | else if (orderTotalRetailPrice <= 4999.99) 0.07 308 | else if (orderTotalRetailPrice <= 5999.99) 0.075 309 | else 0.08 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/selectiveconsumer/SelectiveConsumer.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.selectiveconsumer 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise.CompletableApp 19 | 20 | object SelectiveConsumerDriver extends CompletableApp(3) { 21 | val consumerOfA = 22 | system.actorOf( 23 | Props[ConsumerOfMessageTypeA], 24 | "consumerOfA") 25 | 26 | val consumerOfB = 27 | system.actorOf( 28 | Props[ConsumerOfMessageTypeB], 29 | "consumerOfB") 30 | 31 | val consumerOfC = 32 | system.actorOf( 33 | Props[ConsumerOfMessageTypeC], 34 | "consumerOfC") 35 | 36 | val selectiveConsumer = 37 | system.actorOf( 38 | Props(classOf[SelectiveConsumer], 39 | consumerOfA, consumerOfB, consumerOfC), 40 | "selectiveConsumer") 41 | 42 | selectiveConsumer ! MessageTypeA() 43 | selectiveConsumer ! MessageTypeB() 44 | selectiveConsumer ! MessageTypeC() 45 | 46 | awaitCompletion 47 | 48 | println("SelectiveConsumer: completed.") 49 | } 50 | 51 | case class MessageTypeA() 52 | case class MessageTypeB() 53 | case class MessageTypeC() 54 | 55 | class SelectiveConsumer( 56 | consumerOfA: ActorRef, 57 | consumerOfB: ActorRef, 58 | consumerOfC: ActorRef) extends Actor { 59 | 60 | def receive = { 61 | case message: MessageTypeA => consumerOfA forward message 62 | case message: MessageTypeB => consumerOfB forward message 63 | case message: MessageTypeC => consumerOfC forward message 64 | } 65 | } 66 | 67 | class ConsumerOfMessageTypeA extends Actor { 68 | def receive = { 69 | case message: MessageTypeA => 70 | println(s"ConsumerOfMessageTypeA: $message") 71 | SelectiveConsumerDriver.completedStep 72 | } 73 | } 74 | 75 | class ConsumerOfMessageTypeB extends Actor { 76 | def receive = { 77 | case message: MessageTypeB => 78 | println(s"ConsumerOfMessageTypeB: $message") 79 | SelectiveConsumerDriver.completedStep 80 | } 81 | } 82 | 83 | class ConsumerOfMessageTypeC extends Actor { 84 | def receive = { 85 | case message: MessageTypeC => 86 | println(s"ConsumerOfMessageTypeC: $message") 87 | SelectiveConsumerDriver.completedStep 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/smartproxy/SmartProxy.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.smartproxy 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise.CompletableApp 19 | 20 | object SmartProxyDriver extends CompletableApp(6) { 21 | val serviceProvider = system.actorOf( 22 | Props[ServiceProvider], 23 | "serviceProvider") 24 | 25 | val proxy = system.actorOf( 26 | Props(classOf[ServiceProviderProxy], serviceProvider), 27 | "proxy") 28 | 29 | val requester1 = system.actorOf( 30 | Props(classOf[ServiceRequester], proxy), 31 | "requester1") 32 | 33 | val requester2 = system.actorOf( 34 | Props(classOf[ServiceRequester], proxy), 35 | "requester2") 36 | 37 | val requester3 = system.actorOf( 38 | Props(classOf[ServiceRequester], proxy), 39 | "requester3") 40 | 41 | requester1 ! RequestService(ServiceRequestOne("1")) 42 | requester2 ! RequestService(ServiceRequestTwo("2")) 43 | requester3 ! RequestService(ServiceRequestThree("3")) 44 | 45 | awaitCompletion 46 | 47 | println("SmartProxy: completed.") 48 | } 49 | 50 | case class RequestService(service: ServiceRequest) 51 | 52 | class ServiceRequester(serviceProvider: ActorRef) extends Actor { 53 | def receive = { 54 | case request: RequestService => 55 | println(s"ServiceRequester: ${self.path.name}: $request") 56 | serviceProvider ! request.service 57 | SmartProxyDriver.completedStep 58 | case reply: Any => 59 | println(s"ServiceRequester: ${self.path.name}: $reply") 60 | SmartProxyDriver.completedStep 61 | } 62 | } 63 | 64 | class ServiceProviderProxy(serviceProvider: ActorRef) extends Actor { 65 | val requesters = scala.collection.mutable.Map[String, ActorRef]() 66 | 67 | def receive = { 68 | case request: ServiceRequest => 69 | requesters(request.requestId) = sender 70 | serviceProvider ! request 71 | analyzeRequest(request) 72 | case reply: ServiceReply => 73 | val sender = requesters.remove(reply.replyId) 74 | if (sender.isDefined) { 75 | analyzeReply(reply) 76 | sender.get ! reply 77 | } 78 | } 79 | 80 | def analyzeReply(reply: ServiceReply) = { 81 | println(s"Reply analyzed: $reply") 82 | } 83 | 84 | def analyzeRequest(request: ServiceRequest) = { 85 | println(s"Request analyzed: $request") 86 | } 87 | } 88 | 89 | trait ServiceRequest { 90 | def requestId: String 91 | } 92 | 93 | case class ServiceRequestOne(requestId: String) extends ServiceRequest 94 | case class ServiceRequestTwo(requestId: String) extends ServiceRequest 95 | case class ServiceRequestThree(requestId: String) extends ServiceRequest 96 | 97 | trait ServiceReply { 98 | def replyId: String 99 | } 100 | 101 | case class ServiceReplyOne(replyId: String) extends ServiceReply 102 | case class ServiceReplyTwo(replyId: String) extends ServiceReply 103 | case class ServiceReplyThree(replyId: String) extends ServiceReply 104 | 105 | class ServiceProvider extends Actor { 106 | def receive = { 107 | case one: ServiceRequestOne => 108 | sender ! ServiceReplyOne(one.requestId) 109 | case two: ServiceRequestTwo => 110 | sender ! ServiceReplyTwo(two.requestId) 111 | case three: ServiceRequestThree => 112 | sender ! ServiceReplyThree(three.requestId) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/splitter/Splitter.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.splitter 16 | 17 | import scala.collection.Map 18 | import akka.actor._ 19 | import co.vaughnvernon.reactiveenterprise._ 20 | 21 | case class Order(orderItems: Map[String, OrderItem]) { 22 | // val grandTotal: Double = orderItems.values.map(orderItem => orderItem.price).sum 23 | // val grandTotal: Double = orderItems.values.foldLeft(0.0)((grandTotal: Double, orderItem: OrderItem) => grandTotal + orderItem.price) 24 | val grandTotal: Double = orderItems.values.map(_.price).sum 25 | 26 | override def toString = { 27 | s"Order(Order Items: $orderItems Totaling: $grandTotal)" 28 | } 29 | } 30 | 31 | case class OrderItem(id: String, itemType: String, description: String, price: Double) { 32 | override def toString = { 33 | s"OrderItem($id, $itemType, '$description', $price)" 34 | } 35 | } 36 | 37 | case class OrderPlaced(order: Order) 38 | case class TypeAItemOrdered(orderItem: OrderItem) 39 | case class TypeBItemOrdered(orderItem: OrderItem) 40 | case class TypeCItemOrdered(orderItem: OrderItem) 41 | 42 | object SplitterDriver extends CompletableApp(4) { 43 | val orderRouter = system.actorOf(Props[OrderRouter], "orderRouter") 44 | val orderItem1 = OrderItem("1", "TypeA", "An item of type A.", 23.95) 45 | val orderItem2 = OrderItem("2", "TypeB", "An item of type B.", 99.95) 46 | val orderItem3 = OrderItem("3", "TypeC", "An item of type C.", 14.95) 47 | val orderItems = Map(orderItem1.itemType -> orderItem1, orderItem2.itemType -> orderItem2, orderItem3.itemType -> orderItem3) 48 | orderRouter ! OrderPlaced(Order(orderItems)) 49 | awaitCompletion 50 | println("Splitter: is completed.") 51 | } 52 | 53 | class OrderRouter extends Actor { 54 | val orderItemTypeAProcessor = context.actorOf(Props[OrderItemTypeAProcessor], "orderItemTypeAProcessor") 55 | val orderItemTypeBProcessor = context.actorOf(Props[OrderItemTypeBProcessor], "orderItemTypeBProcessor") 56 | val orderItemTypeCProcessor = context.actorOf(Props[OrderItemTypeCProcessor], "orderItemTypeCProcessor") 57 | 58 | def receive = { 59 | case OrderPlaced(order) => 60 | println(order) 61 | order.orderItems foreach { case (itemType, orderItem) => itemType match { 62 | case "TypeA" => 63 | println(s"OrderRouter: routing $itemType") 64 | orderItemTypeAProcessor ! TypeAItemOrdered(orderItem) 65 | case "TypeB" => 66 | println(s"OrderRouter: routing $itemType") 67 | orderItemTypeBProcessor ! TypeBItemOrdered(orderItem) 68 | case "TypeC" => 69 | println(s"OrderRouter: routing $itemType") 70 | orderItemTypeCProcessor ! TypeCItemOrdered(orderItem) 71 | }} 72 | 73 | SplitterDriver.completedStep() 74 | case _ => 75 | println("OrderRouter: received unexpected message") 76 | } 77 | } 78 | 79 | class OrderItemTypeAProcessor extends Actor { 80 | def receive = { 81 | case TypeAItemOrdered(orderItem) => 82 | println(s"OrderItemTypeAProcessor: handling $orderItem") 83 | SplitterDriver.completedStep() 84 | case _ => 85 | println("OrderItemTypeAProcessor: received unexpected message") 86 | } 87 | } 88 | 89 | class OrderItemTypeBProcessor extends Actor { 90 | def receive = { 91 | case TypeBItemOrdered(orderItem) => 92 | println(s"OrderItemTypeBProcessor: handling $orderItem") 93 | SplitterDriver.completedStep() 94 | case _ => 95 | println("OrderItemTypeBProcessor: received unexpected message") 96 | } 97 | } 98 | 99 | class OrderItemTypeCProcessor extends Actor { 100 | def receive = { 101 | case TypeCItemOrdered(orderItem) => 102 | println(s"OrderItemTypeCProcessor: handling $orderItem") 103 | SplitterDriver.completedStep() 104 | case _ => 105 | println("OrderItemTypeCProcessor: received unexpected message") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/transactionalactor/EventSourced.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.transactionalactor 16 | 17 | import akka.persistence._ 18 | import co.vaughnvernon.reactiveenterprise.CompletableApp 19 | 20 | object EventSourcedDriver extends CompletableApp(4) { 21 | 22 | } 23 | 24 | trait DomainEvent 25 | 26 | case class StartOrder(orderId: String, customerInfo: String) 27 | case class OrderStarted(orderId: String, customerInfo: String) extends DomainEvent 28 | 29 | case class AddOrderLineItem(productId: String, units: Int, price: Double) 30 | case class OrderLineItemAdded(productId: String, units: Int, price: Double) extends DomainEvent 31 | 32 | case class PlaceOrder() 33 | case class OrderPlaced() extends DomainEvent 34 | 35 | class Order extends EventsourcedProcessor { 36 | var state: OrderState = _ // new OrderState() 37 | 38 | def updateState(event: DomainEvent): Unit = 39 | state = state.update(event) 40 | 41 | val receiveRecover: Receive = { 42 | case event: DomainEvent => 43 | updateState(event) 44 | case SnapshotOffer(_, snapshot: OrderState) => 45 | state = snapshot 46 | } 47 | 48 | val receiveCommand: Receive = { 49 | case startOrder: StartOrder => 50 | persist(OrderStarted(startOrder.orderId, startOrder.customerInfo)) (updateState) 51 | case addOrderLineItem @ AddOrderLineItem => 52 | //persist(OrderStarted(addOrderLineItem., startOrder.customerInfo)) (updateState) 53 | case placeOrder @ PlaceOrder => 54 | } 55 | } 56 | 57 | case class OrderState( 58 | customerInfo: String, 59 | lineItems: List[OrderLineItem], 60 | placed: Boolean) { 61 | 62 | def update(event: DomainEvent): OrderState = { 63 | null 64 | } 65 | } 66 | 67 | case class OrderLineItem() 68 | -------------------------------------------------------------------------------- /src/co/vaughnvernon/reactiveenterprise/wiretap/WireTap.scala: -------------------------------------------------------------------------------- 1 | // Copyright 2012,2015 Vaughn Vernon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package co.vaughnvernon.reactiveenterprise.wiretap 16 | 17 | import akka.actor._ 18 | import co.vaughnvernon.reactiveenterprise.CompletableApp 19 | 20 | object WireTapDriver extends CompletableApp(2) { 21 | val order = Order("123") 22 | 23 | val orderProcessor = system.actorOf(Props[OrderProcessor], "orderProcessor") 24 | 25 | val logger = system.actorOf(Props(classOf[MessageLogger], orderProcessor), "logger") 26 | 27 | val orderProcessorWireTap = logger 28 | 29 | orderProcessorWireTap ! ProcessOrder(order) 30 | 31 | awaitCompletion 32 | 33 | println("WireTap: is completed.") 34 | } 35 | 36 | class MessageLogger(messageReceiver: ActorRef) extends Actor { 37 | def receive = { 38 | case m: Any => 39 | println(s"LOG: $m") 40 | messageReceiver forward m 41 | WireTapDriver.completedStep() 42 | } 43 | } 44 | 45 | case class Order(orderId: String) 46 | case class ProcessOrder(order: Order) 47 | 48 | class OrderProcessor extends Actor { 49 | def receive = { 50 | case command: ProcessOrder => 51 | println(s"OrderProcessor: received: $command") 52 | WireTapDriver.completedStep() 53 | } 54 | } 55 | --------------------------------------------------------------------------------