From d97df97a936e63b5089ce9b0c79cab0ddd2f723e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Thu, 8 Apr 2010 17:02:01 +0200 Subject: [PATCH 01/41] Added JTA module, monadic and higher-order functional API --- .../scala/AtomikosTransactionService.scala | 36 +++ .../src/main/scala/TransactionContext.scala | 181 ++++++++++++ .../src/main/scala/TransactionProtocol.scala | 258 ++++++++++++++++++ project/build/AkkaProject.scala | 17 +- 4 files changed, 489 insertions(+), 3 deletions(-) create mode 100644 akka-jta/src/main/scala/AtomikosTransactionService.scala create mode 100644 akka-jta/src/main/scala/TransactionContext.scala create mode 100644 akka-jta/src/main/scala/TransactionProtocol.scala diff --git a/akka-jta/src/main/scala/AtomikosTransactionService.scala b/akka-jta/src/main/scala/AtomikosTransactionService.scala new file mode 100644 index 0000000000..d688564686 --- /dev/null +++ b/akka-jta/src/main/scala/AtomikosTransactionService.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.jta + +import javax.transaction.{TransactionManager, SystemException} + +import com.atomikos.icatch.jta.{J2eeTransactionManager, J2eeUserTransaction} +import com.atomikos.icatch.config.{TSInitInfo, UserTransactionService, UserTransactionServiceImp} + +/** + * Atomikos implementation of the transaction service trait. + * + * @author Jonas Bonér + */ +object AtomikosTransactionService extends TransactionService with TransactionProtocol { + + // FIXME: make configurable + val JTA_TRANSACTION_TIMEOUT = 60 + private val txService: UserTransactionService = new UserTransactionServiceImp + private val info: TSInitInfo = txService.createTSInitInfo + + val transactionManager = + try { + txService.init(info) + val tm: TransactionManager = new J2eeTransactionManager + tm.setTransactionTimeout(JTA_TRANSACTION_TIMEOUT) + tm + } catch { + case e => throw new SystemException("Could not create a new Atomikos J2EE Transaction Manager, due to: " + e.toString) + } + + // TODO: gracefully shutdown of the TM + //txService.shutdown(false) +} diff --git a/akka-jta/src/main/scala/TransactionContext.scala b/akka-jta/src/main/scala/TransactionContext.scala new file mode 100644 index 0000000000..3ed562eeb5 --- /dev/null +++ b/akka-jta/src/main/scala/TransactionContext.scala @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.jta + +import javax.transaction.{Transaction, Status, TransactionManager} + +import se.scalablesolutions.akka.util.Logging + +/** + * JTA Transaction service. + * + * @author Jonas Bonér + */ +trait TransactionService { + def transactionManager: TransactionManager +} + +/** + * Base monad for the transaction monad implementations. + * + * @author Jonas Bonér + */ +trait TransactionMonad { + + // ----------------------------- + // Monadic definitions + // ----------------------------- + + def map[T](f: TransactionMonad => T): T + def flatMap[T](f: TransactionMonad => T): T + def foreach(f: TransactionMonad => Unit): Unit + def filter(f: TransactionMonad => Boolean): TransactionMonad = + if (f(this)) this else TransactionContext.NoOpTransactionMonad + + // ----------------------------- + // JTA Transaction definitions + // ----------------------------- + + /** + * Returns the current Transaction. + */ + def getTransaction: Transaction = TransactionContext.getTransactionManager.getTransaction + + /** + * Marks the current transaction as doomed. + */ + def setRollbackOnly = TransactionContext.setRollbackOnly + + /** + * Marks the current transaction as doomed. + */ + def doom = TransactionContext.setRollbackOnly + + /** + * Checks if the current transaction is doomed. + */ + def isRollbackOnly = TransactionContext.isRollbackOnly + + /** + * Checks that the current transaction is NOT doomed. + */ + def isNotDoomed = !TransactionContext.isRollbackOnly +} + +/** + * Manages a thread-local stack of TransactionContexts. + *

+ * Choose TransactionService implementation by implicit definition of the implementation of choice, + * e.g. implicit val txService = TransactionServices.AtomikosTransactionService. + *

+ * Example usage 1: + *

+ * for {
+ *   ctx <- TransactionContext.Required
+ *   entity <- updatedEntities
+ *   if !ctx.isRollbackOnly
+ * } {
+ *   // transactional stuff
+ *   ...
+ * }
+ * 
+ * Example usage 2: + *
+ * val users = for {
+ *   ctx <- TransactionContext.Required
+ *   name <- userNames
+ * } yield {
+ *   // transactional stuff
+ *   ...
+ * }
+ * 
+ * + * @author Jonas Bonér + */ +object TransactionContext extends TransactionProtocol with Logging { + // FIXME: make configurable + private implicit val defaultTransactionService = AtomikosTransactionService + + private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext) + + object Required extends TransactionMonad { + def map[T](f: TransactionMonad => T): T = withTxRequired { f(this) } + def flatMap[T](f: TransactionMonad => T): T = withTxRequired { f(this) } + def foreach(f: TransactionMonad => Unit): Unit = withTxRequired { f(this) } + } + + object RequiresNew extends TransactionMonad { + def map[T](f: TransactionMonad => T): T = withTxRequiresNew { f(this) } + def flatMap[T](f: TransactionMonad => T): T = withTxRequiresNew { f(this) } + def foreach(f: TransactionMonad => Unit): Unit = withTxRequiresNew { f(this) } + } + + object Supports extends TransactionMonad { + def map[T](f: TransactionMonad => T): T = withTxSupports { f(this) } + def flatMap[T](f: TransactionMonad => T): T = withTxSupports { f(this) } + def foreach(f: TransactionMonad => Unit): Unit = withTxSupports { f(this) } + } + + object Mandatory extends TransactionMonad { + def map[T](f: TransactionMonad => T): T = withTxMandatory { f(this) } + def flatMap[T](f: TransactionMonad => T): T = withTxMandatory { f(this) } + def foreach(f: TransactionMonad => Unit): Unit = withTxMandatory { f(this) } + } + + object Never extends TransactionMonad { + def map[T](f: TransactionMonad => T): T = withTxNever { f(this) } + def flatMap[T](f: TransactionMonad => T): T = withTxNever { f(this) } + def foreach(f: TransactionMonad => Unit): Unit = withTxNever { f(this) } + } + + object NoOpTransactionMonad extends TransactionMonad { + def map[T](f: TransactionMonad => T): T = f(this) + def flatMap[T](f: TransactionMonad => T): T = f(this) + def foreach(f: TransactionMonad => Unit): Unit = f(this) + override def filter(f: TransactionMonad => Boolean): TransactionMonad = this + } + + private[jta] def setRollbackOnly = current.setRollbackOnly + + private[jta] def isRollbackOnly = current.isRollbackOnly + + private[jta] def getTransactionManager: TransactionManager = current.getTransactionManager + + private[jta] def getTransaction: Transaction = current.getTransactionManager.getTransaction + + private[this] def current = stack.value + + /** + * Continues with the invocation defined in 'body' with the brand new context define in 'newCtx', the old + * one is put on the stack and will automatically come back in scope when the method exits. + *

+ * Suspends and resumes the current JTA transaction. + */ + private[jta] def withNewContext[T](body: => T): T = { + val suspendedTx: Option[Transaction] = + if (isInExistingTransaction(getTransactionManager)) { + log.debug("Suspending TX") + Some(getTransactionManager.suspend) + } else None + val result = stack.withValue(new TransactionContext) { body } + if (suspendedTx.isDefined) { + log.debug("Resuming TX") + getTransactionManager.resume(suspendedTx.get) + } + result + } +} + +/** + * Transaction context, holds the EntityManager and the TransactionManager. + * + * @author Jonas Bonér + */ +class TransactionContext(private implicit val transactionService: TransactionService) { + val tm: TransactionManager = transactionService.transactionManager + private def setRollbackOnly = tm.setRollbackOnly + private def isRollbackOnly: Boolean = tm.getStatus == Status.STATUS_MARKED_ROLLBACK + private def getTransactionManager: TransactionManager = tm +} diff --git a/akka-jta/src/main/scala/TransactionProtocol.scala b/akka-jta/src/main/scala/TransactionProtocol.scala new file mode 100644 index 0000000000..4a4c263eb1 --- /dev/null +++ b/akka-jta/src/main/scala/TransactionProtocol.scala @@ -0,0 +1,258 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.jta + +import se.scalablesolutions.akka.util.Logging + +import javax.naming.{NamingException, Context, InitialContext} +import javax.transaction.{ + Transaction, + UserTransaction, + TransactionManager, + Status, + RollbackException, + SystemException, + TransactionRequiredException +} + +/** + *

+ * Trait that implements a JTA transaction service that obeys the transaction semantics defined + * in the transaction attribute types for the transacted methods according to the EJB 3 draft specification. + * The aspect handles UserTransaction, TransactionManager instance variable injection thru @javax.ejb.Inject + * (name subject to change as per EJB 3 spec) and method transaction levels thru @javax.ejb.TransactionAttribute. + *

+ * + *

+ * This trait should be inherited to implement the getTransactionManager() method that should return a concrete + * javax.transaction.TransactionManager implementation (from JNDI lookup etc). + *

+ *

+ *

Transaction attribute semantics

+ * (From http://www.kevinboone.com/ejb-transactions.html) + *

+ *

+ *

Required

+ * 'Required' is probably the best choice (at least initially) for an EJB method that will need to be transactional. In this case, if the method's caller is already part of a transaction, then the EJB method does not create a new transaction, but continues in the same transaction as its caller. If the caller is not in a transaction, then a new transaction is created for the EJB method. If something happens in the EJB that means that a rollback is required, then the extent of the rollback will include everything done in the EJB method, whatever the condition of the caller. If the caller was in a transaction, then everything done by the caller will be rolled back as well. Thus the 'required' attribute ensures that any work done by the EJB will be rolled back if necessary, and if the caller requires a rollback that too will be rolled back. + *

+ *

+ *

RequiresNew

+ * 'RequiresNew' will be appropriate if you want to ensure that the EJB method is rolled back if necessary, but you don't want the rollback to propogate back to the caller. This attribute results in the creation of a new transaction for the method, regardless of the transactional state of the caller. If the caller was operating in a transaction, then its transaction is suspended until the EJB method completes. Because a new transaction is always created, there may be a slight performance penalty if this attribute is over-used. + *

+ *

+ *

Mandatory

+ * With the 'mandatory' attribute, the EJB method will not even start unless its caller is in a transaction. It will throw a TransactionRequiredException instead. If the method does start, then it will become part of the transaction of the caller. So if the EJB method signals a failure, the caller will be rolled back as well as the EJB. + *

+ *

+ *

Supports

+ * With this attribute, the EJB method does not care about the transactional context of its caller. If the caller is part of a transaction, then the EJB method will be part of the same transaction. If the EJB method fails, the transaction will roll back. If the caller is not part of a transaction, then the EJB method will still operate, but a failure will not cause anything to roll back. 'Supports' is probably the attribute that leads to the fastest method call (as there is no transactional overhead), but it can lead to unpredicatable results. If you want a method to be isolated from transactions, that is, to have no effect on the transaction of its caller, then use 'NotSupported' instead. + *

+ *

+ *

NotSupported

+ * With the 'NotSupported' attribute, the EJB method will never take part in a transaction. If the caller is part of a transaction, then the caller's transaction is suspended. If the EJB method fails, there will be no effect on the caller's transaction, and no rollback will occur. Use this method if you want to ensure that the EJB method will not cause a rollback in its caller. This is appropriate if, for example, the method does something non-essential, such as logging a message. It would not be helpful if the failure of this operation caused a transaction rollback. + *

+ *

+ *

Never

+ * The 'NotSupported'' attribute will ensure that the EJB method is never called by a transactional caller. Any attempt to do so will result in a RemoteException being thrown. This attribute is probably less useful than `NotSupported', in that NotSupported will assure that the caller's transaction is never affected by the EJB method (just as `Never' does), but will allow a call from a transactional caller if necessary. + *

+ * + * @author Jonas Bonér + */ +trait TransactionProtocol extends Logging { + + /** + * Join JTA transaction. Can be overriden by concrete transaction service implementation + * to hook into other transaction services. + *

+ * Here is an example on how to integrate with JPA EntityManager. + * + *

+   * override def joinTransaction = {
+   *   val em = TransactionContext.getEntityManager
+   *   val tm = TransactionContext.getTransactionManager
+   *   val closeAtTxCompletion: Boolean) 
+   *   tm.getTransaction.registerSynchronization(new javax.transaction.Synchronization() {
+   *     def beforeCompletion = {
+   *       try {
+   *         val status = tm.getStatus
+   *         if (status != Status.STATUS_ROLLEDBACK &&
+   *             status != Status.STATUS_ROLLING_BACK &&
+   *             status != Status.STATUS_MARKED_ROLLBACK) {
+   *           log.debug("Flushing EntityManager...") 
+   *           em.flush // flush EntityManager on success
+   *         }
+   *       } catch {
+   *         case e: javax.transaction.SystemException => throw new RuntimeException(e)
+   *       }
+   *     }
+   *
+   *     def afterCompletion(status: Int) = {
+   *       val status = tm.getStatus
+   *       if (closeAtTxCompletion) em.close
+   *       if (status == Status.STATUS_ROLLEDBACK ||
+   *           status == Status.STATUS_ROLLING_BACK ||
+   *           status == Status.STATUS_MARKED_ROLLBACK) {
+   *         em.close
+   *       }
+   *     }
+   *   })
+   *   em.joinTransaction // join JTA transaction
+   * }
+   */
+  def joinTransaction: Unit = {}
+
+  /**
+   * Handle exception. Can be overriden by concrete transaction service implementation.
+   * 

+ * Here is an example on how to handle JPA exceptions. + * + *

+   * def handleException(tm: TransactionManager, e: Exception) = {
+   *   if (isInExistingTransaction(tm)) {
+   *     // Do not roll back in case of NoResultException or NonUniqueResultException
+   *     if (!e.isInstanceOf[NoResultException] &&
+   *         !e.isInstanceOf[NonUniqueResultException]) {
+   *       log.debug("Setting TX to ROLLBACK_ONLY, due to: " + e)
+   *       tm.setRollbackOnly
+   *     }
+   *   }
+   *   throw e
+   * }
+   * 
+ */ + def handleException(tm: TransactionManager, e: Exception) = { + tm.setRollbackOnly + throw e + } + + /** + * Wraps body in a transaction with REQUIRED semantics. + *

+ * Creates a new transaction if no transaction is active in scope, else joins the outer transaction. + */ + def withTxRequired[T](body: => T): T = { + val tm = TransactionContext.getTransactionManager + if (!isInExistingTransaction(tm)) { + tm.begin + try { + joinTransaction + body + } catch { + case e: Exception => handleException(tm, e) + } finally { + commitOrRollBack(tm) + } + } else body + } + + /** + * Wraps body in a transaction with REQUIRES_NEW semantics. + *

+ * Suspends existing transaction, starts a new transaction, invokes body, + * commits or rollbacks new transaction, finally resumes previous transaction. + */ + def withTxRequiresNew[T](body: => T): T = TransactionContext.withNewContext { + val tm = TransactionContext.getTransactionManager + tm.begin + try { + joinTransaction + body + } catch { + case e: Exception => handleException(tm, e) + } finally { + commitOrRollBack(tm) + } + } + + /** + * Wraps body in a transaction with NOT_SUPPORTED semantics. + *

+ * Suspends existing transaction, invokes body, resumes transaction. + */ + def withTxNotSupported[T](body: => T): T = TransactionContext.withNewContext { + body + } + + /** + * Wraps body in a transaction with SUPPORTS semantics. + *

+ * Basicalla a No-op. + */ + def withTxSupports[T](body: => T): T = { + // attach to current if exists else skip -> do nothing + body + } + + /** + * Wraps body in a transaction with MANDATORY semantics. + *

+ * Throws a TransactionRequiredException if there is no transaction active in scope. + */ + def withTxMandatory[T](body: => T): T = { + if (!isInExistingTransaction(TransactionContext.getTransactionManager)) + throw new TransactionRequiredException("No active TX at method with TX type set to MANDATORY") + body + } + + /** + * Wraps body in a transaction with NEVER semantics. + *

+ * Throws a SystemException in case of an existing transaction in scope. + */ + def withTxNever[T](body: => T): T = { + if (isInExistingTransaction(TransactionContext.getTransactionManager)) + throw new SystemException("Detected active TX at method with TX type set to NEVER") + body + } + + protected def commitOrRollBack(tm: TransactionManager) = { + if (isInExistingTransaction(tm)) { + if (isRollbackOnly(tm)) { + log.debug("Rolling back TX marked as ROLLBACK_ONLY") + tm.rollback + } else { + log.debug("Committing TX") + tm.commit + } + } + } + + // --------------------------- + // Helper methods + // --------------------------- + + /** + * Checks if a transaction is an existing transaction. + * + * @param tm the transaction manager + * @return boolean + */ + protected def isInExistingTransaction(tm: TransactionManager): Boolean = + tm.getStatus != Status.STATUS_NO_TRANSACTION + + /** + * Checks if current transaction is set to rollback only. + * + * @param tm the transaction manager + * @return boolean + */ + protected def isRollbackOnly(tm: TransactionManager): Boolean = + tm.getStatus == Status.STATUS_MARKED_ROLLBACK + + /** + * A ThreadLocal variable where to store suspended TX and enable pay as you go + * before advice - after advice data sharing in a specific case of requiresNew TX + */ + private val suspendedTx = new ThreadLocal[Transaction] { + override def initialValue = null + } + + private def storeInThreadLocal(tx: Transaction) = suspendedTx.set(tx) + + private def fetchFromThreadLocal: Option[Transaction] = { + if (suspendedTx != null && suspendedTx.get() != null) Some(suspendedTx.get.asInstanceOf[Transaction]) + else None + } +} diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index b7c02135fc..1104535609 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -80,9 +80,10 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { val codehaus_snapshots = "Codehaus Snapshots" at "http://snapshots.repository.codehaus.org" val jboss = "jBoss" at "http://repository.jboss.org/maven2" val guiceyfruit = "GuiceyFruit" at "http://guiceyfruit.googlecode.com/svn/repo/releases/" - val google = "google" at "http://google-maven-repository.googlecode.com/svn/repository" - val m2 = "m2" at "http://download.java.net/maven/2" + val google = "Google" at "http://google-maven-repository.googlecode.com/svn/repository" + val java_net = "java.net" at "http://download.java.net/maven/2" val scala_tools_snapshots = "scala-tools snapshots" at "http://scala-tools.org/repo-snapshots" + val scala_tools_releases = "scala-tools releases" at "http://scala-tools.org/repo-releases" // ------------------------------------------------------------ // project defintions @@ -98,6 +99,7 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { lazy val akka_persistence = project("akka-persistence", "akka-persistence", new AkkaPersistenceParentProject(_)) lazy val akka_cluster = project("akka-cluster", "akka-cluster", new AkkaClusterParentProject(_)) lazy val akka_spring = project("akka-spring", "akka-spring", new AkkaSpringProject(_), akka_core) + lazy val akka_jta = project("akka-jta", "akka-jta", new AkkaJTAProject(_), akka_core) lazy val akka_servlet = project("akka-servlet", "akka-servlet", new AkkaServletProject(_), akka_core, akka_rest, akka_camel) lazy val akka_kernel = project("akka-kernel", "akka-kernel", new AkkaKernelProject(_), @@ -146,7 +148,8 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { " dist/akka-persistence-cassandra_%s-%s.jar".format(buildScalaVersion, version) + " dist/akka-servlet_%s-%s.jar".format(buildScalaVersion, version) + " dist/akka-kernel_%s-%s.jar".format(buildScalaVersion, version) + - " dist/akka-spring_%s-%s.jar".format(buildScalaVersion, version) + " dist/akka-spring_%s-%s.jar".format(buildScalaVersion, version) + + " dist/akka-jta_%s-%s.jar".format(buildScalaVersion, version) ) // ------------------------------------------------------------ @@ -324,6 +327,14 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { val junit = "junit" % "junit" % "4.5" % "test" } + class AkkaJTAProject(info: ProjectInfo) extends AkkaDefaultProject(info, distPath) { + val atomikos_transactions = "com.atomikos" % "transactions" % "3.2.3" % "compile" + val atomikos_transactions_jta = "com.atomikos" % "transactions-jta" % "3.2.3" % "compile" + val atomikos_transactions_api = "com.atomikos" % "transactions-api" % "3.2.3" % "compile" + //val atomikos_transactions_util = "com.atomikos" % "transactions-util" % "3.2.3" % "compile" + val jta_spec = "org.apache.geronimo.specs" % "geronimo-jta_1.1_spec" % "1.1.1" % "compile" + } + // examples class AkkaFunTestProject(info: ProjectInfo) extends DefaultProject(info) { val jackson_core_asl = "org.codehaus.jackson" % "jackson-core-asl" % "1.2.1" % "compile" From 7883b73052fd6ee94f2699163111518f9ff4c66d Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sat, 10 Apr 2010 23:59:40 +0200 Subject: [PATCH 02/41] Refactored _isEventBased into the MessageDispatcher --- akka-core/src/main/scala/actor/Actor.scala | 28 ++++++++----------- .../ExecutorBasedEventDrivenDispatcher.scala | 2 ++ ...sedEventDrivenWorkStealingDispatcher.scala | 2 ++ .../src/main/scala/dispatch/Reactor.scala | 1 + ...sedSingleThreadEventDrivenDispatcher.scala | 2 ++ ...BasedThreadPoolEventDrivenDispatcher.scala | 2 ++ .../dispatch/ThreadBasedDispatcher.scala | 2 ++ 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index 4bc3a9dc31..b80ce3afb1 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -232,7 +232,6 @@ trait Actor extends TransactionManagement with Logging { @volatile private[this] var _isRunning = false @volatile private[this] var _isSuspended = true @volatile private[this] var _isShutDown = false - @volatile private[this] var _isEventBased: Boolean = false @volatile private[akka] var _isKilled = false private var _hotswap: Option[PartialFunction[Any, Unit]] = None private[akka] var _remoteAddress: Option[InetSocketAddress] = None @@ -294,11 +293,7 @@ trait Actor extends TransactionManagement with Logging { * The default is also that all actors that are created and spawned from within this actor * is sharing the same dispatcher as its creator. */ - protected[akka] var messageDispatcher: MessageDispatcher = { - val dispatcher = Dispatchers.globalExecutorBasedEventDrivenDispatcher - _isEventBased = dispatcher.isInstanceOf[ExecutorBasedEventDrivenDispatcher] - dispatcher - } + protected[akka] var messageDispatcher: MessageDispatcher = Dispatchers.globalExecutorBasedEventDrivenDispatcher /** * User overridable callback/setting. @@ -513,8 +508,11 @@ trait Actor extends TransactionManagement with Logging { if (isActiveObject) throw e else None } - getResultOrThrowException(future) - } else throw new IllegalStateException( + + if (future.exception.isDefined) throw future.exception.get._2 + else future.result.asInstanceOf[Option[T]] + } + else throw new IllegalStateException( "Actor has not been started, you need to invoke 'actor.start' before using it") } @@ -593,7 +591,6 @@ trait Actor extends TransactionManagement with Logging { messageDispatcher.unregister(this) messageDispatcher = md messageDispatcher.register(this) - _isEventBased = messageDispatcher.isInstanceOf[ExecutorBasedEventDrivenDispatcher] } else throw new IllegalArgumentException( "Can not swap dispatcher for " + toString + " after it has been started") } @@ -816,7 +813,7 @@ trait Actor extends TransactionManagement with Logging { RemoteClient.clientFor(_remoteAddress.get).send(requestBuilder.build, None) } else { val invocation = new MessageInvocation(this, message, sender.map(Left(_)), transactionSet.get) - if (_isEventBased) { + if (messageDispatcher.usesActorMailbox) { _mailbox.add(invocation) if (_isSuspended) invocation.send } @@ -849,10 +846,11 @@ trait Actor extends TransactionManagement with Logging { val future = if (senderFuture.isDefined) senderFuture.get else new DefaultCompletableFuture(timeout) val invocation = new MessageInvocation(this, message, Some(Right(future)), transactionSet.get) - if (_isEventBased) { + + if (messageDispatcher.usesActorMailbox) _mailbox.add(invocation) - invocation.send - } else invocation.send + + invocation.send future } } @@ -958,10 +956,6 @@ trait Actor extends TransactionManagement with Logging { } } - private def getResultOrThrowException[T](future: Future): Option[T] = - if (future.exception.isDefined) throw future.exception.get._2 - else future.result.asInstanceOf[Option[T]] - private def base: PartialFunction[Any, Unit] = lifeCycles orElse (_hotswap getOrElse receive) private val lifeCycles: PartialFunction[Any, Unit] = { diff --git a/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index 705c3ee142..0c624c2e3a 100644 --- a/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -94,6 +94,8 @@ class ExecutorBasedEventDrivenDispatcher(_name: String) extends MessageDispatche active = false references.clear } + + def usesActorMailbox = true def ensureNotActive: Unit = if (active) throw new IllegalStateException( "Can't build a new thread pool for a dispatcher that is already up and running") diff --git a/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala b/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala index a96f5c5e76..28fe624b86 100644 --- a/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala +++ b/akka-core/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala @@ -199,6 +199,8 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher(_name: String) extends Mess pooledActors.remove(actor) super.unregister(actor) } + + def usesActorMailbox = true private def verifyActorsAreOfSameType(newActor: Actor) = { actorType match { diff --git a/akka-core/src/main/scala/dispatch/Reactor.scala b/akka-core/src/main/scala/dispatch/Reactor.scala index f9db74190f..3f300b1c52 100644 --- a/akka-core/src/main/scala/dispatch/Reactor.scala +++ b/akka-core/src/main/scala/dispatch/Reactor.scala @@ -68,6 +68,7 @@ trait MessageDispatcher extends Logging { } def canBeShutDown: Boolean = references.isEmpty def isShutdown: Boolean + def usesActorMailbox : Boolean } trait MessageDemultiplexer { diff --git a/akka-core/src/main/scala/dispatch/ReactorBasedSingleThreadEventDrivenDispatcher.scala b/akka-core/src/main/scala/dispatch/ReactorBasedSingleThreadEventDrivenDispatcher.scala index 15af513d62..fc99cf88d2 100644 --- a/akka-core/src/main/scala/dispatch/ReactorBasedSingleThreadEventDrivenDispatcher.scala +++ b/akka-core/src/main/scala/dispatch/ReactorBasedSingleThreadEventDrivenDispatcher.scala @@ -37,6 +37,8 @@ class ReactorBasedSingleThreadEventDrivenDispatcher(name: String) extends Abstra } def isShutdown = !active + + def usesActorMailbox = false class Demultiplexer(private val messageQueue: ReactiveMessageQueue) extends MessageDemultiplexer { diff --git a/akka-core/src/main/scala/dispatch/ReactorBasedThreadPoolEventDrivenDispatcher.scala b/akka-core/src/main/scala/dispatch/ReactorBasedThreadPoolEventDrivenDispatcher.scala index 941e701410..3f33d4ffc0 100644 --- a/akka-core/src/main/scala/dispatch/ReactorBasedThreadPoolEventDrivenDispatcher.scala +++ b/akka-core/src/main/scala/dispatch/ReactorBasedThreadPoolEventDrivenDispatcher.scala @@ -134,6 +134,8 @@ class ReactorBasedThreadPoolEventDrivenDispatcher(_name: String) if (fair) true else nrOfBusyMessages < 100 } + + def usesActorMailbox = false def ensureNotActive: Unit = if (active) throw new IllegalStateException( "Can't build a new thread pool for a dispatcher that is already up and running") diff --git a/akka-core/src/main/scala/dispatch/ThreadBasedDispatcher.scala b/akka-core/src/main/scala/dispatch/ThreadBasedDispatcher.scala index 8b1463f655..fbfffc999e 100644 --- a/akka-core/src/main/scala/dispatch/ThreadBasedDispatcher.scala +++ b/akka-core/src/main/scala/dispatch/ThreadBasedDispatcher.scala @@ -41,6 +41,8 @@ class ThreadBasedDispatcher private[akka] (val name: String, val messageHandler: def isShutdown = !active + def usesActorMailbox = false + def shutdown = if (active) { log.debug("Shutting down ThreadBasedDispatcher [%s]", name) active = false From e34819007edbc1b93a92d2db16b1cc2ccf10975f Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 11 Apr 2010 00:00:06 +0200 Subject: [PATCH 03/41] Moved a runtime error to compile time --- akka-core/src/main/scala/actor/Actor.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index b80ce3afb1..627913407b 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -221,7 +221,7 @@ object Actor extends Logging { * @author Jonas Bonér */ trait Actor extends TransactionManagement with Logging { - implicit protected val self: Option[Actor] = Some(this) + implicit protected val self: Some[Actor] = Some(this) // Only mutable for RemoteServer in order to maintain identity across nodes private[akka] var _uuid = UUID.newUuid.toString @@ -548,11 +548,10 @@ trait Actor extends TransactionManagement with Logging { *

* Works with both '!' and '!!'. */ - def forward(message: Any)(implicit sender: Option[Actor] = None) = { + def forward(message: Any)(implicit sender: Some[Actor]) = { if (_isKilled) throw new ActorKilledException("Actor [" + toString + "] has been killed, can't respond to messages") if (_isRunning) { - val forwarder = sender.getOrElse(throw new IllegalStateException("Can't forward message when the forwarder/mediator is not an actor")) - forwarder.replyTo match { + sender.get.replyTo match { case Some(Left(actor)) => postMessageToMailbox(message, Some(actor)) case Some(Right(future)) => postMessageToMailboxAndCreateFutureResultWithTimeout(message, timeout, Some(future)) case _ => throw new IllegalStateException("Can't forward message when initial sender is not an actor") From 90b28210f931b91dbf85b57da458a86dbd295d9d Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 11 Apr 2010 00:00:18 +0200 Subject: [PATCH 04/41] Documented replyTo --- akka-core/src/main/scala/actor/Actor.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index 627913407b..0b5aecf3f0 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -251,7 +251,10 @@ trait Actor extends TransactionManagement with Logging { // ==================================== /** - * TODO: Document replyTo + * Holds the reference to the sender of the currently processed message. + * Is None if no sender was specified + * Is Some(Left(Actor)) if sender is an actor + * Is Some(Right(CompletableFuture)) if sender is holding on to a Future for the result */ protected var replyTo: Option[Either[Actor,CompletableFuture]] = None From 4728c3c1f07c26cd00f8366ffc0763cd28c9672b Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Wed, 14 Apr 2010 09:56:01 +0200 Subject: [PATCH 05/41] implemented link/unlink for active objects --- .../src/main/scala/actor/ActiveObject.scala | 84 +++++++++++++++---- akka-core/src/main/scala/actor/Actor.scala | 18 ++-- .../akka/spring/foo/Bar.java | 6 ++ 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala index e3bd9ef943..cb67ae0244 100644 --- a/akka-core/src/main/scala/actor/ActiveObject.scala +++ b/akka-core/src/main/scala/actor/ActiveObject.scala @@ -4,6 +4,7 @@ package se.scalablesolutions.akka.actor +import _root_.se.scalablesolutions.akka.config.FaultHandlingStrategy import se.scalablesolutions.akka.remote.protobuf.RemoteProtocol.RemoteRequest import se.scalablesolutions.akka.remote.{RemoteProtocolBuilder, RemoteClient, RemoteRequestIdFactory} import se.scalablesolutions.akka.dispatch.{MessageDispatcher, Future} @@ -199,18 +200,71 @@ object ActiveObject { proxy.asInstanceOf[T] } -// Jan Kronquist: started work on issue 121 -// def actorFor(obj: AnyRef): Option[Actor] = { -// ActorRegistry.actorsFor(classOf[Dispatcher]).find(a=>a.target == Some(obj)) -// } -// -// def link(supervisor: AnyRef, activeObject: AnyRef) = { -// actorFor(supervisor).get !! Link(actorFor(activeObject).get) -// } -// -// def unlink(supervisor: AnyRef, activeObject: AnyRef) = { -// actorFor(supervisor).get !! Unlink(actorFor(activeObject).get) -// } + /** + * Get the underlying dispatcher actor for the given active object. + */ + def actorFor(obj: AnyRef): Option[Actor] = { + ActorRegistry.actorsFor(classOf[Dispatcher]).find(a=>a.target == Some(obj)) + } + + /** + * Links an other active object to this active object. + * @param supervisor the supervisor active object + * @param supervised the active object to link + */ + def link(supervisor: AnyRef, supervised: AnyRef) = { + val supervisorActor = actorFor(supervisor).getOrElse(throw new IllegalStateException("Can't link when the supervisor is not an active object")) + val supervisedActor = actorFor(supervised).getOrElse(throw new IllegalStateException("Can't link when the supervised is not an active object")) + supervisorActor !! Link(supervisedActor) + } + + /** + * Links an other active object to this active object and sets the fault handling for the supervisor. + * @param supervisor the supervisor active object + * @param supervised the active object to link + * @param handler fault handling strategy + * @param trapExceptions array of exceptions that should be handled by the supervisor + */ + def link(supervisor: AnyRef, supervised: AnyRef, handler: FaultHandlingStrategy, trapExceptions: Array[Class[_ <: Throwable]]) = { + val supervisorActor = actorFor(supervisor).getOrElse(throw new IllegalStateException("Can't link when the supervisor is not an active object")) + val supervisedActor = actorFor(supervised).getOrElse(throw new IllegalStateException("Can't link when the supervised is not an active object")) + supervisorActor.trapExit = trapExceptions.toList + supervisorActor.faultHandler = Some(handler) + supervisorActor !! Link(supervisedActor) + } + + /** + * Unlink the supervised active object from the supervisor. + * @param supervisor the supervisor active object + * @param supervised the active object to unlink + */ + def unlink(supervisor: AnyRef, supervised: AnyRef) = { + val supervisorActor = actorFor(supervisor).getOrElse(throw new IllegalStateException("Can't unlink when the supervisor is not an active object")) + val supervisedActor = actorFor(supervised).getOrElse(throw new IllegalStateException("Can't unlink when the supervised is not an active object")) + supervisorActor !! Unlink(supervisedActor) + } + + /** + * Sets the trap exit for the given supervisor active object. + * @param supervisor the supervisor active object + * @param trapExceptions array of exceptions that should be handled by the supervisor + */ + def trapExit(supervisor: AnyRef, trapExceptions: Array[Class[_ <: Throwable]]) = { + val supervisorActor = actorFor(supervisor).getOrElse(throw new IllegalStateException("Can't set trap exceptions when the supervisor is not an active object")) + supervisorActor.trapExit = trapExceptions.toList + this + } + + /** + * Sets the fault handling strategy for the given supervisor active object. + * @param supervisor the supervisor active object + * @param handler fault handling strategy + */ + def faultHandler(supervisor: AnyRef, handler: FaultHandlingStrategy) = { + val supervisorActor = actorFor(supervisor).getOrElse(throw new IllegalStateException("Can't set fault handler when the supervisor is not an active object")) + supervisorActor.faultHandler = Some(handler) + this + } private[akka] def supervise(restartStrategy: RestartStrategy, components: List[Supervise]): Supervisor = { val factory = SupervisorFactory(SupervisorConfig(restartStrategy, components)) @@ -366,7 +420,7 @@ private[akka] sealed class ActiveObjectAspect { } // Jan Kronquist: started work on issue 121 -// private[akka] case class Link(val actor: Actor) +private[akka] case class Link(val actor: Actor) object Dispatcher { val ZERO_ITEM_CLASS_ARRAY = Array[Class[_]]() @@ -434,8 +488,8 @@ private[akka] class Dispatcher(transactionalRequired: Boolean, val callbacks: Op if (isOneWay) joinPoint.proceed else reply(joinPoint.proceed) // Jan Kronquist: started work on issue 121 -// case Link(target) => -// link(target) + case Link(target) => link(target) + case Unlink(target) => unlink(target) case unexpected => throw new IllegalStateException("Unexpected message [" + unexpected + "] sent to [" + this + "]") } diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index 4bc3a9dc31..8c38769579 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -52,6 +52,7 @@ case class HotSwap(code: Option[PartialFunction[Any, Unit]]) extends LifeCycleMe case class Restart(reason: Throwable) extends LifeCycleMessage case class Exit(dead: Actor, killer: Throwable) extends LifeCycleMessage case class Unlink(child: Actor) extends LifeCycleMessage +case class UnlinkAndStop(child: Actor) extends LifeCycleMessage case object Kill extends LifeCycleMessage class ActorKilledException private[akka](message: String) extends RuntimeException(message) @@ -318,7 +319,7 @@ trait Actor extends TransactionManagement with Logging { * trapExit = List(classOf[MyApplicationException], classOf[MyApplicationError]) *

*/ - protected var trapExit: List[Class[_ <: Throwable]] = Nil + protected[akka] var trapExit: List[Class[_ <: Throwable]] = Nil /** * User overridable callback/setting. @@ -331,7 +332,7 @@ trait Actor extends TransactionManagement with Logging { * faultHandler = Some(OneForOneStrategy(maxNrOfRetries, withinTimeRange)) * */ - protected var faultHandler: Option[FaultHandlingStrategy] = None + protected[akka] var faultHandler: Option[FaultHandlingStrategy] = None /** * User overridable callback/setting. @@ -965,11 +966,12 @@ trait Actor extends TransactionManagement with Logging { private def base: PartialFunction[Any, Unit] = lifeCycles orElse (_hotswap getOrElse receive) private val lifeCycles: PartialFunction[Any, Unit] = { - case HotSwap(code) => _hotswap = code - case Restart(reason) => restart(reason) - case Exit(dead, reason) => handleTrapExit(dead, reason) - case Unlink(child) => unlink(child); child.stop - case Kill => throw new ActorKilledException("Actor [" + toString + "] was killed by a Kill message") + case HotSwap(code) => _hotswap = code + case Restart(reason) => restart(reason) + case Exit(dead, reason) => handleTrapExit(dead, reason) + case Unlink(child) => unlink(child) + case UnlinkAndStop(child) => unlink(child); child.stop + case Kill => throw new ActorKilledException("Actor [" + toString + "] was killed by a Kill message") } private[this] def handleTrapExit(dead: Actor, reason: Throwable): Unit = { @@ -1002,7 +1004,7 @@ trait Actor extends TransactionManagement with Logging { // if last temporary actor is gone, then unlink me from supervisor if (getLinkedActors.isEmpty) { Actor.log.info("All linked actors have died permanently (they were all configured as TEMPORARY)\n\tshutting down and unlinking supervisor actor as well [%s].", actor.id) - _supervisor.foreach(_ ! Unlink(this)) + _supervisor.foreach(_ ! UnlinkAndStop(this)) } } } diff --git a/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java b/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java index 7e21aaea8f..24e02a3fd6 100644 --- a/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java +++ b/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java @@ -1,10 +1,16 @@ package se.scalablesolutions.akka.spring.foo; +import java.io.IOException; + public class Bar implements IBar { @Override public String getBar() { return "bar"; } + + public void throwsIOException() throws IOException { + throw new IOException("some IO went wrong"); + } } From 4117d943af18c4d62517bfc5fbcf9e56ba52fa12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Wed, 14 Apr 2010 15:37:29 +0200 Subject: [PATCH 06/41] fixed bug with ignoring timeout in Java API --- .../src/main/scala/actor/ActiveObject.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala index e3bd9ef943..17cd482582 100644 --- a/akka-core/src/main/scala/actor/ActiveObject.scala +++ b/akka-core/src/main/scala/actor/ActiveObject.scala @@ -35,7 +35,7 @@ object Annotations { */ object ActiveObject { val AKKA_CAMEL_ROUTING_SCHEME = "akka" - private[actor] val AW_PROXY_PREFIX = "$$ProxiedByAW".intern + private[actor] val AW_PROXY_PREFIX = "$$ProxiedByAW".intern def newInstance[T](target: Class[T], timeout: Long): T = newInstance(target, new Dispatcher(false, None), None, timeout) @@ -227,19 +227,19 @@ private[akka] object AspectInitRegistry { val init = initializations.get(target) initializations.remove(target) init - } + } def register(target: AnyRef, init: AspectInit) = initializations.put(target, init) } private[akka] sealed case class AspectInit( val target: Class[_], - val actor: Dispatcher, + val actor: Dispatcher, val remoteAddress: Option[InetSocketAddress], val timeout: Long) { def this(target: Class[_],actor: Dispatcher, timeout: Long) = this(target, actor, None, timeout) } - + /** * AspectWerkz Aspect that is turning POJOs into Active Object. * Is deployed on a 'per-instance' basis. @@ -260,7 +260,7 @@ private[akka] sealed class ActiveObjectAspect { if (!isInitialized) { val init = AspectInitRegistry.initFor(joinPoint.getThis) target = init.target - actor = init.actor + actor = init.actor remoteAddress = init.remoteAddress timeout = init.timeout isInitialized = true @@ -279,7 +279,7 @@ private[akka] sealed class ActiveObjectAspect { (actor ! Invocation(joinPoint, true, true) ).asInstanceOf[AnyRef] } else { - val result = actor !! Invocation(joinPoint, false, isVoid(rtti)) + val result = actor !! (Invocation(joinPoint, false, isVoid(rtti)), timeout) if (result.isDefined) result.get else throw new IllegalStateException("No result defined for invocation [" + joinPoint + "]") } @@ -319,7 +319,7 @@ private[akka] sealed class ActiveObjectAspect { val (_, cause) = future.exception.get throw cause } else future.result.asInstanceOf[Option[T]] - + private def isOneWay(rtti: MethodRtti) = rtti.getMethod.isAnnotationPresent(Annotations.oneway) private def isVoid(rtti: MethodRtti) = rtti.getMethod.getReturnType == java.lang.Void.TYPE @@ -370,7 +370,7 @@ private[akka] sealed class ActiveObjectAspect { object Dispatcher { val ZERO_ITEM_CLASS_ARRAY = Array[Class[_]]() - val ZERO_ITEM_OBJECT_ARRAY = Array[Object]() + val ZERO_ITEM_OBJECT_ARRAY = Array[Object]() } /** @@ -408,7 +408,7 @@ private[akka] class Dispatcher(transactionalRequired: Boolean, val callbacks: Op "Could not find post restart method [" + post + "] \nin [" + targetClass.getName + "]. \nIt must have a zero argument definition.") }) } - // See if we have any annotation defined restart callbacks + // See if we have any annotation defined restart callbacks if (!preRestart.isDefined) preRestart = methods.find(m => m.isAnnotationPresent(Annotations.prerestart)) if (!postRestart.isDefined) postRestart = methods.find(m => m.isAnnotationPresent(Annotations.postrestart)) @@ -421,7 +421,7 @@ private[akka] class Dispatcher(transactionalRequired: Boolean, val callbacks: Op if (preRestart.isDefined) preRestart.get.setAccessible(true) if (postRestart.isDefined) postRestart.get.setAccessible(true) - + // see if we have a method annotated with @inittransactionalstate, if so invoke it initTxState = methods.find(m => m.isAnnotationPresent(Annotations.inittransactionalstate)) if (initTxState.isDefined && initTxState.get.getParameterTypes.length != 0) throw new IllegalStateException("Method annotated with @inittransactionalstate must have a zero argument definition") @@ -486,6 +486,6 @@ private[akka] class Dispatcher(transactionalRequired: Boolean, val callbacks: Op if (!unserializable && hasMutableArgument) { val copyOfArgs = Serializer.Java.deepClone(args) joinPoint.getRtti.asInstanceOf[MethodRtti].setParameterValues(copyOfArgs.asInstanceOf[Array[AnyRef]]) - } + } } } From 2abad7d4be4922bc43e682bf4d1cddcceab7d09f Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Wed, 14 Apr 2010 23:50:26 +0530 Subject: [PATCH 07/41] Redis client now implements pubsub. Also included a sample app for RedisPubSub in akka-samples --- .../src/main/scala/RedisPubSubServer.scala | 42 +++++++ .../src/main/scala/RedisPubSub.scala | 103 ++++++++++++++++++ .../redisclient-2.8.0.Beta1-1.3-SNAPSHOT.jar | Bin 118217 -> 161101 bytes project/build/AkkaProject.scala | 3 + 4 files changed, 148 insertions(+) create mode 100644 akka-persistence/akka-persistence-redis/src/main/scala/RedisPubSubServer.scala create mode 100644 akka-samples/akka-sample-pubsub/src/main/scala/RedisPubSub.scala diff --git a/akka-persistence/akka-persistence-redis/src/main/scala/RedisPubSubServer.scala b/akka-persistence/akka-persistence-redis/src/main/scala/RedisPubSubServer.scala new file mode 100644 index 0000000000..c5621361fb --- /dev/null +++ b/akka-persistence/akka-persistence-redis/src/main/scala/RedisPubSubServer.scala @@ -0,0 +1,42 @@ +package se.scalablesolutions.akka.persistence.redis + +import se.scalablesolutions.akka.actor.Actor +import com.redis._ + +sealed trait Msg +case class Subscribe(channels: Array[String]) extends Msg +case class Register(callback: PubSubMessage => Any) extends Msg +case class Unsubscribe(channels: Array[String]) extends Msg +case object UnsubscribeAll extends Msg +case class Publish(channel: String, msg: String) extends Msg + +class Subscriber(client: RedisClient) extends Actor { + var callback: PubSubMessage => Any = { m => } + + def receive = { + case Subscribe(channels) => + client.subscribe(channels.head, channels.tail: _*)(callback) + reply(true) + + case Register(cb) => + callback = cb + reply(true) + + case Unsubscribe(channels) => + client.unsubscribe(channels.head, channels.tail: _*) + reply(true) + + case UnsubscribeAll => + client.unsubscribe + reply(true) + } +} + +class Publisher(client: RedisClient) extends Actor { + def receive = { + case Publish(channel, message) => + client.publish(channel, message) + reply(true) + } +} + diff --git a/akka-samples/akka-sample-pubsub/src/main/scala/RedisPubSub.scala b/akka-samples/akka-sample-pubsub/src/main/scala/RedisPubSub.scala new file mode 100644 index 0000000000..ee14c2880d --- /dev/null +++ b/akka-samples/akka-sample-pubsub/src/main/scala/RedisPubSub.scala @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB . + */ + +package sample.pubsub + +import com.redis.{RedisClient, PubSubMessage, S, U, M} +import se.scalablesolutions.akka.persistence.redis._ + +/** + * Sample Akka application for Redis PubSub + * + * Prerequisite: Need Redis Server running (the version that supports pubsub) + * + * 1. Download redis from http://github.com/antirez/redis + * 2. build using "make" + * 3. Run server as ./redis-server + * + * For running this sample application :- + * + * 1. Open a shell and set AKKA_HOME to the distribution root + * 2. cd $AKKA_HOME + * 3. sbt console + * 4. import sample.pubsub._ + * 5. Sub.sub("a", "b") // starts Subscription server & subscribes to channels "a" and "b" + * + * 6. Open up another shell similarly as the above and set AKKA_HOME + * 7. cd $AKKA_HOME + * 8. sbt console + * 9. import sample.pubsub._ + * 10. Pub.publish("a", "hello") // the first shell should get the message + * 11. Pub.publish("c", "hi") // the first shell should NOT get this message + * + * 12. Open up a redis-client from where you installed redis and issue a publish command + * ./redis-cli publish a "hi there" ## the first shell should get the message + * + * 13. Go back to the first shell + * 14. Sub.unsub("a") // should unsubscribe the first shell from channel "a" + * + * 15. Study the callback function defined below. It supports many other message formats. + * In the second shell window do the following: + * scala> Pub.publish("b", "+c") // will subscribe the first window to channel "c" + * scala> Pub.publish("b", "+d") // will subscribe the first window to channel "d" + * scala> Pub.publish("b", "-c") // will unsubscribe the first window from channel "c" + * scala> Pub.publish("b", "exit") // will unsubscribe the first window from all channels + */ + +object Pub { + println("starting publishing service ..") + val r = new RedisClient("localhost", 6379) + val p = new Publisher(r) + p.start + + def publish(channel: String, message: String) = { + p ! Publish(channel, message) + } +} + +object Sub { + println("starting subscription service ..") + val r = new RedisClient("localhost", 6379) + val s = new Subscriber(r) + s.start + s ! Register(callback) + + def sub(channels: String*) = { + s ! Subscribe(channels.toArray) + } + + def unsub(channels: String*) = { + s ! Unsubscribe(channels.toArray) + } + + def callback(pubsub: PubSubMessage) = pubsub match { + case S(channel, no) => println("subscribed to " + channel + " and count = " + no) + case U(channel, no) => println("unsubscribed from " + channel + " and count = " + no) + case M(channel, msg) => + msg match { + // exit will unsubscribe from all channels and stop subscription service + case "exit" => + println("unsubscribe all ..") + r.unsubscribe + + // message "+x" will subscribe to channel x + case x if x startsWith "+" => + val s: Seq[Char] = x + s match { + case Seq('+', rest @ _*) => r.subscribe(rest.toString){ m => } + } + + // message "-x" will unsubscribe from channel x + case x if x startsWith "-" => + val s: Seq[Char] = x + s match { + case Seq('-', rest @ _*) => r.unsubscribe(rest.toString) + } + + // other message receive + case x => + println("received message on channel " + channel + " as : " + x) + } + } +} diff --git a/embedded-repo/com/redis/redisclient/2.8.0.Beta1-1.3-SNAPSHOT/redisclient-2.8.0.Beta1-1.3-SNAPSHOT.jar b/embedded-repo/com/redis/redisclient/2.8.0.Beta1-1.3-SNAPSHOT/redisclient-2.8.0.Beta1-1.3-SNAPSHOT.jar index 0daede37f0471d224388f4634d63c854642064ce..52125e6d5270851b9c045c6dc83764bd94cac1d2 100644 GIT binary patch delta 74689 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

)!C2IdS4j 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 !*|cpunVO!Bf&G^CI;xrmuXb)( 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 diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index 41a51b8c8c..b0466d5ec8 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -301,6 +301,7 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { } class AkkaSampleChatProject(info: ProjectInfo) extends AkkaDefaultProject(info, deployPath) + class AkkaSamplePubSubProject(info: ProjectInfo) extends AkkaDefaultProject(info, deployPath) class AkkaSampleLiftProject(info: ProjectInfo) extends AkkaDefaultProject(info, deployPath) { val commons_logging = "commons-logging" % "commons-logging" % "1.1.1" % "compile" @@ -335,6 +336,8 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { class AkkaSamplesParentProject(info: ProjectInfo) extends ParentProject(info) { lazy val akka_sample_chat = project("akka-sample-chat", "akka-sample-chat", new AkkaSampleChatProject(_), akka_kernel) + lazy val akka_sample_pubsub = project("akka-sample-pubsub", "akka-sample-pubsub", + new AkkaSamplePubSubProject(_), akka_kernel) lazy val akka_sample_lift = project("akka-sample-lift", "akka-sample-lift", new AkkaSampleLiftProject(_), akka_kernel) lazy val akka_sample_rest_java = project("akka-sample-rest-java", "akka-sample-rest-java", From 475649077208f38476ae2740a489f29e0717c9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Wed, 14 Apr 2010 20:47:19 +0200 Subject: [PATCH 08/41] Added AtomicTemplate to allow atomic blocks from Java code --- .../src/main/scala/stm/Transaction.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala index 16a3cd0e5f..209c131781 100644 --- a/akka-core/src/main/scala/stm/Transaction.scala +++ b/akka-core/src/main/scala/stm/Transaction.scala @@ -22,6 +22,25 @@ import org.multiverse.stms.alpha.AlphaStm class NoTransactionInScopeException extends RuntimeException class TransactionRetryException(message: String) extends RuntimeException(message) +/** + * FIXDOC: document AtomicTemplate + * AtomicTemplate can be used to create atomic blocks from Java code. + *

+ * User newUser = new AtomicTemplate[User]() {
+ *   User atomic() {
+ *     ... // create user atomically
+ *     return user;
+ *   }
+ * }.execute();
+ * 
+ */ +trait AtomicTemplate[T] { + def atomic: T + def execute: T = Transaction.Local.atomic { + atomic + } +} + object Transaction { val idFactory = new AtomicLong(-1L) From fac08d26d146cb12d4e0d305922013d342abb71b Mon Sep 17 00:00:00 2001 From: rossputin Date: Wed, 14 Apr 2010 20:33:46 +0100 Subject: [PATCH 09/41] update instructions for chat running chat sample --- akka-samples/akka-sample-chat/README | 6 +++--- .../akka-sample-chat/src/main/scala/ChatServer.scala | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/akka-samples/akka-sample-chat/README b/akka-samples/akka-sample-chat/README index 88720d8c55..66e54e3d44 100644 --- a/akka-samples/akka-sample-chat/README +++ b/akka-samples/akka-sample-chat/README @@ -17,10 +17,10 @@ Then to run the sample: - Set 'export AKKA_HOME=. - Run 'sbt console' to start up a REPL (interpreter). 4. In the first REPL you get execute: - - scala> import se.scalablesolutions.akka.sample.chat._ + - scala> import sample.chat._ - scala> ChatService.start -5. In the first REPL you get execute: - - scala> import se.scalablesolutions.akka.sample.chat._ +5. In the second REPL you get execute: + - scala> import sample.chat._ - scala> Runner.run 6. See the chat simulation run. 7. Run it again to see full speed after first initialization. diff --git a/akka-samples/akka-sample-chat/src/main/scala/ChatServer.scala b/akka-samples/akka-sample-chat/src/main/scala/ChatServer.scala index 6d4bf9679e..51c4c9f91c 100644 --- a/akka-samples/akka-sample-chat/src/main/scala/ChatServer.scala +++ b/akka-samples/akka-sample-chat/src/main/scala/ChatServer.scala @@ -33,10 +33,10 @@ Then to run the sample: - Set 'export AKKA_HOME=. - Run 'sbt console' to start up a REPL (interpreter). 2. In the first REPL you get execute: - - scala> import se.scalablesolutions.akka.sample.chat._ + - scala> import sample.chat._ - scala> ChatService.start -3. In the first REPL you get execute: - - scala> import se.scalablesolutions.akka.sample.chat._ +3. In the second REPL you get execute: + - scala> import sample.chat._ - scala> Runner.run 4. See the chat simulation run. 5. Run it again to see full speed after first initialization. From 51619a0838a0cd8bed7d7e1b696fed0cb519dc71 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Thu, 15 Apr 2010 16:05:16 +0200 Subject: [PATCH 10/41] Removed Scala 2.8 deprecation warnings --- .../src/test/scala/MongoStorageSpec.scala | 2 +- .../src/test/scala/SupervisionBeanDefinitionParserTest.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala index 186157b576..97307dde17 100644 --- a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala +++ b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala @@ -192,7 +192,7 @@ class MongoStorageSpec extends TestCase { assertTrue(l.map(_._1).contains("3")) assertTrue(l.map(_._1).contains("4")) - val JsString(str) = l.filter(_._1 == "2").first._2 + val JsString(str) = l.filter(_._1 == "2").head._2 assertEquals(str, "peter") // trying to fetch for a non-existent transaction will throw diff --git a/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala b/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala index af79ecf5df..e0344ae4fb 100644 --- a/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala +++ b/akka-spring/src/test/scala/SupervisionBeanDefinitionParserTest.scala @@ -50,7 +50,7 @@ class SupervisionBeanDefinitionParserTest extends Spec with ShouldMatchers { val supervised = builder.getBeanDefinition.getPropertyValues.getPropertyValue("supervised").getValue.asInstanceOf[List[ActiveObjectProperties]] assert(supervised != null) expect(3) { supervised.length } - val iterator = supervised.elements + val iterator = supervised.iterator expect("foo.bar.Foo") { iterator.next.target } expect("foo.bar.Bar") { iterator.next.target } expect("foo.bar.MyPojo") { iterator.next.target } From 379b6e2c8517c8cf4eb950670b5b8b073e3eb2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Fri, 16 Apr 2010 00:02:38 +0200 Subject: [PATCH 11/41] Updated old scaladoc --- akka-core/src/main/scala/dispatch/Dispatchers.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/akka-core/src/main/scala/dispatch/Dispatchers.scala b/akka-core/src/main/scala/dispatch/Dispatchers.scala index 49d9c624b6..2030d2026e 100644 --- a/akka-core/src/main/scala/dispatch/Dispatchers.scala +++ b/akka-core/src/main/scala/dispatch/Dispatchers.scala @@ -11,7 +11,7 @@ import se.scalablesolutions.akka.actor.Actor *

* Example usage: *

- *   val dispatcher = Dispatchers.newEventBasedThreadPoolDispatcher("name")
+ *   val dispatcher = Dispatchers.newExecutorBasedEventDrivenDispatcher("name")
  *   dispatcher
  *     .withNewThreadPoolWithBoundedBlockingQueue(100)
  *     .setCorePoolSize(16)
@@ -25,7 +25,7 @@ import se.scalablesolutions.akka.actor.Actor
  * 

* Example usage: *

- *   MessageDispatcher dispatcher = Dispatchers.newEventBasedThreadPoolDispatcher("name");
+ *   MessageDispatcher dispatcher = Dispatchers.newExecutorBasedEventDrivenDispatcher("name");
  *   dispatcher
  *     .withNewThreadPoolWithBoundedBlockingQueue(100)
  *     .setCorePoolSize(16)
@@ -40,9 +40,8 @@ import se.scalablesolutions.akka.actor.Actor
  */
 object Dispatchers {
   object globalExecutorBasedEventDrivenDispatcher extends ExecutorBasedEventDrivenDispatcher("global") {
-    override def register(actor : Actor) = {
-      if (isShutdown)
-        init
+    override def register(actor: Actor) = {
+      if (isShutdown) init
       super.register(actor)
     }
   }

From 5b0f267c403138824895a72b8749398e11b1df69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= 
Date: Fri, 16 Apr 2010 01:48:53 +0200
Subject: [PATCH 12/41] converted tabs to spaces

---
 akka-core/src/main/scala/actor/Actor.scala    |   4 +-
 .../src/test/scala/PerformanceSpec.scala      |  12 +-
 .../akka/spring/foo/Bar.java                  |   2 +-
 .../akka/spring/foo/StatefulPojo.java         |   2 +-
 .../spring/SupervisorConfigurationTest.java   | 174 +++++++++---------
 5 files changed, 97 insertions(+), 97 deletions(-)

diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala
index a78b3d09c8..37a297d5ca 100644
--- a/akka-core/src/main/scala/actor/Actor.scala
+++ b/akka-core/src/main/scala/actor/Actor.scala
@@ -712,7 +712,7 @@ trait Actor extends TransactionManagement with Logging {
    * 

* To be invoked from within the actor itself. */ - protected[this] def spawnRemote[T <: Actor : Manifest](hostname: String, port: Int): T = { + protected[this] def spawnRemote[T <: Actor: Manifest](hostname: String, port: Int): T = { val actor = spawnButDoNotStart[T] actor.makeRemote(hostname, port) actor.start @@ -724,7 +724,7 @@ trait Actor extends TransactionManagement with Logging { *

* To be invoked from within the actor itself. */ - protected[this] def spawnLink[T <: Actor : Manifest] : T = { + protected[this] def spawnLink[T <: Actor: Manifest]: T = { val actor = spawnButDoNotStart[T] try { actor.start diff --git a/akka-core/src/test/scala/PerformanceSpec.scala b/akka-core/src/test/scala/PerformanceSpec.scala index 742a560f06..dd00d5ac3e 100644 --- a/akka-core/src/test/scala/PerformanceSpec.scala +++ b/akka-core/src/test/scala/PerformanceSpec.scala @@ -57,9 +57,9 @@ class PerformanceSpec extends JUnitSuite { } protected def sender : Option[Actor] = replyTo match { - case Some(Left(actor)) => Some(actor) - case _ => None - } + case Some(Left(actor)) => Some(actor) + case _ => None + } def receive = { case MeetingCount(i) => { @@ -104,9 +104,9 @@ class PerformanceSpec extends JUnitSuite { } protected def sender : Option[Actor] = replyTo match { - case Some(Left(actor)) => Some(actor) - case _ => None - } + case Some(Left(actor)) => Some(actor) + case _ => None + } override def receive: PartialFunction[Any, Unit] = { case Meet(from, otherColour) => diff --git a/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java b/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java index 24e02a3fd6..1b9e67e09c 100644 --- a/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java +++ b/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/Bar.java @@ -10,7 +10,7 @@ public class Bar implements IBar { } public void throwsIOException() throws IOException { - throw new IOException("some IO went wrong"); + throw new IOException("some IO went wrong"); } } diff --git a/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/StatefulPojo.java b/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/StatefulPojo.java index 7ee334548c..f2308e194f 100644 --- a/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/StatefulPojo.java +++ b/akka-spring/akka-spring-test-java/src/main/java/se/scalablesolutions/akka/spring/foo/StatefulPojo.java @@ -15,7 +15,7 @@ public class StatefulPojo { @inittransactionalstate public void init() { if (!isInitialized) { - mapState = TransactionalState.newMap(); + mapState = TransactionalState.newMap(); vectorState = TransactionalState.newVector(); refState = TransactionalState.newRef(); isInitialized = true; diff --git a/akka-spring/akka-spring-test-java/src/test/java/se/scalablesolutions/akka/spring/SupervisorConfigurationTest.java b/akka-spring/akka-spring-test-java/src/test/java/se/scalablesolutions/akka/spring/SupervisorConfigurationTest.java index fbbe23d18c..659433cb9f 100644 --- a/akka-spring/akka-spring-test-java/src/test/java/se/scalablesolutions/akka/spring/SupervisorConfigurationTest.java +++ b/akka-spring/akka-spring-test-java/src/test/java/se/scalablesolutions/akka/spring/SupervisorConfigurationTest.java @@ -35,101 +35,101 @@ import se.scalablesolutions.akka.spring.foo.StatefulPojo; */ public class SupervisorConfigurationTest { - private ApplicationContext context = null; + private ApplicationContext context = null; - @Before - public void setUp() { - context = new ClassPathXmlApplicationContext( - "se/scalablesolutions/akka/spring/foo/supervisor-config.xml"); - } + @Before + public void setUp() { + context = new ClassPathXmlApplicationContext( + "se/scalablesolutions/akka/spring/foo/supervisor-config.xml"); + } - @Test - public void testSupervision() { - // get ActiveObjectConfigurator bean from spring context - ActiveObjectConfigurator myConfigurator = (ActiveObjectConfigurator) context - .getBean("supervision1"); - // get ActiveObjects - Foo foo = myConfigurator.getInstance(Foo.class); - assertNotNull(foo); - IBar bar = myConfigurator.getInstance(IBar.class); - assertNotNull(bar); - MyPojo pojo = myConfigurator.getInstance(MyPojo.class); - assertNotNull(pojo); - } + @Test + public void testSupervision() { + // get ActiveObjectConfigurator bean from spring context + ActiveObjectConfigurator myConfigurator = (ActiveObjectConfigurator) context + .getBean("supervision1"); + // get ActiveObjects + Foo foo = myConfigurator.getInstance(Foo.class); + assertNotNull(foo); + IBar bar = myConfigurator.getInstance(IBar.class); + assertNotNull(bar); + MyPojo pojo = myConfigurator.getInstance(MyPojo.class); + assertNotNull(pojo); + } - @Test - public void testTransactionalState() { - ActiveObjectConfigurator conf = (ActiveObjectConfigurator) context - .getBean("supervision2"); - StatefulPojo stateful = conf.getInstance(StatefulPojo.class); - stateful.setMapState("testTransactionalState", "some map state"); - stateful.setVectorState("some vector state"); - stateful.setRefState("some ref state"); - assertEquals("some map state", stateful - .getMapState("testTransactionalState")); - assertEquals("some vector state", stateful.getVectorState()); - assertEquals("some ref state", stateful.getRefState()); - } + @Test + public void testTransactionalState() { + ActiveObjectConfigurator conf = (ActiveObjectConfigurator) context + .getBean("supervision2"); + StatefulPojo stateful = conf.getInstance(StatefulPojo.class); + stateful.setMapState("testTransactionalState", "some map state"); + stateful.setVectorState("some vector state"); + stateful.setRefState("some ref state"); + assertEquals("some map state", stateful + .getMapState("testTransactionalState")); + assertEquals("some vector state", stateful.getVectorState()); + assertEquals("some ref state", stateful.getRefState()); + } - @Test - public void testInitTransactionalState() { - StatefulPojo stateful = ActiveObject.newInstance(StatefulPojo.class, - 1000, true); - assertTrue("should be inititalized", stateful.isInitialized()); - } + @Test + public void testInitTransactionalState() { + StatefulPojo stateful = ActiveObject.newInstance(StatefulPojo.class, + 1000, true); + assertTrue("should be inititalized", stateful.isInitialized()); + } - @Test - public void testSupervisionWithDispatcher() { - ActiveObjectConfigurator myConfigurator = (ActiveObjectConfigurator) context - .getBean("supervision-with-dispatcher"); - // get ActiveObjects - Foo foo = myConfigurator.getInstance(Foo.class); - assertNotNull(foo); - // TODO how to check dispatcher? - } - - @Test - public void testRemoteActiveObject() { - new Thread(new Runnable() { - public void run() { - RemoteNode.start(); - } - }).start(); - try { - Thread.currentThread().sleep(1000); - } catch (Exception e) { - } - Foo instance = ActiveObject.newRemoteInstance(Foo.class, 2000, "localhost", 9999); - System.out.println(instance.foo()); - } + @Test + public void testSupervisionWithDispatcher() { + ActiveObjectConfigurator myConfigurator = (ActiveObjectConfigurator) context + .getBean("supervision-with-dispatcher"); + // get ActiveObjects + Foo foo = myConfigurator.getInstance(Foo.class); + assertNotNull(foo); + // TODO how to check dispatcher? + } + + @Test + public void testRemoteActiveObject() { + new Thread(new Runnable() { + public void run() { + RemoteNode.start(); + } + }).start(); + try { + Thread.currentThread().sleep(1000); + } catch (Exception e) { + } + Foo instance = ActiveObject.newRemoteInstance(Foo.class, 2000, "localhost", 9999); + System.out.println(instance.foo()); + } - @Test - public void testSupervisedRemoteActiveObject() { - new Thread(new Runnable() { - public void run() { - RemoteNode.start(); - } - }).start(); - try { - Thread.currentThread().sleep(1000); - } catch (Exception e) { - } + @Test + public void testSupervisedRemoteActiveObject() { + new Thread(new Runnable() { + public void run() { + RemoteNode.start(); + } + }).start(); + try { + Thread.currentThread().sleep(1000); + } catch (Exception e) { + } - ActiveObjectConfigurator conf = new ActiveObjectConfigurator(); - conf.configure( - new RestartStrategy(new AllForOne(), 3, 10000, new Class[] { Exception.class }), - new Component[] { - new Component( - Foo.class, - new LifeCycle(new Permanent()), - 10000, - new RemoteAddress("localhost", 9999)) - }).supervise(); + ActiveObjectConfigurator conf = new ActiveObjectConfigurator(); + conf.configure( + new RestartStrategy(new AllForOne(), 3, 10000, new Class[] { Exception.class }), + new Component[] { + new Component( + Foo.class, + new LifeCycle(new Permanent()), + 10000, + new RemoteAddress("localhost", 9999)) + }).supervise(); - Foo instance = conf.getInstance(Foo.class); - assertEquals("foo", instance.foo()); - } + Foo instance = conf.getInstance(Foo.class); + assertEquals("foo", instance.foo()); + } - + } From 3ac7acccd7bc6f1411113e6acb58056eb5c68411 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Wed, 14 Apr 2010 09:07:20 +0800 Subject: [PATCH 13/41] tests for TransactionalRef --- .../src/test/scala/TransactionalRefSpec.scala | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 akka-core/src/test/scala/TransactionalRefSpec.scala diff --git a/akka-core/src/test/scala/TransactionalRefSpec.scala b/akka-core/src/test/scala/TransactionalRefSpec.scala new file mode 100644 index 0000000000..b4e9c0e67a --- /dev/null +++ b/akka-core/src/test/scala/TransactionalRefSpec.scala @@ -0,0 +1,68 @@ +package se.scalablesolutions.akka.stm + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith + +@RunWith(classOf[JUnitRunner]) +class TransactionalRefSpec extends Spec with ShouldMatchers { + + describe("A TransactionalRef") { + import Transaction.Local._ + + it("should optionally accept an initial value") { + val emptyRef = Ref[Int] + val empty = atomic { emptyRef.get } + + empty should be(None) + + val ref = Ref(3) + val value = atomic { ref.get.get } + + value should be(3) + } + + it("should be settable using swap") { + val ref = Ref[Int] + + atomic { ref.swap(3) } + + val value = atomic { ref.get.get } + + value should be(3) + } + + it("should be changeable using alter") { + val ref = Ref(0) + + def increment = atomic { + ref alter (_ + 1) + } + + increment + increment + increment + + val value = atomic { ref.get.get } + + value should be(3) + } + + it("should not be changeable using alter if no value has been set") { + val ref = Ref[Int] + + def increment = atomic { + ref alter (_ + 1) + } + + evaluating { increment } should produce [RuntimeException] + } + + it("should be able to be mapped") (pending) + it("should be able to be used in a 'foreach' for comprehension") (pending) + it("should be able to be used in a 'map' for comprehension") (pending) + it("should be able to be used in a 'flatMap' for comprehension") (pending) + it("should be able to be used in a 'filter' for comprehension") (pending) + } +} From 86ee7d8b0b079ac950676e94126827d51f813734 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Wed, 14 Apr 2010 09:27:45 +0800 Subject: [PATCH 14/41] tests for TransactionalRef in for comprehensions --- .../src/test/scala/TransactionalRefSpec.scala | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/akka-core/src/test/scala/TransactionalRefSpec.scala b/akka-core/src/test/scala/TransactionalRefSpec.scala index b4e9c0e67a..07c36ebdcf 100644 --- a/akka-core/src/test/scala/TransactionalRefSpec.scala +++ b/akka-core/src/test/scala/TransactionalRefSpec.scala @@ -59,10 +59,79 @@ class TransactionalRefSpec extends Spec with ShouldMatchers { evaluating { increment } should produce [RuntimeException] } - it("should be able to be mapped") (pending) - it("should be able to be used in a 'foreach' for comprehension") (pending) - it("should be able to be used in a 'map' for comprehension") (pending) - it("should be able to be used in a 'flatMap' for comprehension") (pending) - it("should be able to be used in a 'filter' for comprehension") (pending) + it("should be able to be mapped") { + val ref1 = Ref(1) + + val ref2 = atomic { + ref1 map (_ + 1) + } + + val value1 = atomic { ref1.get.get } + val value2 = atomic { ref2.get.get } + + value1 should be(1) + value2 should be(2) + } + + it("should be able to be used in a 'foreach' for comprehension") { + val ref = Ref(3) + + var result = 0 + + atomic { + for (value <- ref) { + result += value + } + } + + result should be(3) + } + + it("should be able to be used in a 'map' for comprehension") { + val ref1 = Ref(1) + + val ref2 = atomic { + for (value <- ref1) yield value + 2 + } + + val value2 = atomic { ref2.get.get } + + value2 should be(3) + } + + it("should be able to be used in a 'flatMap' for comprehension") { + val ref1 = Ref(1) + val ref2 = Ref(2) + + val ref3 = atomic { + for { + value1 <- ref1 + value2 <- ref2 + } yield value1 + value2 + } + + val value3 = atomic { ref3.get.get } + + value3 should be(3) + } + + it("should be able to be used in a 'filter' for comprehension") { + val ref1 = Ref(1) + + val refLess2 = atomic { + for (value <- ref1 if value < 2) yield value + } + + val optLess2 = atomic { refLess2.get } + + val refGreater2 = atomic { + for (value <- ref1 if value > 2) yield value + } + + val optGreater2 = atomic { refGreater2.get } + + optLess2 should be(Some(1)) + optGreater2 should be(None) + } } } From 5ab81358361cc656dcd987c716041a16752e9643 Mon Sep 17 00:00:00 2001 From: Peter Vlugter Date: Wed, 14 Apr 2010 09:08:50 +0800 Subject: [PATCH 15/41] updating TransactionalRef to be properly monadic --- .../main/scala/stm/TransactionalState.scala | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/akka-core/src/main/scala/stm/TransactionalState.scala b/akka-core/src/main/scala/stm/TransactionalState.scala index e84beaa4f0..9bf4859ee5 100644 --- a/akka-core/src/main/scala/stm/TransactionalState.scala +++ b/akka-core/src/main/scala/stm/TransactionalState.scala @@ -62,6 +62,8 @@ trait Committable { * @author Jonas Bonér */ object Ref { + type Ref[T] = TransactionalRef[T] + def apply[T]() = new Ref[T] def apply[T](initialValue: T) = new Ref[T](Some(initialValue)) @@ -75,7 +77,7 @@ object Ref { object TransactionalRef { /** - * An implicit conversion that converts an Option to an Iterable value. + * An implicit conversion that converts a TransactionalRef to an Iterable value. */ implicit def ref2Iterable[T](ref: TransactionalRef[T]): Iterable[T] = ref.toList @@ -84,14 +86,6 @@ object TransactionalRef { def apply[T](initialValue: T) = new TransactionalRef[T](Some(initialValue)) } -/** - * Implements a transactional managed reference. - * Alias to TransactionalRef. - * - * @author Jonas Bonér - */ -class Ref[T](initialOpt: Option[T] = None) extends TransactionalRef[T](initialOpt) - /** * Implements a transactional managed reference. * Alias to Ref. @@ -99,6 +93,8 @@ class Ref[T](initialOpt: Option[T] = None) extends TransactionalRef[T](initialOp * @author Jonas Bonér */ class TransactionalRef[T](initialOpt: Option[T] = None) extends Transactional { + self => + import org.multiverse.api.ThreadLocalTransaction._ implicit val txInitName = "TransactionalRef:Init" @@ -149,24 +145,36 @@ class TransactionalRef[T](initialOpt: Option[T] = None) extends Transactional { ref.isNull } - def map[B](f: T => B): Option[B] = { + def map[B](f: T => B): TransactionalRef[B] = { ensureIsInTransaction - if (isEmpty) None else Some(f(ref.get)) + if (isEmpty) TransactionalRef[B] else TransactionalRef(f(ref.get)) } - def flatMap[B](f: T => Option[B]): Option[B] = { + def flatMap[B](f: T => TransactionalRef[B]): TransactionalRef[B] = { ensureIsInTransaction - if (isEmpty) None else f(ref.get) + if (isEmpty) TransactionalRef[B] else f(ref.get) } - def filter(p: T => Boolean): Option[T] = { + def filter(p: T => Boolean): TransactionalRef[T] = { ensureIsInTransaction - if (isEmpty || p(ref.get)) Some(ref.get) else None + if (isDefined && p(ref.get)) TransactionalRef(ref.get) else TransactionalRef[T] } - def foreach(f: T => Unit) { + /** + * Necessary to keep from being implicitly converted to Iterable in for comprehensions. + */ + def withFilter(p: T => Boolean): WithFilter = new WithFilter(p) + + class WithFilter(p: T => Boolean) { + def map[B](f: T => B): TransactionalRef[B] = self filter p map f + def flatMap[B](f: T => TransactionalRef[B]): TransactionalRef[B] = self filter p flatMap f + def foreach[U](f: T => U): Unit = self filter p foreach f + def withFilter(q: T => Boolean): WithFilter = new WithFilter(x => p(x) && q(x)) + } + + def foreach[U](f: T => U): Unit = { ensureIsInTransaction - if (!isEmpty) f(ref.get) + if (isDefined) f(ref.get) } def elements: Iterator[T] = { From a596fbbd61a3db1606d335cb4a48421a79ebf607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Fri, 16 Apr 2010 09:23:24 +0200 Subject: [PATCH 16/41] Added Cassandra logging dependencies to the compile jars --- project/build/AkkaProject.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index b0466d5ec8..960d380a05 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -23,7 +23,7 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { lazy val deployPath = info.projectPath / "deploy" lazy val distPath = info.projectPath / "dist" - override def compileOptions = super.compileOptions ++ + override def compileOptions = super.compileOptions ++ Seq("-deprecation", "-Xmigration", "-Xcheckinit", "-Xstrict-warnings", "-Xwarninit", "-encoding", "utf8").map(x => CompileOption(x)) override def javaCompileOptions = JavaCompileOption("-Xlint:unchecked") :: super.javaCompileOptions.toList @@ -236,13 +236,14 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) { class AkkaCassandraProject(info: ProjectInfo) extends AkkaDefaultProject(info, distPath) { val cassandra = "org.apache.cassandra" % "cassandra" % CASSANDRA_VERSION % "compile" + val slf4j = "org.slf4j" % "slf4j-api" % "1.5.8" % "compile" + val slf4j_log4j = "org.slf4j" % "slf4j-log4j12" % "1.5.8" % "compile" + val log4j = "log4j" % "log4j" % "1.2.15" % "compile" + // testing val high_scale = "org.apache.cassandra" % "high-scale-lib" % CASSANDRA_VERSION % "test" val cassandra_clhm = "org.apache.cassandra" % "clhm-production" % CASSANDRA_VERSION % "test" val commons_coll = "commons-collections" % "commons-collections" % "3.2.1" % "test" val google_coll = "com.google.collections" % "google-collections" % "1.0" % "test" - val slf4j = "org.slf4j" % "slf4j-api" % "1.5.8" % "test" - val slf4j_log4j = "org.slf4j" % "slf4j-log4j12" % "1.5.8" % "test" - val log4j = "log4j" % "log4j" % "1.2.15" % "test" override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil } From 584de097ec95bb2eadad815e764071c64deaf55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Fri, 16 Apr 2010 14:36:51 +0200 Subject: [PATCH 17/41] added sbt plugin file --- project/plugins/Plugins.scala | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 project/plugins/Plugins.scala diff --git a/project/plugins/Plugins.scala b/project/plugins/Plugins.scala new file mode 100644 index 0000000000..a929827cc6 --- /dev/null +++ b/project/plugins/Plugins.scala @@ -0,0 +1,5 @@ +import sbt._ + +class Plugins(info: ProjectInfo) extends PluginDefinition(info) { +// val surefire = "bryanjswift" % "sbt-surefire-reporting" % "0.0.3-SNAPSHOT" +} \ No newline at end of file From dcaa743f8085d914c6445727b219852343cbd17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Sat, 17 Apr 2010 18:24:41 +0200 Subject: [PATCH 18/41] added TransactionManagerDetector --- akka-core/src/main/scala/config/Config.scala | 2 +- .../scala/AtomikosTransactionService.scala | 5 ++- .../src/main/scala/TransactionContext.scala | 28 +++++++++--- .../scala/TransactionManagerDetector.scala | 44 +++++++++++++++++++ config/akka-reference.conf | 8 +++- 5 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 akka-jta/src/main/scala/TransactionManagerDetector.scala diff --git a/akka-core/src/main/scala/config/Config.scala b/akka-core/src/main/scala/config/Config.scala index 68cd75d825..f9a1035a7c 100644 --- a/akka-core/src/main/scala/config/Config.scala +++ b/akka-core/src/main/scala/config/Config.scala @@ -12,7 +12,7 @@ import net.lag.configgy.{Configgy, ParseException} * @author Jonas Bonér */ object Config extends Logging { - val VERSION = "0.8.1" + val VERSION = "0.9" // Set Multiverse options for max speed System.setProperty("org.multiverse.MuliverseConstants.sanityChecks", "false") diff --git a/akka-jta/src/main/scala/AtomikosTransactionService.scala b/akka-jta/src/main/scala/AtomikosTransactionService.scala index d688564686..1daa08a204 100644 --- a/akka-jta/src/main/scala/AtomikosTransactionService.scala +++ b/akka-jta/src/main/scala/AtomikosTransactionService.scala @@ -9,6 +9,8 @@ import javax.transaction.{TransactionManager, SystemException} import com.atomikos.icatch.jta.{J2eeTransactionManager, J2eeUserTransaction} import com.atomikos.icatch.config.{TSInitInfo, UserTransactionService, UserTransactionServiceImp} +import se.scalablesolutions.akka.config.Config._ + /** * Atomikos implementation of the transaction service trait. * @@ -16,8 +18,7 @@ import com.atomikos.icatch.config.{TSInitInfo, UserTransactionService, UserTrans */ object AtomikosTransactionService extends TransactionService with TransactionProtocol { - // FIXME: make configurable - val JTA_TRANSACTION_TIMEOUT = 60 + val JTA_TRANSACTION_TIMEOUT = config.getInt("akka.jta.timeout", 60) private val txService: UserTransactionService = new UserTransactionServiceImp private val info: TSInitInfo = txService.createTSInitInfo diff --git a/akka-jta/src/main/scala/TransactionContext.scala b/akka-jta/src/main/scala/TransactionContext.scala index 3ed562eeb5..218bd93828 100644 --- a/akka-jta/src/main/scala/TransactionContext.scala +++ b/akka-jta/src/main/scala/TransactionContext.scala @@ -7,6 +7,7 @@ package se.scalablesolutions.akka.jta import javax.transaction.{Transaction, Status, TransactionManager} import se.scalablesolutions.akka.util.Logging +import se.scalablesolutions.akka.config.Config._ /** * JTA Transaction service. @@ -65,13 +66,25 @@ trait TransactionMonad { } /** + * The TransactionContext object manages the transactions. + * Can be used as higher-order functional 'atomic blocks' or monadic. + * * Manages a thread-local stack of TransactionContexts. *

- * Choose TransactionService implementation by implicit definition of the implementation of choice, - * e.g. implicit val txService = TransactionServices.AtomikosTransactionService. - *

* Example usage 1: *

+ * import TransactionContext._
+ * 
+ * withTxRequired {
+ *   ... // transactional stuff
+ * }
+ * // or
+ * withTxRequiresNew {
+ *   ... // transactional stuff
+ * }
+ * 
+ * Example usage 2: + *
  * for {
  *   ctx <- TransactionContext.Required
  *   entity <- updatedEntities
@@ -81,7 +94,7 @@ trait TransactionMonad {
  *   ...
  * }
  * 
- * Example usage 2: + * Example usage 3: *
  * val users = for {
  *   ctx <- TransactionContext.Required
@@ -95,9 +108,12 @@ trait TransactionMonad {
  * @author Jonas Bonér
  */
 object TransactionContext extends TransactionProtocol with Logging {
-  // FIXME: make configurable
-  private implicit val defaultTransactionService = AtomikosTransactionService
+  val TRANSACTION_PROVIDER = config.getString("akka.jta.transaction-provider", "atomikos")
 
+  private implicit val defaultTransactionService = TRANSACTION_PROVIDER match {
+    case "atomikos" => AtomikosTransactionService
+    case _ => throw new IllegalArgumentException("Transaction provider [" + TRANSACTION_PROVIDER + "] is not supported")
+  }
   private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext)
 
   object Required extends TransactionMonad {
diff --git a/akka-jta/src/main/scala/TransactionManagerDetector.scala b/akka-jta/src/main/scala/TransactionManagerDetector.scala
new file mode 100644
index 0000000000..f86d513ab3
--- /dev/null
+++ b/akka-jta/src/main/scala/TransactionManagerDetector.scala
@@ -0,0 +1,44 @@
+/**
+ * Copyright (C) 2009-2010 Scalable Solutions AB 
+ */
+
+package se.scalablesolutions.akka.jta
+
+import javax.transaction.{TransactionManager, UserTransaction, SystemException}
+import javax.naming.{InitialContext, Context, NamingException}
+
+import se.scalablesolutions.akka.config.Config._
+
+/**
+ * @author Jonas Bonér
+ */
+object TransactionManagerDetector {
+  val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction"
+  val FALLBACK_TRANSACTION_MANAGER_NAMES = List("java:comp/TransactionManager", 
+                                                "java:appserver/TransactionManager",
+                                                "java:pm/TransactionManager", 
+                                                "java:/TransactionManager")
+  val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry"
+  val TRANSACTION_SYNCHRONIZATION_REGISTRY_CLASS_NAME = "javax.transaction.TransactionSynchronizationRegistry"
+
+  def findUserTransaction: Option[UserTransaction] = {
+    val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME)
+    if (located eq null) None
+    else Some(located.asInstanceOf[UserTransaction])
+  }
+
+  def findTransactionManager: Option[TransactionManager] = {
+    val context = createInitialContext
+    val tms = for {
+      name <- FALLBACK_TRANSACTION_MANAGER_NAMES
+      tm = context.lookup(name)
+      if tm ne null
+    } yield tm
+    tms match {
+      case Nil => None
+      case tm :: _ => Some(tm.asInstanceOf[TransactionManager])
+    }
+  }
+
+  private def createInitialContext = new InitialContext(new java.util.Hashtable)
+}
diff --git a/config/akka-reference.conf b/config/akka-reference.conf
index 34cc9eaec8..9d9fc30cbb 100644
--- a/config/akka-reference.conf
+++ b/config/akka-reference.conf
@@ -15,7 +15,7 @@
 
  
 
-  version = "0.8.1"
+  version = "0.9"
  
   # FQN to the class doing initial active object/actor
   # supervisor bootstrap, should be defined in default constructor
@@ -34,8 +34,12 @@
     fair = on                     # should transactions be fair or non-fair (non fair yield better performance)
     max-nr-of-retries = 1000      # max nr of retries of a failing transaction before giving up
     timeout = 10000               # transaction timeout; if transaction has not committed within the timeout then it is aborted
-    distributed = off             # not implemented yet
   
+  
+  
+    service = off  # 'on' means that if there is a running JTA transaction then the STM will participate in it. Default is 'off'
+    timeout = 60   # timeout in seconds
+  
  
   
     service = on

From 3ce5ef8abcd22e14f23b082ce9745f1988636fe2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= 
Date: Sat, 17 Apr 2010 18:35:34 +0200
Subject: [PATCH 19/41] upgraded to 0.9

---
 .gitignore               | 1 +
 project/build.properties | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 69dd6d55c9..b3b2c2ebc7 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 *~
 *#
+project/plugins/project/
 project/boot/*
 */project/build/target
 */project/boot
diff --git a/project/build.properties b/project/build.properties
index 95d001115b..b52f2291c7 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1,6 +1,6 @@
 project.organization=se.scalablesolutions.akka
 project.name=akka
-project.version=0.8.1
+project.version=0.9
 scala.version=2.8.0.Beta1
 sbt.version=0.7.3
 def.scala.version=2.7.7

From d5da87986a1617c1d6490826a64f09359a7e8f91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= 
Date: Sat, 17 Apr 2010 19:28:14 +0200
Subject: [PATCH 20/41] jta-enabled stm

---
 .../stm/JtaTransactionManagerDetector.scala   |  4 +--
 .../src/main/scala/stm/Transaction.scala      | 36 +++++++++++++++++--
 config/akka-reference.conf                    |  1 +
 project/build/AkkaProject.scala               |  5 +--
 4 files changed, 40 insertions(+), 6 deletions(-)
 rename akka-jta/src/main/scala/TransactionManagerDetector.scala => akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala (95%)

diff --git a/akka-jta/src/main/scala/TransactionManagerDetector.scala b/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
similarity index 95%
rename from akka-jta/src/main/scala/TransactionManagerDetector.scala
rename to akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
index f86d513ab3..30e9d3a33c 100644
--- a/akka-jta/src/main/scala/TransactionManagerDetector.scala
+++ b/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
@@ -2,7 +2,7 @@
  * Copyright (C) 2009-2010 Scalable Solutions AB 
  */
 
-package se.scalablesolutions.akka.jta
+package se.scalablesolutions.akka.stm
 
 import javax.transaction.{TransactionManager, UserTransaction, SystemException}
 import javax.naming.{InitialContext, Context, NamingException}
@@ -12,7 +12,7 @@ import se.scalablesolutions.akka.config.Config._
 /**
  * @author Jonas Bonér
  */
-object TransactionManagerDetector {
+object JtaTransactionManagerDetector {
   val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction"
   val FALLBACK_TRANSACTION_MANAGER_NAMES = List("java:comp/TransactionManager", 
                                                 "java:appserver/TransactionManager",
diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala
index 209c131781..44bb545a69 100644
--- a/akka-core/src/main/scala/stm/Transaction.scala
+++ b/akka-core/src/main/scala/stm/Transaction.scala
@@ -8,9 +8,12 @@ import java.util.concurrent.atomic.AtomicLong
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.concurrent.TimeUnit
 
+import javax.transaction.{TransactionManager, UserTransaction, Status}
+
 import scala.collection.mutable.HashMap
 
 import se.scalablesolutions.akka.util.Logging
+import se.scalablesolutions.akka.config.Config._
 
 import org.multiverse.api.{Transaction => MultiverseTransaction, TransactionLifecycleListener, TransactionLifecycleEvent}
 import org.multiverse.api.GlobalStmInstance.getGlobalStmInstance
@@ -272,9 +275,9 @@ object Transaction {
               createNewTransactionSet
             } else getTransactionSetInScope
           val tx = new Transaction
+          tx.begin
           tx.transaction = Some(mtx)
           setTransaction(Some(tx))
-
           txSet.registerOnCommitTask(new Runnable() {
             def run = tx.commit
           })
@@ -288,11 +291,20 @@ object Transaction {
 }
 
 /**
- * The Akka specific Transaction class, keeping track of persistent data structures (as in on-disc).
+ * The Akka specific Transaction class, keeping track of persistent data structures (as in on-disc)
+ * and JTA support.
  *
  * @author Jonas Bonér
  */
 @serializable class Transaction extends Logging {
+  val JTA_AWARE = config.getBool("akka.stm.jta-aware", false)
+  val jta: Either[Option[UserTransaction], Option[TransactionManager]] = if (JTA_AWARE) {
+    JtaTransactionManagerDetector.findUserTransaction match {
+      case None => Right(JtaTransactionManagerDetector.findTransactionManager)
+      case tm =>   Left(tm)
+    }
+  } else Left(None)
+  
   val id = Transaction.idFactory.incrementAndGet
   @volatile private[this] var status: TransactionStatus = TransactionStatus.New
   private[akka] var transaction: Option[MultiverseTransaction] = None
@@ -303,16 +315,34 @@ object Transaction {
 
   // --- public methods ---------
 
+  def begin = synchronized {
+    jta match {
+      case Left(Some(userTx)) => if (!isJtaTxActive(userTx.getStatus)) userTx.begin
+      case Right(Some(txMan)) => if (!isJtaTxActive(txMan.getStatus)) txMan.begin
+      case _ => {} // do nothing
+    }
+  }
+  
   def commit = synchronized {
     log.trace("Committing transaction %s", toString)
     Transaction.atomic0 {
       persistentStateMap.valuesIterator.foreach(_.commit)
     }
     status = TransactionStatus.Completed
+    jta match {
+      case Left(Some(userTx)) => if (isJtaTxActive(userTx.getStatus)) userTx.commit
+      case Right(Some(txMan)) => if (isJtaTxActive(txMan.getStatus)) txMan.commit
+      case _ => {} // do nothing
+    }
   }
 
   def abort = synchronized {
     log.trace("Aborting transaction %s", toString)
+    jta match {
+      case Left(Some(userTx)) => if (isJtaTxActive(userTx.getStatus)) userTx.rollback
+      case Right(Some(txMan)) => if (isJtaTxActive(txMan.getStatus)) txMan.rollback
+      case _ => {} // do nothing
+    }
   }
 
   def isNew = synchronized { status == TransactionStatus.New }
@@ -325,6 +355,8 @@ object Transaction {
 
   // --- internal methods ---------
 
+  private def isJtaTxActive(status: Int) = status == Status.STATUS_ACTIVE
+  
   private[akka] def status_? = status
 
   private[akka] def increment = depth.incrementAndGet
diff --git a/config/akka-reference.conf b/config/akka-reference.conf
index a8184d2662..8e4be0bab4 100644
--- a/config/akka-reference.conf
+++ b/config/akka-reference.conf
@@ -34,6 +34,7 @@
     fair = on                     # should transactions be fair or non-fair (non fair yield better performance)
     max-nr-of-retries = 1000      # max nr of retries of a failing transaction before giving up
     timeout = 10000               # transaction timeout; if transaction has not committed within the timeout then it is aborted
+    jta-aware = off               # 'on' means that if there JTA Transaction Manager available then the STM will begin (or join), commit or rollback the JTA transaction. Default is 'off'
   
   
   
diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala
index a998d07b90..cf1e9c3e68 100644
--- a/project/build/AkkaProject.scala
+++ b/project/build/AkkaProject.scala
@@ -151,13 +151,14 @@ class AkkaParent(info: ProjectInfo) extends DefaultProject(info) {
     val netty = "org.jboss.netty" % "netty" % "3.2.0.BETA1" % "compile"
     val commons_io = "commons-io" % "commons-io" % "1.4" % "compile"
     val dispatch_json = "net.databinder" % "dispatch-json_2.8.0.Beta1" % "0.6.6" % "compile"
-    val dispatch_htdisttp = "net.databinder" % "dispatch-http_2.8.0.Beta1" % "0.6.6" % "compile"
+    val dispatch_http = "net.databinder" % "dispatch-http_2.8.0.Beta1" % "0.6.6" % "compile"
     val sjson = "sjson.json" % "sjson" % "0.5-SNAPSHOT-2.8.Beta1" % "compile"
     val sbinary = "sbinary" % "sbinary" % "2.8.0.Beta1-2.8.0.Beta1-0.3.1-SNAPSHOT" % "compile"
     val jackson = "org.codehaus.jackson" % "jackson-mapper-asl" % "1.2.1" % "compile"
     val jackson_core = "org.codehaus.jackson" % "jackson-core-asl" % "1.2.1" % "compile"
-    val voldemort = "voldemort.store.compress" % "h2-lzf" % "1.0" % "compile"
+    val h2_lzf = "voldemort.store.compress" % "h2-lzf" % "1.0" % "compile"
     val jsr166x = "jsr166x" % "jsr166x" % "1.0" % "compile"
+    val jta_1_1 = "org.apache.geronimo.specs" % "geronimo-jta_1.1_spec" % "1.1.1" % "compile"
     // testing
     val scalatest = "org.scalatest" % "scalatest" % SCALATEST_VERSION % "test"
     val junit = "junit" % "junit" % "4.5" % "test"

From eb95fd8e45106d87b494430303bb332c265a144e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= 
Date: Sat, 17 Apr 2010 19:48:52 +0200
Subject: [PATCH 21/41] added logging to jta detection

---
 .../scala/stm/JtaTransactionManagerDetector.scala    | 12 +++++++++---
 akka-core/src/main/scala/stm/Transaction.scala       |  5 +++--
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala b/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
index 30e9d3a33c..fb78bf922a 100644
--- a/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
+++ b/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
@@ -8,11 +8,12 @@ import javax.transaction.{TransactionManager, UserTransaction, SystemException}
 import javax.naming.{InitialContext, Context, NamingException}
 
 import se.scalablesolutions.akka.config.Config._
+import se.scalablesolutions.akka.util.Logging
 
 /**
  * @author Jonas Bonér
  */
-object JtaTransactionManagerDetector {
+object JtaTransactionManagerDetector extends Logging {
   val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction"
   val FALLBACK_TRANSACTION_MANAGER_NAMES = List("java:comp/TransactionManager", 
                                                 "java:appserver/TransactionManager",
@@ -24,7 +25,10 @@ object JtaTransactionManagerDetector {
   def findUserTransaction: Option[UserTransaction] = {
     val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME)
     if (located eq null) None
-    else Some(located.asInstanceOf[UserTransaction])
+    else {
+      log.info("JTA UserTransaction detected [%s]", located)
+      Some(located.asInstanceOf[UserTransaction])
+    }
   }
 
   def findTransactionManager: Option[TransactionManager] = {
@@ -36,7 +40,9 @@ object JtaTransactionManagerDetector {
     } yield tm
     tms match {
       case Nil => None
-      case tm :: _ => Some(tm.asInstanceOf[TransactionManager])
+      case tm :: _ =>
+        log.info("JTA TransactionManager detected [%s]", tm)
+        Some(tm.asInstanceOf[TransactionManager])
     }
   }
 
diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala
index 44bb545a69..d670060a45 100644
--- a/akka-core/src/main/scala/stm/Transaction.scala
+++ b/akka-core/src/main/scala/stm/Transaction.scala
@@ -297,10 +297,11 @@ object Transaction {
  * @author Jonas Bonér
  */
 @serializable class Transaction extends Logging {
+  import JtaTransactionManagerDetector._
   val JTA_AWARE = config.getBool("akka.stm.jta-aware", false)
   val jta: Either[Option[UserTransaction], Option[TransactionManager]] = if (JTA_AWARE) {
-    JtaTransactionManagerDetector.findUserTransaction match {
-      case None => Right(JtaTransactionManagerDetector.findTransactionManager)
+    findUserTransaction match {
+      case None => Right(findTransactionManager)
       case tm =>   Left(tm)
     }
   } else Left(None)

From 75c1cf50fc1bdc2155ea931ce83208a63d21ae34 Mon Sep 17 00:00:00 2001
From: Viktor Klang 
Date: Mon, 19 Apr 2010 23:49:39 +0200
Subject: [PATCH 22/41] Removed jndi.properties

---
 akka-kernel/src/main/resources/jndi.properties | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 akka-kernel/src/main/resources/jndi.properties

diff --git a/akka-kernel/src/main/resources/jndi.properties b/akka-kernel/src/main/resources/jndi.properties
deleted file mode 100644
index 3485823a1e..0000000000
--- a/akka-kernel/src/main/resources/jndi.properties
+++ /dev/null
@@ -1 +0,0 @@
-java.naming.factory.initial=com.sun.enterprise.naming.SerialInitContextFactory
\ No newline at end of file

From 4a01336153926d9268b88dc9aa68f887fba11477 Mon Sep 17 00:00:00 2001
From: Peter Vlugter 
Date: Tue, 20 Apr 2010 11:07:16 +1200
Subject: [PATCH 23/41] added Dean's test for Vector bug (blowing up after 32
 items)

---
 .../src/test/scala/VectorBugTestSuite.scala     | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
 create mode 100644 akka-core/src/test/scala/VectorBugTestSuite.scala

diff --git a/akka-core/src/test/scala/VectorBugTestSuite.scala b/akka-core/src/test/scala/VectorBugTestSuite.scala
new file mode 100644
index 0000000000..658ace3681
--- /dev/null
+++ b/akka-core/src/test/scala/VectorBugTestSuite.scala
@@ -0,0 +1,17 @@
+package se.scalablesolutions.akka.stm
+
+import org.scalatest.FunSuite
+import Transaction.Global._
+
+class TransactionalVectorBugTestSuite extends FunSuite {
+
+  test("adding more than 32 items to a Vector shouldn't blow it up") {
+    atomic {
+      var v1 = new Vector[Int]()
+      for (i <- 0 to 31) {
+        v1 = v1 + i
+      }
+      v1 = v1 + 32
+    }
+  }
+}

From ee21af32cea14ca51f83632cea7b938a514dbead Mon Sep 17 00:00:00 2001
From: Peter Vlugter 
Date: Tue, 20 Apr 2010 11:07:59 +1200
Subject: [PATCH 24/41] fix for Vector from Dean (ticket #155)

---
 akka-core/src/main/scala/stm/Vector.scala | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/akka-core/src/main/scala/stm/Vector.scala b/akka-core/src/main/scala/stm/Vector.scala
index b76281b909..a526906115 100644
--- a/akka-core/src/main/scala/stm/Vector.scala
+++ b/akka-core/src/main/scala/stm/Vector.scala
@@ -326,7 +326,7 @@ object Vector {
   @inline
   private[stm] def array(elems: AnyRef*) = {
     val back = new Array[AnyRef](elems.length)
-    Array.copy(elems, 0, back, 0, back.length)
+    Array.copy(elems.toArray, 0, back, 0, back.length)
 
     back
   }

From fc23d02d5ed649f36f38ada5642f7e9f6e3aa8a7 Mon Sep 17 00:00:00 2001
From: Michael Kober 
Date: Tue, 20 Apr 2010 10:54:10 +0200
Subject: [PATCH 25/41] fixed #154 added ActiveObjectConfiguration with fluent
 API

---
 .../src/main/scala/actor/ActiveObject.scala   | 74 ++++++++++++++++---
 .../actor/ActiveObjectConfiguration.scala     | 39 ++++++++++
 2 files changed, 104 insertions(+), 9 deletions(-)
 create mode 100644 akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala

diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala
index 8cace19031..2446472e0e 100644
--- a/akka-core/src/main/scala/actor/ActiveObject.scala
+++ b/akka-core/src/main/scala/actor/ActiveObject.scala
@@ -41,141 +41,191 @@ object ActiveObject {
   def newInstance[T](target: Class[T], timeout: Long): T =
     newInstance(target, new Dispatcher(false, None), None, timeout)
 
+  def newInstance[T](target: Class[T]): T =
+    newInstance(target, new Dispatcher(false, None), None, Actor.TIMEOUT)
+
+  def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long): T =
+      newInstance(intf, target, new Dispatcher(false, None), None, timeout)
+
+  def newInstance[T](intf: Class[T], target: AnyRef): T =
+      newInstance(intf, target, new Dispatcher(false, None), None, Actor.TIMEOUT)
+
+  def newRemoteInstance[T](target: Class[T], timeout: Long, hostname: String, port: Int): T =
+      newInstance(target, new Dispatcher(false, None), Some(new InetSocketAddress(hostname, port)), timeout)
+
+  def newRemoteInstance[T](target: Class[T], hostname: String, port: Int): T =
+        newInstance(target, new Dispatcher(false, None), Some(new InetSocketAddress(hostname, port)), Actor.TIMEOUT)
+
+  def newInstance[T](target: Class[T], config: ActiveObjectConfiguration): T = {
+    val actor = new Dispatcher(config._transactionRequired, config._restartCallbacks)
+     if (config._messageDispatcher.isDefined) {
+       actor.messageDispatcher = config._messageDispatcher.get
+     }
+     newInstance(target, actor, config._host, config._timeout)
+  }
+
+  def newInstance[T](intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration): T = {
+    val actor = new Dispatcher(config._transactionRequired, config._restartCallbacks)
+     if (config._messageDispatcher.isDefined) {
+       actor.messageDispatcher = config._messageDispatcher.get
+     }
+     newInstance(intf, target, actor, config._host, config._timeout)
+  }
+
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(target, new Dispatcher(false, restartCallbacks), None, timeout)
 
-  def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long): T =
-    newInstance(intf, target, new Dispatcher(false, None), None, timeout)
-
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(intf, target, new Dispatcher(false, restartCallbacks), None, timeout)
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean): T =
     newInstance(target, new Dispatcher(transactionRequired, None), None, timeout)
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(target, new Dispatcher(transactionRequired, restartCallbacks), None, timeout)
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean): T =
     newInstance(intf, target, new Dispatcher(transactionRequired, None), None, timeout)
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(intf, target, new Dispatcher(transactionRequired, restartCallbacks), None, timeout)
 
-  def newRemoteInstance[T](target: Class[T], timeout: Long, hostname: String, port: Int): T =
-    newInstance(target, new Dispatcher(false, None), Some(new InetSocketAddress(hostname, port)), timeout)
-
-  def newRemoteInstance[T](target: Class[T], timeout: Long, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T =
-    newInstance(target, new Dispatcher(false, restartCallbacks), Some(new InetSocketAddress(hostname, port)), timeout)
-
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, hostname: String, port: Int): T =
     newInstance(intf, target, new Dispatcher(false, None), Some(new InetSocketAddress(hostname, port)), timeout)
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(intf, target, new Dispatcher(false, restartCallbacks), Some(new InetSocketAddress(hostname, port)), timeout)
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, hostname: String, port: Int): T =
     newInstance(target, new Dispatcher(transactionRequired, None), Some(new InetSocketAddress(hostname, port)), timeout)
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(target, new Dispatcher(transactionRequired, restartCallbacks), Some(new InetSocketAddress(hostname, port)), timeout)
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, hostname: String, port: Int): T =
     newInstance(intf, target, new Dispatcher(transactionRequired, None), Some(new InetSocketAddress(hostname, port)), timeout)
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T =
     newInstance(intf, target, new Dispatcher(transactionRequired, restartCallbacks), Some(new InetSocketAddress(hostname, port)), timeout)
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, dispatcher: MessageDispatcher): T = {
     val actor = new Dispatcher(false, None)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, dispatcher: MessageDispatcher, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(false, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, dispatcher: MessageDispatcher): T = {
     val actor = new Dispatcher(false, None)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, dispatcher: MessageDispatcher, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(false, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher): T = {
     val actor = new Dispatcher(transactionRequired, None)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(transactionRequired, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher): T = {
     val actor = new Dispatcher(transactionRequired, None)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(transactionRequired, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, None, timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](target: Class[T], timeout: Long, dispatcher: MessageDispatcher, hostname: String, port: Int): T = {
     val actor = new Dispatcher(false, None)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](target: Class[T], timeout: Long, dispatcher: MessageDispatcher, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(false, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, dispatcher: MessageDispatcher, hostname: String, port: Int): T = {
     val actor = new Dispatcher(false, None)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, dispatcher: MessageDispatcher, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(false, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher, hostname: String, port: Int): T = {
     val actor = new Dispatcher(transactionRequired, None)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(target: Class[T], config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](target: Class[T], timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(transactionRequired, restartCallbacks)
     actor.messageDispatcher = dispatcher
     newInstance(target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher, hostname: String, port: Int): T = {
     val actor = new Dispatcher(transactionRequired, None)
     actor.messageDispatcher = dispatcher
     newInstance(intf, target, actor, Some(new InetSocketAddress(hostname, port)), timeout)
   }
 
+  @deprecated("use newInstance(intf: Class[T], target: AnyRef, config: ActiveObjectConfiguration) instead")
   def newRemoteInstance[T](intf: Class[T], target: AnyRef, timeout: Long, transactionRequired: Boolean, dispatcher: MessageDispatcher, hostname: String, port: Int, restartCallbacks: Option[RestartCallbacks]): T = {
     val actor = new Dispatcher(transactionRequired, restartCallbacks)
     actor.messageDispatcher = dispatcher
@@ -186,6 +236,9 @@ object ActiveObject {
     val proxy = Proxy.newInstance(target, false, true)
     actor.initialize(target, proxy)
     actor.timeout = timeout
+    if (remoteAddress.isDefined) {
+      actor.makeRemote(remoteAddress.get)
+    }
     AspectInitRegistry.register(proxy, AspectInit(target, actor, remoteAddress, timeout))
     actor.start
     proxy.asInstanceOf[T]
@@ -195,6 +248,9 @@ object ActiveObject {
     val proxy = Proxy.newInstance(Array(intf), Array(target), false, true)
     actor.initialize(target.getClass, target)
     actor.timeout = timeout
+    if (remoteAddress.isDefined) {
+      actor.makeRemote(remoteAddress.get)
+    }
     AspectInitRegistry.register(proxy, AspectInit(intf, actor, remoteAddress, timeout))
     actor.start
     proxy.asInstanceOf[T]
diff --git a/akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala b/akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala
new file mode 100644
index 0000000000..8390d2bdbb
--- /dev/null
+++ b/akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala
@@ -0,0 +1,39 @@
+package se.scalablesolutions.akka.actor
+
+import _root_.java.net.InetSocketAddress
+import _root_.se.scalablesolutions.akka.config.ScalaConfig.RestartCallbacks
+import _root_.se.scalablesolutions.akka.dispatch.MessageDispatcher
+
+
+final class ActiveObjectConfiguration {
+  private[akka] var _timeout: Long = Actor.TIMEOUT
+  private[akka] var _restartCallbacks: Option[RestartCallbacks] = None
+  private[akka] var _transactionRequired = false
+  private[akka] var _host: Option[InetSocketAddress] = None
+  private[akka] var _messageDispatcher: Option[MessageDispatcher] = None
+
+  def timeout(timeout: Long) : ActiveObjectConfiguration = {
+    _timeout = timeout
+    this
+  }
+
+  def restartCallbacks(pre: String, post: String) : ActiveObjectConfiguration = {
+    _restartCallbacks = Some(new RestartCallbacks(pre, post))
+    this
+  }
+
+  def makeTransactionRequired() : ActiveObjectConfiguration = {
+    _transactionRequired = true;
+    this
+  }
+
+  def makeRemote(hostname: String, port: Int) : ActiveObjectConfiguration = {
+    _host = Some(new InetSocketAddress(hostname, port))
+    this
+  }
+
+  def dispatcher(messageDispatcher: MessageDispatcher) : ActiveObjectConfiguration = {
+    _messageDispatcher = Some(messageDispatcher)
+    this
+  }
+}
\ No newline at end of file

From 98d5c4adb186291caa936ff5bcd89e22cf01a203 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= 
Date: Tue, 20 Apr 2010 11:44:26 +0200
Subject: [PATCH 26/41] Finalized the JTA support

---
 .../stm/JtaTransactionManagerDetector.scala   |  50 ------
 .../src/main/scala/stm/Transaction.scala      |  33 +---
 .../main/scala/stm/TransactionContainer.scala | 160 ++++++++++++++++++
 .../scala/AtomikosTransactionService.scala    |  11 +-
 .../src/main/scala/TransactionContext.scala   |  45 ++---
 .../src/main/scala/TransactionProtocol.scala  |  21 +--
 config/akka-reference.conf                    |  17 +-
 7 files changed, 208 insertions(+), 129 deletions(-)
 delete mode 100644 akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
 create mode 100644 akka-core/src/main/scala/stm/TransactionContainer.scala

diff --git a/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala b/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
deleted file mode 100644
index fb78bf922a..0000000000
--- a/akka-core/src/main/scala/stm/JtaTransactionManagerDetector.scala
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (C) 2009-2010 Scalable Solutions AB 
- */
-
-package se.scalablesolutions.akka.stm
-
-import javax.transaction.{TransactionManager, UserTransaction, SystemException}
-import javax.naming.{InitialContext, Context, NamingException}
-
-import se.scalablesolutions.akka.config.Config._
-import se.scalablesolutions.akka.util.Logging
-
-/**
- * @author Jonas Bonér
- */
-object JtaTransactionManagerDetector extends Logging {
-  val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction"
-  val FALLBACK_TRANSACTION_MANAGER_NAMES = List("java:comp/TransactionManager", 
-                                                "java:appserver/TransactionManager",
-                                                "java:pm/TransactionManager", 
-                                                "java:/TransactionManager")
-  val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry"
-  val TRANSACTION_SYNCHRONIZATION_REGISTRY_CLASS_NAME = "javax.transaction.TransactionSynchronizationRegistry"
-
-  def findUserTransaction: Option[UserTransaction] = {
-    val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME)
-    if (located eq null) None
-    else {
-      log.info("JTA UserTransaction detected [%s]", located)
-      Some(located.asInstanceOf[UserTransaction])
-    }
-  }
-
-  def findTransactionManager: Option[TransactionManager] = {
-    val context = createInitialContext
-    val tms = for {
-      name <- FALLBACK_TRANSACTION_MANAGER_NAMES
-      tm = context.lookup(name)
-      if tm ne null
-    } yield tm
-    tms match {
-      case Nil => None
-      case tm :: _ =>
-        log.info("JTA TransactionManager detected [%s]", tm)
-        Some(tm.asInstanceOf[TransactionManager])
-    }
-  }
-
-  private def createInitialContext = new InitialContext(new java.util.Hashtable)
-}
diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala
index d670060a45..7fbe9898e6 100644
--- a/akka-core/src/main/scala/stm/Transaction.scala
+++ b/akka-core/src/main/scala/stm/Transaction.scala
@@ -297,31 +297,24 @@ object Transaction {
  * @author Jonas Bonér
  */
 @serializable class Transaction extends Logging {
-  import JtaTransactionManagerDetector._
   val JTA_AWARE = config.getBool("akka.stm.jta-aware", false)
-  val jta: Either[Option[UserTransaction], Option[TransactionManager]] = if (JTA_AWARE) {
-    findUserTransaction match {
-      case None => Right(findTransactionManager)
-      case tm =>   Left(tm)
-    }
-  } else Left(None)
-  
+
   val id = Transaction.idFactory.incrementAndGet
   @volatile private[this] var status: TransactionStatus = TransactionStatus.New
   private[akka] var transaction: Option[MultiverseTransaction] = None
   private[this] val persistentStateMap = new HashMap[String, Committable]
   private[akka] val depth = new AtomicInteger(0)
-
+  
+  val tc: Option[TransactionContainer] =
+    if (JTA_AWARE) Some(TransactionContainer())
+    else None
+  
   log.trace("Creating %s", toString)
 
   // --- public methods ---------
 
   def begin = synchronized {
-    jta match {
-      case Left(Some(userTx)) => if (!isJtaTxActive(userTx.getStatus)) userTx.begin
-      case Right(Some(txMan)) => if (!isJtaTxActive(txMan.getStatus)) txMan.begin
-      case _ => {} // do nothing
-    }
+    tc.foreach(_.begin)
   }
   
   def commit = synchronized {
@@ -330,20 +323,12 @@ object Transaction {
       persistentStateMap.valuesIterator.foreach(_.commit)
     }
     status = TransactionStatus.Completed
-    jta match {
-      case Left(Some(userTx)) => if (isJtaTxActive(userTx.getStatus)) userTx.commit
-      case Right(Some(txMan)) => if (isJtaTxActive(txMan.getStatus)) txMan.commit
-      case _ => {} // do nothing
-    }
+    tc.foreach(_.commit)
   }
 
   def abort = synchronized {
     log.trace("Aborting transaction %s", toString)
-    jta match {
-      case Left(Some(userTx)) => if (isJtaTxActive(userTx.getStatus)) userTx.rollback
-      case Right(Some(txMan)) => if (isJtaTxActive(txMan.getStatus)) txMan.rollback
-      case _ => {} // do nothing
-    }
+    tc.foreach(_.rollback)
   }
 
   def isNew = synchronized { status == TransactionStatus.New }
diff --git a/akka-core/src/main/scala/stm/TransactionContainer.scala b/akka-core/src/main/scala/stm/TransactionContainer.scala
new file mode 100644
index 0000000000..19c7c54255
--- /dev/null
+++ b/akka-core/src/main/scala/stm/TransactionContainer.scala
@@ -0,0 +1,160 @@
+/**
+ * Copyright (C) 2009-2010 Scalable Solutions AB 
+ */
+
+package se.scalablesolutions.akka.stm
+
+import javax.transaction.{TransactionManager, UserTransaction, Transaction => JtaTransaction, SystemException, Status}
+import javax.naming.{InitialContext, Context, NamingException}
+
+import se.scalablesolutions.akka.config.Config._
+import se.scalablesolutions.akka.util.Logging
+
+/**
+ * JTA transaction container holding either a UserTransaction or a TransactionManager.
+ * 

+ * The TransactionContainer is created using the factory val container = TransactionContainer() + * + * @author Jonas Bonér + */ +class TransactionContainer private (val tm: Either[Option[UserTransaction], Option[TransactionManager]]) { + + def begin = tm match { + case Left(Some(userTx)) => userTx.begin + case Right(Some(txMan)) => txMan.begin + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + + def commit = tm match { + case Left(Some(userTx)) => userTx.commit + case Right(Some(txMan)) => txMan.commit + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + + def rollback = tm match { + case Left(Some(userTx)) => userTx.rollback + case Right(Some(txMan)) => txMan.rollback + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + + def getStatus = tm match { + case Left(Some(userTx)) => userTx.getStatus + case Right(Some(txMan)) => txMan.getStatus + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + + def isInExistingTransaction = tm match { + case Left(Some(userTx)) => userTx.getStatus == Status.STATUS_ACTIVE + case Right(Some(txMan)) => txMan.getStatus == Status.STATUS_ACTIVE + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + + def isRollbackOnly = tm match { + case Left(Some(userTx)) => userTx.getStatus == Status.STATUS_MARKED_ROLLBACK + case Right(Some(txMan)) => txMan.getStatus == Status.STATUS_MARKED_ROLLBACK + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + + def setRollbackOnly = tm match { + case Left(Some(userTx)) => userTx.setRollbackOnly + case Right(Some(txMan)) => txMan.setRollbackOnly + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + /* + def getTransaction: JtaTransaction = tm match { + case Left(Some(userTx)) => userTx + case Right(Some(txMan)) => txMan.getTransaction + case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + } + */ + def suspend = tm match { + case Right(Some(txMan)) => txMan.suspend + case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") + } + + def resume(tx: JtaTransaction) = tm match { + case Right(Some(txMan)) => txMan.resume(tx) + case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") + } +} + +/** + * Detects if there is a UserTransaction or TransactionManager available in the JNDI. + * + * @author Jonas Bonér + */ +object TransactionContainer extends Logging { + val AKKA_JTA_TRANSACTION_SERVICE_CLASS = "se.scalablesolutions.akka.jta.AtomikosTransactionService" + val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction" + val FALLBACK_TRANSACTION_MANAGER_NAMES = "java:comp/TransactionManager" :: + "java:appserver/TransactionManager" :: + "java:pm/TransactionManager" :: + "java:/TransactionManager" :: Nil + val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry" + val TRANSACTION_SYNCHRONIZATION_REGISTRY_CLASS_NAME = "javax.transaction.TransactionSynchronizationRegistry" + val JTA_PROVIDER = config.getString("akka.stm.jta.provider", "from-jndi") + + def apply(tm: Either[Option[UserTransaction], Option[TransactionManager]]) = new TransactionContainer(tm) + + def apply(): TransactionContainer = + JTA_PROVIDER match { + case "from-jndi" => + new TransactionContainer(findUserTransaction match { + case None => Right(findTransactionManager) + case tm => Left(tm) + }) + case "atomikos" => + try { + Class.forName(AKKA_JTA_TRANSACTION_SERVICE_CLASS) + .newInstance.asInstanceOf[TransactionService] + .transactionContainer + } catch { + case e: ClassNotFoundException => + throw new IllegalStateException( + "JTA provider defined as 'atomikos', but the AtomikosTransactionService classes can not be found." + + "\n\tPlease make sure you have 'akka-jta' JAR and its dependencies on your classpath.") + } + case _ => + throw new IllegalStateException( + "No UserTransaction on TransactionManager could be found in scope." + + "\n\tEither add 'akka-jta' to the classpath or make sure there is a" + + "\n\tTransactionManager or UserTransaction defined in the JNDI.") + + } + + def findUserTransaction: Option[UserTransaction] = { + val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME) + if (located eq null) None + else { + log.info("JTA UserTransaction detected [%s]", located) + Some(located.asInstanceOf[UserTransaction]) + } + } + + def findTransactionManager: Option[TransactionManager] = { + val context = createInitialContext + val tms = for { + name <- FALLBACK_TRANSACTION_MANAGER_NAMES + tm = context.lookup(name) + if tm ne null + } yield tm + tms match { + case Nil => None + case tm :: _ => + log.info("JTA TransactionManager detected [%s]", tm) + Some(tm.asInstanceOf[TransactionManager]) + } + } + + private def createInitialContext = new InitialContext(new java.util.Hashtable) +} + +/** + * JTA Transaction service. + * + * @author Jonas Bonér + */ +trait TransactionService { + def transactionContainer: TransactionContainer +} + diff --git a/akka-jta/src/main/scala/AtomikosTransactionService.scala b/akka-jta/src/main/scala/AtomikosTransactionService.scala index 1daa08a204..d59c60961b 100644 --- a/akka-jta/src/main/scala/AtomikosTransactionService.scala +++ b/akka-jta/src/main/scala/AtomikosTransactionService.scala @@ -10,19 +10,22 @@ import com.atomikos.icatch.jta.{J2eeTransactionManager, J2eeUserTransaction} import com.atomikos.icatch.config.{TSInitInfo, UserTransactionService, UserTransactionServiceImp} import se.scalablesolutions.akka.config.Config._ +import se.scalablesolutions.akka.stm.{TransactionService, TransactionContainer} + +object AtomikosTransactionService extends AtomikosTransactionService /** * Atomikos implementation of the transaction service trait. * * @author Jonas Bonér */ -object AtomikosTransactionService extends TransactionService with TransactionProtocol { +class AtomikosTransactionService extends TransactionService with TransactionProtocol { - val JTA_TRANSACTION_TIMEOUT = config.getInt("akka.jta.timeout", 60) + val JTA_TRANSACTION_TIMEOUT: Int = config.getInt("akka.stm.jta.timeout", 60000) / 1000 private val txService: UserTransactionService = new UserTransactionServiceImp private val info: TSInitInfo = txService.createTSInitInfo - val transactionManager = + val transactionContainer: TransactionContainer = TransactionContainer(Right(Some( try { txService.init(info) val tm: TransactionManager = new J2eeTransactionManager @@ -31,7 +34,7 @@ object AtomikosTransactionService extends TransactionService with TransactionPro } catch { case e => throw new SystemException("Could not create a new Atomikos J2EE Transaction Manager, due to: " + e.toString) } - + ))) // TODO: gracefully shutdown of the TM //txService.shutdown(false) } diff --git a/akka-jta/src/main/scala/TransactionContext.scala b/akka-jta/src/main/scala/TransactionContext.scala index 218bd93828..0f6ae29454 100644 --- a/akka-jta/src/main/scala/TransactionContext.scala +++ b/akka-jta/src/main/scala/TransactionContext.scala @@ -6,18 +6,10 @@ package se.scalablesolutions.akka.jta import javax.transaction.{Transaction, Status, TransactionManager} +import se.scalablesolutions.akka.stm.{TransactionService, TransactionContainer} import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.config.Config._ -/** - * JTA Transaction service. - * - * @author Jonas Bonér - */ -trait TransactionService { - def transactionManager: TransactionManager -} - /** * Base monad for the transaction monad implementations. * @@ -39,11 +31,6 @@ trait TransactionMonad { // JTA Transaction definitions // ----------------------------- - /** - * Returns the current Transaction. - */ - def getTransaction: Transaction = TransactionContext.getTransactionManager.getTransaction - /** * Marks the current transaction as doomed. */ @@ -108,13 +95,8 @@ trait TransactionMonad { * @author Jonas Bonér */ object TransactionContext extends TransactionProtocol with Logging { - val TRANSACTION_PROVIDER = config.getString("akka.jta.transaction-provider", "atomikos") - - private implicit val defaultTransactionService = TRANSACTION_PROVIDER match { - case "atomikos" => AtomikosTransactionService - case _ => throw new IllegalArgumentException("Transaction provider [" + TRANSACTION_PROVIDER + "] is not supported") - } - private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext) + implicit val tc = TransactionContainer() + private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext(tc)) object Required extends TransactionMonad { def map[T](f: TransactionMonad => T): T = withTxRequired { f(this) } @@ -157,9 +139,7 @@ object TransactionContext extends TransactionProtocol with Logging { private[jta] def isRollbackOnly = current.isRollbackOnly - private[jta] def getTransactionManager: TransactionManager = current.getTransactionManager - - private[jta] def getTransaction: Transaction = current.getTransactionManager.getTransaction + private[jta] def getTransactionContainer: TransactionContainer = current.getTransactionContainer private[this] def current = stack.value @@ -171,14 +151,14 @@ object TransactionContext extends TransactionProtocol with Logging { */ private[jta] def withNewContext[T](body: => T): T = { val suspendedTx: Option[Transaction] = - if (isInExistingTransaction(getTransactionManager)) { + if (getTransactionContainer.isInExistingTransaction) { log.debug("Suspending TX") - Some(getTransactionManager.suspend) + Some(getTransactionContainer.suspend) } else None - val result = stack.withValue(new TransactionContext) { body } + val result = stack.withValue(new TransactionContext(tc)) { body } if (suspendedTx.isDefined) { log.debug("Resuming TX") - getTransactionManager.resume(suspendedTx.get) + getTransactionContainer.resume(suspendedTx.get) } result } @@ -189,9 +169,8 @@ object TransactionContext extends TransactionProtocol with Logging { * * @author Jonas Bonér */ -class TransactionContext(private implicit val transactionService: TransactionService) { - val tm: TransactionManager = transactionService.transactionManager - private def setRollbackOnly = tm.setRollbackOnly - private def isRollbackOnly: Boolean = tm.getStatus == Status.STATUS_MARKED_ROLLBACK - private def getTransactionManager: TransactionManager = tm +class TransactionContext(val tc: TransactionContainer) { + private def setRollbackOnly = tc.setRollbackOnly + private def isRollbackOnly: Boolean = tc.getStatus == Status.STATUS_MARKED_ROLLBACK + private def getTransactionContainer: TransactionContainer = tc } diff --git a/akka-jta/src/main/scala/TransactionProtocol.scala b/akka-jta/src/main/scala/TransactionProtocol.scala index 4a4c263eb1..837955425e 100644 --- a/akka-jta/src/main/scala/TransactionProtocol.scala +++ b/akka-jta/src/main/scala/TransactionProtocol.scala @@ -5,6 +5,7 @@ package se.scalablesolutions.akka.jta import se.scalablesolutions.akka.util.Logging +import se.scalablesolutions.akka.stm.TransactionContainer import javax.naming.{NamingException, Context, InitialContext} import javax.transaction.{ @@ -71,7 +72,7 @@ trait TransactionProtocol extends Logging { *

    * override def joinTransaction = {
    *   val em = TransactionContext.getEntityManager
-   *   val tm = TransactionContext.getTransactionManager
+   *   val tm = TransactionContext.getTransactionContainer
    *   val closeAtTxCompletion: Boolean) 
    *   tm.getTransaction.registerSynchronization(new javax.transaction.Synchronization() {
    *     def beforeCompletion = {
@@ -109,7 +110,7 @@ trait TransactionProtocol extends Logging {
    * Here is an example on how to handle JPA exceptions.
    * 
    * 
-   * def handleException(tm: TransactionManager, e: Exception) = {
+   * def handleException(tm: TransactionContainer, e: Exception) = {
    *   if (isInExistingTransaction(tm)) {
    *     // Do not roll back in case of NoResultException or NonUniqueResultException
    *     if (!e.isInstanceOf[NoResultException] &&
@@ -122,7 +123,7 @@ trait TransactionProtocol extends Logging {
    * }
    * 
*/ - def handleException(tm: TransactionManager, e: Exception) = { + def handleException(tm: TransactionContainer, e: Exception) = { tm.setRollbackOnly throw e } @@ -133,7 +134,7 @@ trait TransactionProtocol extends Logging { * Creates a new transaction if no transaction is active in scope, else joins the outer transaction. */ def withTxRequired[T](body: => T): T = { - val tm = TransactionContext.getTransactionManager + val tm = TransactionContext.getTransactionContainer if (!isInExistingTransaction(tm)) { tm.begin try { @@ -154,7 +155,7 @@ trait TransactionProtocol extends Logging { * commits or rollbacks new transaction, finally resumes previous transaction. */ def withTxRequiresNew[T](body: => T): T = TransactionContext.withNewContext { - val tm = TransactionContext.getTransactionManager + val tm = TransactionContext.getTransactionContainer tm.begin try { joinTransaction @@ -191,7 +192,7 @@ trait TransactionProtocol extends Logging { * Throws a TransactionRequiredException if there is no transaction active in scope. */ def withTxMandatory[T](body: => T): T = { - if (!isInExistingTransaction(TransactionContext.getTransactionManager)) + if (!isInExistingTransaction(TransactionContext.getTransactionContainer)) throw new TransactionRequiredException("No active TX at method with TX type set to MANDATORY") body } @@ -202,12 +203,12 @@ trait TransactionProtocol extends Logging { * Throws a SystemException in case of an existing transaction in scope. */ def withTxNever[T](body: => T): T = { - if (isInExistingTransaction(TransactionContext.getTransactionManager)) + if (isInExistingTransaction(TransactionContext.getTransactionContainer)) throw new SystemException("Detected active TX at method with TX type set to NEVER") body } - protected def commitOrRollBack(tm: TransactionManager) = { + protected def commitOrRollBack(tm: TransactionContainer) = { if (isInExistingTransaction(tm)) { if (isRollbackOnly(tm)) { log.debug("Rolling back TX marked as ROLLBACK_ONLY") @@ -229,7 +230,7 @@ trait TransactionProtocol extends Logging { * @param tm the transaction manager * @return boolean */ - protected def isInExistingTransaction(tm: TransactionManager): Boolean = + protected def isInExistingTransaction(tm: TransactionContainer): Boolean = tm.getStatus != Status.STATUS_NO_TRANSACTION /** @@ -238,7 +239,7 @@ trait TransactionProtocol extends Logging { * @param tm the transaction manager * @return boolean */ - protected def isRollbackOnly(tm: TransactionManager): Boolean = + protected def isRollbackOnly(tm: TransactionContainer): Boolean = tm.getStatus == Status.STATUS_MARKED_ROLLBACK /** diff --git a/config/akka-reference.conf b/config/akka-reference.conf index 8e4be0bab4..d2c5ed3c73 100644 --- a/config/akka-reference.conf +++ b/config/akka-reference.conf @@ -17,7 +17,7 @@ version = "0.9" - # FQN to the class doing initial active object/actor + # FQN (Fully Qualified Name) to the class doing initial active object/actor # supervisor bootstrap, should be defined in default constructor boot = ["sample.camel.Boot", "sample.rest.java.Boot", @@ -31,17 +31,18 @@ service = on - fair = on # should transactions be fair or non-fair (non fair yield better performance) - max-nr-of-retries = 1000 # max nr of retries of a failing transaction before giving up - timeout = 10000 # transaction timeout; if transaction has not committed within the timeout then it is aborted - jta-aware = off # 'on' means that if there JTA Transaction Manager available then the STM will begin (or join), commit or rollback the JTA transaction. Default is 'off' + fair = on # should transactions be fair or non-fair (non fair yield better performance) + max-nr-of-retries = 1000 # max nr of retries of a failing transaction before giving up + timeout = 10000 # transaction timeout; if transaction has not committed within the timeout then it is aborted + jta-aware = off # 'on' means that if there JTA Transaction Manager available then the STM will + # begin (or join), commit or rollback the JTA transaction. Default is 'off'. - service = off # 'on' means that if there is a running JTA transaction then the STM will participate in it. Default is 'off' - timeout = 60 # timeout in seconds + provider = "from-jndi" # Options: "from-jndi", "atomikos" + timeout = 60000 - + service = on hostname = "localhost" From b4c7158b6172700b1210ab07282ae08348d49768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Tue, 20 Apr 2010 13:32:34 +0200 Subject: [PATCH 27/41] Cleaned up JTA stuff --- .../main/scala/stm/TransactionContainer.scala | 10 +-- .../scala/AtomikosTransactionService.scala | 2 +- .../src/main/scala/TransactionContext.scala | 84 +++++++++---------- .../src/main/scala/TransactionProtocol.scala | 2 +- config/akka-reference.conf | 4 +- 5 files changed, 49 insertions(+), 53 deletions(-) diff --git a/akka-core/src/main/scala/stm/TransactionContainer.scala b/akka-core/src/main/scala/stm/TransactionContainer.scala index 19c7c54255..09da21a494 100644 --- a/akka-core/src/main/scala/stm/TransactionContainer.scala +++ b/akka-core/src/main/scala/stm/TransactionContainer.scala @@ -60,13 +60,7 @@ class TransactionContainer private (val tm: Either[Option[UserTransaction], Opti case Right(Some(txMan)) => txMan.setRollbackOnly case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") } - /* - def getTransaction: JtaTransaction = tm match { - case Left(Some(userTx)) => userTx - case Right(Some(txMan)) => txMan.getTransaction - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - */ + def suspend = tm match { case Right(Some(txMan)) => txMan.suspend case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") @@ -92,7 +86,7 @@ object TransactionContainer extends Logging { "java:/TransactionManager" :: Nil val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry" val TRANSACTION_SYNCHRONIZATION_REGISTRY_CLASS_NAME = "javax.transaction.TransactionSynchronizationRegistry" - val JTA_PROVIDER = config.getString("akka.stm.jta.provider", "from-jndi") + val JTA_PROVIDER = config.getString("akka.jta.provider", "from-jndi") def apply(tm: Either[Option[UserTransaction], Option[TransactionManager]]) = new TransactionContainer(tm) diff --git a/akka-jta/src/main/scala/AtomikosTransactionService.scala b/akka-jta/src/main/scala/AtomikosTransactionService.scala index d59c60961b..937f31a54e 100644 --- a/akka-jta/src/main/scala/AtomikosTransactionService.scala +++ b/akka-jta/src/main/scala/AtomikosTransactionService.scala @@ -21,7 +21,7 @@ object AtomikosTransactionService extends AtomikosTransactionService */ class AtomikosTransactionService extends TransactionService with TransactionProtocol { - val JTA_TRANSACTION_TIMEOUT: Int = config.getInt("akka.stm.jta.timeout", 60000) / 1000 + val JTA_TRANSACTION_TIMEOUT: Int = config.getInt("akka.jta.timeout", 60000) / 1000 private val txService: UserTransactionService = new UserTransactionServiceImp private val info: TSInitInfo = txService.createTSInitInfo diff --git a/akka-jta/src/main/scala/TransactionContext.scala b/akka-jta/src/main/scala/TransactionContext.scala index 0f6ae29454..43d2c082d8 100644 --- a/akka-jta/src/main/scala/TransactionContext.scala +++ b/akka-jta/src/main/scala/TransactionContext.scala @@ -10,48 +10,6 @@ import se.scalablesolutions.akka.stm.{TransactionService, TransactionContainer} import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.config.Config._ -/** - * Base monad for the transaction monad implementations. - * - * @author Jonas Bonér - */ -trait TransactionMonad { - - // ----------------------------- - // Monadic definitions - // ----------------------------- - - def map[T](f: TransactionMonad => T): T - def flatMap[T](f: TransactionMonad => T): T - def foreach(f: TransactionMonad => Unit): Unit - def filter(f: TransactionMonad => Boolean): TransactionMonad = - if (f(this)) this else TransactionContext.NoOpTransactionMonad - - // ----------------------------- - // JTA Transaction definitions - // ----------------------------- - - /** - * Marks the current transaction as doomed. - */ - def setRollbackOnly = TransactionContext.setRollbackOnly - - /** - * Marks the current transaction as doomed. - */ - def doom = TransactionContext.setRollbackOnly - - /** - * Checks if the current transaction is doomed. - */ - def isRollbackOnly = TransactionContext.isRollbackOnly - - /** - * Checks that the current transaction is NOT doomed. - */ - def isNotDoomed = !TransactionContext.isRollbackOnly -} - /** * The TransactionContext object manages the transactions. * Can be used as higher-order functional 'atomic blocks' or monadic. @@ -164,6 +122,48 @@ object TransactionContext extends TransactionProtocol with Logging { } } +/** + * Base monad for the transaction monad implementations. + * + * @author Jonas Bonér + */ +trait TransactionMonad { + + // ----------------------------- + // Monadic definitions + // ----------------------------- + + def map[T](f: TransactionMonad => T): T + def flatMap[T](f: TransactionMonad => T): T + def foreach(f: TransactionMonad => Unit): Unit + def filter(f: TransactionMonad => Boolean): TransactionMonad = + if (f(this)) this else TransactionContext.NoOpTransactionMonad + + // ----------------------------- + // JTA Transaction definitions + // ----------------------------- + + /** + * Marks the current transaction as doomed. + */ + def setRollbackOnly = TransactionContext.setRollbackOnly + + /** + * Marks the current transaction as doomed. + */ + def doom = TransactionContext.setRollbackOnly + + /** + * Checks if the current transaction is doomed. + */ + def isRollbackOnly = TransactionContext.isRollbackOnly + + /** + * Checks that the current transaction is NOT doomed. + */ + def isNotDoomed = !TransactionContext.isRollbackOnly +} + /** * Transaction context, holds the EntityManager and the TransactionManager. * diff --git a/akka-jta/src/main/scala/TransactionProtocol.scala b/akka-jta/src/main/scala/TransactionProtocol.scala index 837955425e..2036f0d013 100644 --- a/akka-jta/src/main/scala/TransactionProtocol.scala +++ b/akka-jta/src/main/scala/TransactionProtocol.scala @@ -115,7 +115,7 @@ trait TransactionProtocol extends Logging { * // Do not roll back in case of NoResultException or NonUniqueResultException * if (!e.isInstanceOf[NoResultException] && * !e.isInstanceOf[NonUniqueResultException]) { - * log.debug("Setting TX to ROLLBACK_ONLY, due to: " + e) + * log.debug("Setting TX to ROLLBACK_ONLY, due to: %s", e) * tm.setRollbackOnly * } * } diff --git a/config/akka-reference.conf b/config/akka-reference.conf index d2c5ed3c73..ab365ea6eb 100644 --- a/config/akka-reference.conf +++ b/config/akka-reference.conf @@ -39,7 +39,9 @@ - provider = "from-jndi" # Options: "from-jndi", "atomikos" + provider = "from-jndi" # Options: "from-jndi" (means that Akka will try to detect a TransactionManager in the JNDI) + # "atomikos" (means that Akka will use the Atomikos based JTA impl in 'akka-jta', + # e.g. you need the akka-jta JARs on classpath). timeout = 60000 From fec67a8fc1447545d09effd2937eebe2ea45d156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Tue, 20 Apr 2010 14:53:45 +0200 Subject: [PATCH 28/41] Added STM Synchronization registration to JNDI TransactionSynchronizationRegistry --- .../src/main/scala/stm/Transaction.scala | 19 ++- .../main/scala/stm/TransactionContainer.scala | 154 ------------------ 2 files changed, 14 insertions(+), 159 deletions(-) delete mode 100644 akka-core/src/main/scala/stm/TransactionContainer.scala diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala index 7fbe9898e6..97d588f400 100644 --- a/akka-core/src/main/scala/stm/Transaction.scala +++ b/akka-core/src/main/scala/stm/Transaction.scala @@ -8,7 +8,7 @@ import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.TimeUnit -import javax.transaction.{TransactionManager, UserTransaction, Status} +import javax.transaction.{TransactionManager, UserTransaction, Status, TransactionSynchronizationRegistry} import scala.collection.mutable.HashMap @@ -305,7 +305,7 @@ object Transaction { private[this] val persistentStateMap = new HashMap[String, Committable] private[akka] val depth = new AtomicInteger(0) - val tc: Option[TransactionContainer] = + val jta: Option[TransactionContainer] = if (JTA_AWARE) Some(TransactionContainer()) else None @@ -314,7 +314,16 @@ object Transaction { // --- public methods --------- def begin = synchronized { - tc.foreach(_.begin) + jta.foreach { txContainer => + txContainer.begin + TransactionContainer.findSynchronizationRegistry match { + case Some(registry) => + registry.asInstanceOf[TransactionSynchronizationRegistry].registerInterposedSynchronization( + new StmSynchronization(txContainer, this)) + case None => + log.warning("Cannot find TransactionSynchronizationRegistry in JNDI, can't register STM synchronization") + } + } } def commit = synchronized { @@ -323,12 +332,12 @@ object Transaction { persistentStateMap.valuesIterator.foreach(_.commit) } status = TransactionStatus.Completed - tc.foreach(_.commit) + jta.foreach(_.commit) } def abort = synchronized { log.trace("Aborting transaction %s", toString) - tc.foreach(_.rollback) + jta.foreach(_.rollback) } def isNew = synchronized { status == TransactionStatus.New } diff --git a/akka-core/src/main/scala/stm/TransactionContainer.scala b/akka-core/src/main/scala/stm/TransactionContainer.scala deleted file mode 100644 index 09da21a494..0000000000 --- a/akka-core/src/main/scala/stm/TransactionContainer.scala +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ - -package se.scalablesolutions.akka.stm - -import javax.transaction.{TransactionManager, UserTransaction, Transaction => JtaTransaction, SystemException, Status} -import javax.naming.{InitialContext, Context, NamingException} - -import se.scalablesolutions.akka.config.Config._ -import se.scalablesolutions.akka.util.Logging - -/** - * JTA transaction container holding either a UserTransaction or a TransactionManager. - *

- * The TransactionContainer is created using the factory val container = TransactionContainer() - * - * @author Jonas Bonér - */ -class TransactionContainer private (val tm: Either[Option[UserTransaction], Option[TransactionManager]]) { - - def begin = tm match { - case Left(Some(userTx)) => userTx.begin - case Right(Some(txMan)) => txMan.begin - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def commit = tm match { - case Left(Some(userTx)) => userTx.commit - case Right(Some(txMan)) => txMan.commit - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def rollback = tm match { - case Left(Some(userTx)) => userTx.rollback - case Right(Some(txMan)) => txMan.rollback - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def getStatus = tm match { - case Left(Some(userTx)) => userTx.getStatus - case Right(Some(txMan)) => txMan.getStatus - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def isInExistingTransaction = tm match { - case Left(Some(userTx)) => userTx.getStatus == Status.STATUS_ACTIVE - case Right(Some(txMan)) => txMan.getStatus == Status.STATUS_ACTIVE - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def isRollbackOnly = tm match { - case Left(Some(userTx)) => userTx.getStatus == Status.STATUS_MARKED_ROLLBACK - case Right(Some(txMan)) => txMan.getStatus == Status.STATUS_MARKED_ROLLBACK - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def setRollbackOnly = tm match { - case Left(Some(userTx)) => userTx.setRollbackOnly - case Right(Some(txMan)) => txMan.setRollbackOnly - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") - } - - def suspend = tm match { - case Right(Some(txMan)) => txMan.suspend - case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") - } - - def resume(tx: JtaTransaction) = tm match { - case Right(Some(txMan)) => txMan.resume(tx) - case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") - } -} - -/** - * Detects if there is a UserTransaction or TransactionManager available in the JNDI. - * - * @author Jonas Bonér - */ -object TransactionContainer extends Logging { - val AKKA_JTA_TRANSACTION_SERVICE_CLASS = "se.scalablesolutions.akka.jta.AtomikosTransactionService" - val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction" - val FALLBACK_TRANSACTION_MANAGER_NAMES = "java:comp/TransactionManager" :: - "java:appserver/TransactionManager" :: - "java:pm/TransactionManager" :: - "java:/TransactionManager" :: Nil - val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry" - val TRANSACTION_SYNCHRONIZATION_REGISTRY_CLASS_NAME = "javax.transaction.TransactionSynchronizationRegistry" - val JTA_PROVIDER = config.getString("akka.jta.provider", "from-jndi") - - def apply(tm: Either[Option[UserTransaction], Option[TransactionManager]]) = new TransactionContainer(tm) - - def apply(): TransactionContainer = - JTA_PROVIDER match { - case "from-jndi" => - new TransactionContainer(findUserTransaction match { - case None => Right(findTransactionManager) - case tm => Left(tm) - }) - case "atomikos" => - try { - Class.forName(AKKA_JTA_TRANSACTION_SERVICE_CLASS) - .newInstance.asInstanceOf[TransactionService] - .transactionContainer - } catch { - case e: ClassNotFoundException => - throw new IllegalStateException( - "JTA provider defined as 'atomikos', but the AtomikosTransactionService classes can not be found." + - "\n\tPlease make sure you have 'akka-jta' JAR and its dependencies on your classpath.") - } - case _ => - throw new IllegalStateException( - "No UserTransaction on TransactionManager could be found in scope." + - "\n\tEither add 'akka-jta' to the classpath or make sure there is a" + - "\n\tTransactionManager or UserTransaction defined in the JNDI.") - - } - - def findUserTransaction: Option[UserTransaction] = { - val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME) - if (located eq null) None - else { - log.info("JTA UserTransaction detected [%s]", located) - Some(located.asInstanceOf[UserTransaction]) - } - } - - def findTransactionManager: Option[TransactionManager] = { - val context = createInitialContext - val tms = for { - name <- FALLBACK_TRANSACTION_MANAGER_NAMES - tm = context.lookup(name) - if tm ne null - } yield tm - tms match { - case Nil => None - case tm :: _ => - log.info("JTA TransactionManager detected [%s]", tm) - Some(tm.asInstanceOf[TransactionManager]) - } - } - - private def createInitialContext = new InitialContext(new java.util.Hashtable) -} - -/** - * JTA Transaction service. - * - * @author Jonas Bonér - */ -trait TransactionService { - def transactionContainer: TransactionContainer -} - From 1d329247183ae957f9f6f483ccf5ff36a4ed68dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Tue, 20 Apr 2010 14:53:45 +0200 Subject: [PATCH 29/41] Added STM Synchronization registration to JNDI TransactionSynchronizationRegistry --- .../{TransactionContainer.scala => JTA.scala} | 163 +++++++++++------- .../src/main/scala/stm/Transaction.scala | 19 +- 2 files changed, 112 insertions(+), 70 deletions(-) rename akka-core/src/main/scala/stm/{TransactionContainer.scala => JTA.scala} (81%) diff --git a/akka-core/src/main/scala/stm/TransactionContainer.scala b/akka-core/src/main/scala/stm/JTA.scala similarity index 81% rename from akka-core/src/main/scala/stm/TransactionContainer.scala rename to akka-core/src/main/scala/stm/JTA.scala index 09da21a494..5be0460681 100644 --- a/akka-core/src/main/scala/stm/TransactionContainer.scala +++ b/akka-core/src/main/scala/stm/JTA.scala @@ -4,12 +4,98 @@ package se.scalablesolutions.akka.stm -import javax.transaction.{TransactionManager, UserTransaction, Transaction => JtaTransaction, SystemException, Status} +import javax.transaction.{TransactionManager, UserTransaction, Transaction => JtaTransaction, SystemException, Status, Synchronization, TransactionSynchronizationRegistry} import javax.naming.{InitialContext, Context, NamingException} import se.scalablesolutions.akka.config.Config._ import se.scalablesolutions.akka.util.Logging +/** + * Detects if there is a UserTransaction or TransactionManager available in the JNDI. + * + * @author Jonas Bonér + */ +object TransactionContainer extends Logging { + val AKKA_JTA_TRANSACTION_SERVICE_CLASS = "se.scalablesolutions.akka.jta.AtomikosTransactionService" + val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction" + val FALLBACK_TRANSACTION_MANAGER_NAMES = "java:comp/TransactionManager" :: + "java:appserver/TransactionManager" :: + "java:pm/TransactionManager" :: + "java:/TransactionManager" :: Nil + val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry" + + val JTA_PROVIDER = config.getString("akka.jta.provider", "from-jndi") + + private var synchronizationRegistry: Option[TransactionSynchronizationRegistry] = None + + def apply(tm: Either[Option[UserTransaction], Option[TransactionManager]]) = new TransactionContainer(tm) + + def apply(): TransactionContainer = + JTA_PROVIDER match { + case "from-jndi" => + new TransactionContainer(findUserTransaction match { + case None => Right(findTransactionManager) + case tm => Left(tm) + }) + case "atomikos" => + try { + Class.forName(AKKA_JTA_TRANSACTION_SERVICE_CLASS) + .newInstance.asInstanceOf[TransactionService] + .transactionContainer + } catch { + case e: ClassNotFoundException => + throw new IllegalStateException( + "JTA provider defined as 'atomikos', but the AtomikosTransactionService classes can not be found." + + "\n\tPlease make sure you have 'akka-jta' JAR and its dependencies on your classpath.") + } + case _ => + throw new IllegalStateException( + "No UserTransaction on TransactionManager could be found in scope." + + "\n\tEither add 'akka-jta' to the classpath or make sure there is a" + + "\n\tTransactionManager or UserTransaction defined in the JNDI.") + + } + + def findUserTransaction: Option[UserTransaction] = { + val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME) + if (located eq null) None + else { + log.info("JTA UserTransaction detected [%s]", located) + Some(located.asInstanceOf[UserTransaction]) + } + } + + def findSynchronizationRegistry: Option[TransactionSynchronizationRegistry] = synchronized { + if (synchronizationRegistry.isDefined) synchronizationRegistry + else { + val located = createInitialContext.lookup(DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME) + if (located eq null) None + else { + log.info("JTA TransactionSynchronizationRegistry detected [%s]", located) + synchronizationRegistry = Some(located.asInstanceOf[TransactionSynchronizationRegistry]) + synchronizationRegistry + } + } + } + + def findTransactionManager: Option[TransactionManager] = { + val context = createInitialContext + val tms = for { + name <- FALLBACK_TRANSACTION_MANAGER_NAMES + tm = context.lookup(name) + if tm ne null + } yield tm + tms match { + case Nil => None + case tm :: _ => + log.info("JTA TransactionManager detected [%s]", tm) + Some(tm.asInstanceOf[TransactionManager]) + } + } + + private def createInitialContext = new InitialContext(new java.util.Hashtable) +} + /** * JTA transaction container holding either a UserTransaction or a TransactionManager. *

@@ -18,7 +104,6 @@ import se.scalablesolutions.akka.util.Logging * @author Jonas Bonér */ class TransactionContainer private (val tm: Either[Option[UserTransaction], Option[TransactionManager]]) { - def begin = tm match { case Left(Some(userTx)) => userTx.begin case Right(Some(txMan)) => txMan.begin @@ -73,74 +158,22 @@ class TransactionContainer private (val tm: Either[Option[UserTransaction], Opti } /** - * Detects if there is a UserTransaction or TransactionManager available in the JNDI. - * + * STM Synchronization class for synchronizing with the JTA TransactionManager. + * * @author Jonas Bonér */ -object TransactionContainer extends Logging { - val AKKA_JTA_TRANSACTION_SERVICE_CLASS = "se.scalablesolutions.akka.jta.AtomikosTransactionService" - val DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction" - val FALLBACK_TRANSACTION_MANAGER_NAMES = "java:comp/TransactionManager" :: - "java:appserver/TransactionManager" :: - "java:pm/TransactionManager" :: - "java:/TransactionManager" :: Nil - val DEFAULT_TRANSACTION_SYNCHRONIZATION_REGISTRY_NAME = "java:comp/TransactionSynchronizationRegistry" - val TRANSACTION_SYNCHRONIZATION_REGISTRY_CLASS_NAME = "javax.transaction.TransactionSynchronizationRegistry" - val JTA_PROVIDER = config.getString("akka.jta.provider", "from-jndi") - - def apply(tm: Either[Option[UserTransaction], Option[TransactionManager]]) = new TransactionContainer(tm) - - def apply(): TransactionContainer = - JTA_PROVIDER match { - case "from-jndi" => - new TransactionContainer(findUserTransaction match { - case None => Right(findTransactionManager) - case tm => Left(tm) - }) - case "atomikos" => - try { - Class.forName(AKKA_JTA_TRANSACTION_SERVICE_CLASS) - .newInstance.asInstanceOf[TransactionService] - .transactionContainer - } catch { - case e: ClassNotFoundException => - throw new IllegalStateException( - "JTA provider defined as 'atomikos', but the AtomikosTransactionService classes can not be found." + - "\n\tPlease make sure you have 'akka-jta' JAR and its dependencies on your classpath.") - } - case _ => - throw new IllegalStateException( - "No UserTransaction on TransactionManager could be found in scope." + - "\n\tEither add 'akka-jta' to the classpath or make sure there is a" + - "\n\tTransactionManager or UserTransaction defined in the JNDI.") - - } - - def findUserTransaction: Option[UserTransaction] = { - val located = createInitialContext.lookup(DEFAULT_USER_TRANSACTION_NAME) - if (located eq null) None - else { - log.info("JTA UserTransaction detected [%s]", located) - Some(located.asInstanceOf[UserTransaction]) +class StmSynchronization(tc: TransactionContainer, tx: Transaction) extends Synchronization with Logging { + def beforeCompletion = { + val status = tc.getStatus + if (status != Status.STATUS_ROLLEDBACK && + status != Status.STATUS_ROLLING_BACK && + status != Status.STATUS_MARKED_ROLLBACK) { + log.debug("JTA transaction has failed, abort STM transaction") + tx.transaction.foreach(_.abort) // abort multiverse tx } } - def findTransactionManager: Option[TransactionManager] = { - val context = createInitialContext - val tms = for { - name <- FALLBACK_TRANSACTION_MANAGER_NAMES - tm = context.lookup(name) - if tm ne null - } yield tm - tms match { - case Nil => None - case tm :: _ => - log.info("JTA TransactionManager detected [%s]", tm) - Some(tm.asInstanceOf[TransactionManager]) - } - } - - private def createInitialContext = new InitialContext(new java.util.Hashtable) + def afterCompletion(status: Int) = {} } /** diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala index 7fbe9898e6..97d588f400 100644 --- a/akka-core/src/main/scala/stm/Transaction.scala +++ b/akka-core/src/main/scala/stm/Transaction.scala @@ -8,7 +8,7 @@ import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.TimeUnit -import javax.transaction.{TransactionManager, UserTransaction, Status} +import javax.transaction.{TransactionManager, UserTransaction, Status, TransactionSynchronizationRegistry} import scala.collection.mutable.HashMap @@ -305,7 +305,7 @@ object Transaction { private[this] val persistentStateMap = new HashMap[String, Committable] private[akka] val depth = new AtomicInteger(0) - val tc: Option[TransactionContainer] = + val jta: Option[TransactionContainer] = if (JTA_AWARE) Some(TransactionContainer()) else None @@ -314,7 +314,16 @@ object Transaction { // --- public methods --------- def begin = synchronized { - tc.foreach(_.begin) + jta.foreach { txContainer => + txContainer.begin + TransactionContainer.findSynchronizationRegistry match { + case Some(registry) => + registry.asInstanceOf[TransactionSynchronizationRegistry].registerInterposedSynchronization( + new StmSynchronization(txContainer, this)) + case None => + log.warning("Cannot find TransactionSynchronizationRegistry in JNDI, can't register STM synchronization") + } + } } def commit = synchronized { @@ -323,12 +332,12 @@ object Transaction { persistentStateMap.valuesIterator.foreach(_.commit) } status = TransactionStatus.Completed - tc.foreach(_.commit) + jta.foreach(_.commit) } def abort = synchronized { log.trace("Aborting transaction %s", toString) - tc.foreach(_.rollback) + jta.foreach(_.rollback) } def isNew = synchronized { status == TransactionStatus.New } From 011898b96d070b44d5a6f89716d576572344d1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Wed, 21 Apr 2010 11:18:07 +0200 Subject: [PATCH 30/41] Made JTA Synchronization management generic and allowing more than one + refactoring --- akka-core/src/main/scala/stm/JTA.scala | 15 +++++++ .../src/main/scala/stm/Transaction.scala | 8 +--- .../src/main/scala/TransactionContext.scala | 44 +++++++++++++++++-- .../src/main/scala/TransactionProtocol.scala | 42 ++++++------------ 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/akka-core/src/main/scala/stm/JTA.scala b/akka-core/src/main/scala/stm/JTA.scala index 5be0460681..510e9cf78c 100644 --- a/akka-core/src/main/scala/stm/JTA.scala +++ b/akka-core/src/main/scala/stm/JTA.scala @@ -104,6 +104,21 @@ object TransactionContainer extends Logging { * @author Jonas Bonér */ class TransactionContainer private (val tm: Either[Option[UserTransaction], Option[TransactionManager]]) { + + def registerSynchronization(sync: Synchronization) = { + TransactionContainer.findSynchronizationRegistry match { // try to use SynchronizationRegistry in JNDI + case Some(registry) => + registry.asInstanceOf[TransactionSynchronizationRegistry].registerInterposedSynchronization(sync) + case None => + tm match { + case Right(Some(txMan)) => // try to use TransactionManager + txMan.getTransaction.registerSynchronization(sync) + case _ => + log.warning("Cannot find TransactionSynchronizationRegistry in JNDI, can't register STM synchronization") + } + } + } + def begin = tm match { case Left(Some(userTx)) => userTx.begin case Right(Some(txMan)) => txMan.begin diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala index 97d588f400..37b38b670b 100644 --- a/akka-core/src/main/scala/stm/Transaction.scala +++ b/akka-core/src/main/scala/stm/Transaction.scala @@ -316,13 +316,7 @@ object Transaction { def begin = synchronized { jta.foreach { txContainer => txContainer.begin - TransactionContainer.findSynchronizationRegistry match { - case Some(registry) => - registry.asInstanceOf[TransactionSynchronizationRegistry].registerInterposedSynchronization( - new StmSynchronization(txContainer, this)) - case None => - log.warning("Cannot find TransactionSynchronizationRegistry in JNDI, can't register STM synchronization") - } + txContainer.registerSynchronization(new StmSynchronization(txContainer, this)) } } diff --git a/akka-jta/src/main/scala/TransactionContext.scala b/akka-jta/src/main/scala/TransactionContext.scala index 43d2c082d8..b2574dd478 100644 --- a/akka-jta/src/main/scala/TransactionContext.scala +++ b/akka-jta/src/main/scala/TransactionContext.scala @@ -4,7 +4,7 @@ package se.scalablesolutions.akka.jta -import javax.transaction.{Transaction, Status, TransactionManager} +import javax.transaction.{Transaction, Status, TransactionManager, Synchronization} import se.scalablesolutions.akka.stm.{TransactionService, TransactionContainer} import se.scalablesolutions.akka.util.Logging @@ -56,6 +56,41 @@ object TransactionContext extends TransactionProtocol with Logging { implicit val tc = TransactionContainer() private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext(tc)) + /** + * This method can be used to register a Synchronization instance for participating with the JTA transaction. + * Here is an example of how to add a JPA EntityManager integration. + *

+   *   TransactionContext.registerSynchronization(new javax.transaction.Synchronization() {
+   *     def beforeCompletion = {
+   *       try {
+   *         val status = tm.getStatus
+   *         if (status != Status.STATUS_ROLLEDBACK &&
+   *             status != Status.STATUS_ROLLING_BACK &&
+   *             status != Status.STATUS_MARKED_ROLLBACK) {
+   *           log.debug("Flushing EntityManager...") 
+   *           em.flush // flush EntityManager on success
+   *         }
+   *       } catch {
+   *         case e: javax.transaction.SystemException => throw new RuntimeException(e)
+   *       }
+   *     }
+   *
+   *     def afterCompletion(status: Int) = {
+   *       val status = tm.getStatus
+   *       if (closeAtTxCompletion) em.close
+   *       if (status == Status.STATUS_ROLLEDBACK ||
+   *           status == Status.STATUS_ROLLING_BACK ||
+   *           status == Status.STATUS_MARKED_ROLLBACK) {
+   *         em.close
+   *       }
+   *     }
+   *   })
+   * 
+ * You should also override the 'joinTransaction' and 'handleException' methods. + * See ScalaDoc for these methods in the 'TransactionProtocol' for details. + */ + def registerSynchronization(sync: Synchronization) = synchronization.add(sync) + object Required extends TransactionMonad { def map[T](f: TransactionMonad => T): T = withTxRequired { f(this) } def flatMap[T](f: TransactionMonad => T): T = withTxRequired { f(this) } @@ -170,7 +205,8 @@ trait TransactionMonad { * @author Jonas Bonér */ class TransactionContext(val tc: TransactionContainer) { - private def setRollbackOnly = tc.setRollbackOnly - private def isRollbackOnly: Boolean = tc.getStatus == Status.STATUS_MARKED_ROLLBACK - private def getTransactionContainer: TransactionContainer = tc + def registerSynchronization(sync: Synchronization) = TransactionContext.registerSynchronization(sync) + def setRollbackOnly = tc.setRollbackOnly + def isRollbackOnly: Boolean = tc.getStatus == Status.STATUS_MARKED_ROLLBACK + def getTransactionContainer: TransactionContainer = tc } diff --git a/akka-jta/src/main/scala/TransactionProtocol.scala b/akka-jta/src/main/scala/TransactionProtocol.scala index 2036f0d013..004cf99657 100644 --- a/akka-jta/src/main/scala/TransactionProtocol.scala +++ b/akka-jta/src/main/scala/TransactionProtocol.scala @@ -7,6 +7,9 @@ package se.scalablesolutions.akka.jta import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.stm.TransactionContainer +import java.util.{List => JList} +import java.util.concurrent.CopyOnWriteArrayList + import javax.naming.{NamingException, Context, InitialContext} import javax.transaction.{ Transaction, @@ -15,6 +18,7 @@ import javax.transaction.{ Status, RollbackException, SystemException, + Synchronization, TransactionRequiredException } @@ -62,6 +66,8 @@ import javax.transaction.{ * @author Jonas Bonér */ trait TransactionProtocol extends Logging { + + protected val synchronization: JList[Synchronization] = new CopyOnWriteArrayList[Synchronization] /** * Join JTA transaction. Can be overriden by concrete transaction service implementation @@ -71,36 +77,10 @@ trait TransactionProtocol extends Logging { * *
    * override def joinTransaction = {
-   *   val em = TransactionContext.getEntityManager
-   *   val tm = TransactionContext.getTransactionContainer
-   *   val closeAtTxCompletion: Boolean) 
-   *   tm.getTransaction.registerSynchronization(new javax.transaction.Synchronization() {
-   *     def beforeCompletion = {
-   *       try {
-   *         val status = tm.getStatus
-   *         if (status != Status.STATUS_ROLLEDBACK &&
-   *             status != Status.STATUS_ROLLING_BACK &&
-   *             status != Status.STATUS_MARKED_ROLLBACK) {
-   *           log.debug("Flushing EntityManager...") 
-   *           em.flush // flush EntityManager on success
-   *         }
-   *       } catch {
-   *         case e: javax.transaction.SystemException => throw new RuntimeException(e)
-   *       }
-   *     }
-   *
-   *     def afterCompletion(status: Int) = {
-   *       val status = tm.getStatus
-   *       if (closeAtTxCompletion) em.close
-   *       if (status == Status.STATUS_ROLLEDBACK ||
-   *           status == Status.STATUS_ROLLING_BACK ||
-   *           status == Status.STATUS_MARKED_ROLLBACK) {
-   *         em.close
-   *       }
-   *     }
-   *   })
+   *   val em: EntityManager = ... // get the EntityManager
    *   em.joinTransaction // join JTA transaction
    * }
+   * 
*/ def joinTransaction: Unit = {} @@ -137,6 +117,7 @@ trait TransactionProtocol extends Logging { val tm = TransactionContext.getTransactionContainer if (!isInExistingTransaction(tm)) { tm.begin + registerSynchronization try { joinTransaction body @@ -157,6 +138,7 @@ trait TransactionProtocol extends Logging { def withTxRequiresNew[T](body: => T): T = TransactionContext.withNewContext { val tm = TransactionContext.getTransactionContainer tm.begin + registerSynchronization try { joinTransaction body @@ -224,6 +206,10 @@ trait TransactionProtocol extends Logging { // Helper methods // --------------------------- + protected def registerSynchronization = { + val it = synchronization.iterator + while (it.hasNext) TransactionContext.getTransactionContainer.registerSynchronization(it.next) + } /** * Checks if a transaction is an existing transaction. * From 68bb4af726d2c6eab2ff86a13320e522a0471b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Wed, 21 Apr 2010 11:23:30 +0200 Subject: [PATCH 31/41] Moved ActiveObjectConfiguration to ActiveObject.scala file --- .../src/main/scala/actor/ActiveObject.scala | 39 ++++++++++++++++++- .../actor/ActiveObjectConfiguration.scala | 39 ------------------- 2 files changed, 38 insertions(+), 40 deletions(-) delete mode 100644 akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala index 2446472e0e..9dbe1c939d 100644 --- a/akka-core/src/main/scala/actor/ActiveObject.scala +++ b/akka-core/src/main/scala/actor/ActiveObject.scala @@ -4,7 +4,7 @@ package se.scalablesolutions.akka.actor -import _root_.se.scalablesolutions.akka.config.FaultHandlingStrategy +import se.scalablesolutions.akka.config.FaultHandlingStrategy import se.scalablesolutions.akka.remote.protobuf.RemoteProtocol.RemoteRequest import se.scalablesolutions.akka.remote.{RemoteProtocolBuilder, RemoteClient, RemoteRequestIdFactory} import se.scalablesolutions.akka.dispatch.{MessageDispatcher, Future} @@ -29,6 +29,43 @@ object Annotations { val inittransactionalstate = classOf[inittransactionalstate] } +/** + * TODO: document + * FIXDOC: document ActiveObjectConfiguration + */ +final class ActiveObjectConfiguration { + private[akka] var _timeout: Long = Actor.TIMEOUT + private[akka] var _restartCallbacks: Option[RestartCallbacks] = None + private[akka] var _transactionRequired = false + private[akka] var _host: Option[InetSocketAddress] = None + private[akka] var _messageDispatcher: Option[MessageDispatcher] = None + + def timeout(timeout: Long) : ActiveObjectConfiguration = { + _timeout = timeout + this + } + + def restartCallbacks(pre: String, post: String) : ActiveObjectConfiguration = { + _restartCallbacks = Some(new RestartCallbacks(pre, post)) + this + } + + def makeTransactionRequired() : ActiveObjectConfiguration = { + _transactionRequired = true; + this + } + + def makeRemote(hostname: String, port: Int) : ActiveObjectConfiguration = { + _host = Some(new InetSocketAddress(hostname, port)) + this + } + + def dispatcher(messageDispatcher: MessageDispatcher) : ActiveObjectConfiguration = { + _messageDispatcher = Some(messageDispatcher) + this + } +} + /** * Factory class for creating Active Objects out of plain POJOs and/or POJOs with interfaces. * diff --git a/akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala b/akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala deleted file mode 100644 index 8390d2bdbb..0000000000 --- a/akka-core/src/main/scala/actor/ActiveObjectConfiguration.scala +++ /dev/null @@ -1,39 +0,0 @@ -package se.scalablesolutions.akka.actor - -import _root_.java.net.InetSocketAddress -import _root_.se.scalablesolutions.akka.config.ScalaConfig.RestartCallbacks -import _root_.se.scalablesolutions.akka.dispatch.MessageDispatcher - - -final class ActiveObjectConfiguration { - private[akka] var _timeout: Long = Actor.TIMEOUT - private[akka] var _restartCallbacks: Option[RestartCallbacks] = None - private[akka] var _transactionRequired = false - private[akka] var _host: Option[InetSocketAddress] = None - private[akka] var _messageDispatcher: Option[MessageDispatcher] = None - - def timeout(timeout: Long) : ActiveObjectConfiguration = { - _timeout = timeout - this - } - - def restartCallbacks(pre: String, post: String) : ActiveObjectConfiguration = { - _restartCallbacks = Some(new RestartCallbacks(pre, post)) - this - } - - def makeTransactionRequired() : ActiveObjectConfiguration = { - _transactionRequired = true; - this - } - - def makeRemote(hostname: String, port: Int) : ActiveObjectConfiguration = { - _host = Some(new InetSocketAddress(hostname, port)) - this - } - - def dispatcher(messageDispatcher: MessageDispatcher) : ActiveObjectConfiguration = { - _messageDispatcher = Some(messageDispatcher) - this - } -} \ No newline at end of file From 70087d5d9a3bbcd3c27c672066af66fda0c00170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Wed, 21 Apr 2010 15:37:51 +0200 Subject: [PATCH 32/41] added scaladoc --- akka-core/src/main/scala/actor/ActiveObject.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala index 9dbe1c939d..f3c536fdf8 100644 --- a/akka-core/src/main/scala/actor/ActiveObject.scala +++ b/akka-core/src/main/scala/actor/ActiveObject.scala @@ -30,7 +30,8 @@ object Annotations { } /** - * TODO: document + * Configuration factory for Active Objects. + * * FIXDOC: document ActiveObjectConfiguration */ final class ActiveObjectConfiguration { From 08f835e5fbcf366ad810428aa21499f2d0412bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Thu, 22 Apr 2010 09:53:45 +0200 Subject: [PATCH 33/41] Added StmConfigurationException --- .gitignore | 2 +- akka-core/src/main/scala/stm/JTA.scala | 22 +++++++++---------- .../src/main/scala/stm/Transaction.scala | 7 +++--- .../scala/stm/TransactionManagement.scala | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index b3b2c2ebc7..45fe24daba 100755 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ tm.out .classpath .idea .scala_dependencies - +multiverse.log \ No newline at end of file diff --git a/akka-core/src/main/scala/stm/JTA.scala b/akka-core/src/main/scala/stm/JTA.scala index 510e9cf78c..fd2db0fc74 100644 --- a/akka-core/src/main/scala/stm/JTA.scala +++ b/akka-core/src/main/scala/stm/JTA.scala @@ -44,12 +44,12 @@ object TransactionContainer extends Logging { .transactionContainer } catch { case e: ClassNotFoundException => - throw new IllegalStateException( + throw new StmConfigurationException( "JTA provider defined as 'atomikos', but the AtomikosTransactionService classes can not be found." + "\n\tPlease make sure you have 'akka-jta' JAR and its dependencies on your classpath.") } case _ => - throw new IllegalStateException( + throw new StmConfigurationException( "No UserTransaction on TransactionManager could be found in scope." + "\n\tEither add 'akka-jta' to the classpath or make sure there is a" + "\n\tTransactionManager or UserTransaction defined in the JNDI.") @@ -122,53 +122,53 @@ class TransactionContainer private (val tm: Either[Option[UserTransaction], Opti def begin = tm match { case Left(Some(userTx)) => userTx.begin case Right(Some(txMan)) => txMan.begin - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def commit = tm match { case Left(Some(userTx)) => userTx.commit case Right(Some(txMan)) => txMan.commit - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def rollback = tm match { case Left(Some(userTx)) => userTx.rollback case Right(Some(txMan)) => txMan.rollback - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def getStatus = tm match { case Left(Some(userTx)) => userTx.getStatus case Right(Some(txMan)) => txMan.getStatus - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def isInExistingTransaction = tm match { case Left(Some(userTx)) => userTx.getStatus == Status.STATUS_ACTIVE case Right(Some(txMan)) => txMan.getStatus == Status.STATUS_ACTIVE - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def isRollbackOnly = tm match { case Left(Some(userTx)) => userTx.getStatus == Status.STATUS_MARKED_ROLLBACK case Right(Some(txMan)) => txMan.getStatus == Status.STATUS_MARKED_ROLLBACK - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def setRollbackOnly = tm match { case Left(Some(userTx)) => userTx.setRollbackOnly case Right(Some(txMan)) => txMan.setRollbackOnly - case _ => throw new IllegalStateException("Does not have a UserTransaction or TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a UserTransaction or TransactionManager in scope") } def suspend = tm match { case Right(Some(txMan)) => txMan.suspend - case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a TransactionManager in scope") } def resume(tx: JtaTransaction) = tm match { case Right(Some(txMan)) => txMan.resume(tx) - case _ => throw new IllegalStateException("Does not have a TransactionManager in scope") + case _ => throw new StmConfigurationException("Does not have a TransactionManager in scope") } } diff --git a/akka-core/src/main/scala/stm/Transaction.scala b/akka-core/src/main/scala/stm/Transaction.scala index 37b38b670b..fb9f14b62e 100644 --- a/akka-core/src/main/scala/stm/Transaction.scala +++ b/akka-core/src/main/scala/stm/Transaction.scala @@ -24,6 +24,7 @@ import org.multiverse.stms.alpha.AlphaStm class NoTransactionInScopeException extends RuntimeException class TransactionRetryException(message: String) extends RuntimeException(message) +class StmConfigurationException(message: String) extends RuntimeException(message) /** * FIXDOC: document AtomicTemplate @@ -357,17 +358,17 @@ object Transaction { private[akka] def register(uuid: String, storage: Committable) = persistentStateMap.put(uuid, storage) private def ensureIsActive = if (status != TransactionStatus.Active) - throw new IllegalStateException( + throw new StmConfigurationException( "Expected ACTIVE transaction - current status [" + status + "]: " + toString) private def ensureIsActiveOrAborted = if (!(status == TransactionStatus.Active || status == TransactionStatus.Aborted)) - throw new IllegalStateException( + throw new StmConfigurationException( "Expected ACTIVE or ABORTED transaction - current status [" + status + "]: " + toString) private def ensureIsActiveOrNew = if (!(status == TransactionStatus.Active || status == TransactionStatus.New)) - throw new IllegalStateException( + throw new StmConfigurationException( "Expected ACTIVE or NEW transaction - current status [" + status + "]: " + toString) // For reinitialize transaction after sending it over the wire diff --git a/akka-core/src/main/scala/stm/TransactionManagement.scala b/akka-core/src/main/scala/stm/TransactionManagement.scala index 371d57ad88..d551f7fd76 100644 --- a/akka-core/src/main/scala/stm/TransactionManagement.scala +++ b/akka-core/src/main/scala/stm/TransactionManagement.scala @@ -40,13 +40,13 @@ object TransactionManagement extends TransactionManagement { private[akka] def getTransactionSet: CountDownCommitBarrier = { val option = transactionSet.get - if ((option eq null) || option.isEmpty) throw new IllegalStateException("No Transaction set in scope") + if ((option eq null) || option.isEmpty) throw new StmConfigurationException("No Transaction set in scope") else option.get } private[akka] def getTransaction: Transaction = { val option = transaction.get - if ((option eq null) || option.isEmpty) throw new IllegalStateException("No Transaction in scope") + if ((option eq null) || option.isEmpty) throw new StmConfigurationException("No Transaction in scope") option.get } } From 70256e4bb1b0ea89fc5f1f40394945ad6f25de93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Thu, 22 Apr 2010 12:18:18 +0200 Subject: [PATCH 34/41] JTA: Added option to register "joinTransaction" function and which classes to NOT roll back on --- .../src/main/scala/TransactionContext.scala | 27 ++++++++++ .../src/main/scala/TransactionProtocol.scala | 51 +++++++------------ 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/akka-jta/src/main/scala/TransactionContext.scala b/akka-jta/src/main/scala/TransactionContext.scala index b2574dd478..e6ac00de8b 100644 --- a/akka-jta/src/main/scala/TransactionContext.scala +++ b/akka-jta/src/main/scala/TransactionContext.scala @@ -54,6 +54,7 @@ import se.scalablesolutions.akka.config.Config._ */ object TransactionContext extends TransactionProtocol with Logging { implicit val tc = TransactionContainer() + private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext(tc)) /** @@ -91,6 +92,32 @@ object TransactionContext extends TransactionProtocol with Logging { */ def registerSynchronization(sync: Synchronization) = synchronization.add(sync) + /** + * Registeres a join transaction function. + *

+ * Here is an example on how to integrate with JPA EntityManager. + * + *

+   * TransactionContext.registerJoinTransactionFun(() => {
+   *   val em: EntityManager = ... // get the EntityManager
+   *   em.joinTransaction // join JTA transaction
+   * })
+   * 
+ */ + def registerJoinTransactionFun(fn: () => Unit) = joinTransactionFuns.add(fn) + + /** + * Handle exception. Can be overriden by concrete transaction service implementation. + *

+ * Here is an example on how to handle JPA exceptions. + * + *

+   * TransactionContext.registerExceptionNotToRollbackOn(classOf[NoResultException])
+   * TransactionContext.registerExceptionNotToRollbackOn(classOf[NonUniqueResultException])
+   * 
+ */ + def registerExceptionNotToRollbackOn(e: Class[_ <: Exception]) = exceptionsNotToRollbackOn.add(e) + object Required extends TransactionMonad { def map[T](f: TransactionMonad => T): T = withTxRequired { f(this) } def flatMap[T](f: TransactionMonad => T): T = withTxRequired { f(this) } diff --git a/akka-jta/src/main/scala/TransactionProtocol.scala b/akka-jta/src/main/scala/TransactionProtocol.scala index 004cf99657..c23ec26fd7 100644 --- a/akka-jta/src/main/scala/TransactionProtocol.scala +++ b/akka-jta/src/main/scala/TransactionProtocol.scala @@ -68,43 +68,26 @@ import javax.transaction.{ trait TransactionProtocol extends Logging { protected val synchronization: JList[Synchronization] = new CopyOnWriteArrayList[Synchronization] + protected val joinTransactionFuns: JList[() => Unit] = new CopyOnWriteArrayList[() => Unit] + protected val exceptionsNotToRollbackOn: JList[Class[_ <: Exception]] = new CopyOnWriteArrayList[Class[_ <: Exception]] - /** - * Join JTA transaction. Can be overriden by concrete transaction service implementation - * to hook into other transaction services. - *

- * Here is an example on how to integrate with JPA EntityManager. - * - *

-   * override def joinTransaction = {
-   *   val em: EntityManager = ... // get the EntityManager
-   *   em.joinTransaction // join JTA transaction
-   * }
-   * 
- */ - def joinTransaction: Unit = {} + def joinTransaction: Unit = { + val it = joinTransactionFuns.iterator + while (it.hasNext) { + val fn = it.next + fn() + } + } - /** - * Handle exception. Can be overriden by concrete transaction service implementation. - *

- * Here is an example on how to handle JPA exceptions. - * - *

-   * def handleException(tm: TransactionContainer, e: Exception) = {
-   *   if (isInExistingTransaction(tm)) {
-   *     // Do not roll back in case of NoResultException or NonUniqueResultException
-   *     if (!e.isInstanceOf[NoResultException] &&
-   *         !e.isInstanceOf[NonUniqueResultException]) {
-   *       log.debug("Setting TX to ROLLBACK_ONLY, due to: %s", e)
-   *       tm.setRollbackOnly
-   *     }
-   *   }
-   *   throw e
-   * }
-   * 
- */ def handleException(tm: TransactionContainer, e: Exception) = { - tm.setRollbackOnly + var rollback = true + val it = joinTransactionFuns.iterator + while (it.hasNext) { + val exception = it.next + if (e.getClass.isAssignableFrom(exception.getClass)) + rollback = false + } + if (rollback) tm.setRollbackOnly throw e } From d7e327d2b520b08d19bfd0023aafff0237fbbd4c Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Thu, 22 Apr 2010 12:48:16 +0200 Subject: [PATCH 35/41] updated dependencies in pom --- akka-spring/akka-spring-test-java/pom.xml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/akka-spring/akka-spring-test-java/pom.xml b/akka-spring/akka-spring-test-java/pom.xml index 0d35a47739..11a29262ac 100644 --- a/akka-spring/akka-spring-test-java/pom.xml +++ b/akka-spring/akka-spring-test-java/pom.xml @@ -6,7 +6,7 @@ Akka Spring Tests in Java akka-spring-test-java se.scalablesolutions.akka - 0.8 + 0.9 jar @@ -147,22 +147,22 @@ se.scalablesolutions.akka akka-core_2.8.0.Beta1 - 0.8.1 + 0.9 se.scalablesolutions.akka akka-util_2.8.0.Beta1 - 0.8.1 + 0.9 se.scalablesolutions.akka akka-util-java_2.8.0.Beta1 - 0.8.1 + 0.9 se.scalablesolutions.akka akka-spring_2.8.0.Beta1 - 0.8.1 + 0.9 org.springframework @@ -263,7 +263,17 @@ h2-lzf 1.0 - + + + jsr166x + jsr166x + 1.0 + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + 1.1.1 + From e598c28251bf46e06d30c29ce04fc34c6be01e38 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Fri, 23 Apr 2010 20:46:58 +0200 Subject: [PATCH 36/41] Initial parametrization --- akka-camel/src/main/scala/Producer.scala | 2 +- .../src/main/scala/actor/ActiveObject.scala | 4 +-- akka-core/src/main/scala/actor/Actor.scala | 24 +++++++------- .../src/main/scala/dispatch/Future.scala | 32 +++++++++---------- .../src/main/scala/dispatch/Reactor.scala | 2 +- .../src/main/scala/remote/RemoteClient.scala | 18 +++++------ .../src/main/scala/stm/DataFlowVariable.scala | 6 ++-- 7 files changed, 44 insertions(+), 44 deletions(-) diff --git a/akka-camel/src/main/scala/Producer.scala b/akka-camel/src/main/scala/Producer.scala index 2b7d053457..e793794804 100644 --- a/akka-camel/src/main/scala/Producer.scala +++ b/akka-camel/src/main/scala/Producer.scala @@ -162,7 +162,7 @@ trait Producer { self: Actor => */ class ProducerResponseSender( headers: Map[String, Any], - replyTo : Option[Either[Actor,CompletableFuture]], + replyTo : Option[Either[Actor,CompletableFuture[Any]]], producer: Actor) extends Synchronization with Logging { implicit val producerActor = Some(producer) // the response sender diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala index f3c536fdf8..839ebb1c3e 100644 --- a/akka-core/src/main/scala/actor/ActiveObject.scala +++ b/akka-core/src/main/scala/actor/ActiveObject.scala @@ -462,11 +462,11 @@ private[akka] sealed class ActiveObjectAspect { } } - private def getResultOrThrowException[T](future: Future): Option[T] = + private def getResultOrThrowException[T](future: Future[T]): Option[T] = if (future.exception.isDefined) { val (_, cause) = future.exception.get throw cause - } else future.result.asInstanceOf[Option[T]] + } else future.result private def isOneWay(rtti: MethodRtti) = rtti.getMethod.isAnnotationPresent(Annotations.oneway) diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index 37a297d5ca..8d46ff605d 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -257,7 +257,7 @@ trait Actor extends TransactionManagement with Logging { * Is Some(Left(Actor)) if sender is an actor * Is Some(Right(CompletableFuture)) if sender is holding on to a Future for the result */ - protected var replyTo: Option[Either[Actor,CompletableFuture]] = None + protected var replyTo: Option[Either[Actor,CompletableFuture[Any]]] = None // ==================================== // ==== USER CALLBACKS TO OVERRIDE ==== @@ -502,9 +502,9 @@ trait Actor extends TransactionManagement with Logging { def !![T](message: Any, timeout: Long): Option[T] = { if (_isKilled) throw new ActorKilledException("Actor [" + toString + "] has been killed, can't respond to messages") if (_isRunning) { - val future = postMessageToMailboxAndCreateFutureResultWithTimeout(message, timeout, None) + val future = postMessageToMailboxAndCreateFutureResultWithTimeout[T](message, timeout, None) val isActiveObject = message.isInstanceOf[Invocation] - if (isActiveObject && message.asInstanceOf[Invocation].isVoid) future.completeWithResult(None) + if (isActiveObject && message.asInstanceOf[Invocation].isVoid) future.asInstanceOf[CompletableFuture[Option[_]]].completeWithResult(None) try { future.await } catch { @@ -514,7 +514,7 @@ trait Actor extends TransactionManagement with Logging { } if (future.exception.isDefined) throw future.exception.get._2 - else future.result.asInstanceOf[Option[T]] + else future.result } else throw new IllegalStateException( "Actor has not been started, you need to invoke 'actor.start' before using it") @@ -539,10 +539,10 @@ trait Actor extends TransactionManagement with Logging { /** * FIXME document !!! */ - def !!!(message: Any): Future = { + def !!![T](message: Any): Future[T] = { if (_isKilled) throw new ActorKilledException("Actor [" + toString + "] has been killed, can't respond to messages") if (_isRunning) { - postMessageToMailboxAndCreateFutureResultWithTimeout(message, timeout, None) + postMessageToMailboxAndCreateFutureResultWithTimeout[T](message, timeout, None) } else throw new IllegalStateException( "Actor has not been started, you need to invoke 'actor.start' before using it") } @@ -569,7 +569,7 @@ trait Actor extends TransactionManagement with Logging { */ protected[this] def reply(message: Any) = replyTo match { case Some(Left(actor)) => actor ! message - case Some(Right(future)) => future.completeWithResult(message) + case Some(Right(future : Future[Any])) => future.completeWithResult(message) case _ => throw new IllegalStateException( "\n\tNo sender in scope, can't reply. " + "\n\tYou have probably used the '!' method to either; " + @@ -813,7 +813,7 @@ trait Actor extends TransactionManagement with Logging { RemoteServer.actorsFor(RemoteServer.Address(host, port)).actors.put(sender.get.getId, sender.get) } RemoteProtocolBuilder.setMessage(message, requestBuilder) - RemoteClient.clientFor(_remoteAddress.get).send(requestBuilder.build, None) + RemoteClient.clientFor(_remoteAddress.get).send[Any](requestBuilder.build, None) } else { val invocation = new MessageInvocation(this, message, sender.map(Left(_)), transactionSet.get) if (messageDispatcher.usesActorMailbox) { @@ -824,10 +824,10 @@ trait Actor extends TransactionManagement with Logging { } } - protected[akka] def postMessageToMailboxAndCreateFutureResultWithTimeout( + protected[akka] def postMessageToMailboxAndCreateFutureResultWithTimeout[T]( message: Any, timeout: Long, - senderFuture: Option[CompletableFuture]): CompletableFuture = { + senderFuture: Option[CompletableFuture[T]]): CompletableFuture[T] = { joinTransaction(message) if (_remoteAddress.isDefined) { @@ -847,8 +847,8 @@ trait Actor extends TransactionManagement with Logging { else throw new IllegalStateException("Expected a future from remote call to actor " + toString) } else { val future = if (senderFuture.isDefined) senderFuture.get - else new DefaultCompletableFuture(timeout) - val invocation = new MessageInvocation(this, message, Some(Right(future)), transactionSet.get) + else new DefaultCompletableFuture[T](timeout) + val invocation = new MessageInvocation(this, message, Some(Right(future.asInstanceOf[CompletableFuture[Any]])), transactionSet.get) if (messageDispatcher.usesActorMailbox) _mailbox.add(invocation) diff --git a/akka-core/src/main/scala/dispatch/Future.scala b/akka-core/src/main/scala/dispatch/Future.scala index 0bf9723e31..7e8fd5b087 100644 --- a/akka-core/src/main/scala/dispatch/Future.scala +++ b/akka-core/src/main/scala/dispatch/Future.scala @@ -20,8 +20,8 @@ object Futures { * } *
*/ - def future(timeout: Long)(body: => Any): Future = { - val promise = new DefaultCompletableFuture(timeout) + def future[T](timeout: Long)(body: => T): Future[T] = { + val promise = new DefaultCompletableFuture[T](timeout) try { promise completeWithResult body } catch { @@ -30,10 +30,10 @@ object Futures { promise } - def awaitAll(futures: List[Future]): Unit = futures.foreach(_.await) + def awaitAll(futures: List[Future[_]]): Unit = futures.foreach(_.await) - def awaitOne(futures: List[Future]): Future = { - var future: Option[Future] = None + def awaitOne(futures: List[Future[_]]): Future[_] = { + var future: Option[Future[_]] = None do { future = futures.find(_.isCompleted) } while (future.isEmpty) @@ -41,12 +41,12 @@ object Futures { } /* - def awaitEither(f1: Future, f2: Future): Option[Any] = { + def awaitEither[T](f1: Future[T], f2: Future[T]): Option[T] = { import Actor.Sender.Self import Actor.{spawn, actor} - case class Result(res: Option[Any]) - val handOff = new SynchronousQueue[Option[Any]] + case class Result(res: Option[T]) + val handOff = new SynchronousQueue[Option[T]] spawn { try { println("f1 await") @@ -70,23 +70,23 @@ object Futures { */ } -sealed trait Future { +sealed trait Future[T] { def await def awaitBlocking def isCompleted: Boolean def isExpired: Boolean def timeoutInNanos: Long - def result: Option[Any] + def result: Option[T] def exception: Option[Tuple2[AnyRef, Throwable]] } -trait CompletableFuture extends Future { - def completeWithResult(result: Any) +trait CompletableFuture[T] extends Future[T] { + def completeWithResult(result: T) def completeWithException(toBlame: AnyRef, exception: Throwable) } // Based on code from the actorom actor framework by Sergio Bossa [http://code.google.com/p/actorom/]. -class DefaultCompletableFuture(timeout: Long) extends CompletableFuture { +class DefaultCompletableFuture[T](timeout: Long) extends CompletableFuture[T] { private val TIME_UNIT = TimeUnit.MILLISECONDS def this() = this(0) @@ -95,7 +95,7 @@ class DefaultCompletableFuture(timeout: Long) extends CompletableFuture { private val _lock = new ReentrantLock private val _signal = _lock.newCondition private var _completed: Boolean = _ - private var _result: Option[Any] = None + private var _result: Option[T] = None private var _exception: Option[Tuple2[AnyRef, Throwable]] = None def await = try { @@ -138,7 +138,7 @@ class DefaultCompletableFuture(timeout: Long) extends CompletableFuture { _lock.unlock } - def result: Option[Any] = try { + def result: Option[T] = try { _lock.lock _result } finally { @@ -152,7 +152,7 @@ class DefaultCompletableFuture(timeout: Long) extends CompletableFuture { _lock.unlock } - def completeWithResult(result: Any) = try { + def completeWithResult(result: T) = try { _lock.lock if (!_completed) { _completed = true diff --git a/akka-core/src/main/scala/dispatch/Reactor.scala b/akka-core/src/main/scala/dispatch/Reactor.scala index 3f300b1c52..3eecbef0f3 100644 --- a/akka-core/src/main/scala/dispatch/Reactor.scala +++ b/akka-core/src/main/scala/dispatch/Reactor.scala @@ -15,7 +15,7 @@ import org.multiverse.commitbarriers.CountDownCommitBarrier final class MessageInvocation(val receiver: Actor, val message: Any, - val replyTo : Option[Either[Actor,CompletableFuture]], + val replyTo : Option[Either[Actor,CompletableFuture[Any]]], val transactionSet: Option[CountDownCommitBarrier]) { if (receiver eq null) throw new IllegalArgumentException("receiver is null") diff --git a/akka-core/src/main/scala/remote/RemoteClient.scala b/akka-core/src/main/scala/remote/RemoteClient.scala index 81d5591fbb..4676acc904 100644 --- a/akka-core/src/main/scala/remote/RemoteClient.scala +++ b/akka-core/src/main/scala/remote/RemoteClient.scala @@ -85,13 +85,13 @@ object RemoteClient extends Logging { requestBuilder.setSourcePort(port) } RemoteProtocolBuilder.setMessage(message, requestBuilder) - remoteClient.send(requestBuilder.build, None) + remoteClient.send[Any](requestBuilder.build, None) } - override def postMessageToMailboxAndCreateFutureResultWithTimeout( + override def postMessageToMailboxAndCreateFutureResultWithTimeout[T]( message: Any, timeout: Long, - senderFuture: Option[CompletableFuture]): CompletableFuture = { + senderFuture: Option[CompletableFuture[T]]): CompletableFuture[T] = { val requestBuilder = RemoteRequest.newBuilder .setId(RemoteRequestIdFactory.nextId) .setTarget(className) @@ -173,7 +173,7 @@ class RemoteClient(val hostname: String, val port: Int) extends Logging { val name = "RemoteClient@" + hostname + "::" + port @volatile private[remote] var isRunning = false - private val futures = new ConcurrentHashMap[Long, CompletableFuture] + private val futures = new ConcurrentHashMap[Long, CompletableFuture[_]] private val supervisors = new ConcurrentHashMap[String, Actor] private[remote] val listeners = new ConcurrentSkipListSet[Actor] @@ -217,14 +217,14 @@ class RemoteClient(val hostname: String, val port: Int) extends Logging { } } - def send(request: RemoteRequest, senderFuture: Option[CompletableFuture]): Option[CompletableFuture] = if (isRunning) { + def send[T](request: RemoteRequest, senderFuture: Option[CompletableFuture[T]]): Option[CompletableFuture[T]] = if (isRunning) { if (request.getIsOneWay) { connection.getChannel.write(request) None } else { futures.synchronized { val futureResult = if (senderFuture.isDefined) senderFuture.get - else new DefaultCompletableFuture(request.getTimeout) + else new DefaultCompletableFuture[T](request.getTimeout) futures.put(request.getId, futureResult) connection.getChannel.write(request) Some(futureResult) @@ -253,7 +253,7 @@ class RemoteClient(val hostname: String, val port: Int) extends Logging { * @author Jonas Bonér */ class RemoteClientPipelineFactory(name: String, - futures: ConcurrentMap[Long, CompletableFuture], + futures: ConcurrentMap[Long, CompletableFuture[_]], supervisors: ConcurrentMap[String, Actor], bootstrap: ClientBootstrap, remoteAddress: SocketAddress, @@ -284,7 +284,7 @@ class RemoteClientPipelineFactory(name: String, */ @ChannelHandler.Sharable class RemoteClientHandler(val name: String, - val futures: ConcurrentMap[Long, CompletableFuture], + val futures: ConcurrentMap[Long, CompletableFuture[_]], val supervisors: ConcurrentMap[String, Actor], val bootstrap: ClientBootstrap, val remoteAddress: SocketAddress, @@ -306,7 +306,7 @@ class RemoteClientHandler(val name: String, if (result.isInstanceOf[RemoteReply]) { val reply = result.asInstanceOf[RemoteReply] log.debug("Remote client received RemoteReply[\n%s]", reply.toString) - val future = futures.get(reply.getId) + val future : CompletableFuture[Any] = futures.get(reply.getId).asInstanceOf[CompletableFuture[Any]] if (reply.getIsSuccessful) { val message = RemoteProtocolBuilder.getMessage(reply) future.completeWithResult(message) diff --git a/akka-core/src/main/scala/stm/DataFlowVariable.scala b/akka-core/src/main/scala/stm/DataFlowVariable.scala index 7b2084aec6..332ae5c14e 100644 --- a/akka-core/src/main/scala/stm/DataFlowVariable.scala +++ b/akka-core/src/main/scala/stm/DataFlowVariable.scala @@ -80,7 +80,7 @@ import se.scalablesolutions.akka.dispatch.CompletableFuture private class Out[T <: Any](dataFlow: DataFlowVariable[T]) extends Actor { timeout = TIME_OUT start - private var readerFuture: Option[CompletableFuture] = None + private var readerFuture: Option[CompletableFuture[T]] = None def receive = { case Get => val ref = dataFlow.value.get @@ -88,11 +88,11 @@ import se.scalablesolutions.akka.dispatch.CompletableFuture reply(ref.get) else { readerFuture = replyTo match { - case Some(Right(future)) => Some(future) + case Some(Right(future)) => Some(future.asInstanceOf[CompletableFuture[T]]) case _ => None } } - case Set(v) => if (readerFuture.isDefined) readerFuture.get.completeWithResult(v) + case Set(v:T) => if (readerFuture.isDefined) readerFuture.get.completeWithResult(v) case Exit => exit } } From 71f766faa80d4563bfc0ba51d7489c5f42aa97ba Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Fri, 23 Apr 2010 21:28:45 +0200 Subject: [PATCH 37/41] Minor cleanup --- akka-core/src/main/scala/remote/RemoteClient.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-core/src/main/scala/remote/RemoteClient.scala b/akka-core/src/main/scala/remote/RemoteClient.scala index 4676acc904..2557c33d02 100644 --- a/akka-core/src/main/scala/remote/RemoteClient.scala +++ b/akka-core/src/main/scala/remote/RemoteClient.scala @@ -306,7 +306,7 @@ class RemoteClientHandler(val name: String, if (result.isInstanceOf[RemoteReply]) { val reply = result.asInstanceOf[RemoteReply] log.debug("Remote client received RemoteReply[\n%s]", reply.toString) - val future : CompletableFuture[Any] = futures.get(reply.getId).asInstanceOf[CompletableFuture[Any]] + val future = futures.get(reply.getId).asInstanceOf[CompletableFuture[Any]] if (reply.getIsSuccessful) { val message = RemoteProtocolBuilder.getMessage(reply) future.completeWithResult(message) From 4ffa75937beb3a5130cc537a3ac5223494b80123 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sat, 24 Apr 2010 14:56:50 +0200 Subject: [PATCH 38/41] Added Listeners to akka-patterns --- akka-patterns/src/main/scala/Patterns.scala | 18 +++++++++ .../src/test/scala/ActorPatternsTest.scala | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/akka-patterns/src/main/scala/Patterns.scala b/akka-patterns/src/main/scala/Patterns.scala index fc50292676..d8a49c74e3 100644 --- a/akka-patterns/src/main/scala/Patterns.scala +++ b/akka-patterns/src/main/scala/Patterns.scala @@ -85,3 +85,21 @@ class SmallestMailboxFirstIterator(items : List[Actor]) extends InfiniteIterator items.reduceLeft((actor1, actor2) => actorWithSmallestMailbox(actor1,actor2)) } } + +sealed trait ListenerMessage +case class Listen(listener : Actor) extends ListenerMessage +case class Deafen(listener : Actor) extends ListenerMessage +case class WithListeners(f : Set[Actor] => Unit) extends ListenerMessage + +trait Listeners { self : Actor => + import se.scalablesolutions.akka.actor.Agent + private lazy val listeners = Agent(Set[Actor]()) + + protected def listenerManagement : PartialFunction[Any,Unit] = { + case Listen(l) => listeners( _ + l) + case Deafen(l) => listeners( _ - l ) + case WithListeners(f) => listeners foreach f + } + + protected def gossip(msg : Any) = listeners foreach ( _ foreach ( _ ! msg ) ) +} \ No newline at end of file diff --git a/akka-patterns/src/test/scala/ActorPatternsTest.scala b/akka-patterns/src/test/scala/ActorPatternsTest.scala index 0ce999add0..2235b1a1a7 100644 --- a/akka-patterns/src/test/scala/ActorPatternsTest.scala +++ b/akka-patterns/src/test/scala/ActorPatternsTest.scala @@ -90,6 +90,44 @@ class ActorPatternsTest extends junit.framework.TestCase with Suite with MustMat } } }) + + @Test def testListener = verify(new TestActor { + import java.util.concurrent.{ CountDownLatch, TimeUnit } + + def test = { + val latch = new CountDownLatch(2) + val num = new AtomicInteger(0) + val i = new Actor with Listeners { + def receive = listenerManagement orElse { + case "foo" => gossip("bar") + } + } + i.start + + def newListener = actor { + case "bar" => + num.incrementAndGet + latch.countDown + } + + val a1 = newListener + val a2 = newListener + val a3 = newListener + + handle(i,a1,a2,a3) { + i ! Listen(a1) + i ! Listen(a2) + i ! Listen(a3) + i ! Deafen(a3) + + i ! "foo" + + val done = latch.await(5,TimeUnit.SECONDS) + done must be (true) + num.get must be (2) + } + } + }); } From 12d3ce354586546abe0c592445d3e32dbdb91d5d Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sat, 24 Apr 2010 14:57:32 +0200 Subject: [PATCH 39/41] Added reply_? that discards messages if it cannot find reply target --- akka-core/src/main/scala/actor/Actor.scala | 23 ++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/akka-core/src/main/scala/actor/Actor.scala b/akka-core/src/main/scala/actor/Actor.scala index 37a297d5ca..7947848634 100644 --- a/akka-core/src/main/scala/actor/Actor.scala +++ b/akka-core/src/main/scala/actor/Actor.scala @@ -566,11 +566,9 @@ trait Actor extends TransactionManagement with Logging { /** * Use reply(..) to reply with a message to the original sender of the message currently * being processed. + * Throws an IllegalStateException if unable to determine what to reply to */ - protected[this] def reply(message: Any) = replyTo match { - case Some(Left(actor)) => actor ! message - case Some(Right(future)) => future.completeWithResult(message) - case _ => throw new IllegalStateException( + protected[this] def reply(message: Any) = if(!reply_?(message)) throw new IllegalStateException( "\n\tNo sender in scope, can't reply. " + "\n\tYou have probably used the '!' method to either; " + "\n\t\t1. Send a message to a remote actor which does not have a contact address." + @@ -579,6 +577,23 @@ trait Actor extends TransactionManagement with Logging { "\n\tIf so, switch to '!!' (or remove '@oneway') which passes on an implicit future" + "\n\tthat will be bound by the argument passed to 'reply'." + "\n\tAlternatively, you can use setReplyToAddress to make sure the actor can be contacted over the network.") + + /** + * Use reply_?(..) to reply with a message to the original sender of the message currently + * being processed. + * Returns true if reply was sent, and false if unable to determine what to reply to. + */ + protected[this] def reply_?(message: Any) : Boolean = replyTo match { + case Some(Left(actor)) => + actor ! message + true + + case Some(Right(future)) => + future.completeWithResult(message) + true + + case _ => + false } /** From 716229c8b85b34ce8b7bbd28e50d30fe2545f472 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 25 Apr 2010 18:07:27 +0200 Subject: [PATCH 40/41] Added Future[T] as return type for await and awaitBlocking --- akka-core/src/main/scala/dispatch/Future.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/akka-core/src/main/scala/dispatch/Future.scala b/akka-core/src/main/scala/dispatch/Future.scala index 7e8fd5b087..7fe0c4ab6a 100644 --- a/akka-core/src/main/scala/dispatch/Future.scala +++ b/akka-core/src/main/scala/dispatch/Future.scala @@ -71,8 +71,8 @@ object Futures { } sealed trait Future[T] { - def await - def awaitBlocking + def await : Future[T] + def awaitBlocking : Future[T] def isCompleted: Boolean def isExpired: Boolean def timeoutInNanos: Long @@ -111,6 +111,7 @@ class DefaultCompletableFuture[T](timeout: Long) extends CompletableFuture[T] { wait = wait - (currentTimeInNanos - start) } } + this } finally { _lock.unlock } @@ -120,6 +121,7 @@ class DefaultCompletableFuture[T](timeout: Long) extends CompletableFuture[T] { while (!_completed) { _signal.await } + this } finally { _lock.unlock } From 2fb619bffeba4213bc114be4ad695ae239100b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bon=C3=A9r?= Date: Mon, 26 Apr 2010 08:25:52 +0200 Subject: [PATCH 41/41] Made ActiveObject non-advisable in AW terms --- akka-core/src/main/scala/actor/ActiveObject.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/akka-core/src/main/scala/actor/ActiveObject.scala b/akka-core/src/main/scala/actor/ActiveObject.scala index 839ebb1c3e..f80dd2db42 100644 --- a/akka-core/src/main/scala/actor/ActiveObject.scala +++ b/akka-core/src/main/scala/actor/ActiveObject.scala @@ -271,7 +271,7 @@ object ActiveObject { } private[akka] def newInstance[T](target: Class[T], actor: Dispatcher, remoteAddress: Option[InetSocketAddress], timeout: Long): T = { - val proxy = Proxy.newInstance(target, false, true) + val proxy = Proxy.newInstance(target, false, false) actor.initialize(target, proxy) actor.timeout = timeout if (remoteAddress.isDefined) { @@ -283,7 +283,7 @@ object ActiveObject { } private[akka] def newInstance[T](intf: Class[T], target: AnyRef, actor: Dispatcher, remoteAddress: Option[InetSocketAddress], timeout: Long): T = { - val proxy = Proxy.newInstance(Array(intf), Array(target), false, true) + val proxy = Proxy.newInstance(Array(intf), Array(target), false, false) actor.initialize(target.getClass, target) actor.timeout = timeout if (remoteAddress.isDefined) {