2010-03-10 22:38:52 +01:00
|
|
|
package se.scalablesolutions.akka.persistence.mongo
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
import junit.framework.TestCase
|
2009-11-23 20:23:18 +01:00
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
import org.junit.{Test, Before}
|
|
|
|
|
import org.junit.Assert._
|
2009-11-23 20:23:18 +01:00
|
|
|
|
|
|
|
|
import _root_.dispatch.json.{JsNumber, JsValue}
|
2009-10-08 19:01:04 +02:00
|
|
|
import _root_.dispatch.json.Js._
|
2009-08-15 18:06:04 +05:30
|
|
|
|
2010-03-05 16:07:06 +01:00
|
|
|
import se.scalablesolutions.akka.actor.{Transactor, Actor}
|
2009-11-23 20:23:18 +01:00
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
/**
|
|
|
|
|
* A persistent actor based on MongoDB storage.
|
|
|
|
|
* <p/>
|
|
|
|
|
* Demonstrates a bank account operation consisting of messages that:
|
|
|
|
|
* <li>checks balance <tt>Balance</tt></li>
|
|
|
|
|
* <li>debits amount<tt>Debit</tt></li>
|
|
|
|
|
* <li>debits multiple amounts<tt>MultiDebit</tt></li>
|
|
|
|
|
* <li>credits amount<tt>Credit</tt></li>
|
|
|
|
|
* <p/>
|
|
|
|
|
* Needs a running Mongo server.
|
|
|
|
|
* @author <a href="http://debasishg.blogspot.com">Debasish Ghosh</a>
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
case class Balance(accountNo: String)
|
|
|
|
|
case class Debit(accountNo: String, amount: BigInt, failer: Actor)
|
|
|
|
|
case class MultiDebit(accountNo: String, amounts: List[BigInt], failer: Actor)
|
|
|
|
|
case class Credit(accountNo: String, amount: BigInt)
|
|
|
|
|
case object LogSize
|
|
|
|
|
|
2010-03-05 16:07:06 +01:00
|
|
|
class BankAccountActor extends Transactor {
|
|
|
|
|
|
|
|
|
|
private lazy val accountState = MongoStorage.newMap
|
|
|
|
|
private lazy val txnLog = MongoStorage.newVector
|
2009-09-18 08:56:03 +02:00
|
|
|
|
2009-11-23 20:23:18 +01:00
|
|
|
def receive: PartialFunction[Any, Unit] = {
|
2009-08-15 18:06:04 +05:30
|
|
|
// check balance
|
|
|
|
|
case Balance(accountNo) =>
|
|
|
|
|
txnLog.add("Balance:" + accountNo)
|
|
|
|
|
reply(accountState.get(accountNo).get)
|
|
|
|
|
|
|
|
|
|
// debit amount: can fail
|
|
|
|
|
case Debit(accountNo, amount, failer) =>
|
|
|
|
|
txnLog.add("Debit:" + accountNo + " " + amount)
|
2009-11-25 23:39:06 +05:30
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
val m: BigInt =
|
|
|
|
|
accountState.get(accountNo) match {
|
2009-11-25 23:39:06 +05:30
|
|
|
case Some(JsNumber(n)) =>
|
|
|
|
|
BigInt(n.asInstanceOf[BigDecimal].intValue)
|
2009-08-15 18:06:04 +05:30
|
|
|
case None => 0
|
|
|
|
|
}
|
|
|
|
|
accountState.put(accountNo, (m - amount))
|
|
|
|
|
if (amount > m)
|
|
|
|
|
failer !! "Failure"
|
|
|
|
|
reply(m - amount)
|
|
|
|
|
|
|
|
|
|
// many debits: can fail
|
|
|
|
|
// demonstrates true rollback even if multiple puts have been done
|
|
|
|
|
case MultiDebit(accountNo, amounts, failer) =>
|
|
|
|
|
txnLog.add("MultiDebit:" + accountNo + " " + amounts.map(_.intValue).foldLeft(0)(_ + _))
|
2009-11-25 23:39:06 +05:30
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
val m: BigInt =
|
|
|
|
|
accountState.get(accountNo) match {
|
2009-11-25 23:39:06 +05:30
|
|
|
case Some(JsNumber(n)) => BigInt(n.toString)
|
2009-08-15 18:06:04 +05:30
|
|
|
case None => 0
|
|
|
|
|
}
|
|
|
|
|
var bal: BigInt = 0
|
|
|
|
|
amounts.foreach {amount =>
|
|
|
|
|
bal = bal + amount
|
|
|
|
|
accountState.put(accountNo, (m - bal))
|
|
|
|
|
}
|
|
|
|
|
if (bal > m) failer !! "Failure"
|
|
|
|
|
reply(m - bal)
|
|
|
|
|
|
|
|
|
|
// credit amount
|
|
|
|
|
case Credit(accountNo, amount) =>
|
|
|
|
|
txnLog.add("Credit:" + accountNo + " " + amount)
|
2009-11-25 23:39:06 +05:30
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
val m: BigInt =
|
|
|
|
|
accountState.get(accountNo) match {
|
2009-11-25 23:39:06 +05:30
|
|
|
case Some(JsNumber(n)) =>
|
|
|
|
|
BigInt(n.asInstanceOf[BigDecimal].intValue)
|
2009-08-15 18:06:04 +05:30
|
|
|
case None => 0
|
|
|
|
|
}
|
|
|
|
|
accountState.put(accountNo, (m + amount))
|
|
|
|
|
reply(m + amount)
|
|
|
|
|
|
|
|
|
|
case LogSize =>
|
|
|
|
|
reply(txnLog.length.asInstanceOf[AnyRef])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2010-03-05 16:07:06 +01:00
|
|
|
@serializable class PersistentFailerActor extends Transactor {
|
2009-12-17 21:53:49 +01:00
|
|
|
def receive = {
|
|
|
|
|
case "Failure" =>
|
|
|
|
|
throw new RuntimeException("expected")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
class MongoPersistentActorSpec extends TestCase {
|
|
|
|
|
@Test
|
|
|
|
|
def testSuccessfulDebit = {
|
|
|
|
|
val bactor = new BankAccountActor
|
|
|
|
|
bactor.start
|
|
|
|
|
val failer = new PersistentFailerActor
|
|
|
|
|
failer.start
|
|
|
|
|
bactor !! Credit("a-123", 5000)
|
|
|
|
|
bactor !! Debit("a-123", 3000, failer)
|
2009-11-25 23:39:06 +05:30
|
|
|
|
|
|
|
|
val JsNumber(b) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(2000), BigInt(b.intValue))
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
bactor !! Credit("a-123", 7000)
|
2009-11-25 23:39:06 +05:30
|
|
|
|
|
|
|
|
val JsNumber(b1) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(9000), BigInt(b1.intValue))
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
bactor !! Debit("a-123", 8000, failer)
|
2009-11-25 23:39:06 +05:30
|
|
|
|
|
|
|
|
val JsNumber(b2) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(1000), BigInt(b2.intValue))
|
|
|
|
|
|
2009-08-15 18:06:04 +05:30
|
|
|
assertEquals(7, (bactor !! LogSize).get)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
def testUnsuccessfulDebit = {
|
|
|
|
|
val bactor = new BankAccountActor
|
|
|
|
|
bactor.start
|
|
|
|
|
bactor !! Credit("a-123", 5000)
|
2009-08-28 17:00:39 +05:30
|
|
|
|
2009-11-25 23:39:06 +05:30
|
|
|
val JsNumber(b) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(5000), BigInt(b.intValue))
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
val failer = new PersistentFailerActor
|
|
|
|
|
failer.start
|
|
|
|
|
try {
|
|
|
|
|
bactor !! Debit("a-123", 7000, failer)
|
|
|
|
|
fail("should throw exception")
|
|
|
|
|
} catch { case e: RuntimeException => {}}
|
|
|
|
|
|
2009-11-25 23:39:06 +05:30
|
|
|
val JsNumber(b1) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(5000), BigInt(b1.intValue))
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
// should not count the failed one
|
|
|
|
|
assertEquals(3, (bactor !! LogSize).get)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
def testUnsuccessfulMultiDebit = {
|
|
|
|
|
val bactor = new BankAccountActor
|
|
|
|
|
bactor.start
|
|
|
|
|
bactor !! Credit("a-123", 5000)
|
2009-11-25 23:39:06 +05:30
|
|
|
|
|
|
|
|
val JsNumber(b) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(5000), BigInt(b.intValue))
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
val failer = new PersistentFailerActor
|
|
|
|
|
failer.start
|
|
|
|
|
try {
|
|
|
|
|
bactor !! MultiDebit("a-123", List(500, 2000, 1000, 3000), failer)
|
|
|
|
|
fail("should throw exception")
|
|
|
|
|
} catch { case e: RuntimeException => {}}
|
|
|
|
|
|
2009-11-25 23:39:06 +05:30
|
|
|
val JsNumber(b1) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue]
|
|
|
|
|
assertEquals(BigInt(5000), BigInt(b1.intValue))
|
2009-08-15 18:06:04 +05:30
|
|
|
|
|
|
|
|
// should not count the failed one
|
|
|
|
|
assertEquals(3, (bactor !! LogSize).get)
|
|
|
|
|
}
|
|
|
|
|
}
|