From 60d4010421d73d241f809974ab6d3423facf61f1 Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Mon, 6 Sep 2010 10:15:44 +0200 Subject: [PATCH 01/25] started working on ticket 194 --- .../src/main/scala/actor/ActorRef.scala | 7 +- .../src/main/scala/remote/RemoteClient.scala | 16 +- .../src/main/scala/remote/RemoteServer.scala | 44 ++++- .../akka/actor/RemoteTypedActorOneImpl.java | 3 + .../src/test/resources/META-INF/aop.xml | 1 + .../ServerInitiatedRemoteTypedActorSpec.scala | 86 +++++++++ .../src/main/scala/actor/TypedActor.scala | 176 +++++++++++++++--- .../typed-actor/TypedActorLifecycleSpec.scala | 1 + 8 files changed, 299 insertions(+), 35 deletions(-) create mode 100644 akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index a6b42db579..c712a8d408 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -1362,7 +1362,8 @@ private[akka] case class RemoteActorRef private[akka] ( val hostname: String, val port: Int, _timeout: Long, - loader: Option[ClassLoader]) + loader: Option[ClassLoader], + val actorType: ActorType = ActorType.ScalaActor ) extends ActorRef with ScalaActorRef { ensureRemotingEnabled @@ -1375,7 +1376,7 @@ private[akka] case class RemoteActorRef private[akka] ( def postMessageToMailbox(message: Any, senderOption: Option[ActorRef]): Unit = RemoteClientModule.send[Any]( - message, senderOption, None, remoteAddress.get, timeout, true, this, None, ActorType.ScalaActor) + message, senderOption, None, remoteAddress.get, timeout, true, this, None, actorType) def postMessageToMailboxAndCreateFutureResultWithTimeout[T]( message: Any, @@ -1383,7 +1384,7 @@ private[akka] case class RemoteActorRef private[akka] ( senderOption: Option[ActorRef], senderFuture: Option[CompletableFuture[T]]): CompletableFuture[T] = { val future = RemoteClientModule.send[T]( - message, senderOption, senderFuture, remoteAddress.get, timeout, false, this, None, ActorType.ScalaActor) + message, senderOption, senderFuture, remoteAddress.get, timeout, false, this, None, actorType) if (future.isDefined) future.get else throw new IllegalActorStateException("Expected a future from remote call to actor " + toString) } diff --git a/akka-remote/src/main/scala/remote/RemoteClient.scala b/akka-remote/src/main/scala/remote/RemoteClient.scala index f61a5d63a1..689f55a049 100644 --- a/akka-remote/src/main/scala/remote/RemoteClient.scala +++ b/akka-remote/src/main/scala/remote/RemoteClient.scala @@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicLong import scala.collection.mutable.{HashSet, HashMap} import scala.reflect.BeanProperty +import se.scalablesolutions.akka.actor._ /** * Atomic remote request/reply message id generator. @@ -76,11 +77,19 @@ object RemoteClient extends Logging { private val remoteClients = new HashMap[String, RemoteClient] private val remoteActors = new HashMap[RemoteServer.Address, HashSet[String]] - // FIXME: simplify overloaded methods when we have Scala 2.8 - def actorFor(classNameOrServiceId: String, hostname: String, port: Int): ActorRef = actorFor(classNameOrServiceId, classNameOrServiceId, 5000L, hostname, port, None) + // FIXME: + def typedActorFor[T](intfClass: Class[T], serviceId: String, implClassName: String, timeout: Long, hostname: String, port: Int) : T = { + + println("### create RemoteActorRef") + val actorRef = RemoteActorRef(serviceId, implClassName, hostname, port, timeout, None, ActorType.TypedActor) + val proxy = TypedActor.createProxyForRemoteActorRef(intfClass, actorRef) + proxy + + } + def actorFor(classNameOrServiceId: String, hostname: String, port: Int, loader: ClassLoader): ActorRef = actorFor(classNameOrServiceId, classNameOrServiceId, 5000L, hostname, port, Some(loader)) @@ -99,6 +108,9 @@ object RemoteClient extends Logging { def actorFor(serviceId: String, className: String, timeout: Long, hostname: String, port: Int): ActorRef = RemoteActorRef(serviceId, className, hostname, port, timeout, None) + + + private[akka] def actorFor(serviceId: String, className: String, timeout: Long, hostname: String, port: Int, loader: ClassLoader): ActorRef = RemoteActorRef(serviceId, className, hostname, port, timeout, Some(loader)) diff --git a/akka-remote/src/main/scala/remote/RemoteServer.scala b/akka-remote/src/main/scala/remote/RemoteServer.scala index 5f24def4f5..af1507c3cb 100644 --- a/akka-remote/src/main/scala/remote/RemoteServer.scala +++ b/akka-remote/src/main/scala/remote/RemoteServer.scala @@ -120,9 +120,12 @@ object RemoteServer { } } - private class RemoteActorSet { - private[RemoteServer] val actors = new ConcurrentHashMap[String, ActorRef] - private[RemoteServer] val typedActors = new ConcurrentHashMap[String, AnyRef] + // FIXME private + class RemoteActorSet { + //private[RemoteServer] val actors = new ConcurrentHashMap[String, ActorRef] + val actors = new ConcurrentHashMap[String, ActorRef] + //private[RemoteServer] val typedActors = new ConcurrentHashMap[String, AnyRef] + val typedActors = new ConcurrentHashMap[String, AnyRef] } private val guard = new ReadWriteGuard @@ -130,11 +133,14 @@ object RemoteServer { private val remoteServers = Map[Address, RemoteServer]() private[akka] def registerActor(address: InetSocketAddress, uuid: String, actor: ActorRef) = guard.withWriteGuard { - actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).actors.put(uuid, actor) + // FIXME + //actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).actors.put(uuid, actor) + val actors = actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).actors + actors.put(uuid, actor) } - private[akka] def registerTypedActor(address: InetSocketAddress, name: String, typedActor: AnyRef) = guard.withWriteGuard { - actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).typedActors.put(name, typedActor) + private[akka] def registerTypedActor(address: InetSocketAddress, uuid: String, typedActor: TypedActor) = guard.withWriteGuard { + actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).typedActors.put(uuid, typedActor) } private[akka] def getOrCreateServer(address: InetSocketAddress): RemoteServer = guard.withWriteGuard { @@ -159,7 +165,9 @@ object RemoteServer { remoteServers.remove(Address(hostname, port)) } - private def actorsFor(remoteServerAddress: RemoteServer.Address): RemoteActorSet = { + // FIXME + def actorsFor(remoteServerAddress: RemoteServer.Address): RemoteActorSet = { + println("##### actorsFor SIZE=" + remoteActorSets.size) remoteActorSets.getOrElseUpdate(remoteServerAddress,new RemoteActorSet) } } @@ -195,7 +203,7 @@ class RemoteServer extends Logging with ListenerManagement { private[akka] var hostname = RemoteServer.HOSTNAME private[akka] var port = RemoteServer.PORT - + @volatile private var _isRunning = false private val factory = new NioServerSocketChannelFactory( @@ -270,7 +278,22 @@ class RemoteServer extends Logging with ListenerManagement { } } - // TODO: register typed actor in RemoteServer as well + // FIXME: register typed actor in RemoteServer as well + def registerTypedActor(id: String, actorRef: AnyRef): Unit = synchronized { + val typedActors = RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).typedActors + if (!typedActors.contains(id)) { + log.debug("Registering server side remote actor [%s] with id [%s] on [%s:%d]", actorRef.getClass.getName, id, hostname, port) + typedActors.put(id, actorRef) + } + } + + private def actors() : ConcurrentHashMap[String, ActorRef] = { + RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).actors + } + + private def typedActors() : ConcurrentHashMap[String, AnyRef] = { + RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).typedActors + } /** * Register Remote Actor by the Actor's 'id' field. It starts the Actor if it is not started already. @@ -285,6 +308,7 @@ class RemoteServer extends Logging with ListenerManagement { def register(id: String, actorRef: ActorRef): Unit = synchronized { if (_isRunning) { val actors = RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).actors + println("___ register ___ " + actors.hashCode + " hostname=" + hostname + " port="+ port) if (!actors.contains(id)) { if (!actorRef.isRunning) actorRef.start log.debug("Registering server side remote actor [%s] with id [%s]", actorRef.actorClass.getName, id) @@ -320,6 +344,8 @@ class RemoteServer extends Logging with ListenerManagement { } } + //FIXME: unregister typed Actor + protected override def manageLifeCycleOfListeners = false protected[akka] override def foreachListener(f: (ActorRef) => Unit): Unit = super.foreachListener(f) diff --git a/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java b/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java index 715e5366a4..a30aa26124 100644 --- a/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java +++ b/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java @@ -18,9 +18,12 @@ public class RemoteTypedActorOneImpl extends TypedActor implements RemoteTypedAc } public void oneWay() throws Exception { + System.out.println("--------> got it!!!!!!"); RemoteTypedActorLog.oneWayLog().put("oneway"); } + + @Override public void preRestart(Throwable e) { try { RemoteTypedActorLog.messageLog().put(e.getMessage()); } catch(Exception ex) {} diff --git a/akka-remote/src/test/resources/META-INF/aop.xml b/akka-remote/src/test/resources/META-INF/aop.xml index bdc167ca54..be133a51b8 100644 --- a/akka-remote/src/test/resources/META-INF/aop.xml +++ b/akka-remote/src/test/resources/META-INF/aop.xml @@ -2,6 +2,7 @@ + diff --git a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala new file mode 100644 index 0000000000..76c50f7658 --- /dev/null +++ b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ + +package se.scalablesolutions.akka.actor.remote + +import java.util.concurrent.{CountDownLatch, TimeUnit} +import org.scalatest.junit.JUnitSuite +import org.junit.{Test, Before, After} + +import se.scalablesolutions.akka.remote.{RemoteServer, RemoteClient} +import se.scalablesolutions.akka.actor._ + +object ServerInitiatedRemoteTypedActorSpec { + val HOSTNAME = "localhost" + val PORT = 9990 + var server: RemoteServer = null + + class SimpleActor extends Actor { + def receive = { + case _ => println("received message") + } + } + + +} + +class ServerInitiatedRemoteTypedActorSpec extends JUnitSuite { + import ServerInitiatedRemoteTypedActorSpec._ + private val unit = TimeUnit.MILLISECONDS + + + @Before + def init { + server = new RemoteServer() + server.start(HOSTNAME, PORT) + Thread.sleep(1000) + } + + // make sure the servers shutdown cleanly after the test has finished + @After + def finished { + try { + server.shutdown + RemoteClient.shutdownAll + Thread.sleep(1000) + } catch { + case e => () + } + } + + + @Test + def shouldSendWithBang { + + /* + val clientManangedTypedActor = TypedActor.newRemoteInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000, HOSTNAME, PORT) + clientManangedTypedActor.requestReply("test-string") + Thread.sleep(2000) + println("###########") + */ + /* + trace() + val actor = Actor.actorOf[SimpleActor].start + server.register("simple-actor", actor) + val typedActor = TypedActor.newInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000) + server.registerTypedActor("typed-actor-service", typedActor) + println("### registered actor") + trace() + + //val actorRef = RemoteActorRef("typed-actor-service", classOf[RemoteTypedActorOneImpl].getName, HOSTNAME, PORT, 5000L, None) + //val myActor = TypedActor.createProxyForRemoteActorRef(classOf[RemoteTypedActorOne], actorRef) + val myActor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", classOf[RemoteTypedActorOneImpl].getName, 5000L, HOSTNAME, PORT) + println("### call one way") + myActor.oneWay() + Thread.sleep(3000) + println("### call one way - done") + */ + //assert(RemoteActorSpecActorUnidirectional.latch.await(1, TimeUnit.SECONDS)) + //actor.stop + /* */ + } + + +} + diff --git a/akka-typed-actor/src/main/scala/actor/TypedActor.scala b/akka-typed-actor/src/main/scala/actor/TypedActor.scala index b27f5b4b4d..7f0ee5dbbc 100644 --- a/akka-typed-actor/src/main/scala/actor/TypedActor.scala +++ b/akka-typed-actor/src/main/scala/actor/TypedActor.scala @@ -16,9 +16,8 @@ import org.codehaus.aspectwerkz.proxy.Proxy import org.codehaus.aspectwerkz.annotation.{Aspect, Around} import java.net.InetSocketAddress -import java.lang.reflect.{InvocationTargetException, Method, Field} - import scala.reflect.BeanProperty +import java.lang.reflect.{Method, Field, InvocationHandler, Proxy => JProxy} /** * TypedActor is a type-safe actor made out of a POJO with interface. @@ -183,6 +182,7 @@ abstract class TypedActor extends Actor with Proxyable { case Link(proxy) => self.link(proxy) case Unlink(proxy) => self.unlink(proxy) + case method: String => println("### got method") case unexpected => throw new IllegalActorStateException( "Unexpected message [" + unexpected + "] sent to [" + this + "]") } @@ -408,24 +408,47 @@ object TypedActor extends Logging { proxy.asInstanceOf[T] } -/* - // NOTE: currently not used - but keep it around - private[akka] def newInstance[T <: TypedActor](targetClass: Class[T], - remoteAddress: Option[InetSocketAddress], timeout: Long): T = { - val proxy = { - val instance = Proxy.newInstance(targetClass, true, false) - if (instance.isInstanceOf[TypedActor]) instance.asInstanceOf[TypedActor] - else throw new IllegalActorStateException("Actor [" + targetClass.getName + "] is not a sub class of 'TypedActor'") + // FIXME + def createProxyForRemoteActorRef[T](intfClass: Class[T], actorRef: ActorRef): T = { + + class MyInvocationHandler extends InvocationHandler { + def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]): AnyRef = { + // do nothing, this is just a dummy + null + } } - val context = injectTypedActorContext(proxy) - actorRef.actor.asInstanceOf[Dispatcher].initialize(targetClass, proxy, proxy, context) - actorRef.timeout = timeout - if (remoteAddress.isDefined) actorRef.makeRemote(remoteAddress.get) - AspectInitRegistry.register(proxy, AspectInit(targetClass, proxy, actorRef, remoteAddress, timeout)) - actorRef.start - proxy.asInstanceOf[T] + val handler = new MyInvocationHandler() + + val interfaces = Array(intfClass, classOf[ServerManagedTypedActor]).asInstanceOf[Array[java.lang.Class[_]]] + val jProxy = JProxy.newProxyInstance(intfClass.getClassLoader(), interfaces, handler) + val awProxy = Proxy.newInstance(interfaces, Array(jProxy, jProxy), true, false) + + // TODO: needed? + //typedActor.initialize(proxy) + // TODO: timeout?? + AspectInitRegistry.register(awProxy, AspectInit(intfClass, null, actorRef, None, 5000L)) + awProxy.asInstanceOf[T] } -*/ + + + /* + // NOTE: currently not used - but keep it around + private[akka] def newInstance[T <: TypedActor](targetClass: Class[T], + remoteAddress: Option[InetSocketAddress], timeout: Long): T = { + val proxy = { + val instance = Proxy.newInstance(targetClass, true, false) + if (instance.isInstanceOf[TypedActor]) instance.asInstanceOf[TypedActor] + else throw new IllegalActorStateException("Actor [" + targetClass.getName + "] is not a sub class of 'TypedActor'") + } + val context = injectTypedActorContext(proxy) + actorRef.actor.asInstanceOf[Dispatcher].initialize(targetClass, proxy, proxy, context) + actorRef.timeout = timeout + if (remoteAddress.isDefined) actorRef.makeRemote(remoteAddress.get) + AspectInitRegistry.register(proxy, AspectInit(targetClass, proxy, actorRef, remoteAddress, timeout)) + actorRef.start + proxy.asInstanceOf[T] + } + */ /** * Stops the current Typed Actor. @@ -546,6 +569,114 @@ object TypedActor extends Logging { private[akka] def isJoinPoint(message: Any): Boolean = message.isInstanceOf[JoinPoint] } +/** + * AspectWerkz Aspect that is turning POJO into proxy to a server managed remote TypedActor. + *

+ * Is deployed on a 'perInstance' basis with the pointcut 'execution(* *.*(..))', + * e.g. all methods on the instance. + * + * @author Jonas Bonér + */ +@Aspect("perInstance") +private[akka] sealed class ServerManagedTypedActorAspect { + @volatile private var isInitialized = false + @volatile private var isStopped = false + private var interfaceClass: Class[_] = _ + private var actorRef: ActorRef = _ + private var timeout: Long = _ + private var uuid: String = _ + private var remoteAddress: Option[InetSocketAddress] = _ + + //FIXME + + @Around("execution(* *.*(..)) && this(se.scalablesolutions.akka.actor.ServerManagedTypedActor)") + def invoke(joinPoint: JoinPoint): AnyRef = { + println("### MyAspect intercepted " + joinPoint.getSignature) + if (!isInitialized) initialize(joinPoint) + remoteDispatch(joinPoint) + } + + + private def remoteDispatch(joinPoint: JoinPoint): AnyRef = { + val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] + val isOneWay = TypedActor.isOneWay(methodRtti) + + val (message: Array[AnyRef], isEscaped) = escapeArguments(methodRtti.getParameterValues) + + println("### remote dispatch...") + + val future = RemoteClientModule.send[AnyRef]( + message, None, None, remoteAddress.get, + timeout, isOneWay, actorRef, + Some((interfaceClass.getName, methodRtti.getMethod.getName)), + ActorType.TypedActor) + + if (isOneWay) null // for void methods + else { + if (future.isDefined) { + future.get.await + val result = getResultOrThrowException(future.get) + if (result.isDefined) result.get + else throw new IllegalActorStateException("No result returned from call to [" + joinPoint + "]") + } else throw new IllegalActorStateException("No future returned from call to [" + joinPoint + "]") + } + } + + /* + private def remoteDispatch(joinPoint: JoinPoint): AnyRef = { + val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] + val isOneWay = TypedActor.isOneWay(methodRtti) + val senderActorRef = Some(SenderContextInfo.senderActorRef.value) + + + if (!actorRef.isRunning && !isStopped) { + isStopped = true + // FIXME - what to do? + // joinPoint.proceed + null + } else if (isOneWay) { + actorRef.!("joinPoint") + //actorRef.!(joinPoint)(senderActorRef) + null.asInstanceOf[AnyRef] + } else if (TypedActor.returnsFuture_?(methodRtti)) { + actorRef.!!!(joinPoint, timeout)(senderActorRef) + } else { + val result = (actorRef.!!(joinPoint, timeout)(senderActorRef)).as[AnyRef] + if (result.isDefined) result.get + else throw new ActorTimeoutException("Invocation to [" + joinPoint + "] timed out.") + } + } + */ + private def getResultOrThrowException[T](future: Future[T]): Option[T] = + if (future.exception.isDefined) throw future.exception.get + else future.result + + private def escapeArguments(args: Array[AnyRef]): Tuple2[Array[AnyRef], Boolean] = { + var isEscaped = false + val escapedArgs = for (arg <- args) yield { + val clazz = arg.getClass + if (clazz.getName.contains(TypedActor.AW_PROXY_PREFIX)) { + isEscaped = true + TypedActor.AW_PROXY_PREFIX + clazz.getSuperclass.getName + } else arg + } + (escapedArgs, isEscaped) + } + + + private def initialize(joinPoint: JoinPoint): Unit = { + val init = AspectInitRegistry.initFor(joinPoint.getThis) + interfaceClass = init.interfaceClass + actorRef = init.actorRef + uuid = actorRef.uuid + remoteAddress = actorRef.remoteAddress + println("### address= " + remoteAddress.get) + timeout = init.timeout + isInitialized = true + } + +} + /** * AspectWerkz Aspect that is turning POJO into TypedActor. *

@@ -564,9 +695,8 @@ private[akka] sealed class TypedActorAspect { private var remoteAddress: Option[InetSocketAddress] = _ private var timeout: Long = _ private var uuid: String = _ - @volatile private var instance: TypedActor = _ - - @Around("execution(* *.*(..))") + + @Around("execution(* *.*(..)) && !this(se.scalablesolutions.akka.actor.ServerManagedTypedActor)") def invoke(joinPoint: JoinPoint): AnyRef = { if (!isInitialized) initialize(joinPoint) dispatch(joinPoint) @@ -653,6 +783,7 @@ private[akka] sealed class TypedActorAspect { } } + /** * Internal helper class to help pass the contextual information between threads. * @@ -704,5 +835,8 @@ private[akka] sealed case class AspectInit( val timeout: Long) { def this(interfaceClass: Class[_], targetInstance: TypedActor, actorRef: ActorRef, timeout: Long) = this(interfaceClass, targetInstance, actorRef, None, timeout) + } + +private[akka] sealed trait ServerManagedTypedActor extends TypedActor \ No newline at end of file diff --git a/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala b/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala index 10fc40493b..1fcbe0c5ef 100644 --- a/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala +++ b/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala @@ -45,6 +45,7 @@ class TypedActorLifecycleSpec extends Spec with ShouldMatchers with BeforeAndAft fail("expected exception not thrown") } catch { case e: RuntimeException => { + println("#failed") cdl.await assert(SamplePojoImpl._pre) assert(SamplePojoImpl._post) From ec61c29f21672f17cd30292192cbbee0d2ea8aae Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Mon, 6 Sep 2010 16:33:55 +0200 Subject: [PATCH 02/25] implemented server managed typed actor --- .../src/main/scala/actor/ActorRef.scala | 2 +- .../src/main/scala/remote/RemoteClient.scala | 28 ++-- .../src/main/scala/remote/RemoteServer.scala | 60 ++++--- .../akka/actor/RemoteTypedActorOneImpl.java | 3 - .../ServerInitiatedRemoteActorSpec.scala | 1 + .../ServerInitiatedRemoteTypedActorSpec.scala | 116 ++++++++------ .../src/main/scala/actor/TypedActor.scala | 146 +++++------------- .../typed-actor/TypedActorLifecycleSpec.scala | 1 - 8 files changed, 155 insertions(+), 202 deletions(-) diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index c712a8d408..5da69ea7c2 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -1363,7 +1363,7 @@ private[akka] case class RemoteActorRef private[akka] ( val port: Int, _timeout: Long, loader: Option[ClassLoader], - val actorType: ActorType = ActorType.ScalaActor ) + val actorType: ActorType = ActorType.ScalaActor) extends ActorRef with ScalaActorRef { ensureRemotingEnabled diff --git a/akka-remote/src/main/scala/remote/RemoteClient.scala b/akka-remote/src/main/scala/remote/RemoteClient.scala index 689f55a049..dc3cf6b146 100644 --- a/akka-remote/src/main/scala/remote/RemoteClient.scala +++ b/akka-remote/src/main/scala/remote/RemoteClient.scala @@ -80,16 +80,6 @@ object RemoteClient extends Logging { def actorFor(classNameOrServiceId: String, hostname: String, port: Int): ActorRef = actorFor(classNameOrServiceId, classNameOrServiceId, 5000L, hostname, port, None) - // FIXME: - def typedActorFor[T](intfClass: Class[T], serviceId: String, implClassName: String, timeout: Long, hostname: String, port: Int) : T = { - - println("### create RemoteActorRef") - val actorRef = RemoteActorRef(serviceId, implClassName, hostname, port, timeout, None, ActorType.TypedActor) - val proxy = TypedActor.createProxyForRemoteActorRef(intfClass, actorRef) - proxy - - } - def actorFor(classNameOrServiceId: String, hostname: String, port: Int, loader: ClassLoader): ActorRef = actorFor(classNameOrServiceId, classNameOrServiceId, 5000L, hostname, port, Some(loader)) @@ -108,8 +98,26 @@ object RemoteClient extends Logging { def actorFor(serviceId: String, className: String, timeout: Long, hostname: String, port: Int): ActorRef = RemoteActorRef(serviceId, className, hostname, port, timeout, None) + def typedActorFor[T](intfClass: Class[T], serviceIdOrClassName: String, hostname: String, port: Int) : T = { + typedActorFor(intfClass, serviceIdOrClassName, serviceIdOrClassName, 5000L, hostname, port, None) + } + def typedActorFor[T](intfClass: Class[T], serviceIdOrClassName: String, timeout: Long, hostname: String, port: Int) : T = { + typedActorFor(intfClass, serviceIdOrClassName, serviceIdOrClassName, timeout, hostname, port, None) + } + def typedActorFor[T](intfClass: Class[T], serviceIdOrClassName: String, timeout: Long, hostname: String, port: Int, loader: ClassLoader) : T = { + typedActorFor(intfClass, serviceIdOrClassName, serviceIdOrClassName, timeout, hostname, port, Some(loader)) + } + + def typedActorFor[T](intfClass: Class[T], serviceId: String, implClassName: String, timeout: Long, hostname: String, port: Int, loader: ClassLoader) : T = { + typedActorFor(intfClass, serviceId, implClassName, timeout, hostname, port, Some(loader)) + } + + private[akka] def typedActorFor[T](intfClass: Class[T], serviceId: String, implClassName: String, timeout: Long, hostname: String, port: Int, loader: Option[ClassLoader]) : T = { + val actorRef = RemoteActorRef(serviceId, implClassName, hostname, port, timeout, loader, ActorType.TypedActor) + TypedActor.createProxyForRemoteActorRef(intfClass, actorRef) + } private[akka] def actorFor(serviceId: String, className: String, timeout: Long, hostname: String, port: Int, loader: ClassLoader): ActorRef = RemoteActorRef(serviceId, className, hostname, port, timeout, Some(loader)) diff --git a/akka-remote/src/main/scala/remote/RemoteServer.scala b/akka-remote/src/main/scala/remote/RemoteServer.scala index af1507c3cb..5a7fbd00d9 100644 --- a/akka-remote/src/main/scala/remote/RemoteServer.scala +++ b/akka-remote/src/main/scala/remote/RemoteServer.scala @@ -10,7 +10,7 @@ import java.util.concurrent.{ConcurrentHashMap, Executors} import java.util.{Map => JMap} import se.scalablesolutions.akka.actor.{ - Actor, TypedActor, ActorRef, LocalActorRef, RemoteActorRef, IllegalActorStateException, RemoteActorSystemMessage} + Actor, TypedActor, ActorRef, IllegalActorStateException, RemoteActorSystemMessage} import se.scalablesolutions.akka.actor.Actor._ import se.scalablesolutions.akka.util._ import se.scalablesolutions.akka.remote.protocol.RemoteProtocol._ @@ -120,12 +120,9 @@ object RemoteServer { } } - // FIXME private - class RemoteActorSet { - //private[RemoteServer] val actors = new ConcurrentHashMap[String, ActorRef] - val actors = new ConcurrentHashMap[String, ActorRef] - //private[RemoteServer] val typedActors = new ConcurrentHashMap[String, AnyRef] - val typedActors = new ConcurrentHashMap[String, AnyRef] + private class RemoteActorSet { + private[RemoteServer] val actors = new ConcurrentHashMap[String, ActorRef] + private[RemoteServer] val typedActors = new ConcurrentHashMap[String, AnyRef] } private val guard = new ReadWriteGuard @@ -133,13 +130,10 @@ object RemoteServer { private val remoteServers = Map[Address, RemoteServer]() private[akka] def registerActor(address: InetSocketAddress, uuid: String, actor: ActorRef) = guard.withWriteGuard { - // FIXME - //actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).actors.put(uuid, actor) - val actors = actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).actors - actors.put(uuid, actor) + actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).actors.put(uuid, actor) } - private[akka] def registerTypedActor(address: InetSocketAddress, uuid: String, typedActor: TypedActor) = guard.withWriteGuard { + private[akka] def registerTypedActor(address: InetSocketAddress, uuid: String, typedActor: AnyRef) = guard.withWriteGuard { actorsFor(RemoteServer.Address(address.getHostName, address.getPort)).typedActors.put(uuid, typedActor) } @@ -165,9 +159,7 @@ object RemoteServer { remoteServers.remove(Address(hostname, port)) } - // FIXME - def actorsFor(remoteServerAddress: RemoteServer.Address): RemoteActorSet = { - println("##### actorsFor SIZE=" + remoteActorSets.size) + private def actorsFor(remoteServerAddress: RemoteServer.Address): RemoteActorSet = { remoteActorSets.getOrElseUpdate(remoteServerAddress,new RemoteActorSet) } } @@ -278,23 +270,19 @@ class RemoteServer extends Logging with ListenerManagement { } } - // FIXME: register typed actor in RemoteServer as well - def registerTypedActor(id: String, actorRef: AnyRef): Unit = synchronized { + /** + * Register remote typed actor by a specific id. + * @param id custom actor id + * @param typedActor typed actor to register + */ + def registerTypedActor(id: String, typedActor: AnyRef): Unit = synchronized { val typedActors = RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).typedActors if (!typedActors.contains(id)) { - log.debug("Registering server side remote actor [%s] with id [%s] on [%s:%d]", actorRef.getClass.getName, id, hostname, port) - typedActors.put(id, actorRef) + log.debug("Registering server side remote actor [%s] with id [%s] on [%s:%d]", typedActor.getClass.getName, id, hostname, port) + typedActors.put(id, typedActor) } } - private def actors() : ConcurrentHashMap[String, ActorRef] = { - RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).actors - } - - private def typedActors() : ConcurrentHashMap[String, AnyRef] = { - RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).typedActors - } - /** * Register Remote Actor by the Actor's 'id' field. It starts the Actor if it is not started already. */ @@ -308,7 +296,6 @@ class RemoteServer extends Logging with ListenerManagement { def register(id: String, actorRef: ActorRef): Unit = synchronized { if (_isRunning) { val actors = RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).actors - println("___ register ___ " + actors.hashCode + " hostname=" + hostname + " port="+ port) if (!actors.contains(id)) { if (!actorRef.isRunning) actorRef.start log.debug("Registering server side remote actor [%s] with id [%s]", actorRef.actorClass.getName, id) @@ -344,16 +331,27 @@ class RemoteServer extends Logging with ListenerManagement { } } - //FIXME: unregister typed Actor + /** + * Unregister Remote Typed Actor by specific 'id'. + *

+ * NOTE: You need to call this method if you have registered an actor by a custom ID. + */ + def unregisterTypedActor(id: String):Unit = synchronized { + if (_isRunning) { + log.info("Unregistering server side remote typed actor with id [%s]", id) + val registeredTypedActors = typedActors() + registeredTypedActors.remove(id) + } + } protected override def manageLifeCycleOfListeners = false protected[akka] override def foreachListener(f: (ActorRef) => Unit): Unit = super.foreachListener(f) - private def actors() : ConcurrentHashMap[String, ActorRef] = { + private[akka] def actors() : ConcurrentHashMap[String, ActorRef] = { RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).actors } - private def typedActors() : ConcurrentHashMap[String, AnyRef] = { + private[akka] def typedActors() : ConcurrentHashMap[String, AnyRef] = { RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).typedActors } } diff --git a/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java b/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java index a30aa26124..715e5366a4 100644 --- a/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java +++ b/akka-remote/src/test/java/se/scalablesolutions/akka/actor/RemoteTypedActorOneImpl.java @@ -18,12 +18,9 @@ public class RemoteTypedActorOneImpl extends TypedActor implements RemoteTypedAc } public void oneWay() throws Exception { - System.out.println("--------> got it!!!!!!"); RemoteTypedActorLog.oneWayLog().put("oneway"); } - - @Override public void preRestart(Throwable e) { try { RemoteTypedActorLog.messageLog().put(e.getMessage()); } catch(Exception ex) {} diff --git a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala index 8b1e0ef765..012d42f92a 100644 --- a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala +++ b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala @@ -144,5 +144,6 @@ class ServerInitiatedRemoteActorSpec extends JUnitSuite { assert(numberOfActorsInRegistry === ActorRegistry.actors.length) actor.stop } + } diff --git a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala index 76c50f7658..b800fbf2c3 100644 --- a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala +++ b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteTypedActorSpec.scala @@ -4,42 +4,46 @@ package se.scalablesolutions.akka.actor.remote -import java.util.concurrent.{CountDownLatch, TimeUnit} -import org.scalatest.junit.JUnitSuite -import org.junit.{Test, Before, After} +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.BeforeAndAfterAll +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith + +import java.util.concurrent.TimeUnit import se.scalablesolutions.akka.remote.{RemoteServer, RemoteClient} import se.scalablesolutions.akka.actor._ +import RemoteTypedActorLog._ object ServerInitiatedRemoteTypedActorSpec { val HOSTNAME = "localhost" val PORT = 9990 var server: RemoteServer = null - - class SimpleActor extends Actor { - def receive = { - case _ => println("received message") - } - } - - } -class ServerInitiatedRemoteTypedActorSpec extends JUnitSuite { +@RunWith(classOf[JUnitRunner]) +class ServerInitiatedRemoteTypedActorSpec extends + Spec with + ShouldMatchers with + BeforeAndAfterAll { import ServerInitiatedRemoteTypedActorSpec._ + private val unit = TimeUnit.MILLISECONDS - @Before - def init { + override def beforeAll = { server = new RemoteServer() server.start(HOSTNAME, PORT) + + val typedActor = TypedActor.newInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000) + server.registerTypedActor("typed-actor-service", typedActor) + Thread.sleep(1000) } // make sure the servers shutdown cleanly after the test has finished - @After - def finished { + override def afterAll = { try { server.shutdown RemoteClient.shutdownAll @@ -49,38 +53,60 @@ class ServerInitiatedRemoteTypedActorSpec extends JUnitSuite { } } + describe("Server managed remote typed Actor ") { - @Test - def shouldSendWithBang { + it("should receive one-way message") { + clearMessageLogs + val actor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", 5000L, HOSTNAME, PORT) + expect("oneway") { + actor.oneWay + oneWayLog.poll(5, TimeUnit.SECONDS) + } + } - /* - val clientManangedTypedActor = TypedActor.newRemoteInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000, HOSTNAME, PORT) - clientManangedTypedActor.requestReply("test-string") - Thread.sleep(2000) - println("###########") - */ - /* - trace() - val actor = Actor.actorOf[SimpleActor].start - server.register("simple-actor", actor) - val typedActor = TypedActor.newInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000) - server.registerTypedActor("typed-actor-service", typedActor) - println("### registered actor") - trace() + it("should respond to request-reply message") { + clearMessageLogs + val actor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", 5000L, HOSTNAME, PORT) + expect("pong") { + actor.requestReply("ping") + } + } - //val actorRef = RemoteActorRef("typed-actor-service", classOf[RemoteTypedActorOneImpl].getName, HOSTNAME, PORT, 5000L, None) - //val myActor = TypedActor.createProxyForRemoteActorRef(classOf[RemoteTypedActorOne], actorRef) - val myActor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", classOf[RemoteTypedActorOneImpl].getName, 5000L, HOSTNAME, PORT) - println("### call one way") - myActor.oneWay() - Thread.sleep(3000) - println("### call one way - done") - */ - //assert(RemoteActorSpecActorUnidirectional.latch.await(1, TimeUnit.SECONDS)) - //actor.stop - /* */ + it("should not recreate registered actors") { + val actor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", 5000L, HOSTNAME, PORT) + val numberOfActorsInRegistry = ActorRegistry.actors.length + expect("oneway") { + actor.oneWay + oneWayLog.poll(5, TimeUnit.SECONDS) + } + assert(numberOfActorsInRegistry === ActorRegistry.actors.length) + } + + it("should support multiple variants to get the actor from client side") { + var actor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", 5000L, HOSTNAME, PORT) + expect("oneway") { + actor.oneWay + oneWayLog.poll(5, TimeUnit.SECONDS) + } + actor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", HOSTNAME, PORT) + expect("oneway") { + actor.oneWay + oneWayLog.poll(5, TimeUnit.SECONDS) + } + actor = RemoteClient.typedActorFor(classOf[RemoteTypedActorOne], "typed-actor-service", 5000L, HOSTNAME, PORT, this.getClass().getClassLoader) + expect("oneway") { + actor.oneWay + oneWayLog.poll(5, TimeUnit.SECONDS) + } + } + + it("should register and unregister typed actors") { + val typedActor = TypedActor.newInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000) + server.registerTypedActor("my-test-service", typedActor) + assert(server.typedActors().get("my-test-service") != null) + server.unregisterTypedActor("my-test-service") + assert(server.typedActors().get("my-test-service") == null) + } } - - } diff --git a/akka-typed-actor/src/main/scala/actor/TypedActor.scala b/akka-typed-actor/src/main/scala/actor/TypedActor.scala index 7f0ee5dbbc..385c1831a4 100644 --- a/akka-typed-actor/src/main/scala/actor/TypedActor.scala +++ b/akka-typed-actor/src/main/scala/actor/TypedActor.scala @@ -182,7 +182,6 @@ abstract class TypedActor extends Actor with Proxyable { case Link(proxy) => self.link(proxy) case Unlink(proxy) => self.unlink(proxy) - case method: String => println("### got method") case unexpected => throw new IllegalActorStateException( "Unexpected message [" + unexpected + "] sent to [" + this + "]") } @@ -408,8 +407,11 @@ object TypedActor extends Logging { proxy.asInstanceOf[T] } - // FIXME - def createProxyForRemoteActorRef[T](intfClass: Class[T], actorRef: ActorRef): T = { + /** + * Create a proxy for a RemoteActorRef representing a server managed remote typed actor. + * + */ + private[akka] def createProxyForRemoteActorRef[T](intfClass: Class[T], actorRef: ActorRef): T = { class MyInvocationHandler extends InvocationHandler { def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]): AnyRef = { @@ -423,9 +425,6 @@ object TypedActor extends Logging { val jProxy = JProxy.newProxyInstance(intfClass.getClassLoader(), interfaces, handler) val awProxy = Proxy.newInstance(interfaces, Array(jProxy, jProxy), true, false) - // TODO: needed? - //typedActor.initialize(proxy) - // TODO: timeout?? AspectInitRegistry.register(awProxy, AspectInit(intfClass, null, actorRef, None, 5000L)) awProxy.asInstanceOf[T] } @@ -569,6 +568,7 @@ object TypedActor extends Logging { private[akka] def isJoinPoint(message: Any): Boolean = message.isInstanceOf[JoinPoint] } + /** * AspectWerkz Aspect that is turning POJO into proxy to a server managed remote TypedActor. *

@@ -578,103 +578,18 @@ object TypedActor extends Logging { * @author Jonas Bonér */ @Aspect("perInstance") -private[akka] sealed class ServerManagedTypedActorAspect { - @volatile private var isInitialized = false - @volatile private var isStopped = false - private var interfaceClass: Class[_] = _ - private var actorRef: ActorRef = _ - private var timeout: Long = _ - private var uuid: String = _ - private var remoteAddress: Option[InetSocketAddress] = _ - - //FIXME - +private[akka] sealed class ServerManagedTypedActorAspect extends ActorAspect { + @Around("execution(* *.*(..)) && this(se.scalablesolutions.akka.actor.ServerManagedTypedActor)") def invoke(joinPoint: JoinPoint): AnyRef = { - println("### MyAspect intercepted " + joinPoint.getSignature) if (!isInitialized) initialize(joinPoint) remoteDispatch(joinPoint) } - - private def remoteDispatch(joinPoint: JoinPoint): AnyRef = { - val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] - val isOneWay = TypedActor.isOneWay(methodRtti) - - val (message: Array[AnyRef], isEscaped) = escapeArguments(methodRtti.getParameterValues) - - println("### remote dispatch...") - - val future = RemoteClientModule.send[AnyRef]( - message, None, None, remoteAddress.get, - timeout, isOneWay, actorRef, - Some((interfaceClass.getName, methodRtti.getMethod.getName)), - ActorType.TypedActor) - - if (isOneWay) null // for void methods - else { - if (future.isDefined) { - future.get.await - val result = getResultOrThrowException(future.get) - if (result.isDefined) result.get - else throw new IllegalActorStateException("No result returned from call to [" + joinPoint + "]") - } else throw new IllegalActorStateException("No future returned from call to [" + joinPoint + "]") - } - } - - /* - private def remoteDispatch(joinPoint: JoinPoint): AnyRef = { - val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] - val isOneWay = TypedActor.isOneWay(methodRtti) - val senderActorRef = Some(SenderContextInfo.senderActorRef.value) - - - if (!actorRef.isRunning && !isStopped) { - isStopped = true - // FIXME - what to do? - // joinPoint.proceed - null - } else if (isOneWay) { - actorRef.!("joinPoint") - //actorRef.!(joinPoint)(senderActorRef) - null.asInstanceOf[AnyRef] - } else if (TypedActor.returnsFuture_?(methodRtti)) { - actorRef.!!!(joinPoint, timeout)(senderActorRef) - } else { - val result = (actorRef.!!(joinPoint, timeout)(senderActorRef)).as[AnyRef] - if (result.isDefined) result.get - else throw new ActorTimeoutException("Invocation to [" + joinPoint + "] timed out.") - } - } - */ - private def getResultOrThrowException[T](future: Future[T]): Option[T] = - if (future.exception.isDefined) throw future.exception.get - else future.result - - private def escapeArguments(args: Array[AnyRef]): Tuple2[Array[AnyRef], Boolean] = { - var isEscaped = false - val escapedArgs = for (arg <- args) yield { - val clazz = arg.getClass - if (clazz.getName.contains(TypedActor.AW_PROXY_PREFIX)) { - isEscaped = true - TypedActor.AW_PROXY_PREFIX + clazz.getSuperclass.getName - } else arg - } - (escapedArgs, isEscaped) - } - - - private def initialize(joinPoint: JoinPoint): Unit = { - val init = AspectInitRegistry.initFor(joinPoint.getThis) - interfaceClass = init.interfaceClass - actorRef = init.actorRef - uuid = actorRef.uuid + override def initialize(joinPoint: JoinPoint): Unit = { + super.initialize(joinPoint) remoteAddress = actorRef.remoteAddress - println("### address= " + remoteAddress.get) - timeout = init.timeout - isInitialized = true } - } /** @@ -686,16 +601,8 @@ private[akka] sealed class ServerManagedTypedActorAspect { * @author Jonas Bonér */ @Aspect("perInstance") -private[akka] sealed class TypedActorAspect { - @volatile private var isInitialized = false - @volatile private var isStopped = false - private var interfaceClass: Class[_] = _ - private var typedActor: TypedActor = _ - private var actorRef: ActorRef = _ - private var remoteAddress: Option[InetSocketAddress] = _ - private var timeout: Long = _ - private var uuid: String = _ - +private[akka] sealed class TypedActorAspect extends ActorAspect { + @Around("execution(* *.*(..)) && !this(se.scalablesolutions.akka.actor.ServerManagedTypedActor)") def invoke(joinPoint: JoinPoint): AnyRef = { if (!isInitialized) initialize(joinPoint) @@ -706,12 +613,26 @@ private[akka] sealed class TypedActorAspect { if (remoteAddress.isDefined) remoteDispatch(joinPoint) else localDispatch(joinPoint) } +} - private def localDispatch(joinPoint: JoinPoint): AnyRef = { - val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] - val isOneWay = TypedActor.isOneWay(methodRtti) +/** + * Base class for TypedActorAspect and ServerManagedTypedActorAspect to reduce code duplication. + */ +private[akka] abstract class ActorAspect { + @volatile protected var isInitialized = false + @volatile protected var isStopped = false + protected var interfaceClass: Class[_] = _ + protected var typedActor: TypedActor = _ + protected var actorRef: ActorRef = _ + protected var timeout: Long = _ + protected var uuid: String = _ + protected var remoteAddress: Option[InetSocketAddress] = _ + + protected def localDispatch(joinPoint: JoinPoint): AnyRef = { + val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] + val isOneWay = TypedActor.isOneWay(methodRtti) val senderActorRef = Some(SenderContextInfo.senderActorRef.value) - val senderProxy = Some(SenderContextInfo.senderProxy.value) + val senderProxy = Some(SenderContextInfo.senderProxy.value) typedActor.context._sender = senderProxy if (!actorRef.isRunning && !isStopped) { @@ -732,7 +653,7 @@ private[akka] sealed class TypedActorAspect { } } - private def remoteDispatch(joinPoint: JoinPoint): AnyRef = { + protected def remoteDispatch(joinPoint: JoinPoint): AnyRef = { val methodRtti = joinPoint.getRtti.asInstanceOf[MethodRtti] val isOneWay = TypedActor.isOneWay(methodRtti) @@ -771,7 +692,7 @@ private[akka] sealed class TypedActorAspect { (escapedArgs, isEscaped) } - private def initialize(joinPoint: JoinPoint): Unit = { + protected def initialize(joinPoint: JoinPoint): Unit = { val init = AspectInitRegistry.initFor(joinPoint.getThis) interfaceClass = init.interfaceClass typedActor = init.targetInstance @@ -839,4 +760,7 @@ private[akka] sealed case class AspectInit( } +/** + * Marker interface for server manager typed actors. + */ private[akka] sealed trait ServerManagedTypedActor extends TypedActor \ No newline at end of file diff --git a/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala b/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala index 1fcbe0c5ef..10fc40493b 100644 --- a/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala +++ b/akka-typed-actor/src/test/scala/actor/typed-actor/TypedActorLifecycleSpec.scala @@ -45,7 +45,6 @@ class TypedActorLifecycleSpec extends Spec with ShouldMatchers with BeforeAndAft fail("expected exception not thrown") } catch { case e: RuntimeException => { - println("#failed") cdl.await assert(SamplePojoImpl._pre) assert(SamplePojoImpl._post) From 869549c590ce91686ad19caf5ea2336bd51388b2 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Tue, 7 Sep 2010 11:02:12 +0200 Subject: [PATCH 03/25] Fixing id/uuid misfortune --- .../src/main/scala/remote/RemoteServer.scala | 16 ++++++---------- .../remote/ClientInitiatedRemoteActorSpec.scala | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/akka-remote/src/main/scala/remote/RemoteServer.scala b/akka-remote/src/main/scala/remote/RemoteServer.scala index 142670a84a..6337bd9893 100644 --- a/akka-remote/src/main/scala/remote/RemoteServer.scala +++ b/akka-remote/src/main/scala/remote/RemoteServer.scala @@ -274,9 +274,9 @@ class RemoteServer extends Logging with ListenerManagement { // TODO: register typed actor in RemoteServer as well /** - * Register Remote Actor by the Actor's 'uuid' field. It starts the Actor if it is not started already. + * Register Remote Actor by the Actor's 'id' field. It starts the Actor if it is not started already. */ - def register(actorRef: ActorRef): Unit = register(actorRef.uuid,actorRef) + def register(actorRef: ActorRef): Unit = register(actorRef.id,actorRef) /** * Register Remote Actor by a specific 'id' passed as argument. @@ -295,13 +295,13 @@ class RemoteServer extends Logging with ListenerManagement { } /** - * Unregister Remote Actor that is registered using its 'uuid' field (not custom ID). + * Unregister Remote Actor that is registered using its 'id' field (not custom ID). */ def unregister(actorRef: ActorRef):Unit = synchronized { if (_isRunning) { log.debug("Unregistering server side remote actor [%s] with id [%s:%s]", actorRef.actorClass.getName, actorRef.id, actorRef.uuid) val actorMap = actors() - actorMap remove actorRef.uuid + actorMap remove actorRef.id if (actorRef.registeredInRemoteNodeDuringSerialization) actorMap remove actorRef.uuid } } @@ -325,12 +325,8 @@ class RemoteServer extends Logging with ListenerManagement { protected[akka] override def foreachListener(f: (ActorRef) => Unit): Unit = super.foreachListener(f) - private[akka] def actors() : ConcurrentHashMap[String, ActorRef] = { - RemoteServer.actorsFor(address).actors - } - private[akka] def typedActors() : ConcurrentHashMap[String, AnyRef] = { - RemoteServer.actorsFor(address).typedActors - } + private[akka] def actors() = RemoteServer.actorsFor(address).actors + private[akka] def typedActors() = RemoteServer.actorsFor(address).typedActors } object RemoteServerSslContext { diff --git a/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala b/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala index 7ff46ab910..e03259e573 100644 --- a/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala +++ b/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala @@ -103,7 +103,7 @@ class ClientInitiatedRemoteActorSpec extends JUnitSuite { sender.actor.asInstanceOf[SendOneWayAndReplySenderActor].sendTo = actor sender.start sender.actor.asInstanceOf[SendOneWayAndReplySenderActor].sendOff - assert(SendOneWayAndReplySenderActor.latch.await(1, TimeUnit.SECONDS)) + assert(SendOneWayAndReplySenderActor.latch.await(3, TimeUnit.SECONDS)) assert(sender.actor.asInstanceOf[SendOneWayAndReplySenderActor].state.isDefined === true) assert("World" === sender.actor.asInstanceOf[SendOneWayAndReplySenderActor].state.get.asInstanceOf[String]) actor.stop From 40a66050648079acdf14d553f8e74e6ceda88e3f Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Tue, 7 Sep 2010 11:02:40 +0200 Subject: [PATCH 04/25] Removing boilerplate in ReflectiveAccess --- .../main/scala/util/ReflectiveAccess.scala | 66 ++++++++----------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/akka-actor/src/main/scala/util/ReflectiveAccess.scala b/akka-actor/src/main/scala/util/ReflectiveAccess.scala index 4582304188..878403d026 100644 --- a/akka-actor/src/main/scala/util/ReflectiveAccess.scala +++ b/akka-actor/src/main/scala/util/ReflectiveAccess.scala @@ -29,6 +29,9 @@ object ReflectiveAccess { def ensureTypedActorEnabled = TypedActorModule.ensureTypedActorEnabled def ensureJtaEnabled = JtaModule.ensureJtaEnabled + private val noParams = Array[Class[_]]() + private val noArgs = Array[AnyRef]() + /** * Reflective access to the RemoteClient module. * @@ -62,14 +65,8 @@ object ReflectiveAccess { def ensureRemotingEnabled = if (!isRemotingEnabled) throw new ModuleNotAvailableException( "Can't load the remoting module, make sure that akka-remote.jar is on the classpath") - val remoteClientObjectInstance: Option[RemoteClientObject] = { - try { - val clazz = loader.loadClass("se.scalablesolutions.akka.remote.RemoteClient$") - val ctor = clazz.getDeclaredConstructor(Array[Class[_]](): _*) - ctor.setAccessible(true) - Some(ctor.newInstance(Array[AnyRef](): _*).asInstanceOf[RemoteClientObject]) - } catch { case e: Exception => None } - } + val remoteClientObjectInstance: Option[RemoteClientObject] = + createInstance("se.scalablesolutions.akka.remote.RemoteClient$") def register(address: InetSocketAddress, uuid: String) = { ensureRemotingEnabled @@ -126,23 +123,11 @@ object ReflectiveAccess { def unregister(actorRef: ActorRef): Unit } - val remoteServerObjectInstance: Option[RemoteServerObject] = { - try { - val clazz = loader.loadClass("se.scalablesolutions.akka.remote.RemoteServer$") - val ctor = clazz.getDeclaredConstructor(Array[Class[_]](): _*) - ctor.setAccessible(true) - Some(ctor.newInstance(Array[AnyRef](): _*).asInstanceOf[RemoteServerObject]) - } catch { case e: Exception => None } - } + val remoteServerObjectInstance: Option[RemoteServerObject] = + createInstance("se.scalablesolutions.akka.remote.RemoteServer$") - val remoteNodeObjectInstance: Option[RemoteNodeObject] = { - try { - val clazz = loader.loadClass("se.scalablesolutions.akka.remote.RemoteNode$") - val ctor = clazz.getDeclaredConstructor(Array[Class[_]](): _*) - ctor.setAccessible(true) - Some(ctor.newInstance(Array[AnyRef](): _*).asInstanceOf[RemoteNodeObject]) - } catch { case e: Exception => None } - } + val remoteNodeObjectInstance: Option[RemoteNodeObject] = + createInstance("se.scalablesolutions.akka.remote.RemoteNode$") def registerActor(address: InetSocketAddress, uuid: String, actorRef: ActorRef) = { ensureRemotingEnabled @@ -177,14 +162,8 @@ object ReflectiveAccess { def ensureTypedActorEnabled = if (!isTypedActorEnabled) throw new ModuleNotAvailableException( "Can't load the typed actor module, make sure that akka-typed-actor.jar is on the classpath") - val typedActorObjectInstance: Option[TypedActorObject] = { - try { - val clazz = loader.loadClass("se.scalablesolutions.akka.actor.TypedActor$") - val ctor = clazz.getDeclaredConstructor(Array[Class[_]](): _*) - ctor.setAccessible(true) - Some(ctor.newInstance(Array[AnyRef](): _*).asInstanceOf[TypedActorObject]) - } catch { case e: Exception => None } - } + val typedActorObjectInstance: Option[TypedActorObject] = + createInstance("se.scalablesolutions.akka.actor.TypedActor$") def resolveFutureIfMessageIsJoinPoint(message: Any, future: Future[_]): Boolean = { ensureTypedActorEnabled @@ -212,18 +191,25 @@ object ReflectiveAccess { def ensureJtaEnabled = if (!isJtaEnabled) throw new ModuleNotAvailableException( "Can't load the typed actor module, make sure that akka-jta.jar is on the classpath") - val transactionContainerObjectInstance: Option[TransactionContainerObject] = { - try { - val clazz = loader.loadClass("se.scalablesolutions.akka.actor.TransactionContainer$") - val ctor = clazz.getDeclaredConstructor(Array[Class[_]](): _*) - ctor.setAccessible(true) - Some(ctor.newInstance(Array[AnyRef](): _*).asInstanceOf[TransactionContainerObject]) - } catch { case e: Exception => None } - } + val transactionContainerObjectInstance: Option[TransactionContainerObject] = + createInstance("se.scalablesolutions.akka.actor.TransactionContainer$") def createTransactionContainer: TransactionContainer = { ensureJtaEnabled transactionContainerObjectInstance.get.apply.asInstanceOf[TransactionContainer] } } + + protected def createInstance[T](fqn: String, + ctorSpec: Array[Class[_]] = noParams, + ctorArgs: Array[AnyRef] = noArgs): Option[T] = try { + val clazz = loader.loadClass(fqn) + val ctor = clazz.getDeclaredConstructor(ctorSpec: _*) + ctor.setAccessible(true) + Some(ctor.newInstance(ctorArgs: _*).asInstanceOf[T]) + } catch { + case e: Exception => + Logger("createInstance").error(e, "Couldn't load [%s(%s) => %s(%s)]",fqn,ctorSpec.mkString(", "),fqn,ctorArgs.mkString(", ")) + None + } } From bfb612908b092ee81c34bc3bfc6612744d2ceb22 Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Thu, 9 Sep 2010 10:42:03 +0200 Subject: [PATCH 05/25] closing ticket 378 --- .../src/main/scala/remote/RemoteServer.scala | 5 + .../akka/spring/akka-1.0-SNAPSHOT.xsd | 62 ++++++- .../scala/ActorBeanDefinitionParser.scala | 72 +++++++ .../src/main/scala/ActorFactoryBean.scala | 73 +++++++- akka-spring/src/main/scala/ActorParser.scala | 175 +++++++++++++++++- .../src/main/scala/ActorProperties.scala | 29 ++- .../src/main/scala/AkkaNamespaceHandler.scala | 11 +- .../scala/AkkaSpringConfigurationTags.scala | 7 + akka-spring/src/main/scala/BeanParser.scala | 42 ----- .../src/main/scala/DispatcherParser.scala | 101 ---------- .../src/main/scala/PropertyEntries.scala | 16 ++ .../src/main/scala/PropertyEntry.scala | 19 -- .../TypedActorBeanDefinitionParser.scala | 31 ---- .../UntypedActorBeanDefinitionParser.scala | 31 ---- .../akka/spring/foo/IMyPojo.java | 10 +- .../akka/spring/foo/MyPojo.java | 52 +++--- .../akka/spring/foo/PingActor.java | 10 +- .../test/resources/server-managed-config.xml | 57 ++++++ .../src/test/resources/typed-actor-config.xml | 2 +- .../test/resources/untyped-actor-config.xml | 2 +- .../TypedActorBeanDefinitionParserTest.scala | 16 +- .../scala/TypedActorSpringFeatureTest.scala | 123 +++++++++--- .../scala/UntypedActorSpringFeatureTest.scala | 140 ++++++++++---- .../src/main/scala/actor/TypedActor.scala | 3 +- .../src/test/resources/META-INF/aop.xml | 1 + 25 files changed, 741 insertions(+), 349 deletions(-) create mode 100644 akka-spring/src/main/scala/ActorBeanDefinitionParser.scala delete mode 100644 akka-spring/src/main/scala/BeanParser.scala delete mode 100644 akka-spring/src/main/scala/DispatcherParser.scala delete mode 100644 akka-spring/src/main/scala/PropertyEntry.scala delete mode 100644 akka-spring/src/main/scala/TypedActorBeanDefinitionParser.scala delete mode 100644 akka-spring/src/main/scala/UntypedActorBeanDefinitionParser.scala create mode 100644 akka-spring/src/test/resources/server-managed-config.xml diff --git a/akka-remote/src/main/scala/remote/RemoteServer.scala b/akka-remote/src/main/scala/remote/RemoteServer.scala index bf9b38ca1b..fa57bda71b 100644 --- a/akka-remote/src/main/scala/remote/RemoteServer.scala +++ b/akka-remote/src/main/scala/remote/RemoteServer.scala @@ -271,6 +271,11 @@ class RemoteServer extends Logging with ListenerManagement { } } + /** + * Register typed actor by interface name. + */ + def registerTypedActor(intfClass: Class[_], typedActor: AnyRef) : Unit = registerTypedActor(intfClass.getName, typedActor) + /** * Register remote typed actor by a specific id. * @param id custom actor id diff --git a/akka-spring/src/main/resources/se/scalablesolutions/akka/spring/akka-1.0-SNAPSHOT.xsd b/akka-spring/src/main/resources/se/scalablesolutions/akka/spring/akka-1.0-SNAPSHOT.xsd index c3d7608bee..80b37c41f5 100644 --- a/akka-spring/src/main/resources/se/scalablesolutions/akka/spring/akka-1.0-SNAPSHOT.xsd +++ b/akka-spring/src/main/resources/se/scalablesolutions/akka/spring/akka-1.0-SNAPSHOT.xsd @@ -66,6 +66,14 @@ + + + + + + + + @@ -107,6 +115,20 @@ + + + + Management type for remote actors: client managed or server managed. + + + + + + + Custom service name for server managed actor. + + + @@ -135,7 +157,7 @@ - Theh default timeout for '!!' invocations. + The default timeout for '!!' invocations. @@ -229,6 +251,41 @@ + + + + + + + + Name of the remote host. + + + + + + + Port of the remote host. + + + + + + + Custom service name or class name for the server managed actor. + + + + + + + Name of the interface the typed actor implements. + + + + + + @@ -294,4 +351,7 @@ + + + diff --git a/akka-spring/src/main/scala/ActorBeanDefinitionParser.scala b/akka-spring/src/main/scala/ActorBeanDefinitionParser.scala new file mode 100644 index 0000000000..55aa82b8e4 --- /dev/null +++ b/akka-spring/src/main/scala/ActorBeanDefinitionParser.scala @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2009-2010 Scalable Solutions AB + */ +package se.scalablesolutions.akka.spring + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser +import org.springframework.beans.factory.xml.ParserContext +import AkkaSpringConfigurationTags._ +import org.w3c.dom.Element + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class TypedActorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val typedActorConf = parseActor(element) + typedActorConf.typed = TYPED_ACTOR_TAG + typedActorConf.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ActorFactoryBean] +} + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class UntypedActorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val untypedActorConf = parseActor(element) + untypedActorConf.typed = UNTYPED_ACTOR_TAG + untypedActorConf.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ActorFactoryBean] +} + + +/** + * Parser for custom namespace configuration. + * @author michaelkober + */ +class ActorForBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorForParser { + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { + val actorForConf = parseActorFor(element) + actorForConf.setAsProperties(builder) + } + + /* + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + override def getBeanClass(element: Element): Class[_] = classOf[ActorForFactoryBean] +} diff --git a/akka-spring/src/main/scala/ActorFactoryBean.scala b/akka-spring/src/main/scala/ActorFactoryBean.scala index 11d5274a70..c47efcdb78 100644 --- a/akka-spring/src/main/scala/ActorFactoryBean.scala +++ b/akka-spring/src/main/scala/ActorFactoryBean.scala @@ -4,22 +4,19 @@ package se.scalablesolutions.akka.spring -import java.beans.PropertyDescriptor -import java.lang.reflect.Method -import javax.annotation.PreDestroy -import javax.annotation.PostConstruct - import org.springframework.beans.{BeanUtils,BeansException,BeanWrapper,BeanWrapperImpl} -import org.springframework.beans.factory.BeanFactory +import se.scalablesolutions.akka.remote.{RemoteClient, RemoteServer} +//import org.springframework.beans.factory.BeanFactory import org.springframework.beans.factory.config.AbstractFactoryBean import org.springframework.context.{ApplicationContext,ApplicationContextAware} -import org.springframework.util.ReflectionUtils +//import org.springframework.util.ReflectionUtils import org.springframework.util.StringUtils import se.scalablesolutions.akka.actor.{ActorRef, AspectInitRegistry, TypedActorConfiguration, TypedActor,Actor} import se.scalablesolutions.akka.dispatch.MessageDispatcher import se.scalablesolutions.akka.util.{Logging, Duration} import scala.reflect.BeanProperty +import java.net.InetSocketAddress /** * Exception to use when something goes wrong during bean creation. @@ -49,6 +46,8 @@ class ActorFactoryBean extends AbstractFactoryBean[AnyRef] with Logging with App @BeanProperty var transactional: Boolean = false @BeanProperty var host: String = "" @BeanProperty var port: Int = _ + @BeanProperty var serverManaged: Boolean = false + @BeanProperty var serviceName: String = "" @BeanProperty var lifecycle: String = "" @BeanProperty var dispatcher: DispatcherProperties = _ @BeanProperty var scope: String = VAL_SCOPE_SINGLETON @@ -94,7 +93,16 @@ class ActorFactoryBean extends AbstractFactoryBean[AnyRef] with Logging with App if (implementation == null || implementation == "") throw new AkkaBeansException( "The 'implementation' part of the 'akka:typed-actor' element in the Spring config file can't be null or empty string") - TypedActor.newInstance(interface.toClass, implementation.toClass, createConfig) + val typedActor: AnyRef = TypedActor.newInstance(interface.toClass, implementation.toClass, createConfig) + if (isRemote && serverManaged) { + val server = RemoteServer.getOrCreateServer(new InetSocketAddress(host, port)) + if (serviceName.isEmpty) { + server.registerTypedActor(interface, typedActor) + } else { + server.registerTypedActor(serviceName, typedActor) + } + } + typedActor } /** @@ -111,7 +119,16 @@ class ActorFactoryBean extends AbstractFactoryBean[AnyRef] with Logging with App actorRef.makeTransactionRequired } if (isRemote) { - actorRef.makeRemote(host, port) + if (serverManaged) { + val server = RemoteServer.getOrCreateServer(new InetSocketAddress(host, port)) + if (serviceName.isEmpty) { + server.register(actorRef) + } else { + server.register(serviceName, actorRef) + } + } else { + actorRef.makeRemote(host, port) + } } if (hasDispatcher) { if (dispatcher.dispatcherType != THREAD_BASED){ @@ -159,7 +176,7 @@ class ActorFactoryBean extends AbstractFactoryBean[AnyRef] with Logging with App private[akka] def createConfig: TypedActorConfiguration = { val config = new TypedActorConfiguration().timeout(Duration(timeout, "millis")) if (transactional) config.makeTransactionRequired - if (isRemote) config.makeRemote(host, port) + if (isRemote && !serverManaged) config.makeRemote(host, port) if (hasDispatcher) { if (dispatcher.dispatcherType != THREAD_BASED) { config.dispatcher(dispatcherInstance()) @@ -191,3 +208,39 @@ class ActorFactoryBean extends AbstractFactoryBean[AnyRef] with Logging with App } } } + +/** + * Factory bean for remote client actor-for. + * + * @author michaelkober + */ +class ActorForFactoryBean extends AbstractFactoryBean[AnyRef] with Logging with ApplicationContextAware { + import StringReflect._ + import AkkaSpringConfigurationTags._ + + @BeanProperty var interface: String = "" + @BeanProperty var host: String = "" + @BeanProperty var port: Int = _ + @BeanProperty var serviceName: String = "" + //@BeanProperty var scope: String = VAL_SCOPE_SINGLETON + @BeanProperty var applicationContext: ApplicationContext = _ + + override def isSingleton = false + + /* + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + def getObjectType: Class[AnyRef] = classOf[AnyRef] + + /* + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + def createInstance: AnyRef = { + if (interface.isEmpty) { + RemoteClient.actorFor(serviceName, host, port) + } else { + RemoteClient.typedActorFor(interface.toClass, serviceName, host, port) + } + } +} + diff --git a/akka-spring/src/main/scala/ActorParser.scala b/akka-spring/src/main/scala/ActorParser.scala index 69073bd52f..8736b807d1 100644 --- a/akka-spring/src/main/scala/ActorParser.scala +++ b/akka-spring/src/main/scala/ActorParser.scala @@ -6,6 +6,7 @@ package se.scalablesolutions.akka.spring import org.springframework.util.xml.DomUtils import org.w3c.dom.Element import scala.collection.JavaConversions._ +import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.actor.IllegalActorStateException @@ -27,11 +28,17 @@ trait ActorParser extends BeanParser with DispatcherParser { val objectProperties = new ActorProperties() val remoteElement = DomUtils.getChildElementByTagName(element, REMOTE_TAG); val dispatcherElement = DomUtils.getChildElementByTagName(element, DISPATCHER_TAG) - val propertyEntries = DomUtils.getChildElementsByTagName(element,PROPERTYENTRY_TAG) + val propertyEntries = DomUtils.getChildElementsByTagName(element, PROPERTYENTRY_TAG) if (remoteElement != null) { objectProperties.host = mandatory(remoteElement, HOST) objectProperties.port = mandatory(remoteElement, PORT).toInt + objectProperties.serverManaged = (remoteElement.getAttribute(MANAGED_BY) != null) && (remoteElement.getAttribute(MANAGED_BY).equals(SERVER_MANAGED)) + val serviceName = remoteElement.getAttribute(SERVICE_NAME) + if ((serviceName != null) && (!serviceName.isEmpty)) { + objectProperties.serviceName = serviceName + objectProperties.serverManaged = true + } } if (dispatcherElement != null) { @@ -43,7 +50,7 @@ trait ActorParser extends BeanParser with DispatcherParser { val entry = new PropertyEntry entry.name = element.getAttribute("name"); entry.value = element.getAttribute("value") - entry.ref = element.getAttribute("ref") + entry.ref = element.getAttribute("ref") objectProperties.propertyEntries.add(entry) } @@ -59,15 +66,13 @@ trait ActorParser extends BeanParser with DispatcherParser { objectProperties.target = mandatory(element, IMPLEMENTATION) objectProperties.transactional = if (element.getAttribute(TRANSACTIONAL).isEmpty) false else element.getAttribute(TRANSACTIONAL).toBoolean - if (!element.getAttribute(INTERFACE).isEmpty) { + if (element.hasAttribute(INTERFACE)) { objectProperties.interface = element.getAttribute(INTERFACE) } - - if (!element.getAttribute(LIFECYCLE).isEmpty) { + if (element.hasAttribute(LIFECYCLE)) { objectProperties.lifecycle = element.getAttribute(LIFECYCLE) } - - if (!element.getAttribute(SCOPE).isEmpty) { + if (element.hasAttribute(SCOPE)) { objectProperties.scope = element.getAttribute(SCOPE) } @@ -75,3 +80,159 @@ trait ActorParser extends BeanParser with DispatcherParser { } } + +/** + * Parser trait for custom namespace configuration for RemoteClient actor-for. + * @author michaelkober + */ +trait ActorForParser extends BeanParser { + import AkkaSpringConfigurationTags._ + + /** + * Parses the given element and returns a ActorForProperties. + * @param element dom element to parse + * @return configuration for the typed actor + */ + def parseActorFor(element: Element): ActorForProperties = { + val objectProperties = new ActorForProperties() + + objectProperties.host = mandatory(element, HOST) + objectProperties.port = mandatory(element, PORT).toInt + objectProperties.serviceName = mandatory(element, SERVICE_NAME) + if (element.hasAttribute(INTERFACE)) { + objectProperties.interface = element.getAttribute(INTERFACE) + } + objectProperties + } + +} + +/** + * Base trait with utility methods for bean parsing. + */ +trait BeanParser extends Logging { + + /** + * Get a mandatory element attribute. + * @param element the element with the mandatory attribute + * @param attribute name of the mandatory attribute + */ + def mandatory(element: Element, attribute: String): String = { + if ((element.getAttribute(attribute) == null) || (element.getAttribute(attribute).isEmpty)) { + throw new IllegalArgumentException("Mandatory attribute missing: " + attribute) + } else { + element.getAttribute(attribute) + } + } + + /** + * Get a mandatory child element. + * @param element the parent element + * @param childName name of the mandatory child element + */ + def mandatoryElement(element: Element, childName: String): Element = { + val childElement = DomUtils.getChildElementByTagName(element, childName); + if (childElement == null) { + throw new IllegalArgumentException("Mandatory element missing: ''") + } else { + childElement + } + } + +} + + +/** + * Parser trait for custom namespace for Akka dispatcher configuration. + * @author michaelkober + */ +trait DispatcherParser extends BeanParser { + import AkkaSpringConfigurationTags._ + + /** + * Parses the given element and returns a DispatcherProperties. + * @param element dom element to parse + * @return configuration for the dispatcher + */ + def parseDispatcher(element: Element): DispatcherProperties = { + val properties = new DispatcherProperties() + var dispatcherElement = element + if (hasRef(element)) { + val ref = element.getAttribute(REF) + dispatcherElement = element.getOwnerDocument.getElementById(ref) + if (dispatcherElement == null) { + throw new IllegalArgumentException("Referenced dispatcher not found: '" + ref + "'") + } + } + + properties.dispatcherType = mandatory(dispatcherElement, TYPE) + if (properties.dispatcherType == THREAD_BASED) { + val allowedParentNodes = "akka:typed-actor" :: "akka:untyped-actor" :: "typed-actor" :: "untyped-actor" :: Nil + if (!allowedParentNodes.contains(dispatcherElement.getParentNode.getNodeName)) { + throw new IllegalArgumentException("Thread based dispatcher must be nested in 'typed-actor' or 'untyped-actor' element!") + } + } + + if (properties.dispatcherType == HAWT) { // no name for HawtDispatcher + properties.name = dispatcherElement.getAttribute(NAME) + if (dispatcherElement.hasAttribute(AGGREGATE)) { + properties.aggregate = dispatcherElement.getAttribute(AGGREGATE).toBoolean + } + } else { + properties.name = mandatory(dispatcherElement, NAME) + } + + val threadPoolElement = DomUtils.getChildElementByTagName(dispatcherElement, THREAD_POOL_TAG); + if (threadPoolElement != null) { + if (properties.dispatcherType == REACTOR_BASED_SINGLE_THREAD_EVENT_DRIVEN || + properties.dispatcherType == THREAD_BASED) { + throw new IllegalArgumentException("Element 'thread-pool' not allowed for this dispatcher type.") + } + val threadPoolProperties = parseThreadPool(threadPoolElement) + properties.threadPool = threadPoolProperties + } + properties + } + + /** + * Parses the given element and returns a ThreadPoolProperties. + * @param element dom element to parse + * @return configuration for the thread pool + */ + def parseThreadPool(element: Element): ThreadPoolProperties = { + val properties = new ThreadPoolProperties() + properties.queue = element.getAttribute(QUEUE) + if (element.hasAttribute(CAPACITY)) { + properties.capacity = element.getAttribute(CAPACITY).toInt + } + if (element.hasAttribute(BOUND)) { + properties.bound = element.getAttribute(BOUND).toInt + } + if (element.hasAttribute(FAIRNESS)) { + properties.fairness = element.getAttribute(FAIRNESS).toBoolean + } + if (element.hasAttribute(CORE_POOL_SIZE)) { + properties.corePoolSize = element.getAttribute(CORE_POOL_SIZE).toInt + } + if (element.hasAttribute(MAX_POOL_SIZE)) { + properties.maxPoolSize = element.getAttribute(MAX_POOL_SIZE).toInt + } + if (element.hasAttribute(KEEP_ALIVE)) { + properties.keepAlive = element.getAttribute(KEEP_ALIVE).toLong + } + if (element.hasAttribute(REJECTION_POLICY)) { + properties.rejectionPolicy = element.getAttribute(REJECTION_POLICY) + } + if (element.hasAttribute(MAILBOX_CAPACITY)) { + properties.mailboxCapacity = element.getAttribute(MAILBOX_CAPACITY).toInt + } + properties + } + + def hasRef(element: Element): Boolean = { + val ref = element.getAttribute(REF) + (ref != null) && !ref.isEmpty + } + +} + diff --git a/akka-spring/src/main/scala/ActorProperties.scala b/akka-spring/src/main/scala/ActorProperties.scala index 15c7e61fe0..0f86942935 100644 --- a/akka-spring/src/main/scala/ActorProperties.scala +++ b/akka-spring/src/main/scala/ActorProperties.scala @@ -8,7 +8,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder import AkkaSpringConfigurationTags._ /** - * Data container for typed actor configuration data. + * Data container for actor configuration data. * @author michaelkober * @author Martin Krasser */ @@ -20,6 +20,8 @@ class ActorProperties { var transactional: Boolean = false var host: String = "" var port: Int = _ + var serverManaged: Boolean = false + var serviceName: String = "" var lifecycle: String = "" var scope:String = VAL_SCOPE_SINGLETON var dispatcher: DispatcherProperties = _ @@ -34,6 +36,8 @@ class ActorProperties { builder.addPropertyValue("typed", typed) builder.addPropertyValue(HOST, host) builder.addPropertyValue(PORT, port) + builder.addPropertyValue("serverManaged", serverManaged) + builder.addPropertyValue("serviceName", serviceName) builder.addPropertyValue(TIMEOUT, timeout) builder.addPropertyValue(IMPLEMENTATION, target) builder.addPropertyValue(INTERFACE, interface) @@ -45,3 +49,26 @@ class ActorProperties { } } + +/** + * Data container for actor configuration data. + * @author michaelkober + */ +class ActorForProperties { + var interface: String = "" + var host: String = "" + var port: Int = _ + var serviceName: String = "" + + /** + * Sets the properties to the given builder. + * @param builder bean definition builder + */ + def setAsProperties(builder: BeanDefinitionBuilder) { + builder.addPropertyValue(HOST, host) + builder.addPropertyValue(PORT, port) + builder.addPropertyValue("serviceName", serviceName) + builder.addPropertyValue(INTERFACE, interface) + } + +} diff --git a/akka-spring/src/main/scala/AkkaNamespaceHandler.scala b/akka-spring/src/main/scala/AkkaNamespaceHandler.scala index a478b7b262..b1c58baa20 100644 --- a/akka-spring/src/main/scala/AkkaNamespaceHandler.scala +++ b/akka-spring/src/main/scala/AkkaNamespaceHandler.scala @@ -12,10 +12,11 @@ import AkkaSpringConfigurationTags._ */ class AkkaNamespaceHandler extends NamespaceHandlerSupport { def init = { - registerBeanDefinitionParser(TYPED_ACTOR_TAG, new TypedActorBeanDefinitionParser()); - registerBeanDefinitionParser(UNTYPED_ACTOR_TAG, new UntypedActorBeanDefinitionParser()); - registerBeanDefinitionParser(SUPERVISION_TAG, new SupervisionBeanDefinitionParser()); - registerBeanDefinitionParser(DISPATCHER_TAG, new DispatcherBeanDefinitionParser()); - registerBeanDefinitionParser(CAMEL_SERVICE_TAG, new CamelServiceBeanDefinitionParser); + registerBeanDefinitionParser(TYPED_ACTOR_TAG, new TypedActorBeanDefinitionParser()) + registerBeanDefinitionParser(UNTYPED_ACTOR_TAG, new UntypedActorBeanDefinitionParser()) + registerBeanDefinitionParser(SUPERVISION_TAG, new SupervisionBeanDefinitionParser()) + registerBeanDefinitionParser(DISPATCHER_TAG, new DispatcherBeanDefinitionParser()) + registerBeanDefinitionParser(CAMEL_SERVICE_TAG, new CamelServiceBeanDefinitionParser) + registerBeanDefinitionParser(ACTOR_FOR_TAG, new ActorForBeanDefinitionParser()); } } diff --git a/akka-spring/src/main/scala/AkkaSpringConfigurationTags.scala b/akka-spring/src/main/scala/AkkaSpringConfigurationTags.scala index 2743d772da..2d9807a806 100644 --- a/akka-spring/src/main/scala/AkkaSpringConfigurationTags.scala +++ b/akka-spring/src/main/scala/AkkaSpringConfigurationTags.scala @@ -19,6 +19,7 @@ object AkkaSpringConfigurationTags { val DISPATCHER_TAG = "dispatcher" val PROPERTYENTRY_TAG = "property" val CAMEL_SERVICE_TAG = "camel-service" + val ACTOR_FOR_TAG = "actor-for" // actor sub tags val REMOTE_TAG = "remote" @@ -45,6 +46,8 @@ object AkkaSpringConfigurationTags { val TRANSACTIONAL = "transactional" val HOST = "host" val PORT = "port" + val MANAGED_BY = "managed-by" + val SERVICE_NAME = "service-name" val LIFECYCLE = "lifecycle" val SCOPE = "scope" @@ -103,4 +106,8 @@ object AkkaSpringConfigurationTags { val THREAD_BASED = "thread-based" val HAWT = "hawt" + // managed by types + val SERVER_MANAGED = "server" + val CLIENT_MANAGED = "client" + } diff --git a/akka-spring/src/main/scala/BeanParser.scala b/akka-spring/src/main/scala/BeanParser.scala deleted file mode 100644 index 1bbba9f09f..0000000000 --- a/akka-spring/src/main/scala/BeanParser.scala +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ -package se.scalablesolutions.akka.spring - -import se.scalablesolutions.akka.util.Logging -import org.w3c.dom.Element -import org.springframework.util.xml.DomUtils - -/** - * Base trait with utility methods for bean parsing. - */ -trait BeanParser extends Logging { - - /** - * Get a mandatory element attribute. - * @param element the element with the mandatory attribute - * @param attribute name of the mandatory attribute - */ - def mandatory(element: Element, attribute: String): String = { - if ((element.getAttribute(attribute) == null) || (element.getAttribute(attribute).isEmpty)) { - throw new IllegalArgumentException("Mandatory attribute missing: " + attribute) - } else { - element.getAttribute(attribute) - } - } - - /** - * Get a mandatory child element. - * @param element the parent element - * @param childName name of the mandatory child element - */ - def mandatoryElement(element: Element, childName: String): Element = { - val childElement = DomUtils.getChildElementByTagName(element, childName); - if (childElement == null) { - throw new IllegalArgumentException("Mandatory element missing: ''") - } else { - childElement - } - } - -} diff --git a/akka-spring/src/main/scala/DispatcherParser.scala b/akka-spring/src/main/scala/DispatcherParser.scala deleted file mode 100644 index e9f10e1328..0000000000 --- a/akka-spring/src/main/scala/DispatcherParser.scala +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ -package se.scalablesolutions.akka.spring - -import org.w3c.dom.Element -import org.springframework.util.xml.DomUtils - -/** - * Parser trait for custom namespace for Akka dispatcher configuration. - * @author michaelkober - */ -trait DispatcherParser extends BeanParser { - import AkkaSpringConfigurationTags._ - - /** - * Parses the given element and returns a DispatcherProperties. - * @param element dom element to parse - * @return configuration for the dispatcher - */ - def parseDispatcher(element: Element): DispatcherProperties = { - val properties = new DispatcherProperties() - var dispatcherElement = element - if (hasRef(element)) { - val ref = element.getAttribute(REF) - dispatcherElement = element.getOwnerDocument.getElementById(ref) - if (dispatcherElement == null) { - throw new IllegalArgumentException("Referenced dispatcher not found: '" + ref + "'") - } - } - - properties.dispatcherType = mandatory(dispatcherElement, TYPE) - if (properties.dispatcherType == THREAD_BASED) { - val allowedParentNodes = "akka:typed-actor" :: "akka:untyped-actor" :: "typed-actor" :: "untyped-actor" :: Nil - if (!allowedParentNodes.contains(dispatcherElement.getParentNode.getNodeName)) { - throw new IllegalArgumentException("Thread based dispatcher must be nested in 'typed-actor' or 'untyped-actor' element!") - } - } - - if (properties.dispatcherType == HAWT) { // no name for HawtDispatcher - properties.name = dispatcherElement.getAttribute(NAME) - if (dispatcherElement.hasAttribute(AGGREGATE)) { - properties.aggregate = dispatcherElement.getAttribute(AGGREGATE).toBoolean - } - } else { - properties.name = mandatory(dispatcherElement, NAME) - } - - val threadPoolElement = DomUtils.getChildElementByTagName(dispatcherElement, THREAD_POOL_TAG); - if (threadPoolElement != null) { - if (properties.dispatcherType == REACTOR_BASED_SINGLE_THREAD_EVENT_DRIVEN || - properties.dispatcherType == THREAD_BASED) { - throw new IllegalArgumentException("Element 'thread-pool' not allowed for this dispatcher type.") - } - val threadPoolProperties = parseThreadPool(threadPoolElement) - properties.threadPool = threadPoolProperties - } - properties - } - - /** - * Parses the given element and returns a ThreadPoolProperties. - * @param element dom element to parse - * @return configuration for the thread pool - */ - def parseThreadPool(element: Element): ThreadPoolProperties = { - val properties = new ThreadPoolProperties() - properties.queue = element.getAttribute(QUEUE) - if (element.hasAttribute(CAPACITY)) { - properties.capacity = element.getAttribute(CAPACITY).toInt - } - if (element.hasAttribute(BOUND)) { - properties.bound = element.getAttribute(BOUND).toInt - } - if (element.hasAttribute(FAIRNESS)) { - properties.fairness = element.getAttribute(FAIRNESS).toBoolean - } - if (element.hasAttribute(CORE_POOL_SIZE)) { - properties.corePoolSize = element.getAttribute(CORE_POOL_SIZE).toInt - } - if (element.hasAttribute(MAX_POOL_SIZE)) { - properties.maxPoolSize = element.getAttribute(MAX_POOL_SIZE).toInt - } - if (element.hasAttribute(KEEP_ALIVE)) { - properties.keepAlive = element.getAttribute(KEEP_ALIVE).toLong - } - if (element.hasAttribute(REJECTION_POLICY)) { - properties.rejectionPolicy = element.getAttribute(REJECTION_POLICY) - } - if (element.hasAttribute(MAILBOX_CAPACITY)) { - properties.mailboxCapacity = element.getAttribute(MAILBOX_CAPACITY).toInt - } - properties - } - - def hasRef(element: Element): Boolean = { - val ref = element.getAttribute(REF) - (ref != null) && !ref.isEmpty - } - -} diff --git a/akka-spring/src/main/scala/PropertyEntries.scala b/akka-spring/src/main/scala/PropertyEntries.scala index bf1898a805..9a7dc098de 100644 --- a/akka-spring/src/main/scala/PropertyEntries.scala +++ b/akka-spring/src/main/scala/PropertyEntries.scala @@ -18,3 +18,19 @@ class PropertyEntries { entryList.append(entry) } } + +/** + * Represents a property element + * @author Johan Rask + */ +class PropertyEntry { + var name: String = _ + var value: String = null + var ref: String = null + + + override def toString(): String = { + format("name = %s,value = %s, ref = %s", name, value, ref) + } +} + diff --git a/akka-spring/src/main/scala/PropertyEntry.scala b/akka-spring/src/main/scala/PropertyEntry.scala deleted file mode 100644 index 9fe6357fc0..0000000000 --- a/akka-spring/src/main/scala/PropertyEntry.scala +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ -package se.scalablesolutions.akka.spring - -/** - * Represents a property element - * @author Johan Rask - */ -class PropertyEntry { - var name: String = _ - var value: String = null - var ref: String = null - - - override def toString(): String = { - format("name = %s,value = %s, ref = %s", name, value, ref) - } -} diff --git a/akka-spring/src/main/scala/TypedActorBeanDefinitionParser.scala b/akka-spring/src/main/scala/TypedActorBeanDefinitionParser.scala deleted file mode 100644 index e8e0cef7d4..0000000000 --- a/akka-spring/src/main/scala/TypedActorBeanDefinitionParser.scala +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ -package se.scalablesolutions.akka.spring - -import org.springframework.beans.factory.support.BeanDefinitionBuilder -import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser -import org.springframework.beans.factory.xml.ParserContext -import AkkaSpringConfigurationTags._ -import org.w3c.dom.Element - - -/** - * Parser for custom namespace configuration. - * @author michaelkober - */ -class TypedActorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { - /* - * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) - */ - override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { - val typedActorConf = parseActor(element) - typedActorConf.typed = TYPED_ACTOR_TAG - typedActorConf.setAsProperties(builder) - } - - /* - * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) - */ - override def getBeanClass(element: Element): Class[_] = classOf[ActorFactoryBean] -} diff --git a/akka-spring/src/main/scala/UntypedActorBeanDefinitionParser.scala b/akka-spring/src/main/scala/UntypedActorBeanDefinitionParser.scala deleted file mode 100644 index 752e18559f..0000000000 --- a/akka-spring/src/main/scala/UntypedActorBeanDefinitionParser.scala +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ -package se.scalablesolutions.akka.spring - -import org.springframework.beans.factory.support.BeanDefinitionBuilder -import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser -import org.springframework.beans.factory.xml.ParserContext -import AkkaSpringConfigurationTags._ -import org.w3c.dom.Element - - -/** - * Parser for custom namespace configuration. - * @author michaelkober - */ -class UntypedActorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser with ActorParser { - /* - * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) - */ - override def doParse(element: Element, parserContext: ParserContext, builder: BeanDefinitionBuilder) { - val untypedActorConf = parseActor(element) - untypedActorConf.typed = UNTYPED_ACTOR_TAG - untypedActorConf.setAsProperties(builder) - } - - /* - * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) - */ - override def getBeanClass(element: Element): Class[_] = classOf[ActorFactoryBean] -} diff --git a/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/IMyPojo.java b/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/IMyPojo.java index f2c5e24884..5a2a272e6c 100644 --- a/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/IMyPojo.java +++ b/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/IMyPojo.java @@ -8,14 +8,12 @@ package se.scalablesolutions.akka.spring.foo; * To change this template use File | Settings | File Templates. */ public interface IMyPojo { + public void oneWay(String message); + public String getFoo(); - public String getBar(); - - public void preRestart(); - - public void postRestart(); - public String longRunning(); + + } diff --git a/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/MyPojo.java b/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/MyPojo.java index fe3e9ba767..8f610eef63 100644 --- a/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/MyPojo.java +++ b/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/MyPojo.java @@ -1,42 +1,34 @@ package se.scalablesolutions.akka.spring.foo; -import se.scalablesolutions.akka.actor.*; +import se.scalablesolutions.akka.actor.TypedActor; -public class MyPojo extends TypedActor implements IMyPojo{ +import java.util.concurrent.CountDownLatch; - private String foo; - private String bar; +public class MyPojo extends TypedActor implements IMyPojo { + + public static CountDownLatch latch = new CountDownLatch(1); + public static String lastOneWayMessage = null; + private String foo = "foo"; - public MyPojo() { - this.foo = "foo"; - this.bar = "bar"; - } + public MyPojo() { + } + public String getFoo() { + return foo; + } - public String getFoo() { - return foo; - } + public void oneWay(String message) { + lastOneWayMessage = message; + latch.countDown(); + } - - public String getBar() { - return bar; - } - - public void preRestart() { - System.out.println("pre restart"); - } - - public void postRestart() { - System.out.println("post restart"); - } - - public String longRunning() { - try { - Thread.sleep(6000); - } catch (InterruptedException e) { - } - return "this took long"; + public String longRunning() { + try { + Thread.sleep(6000); + } catch (InterruptedException e) { } + return "this took long"; + } } diff --git a/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/PingActor.java b/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/PingActor.java index e447b26a28..3063a1b529 100644 --- a/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/PingActor.java +++ b/akka-spring/src/test/java/se/scalablesolutions/akka/spring/foo/PingActor.java @@ -6,6 +6,8 @@ import se.scalablesolutions.akka.actor.ActorRef; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import java.util.concurrent.CountDownLatch; + /** * test class @@ -14,6 +16,9 @@ public class PingActor extends UntypedActor implements ApplicationContextAware { private String stringFromVal; private String stringFromRef; + public static String lastMessage = null; + public static CountDownLatch latch = new CountDownLatch(1); + private boolean gotApplicationContext = false; @@ -42,7 +47,6 @@ public class PingActor extends UntypedActor implements ApplicationContextAware { stringFromRef = s; } - private String longRunning() { try { Thread.sleep(6000); @@ -53,12 +57,12 @@ public class PingActor extends UntypedActor implements ApplicationContextAware { public void onReceive(Object message) throws Exception { if (message instanceof String) { - System.out.println("Ping received String message: " + message); + lastMessage = (String) message; if (message.equals("longRunning")) { - System.out.println("### starting pong"); ActorRef pongActor = UntypedActor.actorOf(PongActor.class).start(); pongActor.sendRequestReply("longRunning", getContext()); } + latch.countDown(); } else { throw new IllegalArgumentException("Unknown message: " + message); } diff --git a/akka-spring/src/test/resources/server-managed-config.xml b/akka-spring/src/test/resources/server-managed-config.xml new file mode 100644 index 0000000000..128b16c8b6 --- /dev/null +++ b/akka-spring/src/test/resources/server-managed-config.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/akka-spring/src/test/resources/typed-actor-config.xml b/akka-spring/src/test/resources/typed-actor-config.xml index faca749469..989884e4fa 100644 --- a/akka-spring/src/test/resources/typed-actor-config.xml +++ b/akka-spring/src/test/resources/typed-actor-config.xml @@ -37,7 +37,7 @@ http://scalablesolutions.se/akka/akka-1.0-SNAPSHOT.xsd"> implementation="se.scalablesolutions.akka.spring.foo.MyPojo" timeout="2000" transactional="true"> - + - + + + + val props = parser.parseActor(dom(xml).getDocumentElement); + assert(props != null) + assert(props.host === "com.some.host") + assert(props.port === 9999) + assert(props.serviceName === "my-service") + assert(props.serverManaged) } } } diff --git a/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala b/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala index 8767b2e75a..3cdcd17cb0 100644 --- a/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala +++ b/akka-spring/src/test/scala/TypedActorSpringFeatureTest.scala @@ -4,10 +4,8 @@ package se.scalablesolutions.akka.spring -import foo.{IMyPojo, MyPojo} +import foo.{PingActor, IMyPojo, MyPojo} import se.scalablesolutions.akka.dispatch.FutureTimeoutException -import se.scalablesolutions.akka.remote.RemoteNode -import org.scalatest.FeatureSpec import org.scalatest.matchers.ShouldMatchers import org.scalatest.junit.JUnitRunner import org.junit.runner.RunWith @@ -16,13 +14,52 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader import org.springframework.context.ApplicationContext import org.springframework.context.support.ClassPathXmlApplicationContext import org.springframework.core.io.{ClassPathResource, Resource} +import org.scalatest.{BeforeAndAfterAll, FeatureSpec} +import se.scalablesolutions.akka.remote.{RemoteClient, RemoteServer, RemoteNode} +import java.util.concurrent.CountDownLatch +import se.scalablesolutions.akka.actor.{TypedActor, RemoteTypedActorOne, Actor} +import se.scalablesolutions.akka.actor.remote.RemoteTypedActorOneImpl /** * Tests for spring configuration of typed actors. * @author michaelkober */ @RunWith(classOf[JUnitRunner]) -class TypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers { +class TypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers with BeforeAndAfterAll { + + var server1: RemoteServer = null + var server2: RemoteServer = null + + override def beforeAll = { + val actor = Actor.actorOf[PingActor] // FIXME: remove this line when ticket 425 is fixed + server1 = new RemoteServer() + server1.start("localhost", 9990) + server2 = new RemoteServer() + server2.start("localhost", 9992) + + val typedActor = TypedActor.newInstance(classOf[RemoteTypedActorOne], classOf[RemoteTypedActorOneImpl], 1000) + server1.registerTypedActor("typed-actor-service", typedActor) + } + + // make sure the servers shutdown cleanly after the test has finished + override def afterAll = { + try { + server1.shutdown + server2.shutdown + RemoteClient.shutdownAll + Thread.sleep(1000) + } catch { + case e => () + } + } + + def getTypedActorFromContext(config: String, id: String) : IMyPojo = { + MyPojo.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext(config) + val myPojo: IMyPojo = context.getBean(id).asInstanceOf[IMyPojo] + myPojo + } + feature("parse Spring application context") { scenario("akka:typed-actor and akka:supervision and akka:dispatcher can be used as top level elements") { @@ -37,41 +74,79 @@ class TypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers { } scenario("get a typed actor") { - val context = new ClassPathXmlApplicationContext("/typed-actor-config.xml") - val myPojo = context.getBean("simple-typed-actor").asInstanceOf[IMyPojo] - var msg = myPojo.getFoo() - msg += myPojo.getBar() - assert(msg === "foobar") + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello 1") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello 1") } scenario("FutureTimeoutException when timed out") { - val context = new ClassPathXmlApplicationContext("/typed-actor-config.xml") - val myPojo = context.getBean("simple-typed-actor").asInstanceOf[IMyPojo] + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor") evaluating {myPojo.longRunning()} should produce[FutureTimeoutException] - } scenario("typed-actor with timeout") { - val context = new ClassPathXmlApplicationContext("/typed-actor-config.xml") - val myPojo = context.getBean("simple-typed-actor-long-timeout").asInstanceOf[IMyPojo] + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "simple-typed-actor-long-timeout") assert(myPojo.longRunning() === "this took long"); } scenario("transactional typed-actor") { - val context = new ClassPathXmlApplicationContext("/typed-actor-config.xml") - val myPojo = context.getBean("transactional-typed-actor").asInstanceOf[IMyPojo] - var msg = myPojo.getFoo() - msg += myPojo.getBar() - assert(msg === "foobar") + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "transactional-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello 2") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello 2") } scenario("get a remote typed-actor") { - RemoteNode.start - Thread.sleep(1000) - val context = new ClassPathXmlApplicationContext("/typed-actor-config.xml") - val myPojo = context.getBean("remote-typed-actor").asInstanceOf[IMyPojo] - assert(myPojo.getFoo === "foo") + val myPojo = getTypedActorFromContext("/typed-actor-config.xml", "remote-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello 3") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello 3") } + + scenario("get a client-managed-remote-typed-actor") { + val myPojo = getTypedActorFromContext("/server-managed-config.xml", "client-managed-remote-typed-actor") + assert(myPojo.getFoo() === "foo") + myPojo.oneWay("hello client-managed-remote-typed-actor") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello client-managed-remote-typed-actor") + } + + scenario("get a server-managed-remote-typed-actor") { + val serverPojo = getTypedActorFromContext("/server-managed-config.xml", "server-managed-remote-typed-actor") + // + val myPojoProxy = RemoteClient.typedActorFor(classOf[IMyPojo], classOf[IMyPojo].getName, 5000L, "localhost", 9990) + assert(myPojoProxy.getFoo() === "foo") + myPojoProxy.oneWay("hello server-managed-remote-typed-actor") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello server-managed-remote-typed-actor") + } + + scenario("get a server-managed-remote-typed-actor-custom-id") { + val serverPojo = getTypedActorFromContext("/server-managed-config.xml", "server-managed-remote-typed-actor-custom-id") + // + val myPojoProxy = RemoteClient.typedActorFor(classOf[IMyPojo], "mypojo-service", 5000L, "localhost", 9990) + assert(myPojoProxy.getFoo() === "foo") + myPojoProxy.oneWay("hello server-managed-remote-typed-actor 2") + MyPojo.latch.await + assert(MyPojo.lastOneWayMessage === "hello server-managed-remote-typed-actor 2") + } + + scenario("get a client proxy for server-managed-remote-typed-actor") { + MyPojo.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext("/server-managed-config.xml") + val myPojo: IMyPojo = context.getBean("server-managed-remote-typed-actor-custom-id").asInstanceOf[IMyPojo] + // get client proxy from spring context + val myPojoProxy = context.getBean("typed-client-1").asInstanceOf[IMyPojo] + assert(myPojoProxy.getFoo() === "foo") + myPojoProxy.oneWay("hello") + MyPojo.latch.await + } + + } } diff --git a/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala b/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala index cf7d8d9805..0397d30bf0 100644 --- a/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala +++ b/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala @@ -6,74 +6,146 @@ package se.scalablesolutions.akka.spring import foo.PingActor import se.scalablesolutions.akka.dispatch.ExecutorBasedEventDrivenWorkStealingDispatcher -import se.scalablesolutions.akka.remote.RemoteNode -import se.scalablesolutions.akka.actor.ActorRef -import org.scalatest.FeatureSpec import org.scalatest.matchers.ShouldMatchers import org.scalatest.junit.JUnitRunner import org.junit.runner.RunWith -import org.springframework.context.ApplicationContext import org.springframework.context.support.ClassPathXmlApplicationContext +import se.scalablesolutions.akka.remote.{RemoteClient, RemoteServer} +import org.scalatest.{BeforeAndAfterAll, FeatureSpec} +import java.util.concurrent.CountDownLatch +import se.scalablesolutions.akka.actor.{RemoteActorRef, ActorRegistry, Actor, ActorRef} /** * Tests for spring configuration of typed actors. * @author michaelkober */ @RunWith(classOf[JUnitRunner]) -class UntypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers { +class UntypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers with BeforeAndAfterAll { + + var server1: RemoteServer = null + var server2: RemoteServer = null + + + override def beforeAll = { + val actor = Actor.actorOf[PingActor] // FIXME: remove this line when ticket 425 is fixed + server1 = new RemoteServer() + server1.start("localhost", 9990) + server2 = new RemoteServer() + server2.start("localhost", 9992) + } + + // make sure the servers shutdown cleanly after the test has finished + override def afterAll = { + try { + server1.shutdown + server2.shutdown + RemoteClient.shutdownAll + Thread.sleep(1000) + } catch { + case e => () + } + } + + + def getPingActorFromContext(config: String, id: String) : ActorRef = { + PingActor.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext(config) + val pingActor = context.getBean(id).asInstanceOf[ActorRef] + assert(pingActor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") + pingActor.start() + } + + feature("parse Spring application context") { scenario("get a untyped actor") { - val context = new ClassPathXmlApplicationContext("/untyped-actor-config.xml") - val myactor = context.getBean("simple-untyped-actor").asInstanceOf[ActorRef] - assert(myactor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") - myactor.start() + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "simple-untyped-actor") myactor.sendOneWay("Hello") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello") assert(myactor.isDefinedAt("some string message")) } scenario("untyped-actor with timeout") { - val context = new ClassPathXmlApplicationContext("/untyped-actor-config.xml") - val myactor = context.getBean("simple-untyped-actor-long-timeout").asInstanceOf[ActorRef] - assert(myactor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") - myactor.start() - myactor.sendOneWay("Hello") + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "simple-untyped-actor-long-timeout") assert(myactor.getTimeout() === 10000) + myactor.sendOneWay("Hello 2") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 2") } scenario("transactional untyped-actor") { - val context = new ClassPathXmlApplicationContext("/untyped-actor-config.xml") - val myactor = context.getBean("transactional-untyped-actor").asInstanceOf[ActorRef] - assert(myactor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") - myactor.start() - myactor.sendOneWay("Hello") - assert(myactor.isDefinedAt("some string message")) + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "transactional-untyped-actor") + myactor.sendOneWay("Hello 3") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 3") } scenario("get a remote typed-actor") { - RemoteNode.start - Thread.sleep(1000) - val context = new ClassPathXmlApplicationContext("/untyped-actor-config.xml") - val myactor = context.getBean("remote-untyped-actor").asInstanceOf[ActorRef] - assert(myactor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") - myactor.start() - myactor.sendOneWay("Hello") - assert(myactor.isDefinedAt("some string message")) + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "remote-untyped-actor") + myactor.sendOneWay("Hello 4") assert(myactor.getRemoteAddress().isDefined) assert(myactor.getRemoteAddress().get.getHostName() === "localhost") - assert(myactor.getRemoteAddress().get.getPort() === 9999) + assert(myactor.getRemoteAddress().get.getPort() === 9992) + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 4") } scenario("untyped-actor with custom dispatcher") { - val context = new ClassPathXmlApplicationContext("/untyped-actor-config.xml") - val myactor = context.getBean("untyped-actor-with-dispatcher").asInstanceOf[ActorRef] - assert(myactor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") - myactor.start() - myactor.sendOneWay("Hello") + val myactor = getPingActorFromContext("/untyped-actor-config.xml", "untyped-actor-with-dispatcher") assert(myactor.getTimeout() === 1000) assert(myactor.getDispatcher.isInstanceOf[ExecutorBasedEventDrivenWorkStealingDispatcher]) + myactor.sendOneWay("Hello 5") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello 5") } + + scenario("create client managed remote untyped-actor") { + val myactor = getPingActorFromContext("/server-managed-config.xml", "client-managed-remote-untyped-actor") + myactor.sendOneWay("Hello client managed remote untyped-actor") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello client managed remote untyped-actor") + assert(myactor.getRemoteAddress().isDefined) + assert(myactor.getRemoteAddress().get.getHostName() === "localhost") + assert(myactor.getRemoteAddress().get.getPort() === 9990) + } + + scenario("create server managed remote untyped-actor") { + val myactor = getPingActorFromContext("/server-managed-config.xml", "server-managed-remote-untyped-actor") + val nrOfActors = ActorRegistry.actors.length + val actorRef = RemoteClient.actorFor("se.scalablesolutions.akka.spring.foo.PingActor", "localhost", 9990) + actorRef.sendOneWay("Hello server managed remote untyped-actor") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello server managed remote untyped-actor") + assert(ActorRegistry.actors.length === nrOfActors) + } + + scenario("create server managed remote untyped-actor with custom service id") { + val myactor = getPingActorFromContext("/server-managed-config.xml", "server-managed-remote-untyped-actor-custom-id") + val nrOfActors = ActorRegistry.actors.length + val actorRef = RemoteClient.actorFor("ping-service", "localhost", 9990) + actorRef.sendOneWay("Hello server managed remote untyped-actor") + PingActor.latch.await + assert(PingActor.lastMessage === "Hello server managed remote untyped-actor") + assert(ActorRegistry.actors.length === nrOfActors) + } + + scenario("get client actor for server managed remote untyped-actor") { + PingActor.latch = new CountDownLatch(1) + val context = new ClassPathXmlApplicationContext("/server-managed-config.xml") + val pingActor = context.getBean("server-managed-remote-untyped-actor-custom-id").asInstanceOf[ActorRef] + assert(pingActor.getActorClassName() === "se.scalablesolutions.akka.spring.foo.PingActor") + pingActor.start() + val nrOfActors = ActorRegistry.actors.length + // get client actor ref from spring context + val actorRef = context.getBean("client-1").asInstanceOf[ActorRef] + assert(actorRef.isInstanceOf[RemoteActorRef]) + actorRef.sendOneWay("Hello") + PingActor.latch.await + assert(ActorRegistry.actors.length === nrOfActors) + } + } } diff --git a/akka-typed-actor/src/main/scala/actor/TypedActor.scala b/akka-typed-actor/src/main/scala/actor/TypedActor.scala index 385c1831a4..7d393070ec 100644 --- a/akka-typed-actor/src/main/scala/actor/TypedActor.scala +++ b/akka-typed-actor/src/main/scala/actor/TypedActor.scala @@ -389,7 +389,8 @@ object TypedActor extends Logging { typedActor.initialize(proxy) if (config._messageDispatcher.isDefined) actorRef.dispatcher = config._messageDispatcher.get if (config._threadBasedDispatcher.isDefined) actorRef.dispatcher = Dispatchers.newThreadBasedDispatcher(actorRef) - AspectInitRegistry.register(proxy, AspectInit(intfClass, typedActor, actorRef, None, config.timeout)) + if (config._host.isDefined) actorRef.makeRemote(config._host.get) + AspectInitRegistry.register(proxy, AspectInit(intfClass, typedActor, actorRef, config._host, config.timeout)) actorRef.start proxy.asInstanceOf[T] } diff --git a/akka-typed-actor/src/test/resources/META-INF/aop.xml b/akka-typed-actor/src/test/resources/META-INF/aop.xml index bdc167ca54..be133a51b8 100644 --- a/akka-typed-actor/src/test/resources/META-INF/aop.xml +++ b/akka-typed-actor/src/test/resources/META-INF/aop.xml @@ -2,6 +2,7 @@ + From 59ca2b4055d053163914ad1441bd33c22078902b Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Thu, 9 Sep 2010 14:31:18 +0530 Subject: [PATCH 06/25] Refactor mongodb module to confirm to Redis and Cassandra. Issue #430 --- .../src/main/scala/MongoStorage.scala | 8 +- .../src/main/scala/MongoStorageBackend.scala | 398 ++++++--------- .../test/scala/MongoPersistentActorSpec.scala | 224 ++++----- .../src/test/scala/MongoStorageSpec.scala | 470 +++++------------- .../time-2.8.0-0.2-SNAPSHOT.jar | Bin 0 -> 88160 bytes .../sjson-0.8-SNAPSHOT-2.8.0.jar | Bin 0 -> 181174 bytes project/build/AkkaProject.scala | 13 +- 7 files changed, 392 insertions(+), 721 deletions(-) create mode 100644 embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.jar create mode 100644 embedded-repo/sjson/json/sjson/0.8-SNAPSHOT-2.8.0/sjson-0.8-SNAPSHOT-2.8.0.jar diff --git a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala index 98776253a5..79cacfeb07 100644 --- a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala +++ b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala @@ -9,7 +9,7 @@ import se.scalablesolutions.akka.persistence.common._ import se.scalablesolutions.akka.util.UUID object MongoStorage extends Storage { - type ElementType = AnyRef + type ElementType = Array[Byte] def newMap: PersistentMap[ElementType, ElementType] = newMap(UUID.newUuid.toString) def newVector: PersistentVector[ElementType] = newVector(UUID.newUuid.toString) @@ -29,7 +29,7 @@ object MongoStorage extends Storage { * * @author Debasish Ghosh */ -class MongoPersistentMap(id: String) extends PersistentMap[AnyRef, AnyRef] { +class MongoPersistentMap(id: String) extends PersistentMap[Array[Byte], Array[Byte]] { val uuid = id val storage = MongoStorageBackend } @@ -40,12 +40,12 @@ class MongoPersistentMap(id: String) extends PersistentMap[AnyRef, AnyRef] { * * @author Debaissh Ghosh */ -class MongoPersistentVector(id: String) extends PersistentVector[AnyRef] { +class MongoPersistentVector(id: String) extends PersistentVector[Array[Byte]] { val uuid = id val storage = MongoStorageBackend } -class MongoPersistentRef(id: String) extends PersistentRef[AnyRef] { +class MongoPersistentRef(id: String) extends PersistentRef[Array[Byte]] { val uuid = id val storage = MongoStorageBackend } diff --git a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala index 950165567d..847c226630 100644 --- a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala +++ b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala @@ -9,13 +9,8 @@ import se.scalablesolutions.akka.persistence.common._ import se.scalablesolutions.akka.util.Logging import se.scalablesolutions.akka.config.Config.config -import sjson.json.Serializer._ - import java.util.NoSuchElementException - -import com.mongodb._ - -import java.util.{Map=>JMap, List=>JList, ArrayList=>JArrayList} +import com.novus.casbah.mongodb.Imports._ /** * A module for supporting MongoDB based persistence. @@ -28,294 +23,189 @@ import java.util.{Map=>JMap, List=>JList, ArrayList=>JArrayList} * @author Debasish Ghosh */ private[akka] object MongoStorageBackend extends - MapStorageBackend[AnyRef, AnyRef] with - VectorStorageBackend[AnyRef] with - RefStorageBackend[AnyRef] with + MapStorageBackend[Array[Byte], Array[Byte]] with + VectorStorageBackend[Array[Byte]] with + RefStorageBackend[Array[Byte]] with Logging { - // enrich with null safe findOne - class RichDBCollection(value: DBCollection) { - def findOneNS(o: DBObject): Option[DBObject] = { - value.findOne(o) match { - case null => None - case x => Some(x) - } - } - } - - implicit def enrichDBCollection(c: DBCollection) = new RichDBCollection(c) - - val KEY = "key" - val VALUE = "value" + val KEY = "__key" + val REF = "__ref" val COLLECTION = "akka_coll" - val MONGODB_SERVER_HOSTNAME = config.getString("akka.storage.mongodb.hostname", "127.0.0.1") - val MONGODB_SERVER_DBNAME = config.getString("akka.storage.mongodb.dbname", "testdb") - val MONGODB_SERVER_PORT = config.getInt("akka.storage.mongodb.port", 27017) + val HOSTNAME = config.getString("akka.storage.mongodb.hostname", "127.0.0.1") + val DBNAME = config.getString("akka.storage.mongodb.dbname", "testdb") + val PORT = config.getInt("akka.storage.mongodb.port", 27017) - val db = new Mongo(MONGODB_SERVER_HOSTNAME, MONGODB_SERVER_PORT) - val coll = db.getDB(MONGODB_SERVER_DBNAME).getCollection(COLLECTION) + val db: MongoDB = MongoConnection(HOSTNAME, PORT)(DBNAME) + val coll: MongoCollection = db(COLLECTION) - private[this] val serializer = SJSON + def drop() { db.dropDatabase() } - def insertMapStorageEntryFor(name: String, key: AnyRef, value: AnyRef) { + def insertMapStorageEntryFor(name: String, key: Array[Byte], value: Array[Byte]) { insertMapStorageEntriesFor(name, List((key, value))) } - def insertMapStorageEntriesFor(name: String, entries: List[Tuple2[AnyRef, AnyRef]]) { - import java.util.{Map, HashMap} - - val m: Map[AnyRef, AnyRef] = new HashMap - for ((k, v) <- entries) { - m.put(k, serializer.out(v)) - } - - nullSafeFindOne(name) match { - case None => - coll.insert(new BasicDBObject().append(KEY, name).append(VALUE, m)) - case Some(dbo) => { - // collate the maps - val o = dbo.get(VALUE).asInstanceOf[Map[AnyRef, AnyRef]] - o.putAll(m) - - val newdbo = new BasicDBObject().append(KEY, name).append(VALUE, o) - coll.update(new BasicDBObject().append(KEY, name), newdbo, true, false) - } + def insertMapStorageEntriesFor(name: String, entries: List[(Array[Byte], Array[Byte])]) { + val q: DBObject = MongoDBObject(KEY -> name) + coll.findOne(q) match { + case Some(dbo) => + entries.foreach { case (k, v) => dbo += new String(k) -> v } + coll.update(q, dbo, true, false) + case None => + val builder = MongoDBObject.newBuilder + builder += KEY -> name + entries.foreach { case (k, v) => builder += new String(k) -> v } + coll += builder.result.asDBObject } } def removeMapStorageFor(name: String): Unit = { - val q = new BasicDBObject - q.put(KEY, name) + val q: DBObject = MongoDBObject(KEY -> name) coll.remove(q) } - def removeMapStorageFor(name: String, key: AnyRef): Unit = { - nullSafeFindOne(name) match { - case None => - case Some(dbo) => { - val orig = dbo.get(VALUE).asInstanceOf[DBObject].toMap - if (key.isInstanceOf[List[_]]) { - val keys = key.asInstanceOf[List[_]] - keys.foreach(k => orig.remove(k.asInstanceOf[String])) - } else { - orig.remove(key.asInstanceOf[String]) - } - - // remove existing reference - removeMapStorageFor(name) - // and insert - coll.insert(new BasicDBObject().append(KEY, name).append(VALUE, orig)) - } - } + private def queryFor[T](name: String)(body: (MongoDBObject, MongoDBObject) => T): T = { + val q: DBObject = MongoDBObject(KEY -> name) + val dbo = coll.findOne(q).getOrElse { throw new NoSuchElementException(name + " not present") } + body(q, dbo) } - def getMapStorageEntryFor(name: String, key: AnyRef): Option[AnyRef] = - getValueForKey(name, key.asInstanceOf[String]) - - def getMapStorageSizeFor(name: String): Int = { - nullSafeFindOne(name) match { - case None => 0 - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JMap[String, AnyRef]].keySet.size - } + def removeMapStorageFor(name: String, key: Array[Byte]): Unit = queryFor(name) { (q, dbo) => + dbo -= new String(key) + coll.update(q, dbo, true, false) } - def getMapStorageFor(name: String): List[Tuple2[AnyRef, AnyRef]] = { - val m = - nullSafeFindOne(name) match { - case None => - throw new NoSuchElementException(name + " not present") - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JMap[String, AnyRef]] - } - val n = - List(m.keySet.toArray: _*).asInstanceOf[List[String]] - val vals = - for(s <- n) - yield (s, serializer.in[AnyRef](m.get(s).asInstanceOf[Array[Byte]])) - vals.asInstanceOf[List[Tuple2[String, AnyRef]]] + def getMapStorageEntryFor(name: String, key: Array[Byte]): Option[Array[Byte]] = queryFor(name) { (q, dbo) => + dbo.get(new String(key)).asInstanceOf[Option[Array[Byte]]] } - def getMapStorageRangeFor(name: String, start: Option[AnyRef], - finish: Option[AnyRef], - count: Int): List[Tuple2[AnyRef, AnyRef]] = { - val m = - nullSafeFindOne(name) match { - case None => - throw new NoSuchElementException(name + " not present") - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JMap[String, AnyRef]] - } - - /** - * count is the max number of results to return. Start with - * start or 0 (if start is not defined) and go until - * you hit finish or count. - */ - val s = if (start.isDefined) start.get.asInstanceOf[Int] else 0 - val cnt = - if (finish.isDefined) { - val f = finish.get.asInstanceOf[Int] - if (f >= s) math.min(count, (f - s)) else count - } - else count - - val n = - List(m.keySet.toArray: _*).asInstanceOf[List[String]].sortWith((e1, e2) => (e1 compareTo e2) < 0).slice(s, s + cnt) - val vals = - for(s <- n) - yield (s, serializer.in[AnyRef](m.get(s).asInstanceOf[Array[Byte]])) - vals.asInstanceOf[List[Tuple2[String, AnyRef]]] + def getMapStorageSizeFor(name: String): Int = queryFor(name) { (q, dbo) => + dbo.size - 2 // need to exclude object id and our KEY } - private def getValueForKey(name: String, key: String): Option[AnyRef] = { - try { - nullSafeFindOne(name) match { - case None => None - case Some(dbo) => - Some(serializer.in[AnyRef]( - dbo.get(VALUE) - .asInstanceOf[JMap[String, AnyRef]] - .get(key).asInstanceOf[Array[Byte]])) - } - } catch { - case e => - throw new NoSuchElementException(e.getMessage) - } + def getMapStorageFor(name: String): List[(Array[Byte], Array[Byte])] = queryFor(name) { (q, dbo) => + for { + (k, v) <- dbo.toList + if k != "_id" && k != KEY + } yield (k.getBytes, v.asInstanceOf[Array[Byte]]) } - def insertVectorStorageEntriesFor(name: String, elements: List[AnyRef]) = { - val q = new BasicDBObject - q.put(KEY, name) + def getMapStorageRangeFor(name: String, start: Option[Array[Byte]], + finish: Option[Array[Byte]], + count: Int): List[(Array[Byte], Array[Byte])] = queryFor(name) { (q, dbo) => + // get all keys except the special ones + val keys = + dbo.keySet + .toList + .filter(k => k != "_id" && k != KEY) + .sortWith(_ < _) - val currentList = - coll.findOneNS(q) match { - case None => - new JArrayList[AnyRef] - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JArrayList[AnyRef]] - } - if (!currentList.isEmpty) { - // record exists - // remove before adding - coll.remove(q) - } + // if the supplied start is not defined, get the head of keys + val s = start.map(new String(_)).getOrElse(keys.head) - // add to the current list - elements.map(serializer.out(_)).foreach(currentList.add(_)) + // if the supplied finish is not defined, get the last element of keys + val f = finish.map(new String(_)).getOrElse(keys.last) - coll.insert( - new BasicDBObject() - .append(KEY, name) - .append(VALUE, currentList) - ) + // slice from keys: both ends inclusive + val ks = keys.slice(keys.indexOf(s), scala.math.min(count, keys.indexOf(f) + 1)) + ks.map(k => (k.getBytes, dbo.get(k).get.asInstanceOf[Array[Byte]])) } - def insertVectorStorageEntryFor(name: String, element: AnyRef) = { + def insertVectorStorageEntryFor(name: String, element: Array[Byte]) = { insertVectorStorageEntriesFor(name, List(element)) } - def getVectorStorageEntryFor(name: String, index: Int): AnyRef = { - try { - val o = - nullSafeFindOne(name) match { - case None => - throw new NoSuchElementException(name + " not present") + def insertVectorStorageEntriesFor(name: String, elements: List[Array[Byte]]) = { + // lookup with name + val q: DBObject = MongoDBObject(KEY -> name) - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JList[AnyRef]] + coll.findOne(q) match { + // exists : need to update + case Some(dbo) => + dbo -= KEY + dbo -= "_id" + val listBuilder = MongoDBList.newBuilder + + // expensive! + listBuilder ++= (elements ++ dbo.toSeq.sortWith((e1, e2) => (e1._1.toInt < e2._1.toInt)).map(_._2)) + + val builder = MongoDBObject.newBuilder + builder += KEY -> name + builder ++= listBuilder.result + coll.update(q, builder.result.asDBObject, true, false) + + // new : just add + case None => + val listBuilder = MongoDBList.newBuilder + listBuilder ++= elements + + val builder = MongoDBObject.newBuilder + builder += KEY -> name + builder ++= listBuilder.result + coll += builder.result.asDBObject + } + } + + def updateVectorStorageEntryFor(name: String, index: Int, elem: Array[Byte]) = queryFor(name) { (q, dbo) => + dbo += ((index.toString, elem)) + coll.update(q, dbo, true, false) + } + + def getVectorStorageEntryFor(name: String, index: Int): Array[Byte] = queryFor(name) { (q, dbo) => + dbo(index.toString).asInstanceOf[Array[Byte]] + } + + /** + * if start and finish both are defined, ignore count and + * report the range [start, finish) + * if start is not defined, assume start = 0 + * if start == 0 and finish == 0, return an empty collection + */ + def getVectorStorageRangeFor(name: String, start: Option[Int], finish: Option[Int], count: Int): List[Array[Byte]] = queryFor(name) { (q, dbo) => + val ls = dbo.filter { case (k, v) => k != KEY && k != "_id" } + .toSeq + .sortWith((e1, e2) => (e1._1.toInt < e2._1.toInt)) + .map(_._2) + + val st = start.getOrElse(0) + val cnt = + if (finish.isDefined) { + val f = finish.get + if (f >= st) (f - st) else count } - serializer.in[AnyRef]( - o.get(index).asInstanceOf[Array[Byte]]) - } catch { - case e => - throw new NoSuchElementException(e.getMessage) + else count + if (st == 0 && cnt == 0) List() + ls.slice(st, st + cnt).asInstanceOf[List[Array[Byte]]] + } + + def getVectorStorageSizeFor(name: String): Int = queryFor(name) { (q, dbo) => + dbo.size - 2 + } + + def insertRefStorageFor(name: String, element: Array[Byte]) = { + // lookup with name + val q: DBObject = MongoDBObject(KEY -> name) + + coll.findOne(q) match { + // exists : need to update + case Some(dbo) => + dbo += ((REF, element)) + coll.update(q, dbo, true, false) + + // not found : make one + case None => + val builder = MongoDBObject.newBuilder + builder += KEY -> name + builder += REF -> element + coll += builder.result.asDBObject } } - def getVectorStorageRangeFor(name: String, - start: Option[Int], finish: Option[Int], count: Int): List[AnyRef] = { - try { - val o = - nullSafeFindOne(name) match { - case None => - throw new NoSuchElementException(name + " not present") - - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JList[AnyRef]] - } - - val s = if (start.isDefined) start.get else 0 - val cnt = - if (finish.isDefined) { - val f = finish.get - if (f >= s) (f - s) else count - } - else count - - // pick the subrange and make a Scala list - val l = - List(o.subList(s, s + cnt).toArray: _*) - - for(e <- l) - yield serializer.in[AnyRef](e.asInstanceOf[Array[Byte]]) - } catch { - case e => - throw new NoSuchElementException(e.getMessage) - } - } - - def updateVectorStorageEntryFor(name: String, index: Int, elem: AnyRef) = { - val q = new BasicDBObject - q.put(KEY, name) - - val dbobj = - coll.findOneNS(q) match { - case None => - throw new NoSuchElementException(name + " not present") - case Some(dbo) => dbo - } - val currentList = dbobj.get(VALUE).asInstanceOf[JArrayList[AnyRef]] - currentList.set(index, serializer.out(elem)) - coll.update(q, - new BasicDBObject().append(KEY, name).append(VALUE, currentList)) - } - - def getVectorStorageSizeFor(name: String): Int = { - nullSafeFindOne(name) match { - case None => 0 - case Some(dbo) => - dbo.get(VALUE).asInstanceOf[JList[AnyRef]].size - } - } - - private def nullSafeFindOne(name: String): Option[DBObject] = { - val o = new BasicDBObject - o.put(KEY, name) - coll.findOneNS(o) - } - - def insertRefStorageFor(name: String, element: AnyRef) = { - nullSafeFindOne(name) match { - case None => - case Some(dbo) => { - val q = new BasicDBObject - q.put(KEY, name) - coll.remove(q) - } - } - coll.insert( - new BasicDBObject() - .append(KEY, name) - .append(VALUE, serializer.out(element))) - } - - def getRefStorageFor(name: String): Option[AnyRef] = { - nullSafeFindOne(name) match { - case None => None - case Some(dbo) => - Some(serializer.in[AnyRef](dbo.get(VALUE).asInstanceOf[Array[Byte]])) + def getRefStorageFor(name: String): Option[Array[Byte]] = try { + queryFor(name) { (q, dbo) => + dbo.get(REF).asInstanceOf[Option[Array[Byte]]] } + } catch { + case e: java.util.NoSuchElementException => None } } diff --git a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoPersistentActorSpec.scala b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoPersistentActorSpec.scala index 1acc9ee97d..01f735b254 100644 --- a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoPersistentActorSpec.scala +++ b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoPersistentActorSpec.scala @@ -1,32 +1,19 @@ package se.scalablesolutions.akka.persistence.mongo -import org.junit.{Test, Before} -import org.junit.Assert._ -import org.scalatest.junit.JUnitSuite - -import _root_.dispatch.json.{JsNumber, JsValue} -import _root_.dispatch.json.Js._ +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.BeforeAndAfterEach +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith import se.scalablesolutions.akka.actor.{Transactor, Actor, ActorRef} import Actor._ -/** - * A persistent actor based on MongoDB storage. - *

- * Demonstrates a bank account operation consisting of messages that: - *

  • checks balance Balance
  • - *
  • debits amountDebit
  • - *
  • debits multiple amountsMultiDebit
  • - *
  • credits amountCredit
  • - *

    - * Needs a running Mongo server. - * @author Debasish Ghosh - */ case class Balance(accountNo: String) -case class Debit(accountNo: String, amount: BigInt, failer: ActorRef) -case class MultiDebit(accountNo: String, amounts: List[BigInt], failer: ActorRef) -case class Credit(accountNo: String, amount: BigInt) +case class Debit(accountNo: String, amount: Int, failer: ActorRef) +case class MultiDebit(accountNo: String, amounts: List[Int], failer: ActorRef) +case class Credit(accountNo: String, amount: Int) case class Log(start: Int, finish: Int) case object LogSize @@ -35,63 +22,65 @@ class BankAccountActor extends Transactor { private lazy val accountState = MongoStorage.newMap private lazy val txnLog = MongoStorage.newVector + import sjson.json.DefaultProtocol._ + import sjson.json.JsonSerialization._ + def receive: Receive = { // check balance case Balance(accountNo) => - txnLog.add("Balance:" + accountNo) - self.reply(accountState.get(accountNo).get) + txnLog.add(("Balance:" + accountNo).getBytes) + self.reply( + accountState.get(accountNo.getBytes) + .map(frombinary[Int](_)) + .getOrElse(0)) // debit amount: can fail case Debit(accountNo, amount, failer) => - txnLog.add("Debit:" + accountNo + " " + amount) + txnLog.add(("Debit:" + accountNo + " " + amount).getBytes) + val m = accountState.get(accountNo.getBytes) + .map(frombinary[Int](_)) + .getOrElse(0) + + accountState.put(accountNo.getBytes, tobinary(m - amount)) + if (amount > m) failer !! "Failure" - val m: BigInt = - accountState.get(accountNo) match { - case Some(JsNumber(n)) => - BigInt(n.asInstanceOf[BigDecimal].intValue) - case None => 0 - } - accountState.put(accountNo, (m - amount)) - if (amount > m) - failer !! "Failure" self.reply(m - amount) // many debits: can fail // demonstrates true rollback even if multiple puts have been done case MultiDebit(accountNo, amounts, failer) => - txnLog.add("MultiDebit:" + accountNo + " " + amounts.map(_.intValue).foldLeft(0)(_ + _)) + val sum = amounts.foldRight(0)(_ + _) + txnLog.add(("MultiDebit:" + accountNo + " " + sum).getBytes) - val m: BigInt = - accountState.get(accountNo) match { - case Some(JsNumber(n)) => BigInt(n.toString) - case None => 0 + val m = accountState.get(accountNo.getBytes) + .map(frombinary[Int](_)) + .getOrElse(0) + + var cbal = m + amounts.foreach { amount => + accountState.put(accountNo.getBytes, tobinary(m - amount)) + cbal = cbal - amount + if (cbal < 0) failer !! "Failure" } - var bal: BigInt = 0 - amounts.foreach {amount => - bal = bal + amount - accountState.put(accountNo, (m - bal)) - } - if (bal > m) failer !! "Failure" - self.reply(m - bal) + + self.reply(m - sum) // credit amount case Credit(accountNo, amount) => - txnLog.add("Credit:" + accountNo + " " + amount) + txnLog.add(("Credit:" + accountNo + " " + amount).getBytes) + val m = accountState.get(accountNo.getBytes) + .map(frombinary[Int](_)) + .getOrElse(0) + + accountState.put(accountNo.getBytes, tobinary(m + amount)) - val m: BigInt = - accountState.get(accountNo) match { - case Some(JsNumber(n)) => - BigInt(n.asInstanceOf[BigDecimal].intValue) - case None => 0 - } - accountState.put(accountNo, (m + amount)) self.reply(m + amount) case LogSize => - self.reply(txnLog.length.asInstanceOf[AnyRef]) + self.reply(txnLog.length) case Log(start, finish) => - self.reply(txnLog.slice(start, finish)) + self.reply(txnLog.slice(start, finish).map(new String(_))) } } @@ -102,82 +91,71 @@ class BankAccountActor extends Transactor { } } -class MongoPersistentActorSpec extends JUnitSuite { - @Test - def testSuccessfulDebit = { - val bactor = actorOf[BankAccountActor] - bactor.start - val failer = actorOf[PersistentFailerActor] - failer.start - bactor !! Credit("a-123", 5000) - bactor !! Debit("a-123", 3000, failer) +@RunWith(classOf[JUnitRunner]) +class MongoPersistentActorSpec extends + Spec with + ShouldMatchers with + BeforeAndAfterEach { - val JsNumber(b) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(2000), BigInt(b.intValue)) - - bactor !! Credit("a-123", 7000) - - val JsNumber(b1) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(9000), BigInt(b1.intValue)) - - bactor !! Debit("a-123", 8000, failer) - - val JsNumber(b2) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(1000), BigInt(b2.intValue)) - - assert(7 == (bactor !! LogSize).get.asInstanceOf[Int]) - - import scala.collection.mutable.ArrayBuffer - assert((bactor !! Log(0, 7)).get.asInstanceOf[ArrayBuffer[String]].size == 7) - assert((bactor !! Log(0, 0)).get.asInstanceOf[ArrayBuffer[String]].size == 0) - assert((bactor !! Log(1, 2)).get.asInstanceOf[ArrayBuffer[String]].size == 1) - assert((bactor !! Log(6, 7)).get.asInstanceOf[ArrayBuffer[String]].size == 1) - assert((bactor !! Log(0, 1)).get.asInstanceOf[ArrayBuffer[String]].size == 1) + override def beforeEach { + MongoStorageBackend.drop } - @Test - def testUnsuccessfulDebit = { - val bactor = actorOf[BankAccountActor] - bactor.start - bactor !! Credit("a-123", 5000) - - val JsNumber(b) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(5000), BigInt(b.intValue)) - - val failer = actorOf[PersistentFailerActor] - failer.start - try { - bactor !! Debit("a-123", 7000, failer) - fail("should throw exception") - } catch { case e: RuntimeException => {}} - - val JsNumber(b1) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(5000), BigInt(b1.intValue)) - - // should not count the failed one - assert(3 == (bactor !! LogSize).get.asInstanceOf[Int]) + override def afterEach { + MongoStorageBackend.drop } - @Test - def testUnsuccessfulMultiDebit = { - val bactor = actorOf[BankAccountActor] - bactor.start - bactor !! Credit("a-123", 5000) + describe("successful debit") { + it("should debit successfully") { + val bactor = actorOf[BankAccountActor] + bactor.start + val failer = actorOf[PersistentFailerActor] + failer.start + bactor !! Credit("a-123", 5000) + bactor !! Debit("a-123", 3000, failer) - val JsNumber(b) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(5000), BigInt(b.intValue)) + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(2000) - val failer = actorOf[PersistentFailerActor] - failer.start - try { - bactor !! MultiDebit("a-123", List(500, 2000, 1000, 3000), failer) - fail("should throw exception") - } catch { case e: RuntimeException => {}} + bactor !! Credit("a-123", 7000) + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(9000) - val JsNumber(b1) = (bactor !! Balance("a-123")).get.asInstanceOf[JsValue] - assertEquals(BigInt(5000), BigInt(b1.intValue)) + bactor !! Debit("a-123", 8000, failer) + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(1000) - // should not count the failed one - assert(3 == (bactor !! LogSize).get.asInstanceOf[Int]) + (bactor !! LogSize).get.asInstanceOf[Int] should equal(7) + (bactor !! Log(0, 7)).get.asInstanceOf[Iterable[String]].size should equal(7) + } + } + + describe("unsuccessful debit") { + it("debit should fail") { + val bactor = actorOf[BankAccountActor] + bactor.start + val failer = actorOf[PersistentFailerActor] + failer.start + bactor !! Credit("a-123", 5000) + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(5000) + evaluating { + bactor !! Debit("a-123", 7000, failer) + } should produce [Exception] + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(5000) + (bactor !! LogSize).get.asInstanceOf[Int] should equal(3) + } + } + + describe("unsuccessful multidebit") { + it("multidebit should fail") { + val bactor = actorOf[BankAccountActor] + bactor.start + val failer = actorOf[PersistentFailerActor] + failer.start + bactor !! Credit("a-123", 5000) + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(5000) + evaluating { + bactor !! MultiDebit("a-123", List(1000, 2000, 4000), failer) + } should produce [Exception] + (bactor !! Balance("a-123")).get.asInstanceOf[Int] should equal(5000) + (bactor !! LogSize).get.asInstanceOf[Int] should equal(3) + } } } 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 e518b28d66..fb2034b6c1 100644 --- a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala +++ b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala @@ -1,364 +1,158 @@ package se.scalablesolutions.akka.persistence.mongo -import org.junit.{Test, Before} -import org.junit.Assert._ -import org.scalatest.junit.JUnitSuite -import _root_.dispatch.json._ -import _root_.dispatch.json.Js._ +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.BeforeAndAfterEach +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith import java.util.NoSuchElementException -@scala.reflect.BeanInfo case class Foo(no: Int, name: String) -class MongoStorageSpec extends JUnitSuite { +@RunWith(classOf[JUnitRunner]) +class MongoStorageSpec extends + Spec with + ShouldMatchers with + BeforeAndAfterEach { - val changeSetV = new scala.collection.mutable.ArrayBuffer[AnyRef] - val changeSetM = new scala.collection.mutable.HashMap[AnyRef, AnyRef] - - @Before def initialize() = { - MongoStorageBackend.coll.drop + override def beforeEach { + MongoStorageBackend.drop } - @Test - def testVectorInsertForTransactionId = { - changeSetV += "debasish" // string - changeSetV += List(1, 2, 3) // Scala List - changeSetV += List(100, 200) - MongoStorageBackend.insertVectorStorageEntriesFor("U-A1", changeSetV.toList) - assertEquals( - 3, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) - changeSetV.clear - - // changeSetV should be reinitialized - changeSetV += List(12, 23, 45) - changeSetV += "maulindu" - MongoStorageBackend.insertVectorStorageEntriesFor("U-A1", changeSetV.toList) - assertEquals( - 5, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) - - // add more to the same changeSetV - changeSetV += "ramanendu" - changeSetV += Map(1 -> "dg", 2 -> "mc") - - // add for a diff transaction - MongoStorageBackend.insertVectorStorageEntriesFor("U-A2", changeSetV.toList) - assertEquals( - 4, - MongoStorageBackend.getVectorStorageSizeFor("U-A2")) - - // previous transaction change set should remain same - assertEquals( - 5, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) - - // test single element entry - MongoStorageBackend.insertVectorStorageEntryFor("U-A1", Map(1->1, 2->4, 3->9)) - assertEquals( - 6, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) + override def afterEach { + MongoStorageBackend.drop } - @Test - def testVectorFetchForKeys = { + describe("persistent maps") { + it("should insert with single key and value") { + import MongoStorageBackend._ - // initially everything 0 - assertEquals( - 0, - MongoStorageBackend.getVectorStorageSizeFor("U-A2")) - - assertEquals( - 0, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) - - // get some stuff - changeSetV += "debasish" - changeSetV += List(BigDecimal(12), BigDecimal(13), BigDecimal(14)) - MongoStorageBackend.insertVectorStorageEntriesFor("U-A1", changeSetV.toList) - - assertEquals( - 2, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) - - val JsString(str) = MongoStorageBackend.getVectorStorageEntryFor("U-A1", 0).asInstanceOf[JsString] - assertEquals("debasish", str) - - val l = MongoStorageBackend.getVectorStorageEntryFor("U-A1", 1).asInstanceOf[JsValue] - val num_list = list ! num - val num_list(l0) = l - assertEquals(List(12, 13, 14), l0) - - changeSetV.clear - changeSetV += Map(1->1, 2->4, 3->9) - changeSetV += BigInt(2310) - changeSetV += List(100, 200, 300) - MongoStorageBackend.insertVectorStorageEntriesFor("U-A1", changeSetV.toList) - - assertEquals( - 5, - MongoStorageBackend.getVectorStorageSizeFor("U-A1")) - - val r = - MongoStorageBackend.getVectorStorageRangeFor("U-A1", Some(1), None, 3) - - assertEquals(3, r.size) - val lr = r(0).asInstanceOf[JsValue] - val num_list(l1) = lr - assertEquals(List(12, 13, 14), l1) - } - - @Test - def testVectorFetchForNonExistentKeys = { - try { - MongoStorageBackend.getVectorStorageEntryFor("U-A1", 1) - fail("should throw an exception") - } catch {case e: NoSuchElementException => {}} - - try { - MongoStorageBackend.getVectorStorageRangeFor("U-A1", Some(2), None, 12) - fail("should throw an exception") - } catch {case e: NoSuchElementException => {}} - } - - @Test - def testVectorUpdateForTransactionId = { - import MongoStorageBackend._ - - changeSetV += "debasish" // string - changeSetV += List(1, 2, 3) // Scala List - changeSetV += List(100, 200) - - insertVectorStorageEntriesFor("U-A1", changeSetV.toList) - assertEquals(3, getVectorStorageSizeFor("U-A1")) - updateVectorStorageEntryFor("U-A1", 0, "maulindu") - val JsString(str) = getVectorStorageEntryFor("U-A1", 0).asInstanceOf[JsString] - assertEquals("maulindu", str) - - updateVectorStorageEntryFor("U-A1", 1, Map("1"->"dg", "2"->"mc")) - val JsObject(m) = getVectorStorageEntryFor("U-A1", 1).asInstanceOf[JsObject] - assertEquals(m.keySet.size, 2) - } - - @Test - def testMapInsertForTransactionId = { - fillMap - - // add some more to changeSet - changeSetM += "5" -> Foo(12, "dg") - changeSetM += "6" -> java.util.Calendar.getInstance.getTime - - // insert all into Mongo - MongoStorageBackend.insertMapStorageEntriesFor("U-M1", changeSetM.toList) - assertEquals( - 6, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - - // individual insert api - MongoStorageBackend.insertMapStorageEntryFor("U-M1", "7", "akka") - MongoStorageBackend.insertMapStorageEntryFor("U-M1", "8", List(23, 25)) - assertEquals( - 8, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - - // add the same changeSet for another transaction - MongoStorageBackend.insertMapStorageEntriesFor("U-M2", changeSetM.toList) - assertEquals( - 6, - MongoStorageBackend.getMapStorageSizeFor("U-M2")) - - // the first transaction should remain the same - assertEquals( - 8, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - changeSetM.clear - } - - @Test - def testMapContents = { - fillMap - MongoStorageBackend.insertMapStorageEntriesFor("U-M1", changeSetM.toList) - MongoStorageBackend.getMapStorageEntryFor("U-M1", "2") match { - case Some(x) => { - val JsString(str) = x.asInstanceOf[JsValue] - assertEquals("peter", str) - } - case None => fail("should fetch peter") - } - MongoStorageBackend.getMapStorageEntryFor("U-M1", "4") match { - case Some(x) => { - val num_list = list ! num - val num_list(l0) = x.asInstanceOf[JsValue] - assertEquals(3, l0.size) - } - case None => fail("should fetch list") - } - MongoStorageBackend.getMapStorageEntryFor("U-M1", "3") match { - case Some(x) => { - val num_list = list ! num - val num_list(l0) = x.asInstanceOf[JsValue] - assertEquals(2, l0.size) - } - case None => fail("should fetch list") + insertMapStorageEntryFor("t1", "odersky".getBytes, "scala".getBytes) + insertMapStorageEntryFor("t1", "gosling".getBytes, "java".getBytes) + insertMapStorageEntryFor("t1", "stroustrup".getBytes, "c++".getBytes) + getMapStorageSizeFor("t1") should equal(3) + new String(getMapStorageEntryFor("t1", "odersky".getBytes).get) should equal("scala") + new String(getMapStorageEntryFor("t1", "gosling".getBytes).get) should equal("java") + new String(getMapStorageEntryFor("t1", "stroustrup".getBytes).get) should equal("c++") + getMapStorageEntryFor("t1", "torvalds".getBytes) should equal(None) } - // get the entire map - val l: List[Tuple2[AnyRef, AnyRef]] = - MongoStorageBackend.getMapStorageFor("U-M1") + it("should insert with multiple keys and values") { + import MongoStorageBackend._ - assertEquals(4, l.size) - assertTrue(l.map(_._1).contains("1")) - assertTrue(l.map(_._1).contains("2")) - assertTrue(l.map(_._1).contains("3")) - assertTrue(l.map(_._1).contains("4")) + val l = List(("stroustrup", "c++"), ("odersky", "scala"), ("gosling", "java")) + insertMapStorageEntriesFor("t1", l.map { case (k, v) => (k.getBytes, v.getBytes) }) + getMapStorageSizeFor("t1") should equal(3) + new String(getMapStorageEntryFor("t1", "stroustrup".getBytes).get) should equal("c++") + new String(getMapStorageEntryFor("t1", "gosling".getBytes).get) should equal("java") + new String(getMapStorageEntryFor("t1", "odersky".getBytes).get) should equal("scala") + getMapStorageEntryFor("t1", "torvalds".getBytes) should equal(None) - val JsString(str) = l.filter(_._1 == "2").head._2 - assertEquals(str, "peter") + evaluating { getMapStorageEntryFor("t2", "torvalds".getBytes) } should produce [NoSuchElementException] - // trying to fetch for a non-existent transaction will throw - try { - MongoStorageBackend.getMapStorageFor("U-M2") - fail("should throw an exception") - } catch {case e: NoSuchElementException => {}} + getMapStorageFor("t1").map { case (k, v) => (new String(k), new String(v)) } should equal (l) - changeSetM.clear - } + removeMapStorageFor("t1", "gosling".getBytes) + getMapStorageSizeFor("t1") should equal(2) - @Test - def testMapContentsByRange = { - fillMap - changeSetM += "5" -> Map(1 -> "dg", 2 -> "mc") - MongoStorageBackend.insertMapStorageEntriesFor("U-M1", changeSetM.toList) - - // specify start and count - val l: List[Tuple2[AnyRef, AnyRef]] = - MongoStorageBackend.getMapStorageRangeFor( - "U-M1", Some(Integer.valueOf(2)), None, 3) - - assertEquals(3, l.size) - assertEquals("3", l(0)._1.asInstanceOf[String]) - val lst = l(0)._2.asInstanceOf[JsValue] - val num_list = list ! num - val num_list(l0) = lst - assertEquals(List(100, 200), l0) - assertEquals("4", l(1)._1.asInstanceOf[String]) - val ls = l(1)._2.asInstanceOf[JsValue] - val num_list(l1) = ls - assertEquals(List(10, 20, 30), l1) - - // specify start, finish and count where finish - start == count - assertEquals(3, - MongoStorageBackend.getMapStorageRangeFor( - "U-M1", Some(Integer.valueOf(2)), Some(Integer.valueOf(5)), 3).size) - - // specify start, finish and count where finish - start > count - assertEquals(3, - MongoStorageBackend.getMapStorageRangeFor( - "U-M1", Some(Integer.valueOf(2)), Some(Integer.valueOf(9)), 3).size) - - // do not specify start or finish - assertEquals(3, - MongoStorageBackend.getMapStorageRangeFor( - "U-M1", None, None, 3).size) - - // specify finish and count - assertEquals(3, - MongoStorageBackend.getMapStorageRangeFor( - "U-M1", None, Some(Integer.valueOf(3)), 3).size) - - // specify start, finish and count where finish < start - assertEquals(3, - MongoStorageBackend.getMapStorageRangeFor( - "U-M1", Some(Integer.valueOf(2)), Some(Integer.valueOf(1)), 3).size) - - changeSetM.clear - } - - @Test - def testMapStorageRemove = { - fillMap - changeSetM += "5" -> Map(1 -> "dg", 2 -> "mc") - - MongoStorageBackend.insertMapStorageEntriesFor("U-M1", changeSetM.toList) - assertEquals(5, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - - // remove key "3" - MongoStorageBackend.removeMapStorageFor("U-M1", "3") - assertEquals(4, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - - try { - MongoStorageBackend.getMapStorageEntryFor("U-M1", "3") - fail("should throw exception") - } catch { case e => {}} - - // remove key "4" - MongoStorageBackend.removeMapStorageFor("U-M1", "4") - assertEquals(3, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - - // remove key "2" - MongoStorageBackend.removeMapStorageFor("U-M1", "2") - assertEquals(2, - MongoStorageBackend.getMapStorageSizeFor("U-M1")) - - // remove the whole stuff - MongoStorageBackend.removeMapStorageFor("U-M1") - - try { - MongoStorageBackend.getMapStorageFor("U-M1") - fail("should throw exception") - } catch { case e: NoSuchElementException => {}} - - changeSetM.clear - } - - private def fillMap = { - changeSetM += "1" -> "john" - changeSetM += "2" -> "peter" - changeSetM += "3" -> List(100, 200) - changeSetM += "4" -> List(10, 20, 30) - changeSetM - } - - @Test - def testRefStorage = { - MongoStorageBackend.getRefStorageFor("U-R1") match { - case None => - case Some(o) => fail("should be None") + removeMapStorageFor("t1") + evaluating { getMapStorageSizeFor("t1") } should produce [NoSuchElementException] } - val m = Map("1"->1, "2"->4, "3"->9) - MongoStorageBackend.insertRefStorageFor("U-R1", m) - MongoStorageBackend.getRefStorageFor("U-R1") match { - case None => fail("should not be empty") - case Some(r) => { - val a = r.asInstanceOf[JsValue] - val m1 = Symbol("1") ? num - val m2 = Symbol("2") ? num - val m3 = Symbol("3") ? num + it("should do proper range queries") { + import MongoStorageBackend._ + val l = List( + ("bjarne stroustrup", "c++"), + ("martin odersky", "scala"), + ("james gosling", "java"), + ("yukihiro matsumoto", "ruby"), + ("slava pestov", "factor"), + ("rich hickey", "clojure"), + ("ola bini", "ioke"), + ("dennis ritchie", "c"), + ("larry wall", "perl"), + ("guido van rossum", "python"), + ("james strachan", "groovy")) + insertMapStorageEntriesFor("t1", l.map { case (k, v) => (k.getBytes, v.getBytes) }) + getMapStorageSizeFor("t1") should equal(l.size) + getMapStorageRangeFor("t1", None, None, 100).map { case (k, v) => (new String(k), new String(v)) } should equal(l.sortWith(_._1 < _._1)) + getMapStorageRangeFor("t1", None, None, 5).map { case (k, v) => (new String(k), new String(v)) }.size should equal(5) + } + } - val m1(n1) = a - val m2(n2) = a - val m3(n3) = a + describe("persistent vectors") { + it("should insert a single value") { + import MongoStorageBackend._ - assertEquals(n1, 1) - assertEquals(n2, 4) - assertEquals(n3, 9) - } + insertVectorStorageEntryFor("t1", "martin odersky".getBytes) + insertVectorStorageEntryFor("t1", "james gosling".getBytes) + new String(getVectorStorageEntryFor("t1", 0)) should equal("james gosling") + new String(getVectorStorageEntryFor("t1", 1)) should equal("martin odersky") } - // insert another one - // the previous one should be replaced - val b = List("100", "jonas") - MongoStorageBackend.insertRefStorageFor("U-R1", b) - MongoStorageBackend.getRefStorageFor("U-R1") match { - case None => fail("should not be empty") - case Some(r) => { - val a = r.asInstanceOf[JsValue] - val str_lst = list ! str - val str_lst(l) = a - assertEquals(b, l) - } + it("should insert multiple values") { + import MongoStorageBackend._ + + insertVectorStorageEntryFor("t1", "martin odersky".getBytes) + insertVectorStorageEntryFor("t1", "james gosling".getBytes) + insertVectorStorageEntriesFor("t1", List("ola bini".getBytes, "james strachan".getBytes, "dennis ritchie".getBytes)) + new String(getVectorStorageEntryFor("t1", 0)) should equal("ola bini") + new String(getVectorStorageEntryFor("t1", 1)) should equal("james strachan") + new String(getVectorStorageEntryFor("t1", 2)) should equal("dennis ritchie") + new String(getVectorStorageEntryFor("t1", 3)) should equal("james gosling") + new String(getVectorStorageEntryFor("t1", 4)) should equal("martin odersky") + } + + it("should fetch a range of values") { + import MongoStorageBackend._ + + insertVectorStorageEntryFor("t1", "martin odersky".getBytes) + insertVectorStorageEntryFor("t1", "james gosling".getBytes) + getVectorStorageSizeFor("t1") should equal(2) + insertVectorStorageEntriesFor("t1", List("ola bini".getBytes, "james strachan".getBytes, "dennis ritchie".getBytes)) + getVectorStorageRangeFor("t1", None, None, 100).map(new String(_)) should equal(List("ola bini", "james strachan", "dennis ritchie", "james gosling", "martin odersky")) + getVectorStorageRangeFor("t1", Some(0), Some(5), 100).map(new String(_)) should equal(List("ola bini", "james strachan", "dennis ritchie", "james gosling", "martin odersky")) + getVectorStorageRangeFor("t1", Some(2), Some(5), 100).map(new String(_)) should equal(List("dennis ritchie", "james gosling", "martin odersky")) + + getVectorStorageSizeFor("t1") should equal(5) + } + + it("should insert and query complex structures") { + import MongoStorageBackend._ + import sjson.json.DefaultProtocol._ + import sjson.json.JsonSerialization._ + + // a list[AnyRef] should be added successfully + val l = List("ola bini".getBytes, tobinary(List(100, 200, 300)), tobinary(List(1, 2, 3))) + + // for id = t1 + insertVectorStorageEntriesFor("t1", l) + new String(getVectorStorageEntryFor("t1", 0)) should equal("ola bini") + frombinary[List[Int]](getVectorStorageEntryFor("t1", 1)) should equal(List(100, 200, 300)) + frombinary[List[Int]](getVectorStorageEntryFor("t1", 2)) should equal(List(1, 2, 3)) + + getVectorStorageSizeFor("t1") should equal(3) + + // some more for id = t1 + val m = List(tobinary(Map(1 -> "dg", 2 -> "mc", 3 -> "nd")), tobinary(List("martin odersky", "james gosling"))) + insertVectorStorageEntriesFor("t1", m) + + // size should add up + getVectorStorageSizeFor("t1") should equal(5) + + // now for a diff id + insertVectorStorageEntriesFor("t2", l) + getVectorStorageSizeFor("t2") should equal(3) + } + } + + describe("persistent refs") { + it("should insert a ref") { + import MongoStorageBackend._ + + insertRefStorageFor("t1", "martin odersky".getBytes) + new String(getRefStorageFor("t1").get) should equal("martin odersky") + insertRefStorageFor("t1", "james gosling".getBytes) + new String(getRefStorageFor("t1").get) should equal("james gosling") + getRefStorageFor("t2") should equal(None) } } } diff --git a/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.jar b/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..038768fe144bc03b784deb9c4bc3a323c8dc74e4 GIT binary patch literal 88160 zcmWIWW@h1H0D-8Pm#siF40AFtF!;KLIO=-(x#`1{aWF*AylmAaw%}+U0|P?~0|SEy zvNB&sKTkK;;1E4ux6i(3PWyQ4>RsgZ*3~+9=KSU$gDb`lo)+nNojal9t?R_W{$xqm z6fx}sDiu5DbO#B02L+eR)mkee!!0z~I7qZc_;k?cik)-3jElVvJc?ZT(^Yk5sK7#z z*!5F#Cj3mRV%s&dQgx+kWvAtym_0EJ0dQ}Gqj`gcfq@~vC>`zt1_pE$0#Fsj$%#3M z@g@2BImJk-Bhbtf#iF_-GdC5kg@Xa4WkOdpw+#maL$o#ngAOh&PNkVSDXB%Cxdl0y z$(bd^DtgH|iN(cxXK~iZgvuVT*Uz+m`%O`IX=~7nM#f23Hi?~|bSdZ-Tkxc9+fX8-uYK|Ve_7gj^UeC3 zFFu=`v-$o1egFUe|NifDeZE@tb>{7N`4ZO7(4KJft?w_(<@a?hn2z)H_>0_`BR%Du z&eJzdx(3zdKd(yN+W7R)!L_kWcbn(g&k8;Ko9VpvlueD+?^4dsF5#{`zMf&%FP3NP z&WqkzB%LGh;>|lru^C%$o}N7W@>veCXPjpu*K-JcPE4K9xI}~f?xM}LMKwo0e!rnC z^Sz1ro1=B~hVI2TWtG|I#3Vet!6WA2b>`5UN|U!1jaSp+idlDuY$~wX`7Y`Dl%n;C z=W1;a3wakfv2_cYe9+&eLbd*!DhJabV@$+f`rgoM=38CK|m>PUE`gGL_w;b|V<1F94ah{XJ0@Y`Yv3+x&AMF(@U9j8nXjSt*7J1%j zOpo2f@A>?ke51p+=I1}$WQ*26aVZ7e)kv4 zD(~<~*r2&`lWTa&q!;_Bq`a9AiJ#;#GY_w@E%$E>A=*W8fsyyv14?l^1B?NaSR z3IBA>bqgNnWSiZ+dZm!X>=;*~2!~*C?$g|7u8+KIW?!!1TU*f>R+FjIe@v@*@wU*| z@7-Hox|KcazTly@y*>D~0rNZ6b%(ek7l^x>SLoJq=;S=&oHJizLScf-{w2A#XZCu= z^iD7Qr7v!wDj0cN=*QVG-NxKy+x3hu3Ee8tF650n8?{Gmnd+6z+0jh>N4B!a8FMXJ zJ1yaz>!YYWjhAbbx3|>zcyln%ir>e`=l{uf_w0j}yQ<9mw(j;l+0o3`z_0z5X)sBRST8#9>wH~%_#LyEi4i;-*YtM-nwnam*!oOHkm!;ejnS*IbL_V zly#0xIk0NllMVLn%zx+E=dm-|pOVbGlC)pywAsvAF5#wP7E|0N&n(m{6p>HN&0D_s z*T0Vnzqp$l=dWN5DSP)!EMY_MwKv@_qYjkRc-(39FWNT$(w2rSg{fi&Po8zZdl4aU znQ76(uzlKPxmy}Ce^1&WTwb_t{^RJ^g|*BpI^Q_@+^~68?YjO!@1>>tR~kNjl@KIg z8urouy71+>+vGcUe@ylJsy!tu&*#CC3MEmC^Y=`GxN5A$O>8P(mCtqBck8cS{w|3M zn`|RX&634GnXWI*;{Ly%L-pN3^T==coO9;d^?cCuNQtgwxlke_x6#{g+rcv`ov+ST zta@CqJ5Tl9(eDw<-gB4C+#t=ea*otwRzr&irnAnqC}`gCy3;PdS;0P2(c|#r2QS{f z>f5XxC^&iT!joYS5^m30QlwL8Uw3fTpP&aO%2v4tn2#=cod0aUk+o{$)06x+b^c7_ zGmnzy-Y-5QI=4nUYR)8`Kb{Gl_f8y&X9?R@C6w|ozoc9+IrX7@mCN0^i(Bho3g)Cf zWL%{;`MJQfh69^eyuC|WZkhdmYrKlx`;1V;iesB!IZHoOS3DxKhP7T|k@FcbG0Uh^ z^E(+faD6sl|8p+pSFYgRPxh}hjNA*(oP9N&@81>Ij16o~Gj-nCHnr~Eo)Gw6WZI?g zMZf9{W?yFWvwyC!IQ7eB?&Yn)=hXt(`@gQ6=k(o|D<=E5z^|Q4avJ4CrT?!NUp@7- zsYUSdNbA^hkrffGmp$XpL>jib&nl8@b=N*;HS4~_`AwR0lF!{-U?E+&PH+9$Wpi|I ztg`rP5r6z<+Nqrk&0WSVZ>`QH`&#ba5MB7~fs^cWrQ&msMbq`?EqH8ma>{0Y|1cS8 zWvjmAQ#bnqk4iSLek^k4dUi}<*M}0B`%{9Q7FbNSSn)U}?UZH9Y0>8f^OilfIczg8 zUF+P`ifMhHV}j3p{!nvtCzt+(!nho{^WGxQv-X4>uWbFfYkyGTx`V4r=3Jk3{Ik{3 zonq^+7p^mYx`#7AY|iVCVzSTQKD&P1TI4y~p5Wt^{Xf5cjXSzi@cre&d-y)``r1N)%|4_?jI9npD$iEr~l#Ou3~n(g^z1)?Wwa{Y*GGbv(>rhzLxO^#6_Ry z)p#ENxk~eS_K#hU?lkAWu()q_{ik(TvHrf*kN+Il^Z!pk;k(2AC3EJ_y8ijR$n&~C zF~@&S{aN*Q)-mg;yyxGCy#L>}ulnS(@*m9qJg(Iri+@=v`>(Hl)%E(v_J6cmUOc(} zp9$6wg0w>ZP6_m#&&a@Vl9_?Qkmy!OGN=&(X^L!}bl&f_gTS%GL3`5wozT_6F#va`8Ewk#iSM;j~CL4C1mh4*gWr>-j->urG{cCUh zjn#h2Hf>sPVrGVnKfRy2?AZ6^$nyd#p?~IfT|y#0waUzGk9!Q-+~Ox@ zov4)*4Dxv!6?j(Od4Bdvo3_GhkEWge*A!H_Uih@ysfYjP1zV z84GX5rPPV;3-Mdu6}l!gzRL%{}dEm>se&fl6 zo=J-&`ZnF2m#CVTKWV<>pZm|2^)A?M=rJ$GJlO1VS*3tP#rc5wALQ>8?KI!EuSf0B zHMVb`bl)V&U;NIMy2(a^`ETjTh?MUpPXm_hl(3FcVP17ok!MPV?&7AYCLfsBT1I)_ zmAsaIV^?&Cea~ZaVP7fvx3g|6`)pM9!Ay1T@+D3#QAcEK4vRdS;na2V=q#Du@`=Cs zQDeL1)ULSGj0_ClnHU(fiH&VYJkOovn}69spe_B_y{+Z3>UxIqstpQTE{MIj^vTz2 zg;q<*N+A!y^e1Y0TP8{?AAcybM5gM=Is1nVRnOcNuko$Cbg1+6IurTxd)~kK_vhy6Z1Fv=7f2HEK;+<@*SBd-c!@Y^>UP>(g4NPyEFV;C#YUOYv@~;2pd0Lz0 zKb!crXq`L^zB+ukXz z`7`-TdqLJ&MsdX#?(>zWx-`$LD!q(7=hPPqQlCT%O!!ix@=otUijAGx z{C|(G+OE&`#|hduI+_NZI{4PKdt^TZO;x{yy?lNsmEB}Da7k>O>^Fpy%mNTl% zKF#N>@(Z#3t{C&}l268?oBUFjzGWLfp1NR~Qn{Gv1#z}t(x_=dK7z%tfsuh>HNG_A zQd*Q)l9`_e?TpLnW8UvAZvt^2UFet#n~Up?Z>1M3Zbx3`Z5WOmCN#?^Wj-7KU8-T%OY1|NGvJc@x$?`WLdLBeYIx zPNRwQxBX$IN7Jvmutf?Ttrw}i<@O+Gp0PmQY{kFjN{547gPvP`Z&_sbaQ>Cc5mmwW znQ|`oO=aV;{*pRXV9R%dq>e)iW=C4-P7d!kY_dG^r`xV&$`xUUzCBGx_}V<=_9R6* zN{F3sD^+kUbB$)TU;0atRbH{U-r~ru&Fj81pHJw{sp2`bOV0CeZo=e8ioKi{rNger zBO(&iW8Ickr?r}ufq{pQfk6vb=0=N1NZy_rmK!YPDez7%Vn*yV4}mLtvRX6}lsYRv z^L*?vR5M)RY4GfCDyvF%!`A{&z58pX$iGX!Bfn!+%&T*&ck_o|tKB1CT57wSyJ)pW z_>{+IO1@wA_doyb%l-do2YP$+f#nEH=`}^-Ui*+mOl!sA26=x;bIYR%D%;}8+&cl z(l3urMH%M*QtWu(sWLru!UFF`WdWt>&#Y^i6E$vkvUe%ZG4;rdT>Y|Xrn7(7`suG* zj2`yA4Gh%gn96qU>k{vUnhr0#ceI{6*|DHXlxr2g;->j+@_oXFmru-$+PdH>7jHnz z<7$K1e4!Sc%S9|0muqJKe*Gh{fzP95*&&558U<`gQ#{uuU0JPm=s@B@r8ypFWE~O& z+E~+=7@u>P$6Z|gNJRDZey-;^)1Mzb6Xc`(!@R3~Pc9KTyf&|VE}d0qZ?ovN!G4R+ z7S-1GzA=|c3jS2B&~fHlaqEcHh7X5>cV&e>Ud(m=VIsr4gWOTss?3Rs7bDMllto3I zD_i7VxZhFY9mi$9@W~H#*Ic-m{rdG&e~!s-xeC&4LNy!$6;Fh&uE;83tnia~TC(cD zyYBBTTQ9EImHlO7XIA%zdH)VAa=FYbylf|noz&kgvsm2h8B|R_X|Gs$|G|s%TapfB zidZ?s`?VaCi|$^tn{mxdhs8`xfs<}V1oKUsb*nA;_67S*ydU-Tw!9QG+1k16P^3iB zg|^ula=+d#`kL>2sn_hPm~QIAUYR4gS9;AVIkr!1-Vs}{IF;{Vncy**EfOy|x*M9e z@rKteQr*_zd!gbhLv#Kew#U~Z{Vdqb=e$TwbXb^vCuRMX2~%gMq#DgBamr^mecHCG zgyV|ui_!&tk=YEM4z-1>1&zlty*5dLC#*A77oRbka_0|w)Q|Sre-3}>OKaMnKL5!-rd7B9a<(tC(3L%KS@q!e zI{8iV5|3{@T~)*Nx#FUB)}liy@@!FBOPlJx1bpQ&jO0}Me3Eh7)}5XjTE&Y?s;4BF z9O)}z-{_sVaPbRK*;P%6U%Ki-(9L4Z+sAp9Qt6)Kko6CE5L+`{0k$o6pbsXWwICE7N7~@XFyncH+LO9&hZOMwaR8 zE}RS9GCNr)=JBN;Ph}-sUf+ChiKTP>;tHR|GZgomr^Zk|>m|Ouul$Vj zyicjDobco?Ur1-G;^wa++&ga<&35lC;VgJ-?XphiF^y@`p=45wQG)+R1k&GdJDj+&o!p zX%`b$M^{W7helk5V#f^+QBnOP%>H{m9O8ceJM7Ehgxxcn? zt@Qbq_pc~!*mM7L`{k^P@&}bWovaGn=ay8ax%VG9V>)xr?DFQVOmFALHZxr|tB8E0 zpKEmWkWQmy_LY#%n;U{W9D`#1=H0t#WM}o{hxJ>g0cff`Mm7VVru1`rRUv78$*G}8mW+Co^ z5U$UKTMl^s`t!-Ld9y2%y4#NxX8b>oFWDe*oU0^DJ?Gn|vgYQcBEBunQok=8{v>qdW3x!5RNspz+;ireiD zQ|Q}(LZQxFHgStZ_mvkr-Y(yH;ZWA>wIMg3{a80$G-7tZE?f@}J_U{)5<$v1qw4=rw4=yvI* zIJ1IqSMp*{nX?8$t1li-T(eN%S4L-L!tUhs%0ln%Th}9(C#`k#@9DSlZVX(^oxkdj z=bFP+`)bZF$g@6nbf1cxuEBb?Wt`CuPGs?gc0GQdEX%g2L<2O0xJ`7&0#Q|=pHn0bgk4LL1`i>wk=(mZ?boMP z&=BG#@DO6xGTmz~O#MREh9|6Sw0Fs9UA#KQIhDIdr~T8pWP!k|bvJ@6RX%BMv`bR_^)kAmzb>T&nSJEyj<(DD!yep-NGbJxz(0E{ z%eI+!cKLXEXB=F6gttOA`{S>UX#uC#<#E~wT@|W0qcLGxRMSrJPU%A7<(0#tNIUIyx4!@8F&cs^l9s->IR|DLJx1;xB4y19T!WrkE(^%?;gaw7n!jw zUbWCi1~i0tXWcdz*brhD+sQdzcY2$3j=8+-tdy~QbAD-K+wFLJY|3xRSSWLHQ>EC4b{Ze4zkZdt|}!lb#qfzf<+dAoa4hgg7A-m~2~*PXa$Y|-!h{gL7MugZN}<*u4HtU598 zKnt((zUkkK?is}Xdi^?h9nUe=z*Nz{CAMaFJ%3NE5Qu*mvfpcEnj4FltMJMLmEV3X zbb4@m=7M)8gV(=)vT5qO^1#DK4?NDCe6nz%^jYy8s)5lJf0U+FaNJM2xnH*+>yyEu z_UKaWj|ThB8~t@yaJVH_-Q(vK3H^?PjO*`} zXt8)c2zw;>r-k)rrm*8HIriyx>)u%0P8K!1DJ7M|`fckPPo?8R{g0-vIkld}&tTDI zwmZ|$TmNJG7oWCVVS~y)#o4X*FFth3PJYPGWUSqrpE)^rQmZ|;&IRenAFB^+O7m~h z`7`&Op3)kfKf(Pgb{22>kZ;?xR>#KeYU(GMLqY-(GRc{p+X8OCSnnN?H0wZ&>AbM` z%N+YT71I(#f1DP`P1?2L#6_onek=`7XS7N_@K3vx|M|t-{}JsItU3T)zKIq*decnML#Z(lyRy&5D;l z|A=!=+PTQp7Ox9->86OT$Zu73RF%=PS$!cUO#Lx%+xk8do09FuYC zW`DQHbB4GjkM|t0nKxbQoa&B{w*16t_$$iD$n$OQxbU(V&wf&XFdxQP^?ya=g{xH_+T=m)Y>%VKB zdt0%;?=xfYx!4~I9^L8Xe`z6acKxT^QOlP87Z&oyPiti62h92X!Bh6R@w2a=BaiNs zpMSY`pv!N-60?L2osbWZz6TiNIB&;CAl@A}M9 zx90JmGkfOMue0cXByM$X{n_iU%}wsD%c-c_{p-*6FPDAI_8*u3sJ`hR_dmDyt3UGp z@!tEN{r}T)MHlP+|B(iiKz+{E{+;HYj0_AiObiSv1REfb-e&H^vwnvhL|WhP{X18) zRWRU>tK)?goxRnM54CpY#e85`Hm!|Eb8X|JS*+aCep%_&Fvy>>(B%v}tdlm$`1!oa zKcBt+{aBb`-GZJ2)hA12TFQ;M1hu{|p6|u}7vtQ~d z4jVOsrYf5^<9X| zuJ-s2di-XM|7Wi-7Z#7~*Oa`xWC zWoI*{lz-!&y5n?4zsH~UC3$=jrk!4{YF@TUSCoqWD+rX-JW6U$7VrHd*yCsY3q*K;!Z~_GcQfCN3;b&^T`R`DhJyxs&le29br+rlw~zja?-(*J~L(}bZ;x$xccs!tngj8&Tfm? z5|FbtHa9oheD|%hd1aed-_6Oc{eJfU=k&_|=TFN=nSMU=r}+HN@B6;L-~Imi{r^XP zRx@n5Ea-I8p+-rNr`&x}qEqGh8x=May|$?~H)b>#)GFBi`X>9r=F07>E-T*ms=NL9 z^eI3~u3zo%$`cVc@^6`z3Z&)nxhZe-$kI;CDn6}{cxzpSN5S=7Qojtk4U#vTm>5X! zGn#Tx;8bm+i_LqoDjAC+>fPck1UuU~eAlrF9r+Qtw~*0NLr*Yl=ggFX zoKU^`*E~xOWuCt3s;j$Z?aBKuGK~Dvwy96!Quiw`3fmUKZtTZ>%N?vTv;s4t~DKXv^cIjYe0?b96!%6n4(gW$(?|adQ7cp6QQ_oHk!d z@V61s5V)bTr%>R;!P#81PhV)%ZFzfWk>=?|ow~DRmSt*4`gKaDuQ;ir=Is0M@gdLa zGf!QSy*9Ub=N_9ffo+wSr>rC=hy+eNft7dMRCRP@u&1yPftG40% zt<@ZN?`>ThZMpV-Sj_!>-*2{S=_JU1n<`=-oOOKuy5nnNdt2AkvL?2x=dKP~zBD>! z>DyK3PIYgXF_phEbla=FeVg{0Zz$rOw&7D~i(dYzNNcyK+O@l@&hZ_V({a_UE-+0j z_mo>wK6eX$ov!?v+OEAToNcQGKiDu^p5AwY=gFEMY$ltEc1)4n@Xg__wOYtz2DHt6TY9Tpll{yr&dcTHA2^KOx{Zjn#U<(=1ApLXOW2Q8Y^C|qGx zZhBte_{1w2Dw=#^hclIr+ZoTF6QDW$c!Gl9L%)kT;_0W?M5Onvl`EdKO$LYlg4Ng`~lqx)9;7Y3$}kOjC)i1?n=u~OOtC4tJf!UUEdcmm zUY&FEb6tdF-m@tyY&n0Par(w~zH$e{CpNi5{FaXPoA@eRO*uYu{ovRgE4<|GjD-E$ zp47ZpEm{;+ntsEm_yLtu%d0l$%cRNN%6S2nXP@2)=?MG# zV#W&JTaHnx3Y&|?r!n;Anq`)(GHkM$eVAvroAAd78!36g?f?4zOnBR!=a;f@&ir+|+pIHX zo24=>+i$5j@%9O2&5=CHl>S=w@W~J*0iUO~m9P3*7Ax-l6&qh*{GoJz-^SUmqrSBS z&pUPK|9R);Su3Yr-ZyK*$=%Y=f(5u|SF!|L;NxeXlvpq4qt5Z$;N41Dz0hk5%%YBG zW^Ip~sC{Ve^a(!KU(6HeWz(0hQWSdRcx}Pg5>53K=ZP&(Z27kB+g3iO?t0IQ9A4Il zL!Fa~A9>zb;Cp0!;6<_5S5GIsNn(E2dGt_c?4MI%7M?4L>rXseEW2oDU{cMNZiCBu zHD6tKsmPZKuez!rAM2N)@lEB#@%o0C7Y&~R*UwUV{^Z5tzgb*vKUes#(VP2R!0wg0 zPNCzk7YlCKPBqTl_v+zd$3r%iSM3kXf7aboBJuV?x#aQ-Q%gj7be@GvF@}8Fon5r% zfQ@BX(mt8h8yxzsMra&&J=AtwN@wEZpev^~@_jAJaab%fJ#nXmd4gEl0d~WgkGB6z zdQx9gzW9yKsrM81`#hXp>+7&vLUPlNuR38{=Q(=6w)}DQeAUOzfq!yRg(tdwv~*@# zedM`Wj``WwEH_2=UEB3b?@dTz`NH7Vw_%CpkBdah`nh(C@6~!!C;E-ySICVz(J}`1 z&>N1T-@IhP68l$d@IM$FdLujJhN6vct87?eGsnse?x8mXdsb~Y9&*ES$F(EZW_hPn zy`FocGKz7nLkQ#BFh^~}Wex8F9ajqkJA7Zg;A)5Vg1=fXlzOxmG;3w>_GvG054o_^ zLu0}96$?r|v=$uJ%3u!SdJ!mEBB>^FAzAAMXH2@2`OWSJGJf->O;wcst@AN*uG@pE z$Sptq=N&L!dZf2vTi5f-$xnB3o}ERQ&L~|L;6X}Jx=qV zPo4B!Y>$`d_3YBhxc2Mb^P)epiJqTbT6wQ&yVtznirHPy=kBt3e`vSr`P_>BuIGP$ z*~m|@+5Wg$Yu@EuHsueLCq3tR7xHn=)u)#2{hsqa?}$EH-2bnpHt5rxZvT}PcB@Z+ zwycT!bf;Z^Ma8})r$49GxPG!}Sih=b-@^Cj%~j7g|CrYGeD^M!`-i`)o_GGy*!BGQ zuYZ4BKH2olUsX}J(Ek1YDK`BN!nNkzfA#yb^~ui~b?YDfoUC_#{zvhXpC$gSdi1mX z-`}tPCqG;KTl%Qd>Hoi9`k$Bl*M6`tbC!+C+%yk4`LiJ(&s;laEIDcUoM$U4p1u4P z?D<*MUsN~Xqt2x``z;Zlu3Qij^`E=W{_7QmN~z45UFofQ=e?p-D`%~AJ-XS={(QEo zzx2;g8}ZER^1C8Fty%6JVlzE+eflkpO0&TBuJqOaYI23IFZbfPUEI!aOV2LKaBf+U zdg;Q}vfzFR^Gk8J)Q>Zlw92r*5a{@e5!jM=%OIyl?8O~mWgs>0(l)#`g2y&p%li!UNm{^1jIYcJyy!Y$~;uaW9+|qqa}Hilc4z{B>`XO&0HXFY>`z z$~E!Pk|GHU?tOe(O|~E6Y}CKF)joe$DDYz?pOjPe+IRnQWpuW(IhE_4_T3xuYp+J# z*GJ10;<#9xUhjY2a%sBd@+qmt7aSS+)FLM6GwGC_3OK;kyzbnDv?GD>72H7~A;tQR zKF-Cejtly{rmbr5J+x(l$zqqwCsW+S{+?XDDsr9t&5)Im8QiySxR}0oP7uAiN7gUe zEc0`j(R8=Ok9I#B-fCq}DWAnIx#M(Z|B0AP4d(BbTRUF8=05rE_8h*>A4PSu`}}p^ zaM(D8ojLGwjdR+^xt}YV=gtn^;qW%9FQqEj`5!lG?v=={&m%Ti}KZ%^6&RnzrWQeeeKQUSH165WmtJ@ zFO;TSoT+|`;c(HeMdCXO<LFYMZjuX!@BiR@=QqWtAna8uS=y{S2wN?k93>h1cf| zU(EkYJG*bW-{+DgCo4Q-65q0yJ(IHqjWZyn5w#WE{u1~x!QRD`fjIK?R*Pn?$rqr)5U%F&T*Bq zEV%V@(N%@BpB}EiFz@2$6=IS%y>t&KofW#2Y`ge=)u9DfkL+kM{BmhV@Ig%}b%UFW zauzkmbwzIWJ!7+?W&g&{CVdy?-I*CW@5z0cF0U-cWB&8bh@YC3leF`%joMLTF6Ez> zIPDk_DF8H-utjCwg;l){LlXe1G1#y1Kpf3>J>9 ziWj%E_vPIwxSZgzx;cIF>pR8$v9ZRcZ7a^`zB|eD`)b?U5>flVFK%vosU;^b(Unzs zdD-QtZpETZd(M#N=wojSE*Q>XU#tAUd(-vDdWY{ZDYWGrR&(_(-tl6dLelq`_og|k zPZ#X%m|yj_{33tbH_fx69v+u?&K=>=dvSf$*C_2y(IvtjiiZ#AbXhg4eA!}r_^Q&j zE0)_lJl1u6ekD|Ns9UPyrjJ6u?x%&9I%XeSDf{QO<2B#2rK(dTjB4My2K$_SrByz6 z-$LR2GAk3;vLPj)rA5N_u}4DXr3$}}4JUdh zW=(gGsGZ9w^Q%GTsY^10Kz`(!K-<339G}344yJDVVkHu*#X^4zhc2i(alPe*Ue_0M zMC^e^ABwHz(&sQTFjO-!FsKs~dyq1z|ynsp?k}wYIt&mMl1B6D@|`P9Ia z$t!&N-7n7B%wM$Xzhhj}uD=KCUpNVFvtE;SIONXm|4X=X15YZ7i^v?cf27gNq%yexY|F*kq=a*d${`OP9f7(ma>v=4Dgnn_DtdDDs%w2G? zDe~>&A5{mwg%)nix^B0BnSJKo)Gf1>_Eg?>cYiFl#wzaPT)t}W+q}1>N+#9!DW+feN6O|6ji3oHInbpM0{q5fFf6{exX2$Hlx%*_C-Luap-`+hp z`+fC$^Ue8b<^KPwpGY*!{L!wE-+DFgcQ2Q5h5*l+oLuf%b(cBMbNv3g!0$q}>+`_X zar-htLn{>zPSHA@_o?$))$vVDiSv`UN$pb&bLju7r=$A9`ntS{^D^W43(^A%WgXWW zX&oY1(FsFVDovD}{S)JXSxc-WC#gXQyFMM~!uQ>bed&;ZZ zcK@P*0e`cDX4S%S`)j!zjuV@_>jU`PO?i^*xOc`+d$~oaHN?C7wZg<@gLGy94UHM^h6dOb6yee<2=&NjQ2yxg$j?lsxVT&Xko z0)?k`ByF?wWB0eyOiMJIp2(*y-B@ z(Z71*gjg9y^=65W8lNpRclDg{o2pQ4*+2DhY22mIXTejF4gKFTHPu+E6fJb`O6bkq zn0n-6zwvnV29QS5yC5$#|(sx0Q!yxv9UH@1UenI?+Lq z=h&+AipiqmMcAbn{rG9%i&7k3PBE#YYVm_v&iTRo-?Q+a9ycyVlF1eAj`c*=6~OtGljoZTuNAZ(6idvD^L5 zGL~h_W%mA9bfYo4a@}^pFSm3iNzCq)-g(p`e%-X4OV@=+t8`D9+s$@wPSBepcVl|0 zIn>fr-Cq@NS$Jqubm{Sj=gVIOZOstL-DeU!^~IxOEVK7!T(Uim>>7tN8^v=uP4Tp?oZP2UIW{mylxKXmCG1SR4w zz1<|=wYOs8@?S#USMU26+?snr&Eoyu4T^7W?7Ht+`6(n(>Y&4(U%8&UB`S9NFD+hn zVd^j8H{U|U{_W?`Id*sdnS! z;mzE}B5Rrb=%nFM&OPTte=7X4GnAgIdtmCq35nA`UN3Q*kr4B&Jx9teZMO8v-Zp>E zU&pxb|Ipd3!W%E}jikk%zZW`bYU2q+S2Xi8(O-TF7rw zwoL!G=@i+Y^UFjk*$or+|G3z^|7EX)PojIC(Sj!mY|~{9T$to6arEqsM`!JRcN}B- zW+|1d6#LA;f&X5Z(O-dMZ-o!I-1dB9y#F%OV^^O)w^@8d)~PKHI5uM?en`uOn6CS&3B6Bg;Ki_ZzpiL?kmrWyM5%u$a?3 zZ*?h{w#e&PX@vIUHNpMQgAT4do@&$=KUKIv(V{nF zQeo7U$C3ILj$2MyEB{(b9& z=e*q!cWkHU-(R~T3(6jdNj@(NJtw;(xS;Hjp5*hm(z)-2=fv)aKUV26^|OJT;GDY^ zA;&76YX8}2T2z0S((`$Z)^kf6eT(XkO+B0ar*4+l=RU`{N9nOl$n~ES9>^RMlzh(n z^;fY^LEmFv$>*|PfBsqbV9tSRqjT-A9+ytq`d!3qPrZNPKjm-nhjQ!=rvI3;c1rxC z?6Nt%`5*UM{Og#1^kev+bN&C3+QOjLRGQZz@qg?L3~SUF81#s%Y>^tnYiDxK5jiS) zY=4v(*DP075toU@mSR%`KRybHh>6T&4D9i!N}qN2*v*$)mzReo?K!I5`SILB#e3VE z(rtt%X9yK_Z?`mg7A7n#e0F8n)o)+Ug!q<<8*kcwH*xp=U;C@#!}o{Z-TnCR@p()J zb9q!YgjL_IyOmkEneX7cwR2-5&EK;9%>21!?pDrUrn@Z7GgM}nRIYr!efg1xyp7LO zKd(ISwj`)~Z;x#6th-yLE!cPSbiuVC^XEG6-{lnlF)fo`{ymq+>W+=$V$RNl6G8o+ z*KX}Q-QM1#xcW}XajoO$KE#Jx?R%ZdVb%Lg&1CkCh4&^uop954s$iCd*=EnSpf0v& z4`kjwuxZ*jSvYOOHpbnF<=o91*d6EV9aonSwQotthzz;SA7kzP;A)D8{?QW?1H24w zhIkwHB+R+i<&^XA=smHvWERJ{_gxLF5;+deTlz4nxbgA#0%O0GM|+r#9qIM(zL27` zOv2Y7?zy|+9)pxMt%efEc?yeJk|!PWSiRVEI{%xPfXA;mG}G$7c=?98uUI-mX0z3z z8;5!{#bz0V?luSS zCrW<*ni^=-U*!GfK;+?;tx-ZrJvy2hy*ivnMT{TobROdNPK%z=YWXJaB#U96$nwOG z9j$$v_DRW4vi1zw=C1CcAtul>!B{t2gj?6-u|`wkRM&@R<-?9o-FN(#XMvStc<9Wp zK~mf^ZpfTgK6dZ#q-jZot*=+JiT~8!x`E|%#o{NKC&eQqT?{s;C>?B*sxk1Ge^h$% zl7$hz8~WAm8k|nnUBh@g$pi4>b2 z?PL*75!Ui`{82cm_3MoTRW6?xnjBLFIWL*+OiW?ww$h7qJ(zXIBll9`I%!>tjP&bv zr`P@ESUY2G_k-4|uo%Ylkh#T;sr$H|yvWJAT$an?s-Ddu+Pl&~_Ti*-MfPp>b27|2 z5;h3Ueeh&ftdUv!*@Q(iCoP&CIx)i7iQjMQh33l~-@30(o5or9VX}tsoXy$XNA~zk zS*2;WT3h&%NQ+{6q=j&L_9>U24VR1#p1%^L_M<{sc+zQ>@W7Y_+QNG$x~vnAy3y2K zG4n*mDHn-AlLId_7p!8-*zj9FZ}(STgIjOw7dkQP+<#hs@lfiHn76e@4S457+_`%D z>XXz>v-NM<_WOzi#yD?SKWEmK?EjNmvp=Rb?JYHGvSl&Z)9nADD5r8)sA@!0hwrA4 zJz{J6byjKe#6smm=^|Sf|J?c{KV-Gm7sJ2KUAGS9 zDQ8@nsVKh6TIWum_02A?EA3Zz7c4sZ+4`-*1N~03@Z=M2hIUII_|#~2JXZ&Gcq_K;VcoW~_ivo+n)dO5Q)Gu#O=;@E1v{s2xjti)z}7GMAzIu|@BT65 z?tk==D|MlSrWW_3#pf{p-0? zt_xMZ4T32dec4%mWagZA0qZ%VAe}nC63`dK37K${) zdN@@4-_ts`qA?-t>w*tgy&R?Y2Cc{vznk`a?vxgzkGGy&-MwUKh3PMAze_AF;rlmy zkgALP(d>Ij;N;Yd`H{P|*=GAiWCzmL+&g}5V!?lA{*-v#b7kkfZ6Cz$CH_5_!Q*)U>fZX)oQ$(iW%gTUKmY7EFCpa5 zZkw+Co6iQg@*cJ4_$A)`=tWDc=uvi-!&==M|77hBAK9C>=!br_#l;mG|KcSBzj1hd z3`}jQU{h()HJuzTJ>^Scb+?47YHjIv*PE-deLizLa;E<7@h$70d+OHCZ&$ju`TAKc zNOAZZacO?Uga2+x48Io0z46*~f7zz|L;JUHmM?vAyzZMYJ70p)f`#8zG=6P9&$z+B zK*nOa|ctB7p<{%~_mp{$#u%T)lF|Lz%7pJyA;>-=3cI zaK&E(FW26OD?S>%%HUlkD9n>O82mV{g>>9dM(lU?d4+vLpp z%0On_qO`&*F_K#YnrF2uv%K;+6CQY0=Bm=-SzE(DWHd${S+}t%>QLb6)GJK9S*&4P zR}3STF1l85rO+sAA6u!-HO?!XKc;j=oe!KpEzmtHJ(W8|`jXbEhb#CbOIz5UCQWvj z+q0(YRN)m(qpW#lVf!4O3a(fo*Jl;$cJ``X+gYZsl5-X=I`?Enamp2$w`qbSM$ed282J%5-rtruXS}hd*xnpyy%M8 z1$(=0oeDHRwc@bpirEF59qu+ohhBLrQ#!rJ$|w8xU8Yk0ccFo|EnYE7mabs?svsw{ zqWxn>SJjlo^CkqgXI#HJndz&@zLksSJzMeq)QV=)73m+=IMnjr4-d4qcy-(;tBvie z@Sl(cb#3!kub3kmhEk-PksKI!&mlqRk+N5>EH7X9eDq6eucd65AlD8?b09B z|CmrOG{Nd)r2nsv=ljuG65G1FS*kb~7*ZthjH{w_+aUupQ)fnp3*QtvzFKSgJ_iL4 z0nzu19+_?{cL{L`6cKTh%wNpbA)u}@XGy8s-0t1e`9E9Ce{M79=;xXlKRMFpt}>fj zvTOE@^q&FUd;kCb|N7Ofm3Qy{`+L9sKRbg?go}ars`uHakFE^b*>S;o@4i4i-RW@2;g=9PkrbF@THYp;!}=bP&A>~o=5`K~z;uceRo zEzh0Dv#@Vt$O`e#jFB=`uXkiHo7O*{>u6Z_#WC}-`cvNRWe;lHgXX-IxTN%gTPa3k z?WeZ+>q5G=o-A4zr+-cGTbbQ9(+TpaOWc|Z&GO#%*WQ20B6Y{%hMexD2Wp4@y_Y>M zb;n8Kj?>bEAzNH;|6F+RZU%Q?W&wYq#krZQ4)PodU|-6{FTm$E?S47)$s2FHCL z$3ApcjMobZ`^-Q6p6K*k-e*5~%EF)BJym(Q|1w8^0;9mZ579>hgcKP)8TvFmb_X)w z?PZ&h_ogY&JZ-`8-n|n_ljU=YlkK0}v0%7;*Mh_P&JN+SorN4_y9@c>ls>k*@%|Wd z!G+%Tgd4r>4?Cp$4|PcQA1pMV*DyKHhIRSf8STb-TOO>DK7X*%Jg%WTZx73K%grq5 z91ef-MHalCFJh6uMdZ@_l!Rq(|IM7@krec9wdYEcvY+p^bxNu2VB$P6>E={Nuh)0K zd_6tgUeo!l+TWAw=DoGvd}a3OglkLo<<6LrG~-6Ur)IAsbEH@OYQ}2bU2gTYfg2Xv z8{Rm~UH*-*ETFmo5sj*@U3v#;`YxzbB$jJ2EGZc z%1TTVmP<}L{BOomb${a>8JGCQmfcx=VL^JgiOl)utnu%>d1PK%{`$xpe?_}FPMG!C zmym}))pQhf*QaJysy0t^bnW)ppET9?y1^HYNsEJK=H8e5A{>|z=D&9T-4?F=d8KCS zq_!jmmhWD0Y+q?kbt;tUmXU{+q8C7x;_4T-zCc^SOCX-I3F( zb>S!a>LoZ?T`nrc27jp(4q+0`zy3BSNOkHo#i>TZTDgh8ZW}M07_!SNF#bx`7lkLHQojz53nLzk5JVZ*qBsViz{>3^BE z>&U$86HjSBs%}md7L(NNT&8yPrh6XkEf7jo{}kzc>!RKkG9m+`nbV0lxmuLqf6EE9H!G1A~4whWhKCVi9?@ zlOgWGwfoExUxN0EUFgsG*p-pJa>@6e#(aytO;HoSOshD%sr7Ay`O!t1#x<;$Vy{ST ztACu37cmHXL|TKB()ENesdd-=_e_uf9KCC@?@W@vRTGR$Zyfc z#e0q2cCW6x&!T=b(;{=;^$K_M=Q7T3e0qM_9NQM_*MFnz^Cq9iGq^h}7~Ru<-^;ml z((3P=hRera<%(4a?5t5wKPB;b8+X1y?ftJ3n-5D0o$-s?(S5>v`Rj9e=?!nce|Q)= zMSkbz>8`(Zyv$i<6+6iACp}^J{ATw=M(}-NT-lC`GR<1|PM)i4lM($dC?>ag$IMoj zbDWBEkDSn7<74%7j^L$CvlHx|`<6YGNuQ#+V)GiC=kBJzrxiN}h#S6YHk_+`U`5upB#Lw{d{P}Zs4(Ib}1$m!-opf-U=jCu+q9oek z&fGcH`&y!u(xW`S8@x)kYkp_V&th`;bviR=Wprq{oK?A`Nqfj`fg8$V=YMa?di(PD zp{gdq|HcPbxIUk`U`L{*f2WekyLr(Gmx>qeNx19XEuVeMbk^rpueB}K8`g%->s|l! zLWB07H}!MwXRJG9y5n5Fkg&nYLXo_~vgUq=Yt7Fca_;=kj5_ULE1j4)kBNcd0Sg0z zKCY3Fpv>e9$E4ztqQvA9m&B6Pkj&gvNHeB4_;lW72NB!yB=v2N1D1P6Puh3Ca>|TP zLIEpUx)OvNT~_B!oE))I+gz-(`G}s=Uje&?9sw$1E3)ml3XgkwW$aqvCU>^yyzTch zd(-Rw|NO~)z|4&ESlxx(>vzNFy%+lQe$TIq-BIc})?Ys?72d^uuQU4P71{fVDci3W z-2SJ&{;JBF@aD41c6?HOvPQ?&S;}7DC$-bL!~Szr+zxLIkvx$=8x@Ue$L6CxA$zC& zURE7ce4E|R_ms9*o5$JT1{{C<=D)By%lSerDWF8cLugI%^To-({Z?o0Un{%&t3jiP zVwi^9tN_Er2Vy$sq~$7Qopx3|HmNyyGV8zz=jCrbdzlx$OuL=SR&(W6an1It1-DvW zI&G9X`J!!JKL7cdiC@`DJrmj{a5i6myr=vB$3Dg~pC=Q{3#2!*tgVwg_VJ%e5k#QyMZMqGWsb`_=RZeCESS%4^jav^?)ayhQ;+PNc;r`VlB{;b)Bj3`Ji1KJeR4c{ zd57DhE!ST=ir;+QlhQH6$xCAG6$@4OJLv&UNjVcGj0`0vPxFY`)_mhdMivH!GEO{$e#lAPGq1QLF|P!Y(x--H`%8xl*cQLtT7La?!hyC= zM(-cEH!U<&X$j|GG41q>TCk<}*M%gVh?G5(_O6>!=M-A!QSYR+b>$U)(fOYjuR3@4 z>Bh)6lTLj4diSnz`nfZAXMX%$|NkHR0Ua~WUwwDiOxK?$W+BY>MH(BJ^cq!%wC?m>0Qy z$)VzzvFg6=@63FbZ#uorzdTOJ&G@FnpJ@3-GjiWNV{yIc?P2xpnyEP3uM3MG8-FQ& z#l*#N`rw;*o2u07THWT}ulGE=Au18cj9S^JpbnNA4&>1{44P8x|^ZDKYZ=w zTC%Qe*K@OcG%rD}c+xMsg+Dkezp9qVaZh@%@Xk)H z=|2>fdet>sx@}LJ)fZZto1$8MkJskVs+RSI0oI#*%5?JB=jI(1xg#y^x~(zr#3Ln} zhxyajc)YVTyrfCMEJvryd zd=dN4JKXMMJDmN?_*jMzUfZ|T`?wUl=`$;- zBNfuCPAXgtkzRHC&ymGh!p>$AhgH^FnokmY@lqj*ODAEO>905e+f&(s#koyY0z6qi z+Wv@aywH|q=wVvoT-WtBDT42q!GYIYVbP1_*)P_pg8N+)Eo)0&E!eO|+qXnY z>CseAr;_hSG*s&S#;8>Zx63uzVCPV?~~aFI1LS?mOIAsEY;L;XXTc@ zcGROKwqR#=Qcge#%a+xrSnefEI@ewlbmyGP5fOgpg<7?b(_e&7m0|w$Z0@;lq9SzG3K7?(mNNzN=3e z@B2PS?^a!xSI(ZLo#L5-dwq^g@A=u)blAxCcF*05NAjXdSe{Kt-#lI9_Wc5#_+p)V zbKn0sr+w|0Zg%L@_p$fH>Y4u`)tjIhghl(9p8sKHU@#%JFbPO3D#=XDffgpyVuNpo z3AEj}otEP{?O?*}^%g80oc}Z|CM@AJR5TP2iZVaR(#BnNQP8dQh=lc2*R&nUR(~e^ zYlz!otb6v$qRF?vxtH#87Zd53eBkWIix=M&-@Cim_I3Tgzn}FPHdi zE3?EOTZ3zZ@8wml3;h}wx$b$y*O_5gS4HRSxPN2g-CvEDR|pzST^6nV&a8aRqUUdJ z9?e~yDsC;~-SVO7a={+gLm}Uc_;&g>ZMt(OQFX8A^2Gf&?%&>amp3~)>$F$H`lr_G zZ?4&XYmQ!%@&(_P1uqN)RF9OeEf3q6XPR|%`t99iSGyJ5I8uvetA%`#df_-{!iSSb zr|TWhc%QPWboJG)1@oOxoDeW-`Iqx_j;|-vBo9fCgoe`U{5fKZ@jKV}`1#i_J=r#U z+UpYn%@)d6pWTZ-{WMZ5`sJ0lQ)>m)?B<{1SQl^gGFxrk!?uGrr9L0tV6*bgp>r&@ z0oyK`Omw_;Kts*a@%@@#`_3-ZzIkW+8o%{7?=9P(qSU@-%c;-TKkrSxUT(&B&+9Z# zbeTu=Mw{wp_0{TWd*XXMm!J2UeQ?VX(MtdM$Chk7Kfx>4_TL=NaszeF@}(_(m)}gw zl6*G(Q_XwfMKfn*8{ay6!@lN8#x}k7yGDlnlaA;KZ1K69w{=El(#E|%BJ|j*XKHL0 zpU$x=KQn3R-cM6DlnU0>eQ!NI=l`S5%e!R$+rKGYer>~^+MkM9|Fq*C7+Y?4WQiC2 zrMJU9;GjiHDrZe^{(<|Gbv*VJEwTw0d}esa-}_J8hkXBiAFR*(k$-V||M3Uw_b4s+ zsPVRevFFF){7yw4=6^mynsLwi&1FBxvD((gi zJuxhvHNV|4;9<=}Te%u7%ZHac4lO@9GyT8b9QW|2I%Usoc4{rZGk0@M=sBcq3Gdm*qH8;KCeWmIe$F%E(vFnc5+K&NA2^wym%|T0knZ)v_xvH$VxkF-~ z;9^1N81KnS`7B}ucLf$Fs(tib*u7Ek;_ZcodP2gxcFYvHXdJpObMlXmAC^0(cAWcT ze&LFyjH0*WYyYkoMZHgVidvWX-TT#`=sP#9S(5AB2NBW84{t4FLVmBqeR(0g~M zH!w>4`z58acUIT0{C@Mh;atgNk+a_a+uwv-I_eblB{oBK)~*wA{hBXAG^>%)2IGg#mc}S$Irmv zOicSbC^a!9F)1e%+Bk8`OwCCND9SHLEh?$h15Z*e4bKf04i~X|u4aBG)=Wa>U=x#) zR=(Gfa#x40MHwP2T~YBboWvq#i|@V}G1D*l#SiCS%)btv+fuen-m31M{EPiBzueg^ zeJ5vy_b$*EKYgyvH2t|w%SP4e4UHZDm?@DUt&aj+(iBBr$9n(IaI-^*^&)c4sd+)vSr>FjN&x={O zc)tssUHdB-eY zI^^yzIWYZ`EJp&*jwciKz1f*$Io?{Hu91wIsB+QxVH)$g%FDK%r?owee+}b>c4;6<9mPG3xhwwmU7-L0No_`DS-?93>|S zh;lN&JFsWTeO39*R|VBLM7rMX6Ti!JS1T>^XqE58xiX*1f^Tju&Mcj=rgN9HkMd4Q zSEB^UX-m#=y?i3#DtAOmvhqyrj$0G^PZn-|!l|@H%l7@*hUIckB)1Bu?0&IhUUjbA zs!It6!tY%uIhwUJVfCWmD@l>})J~iAURb9$ZA;dQJFd|e_&@PE-B|YDv9j=D>wVwz zGD#`XFLJpg{xW=-c=7&0iJ#23>X(|cHLW?qAk(CB(~sX(OP1l4=PJd&L06t<%7(sY zNP1#!aPQ#5?O%Erms+RvJNVarxoGf9>}H)sn0&2L&pyv@Dm>TR`~L_zou4F|D`o5a zEkwD*s%M^B^|H(_DmOp6z4)B?h2rcu)2N+Y)X&r-r~k6V3!xXdU#M zpP4M1Eak0kxa~zmWOi`!k z?4tW&C7dS$PTOYfTpcC0B2{p8_7o92j}1$oT71jOSvvjb>Q$l#t2;K$I@I1$xn$#U zA%Qi`EcH$e%YQ6n@_uN1$nN@$DE;j9ntNHyIrsN99@1---r~MVYV#x=r=xowM4vi) zt0}M`dg5v=?}PipdVhzmd{B8cV10$?o)y0iUET2D{*P0Qiy}WvTK`7;PSf<-r?+;+ ze8{hT`l=}YgKYlAC+*D-?ytP?s%9R?Kiks34Zr96YW^`4jJ(CHyQzJzPsqL^^d=<- z?_%v(W(J0Ptaw&{g8HY<8AbVd`8oON&;q|V`KQCaxmV~q2SPT4=4oxg9@;zuufE`Pjef9m5M-zgs! z_f0-|xBC6hbLIPr-|hTLFt?q4ny`A%?+l9<<{wCYqQoGjAc_mrXE3kg&KIvxTv&)dH5{T4gL zGosSskJn9=5-cqJeE9-zP1?!t*XOR2cdZnYv22fy-G7pscXEvW^UdqtZd_v>z9IJD z|4UAMmBQBx=1zDqIiF=;@C5_k3voGi_4_~OHBQLkxp__J&H-O$;~7skzmeX?yz+sV zVs)rg;KOIWA_cP@+&4*0cW7yiD*pPZZtsnpI1QKH@>g#Xa(3t5=i4o-xAYUM_v6jl zajR}_H8<^RV?DR=ZHvD0b;&YY{@T3uKAp8&iZ69X7zG}!<&N~3aYvg!@^fp&*8htp z%=((!nf9<{qvOjbKl~Tl9@9x_7M%B?KBi3d{u+t%p+_pORD`xakWZMVZGTW}e$t!j zIL;R~UOW-;ZO1$9x*{(*m`u(&vis%C)J^S7_mBo88^j%>u0dy!=?; z6tpVFudEQe?m6d6m90e5lRsOHTzwQ>uiX^Mnj_@A{entwQjLh=l4OJ9!AcXoCZ%>g z;n3W)+eb>(_1i4PV>*KGr`xMu681bbV~VQjBcmfJZYphj9@mS1Z2Io&fAaDLYX>QpLm@$kx{kF z%F_F3h)(Rz^p4$<)5O1uwMQDblzK%Rj$OLi+amDY<(tuyMES#v9zC?4lwheUw>Bc+ z%kl1*!}cFs0*Zas>^r)0o#divg16ib@XtJ>7;Apyw#=KA^?RPytXb_Z9-ln(#fyBz za4D$U_h><_Z5=ZMLj?x|gEqeAnoD9ys&8gWUS@g*bO5P$>TSQ<5h88>w+gFoD^zgc zQe|OMb>It*smfdUJzbH^XMJ~}TYvq-0t?RWbF0RRy zp0dlgY0Zxfd#`P;`+u|g?fU<<|CkbFzjOXtcRf$;#`!sIzB8)c%E;tJ>;B#uTs-$z zV)ol72eKk$u9jt&ZT@(!@O4Yf)$D6$mDi`u)O~)#tXkNNqto|zX;gGeO<_PnX1_rF ztE)TSL|iR=EB^Lz?e3rLTlHIYPP^>?HedIA_8gX_{0k5ApF_3!F|tp^sARzCOMq4@sKlxUr_ z@LgM8f9R6>&TIU)ar4S*@$L!1(+c|Qg^T8Wo^|y{oMY2@&$?Sp5122Up4xA4YQDig zeS6LoHPZ|asHXg|5p^+475eaAr8o5R@0An2YNSX$tL~rx@*!v69+Ha(O+xj|9#MBrpvVHN2myKnd_HL_o9u{BtAk<0q zj3vw4Urbpdp?^d|E%}1_r6iTPO3vad1G`R_t-maB zH1+fKBRdp@4sv;QpIj-DTC&Tf>-?o7C2l=m))Lfqe35hpkohp$@|bFf+`aL4UbmqbOWJwLU9JSNT!s`L*nl*DZ?XDxcL`w|ScMtQ|9xw=2EX?AUvDV|mEacWd?D z1>Oj>e7;Z3tSITrZ9l0Ln+H9*p_Mys9#yu=zOD0TquM+UL$Q~aS8Y0LX&=1dcHt{y zv2RoC6)Qe!_y0Mjynn-$geT>4D>p5W6%*;%^I+oim=BiXf8-I_7&H`o>t%#$JsSgq zu^^t^G@z;$l#P*QJEs2*7j_pp{@!QP$v2xE4s?79aB!&9_}Iv`sH2Hrq?c*YwNI)W|Hb?Z^DbX1{^iX7cWZr!|NApJNimWi3S`os*S$}FcW36^Gk?GD z-~W%TA^3waW3pZRr=V}8>e<2>^NNGCFNa->D=7L_pqQGf8#3k2mba{-9Kx#Db!{wcQxfXaU8Oe2yj=}mbEszbInsd^-12V%T`IWRxvI7*XSy2l3}ud zG07sn@TjK1iGb zFMMq2VON`E%Xrn8XKzF9`Tjn5n!U$e!)L}6zwiqyAH1&m6}tO+_4(85Z^jiS-)SkE z@vQGU$BdQw+a9->+%Z|`|1IyK+_S7;8|~xoKm1`SnmJ`zwM5k}wcO&O3OV!qr^Rh@ zCjx7t&)gB8S)sz)U)bc7u-AXd%j!G#?rqKC?EjPU|6F?At|yVM>hI%pEhB3-A1}AO zwD&$!$07b7i9fWqf6qC8a{2L}(z{a{ST*nDOWw%%z-*Wvo%wIwPrXwOJ{uGdhj+#= zdNFCv+yfW$qt)bEOb@CTRDA!*857_d(fCd-x$pRkN8S^@J!RSTjp@_+(`#c_$}La& z#W8QUhrYc8>p#8E|9vY|g+I^we&kDjh5wE{haH|(mOYa_Gu!gAS@lk}*Hq^(<^RW*_FHVrUnz&)s|&tp zc^#i%#qw%`&{a{FUaf$;cCR{wu1dSi4PWr9=1QPymO_WsatGdDD->0;EIO=KJE+!N z@l<`~BVfA7q4&p%&0beB1WMf+r9KCWPg&vZcqNlH>yUeF*rIi8fy+f!Xv(+R%i68- zwqZ8tyVdl1j!TvYXU^`>S2Gt>N!+%Z`l~FUmg%$kSsKT& zihZMO@Je~MtIRk2R$RHydiC!Ozm-?~n_ng0uxoz4ujs+`h9H_*Tdr-B`&ODL*xM!6(O%?Mz$Q^lPVtyS9Ef?_QecXxQ;8 zWRVxs)_u{t%$l}1S1-wqow;s(z*`7oyJN^}xtCGl%U;(ClqOs+X>Dn9|E%orLZ0`O zub|tD+mrYCJ@2wtsuwoDQJYlag~wSg>&4$`^9@U1bVqf_zYd2ugU=H;x(XJ6SFv~2C-uc9ljRcBQ9E}Y)-miN}ZNnSTQmX+_Az?=9> z&{y7LD@$+tty2x!)#rOJ8o6d$y4Zzpxg(c0F=VEPV+;49B~g6E*2#jq*Ldze__lP( zwqwtPCc92My=#g|fv57yDc|qNBnvK&=}-H?@aLW9f*$b~{RZFA&Z9c?tzi9g76yg` zoD2*GgqotDI?OGhe<|F+Aoe5@~jEUph({`{Sqkh9EOpLchw-xtsO zJ*W7+<@fvl|NUY=AhM0K>fp+quGtH7qFWlj7JWRq=VheyQ`_u=h2MS7Z@%;Sx9epy z$*9$DqSpOg6r(BV_PHyh)ayYnwDj*=5=3shiB5XZ*-nyiwPw;@r(t z-`Jl;N26B0eH-zvZKrZ*pSMu_W##jQZ~iu{Ui(GEgjuo0^u-H>BlBmSKeHw`%Yb}PAY^!DXS23%OWSh(=sjq0F#>ORlx>*c>~Qh#qya=PSnP-<+J`_+=~Yp<>f zFVkuXUi{OUk@fsjhiAt={o}jIaM-5GK+mE&OyoFcy1jl%;1|K9e`UR5paqb;hbK zakm$H(!S@M*~LjuPx>=YF9>Y=V{|^WGTqL@Z>{n(-S&NA3QtoL7yUYMVEPWVo$EF) z%lbL_mg_b*AD+7TN_?B`#t` zxIs##g3W{a{xzC0j-mg;1Rk25S}e)&kMY-!Bl8W8-Cy+ET#nQ2;PD1y^@DFWG91;} z+wfoYW=Fkey>HRpCo=`UOGf>ZR^N8~e}myiwwK!_;{LII{JlT9rTXf7_4wbv)eJif z_xmR;+V|Qpj6KXx;?c}Rx%3IoLZ)c7G5!0lWi;c6rJL8C%9Ne+KV7XqSS((!@!jJI z>nGbz@4ENkRQA7RYW$b)#2wxG?L~}s)r8xbcUCO+zkX4cvFaPISY586M`1O4_uH5I z^Onj#FzbK%?Zwux68r7nH{6rkd$a4^;|mky6&iKl)rAD~tWb%Qm634}KDq6Di=I;= z*Xih$+^SBpzWv>Pfm|Pt@-8%I*{iN+^~lhuMDX>J4QHI@?$Xc+b`rkZ*1_`KNUy5@ zsK&zJNh0&Tg^vp_TKw|asx0|9xj!`m@0KrnZmq}GmOSOs(S%t_j$NC!Ox`8Y^+9cp zp~^d53ranO_`+j_|cu85=%`d z`f~p@kPS`kIw-zk-dE3E65`X>9ec4!Z{3FLMqf<}-)`z)(Oj=}U2@*ys;!=vOm-JK z3ma=*{FzuAICXZE)QO`dg+-} zUh_7dej@%|W$gy@)w3eJxfkzTwfdCRVy-KH9;qKWcDQ0i`R9FV+k|S;{;ABizi^~1 zY6Wj`=+9^OtZTggIg5$k|9e5RYX9{ki&eLFT%#;F6tsFY^?$e`H86vKZhKz<2mSoOiG1ZsQ*7eN^ zxLx;Z%_hUwIb}0bH-44Ae3JjAe@1$g^<{op`|s|hRqtX0yc7#1-rbx2+~)bt@AIn9 z-T(jRr#Qo%*5@2qcQbdpAKR8G$#L@fvx~QN?;3wQQ+{mSYZKn<*4o?pO1#xmZs$p? zD^0s~oMYy!$8yvC=Cs9q;)s4FnOYr|+Hzr6=IncFPG;W&Oa3t^Mife^B&|uib?o%F z-Oh*aUgqDq#%8;i*1qpD$FqK_9NyCA*y+U3#I-`e@q*u%;Jqm?zZJf``mnR`NvmR; zz=@~)ixdvd;3~XuV*U4PIf^1#*UOw|&t75KVsjyOa`qdpsbA7ATRGo2b9J4~b=&EE zYu=pSVtZMOGkEbF`9iHD`+iLP%ypQzrQq}jEx*q?T$>Hn9^z5?8P_9kyY5~6zn6A8 z55E^}jpsgeOW>@sXwme{cSg+y(^uCCY(G;NX*DPKZr0@TH9_}FHD+l%D3Z5mDP+8L zajm(&AMfq^FSVXn#x|Dehkc)2a(Yp?p-;ET?+uIiwG}mz3Qzw$=+@EwZ`)Igb^N=Z zAGUMs_*NXSCSFnSiCeg^(~}z?s<;+^xL{uIGQo;}_Abw57gyy<=N-Kit=qgPtLn$M zLmM2!_P$=Sw`+$s=d8FDcC9=g1Y9fJ{&UJ*|B@B@%3ENOna^V1gfEM4UOn_)Aoefw zvHGQg%rm3<7-zJ#WUzKcs4=?qZ@!p%KxFOD7;6)8ftdY{TQ-#O74PRMxGNNsDS!32 zDZl2%b%&UACb6dbmPeZJ;u1Yr#pm6;M7x%0s&~**?^7$MwFjP^d-#+>Xc@~@-=K3V zizd7eT6$E%;aSM@4aa6~=+ErDb$juJa<+>#f8S|*`Rn+FY3uyB+12xIv0s1Y7xeq4 z#+S#AUwH1SbQz}vEc174dhcYk>HAzic9Cfk(|(7DaU@M(-+XF`fZH#jWnGCp0ejdJ zj2K?E98-$oS*+E4)bPaRuC8Q>C!TJJj|0U%J&sA9eA&=vg+=S62SGP_S-QXMWr>{E zI^W8^yvg&;YDcNr(OsK=mhc3tWICpHD=IB-U$()A>*>6lR^9ZOHL7-#CoP@*K`t+G zyNzl@UTAWq7LU)gv~WEIP48@$9lg`v*Ku`A&DPg^+Qw<`kbdJ>pO<7thmAfF`TWHM=O>h5G;vYo*vUolJ>XUe0SSMhs zt^6L>72=z=#O`c39~i>W`?f|heR61P?Zg8Y)@7UYm~*YJJ?d1K7M^);d+k&Xk@Ed& zkEf^Tc&~pMd^~UIg&X(G4ex~CWQc0V57;Xd<9;tbGPxv4QbXudo!xVr^Og5LKYo5a zpW)j^&w#x@Y$RLy7kPTDunN#Eh%%cA@ZR_X!b+|%f%hNm)-uZLpe>?m{<3+kt*`@W+GiIfA+p&3gbAY;Uh0^72z8D zP)dqr_4Jo7T$A1v>76{!Ul+cyFE`~Cr}U@rO_QGfjJU?;BgwtVdV=lgHCDUlKPcIk z@YA_(W6Bu=2i|!-UoE{hg*4VoO;!GxRb+mC>(uv*t3L~#OJB2hx~XnT)XAL_jm@5B z-9E4{Q!xHan(>V{ai)PAmrb5GbKwnxl{+5nTEvw3XnNZ!w$Q?zFQ%OcjEPdREe;Kw z`Q=RV?CG**&--@7tet)$I7C_beCp?}<40M~>tu`Or@v3DY8H#S{&eeEAye0LyB_fd z3!PTJx#wz1df>X$nB%N@7gCk0Yd%jqZGFi*M{@P;`xiI#Ok=pE;v2BI@AU3!@0h*G zpJV&aq()!jFX>GBGghVkI<41-(27(yniva@$WT5H!x5 zn2;tTA{gk%Dzf$gjxco;Hsu+3R8M=EEI# z4_3-Xr^R~xJDdOG)slbmok?xFUi%s?mz11QI_sSow?p>0@)NhzDXVKuOxC98d8JKr za6h9PbE9ug*Q5W^5$~4joS3E`qv~=y>}=_)+%1>jWXbrmu4!&qI(6T{i2Y8_PBUoTZoiZlDD`Zkzyh)S zK*c(ykZ)W&9j_b;2)XC3%UinDsN+;m!B&$iSLZrjxx*2jt--cJCi@9T=%RHSKF+*- z#eEj%qf5Wl+U+zdqyjx-%67+Q9D1QI7u#}3WZH3maj%wRzqEXpYqb7oX)_RQn%B|2 zv7(13_Q)|!oz>yXIk`?pT|2TwQEBgFVe#OV5~`)M)Q+ZSCaGy2`*m*Gs&fyMzXzMo zS+-AGOV8-0#M#YrSz~qFB+jHR?7XulAb-uOQ$l(heGaxx6=}V?BjVw1=M|@7{z+wq zMkYH>%+pGo<&~2jD=h6PxNe7^bLh5+fMpBjO;XysR?B+!b&bG)UA9MU`|dnHaW~s0 zA(`LwV@BK&_U?$#c^k^6SAFN1@?{AEspsniNaY+Bt3MK{y7gh!a zHGJJ*=zlp*@)0n6ojEaG-3R_ofKjFLPN@&(% znF%|(1LRxW892L+u)Z?oZ=JE#SJbuT)Ty+$f4|SZzuEZT*Wbte8E&r->~XHYkZYxX zr_b3#e#hC#)8p3XSN~lbclq579f=Cv<+p!6sLFL(_pto-*WmrB*MGm88Qi~Rcgv4? zuXp}k-r`i*w5-S_VBSyNKI6Luw$HCVcyr_BzK#8P7n09qa zh{&={VtIX~F8{jqw(INm+u4=Q+A~k-$f>Ey!dlCCeGWb<_$$2KUBhhM;?LY?Olrhr zZpMaNb>*yFT0WOixa?T`s{EPE0;vIj)k{G?K^PF zBCAd*%hW+seOHvc`_uJ>p>tkj70XQhy%u;jxHcRi)vX1tp37t1{x{TP) zPL$eEGVyfB6OWAN**Q}^C$JRwv=Q^B1gR^H(-{Bd)_wcv8mr2^nQ`+8dJuJID za_8Aozf|9C=-s_*$99D^ALm`V!oH@~Bk}g!G*(q{Bt-{=RYCPACGb`7!RkJwvs()G(D(9Y#`)yX8 z_J05WzmNTuv@WaroU6N#dv%ZO{)dK3?Y?Ip-+lI6)b3|yF=}~__MaBH{bhFD@@XYDw<_|mu~7v8~Mg(!}T}+X~|q9gnk;-=W_)DqDS9X1v<9e!j>*Og*CEM3s z*156VadUBxdg`^ZNzc~4P-gkK_IWLDTbx3~rl#2*Ti@+#m*{vcTD`ZcJ2|99Q=aYb z{yE!s>u!I{es=A;`zxw-RW{o2=!-9p_W5XDeLFrZ{^^Xc_D%Dgr^#QAYRU`dw0vwi9ZEt>c^ex*zxybr{#hmJ`3nJSu zG~3Gc)_g1cmv0xwl1FKfs8__4r^4;qf zm98%M9$s}}eKGfs4?=U*nM+KnKMT!GXD;D7@O=7v!}JMJ8V=?_rmH)gSYeb zhiV0>FRXMJPlvBLEc4-_=xYP+nyRB#XBgsRHuBw^uyp^*+>U<}w}=Pp*nO$8_3Eg& zaZ^^Xvnb>{kNXLU{yDEz_>NmPy%Sn^lEYFs({Pes;U&S9fqk!5KKdZx{3@T7~=pf@B3f(y?0HYF?+#zwIeZZE&|W|8~{5APHo`73eB<-Tu6yR=f;?{$;#^3$A^fnhceo_*w?tnHJZoR|X|oq$wiydj=ZU-!`}Am*1gw$vpeVQ z%XywW%FSY~%d0`leqO%2{CxiZf4`&|)-^RQ5PQ=VY?5}yNH3^-oAMz_qM9-^I6tz>)Pa;Gj^m$Ewk>M;M2ay=rqSg&)qK9 zW_@0kdM32&8gII1{PS)h72pXM#EIkQqiL&`;ARRdQqM`!Ne=6Tw$uchVH z)!x?FW!RzYa%pBNuS!PPLQbKS3wMpa3k3aq|2bGfH)-P)*muK>G z^H1JH=jLd+Z)U6!j~cF+uUxlOdFz84yW1YW3y!OLpclUQOZL&^jrYH^JeEq?dy_>q zXSv-x_j}(x-}xN)nCWlg{5x#b=Z_n|&kWLET6S90DxCkay8N1zU5j@w%e7rKBXyT! z^22EFiT^m9dFCn3`LCwV-Ijc7sbQV|v(OpMtX^B6-^}=+q$gF?DW4Jlbj#sw9ISHB zg>rA~N|#IgaGYsr&<1TDg_8O27!-r8mmRI0dSpG@dYj$rU#5ueSlHy5>OOm`&f%Fl zudA!s607%>Yb1S`_hq$qi}9i7a}?Hybt!#e(ih-R{gwYPv~^Vy_gU491%_^`3lqMu zFGy?u&!GETea_FhVLT~PmTV7nwsZzA@YpTtaK=LVZa{|m%;Q&;uQ3#T&+oA1y)JhT|FDH%Gz<&LV;C*^SiPxp8Q>_zvDi$ z318bnd6tWsQkxTBv^$pA_wF?K5+ZQd{c*3s7k@{S{$dlJzbYNiJ}sZ{{i1MPm1&=x zUCZ1CREhYrz_GwnQFQ4^VYBd#$>LYt`1vGS$K=l2U)@EUFMXJ=<$u;ItXI@i>R8g7-MAv3Qu^p@SeTqo1SA*dp6BTSeyR%#eou!IIoo&_IVaPZ#uP0x?{$w&4;Qc`KzlwX0g5#v8TdvyJIq)AA;tll?NUFi2K4dHabO>Jp3fzjwURn|Sh)MQvrn9_`HI zoh;QBSC8zH)66mIjDH+_;|tGfTPrQj)R!AC2+!@;u@&TAUpQ&$9`#3O+jP!$x^8uz zyp;94+}6rXTRc89h22&w%I^GAS;wNEv3Z*4woAv9u7sVsr1kLHhHL$TA4DxbOlsKl zr7b1@V|Kp#q4FmevK6m*?&#aPY(k1_(fhp;v>a>P<}b_0CAio_ zE^^t^vzJe@zjV(?kFvVl&UgNAz}$JUKf}b;c@9_U|Nr~8zP$h3zrW|_*E1fFt>{`1 zcDJoOiJw^HE6%Cz?%E3D&1S zzC3jN`@Yw=zBUvsy{n-i)uOPbfy1*7_{nU2^sNiyK{z{)e}}dBAce zsqDf*0lU8|jk^!2IBm)MrMkUr-9Zzd6$Y%vHP+Ai?Y@7vXD`v*7O~U#)@!!Jw_Mko zZ9kb_t~p!TlCtbi;X@^Hy^D-FhIw z)0NsK3`r&X-O6>dR4;EjS*CEBeG^lY65IB>@-i(uR=mHl?ltSC-HmUIS=LT>bi5xb zbSQ7O(%sIN)Ulv63hZRJ+Zo_{OMw|}zE*lC~eBs|M-!pbL74V)ew zK`T0p*c7tnxQR18HWAxUwo+duVt?bdO;wDW_kY=##~Ef7@O8ta2||yx&K%^Ob#UcN zvC_p)0|c*dN4#j)`z{e;6|j``>58MP12&6qxYHjc)v(L+oTV1?O))LqTMS0-u?Mu& z6Ztb6Pi=L4b^C9YU0wQwUvc}Z`c9o+f7UOk^8c+jl3M#EmegHYKX1`&m%=ult}nq8 z9_;SudE>nKCQEB?{7jZmF|Onp?VC?6;o$qa#7}gw#G(%VgAp880_XC+G`tcb)}1^f zU0k%?@Jw; zdgjC>EzWBjet+ywj!Qq;bSQ6mgt)GEdFwj$npboJc#w&@MoD$uBiS)L$=N-eQ!Ds%~Ok5@#aTk#q5ytDVfVlIO7*p zaF=ZRt#t0#DX!{y2TsgOmz$k08uqixwMk}uP`$zTP^BsNf6r3ezvSemFSF`a-Ka7y z`SxtK#JkmR8s1IVFAW+Lc5OP&d@Y(YvrFirn9U!gL19n>ZdHOWyBQk;L%9G0gATrN z2zVI+8X1N*;*i#rz1!q)MdMS@g$98sKMt@f{b111km3@N-}+)w&WWT=2hX}4QUCHo z|I7D(oNq04ZI|1B`B40S#k_knZywoT@!+As%$vXW&fi&{R#x`x{{Mgf*ctQ=s5AYl zS@|e!&F->;N*9dM(?1z6?TMUWV4HnVFRD2;XNJaVi^$WuIXBL6HJ+O}Y4@Zk-PE&F z+GkzS>lb92D7`JYI+^o2P3r*FDZ{JH0C1i$vt)YGvB(<62Eo|*jp-yQ9# zT&avVgcC$en4JW!IkdAeZ4CXqb*9#gb6#3|_hhB+(J&F>`1(~}YJtOMK3{>uetY-C zswph8-ofwl(x84r#xAq&bNq(8@~VG&UQg-t`z~*w@%HzO#TSwe1X?URT$pjRK=+u+ zWfrLj{>RfEhu-1!S+{QLxy0jaa|Ha<4QKd-E;G6*e7c0!LiNmY!>7D84i3rN*j108 zemvv)tCw>o%oqG*$K(~5Vexum>fR%YE5a^YhUFFXZ$9)`rfkEx&>9~#AD+c0*Do#J z!_RzO`g+_Emd$CV=QLyLSfo~8E#b4}*`c%~TyXYtrn$$ntVPd%I__w4$?{q1fsTS5 zjos^Ctln{Y&s0szNc)P52P$?PYKT7c(#&dVZEXDgeV=E{DD}>s_Byvm;sT%Wnd(Um z6K|TT-{(9$h0%6V8Nd3b2>Wx!kA5e}7*c%y;MLB3OsOX@|IrCS{D-0H*{-|Ts^+90sd_q?8ExmFot{J)9KVHJ;}H3BOf zAN-gXIB#pr?kTc~>pyMcQ%fm$=327&pl!-Iqs?#revbXte~N*-NGJZzwne9vSnfpc z(@l&K&)y@40zKjL;Y6EKw#kKFXxjIY`7t;IC!ID^xvtKXcz<#^hbD}Q2OuAs${<9bIIUakGcT*7=ip0no2 z|HC=K$NGJ=uZeafNFKc=5%7ok!er+9rip*J`Tq9la%oNTVcx-gY@>}@h=(Q9f$W~P zYNgw%wbxdhzci`7;o?5t-2JQp(_aTf@h*MXefwbG+@O$f0aF#n(%&nBgF=)#cBwdi z{jnlWbCrF^E)7T3+A9qrrV=i(6BY#7z1kyEs@Y+s@5uXe#p$3cTNSia9eIDRcpY>l zL%>wSvG?bS*+Ez4D!iJ*V)}0Jhrm3IlvZJlpkf0t+b#9gO@d$!Hb zQIVDKTkhxYBG3~b*IQP4;aKd)Ri`w6^gJ*7uO+&?^Pa@pgS&Y`r(Sy}n-M(KPUMU5 z|JU-%b3H>PEjBZ!d%peAFn8|d7YXdWto^))e%n;V22AZJXaHB_bu7ms@Rv*aj1ZOeoylE z?Hj8`Te8Ff?cbYJ|UuBXxa0!?sW~KeOUHQnGS`+HtDZ*uvvSg_HlBRP&S}dN zxrxi}bv>P-Fa4g+C;z&|1Fg?

      CAS01}t&Y*Ov9J^~Xhl^;yLUu=cL%-gpT#Mta z;q2;jLG%hlyW8Oo1~Lw z^v+xRUR6IimSk}GgRb?5w#YNF&sAg)f#6*gGlnW=RXfH2wru7FV6TUG^3no)_Sv)nOJA;rY@){Aw=m)&0$+RV;)(V1oEsl%(Y1UtjZWG-vpblaO3 z^rbhp_?E?*i7%g~UcTr$L5$^kYK(nb=7WztA6owX{Udl`bNEVq$DUgjz1JEq_NR%9 z)wAb55O) z7NmHIHwtmgj5o6hXj;&2-Mo^)E>cOQOqBXAx zSMhDKVX`-^&vSD(3^`I_W;QwH@kvpQ24)JbId+XnL@>43@edNsF5d5| z7j0OsDW+*%$8b;OigDyEt=I$8HL}{yZHrgA=@6NCK3L-R$1}>Gdk<;FZ;S}w4w=QD z6TejH=iK!tf2c~HzLfndWMgP_$+q9VOy5Jc`F}t8y8F<+yU80DoH&rB?Z_LPbgQBL zKho_@pw{dYcfLIaEDQ|OI2mwX7X%-UL~6-~MS~8u`1kF`CRN2|MrOt{cN-d6*o&S9 z3F$4IVA065bo%tZ158IGW^a62R*`J=Q}Hk7TOaTAUl$%)+ON87JIUaf^@9Twm{l2zp?skr~(ck_BZrhRnto)5|_NFuXYR+1%b3#we zzuD^Sl)f#(>WqwdaoVv&pJiS1lwF>^ZF;LCFzKbkl^c4Bdt%NMZdtY_c7t7r-?ufF z`gW>6=J(P!f93DDafW{NIdd1+g_0bKYaHE~73*^T<)nQ|duXtEV|mV7>%b!%3%}f0 z$H?kyB_;U0;oQVnwI!Rre=e5NeV24y=Vs7Uec!)F%lfYE2s7IKb))sF3xWw1-JIpV%k@AkF2 zjT0WTtP_`JpSQYk%5#%5eyQ@)z1W{kyrm=RzjjjbG0sf)>s7}mOw)PH^7u*Hvi5lo z1s86N5x0CiQ%-owmCSvIq&LpkSF0YTrFwZ|ck9xwxzCl&%fn{|#~%_n8hohv{G~fv zyvhvC)%hWBtbU-JGb|;6ZL^X8HL<&&4+*>N(+RvcCD@!{ z;rBeo9-ot^HisSkYqH_V(b#9syL6dzZ`b(ueswyirThGB(aofs|Fyqmj$VILzg5X( z_bg+*hgZ*5bu*rjcC9%6U-y$#k#y#Ra#q1Qx5}*y2kRT`>^_S3eB6KW5BqwSkUB4( z3vDd+w{1l>Z(({jJLU1`^WVzmzt*1bE&X5E?K|tP`t^&Y)-AlWDnx41i^7#v%q!Vm ztqfueX|}r>pm5d0_G$pb)fN6zU2ZLZRerreYo(rn;>;ConGN%s9bcTR+OnmeZQ=4) z7i<5_IVLRs_m=;u2`@Ui_v|@PQyXV%S}A#T50Bo7Ch2Uc&D*DDJQ7>=u0d#TlH#PS zk9vvPUvuVu=ZNMJ`6d~9eC{D9J-47e5BphJ?cG9zL&Ph&2-Y*tjd_qMzH+BXT+k}Tz; zPcON+JDFU$;~)C=$ol8|=eF-&k(PAawLHFOlFo8>@x6j-&(s!l{#$ZkRa9uIfV`p0 zzdwE*S3F{PJ}Zc>x@1|<+GK&)$E%DCt2*1CeAngs>>)ha^J2y3_{w?x zH8QEVDJ0(PDpx>=WM0hKz;D-^DNC3+kVRO+C%UwWtF8;6?{FlPs6FNNc9dC{P8 zxMR?9xbs!{@2c|7e);pcX08>_B(pimmp7l+ea@1&@KKd|Yw-6Q8b6lkmZa%!T`nr9 z^m5qE+hVVCr<60_Oy`KwZ*JPRTBYT#nd>g$%kL|$JaA;F=uil+{1Ku%Yt2;I<6peC z1+Jeh5<8i>Orn){*0p^hx7KkSXnL?E`J3Sx=KjzY^)L3PnCI9fPJJchvZ5y_TUg?6 zV1C}y@(V@v2TrWNlB5uP(v(4Xg22)M!BifHD;B+aEXOi+65a;Hdu*s<&Q1Nwlve*T z`L58Kk_E34RTWi_h0bV}HfavNtozFC^g^W-;u|jT$G$UMQL?~``&3|e_`)>(gq`wR zO&Cfi&nXUJ+oT&3wS_UIZ&yR8-yyk6OjEO)uWbK)wdT(`g|B<-zRFCUA3uBUl28A( z-ZT!WGxYeoBHq^3yz>!{q{z$V3J1#tC2zK;ZRX^X-fzUYN=M|F0e|{bPeGYio^!R_ z3|$1}n>GrpSZpPIDS5?89kJsE=k>Jsl4m@ZYVFO?5n|bO?wDHAjDSKJ)q_DdQkr}%Z5G^ms(D}+aWN=&b0OH@_?5=!fregn!Nkn^WaS- z!gdFAbM}UEPCNNh$2*^8^3s=2y=P2Z8Y;9V@%v-><9p6cVr{+aw^1)@+B>dT|6>!D zRxf&OZ|OSi9s3?XyGcvg7cHNABIV`84EB?O)2^-D+IVpHq=cWok+-zs4khylY0BOd zZngE@5ODJ&^GD+#+v!W+UQ(^;e58Bf=DE`r(%RZ@T$z-2&#TPY7aH+YTa2@4kJsOW zcdmB^)P2vM^`}ei^d;$EE0bQIy>Jt{HuJ7xy}8J@w|3nu{p@SD3tbXXU8Gy_18q6b zpNEp-+Dr@#iP5czk`_max?m$UxexwiJL+4G#$I)3s~ z_iEVnEPCLU=s&4!q4!In7e@2D-|&C*4+vo6dbGKIN=4o@r;CaY=T)D#y#KSP?(eU^ zZ`m7|?2ZzUOj3sW9|>@-uf4M&n9e?6-hWetH%hnqd9Y=)P>t%;PS>irCocjY{)ojR=mX|kQO25XU zGmVS+NGW}iQBd5-I&D!ciI#jYjQ1!Y*hTooK7VR=fQH)@inlzFhTYqQR7 zrT1bdCNA3Gv@1%^)AiC-t(%LUbll1P9}~Dze1+CZH@8!%-B(wg&@a8Q>DtG{e}(JJ zJCxe;ZzOHjDw*1ox}~7kEHyh`G}?OIjb~3qrft0Ur?V~fjnM-;-CXD1Z!sSqBsN;b zc!hVx^-X^C^7f-Z_XzGOsc(uGxO-?Y81oSNzl^~%f)42#$Z zEIkWAKKreAO0NIq0FmSWpXc4ZwXWIMY@*>j_IHdcA8gjpQdlo$>QIr*wK~EmEZ9o; zzV0#pNA4F~3=J=SxW%7pvt8F>W=H0wuixfg+ecH_}QM;dAersEO++WGJW2$yEo5+{fFPz&>|J0w$d?@9wjg|ZZr@K!ZbWayA zS>!P-sOoBt@k7U-?>nw;OaIw-J1WoG_n%kyRO9{rFY7-|mQab&`W}#TzEHaR(;sIm zy@d`Z{%`dYaF=pev(7)~Fh3i5db@e$-ZGifKFQa=`Cns8 z3!HpJo2As*>wdwF!#5v@qaBE% ztNJJEo=k0!a%HM~@}thMC+QS}fQ_fr$EnZvPWWn&Bze`}Agf)%?#Pk$;4K^VW)(Ua z8+5MhFcZ7@&fP?J(bolTy8>=;dO34mX3TmPe@JV2=HcfYvn_)o__JQfB!*s5x>B)$ zEu_=x$Q+eGaZe+S1>7_CeDysT<)r#?9cWMg%5`biL3{c$Rbw?cy-IgREmt{uBiE4U z$b$G29U*GIZF-k#MW!EiTVB_7`r_iwHHtzDYmJf~B+V+adg{p;F-zi1XUOz}A*CGo z*6*!9GL`mT?var54xae5=S7mq!Gvu))Q)v4Nos~p_1tq<#PzVLx|gw};n~T4D^6WF zBFMEW=vf2T-@F+%p~j2MrZ0<(jnNEJx^tn$QhSrF>|(9YbM`p|YCm&%IBQkR{;C#f zk*|i?dscQvyI%aB{=aQzxy9pkt90(!9kR8O6I=6PpW3vR7xt#IAx{;b+bk+rQS{14 zy!yb=L%pGOpJya$^ZaH-vwLc`oqHxV)oKSS@A8TaJKEiKE|66fL_`3Gb9vU3|F9{IC1mcN3o+ zarV9SF~ZJ0{rk=DZ*Fe>_xb(({|p7v-#M=3Eti+s(Qk8K^^E;@zPNW5jxQ|#`CWLq z%dsNpy{yGP<+i`y7E1q6kNe#bkXPEY_*mid3z;qVw6{-839~Ubucok=8ppSd zpKoRU@cX^uW6u8@wj9PM1h;sV>T}Bmi>W1g-}tcfy7HB%|K?q9a+X~cJZ!7}Vag$w zW$#LM|Km*De5p#j!kkAXR{G=Yx)(otPh7pbqR+qc!1R0bSM%rz8rhoPc~|1UX~yw6 zt*13s_3pUe?sa?rqSJvt0W}Ufv~_w8WPmuqry7GR>7Ylr>6OV)v_xGF#34Ih*g6HRs-c`8CFJ@6C-%c%&Yg z_{bz$S>1DUkqkQ`tK%HX^l)8GddSZcM=zwVQPw@*b7tFtPmF7B-CBR*THWC<=Z%sj zK82Vakm0&$yh+GQuEVWMP5StnHAmMR|K!&lv0q{Lwz{s?`R6uz39goj?%g1k+PdhX z(7KNHMbm^|r7gP9vts#iyCik+CzQWE*iLvU_k@U(xIKI-HO?3d(WGqALO_9=4BQjMj2H=50}n2WyrewsHOVW$oAa`a93x z=G*B%COlfLHv89!C;i-OOm8RV{8<^1EAi{E|G5pGfy*~kzCNh(+}`--XQrj)Oy4j3 zJ@=&j_MATJ2G#e;_w~7>{$31!^rzKA`d9ML%%DqwIsbmmma*NqLp3ri_UM!4{uZ*^ z#jY$aQnh>${V6{9FFR_UUBB%~;!YL@hI3pD4667>qXSZlGV@a)4Y8?VxuD}U>e428 zrcGy6Qqky85;9cGYKdCi^uuXG(?i#$i2J)$wAb)P`fs}O*xaw*eu@18`8CV8F8_Ps z@biBw=H0t_cS?z`S9Oum%-!dU=U1n{dv@;6$LIO`nP;>dT`+yl-Kv|rlEY7WthxF0 zZRqMe>#5wmn<7m)@2?GAQ}uOi?Cm584Xdwf|7b5ydKO!K{M7F&nQb?EVmIH7Y&+;v z?N!#SX!Sfq!z2IVy2#VJt!IbT-n|o&=su-e{C(?di>qz#D<^o%%xdIVBvn!pIBorz z{IfMz*W^A8H;a!kBJd2MyAJP21Z;|tlw#e&+ba@nMmI0KnkCVxa(k_H-(r>bR!b*LYjos!VbP{|Yq#&~rNPTP zf*vX}IVx^1IOViuy;=;?N+0Ol{lk{5THK*$KS?dD7?Ycs5XJt>Hy`@Cvxx^!H?N;`q zKTQr-{65t-Cxcgf!u0||UMWLd9r9}U3{oeEN$$oD^ZjroPo_P!3*zTF^KjcD zk&aKtHtoy&@xa;C^utpAdrNI|B2t%!DMqO8cp}aA?q`eA(L04%@5~-bJ@meJ=Rg<# z`bqmwo!H9sU-iC0z$ebvr|z!{d%JAEx%r0+?fVx!*|zdI%gMW;!OZ?OA2jzmhDkX z$^1fi=ln#UKTXQ^la4B8hR7(is`N73b%*?7%wKjzqBf}IXVh{V=?TL22Y+-texG?R zYKq=PiymRd-&F#Ur++Q|HAoIgP>HHIS zwcdWa+Ty=+25;8!cO^;sEjN=*^vaU-TW>Bl(JN27&%fC{Bj)vyI)Sv+(^D4<{|=l{ zJLlN(xi`)qdgCZHzdUJn;Z&t4hVv1+%6KesN#$U9}>7(QHYp_wNz|OGPh4>UXc|Zamg6Ve{;J3(I@?7ipez zm(KXG#iY*U5 zk;%Qp9@g*kRQ&tZe@O0XE6`+f|8A(_+Qt$2taP);>;*nWQufoIOl+N{eUz2|%kKpu z;9;BF2F7}q(4G;b~zi@WC&N=mwpkn_` z_3#ZM)lP!*v;NHNlCb&1jav3usZ0!QWM*K!RX(Yh?de&kzs^6H zD;PLi9x#49BU~?-RA!#rE6Z$EX;S=o=FOX5-`tU}|NBe2A^L{u4fe$Kw?EALncE*w z@zC5at|ITUZC15)-jlswtnVF*Je<4R=UdaV>$&rPA2dv?)PEy)-S6-*WjCG(8CBOksJ-`Ert$gGy9 z3T-=Br8S+tUHN-z?bf>wc7)kmeU%Cn%5+*2V_A}*ketjE8E0i^Y}_ikT_ zJMm=U?XOE8FS_8fT%5P4C1|J7mal*7mF8r9ynkM;GxAW12p_A}jKe-$;Q ztEYPQOy6nN6(M|fSN>VgdD0?1?Sa7u$?UIU$$WO#{#D;8leK^0I`7``eV1PAdRkR1 zSm@h!ZjYOUNvSTo*Yl+!ZyroMCinTI+=)7i4?7>Khh|^7(!KremR_M0alx`5=Bm;C z>qEOrnK`FfLbyCq(^#{9_ojeE@c*7tl`t+V>^59!4< zollQh{JW;H|H!9r7XKCp#?3o(xA2ep;y*U0e?R`wzW9&K>9vJ_CI{9{`(GOLSK-5y z*m|iSCW(_OF2Dbt*m)=Q@o}+@7DttrN^*Mfp3#{-N#yZw`NJhV-$KHcY|1RknK*C3 zjycU{sTz9A7pbbrPP|Y%NA$9B=Bb-g)SaJhI+EkJgw5)%$@34s|8LjrabT8jTr(ry zD|q7R$PGu6nuI@J*m$HUFs0qwtXFi;L9N&tp`u5xwyueIoT=lJyzYxN*RsI01y17M zjr>E`W!)9JUaa)Zro$)MdH2`5X1%jdg)BZ`lBex_j`#A8#j-^gW*m!6eQ_{F>iZV& zbJCZ0tnb;r_`HeP%{q_wYMJx4RaE!ueB7aN=KJo7Q&-G*&;PV}Yp^G~Gu?Hf*HEH-WbQk)4kmr}#!s@-LLJP7{(9I&EoDsC?XV zTLbe_#zzMnii0l*DPCT1blM`WFMb-I6lxp3Epp)Z)bEPK^{oOMEyr+`i(e6>s|e^7*As>YtjfMeTm~gHPLf zZJYG3`_*gJ5_gKVoIRCzp+IY5`I9iWxvN^POuPPRQSPCvhry2?$tnIc$td8MY`Xqw z-A&6|<%h2aIcs=5kK>7VcYLrt`PdAP&=+b#9A%j~GXESGA%~SC%h?!ha275 z<7Nk}n``N(cdkS?;psf#sv@OxA#2&)?^`I^Kjq$f@xkh&vWG6cYz-;===1cRVIkvm z9=*#&`gQ7)1m{LfTyR`)FQ3ni2%luma1Jj4*&T}EFHU@}ooRjji)Huhyf2H=4FjD5 zyH2xIZ>f50H2I74rg_;dTki72suwxm{`;+7>*KSW8|G@)3cZu^g-vdk-_A{&b>oHf zd#~g@(>l-Y`)KU9ez{5D+vsf^)#?wAhu{Ada&o)9)tgfv_y2lVb#|I?=@awoRlzw5 zDSZb+ZPa|`7KDEk(Vnn7;sNKpM*D?#{@l4?Wv}h=amAsUUfhtr;cNq+<{9w)E+;IXFMuVHq-Rd;RzeQ@9_IE^L|E$ z>FLF`Cmz@BsaO?K*M2I~a|zq)Wo({nK7X`qD}KIbPVdJ#)z6>i^gX|0ufN=C-scB3 zb6otD|H^;+xBX*>{n2nPvtX&t?RWFUcb(|9mgb%45fo%LQE`gM(ssU(<`A!f`Kl9q z56#$@(z#`-;$?xEoL;BynPg0wv?;jMFtsaWzwa8CvNSzs%jQo@zGUp0ahB6GPtW^I z{Fw_(`qc*ox;4K&SG#ufQqu-E=glrc8x6Xjkx`9>e*zbn7{q@~m@1=FWo_a12- zEl==^(T-Yl;ClasDWSiapP4K@)AU+IeqrUVgI`QzLyD9lxm6yhoxi`RKitXh&c4%K z*1K+;=se=5eQ8Sd>oW`gMgBn=76y$WybUrIn#01tu!)Pn7y_(LM%gVoby~E)w7bah z|LJeQm%nT{dz8_sWAYEDf|E-<8zwMtavp4C;mCdAymiJ@L!*1w;*^&9uQbYtT(4 zo;B0BtXEIlnVUCt?NP6@hT7*U)+S8z6}z)OC;IBA)j>-=ET(T?ee}+{y}8fkT$(pE zJJQ&d!)LmdYI>W&EGtK|y^bHw-@d(Ozjpkph||BTqr*yLy99!Y)@C2v-j_Q)TW-tFH1fhPg$m-(6*ybqv~c}`tlIvluz@s)!dh<=Wl+{7O8u(@cj9N zgZF-)TT`Sz@6`5Nc2nb2cxsKh9C>Q}R*3V?IchmK+2oN_zSX69X0e@aqpb5V+!YH^+NwMW-}SWOL`LL zEVyy5FErJC&xiH5+2p$&U!Dl%yTD)8TAs2)m+8Cj@u=tL)Xtwy5Y=@ruA*U@miQ^7u-5`RqV#oWgkToH~5~dNuD;fWoCg%!UMT? zUZ->vZ_ZjgSIFa+ss83~c8XJU!gR|P&f2o?|NNHQe{9|?Yh<;V6mc%VG;&*(X_{2T zD*k6r7c4)0ps2Q>_3%3{sfsZ9NL8EscKPdewTJedlR35ip*7v76+qWCu zoFIPoQ#r@}EhqR3tPi`DJvqPi^Y0yVGjc93c^;!Mxwf5s{;kChhu1qK_ltelApHDe zrlt2>OMk!9k?+n_Rz;oLd~VLx%Y`-TdSd?H`5S+nE&r%JR~WNa=XG{=_6sgs4*y-n zJ%9e6O-?VII0II^^t-0%VW+2L=koVL^*L7;4IwpW@rTAwrnkB;*ZXwZXh~F0WzE6al?02b$q&BIppHwb;^rh78!dPMRq(k$!FI_d;roQ{mYCXU0GYwaEu52-w z_1+?MXY+MsVB^}B`g-d?6A7~oUn?M!M--$9Sg+wAWunvX3*Q_u?2r;VqjR$ zN@U-|Gp{7Is0?;5O>aoFzi^XWRx-9s-MN>;wPrgPtN>s_nFMUQ@d z{nQw4e7dnVW2(ONTEV2(+p^iETVDRzf1owispRtNLXN$nx3|X0$iIvH*LOsK>yky! z!_Wtn=K{MIdoL->U{mVIx-d~;3Evsv&2MJp?fp~q{=h1>LKV)5HHp_7n&M<_JvQD? zxNoO4MeG0Dw&K_8D|+sj)<$bZH6G*3Jw7q!%_GUgH}5@<8y+c_s_5^lHQr!5J>Ncl zM@eAr-C2!^DZ)G21P>*Y`^YS2S)0dt{;&p5PjbL<2{n$aw|Cl$L}t`oxH?@j!g|kx zHz8>^r&%y>o-r}+xrOHGwS|3as@`1V5t-Gc^gQ71ciZd5dwyvumbaFFjyZkC(`>(m z!G;djlR;_oZ-08VJ@?((eH*x*epnE@L)6Dftf!#)e_dqiy9W~Mrv@CW+|GDVeu236 z`~;Ex3*IxoXSpIKbVA{5(T2{XX^aXno@S5q5ARaDu9LjPb<+>QW8qIY<{SR(<6qI7 z*Ys3mlkVh|6P$MC&)&1YMLyZGcjmhvufsno>wK<1G=1Hp_Q-R|3$5#3YSpcnx@6C` z)UdsQ9}JdFvdA`(y5O=a`txfgp`|O3x5FcaBU zfX+KW1~GeseETm2h}eGL+WIZ9?UUu90}md!Hv~#wpLNscQP&E|GdaGEY+hEmL0u=` zbj*~0xg;^>_u`)Itg=mRzL&3;|2}8^{Z93}?eFLBXQ=2fbL2amYEm}8VxRYl z`IV1v&V21NXX1Oi@d+OrbZ+rK2|kGs$4@Cb8- zFbi2WnU=>{ZhOxX{QG9cyS~RdpIDaOwl-h5P+=o`_vK^8`HnC6|Cw`(he@^Y*pw^j z|J*-O^Y~^B9w+-baYD21-t#-Te81n!1z)e1rEK&+;eYqXuf9&JEXz9%%?tR2r#koB zYAkDd`{np~sb~wQAnAt_?gw2GFpK&S=iGK($6Z~9``@X5AK7R8?bq1nZD_TglWoPK zL)p2#Nz;rbyc05ewER?h_BuZ)^_cgGb%z)q>bh?^8tT1S`RvUni?_A?mYjR!YSz{! z#Z&ben?$<2vM$MJZsB>lt!LsC)z;t37p!rZrZaK#rxdS=_mxczqk|f+9I0{^@tqjC z{ix8>sCinGRxMc?({^Kh$DOdpC42j>uQu1->KL7z*V$Y)ZPw1!{eWwGhVd8C!r$vu-+eg38M|7(*46p?E}j<`ugwWrd7iV6w{X4YldU)H84xK9 z)EX^j;$jkEVqloU%)p?FZ*&~i8bux@?+x%^<$HB<&)KPHRCm zRVeIQkhLmi;^c^xi%r(eVOhDI<7@i|VFxBf3x#ukG>)th&hT2w6?yOO&C1_t&(1=M z=jAt)Pvq_BVEcDE;gRsR{^o-(H@@fH`|pVTr^N~~b~AbRbROSvwB&{RqGtu5(IW|GdZ{cKzfj&o};l@@P(P)aI}Ld)@2z`ULFu?N&L}^5w+* zc-xJEj+(8xA)IO9PjB!>{L7iMPCEY2$_;&NA*pv{{uO)iXAQj zot3Vp^R8FBRX1((SQAp{vTw?=rX@;}hSOC~u}-{o;jD>v#rB&0S5|K^e9(8}rgw1f z<(1wWXD1}(%~XAN<;bWkR$;(dCwLyZ3fgzR^PXQKOl30?N3>tcbT~nX}DZr*qzUU|HDAM*{ zbIQ`!VGbgUtRm4j#a=|Y2Ci7J@s+Q(cxX>}+AY6R*WTuBk`1}%^_bO7SpGn}(Y%|H z`aHtH<&!OE*6%C+H}mFZ`}*47j0?Js^OR{@^;tifDbLaNbJ~+j@2~6>@!c=|Yp-&o zWLd=l$xXVlYo9s(4Sk&1xbRNLqj}SH_D>BI;jw+m-%`HKC2@MGz*)=BgpEr-?)B8P6l}{2}S6{yryI1Vd+~!c- zdkSA%c$RkT{&U1JaJBi%$J$%gnS^Jndhs7U{v?3!+SVP8FTH%`YDIkw3b78|S~1&2 zl6R3spPj{xOOIlWf>M&Utlj$iiuuB%vM+P;GIEdn-uPicq8j7gC7u6Ej@rF-GIe9C zu3%|ny(<4D{>j9{EIL7InP%RBw%5vUEI(22rzUx|Q8Qr4iU~D8MU0>7&(N(u+H^?b z++~Z2I_EC#=+MqQ6*Oy-YRS*ZjIoA`uKT`NC8#^&!Xm*~ zaTQ@$(ir%LGDt#OI^|`Mu%p27|ICeNH*zUuJ>g(sQi%!N!|H!1lSxEFZ2lV6nM%zk z^|s#Z*faGPBlp@*j5Qjk=lOC4pA^{a8e|=8efRI{d3N8v&zIlNaObF)bHQ<~+mZWp zxaKJCc`VU)Vi`|+y308c_R92aQ%t$ooYQ?~9O^XmIevLs?Sztkdy8qI8mh8^PfogB z{&_^NIsf7*!JXQ=KKlao-WC5|yHUTk^vIk&8ip!S#|*zL(Y7X1E__mn9sZoRkvTPYiUQS^Xl ziLuWXi6w1vZDp6O(mIaJW?QpZ+}vqL-)tUbuU}S@^V%0Hp1H7Bchk=(uERllW=`hT z3hiH*@auy7p{DvF)u>m2X+7HAy(nNnEq^cjk;Kj{`)~X5K1aXqS5X&WV%9)_nROye;z#i(zYI zTSLy{o>Z&KS-p$yl+Fq;-?B4n^8V+4l<&TnVJ6G3wW&R&@{l**_TpolrQB<+=7psA zYi_)xr>a^vxqe-rr~8`~0eUYh;tKyY2?RxyzS9be`6;_PxQ ze!;ORS6aP;+^1eizqjh^rg>7KLGhKV!S2?XyT3haI@Nvt-NiXKjTb+@Utjl+slj?fV?z1j^M(F0tjdiWZu_*5m?yGal zr|!-(aJUgTEok%m$r@6<#_7-5Pc?Tx;QOn;&-~Nj#_z8V2YgY9c8l<0ZzMHq_Ly z%`Re-%5s=Jqx0OU8)nZ8)x#$;PtRH2k@AV@?6S+oT9@jrLV1>NDLlAzYnEW_>}#hp zR;;}d;pqJK#G+Z=Y1?>iZkSj0+wd>n?5Qz(Ra(XM#WyBCR!v#S*}YBb$(o!Ao3+16 z#<*^lY;nA_=8@^joq2(q9gj($G}sqA{oAq)&VNc}YK01o3Nx>32fhvyyj^rj+U%~$ zQt6py-QR9SM10M^_$@bj)113g&);zt?UuS%zVhpVMeVu2me1Jio_W#6b&jpy`|gIq zZSw^`e<&=QAapvV=vztTqNLL|H?LXgXOiANGi74gxjv(lf5IQUsSMln!{4HMqt3}) za@NK#7VKPqtt`M5`EF}ai}K{!$mlF428M%d3=CR?i(}Bzd1!UtJ2}_?ii1e&c^&=K z>y8crtga$rcVee>Ez;0$+NAJ;>6Owk|79iW5iz%xM2GyFdW>Jf_YdPgjmL9mIwkE< zyRA0&%**0u^D^K6`Stg&dV^*NYthvg2H&n`1%xxzUOnQ?wEvB2mbFE|WZwI$tC?~` zt*xY^pN3DJy5~TJYS;Ie&%bhoT)(!>7I^odF@D`sePdTQ@p853G3a!qCH^ojYG zg%1(R&U2|_lYQIwRTGXu3x42)3J8Vf=kxAwzy~wwLueL=_ zUmLM3U|GPd(yOQZgMO;K-Zgc8(Db%lK1a`)v$)OvVa>OVrHbdtti?r_n)P2Ozc4%V zz%tG$!J%Av?w+`mi+t@<_>6@wO#jThGr6SBaB|PP?;;Y51C{44P5v@v-=ghbc$uYy zExgZ{ecm*GUh|GAQEfrq(hI}wj9(aZ$34CvD0a5^g+zDU^9xMf(=1AwT=R=wsC3_Z zbU{;W@8N})b*lQE(<8nxwx8Sd!lqqs!waQ$y^SxD`C`*9it@##UNq&4O~05f6P0-J zZ2iR^N#m03FAjf1S|J2#K$uSxR7qfFU})pUGg5^pCc!BdrSb4P=(2;zvHeG#ZV9;x z3hFmFG){@$d*8uv4Of(2ke-rOTM2tp?x7_sPere+f3U#Vu7Uq)g;{6BZC&SMWs}Z6 zoAdd0vERPC^Y`!j%TT~NM{(X(@4kcbvnyw9Nr}B{+Pmf5C)dpf+`HQ5U6+&+T9&$t zlg;<^jdN+Is`tLTRkm+N(MkQU`8P6(W-5NVa87WeW^z=`wvZbS&Mn%fcx{FMj_9t>?%6g^S!MZxnGa{A)+bo3zv>g>%6-n{kx7)~ zuBBI&i%&iYRD+8ZCoN}NtCgX@@%o4Q|)d@ROUzZ15XKA<> z)L>S7Lh6ar5APRQF6z4^_2izdY}n_Na{c$O+|a+Xp7&)w@3_|f&!TGKRj+M(%vhP7 zp0QrmnrMohT0SAQilp;nk5oDI6KeXn6}$>mVQUzQrYKAOH2hma6Ny& zViLc}iX9Jc=LJ2{)8BRHleVwMA^`}&sbVCUHMS*<=ObL#bF4^)?0s`2gr{Y7)jlTBUW6|%>r?gg|({x9*7 zOKex?Sv6-H%g*_CuHApVa!+3FvS()7b~+ZBDaGD!;CpnvN1{)W>Cxq!33_Vl&X=rq zX1*)EM?Cil+lqaHT#HPO3AiT3Kp^oIU#Ru^8bfOj^kCc?Go*kv!?vg zU@7%-dOJO6xpRQAP}ck@R>FSkKTo~VGUb=eoVe91Y=d4oS?mo9+`aTljzwx{;O-?? zj{aW~Hz{cPp_ipgC*9Iqx{qV|b*-%SDZ8Zn)`wmx)7<*Bp=#=$R^4B+*{kC>#!1#M z)UlH~@>{U`k42>*!uBJrItdRChY;df<6)xw?ek=2vyi2lgL}dyIE)j{UGS z{o9i}#r9S2=kMQN$9zC`hI4`SN6$;5vlIOlOstJ2B$g{rNHVt(TKzZg0=Gbg{)^ML zf+`%Fy-(jREP5b+?02L7)MMw5`U}Xctrm3uw7IiV{fM~h{H}c>C+iBj0wMInU^N(+3w%O-v`j6+fJ-zo! z`MaL0RY!SA|J)A@iE-xBw*3h_R=9rt*YZM@)EQ16_?p)@-sfJ#{Qv55=ZjNQXSl6c zzOe4q^vj#goVe2%7OrKB+a40OfkDKmdE3EFj(HahUr4QykTH%hjWD*zyI>q)Zjrbl zal=YMkI*IcoNA&n#yDDu#OReuH*0#@ znh#2bzOeG8Hv`f5B!^r_rG?fwA`rUM^d^DzN(Pg;!jk_LQ}Y-I!ymt@Pzm@wSOV zM_miHMS6VMS+H%Ys$O5`=I5+auWy>$`gHk;o2S=^6)&ChHf5%p_?erpl~lCT%qREc z&ELGeRYm(*Z7N4~#`>4`>3ciRAzhOLs%H+>nkcEbp8!9bQM$3`Wu$_q|k=L$P%Bwjk^8Jzwn%wk`|MM?Vx z`BR1O^|n~t>khj;)8@^;Z}rvHv;Y2leVd=bxZ=Ekeue4%P1j|wr%C9sq&w&`FJ? z(0ujOw||AAvcfygPxcRDXFKjUHEOq(@%!MXBBiY%cdH}kN&gJK_(k?!dE)1zr@t)> z);`T-H)ZLp#2s2^dvBfdy8gJTMd#fd6Z@t9$G&Q*p49Vrtkit2mv`ct5*zMJ&5b&} z=TBCI6vuk5^LVV~S&;0kq%QkfQCHP$$wyK1Yv;{YTih1Awf#%|^V?3&`AOaB`+b+H zW`%!S(HEOp=bWtbCb});vS#LuYvokgS~ ztzNF3|L)K7TK7LZvGGT|TlzN&%vfOUzUxx@3*UOz`d>26GK=pm%nq?&e`(b-JMn~v z#3jcUZ-ZH_R=K=iaiLIkR=`5x9pZ~Dw_KcWHg(<{EB99+IWLWJE|mHxd33M(u-wy! zbAKEAjAa~d_dBz`y05yLKJ|sLsMT_p=b zX7*VHKe@`OI>jLKvC2y0I_=uznh(rB7RtPT)+Tnnv@Of}K#`QBtN`BfIazkOF1ru=5VHgQi}zMV}eQ)7^LBhzBh{yFW_ z-k+Fap(Q-0s!pSp>HY3m2~TW4_vxC=b=mZDxAvCWT?OyB&YAEuK0P!wnDPc?o$IdxaqX1~(=nb(q+ zidwCc{CZ>AyFZCSH#GhzXC*LAF8UO_LJ1A#;{NMlA)_3pSvo`Xup4nS5xzji87haq6TK2s&vwvir z+JwIEYPuy;@5?`%`d%l#c&&oR67yN&%Kedjn|+2`NnbW6Xy+#UN{Psi2e#+ z_FN&4iR5ji0wXT)Vpb#&G8T7t-7&ElXJ^lj>m=HOx3ks zmV7*P+lo~60^_vrC+;fWT{^YcXLX6FKp}&DGNKDmuQ`4p6PSpMzq!OKCbgFA1tJA&0cGMFGsRU`$f{c)rObrJ8$+T z2~AXYyl<-~Tm9|LyZ8I;>zEG6_$X$W)^86kpB8)GQR5nub;0ZYHwPliqt9)+pLz3O&i1@& zv!+{rpOiD5R6a9BTf1&~=Gz~)w~2C`%4&&vy(05L$tsSjj2pi%7pr{dxOslzD%~$z zAFS?4T)1aba{Wf#^LgfLqb?NQKAUyq4cof(wLg@kmPc7?3YBMC-hP*-ZKW!l`|EgC zEmLE4-EPT`%0)M)f4mo_BfWc`c=wYchby;FJxzSk?d<@s zR2W${NlcXpKHwbkc&}Kp)v<{ZUnPtU(~?b+&K$Mi`YGVs>7c~)Q&mFvL%XB#(FuGW zuEjTWO}$hMH773+3^uTxAjKOzL*S)XgJV>bd(aP#x9{J|PVjTCeLDH&nIB6WBZJ+Y z7vIrPOMPZ?COPO?;8MfpDH@4~QWd{^uYE7=^F+9{$3SP^rHxA-ifl6And&!P!*)XI zxpN}l9_naa&b!UK%-2Re=iF+O*hNp;5-fIW{EI)-c-(xYO?tr5ONDNWPlpF&epy!= zx;WC6MQxSm(rn`?)`4zwV)uXGer728eX65*UiylP;HA;3A=j+VZMz(h`H)XDur{md z`5G;!h$Dx+#2D{9&Uv}6t=~eoJF08y_2A%m6fvbhjaW-f7r~< z^_gsX*CX!9o2QolpMQ@zH}CWH`}_ZMK9I^{IyfbAbJ49;c^ORNLEExk@0^j9ow;|7 zt(o?%9Mk1es~qB2*?GN|-sqF0sFI{>1 zTwhw0XEW_Roi@*=?QfQcgusjV0+W$s7T6L3Or`Z2|_Ot^oJ5-H+M}GP6 z)@J$Fb@leLQ&W5rx&-wCSH6+m>oxOUZbDDl4DAA|#7Vz>uU@j;nb+2?(YGY(+P~hX zo7{EIiuu`mzppw^%vADnM}bdQLyYAiqslWWQR+wJFLPy`-EeY`=-#CbOxgcu?_MjO zW_bL<^%Vl=Ur26s*|<~n*x`wNS0mmo`fSB_i+jyP=VwpYL4TDIBJ zZlxOnU-=yPOhVhzpucyvhd-3zi zf<-CUKABXPk>t4($=i_G%wbJ=m zsldQ(R@E`3kF}&F>XAT(&Dk3!e2ENyCphd@y*qc|#QIC}9QzKPe16WifnR^!!FH2P zCq18vx=j(cSTB<>=d4Ejm;3Mc$SnNN>9Z?1Z?Z$feNAKb9e?f!=4|`h@$dXq-4Bh2 zPlPL8ziM(Q&dRv1blrnhi;cTiOg~q;-%37sTf}oVyTa0AcSF7ea^=0NUh+U}XO{Wj zJEuzo<11#|`)CyFcINPxb<~UzC@{#cVms$?jZ{N$f|I77u(ZzWWR)(+IR99Mb!rJO^ zualip)wW$L|Fk*1_qW}zuqa3Mp2<5m5v|+jyr+mfzj$6h@Q3Jb{`7m7ax?3nz0JNP zc|>Mwbao>1xf4_7$p1kbeyG1wzc-1Qf#EARQIn0Ju|jaM1FnuCmGaVQH-n@cMcV$m ziA?W|zGHqsH#x>YP0cVZxU=igqz1KDu31L}k4w!E;febZ;<;``%oD~>0W0Uo2`rR6 z&>3*%+VlUv-d|q6{O9NI*Yg?t3l3e7F*I5^W7oQuTF&uDLVR?^LVQeDsa{D|UU_b5 za@RTolb&qPvoh&3CLVUvlG`9xZ_4ZJ&+gO3JH>nYaz4pB*~cU&-aZr+boZ=^-_?*1 z%cr$h`)7+s%}RWA&Z6zy2FJp7FR#ZvHY-%u&ibwS*vmRNm`A#Ndpc{JRJxbmp*?(e z5_Fz3FFkuSh+m~maHp1olX2ASj|{myz4y8Pv90dV@HLqhtGe)BU;1ym$C_@D$x>TC z>aciLbmYC;-R|f$t>WV~fnd{#{})uPODUP0_<8@@>7f(Dzv#MnNE{F_xAhS%_dPGs zwn}qF)}y1Lo8K*1vD)v!hM2XnP4dZi)YdNEw!`9T^mil0GcEVed3g4;c%Ha(M&xYt zo{W1kI*J?l6;CRss(CS~_#{uWo^g6<@>8#iCHq!5J60L)DN1@Av@?iFGWasH;MFDb ztW!SDY^l`waBkkEC-bFON*f;TY_{5)de4-ddsFhn7S}xw!s3leZm!gfHEG)Wxi~7% zde*wn-&ZtW6eo3YZs<}d|K4aellxus!O1%|UW&>JbkZ=NqRX{xS-^ycCo*kccp3Sv)zp<-AIkfA zlXGW_LD|lOwrtbOj(tC1#Nd3_vBcMjWuIDC&!qsRow3#5`j{pAUwT=+J(%A1E&9*B zmrJFLZ7qs^e`G!WV1Ag%LcY8J`4V$q`(yR?FCI8C%Sx|d-z&oAvD2;Mg2!{UTY}M_ z4yju0ei36o*LAnXye&s;cQ)*q5nsAs@~zn!&962c{L(d}lWT_Em*^RcJhu|uT+T$W z_K4eYKR%uzXY8K7t{~m;W9)(+G1Vin9w$`fre@1`N9|u*CT0BeR>O*~H?y9M-Fn*TP7E2S&uJoCZ^lC?5fkt=f zMB|jgnw=d?%NeD`+T(LCP3>OfwC&h7*9Q))$Uh8&p=ld$YLm3yh*KVJ( zy*B;s-D0_`+2_uto&1*5QgQj?b*Znl+uxW7sBPkZ-J6xSJ#JgEop$j*$9EN9&%IGQ z#Be(NjqP)hnfn+zwVci*=)4bkGwr$Zo=g5Nzqy`%%-XW@_jGsMx)(pzZ2DEqUEDqAT6f;o zwBKvl?7J$K-m_TOU0^MJaeh>Rr+!S*)}Tc1>9OaFXE!cx^`0?N_!;AQ*W#ONx7M_u zJel**$fQ)vRbY(=aL)IR7 zH`4ZHiOx`SSTI{XahXel%zkKbKGap)DfwSs9b7_nKm@+XpS4v*XnZPcK;iLy_%!vtR!yRRaV)2X=|ir-J=}l-}$lJH$DG9SJ}pMYrgX*{*qryT{~B}bup*a zAAa#S@1gLsy6Z3d#ZC$*u3=vDM!{{L%rv9Bir-$G*Gq|9z{b@0H-3r3(vL-J7MCpS z?2Ib;AK}ig!Mu3>-iD6PL5q}ke_(mqzx-9moU6<$bXhL%%UJkN9<@gy{%%&^dqxHZ zUlzg>*U*i1Dv&~=chX(ZQi;~{OYCn;wEq*o#nh^nn=78hRldc^gQaEFl(?2>V!1i1 zOP}1B^j6_xn1WtZ;}PZ$0UbBD1~-bRq>HY%t$t>`dH3hn-@ngi;QKfyp=5I4E7ttO z8VYMlif6XRRZsaePFU2-n*k;c!zqf^B_gs~|1z$fMxA9zb=Ir6b2mfZ@ zvr;-ETOBCT{?q%=l+^DWm)8r=pBokWiS<>>&E&cHkHlISnbi{a2_HN7`{D-qCGsip zqWcUc%wHmX^2Z_D*ZfTS;xo)QFkC)jlJ;uOHkS>nE2<`*F=`Yw%hOE0wfCN@@yp?rQrV}&VZO5^{Ho8teuIsT)u;a0$rsg0 zZd!ce*u4^`h*v5+?@yY9q=(7f@twIm(dJb18JEoqKI}WXp+itA`KooE*tIvJQr*#y zgIqbDMO>UVKXOAnhg`9y9XrQauGzu`oZV4RUiC@GKUL%}oo#A!)L7u!)ulzu=NFx; zKRD6yU(~b6zeo5b!ZT{F%9~y_Tbj_Gu9|5P9#OI{@#fBByCwb=1PPeVnn83)u(96t#`yOX#^#V&aNUg!%i5IR;>*`e zw9l?NV-(OdO>gn+1swvbxewo#D4#ABS)NeNQ?+_mw_a6)oR-z*)d7nh=oxPYU6Ukv zuI7JD9ixS7v*PWkiw~5oZ+&i*Qz9CEXrC187LWfbTXr6oyu|yE+qGcx&4~vmi?+&7 zof4O!X;`xMf9mtKa?Y=UtWwUDx4ggC+g58{@t}6^@gQg0?gp8cqh}o?CA+@o1KqHSx>bvrp;+-OpP+WGHGhzL&=MtYggAU_+Sc8l{?q(;f$!+xfH^ zIw|WcQhHf-Oj$2-*WB{Oi7f%AyEWdrE{>BZ-@IkI@)k3@9J$oRbp|J@0s~Zz7aFL) z)q7yC_HsvuDZQPD0 z!D}Bbe1GAVrHIwk=28)>#lFvLjnWe%)Z7Nt<+jP}Y1(9xaCxiP zPVRb#ldsQe-f`GGcZui4ya&F|*4)05H}S_AVb%Q4->r9)XzZ5sN-ArwmQKCH%)r1Xz`$UHe?ZDDzo6l&ii~{D+}V!$199 z+GN+`XN5kwKYQ~o|DEyf^0aq*s{g-_W(fbV+G*2^H@i}^FRqK?sy%JJ-*R2}`KzbX zu7#n?6D<0QV!91n5U-ZqauLt=neVv0QF+ble%>rGLWeW=;on;wZm z!Ja+}%L``5^iEiPaNRNWBi^N_7nf*A_6liAUVG2=(&KNM>Bc1*reiH8=RN_*=#C*f_rh?w8BRjy;$wUHa4B<~I%(Za#kJ@LJ^ohu%Z(z6YA$SNfdEOkTr#Q^wfn z;)8h)leE~R?HC@fk>wG5nPFyQy~Xjc^5GQeFU^{Y;{5r&tZ$t-YkMRXwy-yJ*3LQ1 znWHIly&&%V*@F!UmzSJ1b`p2_;@kntOW6z}2CHlYF z9^c$Q@#Mc|n`9G}%k}+r7B2s%zqGBHE*Q0-dw!~V+~ZY8>RPjfs-2VaA7yvWopwXA zMtkFCp=#-Jl`_Ai=NlC^dJFxQou>M2-x0A5N*h-T?QUxqik@<#)MnZZb)nz3brWu+ z3*GL2&ik!KS$Egw^#x!38D$UjGgUb|v45Q0;OG6qa|%mIm{a?Q%?;-~Ul{l>#!k9m zd_&~IZ=ou_RL0n;7X(9CwrDIAPvm?d%(=HEp5@EU;~CSp?fKchP?z;ck^s3tpyFUZXP{lo%K+6$|*gQ11-r1 zZW!EnzlwjYY3&R4FN>qz7PCpjt~TvC(4J_V{l5JD^EYQLpP#=Uzn{@((V+`7uV}l4 z^=7k9U0A)hVu^H`Wz;z>*4Bm2*SuzF8vC#a@0!@(c|9}uI``8VD@CtHTnh2gmA>Aa z-!(C~H|s}^#?cGgmOS^^q`q+Km9t)ki=}pZwW)LN($Ljjwrk;2uc&_qru_Y2{KI_P zwC_UNAD(|LPm&=&fG1QY!Oj3S(yE z-%|pGv70Zmwg)kOS+Jd3ykMeZp1}6g+@Br`9rjo(awv4oaZ%3cg;Nt3X4BR?(ceR^rRj=^Tc) zG9t3qG37fs@l5vR&kj9(dBe5OMQeV~coxBbXX7>RDbFG_1+E=tizp3m{2Fmc*(OtL z^|Gg|r%#^VIPa0<@v3tMZ-gdacxjt&T6osDXV**XeM~ulhnS{Gh|OkN-F-79F?^Ho zOv_tbk7RPEiXHI|D19BXy!c?SQ2PHG&4;W)JRh$K%=I;S#e4atr}@UCf!l3MHg^3y z}oJW?X(3-=MC=5OwdggX1&{) z6=^xC&AIPO>%v$w{w+qblIgpIjJ|ZvTQ1`K`1^rZsT*uF@0+NfU!a=K?KSgV7R!a| z6*Afzn}SXq-)7XFb?wXIJ7x^bYCqa97R_~*uHAll{rnFTS)UxT7uay&IkV(-oiE&v z=dnggC~^xX`b5qUJhrCQGEpZwLiku)t8C&Doj2Udxr*97%-pQ<`-0`4A@AHh%w=;m7O9lXsQ6WIWZ^$XL=z7* zku>qbv)>*p3=F?{7#Ivm%Zrev-qPuJi!O%=9527Vt)x#z+b`*;LjzmDWQ%yVpj$yv z3L;LI4++e=BxW@?#6oJZ$;OT~4w=g@SEz}{d=sqbi~F)@x{b)?;y#_*Uct8?->Eph z`tJSu^RH@NzTLh5A6tX>2gRn1@7}YDmLI)cCRnuE`utyoHTu`I&uwd7u-?+~h2?FI z%f?Hq>jHFFvr8|0pMU-I!tIZzU3XUhqIK5dw^Y-%XKeHKcN$z(c5OY$Q+Hlm@qULp z$I^G6YdvO85B{HLy-@n8-j|8e?%PY3F1L@B)ckxSOShz1e3P#6-OG<0rj}Og3)t|U7hsqOBZCh2XvwX@%5Z=1Hy zb%)`MS+n*|yiroV@tMu^sO@Z*vV4DeJMIM_nl`SJ-2(b@R*8H=GlnIhCJ1BBxaPUe4S6 zmbcQ=qWDuE-xc@BsV~dxms3xB`@puZd_!IJoqHdb=tWPT_&QEBah><0he9PgIwssN zinDl{yF5(SzGBzYFeh0({4C9^_&9hIzHFi^iF6pY1klnVanA;&C>@1 z%($ITJv$ImCh_T1-+>jIk^=&+H5I0I7dYLVT;O$+(_-x#NsHBQsts1Yv9wtKhIPZ# zTKQ#D=C(_--LCCxluOAU3cGXmTW z=eWo7UNKayn%iU}c~fT6v9=cp_5V$uzNpVge!0f7&dC38tIRWADaoT-a&|W+&zPp) zIC-kN?yGGLVxk|<|7Dva;VtSrvEr2vpE{3CuVg64^3B3i-d4Ek7)RPP6tB1$cb5At z(>!rYaf_S6Q|eS~eCBKY+g#6Zd4@%j?i|CFB{!Gc^j13?Ym&Kd&SBPOG0Cj?Z2RQn zp06mr$e?%YU}k5*m;J&MGsC;`zg;$-@%oFO(O+fNpN8AsF^R>A7R@i%<(8oqE_m5) z@`TkVlKUC%1T@U9cwv?FaMOW1kG@zYK3t*OkXf#hBiTN=u5r)m3GoS!>QWxAVyHX$ za`h30$H#qk>ld!R$dJ6g!8TxnX-@1#of-{?_~NL1{tZ7nGW%7YER;yNtjus<^T78T zyCkE2L@>-N__dI=L*DSMJ7-0Y%{Pt%ZA^b!-fFLG?NhLM8los;S8~Vj>{Jf+-f7+j ztGYh&OunAPZF}(Mqc6q}ZwR)VUM^EP?QHx|%qIThyyV|ic@Nhmu+DjPa+cD$c~9P$ zWoH@1X&uwQ@ke9Bhp7yIPQ5fgwD8<(!OL~iZ>|sM|L1S?t=%h^;h4OlnCS(dsZvLR zdqooU3a2NpnK-e9O(}as*R@{Jzc4r>B^M_e?Sz-k`1!$iuI_ZqDZ#%>|1kEGItKW0|-{#`0gl z)zTVw2j_`y*&7Wu&zkScDRH8G>fJf}_6WBwj6dsoukp;^*m+Xo7fc@hihJPN&vIp{ zaW>Pd<%f;Rysif2JH|LXZdteeAx{LSM}zJH)%UDLFF!0$(bo3!WX-=@-Zy8?dzRfh zoigKhO=Q*O%wWCUR+Z6_eAAL~W>nR#eZgy^E;RNAuQI%sV-0jva6eOmv6=K2bUM{J)G3APik5(Ybs-I z)Tx)YHM=bDJ-i+GTbWI$%+X=`Ewvw>9!@e+2c0gpq`$RTHUF!ukotu`k@JsSaWiqU z@AJw$H_svZ8~2v<{Z7*_vCKJ|ym;S@&f_XEMyhw0En&Q>JzrvLd+7SYEdABXEVj-M zU0<9vD|GW5s}rltElWE>^9x@YtiJc~ieT8@<0}`h`PC47JfgHISbozhmF0dLUkUro zO}iTDH#hlerC)5q)#r0&rC(L`)7|jO+b=fls;FOV>eW)ey9rmr=j=MVD&L}XcIf@0 zRfku-+n?prZ}{i>O8t}hhzt=i^Rm_Zx29hdSr{1X_z8})!7~JMTX|}3xU{Rt@&C*x z%o4Y5y%zL>nU#t6(xf2y+fmw+E-iCf*%EY7E6xBjt&v*Oa7n^b=&-3@Ae%(FPN z_{c)VKQ4Uv4|Cfd@<_5(N&f0)e}2DMSigUt+#DzS!=AN0jmBFRT0bn}SD(c7y*9u3 zcT)R==LQpwN6tx?xU{We`liRM#s)5zq6*#|3(*zWm)gymclD`T_#KCcbu$VpHt%y! zuZXg_aB$U^>6=vCjef-2M|NM%VD0&mAhDRooUhd=KtOrQW4q7T)>J z{~_>8i`|ny8#}XC`Bq)pwaxi3ujp4!-_3`ar8ebV+$LqTIOpw*89|)$4NI;)t&Kc- zziUPLiz7yb(X4kBvy@Hlga*#PJX37l#K(DDc}adp?=DVWBRccl>J$2lPH1hn-JZ8- zm6EuoQHPJW)+fc#vY(Sb95LLu=Fz@I-Ty5y%U5KTTYTWL_a`*=Ond(Oo*A#! z--ipf6Z|Qn&Dg~G zfivFpmHAd=Jzcs|r@qziCR-?H@u5c-d$l|>L%(iM2#cBi`|_3+M*c;~CQl;_J}8S; ze3j@=wb;hJZHaWnv4Whtw>EkCo{rT%d;Qu@U7kO;OgrK~NIzN4Z&|X4`-{=N^OhXqh1(3dBf!28<-8hMQMF|EKzODSsmH^*F@A> zHaUIL236g%R~v3{>V7NJ+9s5oK6%6REjMzgjK3`g-vdz5TiCuP@qmdTPj;t8weax7>)*nzmj1+6u93c`@rnN7pS&l79O& z>O#`okT;LNRmomUn!Pe&``)+4pA&ShDeb?r>cQ0+a;MB-e)Xk(7 zn%ooU)T7A48<({Act`fl14%c1Z!tdQ6_%^`@;&1b{|9%;=SADN_kFQ0uG*Ks&$c-2 z-=Dufr5W-L*)-kv;@|x?L;0Nx$JS{)sdwt8oLEsUQWjSAc7w1|hu@9K?-y<8HrTjh z%ha;+*}q@<-I(&x@yeO5_sSK!-g(YCVf$!L=OTTrTA8mQXZzbza8f77~IX)5kzYgi%-4?c|+hBcl>aHESm$zKrth@WhQzaio zmfsHpZ@g(anfLkT^S>)}y9+oIPXx?%)Av5Vi{r8ZGe_|&p5rntOco4WmJ7WjR76_j z7Wzjh$tdUuhjFlTG9OYrlz2!hvA3Y5V9EwR3pWct3&$H5IDBe5|D_tNd?4QL@7njK zr!r#a4fXk=G9@L)EiR(#2mMKe6r!<^I~GbCL9W$=tlp_5?{D)L;F7{eztoB@Z5L(2do9yy@4m)pom| zY3XNuvDvoijLWl_57L$w`Q|;??z75%TGM^A&YUFH{mIwLCb90HJ^i(y_@8;6Pd@Aa z$ckH}-~J%G?E0Tq9RC#jQ{CjEqRKw9#BX-{s5*G?zDURr;i_5LaE zHPe@uJ^s@v@TYR#sXco(Y)-nQE%*Ge-SX(H%6#u%`m-L$e-g@m zmi$s=+r`+c&-a8mzek(-WW0Xl`*tP5F zQtj0)`AKJIKc6#s@|-8f&#%AF=#wy6G4d0C)24_r+k+RPxhh{w>#mzw%_`UR(dutO z+p|gU)?WR3D6CNPs6p75=2&;%{k*mJS6eTP7CmSA`wEZq>ztd#*?B)Y78*{zo0bvv zMO#E$_H<(_i}UFD`+FtC9_{+Sm|wbiht6R>W0jN}q5H+&-ITca>S~7J`qqm4qpe5cK7ZP%b&DUWU(uaysaDQ* z!6D1*1$NE*+~1pYqhsgIkF~dE92efX!&U2j!TF`CrlA>4t3K@!ZqrNMewI00EH7kl ztkF`}cb!qK?8`GP(=S`i{ga&){eONG&wjJ84^GR%mn7@B+c@{1v-mqaj)Z^(Za`WU$$}1HCR;c`(laU-h8L!JujSvt#Vz`eP7HGtj%$HzENP|+f@iJdWwX6J&S9sm-)Y~SU-!_mFZ7!2<|AJx zK>PQ zOxx_UX-{{rwRT;cbzkc7j41}ORmn41FKefZtl&BI=FX9zx=CCYwflcuuMatzw`KVT z?rI66vrCpI$UAqx-IHvw#?&v=m7Q-(qP>;muO)XRgRTk%UO2iT`=HL6s{%a9QO-4C z`!?h!=Ca$#9Hhm&55ecwGZ8E5&!6X)89UI;s4 z{zvV_s?|4Z-_7!ynDJM_NkJ>m75DD%NPBdb?s9ho2dAzZQV=mJV@=|?V8+N)jUtj zZPhmIkf4RL1*R=Id!IdBabjh1=qasr;m!SmP7=)z_9dH!Tq^eJd)-wQd}Iy3S4E%v ztAyrI%^8ax#fWY{SnB0*T5t7^=9eaU$!&?>y{~I>Zr7X~?Yin-&181t{ds##zjUQX z{j%rJ^2@##?JD=(rKmXM)}s5rst;xDigUU3xb5iMMGyaZ)@oM0su!?cwm$o!h?dw! zm1z#b z>Vt}dUTw|TU8?MJ)@!Bt;%8?f2k&n9cSNjq)3Wy&QTNraJ~xzp;hxcRamI<-qklL2*rES!jTKAMN3(v` zP0L?&RVy5{;YqHNN{~xDI^FQTecSP*U*?FuA1H&SEv(4j%*4QOlkn0Ucm)M%j@3e& zW6w*{LA!nz>awmn=G`1KMf9k4hxmkymIav{ipM4_jV&yEIqU7BPhp_;*wy`ynjhYj zPn~mfZmwr?#Y^+gUu*7L7MGp-^Yiy>bA~gHq6gQowfJ}2lDc1gSFX!JYtr&%XbS51EHa(wXv1&+fHR!{IWo3>G-qt{6Hq_|zn!NxQD zUhS;g+-qRGbKCox!Z(Lce15xQ&1WvTX`fP$mq+`m`{=&eEcSB#^IIQw<%Hb5?Y!dT ziXHjZfdW6?s_a<5F;YW0qEP65=G&XnyMN~?pK@YN)VOwQW1?68OAciNE{?~Kc8i)6 zR$O3UF>2j)XpPgl3jrBgXEbF3W`susSgg3vv{0gbp@z|<6FT*rUQBgH87Y#NI3+D^ zlp9PolFIsYLd{UNI9)0+Nz8bWrR0VaXP!+_Z;WbmG_sec*>=mEXF(SQ^rga zlb%^`CU?4CNx9CW6}?3Ltzh2N8?W4-Tla_=Pi+$Di$9Zm^$7QgM_fF+LYCf{Ij^W} zXAw_$!1_vE{Z&&xthx|mydq3BH`t)ZKg1>&zU`{Ef=w@o-OclMp0OQ=RKa{Z8y0mGM~E{mb5Bu z{^qwXmukf=s;7Hga&O-J-P7Z{>%*Mo;*)k*?M%MmS{qn*c&U&S@-eEQj>v`CcjY;l z85m4B2@ZR}lRjwGIkbH?cgoG6D-I%U|I@mA9H)d99%L;^6Hv<2cyMab3x-!4R&~tu z={`6iB(>>ObY|tkrJ;5W{Hc|@tb<#xY0A#&KKJGR{hO1De}P+PJcor(h%A14WAf!X z1=cx1y?JMn!(o?@Wixipnj4WJmLYZBFXv7BR7bXL4%r9!pWgWv z=6Q@e&+}?w!R6Z5R*lkm7N3{;zEl;9GPdBj?ZzxM`{WwmgcZ*Y#61Yp60_2%n&0KO zl|L<8uTE!s%3|)jk|mD}_OcgETjOOe_TucbEz{4gcq8@sJ}+z9H=$?i_vL(fFvIKN z4u{QNiDq1rubWl%RvG!`M4mD?^OOF_v(S3eq!T;dyu0zj{8i*D##TQq-RvzfS#|Sg zojleccs{uNrAOV4xgOHaZe>9`HTG)g+`iHlwa-J(>1W)2De39G6|F`ANweN`1g>AY z!n5p3$Eu@~GPJuMznI%A?Y1Osv&hQgy}4VgS8hF7dZ=Ualqq~uo>}ESsa_d#w)EHu z{uS-cqRSG#ZDwMRJ9=?i)$Pmscut8jsz+al-7)t^<6^#l9~)W1zJJ*G{n_nfHgc<7 z=3ls>y1v$H{X(C16U}-np^VGhyz}0yPV`b=EyN{oFlK~1(+FzmhP^k3nzx}_DJLNB;N&3R(i z&CPHV)(QG{aoyZ`uhMIspBYElz4D#A>u}J5`SZ9kPu3q>_(y%q^ZgGx4z1Xnk$b8A z?v`&4GO~(nOfyx!9ARgBn|d)=rfQb!Z;LJSUC$S0OmqD#V|8Frvy9b|MdB7EGhFox zU!;odJ+)9-r)sXVdt}KR=ll&Xbout4T~uya(&W0_qGXoq`{EZo-F8ndFm<0Rd|@NE z^o7P*x4BlQ`D+|MHqV=S_l?oo*tP>ra^_$21--3~aD`dgpU?ifxlOzzZn4t&#d^v*OYFI^0&;|G3C&U zkCi;>Pmin;-NiI%0@u8&UDtMq8tJ5*^xKuk)+hRV$#toqNn*32=Vk8>T)oCI{esx^ zRsGqUugpmNdg}i#|JN3s!LEB+FY_ELynLl#+QSz4Jy?wW!<6&YJ1(>{#w>ZaV)kQq_DhmW%DA}B1~qKF zkg8au_~*C8yTbx{#xZZ6t!#|*uZ!k-Qsd4q|M3E^%%a{0QJmq420b$C9e5WfC7ZjnkzSK!~3nV z=N~FAtrVZn`QSOv=Jq#ROU_(uy)WE){z9e-_wq8gC5P@U+jG%Se0_!e#_Ky}fAHOm z{^WMN`Nlt0(Py9KCpRxxzUS%IXe+s6v9XKpaU@>~eJSyMg8=JU1)ih-E*;6aqgbeJ zrNUrjZFSO1XhMp*{I{T;7nVBk-Va!}{pice>2Y0wkyk(J&YSu{|XHTxR>w3~e~=S7O$m8+-p@$OhO$G7TWnb6@2>y;)py!zOl|7b_)l-wZoxk-$ro# z?;A|NX1>!`{lfR3i8+cbKfW&R5I)f?ws5)OFXNB@XX;sgxwJLhk7t7Pg5z!(8W$p5 zIBPv0*f_T2eh{A{dH3jUp4~g9Fxx&YFH~)p4Jk|bH)WBZ>;k@b!TBi*{4`fg_IkxG zxk6rhh56Jg{zd`IMXyMEy?SfDWR};fxja|&%L06Z)u&v!$#bQWwKT>v_JqS)%@xBcc@vOp5g z%5rV7Q&+!qosw3w`S9ZY$&&y)6&bIeV;#nKSP3)o4}f= z4f5BHPI}JW@-OO%TU@Q`irxPfNq26)H*ZGP$=>b*wcAB=xy)U5*w}3pYPo&R??S;` zk6#wgB|=$(E!VT1eKA=rk7J|uS@uPuof{s?DLk1{8Mrp7J#}W=8MSAXXa9P<@b$gY zoaSt0demI@UdZW?n6<)%a;(*k5^V8Hw{23pm9=zha@4A|W-H5I2K-ZM}OUB`O1BZ>q7pQzsXbG_~6%~n+1nM(w?pH`Sp2!(4J+d;y!N^JT^7G zhC4b?;K$mQf`g$?6BrKmHUG>jz8(2{gYv6DzBYkfN&AGmmp${2Y;lxet6ccWbP7)- zhXI3%QEz0gWpB*ll@~9~xPD{d#swQ!Yz$t!cB6Ol_G6QdZ8|3RqIV-xt&#C0$yNG} z_72BVHcU_UIG&R9vPmgois6NhN1~^b{`oxAIg{unX3T6B`I9MVTKR>G4<8zezED~@ zX`=qOZ?~^R>Mh6(3^$irwN7l>g@91AmB*Lm1o^Fu=zLlyQ`f#PXj#ydBaIK4_@XRY zbj}@G;@v#OzLmSv#^X z?e(-e|Lo1dFPt_mao2ff%h~EBTwP=t$@z7T@2vGg{;ZbOT8nKC*GqZ2Rh+wdXtHpG z%VNc|LC;z}y+e(r2&bQ(^i?QS_}c@W^!1P4&SZO!y zTqX3fYwV#s~&0L4X6?>6pgb;W@cbu z;UaF-5tM~st=iCR&}kmF-4{H1_lSf{X_r{SuV@#@7`TAr%Cs)s2hEadOBfHeq~tXB z*Dqb%*K_U%^Pj-Ja`Tl&8%h@aoN)G@ZS}jG)$i~9`SbNHdxLaDa)r8`|@;5JBDbeG#Z$jj`vspaOmg_ey?S5K%|MQ$x?}X#*x5P`Q zTUaJqZOi^WnMYmdq-fhQ$@M<_*pvk|GSWJHIhaJ3FE}FD?zN_J_nTYMx9twIDyAOS zmS#LU|LmR0I>pU949w$`GS1#yw{^$%-HD-I$Jp$1Z})vXc0u;=-Amlk*3({Uzvy_( z6C&E1k)`H7muIDik?-*pVWM-C*GgUY67*72YR$0Ox#$touE!Sr-6}IW{#1H#9zN=_ z>Di!RBZUbu+8_D-&SC}a8T?RRbO2N?>?Sl{w&QEF9Q2YHMcMRbh@jc?;0XJ zH~1(|+Rh!vMQ$1;zHE}a=R5K7rgWa;MW@}Cs>RxC?A;a-{_oTN)BOilD|O4*JI#C7 zA^Ax1+^GnOV;yeR3mhKj+WPuZ*AMYE^>Y4Mw!)3=2OJBeEJvqr(8dA z|C6|oEvzP2wrtgS81!Mz!q^2RACIrtdUblq<;wKj1KfKKalR_QlJJuM%E8vIjlMl^ zGu(K%G-^z&ot5jSyzi(#`m#Ui(t&y=#e@lNe3o;6b!}eh|HA9-W1;qmTvf-9JgThv zzQFv>CE>hGr58{6gtU9@%Iq1a%o>>!|f{ikwxMZg}x-Yr#+UNzZ=9g(aRnp~7 zFLai!oT#pF)qGEXwY-{`vFVMzRLio^w%my+uZ_LjH(&J=cV~;W<;oRM{(k0hKhhaIpaB@2HU1A?F*7jm z@(?#d3eKzG>JC!r&7E3ZE$k|C>^|q&t*2erF>O`daA4WemPLzVSZ=drWL@IOT&*Fs zeRi%<>E`a;(o^%7{%O>;>z6;k|Mc<9lV!T6^X47-ck#^AJ^L%4pP6}c|NqL*;ti}8 zJgv(vm-BuW-0ssB>>K}mXJ9eU-KgibpLetX8 z+cbfSa7VdTo1z&h0q%xVjwZHQi>#FE2ytB^)fo~!@8$1hw}Za-ZVCCU-lnO0(Kj>r zV4%$GBZ7O(|7q>X{p@#Le6@y!;ek0R9;@r7SM!9sd0QMVym|9VtllP>C06M#=k0e= zKeNH&)kW2JlM~gZ1-#w6VaLH*|Fe&5oVVRuk#j{ZXsgKM@IU(V_&zcFJU48WY;)fB znfq|1?#+fKCB3dcF-bcly5m$sJI+5cI{a|?ofmB!ZS2nH4DY>*X82SfdP`;90+-+2 z_vgKp-(cX*DaazF+88FgiKA7ZPVTyTA#bYI&cun4Iu}G=pIUr%3rp70_m>`8eG8f& zoqJjJSNo3@4YPk(Xg22Onsxghx%<9w_j^_GfRc%c&&#FDxqgU*c(De~ELHklqA_z` zj>ZoA4c`@Jjc1#H&XVSG;((%6QjW#Ic1fy7Dt3t8@>~9_PJc-m$X! z%p~_fe<(>H_Y=1Ln2iCr9uoHhJ{ln-&cuHl^motC#W(9r?UAfP}Fezn$ z*xJ&Z6F+&(K8l{@dUT!fqnV&fO;wuseYeKxsvoxKd@SMcVLxEA*Xn+&gjvU|+ei5W z&n#u>>C^2rywG>BWYLTyre)lEsXoV+Z%}Ugk91)=sNATV5+vcm%)nqv_%L8_QyEll zKuV0MA+g1m!vyZezCIOhv~B8*~yz$lC_xEhy*j>=fS{C%bOvd%#fiSD; zXUsR>Hi{pQ+n<{*mwUT<;k&={pRcv~_5WUGkaGUbphMH{J8!tE7;LwAb6U-(0=9!* zSwcG!&)=-v^t|+u%^c?$-dp^VX0%Q(Y1tgW-17O(;?&;VOePF&6Bo@(vU8oeXyT%o zNsN9=BYZZnJY~_H;ay<&RCh^4<53oWZI(woS0*punqbVb=LGkX>nRD9&zjVpnp}C5 zqFSQkBYNbB&9faImK#p!h)gW0?CNVfd_81Ig1@5W;Tp(}z{ny8ZuBFRewB^MLr>?I&xrD#uT-)ta z3r?@j=$UN(;*scNXQTgtU*A6zj6Gbat^a72tG2v!N$pfMrj_+Uf0I|NUpVpOwA6pc zB4(FuzLKB1#$=Pu(cFDU(|_pO$G2XyoBpWVzyG24l({9R7k%CBnlq!MoqJWCSMxhl zk2Eh$vzvL`>28eCwW_Ygk0rJRzAl=P`%ZexWd+}fOMc7>z5n3Nilj*IuDk5VKL~vN zt8&h?F6F}Zn8#@%-`ozp*WS5RzAoiaxzChG>TUA_->Vq+W`Di0;G5RW$&%`=r&ndK zc(zz%Zi=m`#nKOI3oj;2T{OMrSaLX5{gIhGshV{&Ta0f0^O(k4qq*pippEU}bJja9i<`g3gk+idOhcT0^yZQ1`u;~y>0E<9A6c7Tb6gY#{K*w3pqvz}e; z^>}+_ZfAu^OlQ&=_m?W?O`NSA*b_Ufr1l*Q!X(yG0-zKi-5R0fQ>P%PvFP1YqL-k)mGo8$3o7Np|3xdAGBPkMVPs&?ASz@bWnAcl-F}A+MB2_;m}y5 zsgQA`Pa<;V6m|zSm#M3_IIeU|I(=cG@*X~B{sYd6bEY)TJ2+QrSB>vPZ(k0UC{5}>6%QFa1(sU`wKf84A zfurkuZ!LNlomq2mQK$R?{#2XoYujc_`q~>^lwbYs|DSU^pU=AuKDRsIIGc0bdFzk5 zAA_u#5{vYoEZkDos&BhnEaJ{X9_H$gYxdr{S#$lCmXS#Gy!s5c)9;Htb+>10JDEEvacQ~B>4%OB9vZdkuG+BWZ0h3g zcG6F`^-0E@OqL2?8MeIXH{;X1XkyzI$qMQ%^a+%0>axBo=Sk!40FtuEPe zvff;^F*o$&lE)rKSCb}t{!z}7-E<}Sw5-)UW=)yuU^y9%_#I+TFPQzid0hHn?$mpF z-10Jf4z$3utba7G{vgfiz5GSd4FPl;ur5P zlY+yw1gB(#O}cnWQcWw~F!W_o*hvY!ixv{PewQuUe;#>i$XJxM*69Ax-t&SV_LOxV zn;FsAs-gbXr2VKyh~nJfGp96F<^)-#t7cG2!{TbmxwUKNWF1<$v^X#Q z=@lVA+quQ&+RNt^Tr1Fjn6t3|-xcqvud>AXFFST`e4_7hSlO$=)xTFm^cTyr<=f_& zZcUpKrFTiAUQ}>D+HlAnmh3!zCI$v!HiF00KrBqJQwN^!Gg;T7E&KV7@s6XCpJr`Yy53Nx z{8wM~;`UGK_X~CWzd!YP@xLvuyq#y~_CC)oz5BKt{rxjX-9{w+Ky%j=fvZJ%4k@e{(rZ`CDCo~G)VrLofU z59eQ#C0nL`nxSSj?U0fFlM0=fL}`=0qgu%ww&%7!NHaL2{qEPK=+mcG8q1XZMRl~RP7?IRL^eM@m%!7l*Hmw`wQ+q z`quj|(L}T@{=ny$+5hzQ1@?GpD0Rzp_}xGEBI)4Qxp|ULUe4ohmEFXc8tTIMx!IS+?k&ZhGuB!xo7%L}Wk!}#ntEU6vV>KF z%cgbQ>`s=_oDtQsQay?9(TrvhAKODFLWXNC((G9;cNn~NIg=<8?0;~Ja!=c0j$+4o zITNg3-(4j9{7vheX+?JmKfQQlvgUc_Wk1cpC2#$*-q`T1S!!~PTl#d%Jf6K@9sZ%6 zY4#xD&)#e%28Qp<3=HZ7sxNRE3aPzXgJb=#1PIKXS(kG=^=50VON{BF72H~)`&p0Z zy9A1)u=dWHarzyr-ZZnDx>ENH9yYqy_w%=<*H0;|y6qb#^w`_K`rMhBGjHz%r#S{; zgN6rJ7talIKDW|A>C5$G!PUnm<*Zy`c`vWSK_Oyy(3V>q&Rc(8Y+bSSVZN*H!{b@e zzcL?)9Iu)B$@ut_Tbl%1ed{)FUuC-xHeC2yJtE}n2RlkrPF)e9nQ(IPzB-f1Cv9~W&z#fj&8)iVa^uac zfZsLuf3qDweJnv=eV#?=fmQA57cV`HwTfL@b6wN&+RTHs5^mKlty*#`A4HzBe16Ge z>KV~Anb%CO#RLDZlW!rt{$l~9jhFRN}$-Msd)p^HZ*<*W5-qc@TdeTr!E>iXQtZRvFJUfr^{Jwj< zB!A~x_A_@Luj>2S(RzGFu9@~`AKNRZWVHP^ZvLxdoBXbG+PuwQm^MovI>i+HIjZvL zDW$SJL2vIfd#_wF@-Fq!m*UQ!QMtA?RP0_-m3L&t@iSdw_nv=$$b9Sa$-wg=7ycrp zS5R~7a7gi0P(yYuBcAjcQdy9Sdb~pG1YiF{4gziWH7_jP%F*$oaPkR9iyIZwDn2S3 zt}J5MJ1h6@gh|`HZzVlCZzSho@)30M|8DJhHxK)zJ+HktbME{3{rU{)779Crcb-u$ z6jpD#u)|l}NWc5C(j!jwD{A!<@Aj^jYkF<>&|<>RGv-g12c25-;k1S5zb~bV*K+>P z36OX7oOARk>%tx{;s-(ZQ5Jf-@nM zo8$L47IVJbIK%Kv{$#M^5hF>K-|_v2;?j&yGo=~Le#tH=lAod)bxZAc%IpU569?xO zaq37WJP~G@CAK5DnDs`$A|2IdiwkTWPR~5}`Qr~J+c{w&Jlkv5zJ0tjlE0>L`uc{6 z$AtnImHAJ8T-85~&vRMU!k>2k4z3H|5M?%ZrAB}UyL zPHzm4mnKQivUOZOGbOP~D>=#EGEd<4l^pJ$(XR0}(}io!6;6Jztxf;JZ+`XTHy-Zr zW!=4Nf#J(}2f}5_In7LKdc&-Jl&`9PYD=E%;XT=Q%Wb2xispZw>f4yFD6tlJp6d($L#aQk2wo9e@t4URiRg;R&O1n z5!xrH^`UD6$8?L>2_4;*yLRt&Q@^MoB^!12!e1xe2WElXLH1{30@~{iLJ75!Uk_|5`a#?o~hL z*kdE9ah7%cscxN%c}g=M$+UNUjybmCCWESu$lQk^FPAQ2?VPCGP}};^|ddg$XHALEOQLe~H1cE4C?v3KSj-~T&mJ1(ATjPA5q895_( z>xy}crif>(yePcRu-5u-OOA;5oaq`mulaKL@n0-o)c z4U(_S4NzP>=XKsW$8hoV=jN9`Uy!(-D0REXOZ7!?>H^N7BHnAFkqsvtZ|J3*J=FCi z=J*|!%UrjNGp0%0Q+D37%;>Z7-j=fuuH{utbmXgj+@ta^v*(uU=GECv&(^&@Up(P} z>+utL*FM<#oV(PfaCuRfk7e?%6^aGAGOv#q+UqYBQ0U&`)g!|$Yj&#dmA8&T;+m-q7 zi{OdtZ?CM_&8We0{lf0P=HJ&YzcYIA!?mDF!^3ZbU-WW@q$iJyuH_n}#=Yb#-7g!I z{=NEw-5xCvE82P$C?seT!w=UzR5`&h8aI@WRq}f7oJwCg+%c#R*f&zm~0^ zdwbPaMqb63SNEdT|1em%XEWRg{BrtqL> zF6FG>->6r-P}TG4huElI$5nlc{qqk?v?N}hb2o(LVzBGyH7|Yl2xf|9gBO|oswbpR+sSUfkSg65Yku0+eQ~bUTb;D8YfszdJ)B#uwz9JK zsoRULSj`0wEvD~VGfnMRszxTCE92@o(FG54w$GaS(;RF<^6t9 zbMq$mp4{tv-IeMQ0;&ry?eCP*4|lC#vE+2(-;$p(?_F+&@9{hD-@QJ0+V5`ZwKr2) zRL*8Wo zu}*5|Rj2cjUf-vdt&_cAP#zzjxaG=*qL_^(3+=NPo&Np#_~zTQ+fN=n`*ZKPjk&kQ z=jOBS{b*-9JI43h_xs!b-xA6QD=Tu#%{F76cP`58zk=^)>+)IKCp->(RGS(5x@PX= zCWEUs**w#W4qkn|+*9JG(H^Pm%u34Vr}pehUUY2Tm&AJ|-Ls6B{&!t*d_uj}B>{uW zXXW-u*(A#5*k}A_zSJ-Bs&ZNUw6d0M-sab;W_(SOS#(Y#r#|+LW32c6*DGJo+@Y&# zJKvJI7dCnw#O`o&~J>9l0WI@{!^n z?oXc^pR)UegwDV6mL0DT#a(2~@o;{TtgG-|+Gk@@LB}1ImLjX*>`z-<_i;ZHwY;HM zay^T^=g9-4(^n&BUbZryx^a0K0|P@H0|WS2EMg1Xf};F_)S{Bi)Z*YW|Fhn@UOJw> zfgYjJO&1a76t|e z5s*qKUeXBaX~DJmI{JCKxdw;m`MM!YU|;~X%Agv+6bqOH5g?0?L)&QhbO?YHgE2_Q zOQ;TT4I2-uVG$k$Ez1Bah7zJsnt=hNTbz-BA-^bHA6YlJfw;3`-ila2erLnwbMR5G^M&ITL;)8oJ4Urv&=WXJlYF$;<#gtOaKHlEw?1 zI2?yzGSaXKy74WicEz1$WMKHt1Zf>0jNivir16kOF}lI>5iEucj0_B`F$})Mi^E`- z(xSwY%=|p4`&BRv2BnT|X?0qwSs563_#nqbAY43~pE!fDn|pM3a*iDb1B0(N1A{8e zPzF#G_KD&!*E6>OXWW6D+S?=kL57Ke0n~VdnYyHrUyhKGn2rRcvjlIIgYUQ) z80sw<7?fegfgL$Z9=D14h!gkFqsdq~)!UzufkBLkfk6qyz>`||4aDxiq@p+T_AoLq zoM48GPQ#2`(iowG%e=f2LIJr&W!{BiMg|66X2?h(!qg}|Vob%80gA2V(&sQTFjO-^ zy4DDzB@Bo#8Z#6?5wB-ov`USgfnlK#-cEjJAZ@kFJ!oX0*i5?u$zC;-AnO9trm{)>nHYlhU z?PGfW2f7IbblD2R;VS}(FdI6Zh-o$`bu3Sx`fC*{1A`nt1A{ZnU~ne>5K4sEL8*x; ziAg!BQ1`oKrskvsK-vzKm==OO!@;{)JC>P&;U08rKg`Z0jlZMuSm>Nll%JQMlb?>m z*hdR$ZR?mB7%Di>>yG*Hc#H)_s&8gWUS@g*mcR!E(5;sds`YFP490@!c`_}TfWg>P z3n&l{eJfc1oP~km04HQY7a|a}vWPGga=}_jY7u4-gPc5@amuwH%nS^n9Owx>KA#BF z@yB1n3C&(PCI*HgW(M$Xb%fKG6cb@M;%tBP(m!7KvCu;%28La%=mE`LhR|zp~)H3Oi<*lU3J@NHZubQsDwhTretgInu#NYO<|hy z)0~xoVKxtXY4@WZk9pt~JD~K7=}J%{kbSuN={IHuhBOZJBv#r=jH$R&0m$@K3BK%R zYzz$L0_bhbot?y(jyr3_DcQ{h6?UsQAe(~_F=jb|Sd;O){fRr@9s?EzhH0GW;oUxk zkPX<~4GIzF6$)3sVh!)VGl(%2znlL&loZ!yVqjRzjNaCAn2Xo+ymZXu0ZPO*)f?)S znHd-sv7u*-HS_Tph_fyMnd$quer5(U149`bdL1adn6R0+0{~?9`fX1Vcd{@roZ~`o z5g%WM&upX_GxP$-N@ZebBQpa-HamJTd3ZG*BVkn@DC=QQ4j}j1FVxnTOXQ>7<{B4l2&>R*9hD}`P0Uf@X2*YufZlHABHjgbB)B#)1iXJNVJBTv7B(P`Y^%XO5F^MoSFigQ{mp|Q0jM;cnik+lug9H-; zLo9U55u!|TIe^1x=#>MBC8@CH1&%Icjot30ppyo-b3m@NLYTec5bM zVqn;c5iW<05^Vuybb`|1$+eNuSxgKJ2chdr5pFj>LC|PW-oX(j=F`gj&Ss}b3_@A85W?VeF*J(X@s$X7l|?!(x}4{BA}2)zl9fJddC$~ zOve!}AP=Blc8ai~jDJV1#N{aOx$;ViF- zG8{83K*pk9;eaq!|1Dl)aa`ztT;GGji1EG(e`Op`E6KFuj|LIMXqM z0~E^WN0lJVZQ~`*TpTqr$Z+%{8xV%y79hcJ9Kiyz0exFH!UjoE;%&elH0XO>5oRYy z5H=e}uz=zcePbTNU>+GF4aN~9Ak)#e?;uQfQ6SEA?7@M)6#-%H1!cVELfhXsvJLt= ze1x%Ds>B$J-LdFP%n_z8Rl{#8xIvG_t)SF~zT6jKYQ82hrs9Y-kkRPtIT1#`)F#4c z%uoO&1@uLC2t#M;;V=|wsTh_(02zwD;tXNvM}5MEqB|6Gfe~mm62wf<{H3N5Bj(y9 z#N;RXEH7wv5<+trre^fTLWt@f(bm!GF%qL-YLSX?|cEV^7cROBCDuJKueFG9Ko3SAP4 zo|;Q_6;yOjEKM-f^>Er4H`DC0(TucHDM{S--hb!5_p$E#*;XCT*s_&hU9W$-D*vF_ z{$Tm^`!hKMZmB#teCB5Q{Mvn+zn6cz`TWnv_x<+FFAkV9Wu4NqP<|Gtoy`?9xu`eh zSb@>Y$--%M&UHslNHLvyvE#PjyE`X7ru?h8cW!I-{=M6b^LB97@3|~JJ!rc6Rm)`) zPjav6Rxu8%J5=D__jB{N=j^>X2Yp;7%T-nHGMH$(yCZ1#?Q<7ek1d&M%+B@r@UqEf zZA*i7KQoqD^%VvkOgeXR_RYY?*-GLU+}`9|lPT1_75VrvYq-)D^|gCuU)aFHt7T$x zX{FY&#H_b-{)b;UW7?N`NS5h(QSakp&2H7#PV{b-m#{lGlVva4^;H}Tm-ft#o5M9t z@X*;dYm*n38U?~Q?gSidWYm@pE08_;#(u-|f=%W+1<6}qEU}f{DQC{Ik!v;UtY^Dd zCiFTSun9ESbZ6qU6NfAom1lB1P7^gxT-#t)o-X2IvGew+$aiIpxv6ULy=qn~6x+f? z1J{U0l-S?Ap)j+1QEZvDsoLV}Z>QYVwGPqoU$;}4 zY!_%2n0M4~s@$7P0nZP=zbNow>E1iW^FP&b-hXYeZSjL`T+vSrcgDNQ=S`nkdgj!5 z<%GnLPbEJCbyf?oM4DSa@&A-x#@RB-%vQ?eg!1GyB|_%$cN5?FEw>hWS)#sw-I1~+ z_TXy~=L)#_44EePJ)KpTx?>HKi}tQAIL9;pnE;v}RkmeoK(SF=mUG z62k3g0@q*QEO9?2F>Pgqc$4j+o5E?wW}ju%l?j(T!mM-ZO1gj5@`{5`rc^GHdDf(5 z)i=kBEw`~ia!Td1x=r&Yq+S<{DAeq5Zk?0eJ9}@i^0SsXD-MW8L>n%e@;YMY##mPE z*@l%Saw01_f7}SxQad=|)gGNy$DQwJN!IEqSN@LQ61$B%;LEJM52toSSz) z)qiHC+k`atgTi;#Csu3aet#NMd?sa%w{mTww@bg6&j@jsGPoYnMt_RK1q z<5rJv{d@KKy=469mHH1{U!L*)+~t&c=lLU1QPw?PdNC(k9S;A|E-PMg%W`cEL-c8( zNX>)&5B#QvzSU(+Jngh8VHJDC$_Z06=W@MLeIVHwvZQ9A_ms-%F>|%7H1uuN0u*w@ zV|G@b*3xl|3;tJ~QT~-61zoaEq!RI^5xi#?>7REdv29ZF>FAEa2Y%+4YmU*<<~0|CiaFz37`VRc4Vq^Bd2X41ANGuiqhG z9*}ip`xZ-=m7NxAJp!|iE!ci6@z%!$KYlmX??~glee&ABulpbNNoMf8zgH~5_owBl zvnh|~q=)R*v!qsi6^*Ss5_08dmu}sS#VaFaw3VWd95MN%vcSNk`Q*aPIb60eM; zZGSMuFZyW@s+HdBectQJ#=tP27r9nCiB>Bqpw>#x`8heM$t9WjdElxEQYCE-e;pR( zDYlNgw6ArMrr^f~8C(Lc%`8(^WW)puI3D$0ePoN{@z4Vok8z(^YpixRdtP$g+3GL- zl?|0EB7+yK@Vv6EdhNf$b7|pkPjA=X>i_Zn|G(el85|_eI49iIHD6U}9BE^&xZ(Y^ z)RhXWThClw8aaE_)2BN_l%5}rvW;99cfyM0@#EK9FGuYZ$=bUvT%3u2jbp*K8J%nA zpPHZgB4(POQ}oVYf z#hjbh`kd~aDP5*B>xbeEiT`_l%|UroZ!7Za;dnv^zLVGcm8;(pZU#<;pvNYm#gy| z?|!cTB`&{hy#UW0MUC*+X%FT4ewG=lN-v$(`{A?qzxPM)F2D8p+M}ej?RR^;J}$oy zwlyK_tW$C2%ZaPr|C>4C_QtKe&KEY`773rs*7PvH&$;?}6`N+{q}{7#n|jnpsk~$I zaqtm7l5nWWJtscsF8k%yMgONyez$k~)Gujm-W`IxJb#mmJ)GyN2;4h$Opkwe;*D;F zr?)?AD0-Y%xcRJb;Z1|d=X#rG`pGPHN;y1Vs4lECc}ahSvAkXntJ}1jee6owHxptK z{NEk1pE65kroCOK@iSi`nI&tK%9ovfx%F$LwBxayDVxeUEjnb6#ax(i_U6hrwdz}Z zytFO!e<`KE%!_Yl-`gM(|CKAn-uBDyFb|<3KiBsfZuc@<4Qd5)4cLzxu>ZACn5+8T z=~c$tJDI<$)$dIZ?2mcu6E7BL?{B@(%?-0$O}_?f@U6hv9-75 z=&6TyQa^r{64q?;ogDTcamNS8=syBSG^*To&Ny*Qitp4f9hS49hXM|?+;%tQlqisR z{MN7kP36gsDYb=$kCfhdEv~kxsSUjm$o^V9>08U~p2z2U`j~`ss*PB!&-|Nt<;ro7 z!+y^vXL^_kZ~Qv>!SNZVp08rFHRx_$TeHvp(woN%|8D>L!wKM(utG)A|{}JAEijOXaYbz=jO}sb9C-?3Z*5j;) zE&P zPkP-{%v&0$Z1Xaxwjd+cwc|+LTh$V49`P9m&xnhPUfTLs@7Xeu!z_n1Pwl;VbK6~? zs`Ohf{Mz+zTwgEFs`s@xx#HvJWz`QWLZ5C;mpuE>k^lP% zyDb%u=LH|3rCJ@MbR{G;1)3=2-|ve%j$Hh0Fi@}?!{lAKotl&za1-nd~t zzs7I2)ob`4&D;KM>H_VuJ+B0H_9(U`vh(&h&bi;qn`z<4e4|srpiF9M)yG43w6Aq# z{pt!@88iKCXH4^g?5!b2t2>J$SFh~k+VlNc#iO#K?eEsu&DWXgz3+sy><+~OTfG&{ zf~MzgUpl`@@@nIAKlNWab0uYa?!7$Y@Zs6_l0IqG``hQ2$49U~7T~c{WH@-ISf{?e zG~?F1mIHs6aIkpSpZRtDdegFg&8h3tIaZy%y4{>Xc17rM+y8};+RyS=S*(5M{C!){ zvaBgdaqO#X-uz;|`^#Xv?%6DXJuHF^2YD)(pIzCnaF>*9h%imvg@-|9j2W%7~=>ql%147c#smky}) zrB|_2YGNLw2bWfwr&0{++o^yFm{dVgesXGYv2S95iV>tf?G1gMCmb&D&o3wa@zG7vL$KF1$+J0o|fo(C} ziEFZU_Z1vcZHv&mpVqOyg8!tBY{C7Fcg(&s9~b7omT$1(Hc$7#vIm`;glyfHWpeuG zxE>183hwM*weI~*8;)0D*JCaiq$P+%33I^PQ0 zyz%3b)A^Q4A!c{i{ccHA3lF|I#eC7a)8bk`4@b96Hg*5tw&n7}e{EL-b!xH}J~TOe z;=D+%&GhxT(YwQLZauQNX!GW?Z#N_*Fio~PR_GR`D{{o06t=qPP0(7v)Fi z9WkAwD#!dTPjbe=6t2q_=?4yQ&ioY~(jWI>+IHt<@zOtd)R#VEIy_T(($#{Am+nqI z*SW!2Av{K#gCkk4RisEmX!F%NNA;rSlTU85JuC2R?=*5wYpCLNIecT<`?n{4@Prp{ zU1%BhOU8V+;#c=njokYWZi|S0kUe}<(WCFsx8^6`y52a5F3@vc|7+j#?1(FY6E|$< z7vEDQ^W6J?W_R+wBNK8g`&&fa>;mnrdoCufTt4yQ$|d%J@-6*y8x~sKczS%E43ps| z?^H`u2~WAIDWSI)WiCDUZhN_TtmS{vG;H<=a6 zzFv_1Ag15bUTERw!>3QrRrsQ79n;;SyY{*1m69tp9{-Z0Bptr{G`qz8 zGiu*HsyaUuJ(vD?iEYD9&)GL?r$~G&sFc5dJY(;#KMYgPUXok&o=sjuY3jr$8fy<2 zP4nn`5cNwg@%KjlzfyBOKAd#=XnX8_!q1&XzrJiWoiy22Ip%nmmwu;ogsSc0M|a=s z&YI-gb8Y1#c@Noy+G76ld!k+I3-<+7_1-)rC#C=I_|Gf$=a!4y{PHlsOej8h?)|0v z{n9<9{#Bp&!wbsHrP)#&zPBj2qGId5^j<=>)5y4Uwye|C6@Ryd3KgQnLFAI!RZ(oQLD`DAY) zdz|I@>OX?}#0wYhFTd>)u07R6`nKcVMCXdV%))Gen$wd)3!P&&KE40-`|OYj&rV73 z$Nsbkt`>c^Dx&?f%H_=LbXhT)0ZrL{FrGYFEi(4meu-w@<;mvWLF2vU)h*nr*V8U zqvWy))6d>~bMpP4^Qym}zyB`JkoVBic}IfT+D+3gXG3jBq&fYx<(<^(8dBV2L z+IC*nN-FT}s$8ke@{=>e=2}a(oxHdG=3J+=);=fJ+WQauE-&hDUjN#!rc-g%t_LOS zGN;JiH4!_RzhQ}W>AenpqYXBPBb08M2PGU{BarEFl{a^Hq<-c#K7G!!mt*hc{`_cR z?xVa<<>8(lfkTQsi4korj)_M*irm^-0)Hs zXLKa+OrGM}Z^wDr=nn_SADzQ4-1(-w?(4H(mVRCvcGI+^XtzV>o4n`c63asP)wxPJ zr#tWH(4G>rKkLu>f(w@_j;492Ug^p+-w|w=u`~CkYg~5L%zc$V1UV<4k>)uZ`(!$k zs-os&sderP6kNULGSB_6c~yCSRG_63rsd|V1=TvT6TbG6HkE)v4H(8mcwJJ`05!~%_K0v)Y zqtrRzbMi#ImrBrf#3Q9Ph_OcqELtMJ4Vcjsc|a* zd0)>zXE0SSZO37rZ@Y-tN*@nfvg(RoCilV#OG2aVoqCO+e1*)f4@_FesA^n^ZV=m z@;WfyH_`}IFL>>CbPG@0=g|4LdEc33osViOzr4D*+xSTN?0wrjcJJE$-m=W@Pl(L+ zlYf<*AFa&()pmTtoNI@ff9^_-bh~SQ@>Gr)^ZfS_ZgJ+e`)6E~UUvOa-Cs|ZPY0rl zWB>I8sGas}=7^Elp`Y0Ga8BN>V}}>)Ox|JIc%oo+x8Kgk#!J>^-YLt;E!3R#hHabJ z#VcGw=LBAgUN-w1+Ex-J&%AWT&7eZD^%?<%eTjK4b7r-OG;i5h`F`WRb%rsLsy$&L zg>vgp%{a!&w)NcGpoFO2uGc1#MlXH-|Fk`97PEPte)&wH6RLZb37mM{Y;i57E@8*K zf>pEcOA6gf=rFD9cC4Sd(<`BUTkE44a>l>x>>pHi->&|7c5bbas!;x;wZ=PoBHL~X z7JAP}zi@2D?0|o(xl$&~nfy<9@~gMEB4#Gf_;9kKZV7)|=3T|vy6`Dl5Mf`ktN;0u^oZR&;p|sk`!{yk+!Nd+ zDlzTf@Eo)$V0`ulqgS)O1^5A%_{ybiY95DgW!aM3=TImRmW+zn!!qxkDw<_vq~%D;KmB zy;#7%V!xEqe62?w9mS$-&pIYaA4n{W@YmwYIOKF$LM`>s-xkH2+?t8ac~w?Hb-@Rn zYrJFwika>gNb2zyyNO2k$~E@ZE&J;ecFeoWqr2+y|~pBF2=Wu9vOZDPnU~=CW6#(8`RDx$s;hrBC$IT)uh3Ta$8o+1J+?{B+n$y`RJE$;oHEm0Qj;@t6LZ}1i*gf7R3K$TZ%AzLWd{*k?+j<>r@?k! zt^%vSc{9~{b9XTD`%S#4WNfGRHtNt4Pjyexx~})HestI!=sBmc@7H3P{7X_=xf1bZ z=DWYovwePN_TR6+Uzam{jz|pXO1rDoS?Vj!bJ(?AIyGHX&bP4IEw%ErSdRV?>3@bwg<7R|w|LYvO);FDsB?h7 zZDEtA`t3WHV*2Vkqnf5m+m)qFRC-o3n^OpRwJz- z^q|O!zPozv*XJ%+cY;OwLYQOQm;cvY-L$#_do(?iokOgDDLzgU5!!WE;i_(EyS4h{ z#=8zll5FRmo!D_!K~>uAPG|x9_Y<32BFkG`*Rd}Sj24mpfAe#h(%MYk&YulZ`cG%E z%gtD?9CSsCt^AeP*82(4GkRRhf9K5WNv)E(v}7T-cZG|;E617Ao?`;FPc!#-Kbxoj z;;c;Kg9+DSJ1$8q?3kp*8?K5g1j?SZn0S*hclIC*?NDe z<@URAob$JZ(63n*-M(`?tcsg%DxaClF8Xny(vr%1mu;G(eOI)b><(P`#qeFd9lP3- z!z~{Ucu&3~(SNdO`~2_wit52rpewep#V==KV7P!%Gooh#CDcp+?IHVRCa3x*Wu+#U zK$3lKP+-20qrg8gF81r2Z4a!PBDQi~m4oibP!4{xSs5I&96WyQc)->_r~27DgTIlp zB4%eS>R0=x@%T+q)S<7bX?b~R`||hAy9OI>M<-@`%jrA6I%9p%NKDZ=RX}%c2DI@y>K=X6kI=wqK!TJHGriZ&c@e`}3zyO~+mbDGQaqcb^?`;w|^R zaxp;K)Z^ffx$d8?UG~U+v-_1RbJaagep}UoMMYn&npE18rbw?nxA3;Ky>Icm<-yFg z_Of@s$zQ9|T*|TaLQ}@HpaQn2lD_be(6i5EnEMM|ZcRH_(8N<&Rnzsk}(vcJQT-)=%?(BzV?*jZvWz}LWU){Q&6UdTf z;lXh3MEc3=QOCtM+}^+8uk6>&-ZFo4w(4)Vzw5}}^HPbm2bjx{<|Z}dSW>$cAl<*ZB$47Zt)OI89i7^qzoLr9Y9 z4b3jU?IN%@TRlwo_Q%Egv)H7f1pj39E^HA}Z@kj$-8IQ!Zd=>-q|<#tkw%kZZms{r z{m=Df=3L<-zTEwv7F(S0vO5;B?Y92zOYhFiwKgs<|Nrm#`TYzciN+nFo0G3z=~}j$ zXXEWv3!|dm&Wa5#X;r*2p^L}!=3m_qE4Q;3eHKR~&B)s#v#&gA8-MO)jUCQMw?uV0 z?_L(u9 zk=?29kkWB_*228fsNbu$9zH1XiP@zp(_>Q7jJA+3&)kkru!#!_bm)oD%WiwN#wfk)x4wxF7SJGj_@DD%`yI z<=3v%Z4yQ=d+u+3IZ|-`X9*d`7I@M+pX6lX5lXDWovHo9iAFEGizPgG>$d~fWbe8nW?ovF66Qj^Zh6P6cpwhKSLeOjR4#Ir{%3yqefOfD-I z^?uB4d-Uwm@>RkvGV8K@oeQnRX8J|13i^KT@P?o37M1^qoFP(IW>e27?=E;GM56mg zi_x~wy@E%VM?6rCNIJ3i)Vg=)>7CG_^Mlbv_&`k@~(=B`p6 z%ReeU<&0SVN#pIde=`yl-(J7&aq{jlalw!c2MZE&?y}T)PW^rUXWsFGEz@@WTd{20 z&V>!WyY`=|Jzn`WaQ40{zch0$-!=AF`YK}9#W$U=vjwAfcrSfsnrdOcY;lbI<5|AD z7M|<5eyF3z{!r<}sG9pGE5O?Npd}AhT@igp{%s&OGUEuBy5rd$+5k2^|(8&AJ~AE|bKY_VDR_u@~sSKFL5eW?-V}sCbfS;RoC`iGpvt2Rfu!4a+~##W2btGxWklV0*MX# z>=t~S^Ss|?9n-t(rXm^tov-imh+onZx8=S@)r4nD@1*?`RobsSyS7dG=k$(}K&yjX z{QG{1Jk`tFd9c4w-GS$cy84Y<-nFGIue$BpBh6<^JUn+nVzGN#+oNM0!8HxngXUis zDl1rjKHGew+n#+3|8LkWUa;2rW{i0C;Yv5Q4^FjvCbYX-FztK$aLt#3LpNnMzI*iB zGvJ|Jh=lzE_gl+Pv)g}Na4N+)*V$%veV_Er)#n!m6iU_mF!X$9e4O#9;oI^U>2sdN z*Z1Dx|9D_+{DhE8O^qra<61mF^PJ69`OgX}Z`Rw^e^+8*V5rC2l0zF30o7c2sl}Nn zV@9<>f%%sLME?0XY6aXpcC2K@?BcsRIcwgXWqI7(-5ovaXT!lCE_!#*h6P10Nj>&g ztM=l8PWuD$sW#gcq+aAqzCEvc-}{>9HQ)dJ`TCZeNSEXYVf>Csl593q=Q@Bm6EDQK{xhsoO|mW@3Lp&g|~|n z9~_I5kTJ>r?;vu%cXrF0#EN5~;xhUZmLD#UwXD$HwW@QfiKwiVqWnKj&evgQIoY3o zD|xNIO?18~?~_1{M`~RyufAHyZs)jt?GA^gtn`V5g$+VtJ73P^ES?y~=Vy~>-2 z&rhyYX|E93*b};J*2h+0i4qdi+ zMKwv)=e{uMn{j4epHsZ{?1JT7UwS5N%*thsHkhh>yX9_6lG^DhNo7is$BQl)E`L|< z^K;&Y#J}&~ggkZMaOLIIiZ9oG7=>M)WfYs*)MtC>(VGQU-!{+AIM7yQvQI!VV{uW^?g8o{2udY;3+dvr0zzj_xRo2t#)iohUMUS7q<6E|Q z_Us8PP7($J39)9gTT(usQ(Le=;Q~vm%cTx4^OLGNd1Z&~Shi$+49s3TEiB4AK5Ffw zXa_ULN6f2^c5U6dHYz%EEC1@PSFc^O%gKHF{?1O$^}adw^8a5vf1GY#e(w99b9Z(c z7oR_K$M(zn+J1qF^Z%)yxGLv+tg7ee!VQWUN!3acFL{quS)1`oeBAkQUV??jiGADb zAC{Xd-jRxz?|0KWn^b6WP2R_3b%ANSWR}~4GL~0v3+h;;+!hqF%#x7#GhflUd%;XB9)rRM-!-?H8MS)*vdC;QoSoKIjTRDd|h?7&T;aGp05YFo2NcZShD`p znRj1z2>1Fem({irKqfrZ^fOdwJkfz>T*HEN7hEX6$dj zyL4jog)<-Bj(@#4G3b-!N$=FAPkrx~HEVq^>g7F|AhCYQh3ZEZUgs~aoU|&&WV*qX zImvOo%~=KI-Woz)XT-n6&Dw2me8&hcQ$3& zC04FGmv4$*ec9>hykmKD{ko^@o^iKo?lg^Z&E33KwJtl?zA<~XC1z*(rrS#{b3I)r zk`n!R{iT)8KhtO1{kppHPyZ}CtFpdN;j`_k%I1AKZ(dvSHu==?WkEGp!Zz`^?iS=` zJ-o2HX-3?%r!&aj?wTefGxvi;{AUzcnOY@6(~ zB`)CFt%xo2Uvteezjm+i%i-`(c9-XrWwx!n->TR$h-7Z;1|xhMWV6mF0;6XznZfA#bm3e+P_4uEr|-swVC0% zF~a280{{MvB4V`V=sO+=1M`}X;)eC zuQ3Hq(!DVW|F=p0)7t!HcIwv1Yco{yv@?2>ELJyFJipPnCU@8S{dZnPTUq@%D=T|- zM^?erN2?Bp_OE6PS6-(U^G{1}$FljWf9=#e*ZEVTbF=LgaZa^KhBjCC8H#yHyjo)U z%|%nTSk3F+^ED2A_w_Qa{g&0#I{Uuh65p=klFK&QX*KXL^xn za^s2Z$7f~9&b!R~Ee^>qJ{~*cT|Rfw^310@US+A;9!&pN&f;rp)G_s>htidNom*Fu zc|P>=S*e^be%5hIcw@kl{6r&>ccvTi_IHOq2+gfO@9KZ~yP=8k^Gz?d+po%R#zvjx;&M8@$4Q8w+oEN9qo_(e$|)vsza|=rN>TLEn`#p z%Zs}gE?si^vf)LbZz>J)OTm}`)+Ss zvtw3w{LUAezf7Mg9V{+X`PpCd;A^G*joySetZ~!I-W`v!zQ5#l%#Ee-;wp>wY@b`O z`sRa(>gZX`%Nx{+n(GCcHk;f%c6ZLR;=j&XQ`bFO@28`*+j^Ey0h@TQVfu-+WnZ_e zTmH*i8I|`lVRJ9PTy3UG(h)yy_Q!>GMO>G^U3RQLK1<4SChODr7mV5e3jhD$B(ZHl zxu*I0hW+&&3C6nF+t(c46gjOl>RRf}oYSwSo!ywTE>%6k?EI#mIUhw1T6#(ODxInj zvzdS5o6pTpUCbd{?v^b`vKM7FN;#(B%bjxS40FhyyJf4--0?rkxO)r#r89p`tREzN zV%d~3O>)0STHzhdxnXyXKH*F`onySuDXlnW`YzG)iJw@CQrAg-KNZvGKS6j+h1_KI z3HqNc*X>pRY#FD(z2=9Dnt#HiIVs2HPoHcNr@_7UM~a&N0uQm7AFERZk1s!=85Uzy z$rSuF#JFzhoKqh&i)M-1cb*j1n07R+W~3gr$3chKS4CC=3Smv`f=eEZ>pNU_(fJuC@Syxobps5G+#WOKR9=KiO2La z6Dx}r&CywE<9Tkv<|ijFX`g;_M=>i$TYgDe-fAv``3LzU4g~O8%w5P7S|QU#YDMSXDVetlrJ?mG7PoI>UwLfz5_r$^plxtCPPP}6$!?1R-hd5rP7A8kIE zCY-r;E^*hk)kz)e>c9MF*>K5J%Vj5z_M9VpxvO_L-&T8fB~Pq8^Lxkct$QEIy)7(U z|E*_-zqQW0>v6)?Telz8lkU807Hg9*Tj|@4J4&gmg_^gC6}G<(+1R|V@mt=(x~9KP zcHYqs4Qu=C{GDd2-6>&S;Z@w7xrO)1buAahTE>vISGm<|1w$6kRqDuNxtituBL9nO zz}#(3_m;d}Q1w=(s&yYv)`hAU2DPkLRZ zeY#Ejl;rwLx&GDRZ9>mh&uN~pvV~Raqr#1;1-nk!aqy|mSr{bXG)-vQN}HB|2|-@7 zPq~G$O)Z*s$}_BO%B6Kilc(@>KSzmzHjtGPTyrGW@BQ_F=Ec#YWSnwokdV z_EQ=Ew5Q6=U)DNWS-AKsHP3$~wQTwBwih#-O&1G5@oLf>ijZUdC}NF5~cn;|pY4YK3koX81SHZ~n&gruzb)b2aB5tu5CR?>Br?+p_(@ zdxvWAZ?+rx<0tZS&93lx(v=v zgD38nx~{f-eUANAzDw`R`t;JQS54UQ)noDxjSaI{{?5Cn(723i!<>*{#?_j;*XK<5 zXAx)p?PA%REXT*;?)i~#W4f&(zFm2#?SGMVUzbSkhm$=bM{A9YT|MV-S)Q@j-zsR! zP4&$|B^&*%f=fF6cLkNq^iNx!(e3|AbJrpD;-HeL{?C@Z$eKTA!lLXN=P%1+AD65t z-jY1`OL~>`N1k=bd~fuZt$X%i{SDs_;o=(w_B#Jx@aI2x;4UC=xT^4v-#fehe*8Yh8OPvP_(6Y8@TWr{f&HZH@EVh=-bO>r9p1 zpSRoYKdw?@>|tH~TK}@NVOwDSj0mk3IhN)L3dUcSx1KB4Udm(6zokbx?@s8F8KQT^ zRM)MM3e9^P{)IWpV~4}cl%7|O3&nUYr%9Atk;^bn$-5O!OpUlz%dR z)Q%;*UXKEdTPFuAZ)`ZZb5lPQGi)7|(k#Mux7O`ou z^;_1I8UM;7Rqk*<6w*5#aiF#>z%@U2f#Ujo3eI95FT7OPbfsVK`3vRctK_&=%$g^q zk-sbBIlo6lp>DZtSwMzF+Y*d)HmPgU)fNe}ns zD{>*Oao68;dDaT#HBa2|HfO%QdSZK>Cb!Dnc9XJ~?7|GiF^dcxRv+K;e(}!l##QQm zf-4h$DC^d8T)g4ycz{7ILuOrjBFo~2-)3!vzs}9cGCdF{ykY*cjH2{!>T35F=YL~Y z%ia;?{OEAFO1x{{yo7(%!uC!^4F-lTFNE3+JswMjZ#gM?H1`gpbcEu%-Ms7Ec^@8} znZnEO@w4*gU)_L%Y9SjqpQ`UNTM?{tb;FF%PZ|CpmcdveeCx<AzfhMDg~_GerU&OT0Pz zVdH}=%`ZF7OS*;I#A`{~rg(&|+Rbx#^)JzbpB8ecd@pwu@LC&}W0ROLo2U3;-HNHR zTjVA_E?9c?b%f=_dpWuJmc1r!xe*@e6C9@ z^eorLkm?eJ>r=dPj?Uf2F;7}a+U!{GK2B-z3C88(l3dKcF70sP{UVhdVZZdw&Mg0n z$&b0#^*uE5GI{r+mq+p$d%CejL{`hitgdD)pP ztx2*E?Xubf5A4m*4fAB&bj3-mo6YjltDo2I1$eQnw&Qnp{H)Ex)17EEQDN(CuQfXN zrCT%aoHSo?)z#pRYnPd$AL}RA#~n|+>K3r4&oI#J3YoZ1)PUu))XAD7Hxi4Y4V+*9 z-{Td)eB!EI;wHbuSKRBCerODG)(G0d!SUyq&e4@}BG(P)3OqGvmQMJY?ozSY|3`xX zTk6$U^B>MRnmqSI#0+zrFM5x|=BLj#(A+icmA&CN-mBA(ulT>>YQI5bw8f#2w-Vm% z=@X{=o$x+c@xor|phRKbr2W40b)FhMdB(<74_*<(YWwz2783(Q84JFuKn}Gk2u7aB zgp6FY24@FLd5XL}{>6d2+03hh&G+mF0~Ido*UYYK*Cz7lNNi*NdsTgYXYH=l=i}@De^h61VD}a{V|uLo^19d( zHZzuP-v0h|KfnC?^Y2=jao%)(3zmaTm$m!%JgTsIBFCa1zvTXo+H#vyc0Tt5gyNlr z*#&euj%>VJ+E(-7#Q8t`|D}JdY2R;n`C>tpz?!|47pBa;WzM|&b*j9!^1Hjfxs9rG ztIwAy{)%loth>U|#bixDUqHrQxi_tPrQ2idKc?${W?Zy_XHo2X=YMlnvTA&fJ65+g zPI1S%1qrMYo_<+UQnVopm|KsZFi`V6kYtBz%s;YdrgrTKrQgSkrE->ax*pl< zr+KYgt>#~!>y3OBU9&P{az|k~yy~VQ%gW|@FZ>?Jy7=K?^;6GNe zq&1a8#DHHQAt6vg-cuk!fl(ohDP`h8hI5~iexz9|Ej!@M@x60_-i+Vupn=5eXYc>8 zWoBS7#hVcc4kQ{tvg6tC?2wS_qW{iu7@w(dm2kDtIPs$6u!5tABb&*`NYjSS1=DUF z;XA{6oX6P0_=x+hEoxiO-QCC6?p-)M9sK=k%8lb63I{(IT4@@;CFY3~1T z{^9$J&ren0S#mZiRy+He?ep9B?b7Ghp1YTR-&+6w@1NcbuRe4&ELS$1@y5{HJ$c!I zIejG-a-8bQbKBX^T=88wuRurV!K=6a*MGi#wKCuLt-tD{tznxjC%)ldK6`6j>(cOy zrxL5=omU?VI4qGRB*pSbS+~)? zrhJE)Sl{=ndGGI?pJ}2M_{x#dcb%CvlwPu+II(A_*b?sM6=!jhwDs@J3=79ZW}6_ffvOm|&yQgb73)SVSa+?Z!?{UN8i&TKq zZsvid7uk89WM0WVur}~WgZtt*W@FP^=?5-k*k2AykDXnV8*DVwW0$~erRcQ`$8BG@ zJH@W64O~|0`udCeBCQ^~rbn874Ti?ALl$p37PE3z)zuYi45dRZuaKH*nl`y%yQyP) zd8?#yob&Fmt$)j(u3Nf!^SYgzi&^JSt$Fox&7;c)mQOsNW#@Y~`{l&l%D2S%m$Oga z^44ZX&obZG;QSSHLxb~HJjz|AB>cXv(KSEZru3a`@#DomW*pG5{k3h0q+xaDL*Z2! z_YXuKwtsp|{o_l?8uy(qtF0FD*XM3yGSVyBz1G%mrz-ROgXy76^8(uDt}0xq*8hI1 z?PT5EA2?3mJeX3E(0(&}uNg}eQzTnjWqpi&!`(Ty32tj{#{4&|xp%mC?i204njW^< zOq12iXRvR&Ip>StFQt7?9Cv$&o$W^_shKEDSehD{cPIW&ToYN)v?!x^Ok*^M=oYsA>@?zB# z>FCFW#yb_o{Fa6Y>YsYm%e_D0UtE1Ur-)zF{nq%;i|f_R)jvq@;NJ7fmVd+ft<}r# z?tWu;{@vZO_ZxrDEX!XpZ*!{`qlJO=jEjCUEHdh=ZfCDMP<^RzOPa{NfW9*)6XG<& zpT#+z^mOk(BkSsULUXR9j)ZIFiKP?tr>K9{sr7iKxi@-O?p}Ar_>5(9GGB5(^;v4Z zB=L2+<`eFxr)!pq=bxxqI$t;a_AjQ^nuSi0887uuJ1zb0wSMjV#S`>R!q08}(tm2F zSG-Dh&R?6>nuSS`IWK>wZoD-8giy4;=%-e%)Rjx`Yj{ts+cziwwYbI;?x`n@yzS$s zY1Z!4+5VOLeBw%>nuT5K#1r346AhnOFI7KPRPxH4BU=TplzC47aw2k*!?}r{mrUV) zawu_4PV_X#=1mWjGlCKX9lM#jIrG8}_-}}7RArehvO?v?q60xq(V`pzTeJg~PP1y~ zd&xcT_&c6&8hbi^@JZ{d-OEn4FT8QgyQ4ZM#xv(ug=XRWUgjf#(&2*lUC;Q=k#(#O zKWo6I_y0eG@zqAAZMzv&2D#iT*MNE${el@wn4(Y~isb!lpsJ zu;YZj+{5q#iHlSp71s63D6SG&UdZ!ZKvy*VqvO9ze=e6cmf5?6wDw2T)z2vJDUH5w z_%ZsP%h$I>Yj@?|4DIPTaJr%PkBr<0x&0lV)=ApLP8GGP<1Z_Dnz6`}eWJ@cwWtym zPYct+&3x*d)77rMo-ygfbY`v9iLS~Bmp85q- zD(wEX6+QAQV*GaX$+U^9Rr6G?AKBM>N2+MMK)&Mk-dgFMWkuVC%O~DG{Linb`edo_ zdv)>T`3mL^dagD6JF9QXtCq{}Og?h_#4$g+yh-s5 z-!-0xeGAS=9=f;SjI5J|#$4wYoL<)j&MY`1?Gzudt6hNC>%71_F22W4)0ao?%FKDG z_D8~gq1|z*#WDX^9(^7BOS10VJ?FcAe}wisJ%p?ji+D79dN&6H!*wC#UIzL~F@lX7 zGf3m+?exl;kgKA{&(}^qqcVBAsM8Jw#RCU?)HqM3oKHy<){#C3%Lf5nCM=oqaGiWQ$VX*4d>izm?ys<-D!3t^UW) zFP}5*f1a_u|JnNc-SYRf&*y#rbiv*Fy_(W|578$%{a@U7_qZOMpqLR;Z6olG&tUc(L08Yqb-PGvD$Z`^EHe9dFh&?<8&Jg>{QJ z?zkG`Q|VqGf8xW{gj<^2N_QiQ%CZe|wtW4*&Ule*W9c5wpYHtz`ck|(g#nD~Iwcp% zIU3s>K3wYRq_|jH&Sif>et!KNca41BV~={RJZC*w(rFSop}Oas-inDEvSdwIzu&m< z_7kU6e{~Me97zp+)HynHN5_WN;{Jt33)j)rJyJb#gCm0>2gy3nm&p~ZdD zd&@G}qsw1~INGUC4PLt7jIvyB^QywqU;~++?Hm(lKY8^(b!o*?zpWw9mq+PminDNm7BQTYx&MobC2h3_2ZsWZL{0dN;l|T`i-?$GdC7Z-w?e#cy-bA zP0{Nw8$F$OB(K>|d&+IS6WgwZFT8y6RD9;I8P_#Wy*H`#$=UkJKKs{R*Hc!%(6)N2xl87Gxa|I=d)BmG%uso?K-=ANTiU1EtDd)CXst7nZnC)V zx;8mqeR|Z^TGqvHwr+sD3=e&1g-gNjw=!~`JbF|NHJC(Eg ztl0IZTirISn9BHsA!}Ob4d?&QPA^=!`0(``7Xv%vMeDBqxcVTxJ6^W#>krch&sRTa ztNyiPb^q^QJB;J>bt6Kmstal@1m5>Qy7tZYV#1vO{T0gbE=L#Vd0kZBlQiLny_m`0 z$@g5ezSqz4vtM}IE$gjk;Nsga4suxk;H_v$E8DL;x6X}I+D>Sb;m-;e+XK^5j{exN zeE+GKCo8-!z4ZHKDc>;HX2V=*cJs_TONt*qQEQf-JKHC3(}%kof8F?P^&-6Ki|<8; z`&^5U*En-$eNuTj<4LIP9^2IgrSmW5#`WH`b=Y5)bdF;f)>vsp#o-W_)>lyA;B zzUQC(vilYrpZ9YVsY*Uw@$Oa1-)Z~B>(nzYEp%P;g8z8)-eZT(#=6PJzgTdedzR^| zu3L{UCRF_W^Dkk=?E{won9nu4_wK#!S^a`j=d8~jlPa$BQ^V!oJ*)lQzwK?u?1ZJ~ zFKo8?Q9NVEF11x^v3B;GZgwQH$`;6M;Xj`pn^NqaQfQI4#(YteT7{T=oVu>8@;(oJ z%a^RT`kg~ka^4;O+xyO>iuI_4TwuBVFJ{k`JHCWk9hhACb_G}3(W`NL-Yq%)lJ|}4 zobQh=a9VsRl=xJ`rJDFHex!`vZtY9*&(G*dq)JaLe*0?vs?EprmS#O!=C8>8 z^Xd|PzV_Z1O#upGkEaLKT{>wsA%2S4A1U!E)n~rQ9CEW$@w4Qd=ycPv^3P2dmf2a6 zyHjPS`EJhHl{$CY*$r#*Zu*=~i!{5v$#UA+tvPbhcYF)@CcFJCSl0Ypc|Wf-&Jpa^U1gVlQYw5mis)e z*yXeRk)PUjd*`$nA9IbGAMd)dwnA<4?fI&k&-`GUdGuq>%-o~JGw%G0nVEg;`H7gm z=O=Bn?fOo>mDfK$`y*$O&GFBv`$Xp-c=ApB1XEvj{wbzD|0%+2ccc{kRzK;P_NUIW z^YP@TDcW^1lW*I*`WZgHeo9kDe@bgujn?Gb@haVCDni5xYi7+!JG$I>=U?L|JjVMX z&OO-Sy=%=6AGPiE)8?ce>wcmGo=mNtapzxBc>vW#&2(MaO!8!SMyq5RbAB#6l zd7QfGOxO9eqTi=ecS`OLoL0QYDJ-J!KA%?d@we;W)HHkv-Be_rdROPsrlN*b+qy3Z zTrKfxFFEBiQ~CHw&Y)9|pM+S(o|_^Usg&-0U%^yIS9XD#+*&S!_JvXg^B1yhn6OZ+ zfXP|*flFJJgS0bqf@QP(g@YG3B92~Qv6#A0azoof)&i@;G7jR0V;uMo+su2z_iN6Y zPxl-rGIkk;H~-dM$C$rqhuEH@kE}l26uaJhe(Mg#ntey>KCp_JH|cNM!MW$^qqGl4 z#kM!-Z>-?^5!C&>Nq=*N+z&2;Z6CHN-ntfP<`I7X^L@4zB2vB%x7)ZJllsk6zGZ#y zP~Xyibn-3XN1D0&E265SZ5G!m%-d>tqD=&n=$h2900xA=C=&tp1!wJoC~kL~PY`B~M4+^^K^ z)I#oFDO)7uR2|*Bx_6vS1T`yu^GThM-qRg3*UTo}PF72~{g!p`6a)9Asx2X^ zXTo=K9$eSbs^zF?F{PmCR1im|mqp-Gt(HCU8cTgub+pShQ+IkrhrJ8)YWIo@dlz_0 zYwDpjZ-P#()S4K%Gh8z}#A@>=@1=9rO`0|J_moRZgJw_tJ@wLJp_&!7;&0^^w6ndq z=vc+G@?XVsfwwPZmZ*Q8C%54FXSVlCrr-Q0c$Ps{W{J7`CciBgjJB+w7%Q`c>7$h| zlfM}I3IQALS?t?bqBOS@707O3yP=kG_QJC!|AuWWWtv;eGEN_ydm-%bJhoijEu{~7 zFF0)w%&5JP^kDPCx$M=9TeuhIvc|K$^}4ZoK`ifEsSl+W@*4IE-txVW$9SLbE#D2t zjB+Pwru*!<=5LxW$TfZAsNmnCo;bhpo6eTn2jL54TWW=GMPInbxS##4^NoI1yDLW- z6NT4z^ArZ89DL?Z4`0m_ zV^imgKkU8pPwK6bT(JL+SrEgU7fz2hIy*mVJ|ZRbCH$8(Cy#00zdYWj+uJr(d$YZe zPwq{Ymps0Xi{W3^OWW)@xi6ZH8fA2R1CB&bTTr*&$Mb^PdLQ2lV(Yi4zsOl%qVghU zy^r^WpK&wiE#TgBY373NJx0oo(R+jZFC2}VIdg&c9wTkX=~by^O?sbC&0&dsD7)l) zzI<|3w%fApZFURz1^pk|J^S=8^Hr9*{i6P#_Y03ywF=DmvL8J6y1ij(#!fB<24e|) zO<`ly0eAEnBSXlBLeS(y zfvWS8oXTy-!b_WtJzk$P3O}3a9?Uy+;qC>JyH@V*=4mWhcId*+q_+Y(&kCZ$d%3pD z-Zk2O?F4Uh`i!D)vZq5@GA$;hUfpsg_WYD|z4olVc`~OqE0%b_aMI{8P~wT*^mON) zG!N0+J5L(3gkGMKF4^{vd78KM?Do=sQRcCYADaz)mURDq)woJx)z2`^(C<5+Ju^N# zU!tmO*Gcm|QB!Zv&bsMi%1rMqdUikh$ zU3D}-wq5CdtISc0cY)uypO?PNpSq!|<+q3X1gAE|dq)&oWHM8)Z_BEyY5Q5SQFUU? zM#n`V<(_71UnwMVT@LhdQe3;Ude`9vTc$csUbSLM&#QSEp^sfIF8U!lb&I#CSZZ2? z$M);Dw<>-9tyn6Vdf2$fTuis%_f_j&?k+`}ug2FU56V1~bd&m$d%onWRQR-2ec7vL z?S3_LR==9&Q)P`GClYpEY`8QlvnYB>cd(w)^xuor_IS-*=%3@heafa6{(txS8@4>X z!Q;7X!mi-vmE!}A4fg$`RJeKDD~{lu?^ zjc?Mw)E<0$ho5J@;#S|w7lVBa<|tHmu`|}F1h5OG?0ymcfLU&`Y{nlS^$TkXY}?u^ zTweEV`ufDirPF}1O1;=#(Dv}1Z=7QL(ui55OfJa2wB;aMJX7@KFOz0W5(tV~;jHI3ankmKJZTp;l?l_$;t~HMn@tAr_n(0{42G^}QOPs>1q_ukP9Qa@OwXq>W zSi^01-~;QEmr6r4JnTPgx2{{b_R;ag4f3K{YUgfBw9Ry6o8}Pp?tQgBtJczE>ue_U zssG(3yeij!Ytr+DUFA({t9DLgD4JLL&DJzte8-J~+O79)-VS+aymRHm{;$XL?4qt8 z43XGxQW%)fo4mz7?eG!% zeQ}nAuz_S!>^s5dOM~{WPm|eIH~X0Ij4Q|Fxq0-zNX&oP{xPF?`ij@hlRmZeuKsc4 zkj1)*XTF844G`WDlr`s|``$l?j-~I&oA_5#O2%XT;v5mfO1Vg%%||}=NgGx@Ztgl6 zwPbtj(_BuUd9n7}*mq|>^6}a8-&5f}$BEl*DcKvB?A>Me@4>bRi6^B2n@&W`Tz30g z>K)FPb-zR=oo-f=Hq*;EmB+;+d&T4$qio;N-w%Cijy6@D%0F&>l4I?y9lh3K``E6! zdpFBm|2if1^)k`2KR50MpSzSM(!NP_eaW9`?@awJEYjPR_)zbip@ZU!BU&O7mPh4Z zUw*^4dGD*=T0XxbHy+r@7%Qy(rO<2Q&8?nkc@In`A1trGcGPg=thEy-?-q!7Y`t*# ztoc(-QZ*%}EcvP5uJ|I}>NESf{eFFxXVs4W`o$~y_1~6R=jD>V=x*ZAUvvIn^XdBJ zKb2nv!oO6v_{S&Ked`OHY0ww&gEjeA**E_)95VNx-{1UWZe05Z!RuT)>#pzg+jyMY zM)apoBH!g1&tIy2|2^ZNFkjo`7t&IKUK;8Z^Mis?t;F^n=RI>Nd*W{yf42P#txi3^ zseUGN-}F!BQ(i1Fp86{K34h^hmQx0y7r2dYg zN%5<;&tuV^EBPYbcFSGYg-h^0R_Na>_WB0ZO4I&>e zeEX&Pj8EFVd+~9MF^?`d9X!<7s^naBE>uC{)6-LTTh3R87QPMneQffrm4;8&Z7Jh5 zzjU?K@L8h~PlAW1`);|LkDi`V@blz6U6XjW&^n+?@oqVDzRJ3#N|Eoj-OntyEmyPI zyo2Mlfk<_uS&^3968&~B9qykz>e_oh$S=Aj?DoQ7(K)aG9=p~}TFlD1_FnDnylqlE z%P&sbWzDrLMaX}vy|($+%^#jjf1(kURD52r?Z@VfeG6S8j>er~=?q%iR(pA+vB;lJ z|2NOKTB)|ZG4f5f5LA}>TbT0f=?kwOF~P;c?^Na`cqmA&)Bf{w14qQ+U*-l5vl@Ei z8D<)b9NW;TTXkOZpxH&vMxCM?iW$@W>{{dwvV7a{A3Tr0!Rp6RKSlQc*h}7vvsky4 z36{0S`uN_iztX;pzxIW5y+eJ-<;be)6`tyr#n0yaJZElfdB5iOzo*&^DT-|ma%Vga zbiaM|x74AW(t?}Ut#{?-M#yemuhemPV`lcL?9!G4>z9eJOiwD4G)eiYbNPhNGW$?( zxdP>iZ~WT++1B116L0mdcx2+!sL{l}=kkH()2G8ObJ+d8W8i-4@x;nZMU}ANttVBD zWH;ZxZnf>4#g~H#Iji69o3`@hw6hgy3D;K6b3XB3et+!imEl>-)=NE~*=BpG_jA(9 z6ZdW;u&@4Bab%w9)dGzOrib73cTN0uYT@ZW_kMDwmOA~ubxr5U_B2ynr6RT;8S38k=E4@|SMQ`!%8r@&d9<(?|E>Vedyr;i;lf}`>_~2<%Q=QLV z{q*waqPbtfHJ^+9Ji5wn#p$#67b+c{yry!8+9Z}w;-@~AEb-a9xv6CDp8LD5Up>9# z-22KMC0;UW8u{;c?w)9JUU$jSv~P=M%4#fiD0*yRuD0-*jreg1{gnrsC;xu@y|l1{ zt6tsV&tcV9?_|a9sTy-@zFs3!JJG?(#;GrUWrC85*C`{>s6Wxe~w zZSB(UGWXn0rhVsBV_DSJ6ETtF^jp7~zuvJONmFlJw@WOpYG1w1hqY@fLwQBlx+Pji zE_=w)nYdxj(x&X=-j9|a^|qA}HCy5|V|`I!W%)tvE5(I6$4j3kx-MmJI>YKQK`S~u z-Tigt#;IF1lsw#%4_{-yWqkKicyGq$}lnV+dqG5L>;g5sIKp@$N8AHVgw(e<06 zm;%?i2M$FooNG?%a2dSkIj{bqmhFGLr-J3JMUUL(49x2_Nh69H7>8W^Qh2Nn%n?YH)s0Noq=P zDr}h}baB?wkXXp#tj5&qTDtK}O&U58T$`@=Z&}K8RL%72v7CpsxB51u?C$>Uc*OqE zeSr|48Uvf(i+jqqZ#B%+RQZ;;tNQ%T=jY~Cr`P@c_fgn^>o!kU-fzFqC%nN1Jg?=p zXWdyjwV%Ir_oJ|#Cned8LN9ON(LXKVsq@b0%d-6n&t-fEG($c8$OY!7{ zg$7@4`uX}A9_fCVw`EU$Yt>4V#6KS{*rc6MnU%0L)bH}q-(QldcZ7y(TRR0rPMW!U z&Qh-qLyz^|x2=y(*E;-1bjsBd5od{Mg2y=yCtTccM0$Umpr2)eVb9;km&M9^vkoh9 znoAz*o_jE$$my7c+TxB!RSB0wntjwbmn+Wp*kbwhI{T;ccx~2@!?%=I-kuTY@ruv+ zArrf`TG@uJS6zelHb%DYtL#hsRy0}V%w?O(-)*Ni1pe87a$(pe{wLYb{Szg$Zn3;6 zl8W-mFH^k#^R#iQ?cBYW7))neVZM7~^N#1%D!V3V$bC>=H}lS|%U)BNPpPimCUDqe z`7NO>2aY(C^auo^XfO-hXy{St6kiK^6yAqY)a)+PcOPIdga#1?#U{97ARM=ew!4% zkiE@Uja9t=qu~S38(m9JtTZ)va-q5N!6$|!=Yu~NM6a8^fn)6!^QvxPgd`h;Xd;pzLUICtW%@5fIWH3x5-jU^EbGhPwr*8-F*Y;7|IFYzJwMms0uuv+GD@9}o`bB2 z$U!PbrihMdQEFmJF|1?S3+D z(lPb_rxMuWBK&oBr~Rpl^yrBmin*6gzPn?b{?7LKy}w_7pEhUEv`PQ)-1oZHYUx?) zjJ`aP%?sHPsws0dr23X#@m%8)-KnK#FU%2b`*tH_;zH%*Fb>&Gq27JdPBJo?TbCd{4anyW{;-C}e36q@8D6TcX-6wg|ixAW?xEYS+j zaAuANZ7bH!Pu$bmrNR^ByF1il0fb9-XX?OS=c)>l$hg=t6Ug84F)2QHM&5YjZYd>5T%Qm1n*NV)5! z#NXqapT7F#9c;W}>h=56uFkShOf+*?eEs2ZnP)3k?@{!QTK6y`)O2UmCfV-kb9t|S z_z^55_j9AhgtQME^!zkT1~e*U7B$m0N;MG9&Lo*y;&Wx4k%J*s$oFZGsS zhUL5;jlY-A*w+_iw|vUPBhFpMMv2QDr{)H@3fo`&D_Wd?Zo>!Lc|Up@e@M#7#~+rq zecoI3=z@Asg3Uhldv&w^306rjY6t%n|sh8~O zCCls=*spq7-G1+^%H>0MYV)R_w>y8&_Vm5?_Q&^kcqsNxTz?@$Qnb{0_3EBBF5iXW zt%16Af7i^~oaTCkRe9gk_}eGfwoCLrJyW}Rq5GT5T?SjbblWcV{4$!lYgzVL9cBND zRYz7-Zw+)kaq*n8#aF{uXKyI~d9pZ8zH;UKiQ5xHLY=3qz1ecuvYKsT@3gqgEZ$g8 z|GD2}I-bR;#+t_Z;=)~z7O%eYUY)4B4@xFq^l)z#p2lqdRi!X!&O56jFC}$LU+bbEC3TCtiBCNBPLo z(w8aEEOK}Jn&T3_VUn3_Y~+NO5eAmw{wGUxX6{+7o@ym_mOI@4M2XI9+3^0;B|3A$ z*U3$Ox#E>{`1H_F$I$z`cdc2)wMr`{HY}7ibnPyyRZ=9i-#{tF|FmdS9~w{lm9K-V(yZOyK)janRhw7=VUEQ@5}hXE)^?s z(C^Tr_ij^{Pcx|5aVH&;s+nN4)3@VY$agb_^!>b zIdR)ejbhukzYle<*r9Db*Wyub{0?79x01A$^4olj{=(5fgv@v%v%juD>6*kjTNPnkiB*OM(XP&UpL6y>!b(B6)tY>(3_N|E$}Dwzz0`f@+0U;#ojaF5Gsoob`cq+Vep^Rr86BLwecp>%%9$ZKQw-<0ikh~~luH#@ z?NQe9dxNYSx(Nn+3^3%Lm*CW* z%*33`s?;J#L0=mZoiF7sa?g14=FpExKeM zXB^~?YdUyGXyUTa`f&MGHs9StWv$oVYF}kMt9VKA&vTZGXTJY#U-#E{l8W;z_LI@_ zKJL2DPrS8MID2*O#O&{P&Y#n;&bw(Vt>XXJ@%mjpkMsWWxjQFberLJX^Ig9G{dqp_ z=j1$=SkAUcJ`gXea4x`@(fGdJ<7-j3BR`67?)i@EQI3UnsYIW zIefga2#rvlva+n+Vw$2`uWw_Px44Fa#j#O^(6RalR?_m zh@ZDUO!^e?_*vF#n_p`rlr*w(f^Tnn(77V`M$dbb=b0-r3!jz-?(Dp)n^SASsaIWM z{9@@1zTz!6a@XkGuj)G>e4#CI$#jQf#<9lN{Vx4}oSW0ewCC3a_0K|5*DRuDPFS&D zcuPT6|HLe=Cn0~2W*R>Ex^UBr&80pCUz-Ybqh9R#wQk#^OecZ(Gqb}aE@~fibt!{eD|2r_PB+m1=$6~eUJKMFN?b+yHCjGY6{ONL+pCL}3KNv4q8^?V$ zu!aGjg-=W^jn8wEq_$Q*TpXP| zOJD7o?s|9Mi2jKjzH?1`&P$y&xo%js+tXBc^N#Cj6CYm5c4%~6m7gNJ&OoH?)>iuuozQywFwG0~}hbJ2HYFn}#Cb~W{+|O7llelr`>pxHQ=X|p= zEoca z`TZOl+p86iS^1_+4=e6Gl-qJhciV^E?d1YiPOZCNH?QKYmYKRnONVRk+jQTL|CZhL zO!=(!{^QQ&0gr#52w#_99}$t7a-({6@uE$UoyNVvso&PDtX*FBmo=F;^24W{Vh_$d za5-l8P|f@8rcEEUD%RdN*E+p==j*jEe?Mlvt+!95W62Mp`^$AM$M$B6J#cZmo-^}e zvDfV7O7^)g64u?_-!0dWr)^(6C&^iu>8S4ia|$!}Z9gRaQh-%6ZuU3zcXK}{qXtbfD``dV|(a zmSh{Z#wCCGj}^UMI7y4u@zdYa3O80|*KaAd-F^S$T+`i66GJ*VgFbsFzPvq2RY0h- zxAm9{N2ttv1N$8(B7#)<4O(1V!k@5;XtlP?oBVP2P-ZrH&&NihA z_(my9NQrYcJhwa~T(r*5<5{n_2b1C)hZ7ELvwVb-gqRYAHtgAUuw}dFLc!=gC-1a$ zPM%qizRV%|_p*@O+_kK0Bciuv-4B(T?Wya;oV${#^yR17_6Ou2u-pGQ__X-lP8JWf zm1~|x?b`Hg&U2gJb85fYRnM=@TleSnUv7q3A6Ggk%sO?8`P#XPLqZWEXJ?$8k$Jqz zzbESD)|ZUNiCh)z?nY-9J~KPD`|{zJA)8KSmMwcDXnc9$Q{BFph1WE<ThH2XYiIt$Z6Q5w z`fvKw*WSH3vF`Lc)8A92pI#Qt*_iveMO-yz@;0GnCey9gxAHC#;>=bMe!l0&2@kWy zCMKoFFVEy-PK^+8&=kA8!cwjM#Ii}dz9w;gw-Dks<<@y7cla4|TJRwsKGU}^<)3+J z#d1kHuxfpt@$$#ZqeZJus5qTv3)5iM3eXL(ZD-v5xO0NIcI6D)>TUNg?^RaWP^9<% zN{v)d^91?T8=@AZ?3x*ONAbbNzO=I}%0Y3~m-~0=ORaFWXo$MPEn)6{tlLfRU618(G$K+u#;~Lh*Aq6v@-&vKkN$mBN4Mz`D+1^>uwwBH8RiX>;b;%c3 z4*O=^NPFx14X>)X@iiUM( zMTIWjbSNwMY}M5jYc||sJ+&*?|cwcj=AAQu_0kI2%u``pA=g$?&$!-F#lFt?>&so1H&m(|@4!K7Y~I6{5vq z#joE#*vU})OYgqsCr+(95!S_pSFC3FKksROD&ogpvEaTi$J2{Oxx4(nh+pyk!rXXD z+V9PIHRF$h*~dRJW{2p0dDPC7o%U5@*2c#t7p}cop_KM9>aRya+}dk@ix~7D9xplT zsU=@`YJuka$%dtQ+e>q{=Q#P-x6Rjnx_`mLs@}D|dADX+qv@2zIV+vyCN!>p%n{iLy_6o7dcc$tGYDlb{AlkPlFihiT(9SDUy<3|mdPTlG!JWEvlK$k= zF+V>Ut_f^TShDK9umpSR)l`xFZTnBGl94+1;FaZ?x0B-}Cvc~9#;8p9(46kSVCizj zcC)Q@td6^xuCmB-v2dDcZacxE(;f%2am2E5i@*iTg~TO+bb;b@43Q^5PqL1Rj-UcqpN_=F}C&YTxU|yT=mY^j+4VHI?b+#8WNiLEqob-9K zi(-^<3wuXc@G>3SbI}LAR^_)^oZfqN`jPpss-^TE_ZDlUn#f5XKDkN#{Nh#la{Ym> zQS8rGX2dU4yH(oaA(FYsxxKT+U3hls8jlLojjrrHE&jr{ORsoLv0XcJcgY)(&$(yc zToSC&vg%APuCQx=@Be@&`DfC7%RN`j8|P2iJL&N1RR4YL+e80rn_2ewCj4;=IdVk{4XL>{b8D@~``^_%C11CX=^H zg(*K-IIY<#-)}~KaiaDsi4^av)4SZSvT<%{o*LOueo5V9f_C8BHHVxhA9ic&ZFJ3b zX^_>kX<$zkIdt;-^F+zh4<0O5QwqLT)O6isg&C*rlwQ}Q?s%T;nyY48CSUAw@7l=S zFLc+Zd9_4T?l<%5w+#!H@-qLPn&G87zdx~mwozWPMtnw+s_MeI3m-@*Zn>3ydd~5U z>m@>8OxY--y5|zZ)9Z^~&boPO!%@BcF>c>Zzc|KdE_FfKdDDW|PZOn6zC`}xKN*qu zQ+`5V%1oZ3$!?PFx|7vYrbMav3*K-qe(X^(>ww+dfJb|?Qp%MUUf|69cvB+m`K0`| zD&{R#>)MWHt*YZtG!N7|v*Gge-5M+AZTW0E|HPv&u6K?+PHcNyx%bIS#hQ)JmQ6Nw zx_iWCT_Ar{{$-o=S?phBrp>wUm_B==$o6wK&Y1~y!WqxBd1k!|@Ax>SVwrd7uU}2w zTg}4*q_2q@PgS^msAW})W=XF;zhv%>)Wcrh$`4JZ&JI~D_)27-tKz$$M=X~l=aeRk zJWtv2;F*Z!`SX$QY?A^%E|QkraU@{k)w>SEW^_G z86Lf#cL?2E6vMM|nU`$u#UGE0r?*bAv0x0eY{{2bJpp*~|v z%kHvem)E+?oc--@a#x-~%=epn4T^qlSTeJfzh{nktFLwZt&mdt!)-eo?yk((a{NL` zm2&FaBhPtG*N7GFFL*4qZwd1r0SV6d-5+_wMcLMFpH}~I_F=CDtqpmqE1uR`DQ;0; zr84=?)Zl5|b6ezBG^gz1*}JMJ#`DL_xQ3@wSPt{<@H+kDEXRZ;yMsJ5mAj25`K_Ap z<(lB6cjAiSDJh!MpEQ5m>&h6$m?Qh6m;2AN{jFWIG;=f#n5^1$Y-6R2>Ej#k{xI`4 zpU%6SF2g_Ztluwl|3ve{EZ_VVH>YJU-!T8d&Q`ryvyL2mups%IT}JkO{WtvQZ-}3} zDZg=Reb5d0(>MH`XEsFdOwSxyf= ziQFrB@GLh*Ql~my=>5H!tO>?~3@;XbbPQP_{#ww**p|KTfxypd6WjGwL6mFp{zB9i*R;7E}O`Tgu9wt8G4?J3)bZ^xst6WPBB377mo_l{b%qKYD? z&r_j<%*~!JW{E7kyF^1EX`wfh+J$9nbKCBEO5B(mnO?RtGy7+`Ay68bCu^S*YE#c`}{8d{TgWp zb~}L&{1K7X>%L2Q#wzT%d~;Ryv$uXR-dRjn?|xj8C^hpe(^K7twM?S7*RY(-lD%uT z<%!+hGM>|Sb}x2!cq0<=PNqc1_>tWw@m8l~5l zI-R0(UkQCUs%yMh{%ngCOIFf4i;4eA+V1RFxg=Nn?TNL|4u#$EQv15&PM^fe5VOT^ z{a;Otjhc02YLwMn8?_CamO5C^@KyhMZrz7B;f+TP4`2DcY31Ktwi@RzOBrj5m~c(8 zby~)&`|yV2+YMr#SRGxqPi}p3KbCy7j_4*B*?rpQfymewrzwH+to(j77mwPq$4= z`q}lK!(PCJ`%vAl#i0rlu5UeW^mRg0nN($X{rP)0zD+o==tht2rhT%4^4A_OE=ej^ zv=Vy|5yAPMdy#Ot$5rzSic4dcCnO6v>P-4rlrCTzp_6_wbN!RWj*jNZ#UbZeHbzQ) z&8wL+Z_mlf=%8X5*65}cu5GLN4!~^|nB}5{kdhS)+t756^-Yr=qAACXWv+Od@Edg`m zPM?!@PblwZU*x^gDq2Oq_x9Ts^rlTfn zd!`*f&enfn?#4yFlil*>E!q=*XmXuRqrUK?rKjGUT^ap7y}FOPJ%Z#%2- zRQ3J=^S^tZd||q9vi~ie*VAw?~U|6 zeSz2m|29@GTK!;#{EtF5{S*66PpzNM^W*Kd+?!l+UmmM=c6FU$Je(M)d$v9E=#`*d zXBK=tTH18-)rJ;+<|?_EjpwbcerFuC%gK41XwN-qf63 zꏆ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
    1. both parts of the key need to be based64 encoded since there can be spaces within each of them
    2. */ private [this] def makeRedisKey(name: String, key: Array[Byte]): String = withErrorHandling { - "%s:%s".format(name, byteArrayToString(key)) + "%s:%s".format(name, new String(key)) } private [this] def makeKeyFromRedisKey(redisKey: String) = withErrorHandling { val nk = redisKey.split(':') - (nk(0), stringToByteArray(nk(1))) + (nk(0), nk(1).getBytes) } private [this] def mset(entries: List[(String, String)]): Unit = withErrorHandling { @@ -124,27 +124,22 @@ private [akka] object RedisStorageBackend extends } def getMapStorageEntryFor(name: String, key: Array[Byte]): Option[Array[Byte]] = withErrorHandling { - db.get(makeRedisKey(name, key)) match { - case None => - throw new NoSuchElementException(new String(key) + " not present") - case Some(s) => Some(stringToByteArray(s)) + db.get(makeRedisKey(name, key)) + .map(stringToByteArray(_)) + .orElse(throw new NoSuchElementException(new String(key) + " not present")) } - } def getMapStorageSizeFor(name: String): Int = withErrorHandling { - db.keys("%s:*".format(name)) match { - case None => 0 - case Some(keys) => keys.length - } + db.keys("%s:*".format(name)).map(_.length).getOrElse(0) } def getMapStorageFor(name: String): List[(Array[Byte], Array[Byte])] = withErrorHandling { - db.keys("%s:*".format(name)) match { - case None => - throw new NoSuchElementException(name + " not present") - case Some(keys) => + db.keys("%s:*".format(name)) + .map { keys => keys.map(key => (makeKeyFromRedisKey(key.get)._2, stringToByteArray(db.get(key.get).get))).toList - } + }.getOrElse { + throw new NoSuchElementException(name + " not present") + } } def getMapStorageRangeFor(name: String, start: Option[Array[Byte]], @@ -207,12 +202,11 @@ private [akka] object RedisStorageBackend extends } def getVectorStorageEntryFor(name: String, index: Int): Array[Byte] = withErrorHandling { - db.lindex(name, index) match { - case None => + db.lindex(name, index) + .map(stringToByteArray(_)) + .getOrElse { throw new NoSuchElementException(name + " does not have element at " + index) - case Some(e) => - stringToByteArray(e) - } + } } /** @@ -252,11 +246,11 @@ private [akka] object RedisStorageBackend extends } def getRefStorageFor(name: String): Option[Array[Byte]] = withErrorHandling { - db.get(name) match { - case None => + db.get(name) + .map(stringToByteArray(_)) + .orElse { throw new NoSuchElementException(name + " not present") - case Some(s) => Some(stringToByteArray(s)) - } + } } // add to the end of the queue @@ -266,11 +260,11 @@ private [akka] object RedisStorageBackend extends // pop from the front of the queue def dequeue(name: String): Option[Array[Byte]] = withErrorHandling { - db.lpop(name) match { - case None => + db.lpop(name) + .map(stringToByteArray(_)) + .orElse { throw new NoSuchElementException(name + " not present") - case Some(s) => Some(stringToByteArray(s)) - } + } } // get the size of the queue @@ -302,26 +296,19 @@ private [akka] object RedisStorageBackend extends // completely delete the queue def remove(name: String): Boolean = withErrorHandling { - db.del(name) match { - case Some(1) => true - case _ => false - } + db.del(name).map { case 1 => true }.getOrElse(false) } // add item to sorted set identified by name def zadd(name: String, zscore: String, item: Array[Byte]): Boolean = withErrorHandling { - db.zadd(name, zscore, byteArrayToString(item)) match { - case Some(1) => true - case _ => false - } + db.zadd(name, zscore, byteArrayToString(item)) + .map { case 1 => true }.getOrElse(false) } // remove item from sorted set identified by name def zrem(name: String, item: Array[Byte]): Boolean = withErrorHandling { - db.zrem(name, byteArrayToString(item)) match { - case Some(1) => true - case _ => false - } + db.zrem(name, byteArrayToString(item)) + .map { case 1 => true }.getOrElse(false) } // cardinality of the set identified by name @@ -330,29 +317,23 @@ private [akka] object RedisStorageBackend extends } def zscore(name: String, item: Array[Byte]): Option[Float] = withErrorHandling { - db.zscore(name, byteArrayToString(item)) match { - case Some(s) => Some(s.toFloat) - case None => None - } + db.zscore(name, byteArrayToString(item)).map(_.toFloat) } def zrange(name: String, start: Int, end: Int): List[Array[Byte]] = withErrorHandling { - db.zrange(name, start.toString, end.toString, RedisClient.ASC, false) match { - case None => + db.zrange(name, start.toString, end.toString, RedisClient.ASC, false) + .map(_.map(e => stringToByteArray(e.get))) + .getOrElse { throw new NoSuchElementException(name + " not present") - case Some(s) => - s.map(e => stringToByteArray(e.get)) - } + } } def zrangeWithScore(name: String, start: Int, end: Int): List[(Array[Byte], Float)] = withErrorHandling { - db.zrangeWithScore( - name, start.toString, end.toString, RedisClient.ASC) match { - case None => - throw new NoSuchElementException(name + " not present") - case Some(l) => - l.map{ case (elem, score) => (stringToByteArray(elem.get), score.get.toFloat) } - } + db.zrangeWithScore(name, start.toString, end.toString, RedisClient.ASC) + .map(_.map { case (elem, score) => (stringToByteArray(elem.get), score.get.toFloat) }) + .getOrElse { + throw new NoSuchElementException(name + " not present") + } } def flushDB = withErrorHandling(db.flushdb) From a13ebc5de9a0e785a37006c336aa36c64dfdf613 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sat, 11 Sep 2010 15:24:09 +0200 Subject: [PATCH 12/25] 1 entry per mailbox at most --- .../ExecutorBasedEventDrivenDispatcher.scala | 106 ++++++++++-------- .../main/scala/dispatch/MessageHandling.scala | 42 ++++--- .../scala/dispatch/ThreadPoolBuilder.scala | 7 +- akka-actor/src/main/scala/util/LockUtil.scala | 6 + 4 files changed, 92 insertions(+), 69 deletions(-) diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index 0e9acf62e1..19045e123b 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -7,7 +7,7 @@ package se.scalablesolutions.akka.dispatch import se.scalablesolutions.akka.actor.{ActorRef, IllegalActorStateException} import java.util.Queue -import java.util.concurrent.{ConcurrentLinkedQueue, LinkedBlockingQueue} +import java.util.concurrent.{RejectedExecutionException, ConcurrentLinkedQueue, LinkedBlockingQueue} /** * Default settings are: @@ -80,6 +80,52 @@ class ExecutorBasedEventDrivenDispatcher( val name = "akka:event-driven:dispatcher:" + _name init + /** + * This is the behavior of an ExecutorBasedEventDrivenDispatchers mailbox + */ + trait ExecutableMailbox { self: MessageQueue with Runnable => + def run = { + try { + val reDispatch = processMailbox()//Returns true if we need to reschedule the processing + self.dispatcherLock.unlock() //Unlock to give a chance for someone else to schedule processing + if (reDispatch) + dispatch(self) + } catch { + case e => + dispatcherLock.unlock() //Unlock to give a chance for someone else to schedule processing + if(!self.isEmpty) //If the mailbox isn't empty, try to re-schedule processing, equivalent to reDispatch + dispatch(self) + throw e //Can't just swallow exceptions or errors + } + } + + /** + * Process the messages in the mailbox + * + * @return true if the processing finished before the mailbox was empty, due to the throughput constraint + */ + def processMailbox(): Boolean = { + val throttle = throughput > 0 + var processedMessages = 0 + var nextMessage = self.dequeue + if (nextMessage ne null) { + do { + nextMessage.invoke + + if(throttle) { //Will be elided when false + processedMessages += 1 + if (processedMessages >= throughput) //If we're throttled, break out + return !self.isEmpty + } + nextMessage = self.dequeue + } + while (nextMessage ne null) + } + + false + } + } + def dispatch(invocation: MessageInvocation) = { val mbox = getMailbox(invocation.receiver) mbox enqueue invocation @@ -93,56 +139,18 @@ class ExecutorBasedEventDrivenDispatcher( override def mailboxSize(actorRef: ActorRef) = getMailbox(actorRef).size - override def createMailbox(actorRef: ActorRef): AnyRef = new DefaultMessageQueue(mailboxCapacity,mailboxConfig.pushTimeOut,false) with Runnable { - def run = { - var lockAcquiredOnce = false - var finishedBeforeMailboxEmpty = false - // this do-while loop is required to prevent missing new messages between the end of the inner while - // loop and releasing the lock - do { - if (dispatcherLock.tryLock()) { - // Only dispatch if we got the lock. Otherwise another thread is already dispatching. - lockAcquiredOnce = true - try { - finishedBeforeMailboxEmpty = processMailbox() - } finally { - dispatcherLock.unlock() - if (finishedBeforeMailboxEmpty) - dispatch(this) - } - } - } while ((lockAcquiredOnce && !finishedBeforeMailboxEmpty && !this.isEmpty)) - } - - /** - * Process the messages in the mailbox - * - * @return true if the processing finished before the mailbox was empty, due to the throughput constraint - */ - def processMailbox(): Boolean = { - val throttle = throughput > 0 - var processedMessages = 0 - var nextMessage = this.dequeue - if (nextMessage ne null) { - do { - nextMessage.invoke - - if(throttle) { //Will be JIT:Ed away when false - processedMessages += 1 - if (processedMessages >= throughput) //If we're throttled, break out - return !this.isEmpty - } - nextMessage = this.dequeue - } - while (nextMessage ne null) - } - - false - } - } + override def createMailbox(actorRef: ActorRef): AnyRef = + if (mailboxCapacity > 0) new DefaultBoundedMessageQueue(mailboxCapacity,mailboxConfig.pushTimeOut,blockDequeue = false) with Runnable with ExecutableMailbox + else new DefaultUnboundedMessageQueue(blockDequeue = false) with Runnable with ExecutableMailbox def dispatch(mailbox: MessageQueue with Runnable): Unit = if (active) { - executor.execute(mailbox) + if (mailbox.dispatcherLock.tryLock()) {//Ensure that only one runnable can be in the executor pool at the same time + try { + executor execute mailbox + } catch { + case e: RejectedExecutionException => mailbox.dispatcherLock.unlock() + } + } } else { log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, mailbox) } diff --git a/akka-actor/src/main/scala/dispatch/MessageHandling.scala b/akka-actor/src/main/scala/dispatch/MessageHandling.scala index 015ae9422b..c2ec47c446 100644 --- a/akka-actor/src/main/scala/dispatch/MessageHandling.scala +++ b/akka-actor/src/main/scala/dispatch/MessageHandling.scala @@ -85,28 +85,36 @@ case class MailboxConfig(capacity: Int, pushTimeOut: Option[Duration], blockingD */ def newMailbox(bounds: Int = capacity, pushTime: Option[Duration] = pushTimeOut, - blockDequeue: Boolean = blockingDequeue) : MessageQueue = new DefaultMessageQueue(bounds,pushTime,blockDequeue) + blockDequeue: Boolean = blockingDequeue) : MessageQueue = + if (capacity > 0) new DefaultBoundedMessageQueue(bounds,pushTime,blockDequeue) + else new DefaultUnboundedMessageQueue(blockDequeue) } -class DefaultMessageQueue(override val capacity: Int, pushTimeOut: Option[Duration], blockDequeue: Boolean) extends BoundableTransferQueue[MessageInvocation](capacity) with MessageQueue { - def enqueue(handle: MessageInvocation) { - if(bounded) { - if (pushTimeOut.isDefined) { - if(!this.offer(handle,pushTimeOut.get.length,pushTimeOut.get.unit)) - throw new MessageQueueAppendFailedException("Couldn't enqueue message " + handle + " to " + this.toString) - } - else { - this.put(handle) - } - } else { - this.add(handle) - } +class DefaultUnboundedMessageQueue(blockDequeue: Boolean) extends LinkedBlockingQueue[MessageInvocation] with MessageQueue { + final def enqueue(handle: MessageInvocation) { + this add handle } - - def dequeue(): MessageInvocation = { + + final def dequeue(): MessageInvocation = if (blockDequeue) this.take() else this.poll() +} + +class DefaultBoundedMessageQueue(capacity: Int, pushTimeOut: Option[Duration], blockDequeue: Boolean) extends LinkedBlockingQueue[MessageInvocation](capacity) with MessageQueue { + final def enqueue(handle: MessageInvocation) { + if (pushTimeOut.isDefined) { + if(!this.offer(handle,pushTimeOut.get.length,pushTimeOut.get.unit)) + throw new MessageQueueAppendFailedException("Couldn't enqueue message " + handle + " to " + toString) + } + else { + this put handle + } } + + final def dequeue(): MessageInvocation = + if (blockDequeue) this.take() + else this.poll() + } /** @@ -128,7 +136,7 @@ trait MessageDispatcher extends Logging { } def unregister(actorRef: ActorRef) = { uuids remove actorRef.uuid - //actorRef.mailbox = null //FIXME should we null out the mailbox here? + actorRef.mailbox = null if (canBeShutDown) shutdown // shut down in the dispatcher's references is zero } diff --git a/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala b/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala index 5ad1b89aca..eb573cde70 100644 --- a/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala +++ b/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala @@ -11,6 +11,7 @@ import ThreadPoolExecutor.CallerRunsPolicy import se.scalablesolutions.akka.actor.IllegalActorStateException import se.scalablesolutions.akka.util.{Logger, Logging} +import concurrent.forkjoin.LinkedTransferQueue trait ThreadPoolBuilder extends Logging { val name: String @@ -69,7 +70,7 @@ trait ThreadPoolBuilder extends Logging { def withNewBoundedThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity(bound: Int): ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedBlockingQueue[Runnable] + blockingQueue = new LinkedTransferQueue[Runnable] threadPoolBuilder = new ThreadPoolExecutor(NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory) boundedExecutorBound = bound this @@ -78,7 +79,7 @@ trait ThreadPoolBuilder extends Logging { def withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity: ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedBlockingQueue[Runnable] + blockingQueue = new LinkedTransferQueue[Runnable] threadPoolBuilder = new ThreadPoolExecutor( NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory, new CallerRunsPolicy) this @@ -87,7 +88,7 @@ trait ThreadPoolBuilder extends Logging { def withNewThreadPoolWithLinkedBlockingQueueWithCapacity(capacity: Int): ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedBlockingQueue[Runnable](capacity) + blockingQueue = new BoundableTransferQueue[Runnable](capacity) threadPoolBuilder = new ThreadPoolExecutor( NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory, new CallerRunsPolicy) this diff --git a/akka-actor/src/main/scala/util/LockUtil.scala b/akka-actor/src/main/scala/util/LockUtil.scala index ee7f4f0efc..3d1261e468 100644 --- a/akka-actor/src/main/scala/util/LockUtil.scala +++ b/akka-actor/src/main/scala/util/LockUtil.scala @@ -102,6 +102,12 @@ class SimpleLock { else acquired.compareAndSet(false,true) } + def tryUnlock() = { + acquired.compareAndSet(true,false) + } + + def locked = acquired.get + def unlock() { acquired.set(false) } From 155f4d81ce635a776aed1a055cf9bda14d61cac0 Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Sun, 12 Sep 2010 01:40:37 +0530 Subject: [PATCH 13/25] all mongo update operations now use safely {} to pin connection at the driver level --- .../src/main/scala/MongoStorageBackend.scala | 94 ++++++++++--------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala index d51ff17dab..a85932af49 100644 --- a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala +++ b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala @@ -46,22 +46,24 @@ private[akka] object MongoStorageBackend extends } def insertMapStorageEntriesFor(name: String, entries: List[(Array[Byte], Array[Byte])]) { - val q: DBObject = MongoDBObject(KEY -> name) - coll.findOne(q) match { - case Some(dbo) => - entries.foreach { case (k, v) => dbo += new String(k) -> v } - coll.update(q, dbo, true, false) - case None => - val builder = MongoDBObject.newBuilder - builder += KEY -> name - entries.foreach { case (k, v) => builder += new String(k) -> v } - coll += builder.result.asDBObject + db.safely { db => + val q: DBObject = MongoDBObject(KEY -> name) + coll.findOne(q) match { + case Some(dbo) => + entries.foreach { case (k, v) => dbo += new String(k) -> v } + db.safely { db => coll.update(q, dbo, true, false) } + case None => + val builder = MongoDBObject.newBuilder + builder += KEY -> name + entries.foreach { case (k, v) => builder += new String(k) -> v } + coll += builder.result.asDBObject + } } } def removeMapStorageFor(name: String): Unit = { val q: DBObject = MongoDBObject(KEY -> name) - coll.remove(q) + db.safely { db => coll.remove(q) } } @@ -73,7 +75,7 @@ private[akka] object MongoStorageBackend extends def removeMapStorageFor(name: String, key: Array[Byte]): Unit = queryFor(name) { (q, dbo) => dbo.foreach { d => d -= new String(key) - coll.update(q, d, true, false) + db.safely { db => coll.update(q, d, true, false) } } } @@ -128,37 +130,39 @@ private[akka] object MongoStorageBackend extends // lookup with name val q: DBObject = MongoDBObject(KEY -> name) - coll.findOne(q) match { - // exists : need to update - case Some(dbo) => - dbo -= KEY - dbo -= "_id" - val listBuilder = MongoDBList.newBuilder + db.safely { db => + coll.findOne(q) match { + // exists : need to update + case Some(dbo) => + dbo -= KEY + dbo -= "_id" + val listBuilder = MongoDBList.newBuilder - // expensive! - listBuilder ++= (elements ++ dbo.toSeq.sortWith((e1, e2) => (e1._1.toInt < e2._1.toInt)).map(_._2)) + // expensive! + listBuilder ++= (elements ++ dbo.toSeq.sortWith((e1, e2) => (e1._1.toInt < e2._1.toInt)).map(_._2)) - val builder = MongoDBObject.newBuilder - builder += KEY -> name - builder ++= listBuilder.result - coll.update(q, builder.result.asDBObject, true, false) + val builder = MongoDBObject.newBuilder + builder += KEY -> name + builder ++= listBuilder.result + coll.update(q, builder.result.asDBObject, true, false) - // new : just add - case None => - val listBuilder = MongoDBList.newBuilder - listBuilder ++= elements + // new : just add + case None => + val listBuilder = MongoDBList.newBuilder + listBuilder ++= elements - val builder = MongoDBObject.newBuilder - builder += KEY -> name - builder ++= listBuilder.result - coll += builder.result.asDBObject + val builder = MongoDBObject.newBuilder + builder += KEY -> name + builder ++= listBuilder.result + coll += builder.result.asDBObject + } } } def updateVectorStorageEntryFor(name: String, index: Int, elem: Array[Byte]) = queryFor(name) { (q, dbo) => dbo.foreach { d => d += ((index.toString, elem)) - coll.update(q, d, true, false) + db.safely { db => coll.update(q, d, true, false) } } } @@ -201,18 +205,20 @@ private[akka] object MongoStorageBackend extends // lookup with name val q: DBObject = MongoDBObject(KEY -> name) - coll.findOne(q) match { - // exists : need to update - case Some(dbo) => - dbo += ((REF, element)) - coll.update(q, dbo, true, false) + db.safely { db => + coll.findOne(q) match { + // exists : need to update + case Some(dbo) => + dbo += ((REF, element)) + coll.update(q, dbo, true, false) - // not found : make one - case None => - val builder = MongoDBObject.newBuilder - builder += KEY -> name - builder += REF -> element - coll += builder.result.asDBObject + // not found : make one + case None => + val builder = MongoDBObject.newBuilder + builder += KEY -> name + builder += REF -> element + coll += builder.result.asDBObject + } } } From b8a35223ae341382fb2f0fee2ce5688b19b5e8df Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Sun, 12 Sep 2010 01:58:17 +0530 Subject: [PATCH 14/25] refactoring for more type safety --- .../src/main/scala/MongoStorageBackend.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala index a85932af49..01d8ababce 100644 --- a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala +++ b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala @@ -81,7 +81,7 @@ private[akka] object MongoStorageBackend extends def getMapStorageEntryFor(name: String, key: Array[Byte]): Option[Array[Byte]] = queryFor(name) { (q, dbo) => dbo.map { d => - Option(d.get(new String(key))).asInstanceOf[Option[Array[Byte]]] + d.getAs[Array[Byte]](new String(key)) }.getOrElse(None) } @@ -118,7 +118,7 @@ private[akka] object MongoStorageBackend extends // slice from keys: both ends inclusive val ks = keys.slice(keys.indexOf(s), scala.math.min(count, keys.indexOf(f) + 1)) - ks.map(k => (k.getBytes, d.get(k).asInstanceOf[Array[Byte]])) + ks.map(k => (k.getBytes, d.getAs[Array[Byte]](k).get)) }.getOrElse(List.empty[(Array[Byte], Array[Byte])]) } @@ -224,7 +224,7 @@ private[akka] object MongoStorageBackend extends def getRefStorageFor(name: String): Option[Array[Byte]] = queryFor(name) { (q, dbo) => dbo.map { d => - Option(d.get(REF)).asInstanceOf[Option[Array[Byte]]] + d.getAs[Array[Byte]](REF) }.getOrElse(None) } } From a5c5efc4ff4384e5684d13bc79f9fd05516b195a Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 12 Sep 2010 11:24:27 +0200 Subject: [PATCH 15/25] Safekeeping --- .../src/main/scala/actor/ActorRegistry.scala | 4 +-- .../src/main/scala/dispatch/Dispatchers.scala | 2 +- .../main/scala/util/ListenerManagement.scala | 17 ++++++++++++ .../src/main/scala/remote/RemoteClient.scala | 22 ++++++++-------- .../src/main/scala/remote/RemoteServer.scala | 26 +++++++++---------- .../src/main/scala/actor/TypedActor.scala | 4 +-- 6 files changed, 46 insertions(+), 29 deletions(-) diff --git a/akka-actor/src/main/scala/actor/ActorRegistry.scala b/akka-actor/src/main/scala/actor/ActorRegistry.scala index f3a479e6fd..51bbfd3477 100644 --- a/akka-actor/src/main/scala/actor/ActorRegistry.scala +++ b/akka-actor/src/main/scala/actor/ActorRegistry.scala @@ -125,7 +125,7 @@ object ActorRegistry extends ListenerManagement { actorsByUUID.put(actor.uuid, actor) // notify listeners - foreachListener(_ ! ActorRegistered(actor)) + notifyListeners(ActorRegistered(actor)) } /** @@ -137,7 +137,7 @@ object ActorRegistry extends ListenerManagement { actorsById.remove(actor.id,actor) // notify listeners - foreachListener(_ ! ActorUnregistered(actor)) + notifyListeners(ActorUnregistered(actor)) } /** diff --git a/akka-actor/src/main/scala/dispatch/Dispatchers.scala b/akka-actor/src/main/scala/dispatch/Dispatchers.scala index 9a7e44a197..2ebba03928 100644 --- a/akka-actor/src/main/scala/dispatch/Dispatchers.scala +++ b/akka-actor/src/main/scala/dispatch/Dispatchers.scala @@ -45,7 +45,7 @@ import se.scalablesolutions.akka.util.{Duration, Logging, UUID} */ object Dispatchers extends Logging { val THROUGHPUT = config.getInt("akka.actor.throughput", 5) - val MAILBOX_CAPACITY = config.getInt("akka.actor.default-dispatcher.mailbox-capacity", 1000) + val MAILBOX_CAPACITY = config.getInt("akka.actor.default-dispatcher.mailbox-capacity", -1) val MAILBOX_CONFIG = MailboxConfig( capacity = Dispatchers.MAILBOX_CAPACITY, pushTimeOut = config.getInt("akka.actor.default-dispatcher.mailbox-push-timeout-ms").map(Duration(_,TimeUnit.MILLISECONDS)), diff --git a/akka-actor/src/main/scala/util/ListenerManagement.scala b/akka-actor/src/main/scala/util/ListenerManagement.scala index 0e17058380..7ad0f451f1 100644 --- a/akka-actor/src/main/scala/util/ListenerManagement.scala +++ b/akka-actor/src/main/scala/util/ListenerManagement.scala @@ -40,6 +40,23 @@ trait ListenerManagement extends Logging { if (manageLifeCycleOfListeners) listener.stop } + /* + * Returns whether there are any listeners currently + */ + def hasListeners: Boolean = !listeners.isEmpty + + protected def notifyListeners(message: => Any) { + if (hasListeners) { + val msg = message + val iterator = listeners.iterator + while (iterator.hasNext) { + val listener = iterator.next + if (listener.isRunning) listener ! msg + else log.warning("Can't notify [%s] since it is not running.", listener) + } + } + } + /** * Execute f with each listener as argument. */ diff --git a/akka-remote/src/main/scala/remote/RemoteClient.scala b/akka-remote/src/main/scala/remote/RemoteClient.scala index f61a5d63a1..264081259f 100644 --- a/akka-remote/src/main/scala/remote/RemoteClient.scala +++ b/akka-remote/src/main/scala/remote/RemoteClient.scala @@ -220,10 +220,10 @@ class RemoteClient private[akka] ( val channel = connection.awaitUninterruptibly.getChannel openChannels.add(channel) if (!connection.isSuccess) { - foreachListener(_ ! RemoteClientError(connection.getCause, this)) + notifyListeners(RemoteClientError(connection.getCause, this)) log.error(connection.getCause, "Remote client connection to [%s:%s] has failed", hostname, port) } - foreachListener(_ ! RemoteClientStarted(this)) + notifyListeners(RemoteClientStarted(this)) isRunning = true } } @@ -232,7 +232,7 @@ class RemoteClient private[akka] ( log.info("Shutting down %s", name) if (isRunning) { isRunning = false - foreachListener(_ ! RemoteClientShutdown(this)) + notifyListeners(RemoteClientShutdown(this)) timer.stop timer = null openChannels.close.awaitUninterruptibly @@ -250,7 +250,7 @@ class RemoteClient private[akka] ( @deprecated("Use removeListener instead") def deregisterListener(actorRef: ActorRef) = removeListener(actorRef) - override def foreachListener(f: (ActorRef) => Unit): Unit = super.foreachListener(f) + override def notifyListeners(message: => Any): Unit = super.notifyListeners(message) protected override def manageLifeCycleOfListeners = false @@ -287,7 +287,7 @@ class RemoteClient private[akka] ( } else { val exception = new RemoteClientException( "Remote client is not running, make sure you have invoked 'RemoteClient.connect' before using it.", this) - foreachListener(l => l ! RemoteClientError(exception, this)) + notifyListeners(RemoteClientError(exception, this)) throw exception } @@ -403,12 +403,12 @@ class RemoteClientHandler( futures.remove(reply.getId) } else { val exception = new RemoteClientException("Unknown message received in remote client handler: " + result, client) - client.foreachListener(_ ! RemoteClientError(exception, client)) + client.notifyListeners(RemoteClientError(exception, client)) throw exception } } catch { case e: Exception => - client.foreachListener(_ ! RemoteClientError(e, client)) + client.notifyListeners(RemoteClientError(e, client)) log.error("Unexpected exception in remote client handler: %s", e) throw e } @@ -423,7 +423,7 @@ class RemoteClientHandler( client.connection = bootstrap.connect(remoteAddress) client.connection.awaitUninterruptibly // Wait until the connection attempt succeeds or fails. if (!client.connection.isSuccess) { - client.foreachListener(_ ! RemoteClientError(client.connection.getCause, client)) + client.notifyListeners(RemoteClientError(client.connection.getCause, client)) log.error(client.connection.getCause, "Reconnection to [%s] has failed", remoteAddress) } } @@ -433,7 +433,7 @@ class RemoteClientHandler( override def channelConnected(ctx: ChannelHandlerContext, event: ChannelStateEvent) = { def connect = { - client.foreachListener(_ ! RemoteClientConnected(client)) + client.notifyListeners(RemoteClientConnected(client)) log.debug("Remote client connected to [%s]", ctx.getChannel.getRemoteAddress) client.resetReconnectionTimeWindow } @@ -450,12 +450,12 @@ class RemoteClientHandler( } override def channelDisconnected(ctx: ChannelHandlerContext, event: ChannelStateEvent) = { - client.foreachListener(_ ! RemoteClientDisconnected(client)) + client.notifyListeners(RemoteClientDisconnected(client)) log.debug("Remote client disconnected from [%s]", ctx.getChannel.getRemoteAddress) } override def exceptionCaught(ctx: ChannelHandlerContext, event: ExceptionEvent) = { - client.foreachListener(_ ! RemoteClientError(event.getCause, client)) + client.notifyListeners(RemoteClientError(event.getCause, client)) log.error(event.getCause, "Unexpected exception from downstream in remote client") event.getChannel.close } diff --git a/akka-remote/src/main/scala/remote/RemoteServer.scala b/akka-remote/src/main/scala/remote/RemoteServer.scala index b10d8e5825..27b5a50bfc 100644 --- a/akka-remote/src/main/scala/remote/RemoteServer.scala +++ b/akka-remote/src/main/scala/remote/RemoteServer.scala @@ -245,12 +245,12 @@ class RemoteServer extends Logging with ListenerManagement { openChannels.add(bootstrap.bind(new InetSocketAddress(hostname, port))) _isRunning = true Cluster.registerLocalNode(hostname, port) - foreachListener(_ ! RemoteServerStarted(this)) + notifyListeners(RemoteServerStarted(this)) } } catch { case e => log.error(e, "Could not start up remote server") - foreachListener(_ ! RemoteServerError(e, this)) + notifyListeners(RemoteServerError(e, this)) } this } @@ -263,7 +263,7 @@ class RemoteServer extends Logging with ListenerManagement { openChannels.close.awaitUninterruptibly bootstrap.releaseExternalResources Cluster.deregisterLocalNode(hostname, port) - foreachListener(_ ! RemoteServerShutdown(this)) + notifyListeners(RemoteServerShutdown(this)) } catch { case e: java.nio.channels.ClosedChannelException => {} case e => log.warning("Could not close remote server channel in a graceful way") @@ -323,7 +323,7 @@ class RemoteServer extends Logging with ListenerManagement { protected override def manageLifeCycleOfListeners = false - protected[akka] override def foreachListener(f: (ActorRef) => Unit): Unit = super.foreachListener(f) + protected[akka] override def notifyListeners(message: => Any): Unit = super.notifyListeners(message) private[akka] def actors() : ConcurrentHashMap[String, ActorRef] = { RemoteServer.actorsFor(address).actors @@ -413,18 +413,18 @@ class RemoteServerHandler( def operationComplete(future: ChannelFuture): Unit = { if (future.isSuccess) { openChannels.add(future.getChannel) - server.foreachListener(_ ! RemoteServerClientConnected(server)) + server.notifyListeners(RemoteServerClientConnected(server)) } else future.getChannel.close } }) } else { - server.foreachListener(_ ! RemoteServerClientConnected(server)) + server.notifyListeners(RemoteServerClientConnected(server)) } } override def channelClosed(ctx: ChannelHandlerContext, event: ChannelStateEvent) = { log.debug("Remote client disconnected from [%s]", server.name) - server.foreachListener(_ ! RemoteServerClientDisconnected(server)) + server.notifyListeners(RemoteServerClientDisconnected(server)) } override def handleUpstream(ctx: ChannelHandlerContext, event: ChannelEvent) = { @@ -446,7 +446,7 @@ class RemoteServerHandler( override def exceptionCaught(ctx: ChannelHandlerContext, event: ExceptionEvent) = { log.error(event.getCause, "Unexpected exception from remote downstream") event.getChannel.close - server.foreachListener(_ ! RemoteServerError(event.getCause, server)) + server.notifyListeners(RemoteServerError(event.getCause, server)) } private def handleRemoteRequestProtocol(request: RemoteRequestProtocol, channel: Channel) = { @@ -491,7 +491,7 @@ class RemoteServerHandler( } catch { case e: Throwable => channel.write(createErrorReplyMessage(e, request, true)) - server.foreachListener(_ ! RemoteServerError(e, server)) + server.notifyListeners(RemoteServerError(e, server)) } } } @@ -523,10 +523,10 @@ class RemoteServerHandler( } catch { case e: InvocationTargetException => channel.write(createErrorReplyMessage(e.getCause, request, false)) - server.foreachListener(_ ! RemoteServerError(e, server)) + server.notifyListeners(RemoteServerError(e, server)) case e: Throwable => channel.write(createErrorReplyMessage(e, request, false)) - server.foreachListener(_ ! RemoteServerError(e, server)) + server.notifyListeners(RemoteServerError(e, server)) } } @@ -559,7 +559,7 @@ class RemoteServerHandler( } catch { case e => log.error(e, "Could not create remote actor instance") - server.foreachListener(_ ! RemoteServerError(e, server)) + server.notifyListeners(RemoteServerError(e, server)) throw e } } else actorRefOrNull @@ -590,7 +590,7 @@ class RemoteServerHandler( } catch { case e => log.error(e, "Could not create remote typed actor instance") - server.foreachListener(_ ! RemoteServerError(e, server)) + server.notifyListeners(RemoteServerError(e, server)) throw e } } else typedActorOrNull diff --git a/akka-typed-actor/src/main/scala/actor/TypedActor.scala b/akka-typed-actor/src/main/scala/actor/TypedActor.scala index b27f5b4b4d..52bd0e6cb6 100644 --- a/akka-typed-actor/src/main/scala/actor/TypedActor.scala +++ b/akka-typed-actor/src/main/scala/actor/TypedActor.scala @@ -674,7 +674,7 @@ private[akka] object AspectInitRegistry extends ListenerManagement { def register(proxy: AnyRef, init: AspectInit) = { val res = initializations.put(proxy, init) - foreachListener(_ ! AspectInitRegistered(proxy, init)) + notifyListeners(AspectInitRegistered(proxy, init)) res } @@ -683,7 +683,7 @@ private[akka] object AspectInitRegistry extends ListenerManagement { */ def unregister(proxy: AnyRef): AspectInit = { val init = initializations.remove(proxy) - foreachListener(_ ! AspectInitUnregistered(proxy, init)) + notifyListeners(AspectInitUnregistered(proxy, init)) init.actorRef.stop init } From 7ba4817a5e7e69ab080fdd680e4e0ce4c5ba154c Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 12 Sep 2010 18:59:21 +0200 Subject: [PATCH 16/25] Take advantage of short-circuit to avoid lazy init if possible --- akka-actor/src/main/scala/actor/ActorRef.scala | 8 ++++---- akka-actor/src/main/scala/dispatch/MessageHandling.scala | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index 86b6c2ec65..81d4c40b74 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -972,12 +972,12 @@ class LocalActorRef private[akka]( protected[akka] def postMessageToMailbox(message: Any, senderOption: Option[ActorRef]): Unit = { joinTransaction(message) - if (isRemotingEnabled && remoteAddress.isDefined) { + if (remoteAddress.isDefined && isRemotingEnabled) { RemoteClientModule.send[Any]( message, senderOption, None, remoteAddress.get, timeout, true, this, None, ActorType.ScalaActor) } else { val invocation = new MessageInvocation(this, message, senderOption, None, transactionSet.get) - invocation.send + dispatcher dispatch invocation } } @@ -988,7 +988,7 @@ class LocalActorRef private[akka]( senderFuture: Option[CompletableFuture[T]]): CompletableFuture[T] = { joinTransaction(message) - if (isRemotingEnabled && remoteAddress.isDefined) { + if (remoteAddress.isDefined && isRemotingEnabled) { val future = RemoteClientModule.send[T]( message, senderOption, senderFuture, remoteAddress.get, timeout, false, this, None, ActorType.ScalaActor) if (future.isDefined) future.get @@ -998,7 +998,7 @@ class LocalActorRef private[akka]( else new DefaultCompletableFuture[T](timeout) val invocation = new MessageInvocation( this, message, senderOption, Some(future.asInstanceOf[CompletableFuture[Any]]), transactionSet.get) - invocation.send + dispatcher dispatch invocation future } } diff --git a/akka-actor/src/main/scala/dispatch/MessageHandling.scala b/akka-actor/src/main/scala/dispatch/MessageHandling.scala index c2ec47c446..25a02f2603 100644 --- a/akka-actor/src/main/scala/dispatch/MessageHandling.scala +++ b/akka-actor/src/main/scala/dispatch/MessageHandling.scala @@ -30,8 +30,6 @@ final class MessageInvocation(val receiver: ActorRef, "Don't call 'self ! message' in the Actor's constructor (e.g. body of the class).") } - def send = receiver.dispatcher.dispatch(this) - override def hashCode(): Int = synchronized { var result = HashCode.SEED result = HashCode.hash(result, receiver.actor) From 9a0448afd4d03d67c64d7367e53a8a28f0cf161a Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 12 Sep 2010 21:00:50 +0200 Subject: [PATCH 17/25] Switching dispatching strategy to 1 runnable per mailbox and removing use of TransferQueue --- .../src/main/scala/actor/ActorRef.scala | 21 +- .../ExecutorBasedEventDrivenDispatcher.scala | 51 +++-- .../src/main/scala/dispatch/Queues.scala | 182 ------------------ .../scala/dispatch/ThreadPoolBuilder.scala | 7 +- 4 files changed, 34 insertions(+), 227 deletions(-) delete mode 100644 akka-actor/src/main/scala/dispatch/Queues.scala diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index 81d4c40b74..7777051ac1 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -199,10 +199,7 @@ trait ActorRef extends /** * This is a reference to the message currently being processed by the actor */ - protected[akka] var _currentMessage: Option[MessageInvocation] = None - - protected[akka] def currentMessage_=(msg: Option[MessageInvocation]) = guard.withGuard { _currentMessage = msg } - protected[akka] def currentMessage = guard.withGuard { _currentMessage } + @volatile protected[akka] var currentMessage: MessageInvocation = null /** * Comparison only takes uuid into account. @@ -1010,7 +1007,7 @@ class LocalActorRef private[akka]( if (isShutdown) Actor.log.warning("Actor [%s] is shut down,\n\tignoring message [%s]", toString, messageHandle) else { - currentMessage = Option(messageHandle) + currentMessage = messageHandle try { dispatch(messageHandle) } catch { @@ -1018,7 +1015,7 @@ class LocalActorRef private[akka]( Actor.log.error(e, "Could not invoke actor [%s]", this) throw e } finally { - currentMessage = None //TODO: Don't reset this, we might want to resend the message + currentMessage = null //TODO: Don't reset this, we might want to resend the message } } } @@ -1182,7 +1179,7 @@ class LocalActorRef private[akka]( } private def dispatch[T](messageHandle: MessageInvocation) = { - Actor.log.trace("Invoking actor with message:\n" + messageHandle) + Actor.log.trace("Invoking actor with message: %s\n",messageHandle) val message = messageHandle.message //serializeMessage(messageHandle.message) var topLevelTransaction = false val txSet: Option[CountDownCommitBarrier] = @@ -1529,10 +1526,9 @@ trait ScalaActorRef extends ActorRefShared { ref: ActorRef => * Is defined if the message was sent from another Actor, else None. */ def sender: Option[ActorRef] = { - // Five lines of map-performance-avoidance, could be just: currentMessage map { _.sender } val msg = currentMessage - if (msg.isEmpty) None - else msg.get.sender + if (msg eq null) None + else msg.sender } /** @@ -1540,10 +1536,9 @@ trait ScalaActorRef extends ActorRefShared { ref: ActorRef => * Is defined if the message was sent with sent with '!!' or '!!!', else None. */ def senderFuture(): Option[CompletableFuture[Any]] = { - // Five lines of map-performance-avoidance, could be just: currentMessage map { _.senderFuture } val msg = currentMessage - if (msg.isEmpty) None - else msg.get.senderFuture + if (msg eq null) None + else msg.senderFuture } diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index 949d701a21..bd8416c95f 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -85,30 +85,15 @@ class ExecutorBasedEventDrivenDispatcher( */ trait ExecutableMailbox extends Runnable { self: MessageQueue => final def run = { - var lockAcquiredOnce = false - var finishedBeforeMailboxEmpty = false - // this do-while loop is required to prevent missing new messages between the end of the inner while - // loop and releasing the lock - do { - finishedBeforeMailboxEmpty = false //Reset this every run - if (dispatcherLock.tryLock()) { - // Only dispatch if we got the lock. Otherwise another thread is already dispatching. - lockAcquiredOnce = true - finishedBeforeMailboxEmpty = try { - processMailbox() - } catch { - case e => - dispatcherLock.unlock() - if (!self.isEmpty) - registerForExecution(self) - throw e - } - dispatcherLock.unlock() - if (finishedBeforeMailboxEmpty) - registerForExecution(self) - } - } while ((lockAcquiredOnce && !finishedBeforeMailboxEmpty && !self.isEmpty)) + val reschedule = try { + processMailbox() + } finally { + dispatcherLock.unlock() + } + + if (reschedule || !self.isEmpty) + registerForExecution(self) } /** @@ -144,6 +129,20 @@ class ExecutorBasedEventDrivenDispatcher( registerForExecution(mbox) } + protected def registerForExecution(mailbox: MessageQueue with ExecutableMailbox): Unit = if (active) { + if (mailbox.dispatcherLock.tryLock()) { + try { + executor execute mailbox + } catch { + case e: RejectedExecutionException => + mailbox.dispatcherLock.unlock() + throw e + } + } + } else { + log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, mailbox) + } + /** * @return the mailbox associated with the actor */ @@ -158,11 +157,6 @@ class ExecutorBasedEventDrivenDispatcher( new DefaultUnboundedMessageQueue(blockDequeue = false) with ExecutableMailbox } - protected def registerForExecution(mailbox: MessageQueue with ExecutableMailbox): Unit = if (active) { - executor execute mailbox - } else { - log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, mailbox) - } def start = if (!active) { log.debug("Starting up %s\n\twith throughput [%d]", toString, throughput) @@ -186,6 +180,7 @@ class ExecutorBasedEventDrivenDispatcher( // FIXME: should we have an unbounded queue and not bounded as default ???? private[akka] def init = { withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity + //withNewThreadPoolWithLinkedBlockingQueueWithCapacity(16) config(this) buildThreadPool } diff --git a/akka-actor/src/main/scala/dispatch/Queues.scala b/akka-actor/src/main/scala/dispatch/Queues.scala deleted file mode 100644 index 8c75d6a42b..0000000000 --- a/akka-actor/src/main/scala/dispatch/Queues.scala +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ - -package se.scalablesolutions.akka.dispatch - -import concurrent.forkjoin.LinkedTransferQueue -import java.util.concurrent.{TimeUnit, Semaphore} -import java.util.Iterator -import se.scalablesolutions.akka.util.Logger - -class BoundableTransferQueue[E <: AnyRef](val capacity: Int) extends LinkedTransferQueue[E] { - val bounded = (capacity > 0) - - protected lazy val guard = new Semaphore(capacity) - - override def take(): E = { - if (!bounded) { - super.take - } else { - val e = super.take - if (e ne null) guard.release - e - } - } - - override def poll(): E = { - if (!bounded) { - super.poll - } else { - val e = super.poll - if (e ne null) guard.release - e - } - } - - override def poll(timeout: Long, unit: TimeUnit): E = { - if (!bounded) { - super.poll(timeout,unit) - } else { - val e = super.poll(timeout,unit) - if (e ne null) guard.release - e - } - } - - override def remainingCapacity: Int = { - if (!bounded) super.remainingCapacity - else guard.availablePermits - } - - override def remove(o: AnyRef): Boolean = { - if (!bounded) { - super.remove(o) - } else { - if (super.remove(o)) { - guard.release - true - } else false - } - } - - override def offer(e: E): Boolean = { - if (!bounded) { - super.offer(e) - } else { - if (guard.tryAcquire) { - val result = try { - super.offer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def offer(e: E, timeout: Long, unit: TimeUnit): Boolean = { - if (!bounded) { - super.offer(e,timeout,unit) - } else { - if (guard.tryAcquire(timeout,unit)) { - val result = try { - super.offer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def add(e: E): Boolean = { - if (!bounded) { - super.add(e) - } else { - if (guard.tryAcquire) { - val result = try { - super.add(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def put(e :E): Unit = { - if (!bounded) { - super.put(e) - } else { - guard.acquire - try { - super.put(e) - } catch { - case e => guard.release; throw e - } - } - } - - override def tryTransfer(e: E): Boolean = { - if (!bounded) { - super.tryTransfer(e) - } else { - if (guard.tryAcquire) { - val result = try { - super.tryTransfer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def tryTransfer(e: E, timeout: Long, unit: TimeUnit): Boolean = { - if (!bounded) { - super.tryTransfer(e,timeout,unit) - } else { - if (guard.tryAcquire(timeout,unit)) { - val result = try { - super.tryTransfer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def transfer(e: E): Unit = { - if (!bounded) { - super.transfer(e) - } else { - if (guard.tryAcquire) { - try { - super.transfer(e) - } catch { - case e => guard.release; throw e - } - } - } - } - - override def iterator: Iterator[E] = { - val it = super.iterator - new Iterator[E] { - def hasNext = it.hasNext - def next = it.next - def remove { - it.remove - if (bounded) - guard.release //Assume remove worked if no exception was thrown - } - } - } -} \ No newline at end of file diff --git a/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala b/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala index eb573cde70..5ad1b89aca 100644 --- a/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala +++ b/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala @@ -11,7 +11,6 @@ import ThreadPoolExecutor.CallerRunsPolicy import se.scalablesolutions.akka.actor.IllegalActorStateException import se.scalablesolutions.akka.util.{Logger, Logging} -import concurrent.forkjoin.LinkedTransferQueue trait ThreadPoolBuilder extends Logging { val name: String @@ -70,7 +69,7 @@ trait ThreadPoolBuilder extends Logging { def withNewBoundedThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity(bound: Int): ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedTransferQueue[Runnable] + blockingQueue = new LinkedBlockingQueue[Runnable] threadPoolBuilder = new ThreadPoolExecutor(NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory) boundedExecutorBound = bound this @@ -79,7 +78,7 @@ trait ThreadPoolBuilder extends Logging { def withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity: ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedTransferQueue[Runnable] + blockingQueue = new LinkedBlockingQueue[Runnable] threadPoolBuilder = new ThreadPoolExecutor( NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory, new CallerRunsPolicy) this @@ -88,7 +87,7 @@ trait ThreadPoolBuilder extends Logging { def withNewThreadPoolWithLinkedBlockingQueueWithCapacity(capacity: Int): ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new BoundableTransferQueue[Runnable](capacity) + blockingQueue = new LinkedBlockingQueue[Runnable](capacity) threadPoolBuilder = new ThreadPoolExecutor( NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory, new CallerRunsPolicy) this From 5a1e8f52351dc027a855446a31287761e26d46ba Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Mon, 13 Sep 2010 13:31:42 +0200 Subject: [PATCH 18/25] closing ticket #426 --- .../src/main/scala/actor/ActorRef.scala | 4 +- .../src/main/scala/remote/RemoteServer.scala | 49 +++++++++++++++---- .../serialization/SerializationProtocol.scala | 4 +- .../ClientInitiatedRemoteActorSpec.scala | 3 +- .../scala/remote/RemoteTypedActorSpec.scala | 10 ++-- .../ServerInitiatedRemoteActorSpec.scala | 21 +++++++- .../config/TypedActorGuiceConfigurator.scala | 1 - 7 files changed, 72 insertions(+), 20 deletions(-) diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index e78edba96a..8267a93a54 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -1358,7 +1358,7 @@ object RemoteActorSystemMessage { * @author Jonas Bonér */ private[akka] case class RemoteActorRef private[akka] ( - uuuid: String, + classOrServiceName: String, val className: String, val hostname: String, val port: Int, @@ -1369,7 +1369,7 @@ private[akka] case class RemoteActorRef private[akka] ( ensureRemotingEnabled - _uuid = uuuid + id = classOrServiceName timeout = _timeout start diff --git a/akka-remote/src/main/scala/remote/RemoteServer.scala b/akka-remote/src/main/scala/remote/RemoteServer.scala index fa57bda71b..9c5d21b895 100644 --- a/akka-remote/src/main/scala/remote/RemoteServer.scala +++ b/akka-remote/src/main/scala/remote/RemoteServer.scala @@ -292,7 +292,7 @@ class RemoteServer extends Logging with ListenerManagement { /** * Register Remote Actor by the Actor's 'id' field. It starts the Actor if it is not started already. */ - def register(actorRef: ActorRef): Unit = register(actorRef.id,actorRef) + def register(actorRef: ActorRef): Unit = register(actorRef.id, actorRef) /** * Register Remote Actor by a specific 'id' passed as argument. @@ -555,6 +555,32 @@ class RemoteServerHandler( } } + /** + * Find a registered actor by ID (default) or UUID. + * Actors are registered by id apart from registering during serialization see SerializationProtocol. + */ + private def findActorByIdOrUuid(id: String, uuid: String) : ActorRef = { + val registeredActors = server.actors() + var actorRefOrNull = registeredActors get id + if (actorRefOrNull eq null) { + actorRefOrNull = registeredActors get uuid + } + actorRefOrNull + } + + /** + * Find a registered typed actor by ID (default) or UUID. + * Actors are registered by id apart from registering during serialization see SerializationProtocol. + */ + private def findTypedActorByIdOrUUid(id: String, uuid: String) : AnyRef = { + val registeredActors = server.typedActors() + var actorRefOrNull = registeredActors get id + if (actorRefOrNull eq null) { + actorRefOrNull = registeredActors get uuid + } + actorRefOrNull + } + /** * Creates a new instance of the actor with name, uuid and timeout specified as arguments. * @@ -563,12 +589,14 @@ class RemoteServerHandler( * Does not start the actor. */ private def createActor(actorInfo: ActorInfoProtocol): ActorRef = { - val uuid = actorInfo.getUuid + val ids = actorInfo.getUuid.split(':') + val uuid = ids(0) + val id = ids(1) + val name = actorInfo.getTarget val timeout = actorInfo.getTimeout - val registeredActors = server.actors() - val actorRefOrNull = registeredActors get uuid + val actorRefOrNull = findActorByIdOrUuid(id, uuid) if (actorRefOrNull eq null) { try { @@ -577,9 +605,10 @@ class RemoteServerHandler( else Class.forName(name) val actorRef = Actor.actorOf(clazz.newInstance.asInstanceOf[Actor]) actorRef.uuid = uuid + actorRef.id = id actorRef.timeout = timeout actorRef.remoteAddress = None - registeredActors.put(uuid, actorRef) + server.actors.put(id, actorRef) // register by id actorRef } catch { case e => @@ -591,9 +620,11 @@ class RemoteServerHandler( } private def createTypedActor(actorInfo: ActorInfoProtocol): AnyRef = { - val uuid = actorInfo.getUuid - val registeredTypedActors = server.typedActors() - val typedActorOrNull = registeredTypedActors get uuid + val ids = actorInfo.getUuid.split(':') + val uuid = ids(0) + val id = ids(1) + + val typedActorOrNull = findTypedActorByIdOrUUid(id, uuid) if (typedActorOrNull eq null) { val typedActorInfo = actorInfo.getTypedActorInfo @@ -610,7 +641,7 @@ class RemoteServerHandler( val newInstance = TypedActor.newInstance( interfaceClass, targetClass.asInstanceOf[Class[_ <: TypedActor]], actorInfo.getTimeout).asInstanceOf[AnyRef] - registeredTypedActors.put(uuid, newInstance) + server.typedActors.put(id, newInstance) // register by id newInstance } catch { case e => diff --git a/akka-remote/src/main/scala/serialization/SerializationProtocol.scala b/akka-remote/src/main/scala/serialization/SerializationProtocol.scala index 7da001edab..afebae8f3b 100644 --- a/akka-remote/src/main/scala/serialization/SerializationProtocol.scala +++ b/akka-remote/src/main/scala/serialization/SerializationProtocol.scala @@ -230,7 +230,7 @@ object RemoteActorSerialization { } RemoteActorRefProtocol.newBuilder - .setUuid(uuid) + .setUuid(uuid + ":" + id) .setActorClassname(actorClass.getName) .setHomeAddress(AddressProtocol.newBuilder.setHostname(host).setPort(port).build) .setTimeout(timeout) @@ -248,7 +248,7 @@ object RemoteActorSerialization { import actorRef._ val actorInfoBuilder = ActorInfoProtocol.newBuilder - .setUuid(uuid) + .setUuid(uuid + ":" + actorRef.id) .setTarget(actorClassName) .setTimeout(timeout) diff --git a/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala b/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala index e03259e573..6670722b02 100644 --- a/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala +++ b/akka-remote/src/test/scala/remote/ClientInitiatedRemoteActorSpec.scala @@ -93,6 +93,7 @@ class ClientInitiatedRemoteActorSpec extends JUnitSuite { actor.stop } + @Test def shouldSendOneWayAndReceiveReply = { val actor = actorOf[SendOneWayAndReplyReceiverActor] @@ -134,6 +135,6 @@ class ClientInitiatedRemoteActorSpec extends JUnitSuite { assert("Expected exception; to test fault-tolerance" === e.getMessage()) } actor.stop - } + } } diff --git a/akka-remote/src/test/scala/remote/RemoteTypedActorSpec.scala b/akka-remote/src/test/scala/remote/RemoteTypedActorSpec.scala index 780828c310..8b28b35f57 100644 --- a/akka-remote/src/test/scala/remote/RemoteTypedActorSpec.scala +++ b/akka-remote/src/test/scala/remote/RemoteTypedActorSpec.scala @@ -4,10 +4,7 @@ package se.scalablesolutions.akka.actor.remote -import org.scalatest.Spec -import org.scalatest.Assertions import org.scalatest.matchers.ShouldMatchers -import org.scalatest.BeforeAndAfterAll import org.scalatest.junit.JUnitRunner import org.junit.runner.RunWith @@ -19,6 +16,7 @@ import se.scalablesolutions.akka.actor._ import se.scalablesolutions.akka.remote.{RemoteServer, RemoteClient} import java.util.concurrent.{LinkedBlockingQueue, TimeUnit, BlockingQueue} +import org.scalatest.{BeforeAndAfterEach, Spec, Assertions, BeforeAndAfterAll} object RemoteTypedActorSpec { val HOSTNAME = "localhost" @@ -40,7 +38,7 @@ object RemoteTypedActorLog { class RemoteTypedActorSpec extends Spec with ShouldMatchers with - BeforeAndAfterAll { + BeforeAndAfterEach with BeforeAndAfterAll { import RemoteTypedActorLog._ import RemoteTypedActorSpec._ @@ -82,6 +80,10 @@ class RemoteTypedActorSpec extends ActorRegistry.shutdownAll } + override def afterEach() { + server.typedActors.clear + } + describe("Remote Typed Actor ") { it("should receive one-way message") { diff --git a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala index 012d42f92a..2fe8bad905 100644 --- a/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala +++ b/akka-remote/src/test/scala/remote/ServerInitiatedRemoteActorSpec.scala @@ -79,6 +79,7 @@ class ServerInitiatedRemoteActorSpec extends JUnitSuite { } } + @Test def shouldSendWithBang { val actor = RemoteClient.actorFor( @@ -139,11 +140,29 @@ class ServerInitiatedRemoteActorSpec extends JUnitSuite { server.register(actorOf[RemoteActorSpecActorUnidirectional]) val actor = RemoteClient.actorFor("se.scalablesolutions.akka.actor.remote.ServerInitiatedRemoteActorSpec$RemoteActorSpecActorUnidirectional", HOSTNAME, PORT) val numberOfActorsInRegistry = ActorRegistry.actors.length - val result = actor ! "OneWay" + actor ! "OneWay" assert(RemoteActorSpecActorUnidirectional.latch.await(1, TimeUnit.SECONDS)) assert(numberOfActorsInRegistry === ActorRegistry.actors.length) actor.stop } + @Test + def shouldUseServiceNameAsIdForRemoteActorRef { + server.register(actorOf[RemoteActorSpecActorUnidirectional]) + server.register("my-service", actorOf[RemoteActorSpecActorUnidirectional]) + val actor1 = RemoteClient.actorFor("se.scalablesolutions.akka.actor.remote.ServerInitiatedRemoteActorSpec$RemoteActorSpecActorUnidirectional", HOSTNAME, PORT) + val actor2 = RemoteClient.actorFor("my-service", HOSTNAME, PORT) + val actor3 = RemoteClient.actorFor("my-service", HOSTNAME, PORT) + + actor1 ! "OneWay" + actor2 ! "OneWay" + actor3 ! "OneWay" + + assert(actor1.uuid != actor2.uuid) + assert(actor1.uuid != actor3.uuid) + assert(actor1.id != actor2.id) + assert(actor2.id == actor3.id) + } + } diff --git a/akka-typed-actor/src/main/scala/config/TypedActorGuiceConfigurator.scala b/akka-typed-actor/src/main/scala/config/TypedActorGuiceConfigurator.scala index 339c4d297d..5ca249a3ec 100644 --- a/akka-typed-actor/src/main/scala/config/TypedActorGuiceConfigurator.scala +++ b/akka-typed-actor/src/main/scala/config/TypedActorGuiceConfigurator.scala @@ -122,7 +122,6 @@ private[akka] class TypedActorGuiceConfigurator extends TypedActorConfiguratorBa remoteAddress.foreach { address => actorRef.makeRemote(remoteAddress.get) - RemoteServerModule.registerTypedActor(address, implementationClass.getName, proxy) } AspectInitRegistry.register( From 4d036edc5acec8e4d80356d54c34fa03bf242282 Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Mon, 13 Sep 2010 13:50:31 +0200 Subject: [PATCH 19/25] merged with master --- akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala b/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala index 67ab6398a6..0397d30bf0 100644 --- a/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala +++ b/akka-spring/src/test/scala/UntypedActorSpringFeatureTest.scala @@ -59,7 +59,6 @@ class UntypedActorSpringFeatureTest extends FeatureSpec with ShouldMatchers with feature("parse Spring application context") { -<<<<<<< HEAD scenario("get a untyped actor") { val myactor = getPingActorFromContext("/untyped-actor-config.xml", "simple-untyped-actor") myactor.sendOneWay("Hello") From 5a39c3b7b10d7cf5fb985aeffa04708d4dfd1385 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Sun, 12 Sep 2010 21:00:50 +0200 Subject: [PATCH 20/25] Switching dispatching strategy to 1 runnable per mailbox and removing use of TransferQueue --- .../src/main/scala/actor/ActorRef.scala | 21 +- .../ExecutorBasedEventDrivenDispatcher.scala | 50 +++-- .../src/main/scala/dispatch/Queues.scala | 182 ------------------ .../scala/dispatch/ThreadPoolBuilder.scala | 7 +- 4 files changed, 33 insertions(+), 227 deletions(-) delete mode 100644 akka-actor/src/main/scala/dispatch/Queues.scala diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index 81d4c40b74..7777051ac1 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -199,10 +199,7 @@ trait ActorRef extends /** * This is a reference to the message currently being processed by the actor */ - protected[akka] var _currentMessage: Option[MessageInvocation] = None - - protected[akka] def currentMessage_=(msg: Option[MessageInvocation]) = guard.withGuard { _currentMessage = msg } - protected[akka] def currentMessage = guard.withGuard { _currentMessage } + @volatile protected[akka] var currentMessage: MessageInvocation = null /** * Comparison only takes uuid into account. @@ -1010,7 +1007,7 @@ class LocalActorRef private[akka]( if (isShutdown) Actor.log.warning("Actor [%s] is shut down,\n\tignoring message [%s]", toString, messageHandle) else { - currentMessage = Option(messageHandle) + currentMessage = messageHandle try { dispatch(messageHandle) } catch { @@ -1018,7 +1015,7 @@ class LocalActorRef private[akka]( Actor.log.error(e, "Could not invoke actor [%s]", this) throw e } finally { - currentMessage = None //TODO: Don't reset this, we might want to resend the message + currentMessage = null //TODO: Don't reset this, we might want to resend the message } } } @@ -1182,7 +1179,7 @@ class LocalActorRef private[akka]( } private def dispatch[T](messageHandle: MessageInvocation) = { - Actor.log.trace("Invoking actor with message:\n" + messageHandle) + Actor.log.trace("Invoking actor with message: %s\n",messageHandle) val message = messageHandle.message //serializeMessage(messageHandle.message) var topLevelTransaction = false val txSet: Option[CountDownCommitBarrier] = @@ -1529,10 +1526,9 @@ trait ScalaActorRef extends ActorRefShared { ref: ActorRef => * Is defined if the message was sent from another Actor, else None. */ def sender: Option[ActorRef] = { - // Five lines of map-performance-avoidance, could be just: currentMessage map { _.sender } val msg = currentMessage - if (msg.isEmpty) None - else msg.get.sender + if (msg eq null) None + else msg.sender } /** @@ -1540,10 +1536,9 @@ trait ScalaActorRef extends ActorRefShared { ref: ActorRef => * Is defined if the message was sent with sent with '!!' or '!!!', else None. */ def senderFuture(): Option[CompletableFuture[Any]] = { - // Five lines of map-performance-avoidance, could be just: currentMessage map { _.senderFuture } val msg = currentMessage - if (msg.isEmpty) None - else msg.get.senderFuture + if (msg eq null) None + else msg.senderFuture } diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index 949d701a21..6cabdec5e5 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -85,30 +85,15 @@ class ExecutorBasedEventDrivenDispatcher( */ trait ExecutableMailbox extends Runnable { self: MessageQueue => final def run = { - var lockAcquiredOnce = false - var finishedBeforeMailboxEmpty = false - // this do-while loop is required to prevent missing new messages between the end of the inner while - // loop and releasing the lock - do { - finishedBeforeMailboxEmpty = false //Reset this every run - if (dispatcherLock.tryLock()) { - // Only dispatch if we got the lock. Otherwise another thread is already dispatching. - lockAcquiredOnce = true - finishedBeforeMailboxEmpty = try { - processMailbox() - } catch { - case e => - dispatcherLock.unlock() - if (!self.isEmpty) - registerForExecution(self) - throw e - } - dispatcherLock.unlock() - if (finishedBeforeMailboxEmpty) - registerForExecution(self) - } - } while ((lockAcquiredOnce && !finishedBeforeMailboxEmpty && !self.isEmpty)) + val reschedule = try { + processMailbox() + } finally { + dispatcherLock.unlock() + } + + if (reschedule || !self.isEmpty) + registerForExecution(self) } /** @@ -144,6 +129,20 @@ class ExecutorBasedEventDrivenDispatcher( registerForExecution(mbox) } + protected def registerForExecution(mailbox: MessageQueue with ExecutableMailbox): Unit = if (active) { + if (mailbox.dispatcherLock.tryLock()) { + try { + executor execute mailbox + } catch { + case e: RejectedExecutionException => + mailbox.dispatcherLock.unlock() + throw e + } + } + } else { + log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, mailbox) + } + /** * @return the mailbox associated with the actor */ @@ -158,11 +157,6 @@ class ExecutorBasedEventDrivenDispatcher( new DefaultUnboundedMessageQueue(blockDequeue = false) with ExecutableMailbox } - protected def registerForExecution(mailbox: MessageQueue with ExecutableMailbox): Unit = if (active) { - executor execute mailbox - } else { - log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, mailbox) - } def start = if (!active) { log.debug("Starting up %s\n\twith throughput [%d]", toString, throughput) diff --git a/akka-actor/src/main/scala/dispatch/Queues.scala b/akka-actor/src/main/scala/dispatch/Queues.scala deleted file mode 100644 index 8c75d6a42b..0000000000 --- a/akka-actor/src/main/scala/dispatch/Queues.scala +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (C) 2009-2010 Scalable Solutions AB - */ - -package se.scalablesolutions.akka.dispatch - -import concurrent.forkjoin.LinkedTransferQueue -import java.util.concurrent.{TimeUnit, Semaphore} -import java.util.Iterator -import se.scalablesolutions.akka.util.Logger - -class BoundableTransferQueue[E <: AnyRef](val capacity: Int) extends LinkedTransferQueue[E] { - val bounded = (capacity > 0) - - protected lazy val guard = new Semaphore(capacity) - - override def take(): E = { - if (!bounded) { - super.take - } else { - val e = super.take - if (e ne null) guard.release - e - } - } - - override def poll(): E = { - if (!bounded) { - super.poll - } else { - val e = super.poll - if (e ne null) guard.release - e - } - } - - override def poll(timeout: Long, unit: TimeUnit): E = { - if (!bounded) { - super.poll(timeout,unit) - } else { - val e = super.poll(timeout,unit) - if (e ne null) guard.release - e - } - } - - override def remainingCapacity: Int = { - if (!bounded) super.remainingCapacity - else guard.availablePermits - } - - override def remove(o: AnyRef): Boolean = { - if (!bounded) { - super.remove(o) - } else { - if (super.remove(o)) { - guard.release - true - } else false - } - } - - override def offer(e: E): Boolean = { - if (!bounded) { - super.offer(e) - } else { - if (guard.tryAcquire) { - val result = try { - super.offer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def offer(e: E, timeout: Long, unit: TimeUnit): Boolean = { - if (!bounded) { - super.offer(e,timeout,unit) - } else { - if (guard.tryAcquire(timeout,unit)) { - val result = try { - super.offer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def add(e: E): Boolean = { - if (!bounded) { - super.add(e) - } else { - if (guard.tryAcquire) { - val result = try { - super.add(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def put(e :E): Unit = { - if (!bounded) { - super.put(e) - } else { - guard.acquire - try { - super.put(e) - } catch { - case e => guard.release; throw e - } - } - } - - override def tryTransfer(e: E): Boolean = { - if (!bounded) { - super.tryTransfer(e) - } else { - if (guard.tryAcquire) { - val result = try { - super.tryTransfer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def tryTransfer(e: E, timeout: Long, unit: TimeUnit): Boolean = { - if (!bounded) { - super.tryTransfer(e,timeout,unit) - } else { - if (guard.tryAcquire(timeout,unit)) { - val result = try { - super.tryTransfer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else false - } - } - - override def transfer(e: E): Unit = { - if (!bounded) { - super.transfer(e) - } else { - if (guard.tryAcquire) { - try { - super.transfer(e) - } catch { - case e => guard.release; throw e - } - } - } - } - - override def iterator: Iterator[E] = { - val it = super.iterator - new Iterator[E] { - def hasNext = it.hasNext - def next = it.next - def remove { - it.remove - if (bounded) - guard.release //Assume remove worked if no exception was thrown - } - } - } -} \ No newline at end of file diff --git a/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala b/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala index eb573cde70..5ad1b89aca 100644 --- a/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala +++ b/akka-actor/src/main/scala/dispatch/ThreadPoolBuilder.scala @@ -11,7 +11,6 @@ import ThreadPoolExecutor.CallerRunsPolicy import se.scalablesolutions.akka.actor.IllegalActorStateException import se.scalablesolutions.akka.util.{Logger, Logging} -import concurrent.forkjoin.LinkedTransferQueue trait ThreadPoolBuilder extends Logging { val name: String @@ -70,7 +69,7 @@ trait ThreadPoolBuilder extends Logging { def withNewBoundedThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity(bound: Int): ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedTransferQueue[Runnable] + blockingQueue = new LinkedBlockingQueue[Runnable] threadPoolBuilder = new ThreadPoolExecutor(NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory) boundedExecutorBound = bound this @@ -79,7 +78,7 @@ trait ThreadPoolBuilder extends Logging { def withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity: ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new LinkedTransferQueue[Runnable] + blockingQueue = new LinkedBlockingQueue[Runnable] threadPoolBuilder = new ThreadPoolExecutor( NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory, new CallerRunsPolicy) this @@ -88,7 +87,7 @@ trait ThreadPoolBuilder extends Logging { def withNewThreadPoolWithLinkedBlockingQueueWithCapacity(capacity: Int): ThreadPoolBuilder = synchronized { ensureNotActive verifyNotInConstructionPhase - blockingQueue = new BoundableTransferQueue[Runnable](capacity) + blockingQueue = new LinkedBlockingQueue[Runnable](capacity) threadPoolBuilder = new ThreadPoolExecutor( NR_START_THREADS, NR_MAX_THREADS, KEEP_ALIVE_TIME, MILLISECONDS, blockingQueue, threadFactory, new CallerRunsPolicy) this From fd25f0eb6d54a18b9ddb85533c4a7a46c2cd8507 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Mon, 13 Sep 2010 14:00:24 +0200 Subject: [PATCH 21/25] Merge introduced old code --- .../main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index bd8416c95f..6cabdec5e5 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -180,7 +180,6 @@ class ExecutorBasedEventDrivenDispatcher( // FIXME: should we have an unbounded queue and not bounded as default ???? private[akka] def init = { withNewThreadPoolWithLinkedBlockingQueueWithUnboundedCapacity - //withNewThreadPoolWithLinkedBlockingQueueWithCapacity(16) config(this) buildThreadPool } From b5b08e23cfd7972a3ae5bdf2a31ed991a3e37794 Mon Sep 17 00:00:00 2001 From: Michael Kober Date: Mon, 13 Sep 2010 14:04:22 +0200 Subject: [PATCH 22/25] merged with master --- akka-actor/src/main/scala/util/ReflectiveAccess.scala | 3 --- akka-spring/src/main/scala/ActorParser.scala | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/akka-actor/src/main/scala/util/ReflectiveAccess.scala b/akka-actor/src/main/scala/util/ReflectiveAccess.scala index 8bfb7f857e..abccd5d9b0 100644 --- a/akka-actor/src/main/scala/util/ReflectiveAccess.scala +++ b/akka-actor/src/main/scala/util/ReflectiveAccess.scala @@ -29,9 +29,6 @@ object ReflectiveAccess { def ensureTypedActorEnabled = TypedActorModule.ensureTypedActorEnabled def ensureJtaEnabled = JtaModule.ensureJtaEnabled - private val noParams = Array[Class[_]]() - private val noArgs = Array[AnyRef]() - /** * Reflective access to the RemoteClient module. * diff --git a/akka-spring/src/main/scala/ActorParser.scala b/akka-spring/src/main/scala/ActorParser.scala index 8736b807d1..9858e9ad7e 100644 --- a/akka-spring/src/main/scala/ActorParser.scala +++ b/akka-spring/src/main/scala/ActorParser.scala @@ -184,8 +184,7 @@ trait DispatcherParser extends BeanParser { val threadPoolElement = DomUtils.getChildElementByTagName(dispatcherElement, THREAD_POOL_TAG); if (threadPoolElement != null) { - if (properties.dispatcherType == REACTOR_BASED_SINGLE_THREAD_EVENT_DRIVEN || - properties.dispatcherType == THREAD_BASED) { + if (properties.dispatcherType == THREAD_BASED) { throw new IllegalArgumentException("Element 'thread-pool' not allowed for this dispatcher type.") } val threadPoolProperties = parseThreadPool(threadPoolElement) From d24d9bc9d704277b1fd790a9daea5b4fc5c4e4db Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Tue, 14 Sep 2010 11:54:25 +0200 Subject: [PATCH 23/25] The unborkening of master: The return of the Poms --- .../redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom | 8 ++++++++ .../time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom create mode 100644 embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom diff --git a/embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom b/embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom new file mode 100644 index 0000000000..12558da1c4 --- /dev/null +++ b/embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + com.redis + redisclient + 2.8.0-2.0 + jar + \ No newline at end of file diff --git a/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom b/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom new file mode 100644 index 0000000000..d8a6723b92 --- /dev/null +++ b/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + org.scala-tools + time-2.8.0 + 0.2-SNAPSHOT/version> + jar + \ No newline at end of file From 46904f077c1dee2a859470a0e424dd880e518621 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Tue, 14 Sep 2010 11:54:25 +0200 Subject: [PATCH 24/25] The unborkening of master: The return of the Poms --- .../redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom | 8 ++++++++ .../time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom | 8 ++++++++ project/build/AkkaProject.scala | 4 +--- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom create mode 100644 embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom diff --git a/embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom b/embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom new file mode 100644 index 0000000000..12558da1c4 --- /dev/null +++ b/embedded-repo/com/redis/redisclient/2.8.0-2.0/redisclient-2.8.0-2.0.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + com.redis + redisclient + 2.8.0-2.0 + jar + \ No newline at end of file diff --git a/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom b/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom new file mode 100644 index 0000000000..fc1cf3406e --- /dev/null +++ b/embedded-repo/org/scala-tools/time/2.8.0-0.2-SNAPSHOT/time-2.8.0-0.2-SNAPSHOT.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + org.scala-tools + time + 2.8.0-0.2-SNAPSHOT + jar + \ No newline at end of file diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index 0ee2b18108..dccad5ffca 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -49,8 +49,7 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { lazy val JavaNetRepo = MavenRepository("java.net Repo", "http://download.java.net/maven/2") lazy val SonatypeSnapshotRepo = MavenRepository("Sonatype OSS Repo", "http://oss.sonatype.org/content/repositories/releases") lazy val SunJDMKRepo = MavenRepository("Sun JDMK Repo", "http://wp5.e-taxonomy.eu/cdmlib/mavenrepo") - lazy val CasbahRepoSnapshots = MavenRepository("Casbah Snapshot Repo", "http://repo.bumnetworks.com/snapshots/") - lazy val CasbahRepoReleases = MavenRepository("Casbah Snapshot Repo", "http://repo.bumnetworks.com/releases/") + lazy val CasbahRepoReleases = MavenRepository("Casbah Release Repo", "http://repo.bumnetworks.com/releases") } // ------------------------------------------------------------------------------------------------------------------- @@ -77,7 +76,6 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { lazy val scalaTestModuleConfig = ModuleConfiguration("org.scalatest", ScalaToolsSnapshots) lazy val logbackModuleConfig = ModuleConfiguration("ch.qos.logback",sbt.DefaultMavenRepository) lazy val atomikosModuleConfig = ModuleConfiguration("com.atomikos",sbt.DefaultMavenRepository) - lazy val casbahSnapshot = ModuleConfiguration("com.novus",CasbahRepoSnapshots) lazy val casbahRelease = ModuleConfiguration("com.novus",CasbahRepoReleases) lazy val embeddedRepo = EmbeddedRepo // This is the only exception, because the embedded repo is fast! From d6f099632b3d9953bc7eec837b7a3cffcde77502 Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Tue, 14 Sep 2010 16:28:52 +0530 Subject: [PATCH 25/25] disabled tests for redis and mongo to be run automatically since they need running servers --- project/build/AkkaProject.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index dccad5ffca..6a97dbccfd 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -480,7 +480,7 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { val commons_codec = Dependencies.commons_codec val redis = Dependencies.redis - // override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil + override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil } // ------------------------------------------------------------------------------------------------------------------- @@ -491,7 +491,7 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { val mongo = Dependencies.mongo val casbah = Dependencies.casbah - // override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil + override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil } // -------------------------------------------------------------------------------------------------------------------

      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 name.endsWith("Test")) :: Nil + // override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil } // ------------------------------------------------------------------------------------------------------------------- From 2c0ebf26f003976c839c0baa8b923bf68bfd77b1 Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Thu, 9 Sep 2010 17:34:05 +0200 Subject: [PATCH 07/25] Optimization started of EBEDD --- .../ExecutorBasedEventDrivenDispatcher.scala | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index dbbfe3442a..bbe4b53cde 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -64,7 +64,7 @@ import java.util.concurrent.{ConcurrentLinkedQueue, LinkedBlockingQueue} */ class ExecutorBasedEventDrivenDispatcher( _name: String, - throughput: Int = Dispatchers.THROUGHPUT, + val throughput: Int = Dispatchers.THROUGHPUT, mailboxConfig: MailboxConfig = Dispatchers.MAILBOX_CONFIG, config: (ThreadPoolBuilder) => Unit = _ => ()) extends MessageDispatcher with ThreadPoolBuilder { @@ -109,7 +109,7 @@ class ExecutorBasedEventDrivenDispatcher( // Only dispatch if we got the lock. Otherwise another thread is already dispatching. lockAcquiredOnce = true try { - finishedBeforeMailboxEmpty = processMailbox(receiver) + finishedBeforeMailboxEmpty = processMailbox(receiver,mailbox) } finally { lock.unlock if (finishedBeforeMailboxEmpty) dispatch(receiver) @@ -128,20 +128,24 @@ class ExecutorBasedEventDrivenDispatcher( * * @return true if the processing finished before the mailbox was empty, due to the throughput constraint */ - def processMailbox(receiver: ActorRef): Boolean = { + def processMailbox(receiver: ActorRef,mailbox: MessageQueue): Boolean = { + val throttle = throughput > 0 var processedMessages = 0 - val mailbox = getMailbox(receiver) - var messageInvocation = mailbox.dequeue - while (messageInvocation != null) { - messageInvocation.invoke - processedMessages += 1 - // check if we simply continue with other messages, or reached the throughput limit - if (throughput <= 0 || processedMessages < throughput) messageInvocation = mailbox.dequeue - else { - messageInvocation = null - return !mailbox.isEmpty + var nextMessage = mailbox.dequeue + if (nextMessage ne null) { + do { + nextMessage.invoke + + if(throttle) { //Will be JIT:Ed away when false + processedMessages += 1 + if (processedMessages >= throughput) //If we're throttled, break out + return !mailbox.isEmpty + } + nextMessage = mailbox.dequeue } + while (nextMessage ne null) } + false } From 77bdedc1f1e626548a5213d72431be84ed4044be Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Fri, 10 Sep 2010 18:12:09 +0200 Subject: [PATCH 08/25] Massive refactoring of EBEDD and WorkStealer and basically everything... --- .../src/main/scala/actor/ActorRef.scala | 6 - .../ExecutorBasedEventDrivenDispatcher.scala | 94 +++++---- ...sedEventDrivenWorkStealingDispatcher.scala | 77 ++++--- .../main/scala/dispatch/MessageHandling.scala | 61 ++---- .../src/main/scala/dispatch/Queues.scala | 195 +++++++++++------- akka-actor/src/main/scala/util/LockUtil.scala | 48 +++++ 6 files changed, 280 insertions(+), 201 deletions(-) diff --git a/akka-actor/src/main/scala/actor/ActorRef.scala b/akka-actor/src/main/scala/actor/ActorRef.scala index c41408a1de..86b6c2ec65 100644 --- a/akka-actor/src/main/scala/actor/ActorRef.scala +++ b/akka-actor/src/main/scala/actor/ActorRef.scala @@ -196,12 +196,6 @@ trait ActorRef extends */ @volatile private[akka] var _transactionFactory: Option[TransactionFactory] = None - /** - * This lock ensures thread safety in the dispatching: only one message can - * be dispatched at once on the actor. - */ - protected[akka] val dispatcherLock = new ReentrantLock - /** * This is a reference to the message currently being processed by the actor */ diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala index bbe4b53cde..0e9acf62e1 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenDispatcher.scala @@ -81,72 +81,70 @@ class ExecutorBasedEventDrivenDispatcher( init def dispatch(invocation: MessageInvocation) = { - getMailbox(invocation.receiver) enqueue invocation - dispatch(invocation.receiver) + val mbox = getMailbox(invocation.receiver) + mbox enqueue invocation + dispatch(mbox) } /** * @return the mailbox associated with the actor */ - private def getMailbox(receiver: ActorRef) = receiver.mailbox.asInstanceOf[MessageQueue] + private def getMailbox(receiver: ActorRef) = receiver.mailbox.asInstanceOf[MessageQueue with Runnable] override def mailboxSize(actorRef: ActorRef) = getMailbox(actorRef).size - override def createMailbox(actorRef: ActorRef): AnyRef = mailboxConfig.newMailbox(bounds = mailboxCapacity, blockDequeue = false) - - def dispatch(receiver: ActorRef): Unit = if (active) { - - executor.execute(new Runnable() { - def run = { - var lockAcquiredOnce = false - var finishedBeforeMailboxEmpty = false - val lock = receiver.dispatcherLock - val mailbox = getMailbox(receiver) - // this do-while loop is required to prevent missing new messages between the end of the inner while - // loop and releasing the lock - do { - if (lock.tryLock) { - // Only dispatch if we got the lock. Otherwise another thread is already dispatching. - lockAcquiredOnce = true - try { - finishedBeforeMailboxEmpty = processMailbox(receiver,mailbox) - } finally { - lock.unlock - if (finishedBeforeMailboxEmpty) dispatch(receiver) - } + override def createMailbox(actorRef: ActorRef): AnyRef = new DefaultMessageQueue(mailboxCapacity,mailboxConfig.pushTimeOut,false) with Runnable { + def run = { + var lockAcquiredOnce = false + var finishedBeforeMailboxEmpty = false + // this do-while loop is required to prevent missing new messages between the end of the inner while + // loop and releasing the lock + do { + if (dispatcherLock.tryLock()) { + // Only dispatch if we got the lock. Otherwise another thread is already dispatching. + lockAcquiredOnce = true + try { + finishedBeforeMailboxEmpty = processMailbox() + } finally { + dispatcherLock.unlock() + if (finishedBeforeMailboxEmpty) + dispatch(this) } - } while ((lockAcquiredOnce && !finishedBeforeMailboxEmpty && !mailbox.isEmpty)) - } - }) - } else { - log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, receiver) - } - + } + } while ((lockAcquiredOnce && !finishedBeforeMailboxEmpty && !this.isEmpty)) + } /** - * Process the messages in the mailbox of the given actor. + * Process the messages in the mailbox * * @return true if the processing finished before the mailbox was empty, due to the throughput constraint */ - def processMailbox(receiver: ActorRef,mailbox: MessageQueue): Boolean = { - val throttle = throughput > 0 - var processedMessages = 0 - var nextMessage = mailbox.dequeue - if (nextMessage ne null) { - do { - nextMessage.invoke + def processMailbox(): Boolean = { + val throttle = throughput > 0 + var processedMessages = 0 + var nextMessage = this.dequeue + if (nextMessage ne null) { + do { + nextMessage.invoke - if(throttle) { //Will be JIT:Ed away when false - processedMessages += 1 - if (processedMessages >= throughput) //If we're throttled, break out - return !mailbox.isEmpty + if(throttle) { //Will be JIT:Ed away when false + processedMessages += 1 + if (processedMessages >= throughput) //If we're throttled, break out + return !this.isEmpty + } + nextMessage = this.dequeue } - nextMessage = mailbox.dequeue + while (nextMessage ne null) } - while (nextMessage ne null) - } - false + false + } + } + + def dispatch(mailbox: MessageQueue with Runnable): Unit = if (active) { + executor.execute(mailbox) + } else { + log.warning("%s is shut down,\n\tignoring the rest of the messages in the mailbox of\n\t%s", toString, mailbox) } def start = if (!active) { diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala index 9b1097213e..c97c16c238 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala @@ -56,21 +56,14 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher( /** * @return the mailbox associated with the actor */ - private def getMailbox(receiver: ActorRef) = receiver.mailbox.asInstanceOf[Deque[MessageInvocation]] + private def getMailbox(receiver: ActorRef) = receiver.mailbox.asInstanceOf[Deque[MessageInvocation] with MessageQueue with Runnable] override def mailboxSize(actorRef: ActorRef) = getMailbox(actorRef).size def dispatch(invocation: MessageInvocation) = if (active) { - getMailbox(invocation.receiver).add(invocation) - executor.execute(new Runnable() { - def run = { - if (!tryProcessMailbox(invocation.receiver)) { - // we are not able to process our mailbox (another thread is busy with it), so lets donate some of our mailbox - // to another actor and then process his mailbox in stead. - findThief(invocation.receiver).foreach( tryDonateAndProcessMessages(invocation.receiver,_) ) - } - } - }) + val mbox = getMailbox(invocation.receiver) + mbox enqueue invocation + executor execute mbox } else throw new IllegalActorStateException("Can't submit invocations to dispatcher since it's not started") /** @@ -79,22 +72,21 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher( * * @return true if the mailbox was processed, false otherwise */ - private def tryProcessMailbox(receiver: ActorRef): Boolean = { + private def tryProcessMailbox(mailbox: MessageQueue): Boolean = { var lockAcquiredOnce = false - val lock = receiver.dispatcherLock // this do-wile loop is required to prevent missing new messages between the end of processing // the mailbox and releasing the lock do { - if (lock.tryLock) { + if (mailbox.dispatcherLock.tryLock) { lockAcquiredOnce = true try { - processMailbox(receiver) + processMailbox(mailbox) } finally { - lock.unlock + mailbox.dispatcherLock.unlock } } - } while ((lockAcquiredOnce && !getMailbox(receiver).isEmpty)) + } while ((lockAcquiredOnce && !mailbox.isEmpty)) lockAcquiredOnce } @@ -102,12 +94,11 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher( /** * Process the messages in the mailbox of the given actor. */ - private def processMailbox(receiver: ActorRef) = { - val mailbox = getMailbox(receiver) - var messageInvocation = mailbox.poll - while (messageInvocation != null) { + private def processMailbox(mailbox: MessageQueue) = { + var messageInvocation = mailbox.dequeue + while (messageInvocation ne null) { messageInvocation.invoke - messageInvocation = mailbox.poll + messageInvocation = mailbox.dequeue } } @@ -145,11 +136,12 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher( * the thiefs dispatching lock, because in that case another thread is already processing the thiefs mailbox. */ private def tryDonateAndProcessMessages(receiver: ActorRef, thief: ActorRef) = { - if (thief.dispatcherLock.tryLock) { + val mailbox = getMailbox(thief) + if (mailbox.dispatcherLock.tryLock) { try { - while(donateMessage(receiver, thief)) processMailbox(thief) + while(donateMessage(receiver, thief)) processMailbox(mailbox) } finally { - thief.dispatcherLock.unlock + mailbox.dispatcherLock.unlock } } } @@ -191,18 +183,45 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher( } protected override def createMailbox(actorRef: ActorRef): AnyRef = { - if (mailboxCapacity <= 0) new ConcurrentLinkedDeque[MessageInvocation] - else new LinkedBlockingDeque[MessageInvocation](mailboxCapacity) + if (mailboxCapacity <= 0) { + new ConcurrentLinkedDeque[MessageInvocation] with MessageQueue with Runnable { + def enqueue(handle: MessageInvocation): Unit = this.add(handle) + def dequeue: MessageInvocation = this.poll() + + def run = { + if (!tryProcessMailbox(this)) { + // we are not able to process our mailbox (another thread is busy with it), so lets donate some of our mailbox + // to another actor and then process his mailbox in stead. + findThief(actorRef).foreach( tryDonateAndProcessMessages(actorRef,_) ) + } + } + } + } + else { + new LinkedBlockingDeque[MessageInvocation](mailboxCapacity) with MessageQueue with Runnable { + def enqueue(handle: MessageInvocation): Unit = this.add(handle) + + def dequeue: MessageInvocation = this.poll() + + def run = { + if (!tryProcessMailbox(this)) { + // we are not able to process our mailbox (another thread is busy with it), so lets donate some of our mailbox + // to another actor and then process his mailbox in stead. + findThief(actorRef).foreach( tryDonateAndProcessMessages(actorRef,_) ) + } + } + } + } } override def register(actorRef: ActorRef) = { verifyActorsAreOfSameType(actorRef) - pooledActors.add(actorRef) + pooledActors add actorRef super.register(actorRef) } override def unregister(actorRef: ActorRef) = { - pooledActors.remove(actorRef) + pooledActors remove actorRef super.unregister(actorRef) } diff --git a/akka-actor/src/main/scala/dispatch/MessageHandling.scala b/akka-actor/src/main/scala/dispatch/MessageHandling.scala index 383c58905a..015ae9422b 100644 --- a/akka-actor/src/main/scala/dispatch/MessageHandling.scala +++ b/akka-actor/src/main/scala/dispatch/MessageHandling.scala @@ -8,10 +8,10 @@ import se.scalablesolutions.akka.actor.{Actor, ActorRef, ActorInitializationExce import org.multiverse.commitbarriers.CountDownCommitBarrier import se.scalablesolutions.akka.AkkaException -import se.scalablesolutions.akka.util.{Duration, HashCode, Logging} import java.util.{Queue, List} import java.util.concurrent._ import concurrent.forkjoin.LinkedTransferQueue +import se.scalablesolutions.akka.util.{SimpleLock, Duration, HashCode, Logging} /** * @author Jonas Bonér @@ -63,6 +63,7 @@ class MessageQueueAppendFailedException(message: String) extends AkkaException(m * @author Jonas Bonér */ trait MessageQueue { + val dispatcherLock = new SimpleLock def enqueue(handle: MessageInvocation) def dequeue(): MessageInvocation def size: Int @@ -84,40 +85,28 @@ case class MailboxConfig(capacity: Int, pushTimeOut: Option[Duration], blockingD */ def newMailbox(bounds: Int = capacity, pushTime: Option[Duration] = pushTimeOut, - blockDequeue: Boolean = blockingDequeue) : MessageQueue = { - if (bounds <= 0) { //UNBOUNDED: Will never block enqueue and optionally blocking dequeue - new LinkedTransferQueue[MessageInvocation] with MessageQueue { - def enqueue(handle: MessageInvocation): Unit = this add handle - def dequeue(): MessageInvocation = { - if(blockDequeue) this.take() - else this.poll() - } - } - } - else if (pushTime.isDefined) { //BOUNDED: Timeouted enqueue with MessageQueueAppendFailedException and optionally blocking dequeue - val time = pushTime.get - new BoundedTransferQueue[MessageInvocation](bounds) with MessageQueue { - def enqueue(handle: MessageInvocation) { - if (!this.offer(handle,time.length,time.unit)) - throw new MessageQueueAppendFailedException("Couldn't enqueue message " + handle + " to " + this.toString) - } + blockDequeue: Boolean = blockingDequeue) : MessageQueue = new DefaultMessageQueue(bounds,pushTime,blockDequeue) +} - def dequeue(): MessageInvocation = { - if (blockDequeue) this.take() - else this.poll() - } +class DefaultMessageQueue(override val capacity: Int, pushTimeOut: Option[Duration], blockDequeue: Boolean) extends BoundableTransferQueue[MessageInvocation](capacity) with MessageQueue { + def enqueue(handle: MessageInvocation) { + if(bounded) { + if (pushTimeOut.isDefined) { + if(!this.offer(handle,pushTimeOut.get.length,pushTimeOut.get.unit)) + throw new MessageQueueAppendFailedException("Couldn't enqueue message " + handle + " to " + this.toString) } - } - else { //BOUNDED: Blocking enqueue and optionally blocking dequeue - new LinkedBlockingQueue[MessageInvocation](bounds) with MessageQueue { - def enqueue(handle: MessageInvocation): Unit = this put handle - def dequeue(): MessageInvocation = { - if(blockDequeue) this.take() - else this.poll() - } + else { + this.put(handle) } + } else { + this.add(handle) } } + + def dequeue(): MessageInvocation = { + if (blockDequeue) this.take() + else this.poll() + } } /** @@ -156,14 +145,4 @@ trait MessageDispatcher extends Logging { * Creates and returns a mailbox for the given actor */ protected def createMailbox(actorRef: ActorRef): AnyRef = null -} - -/** - * @author Jonas Bonér - */ -trait MessageDemultiplexer { - def select - def wakeUp - def acquireSelectedInvocations: List[MessageInvocation] - def releaseSelectedInvocations -} +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/dispatch/Queues.scala b/akka-actor/src/main/scala/dispatch/Queues.scala index 2ba88f25c3..8c75d6a42b 100644 --- a/akka-actor/src/main/scala/dispatch/Queues.scala +++ b/akka-actor/src/main/scala/dispatch/Queues.scala @@ -9,120 +9,160 @@ import java.util.concurrent.{TimeUnit, Semaphore} import java.util.Iterator import se.scalablesolutions.akka.util.Logger -class BoundedTransferQueue[E <: AnyRef](val capacity: Int) extends LinkedTransferQueue[E] { - require(capacity > 0) +class BoundableTransferQueue[E <: AnyRef](val capacity: Int) extends LinkedTransferQueue[E] { + val bounded = (capacity > 0) - protected val guard = new Semaphore(capacity) + protected lazy val guard = new Semaphore(capacity) override def take(): E = { - val e = super.take - if (e ne null) guard.release - e + if (!bounded) { + super.take + } else { + val e = super.take + if (e ne null) guard.release + e + } } override def poll(): E = { - val e = super.poll - if (e ne null) guard.release - e + if (!bounded) { + super.poll + } else { + val e = super.poll + if (e ne null) guard.release + e + } } override def poll(timeout: Long, unit: TimeUnit): E = { - val e = super.poll(timeout,unit) - if (e ne null) guard.release - e + if (!bounded) { + super.poll(timeout,unit) + } else { + val e = super.poll(timeout,unit) + if (e ne null) guard.release + e + } } - override def remainingCapacity = guard.availablePermits + override def remainingCapacity: Int = { + if (!bounded) super.remainingCapacity + else guard.availablePermits + } override def remove(o: AnyRef): Boolean = { - if (super.remove(o)) { - guard.release - true + if (!bounded) { + super.remove(o) } else { - false + if (super.remove(o)) { + guard.release + true + } else false } } override def offer(e: E): Boolean = { - if (guard.tryAcquire) { - val result = try { - super.offer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else - false + if (!bounded) { + super.offer(e) + } else { + if (guard.tryAcquire) { + val result = try { + super.offer(e) + } catch { + case e => guard.release; throw e + } + if (!result) guard.release + result + } else false + } } override def offer(e: E, timeout: Long, unit: TimeUnit): Boolean = { - if (guard.tryAcquire(timeout,unit)) { - val result = try { - super.offer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else - false + if (!bounded) { + super.offer(e,timeout,unit) + } else { + if (guard.tryAcquire(timeout,unit)) { + val result = try { + super.offer(e) + } catch { + case e => guard.release; throw e + } + if (!result) guard.release + result + } else false + } } override def add(e: E): Boolean = { - if (guard.tryAcquire) { - val result = try { - super.add(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else - false + if (!bounded) { + super.add(e) + } else { + if (guard.tryAcquire) { + val result = try { + super.add(e) + } catch { + case e => guard.release; throw e + } + if (!result) guard.release + result + } else false + } } override def put(e :E): Unit = { - guard.acquire - try { + if (!bounded) { super.put(e) - } catch { - case e => guard.release; throw e + } else { + guard.acquire + try { + super.put(e) + } catch { + case e => guard.release; throw e + } } } override def tryTransfer(e: E): Boolean = { - if (guard.tryAcquire) { - val result = try { - super.tryTransfer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else - false + if (!bounded) { + super.tryTransfer(e) + } else { + if (guard.tryAcquire) { + val result = try { + super.tryTransfer(e) + } catch { + case e => guard.release; throw e + } + if (!result) guard.release + result + } else false + } } override def tryTransfer(e: E, timeout: Long, unit: TimeUnit): Boolean = { - if (guard.tryAcquire(timeout,unit)) { - val result = try { - super.tryTransfer(e) - } catch { - case e => guard.release; throw e - } - if (!result) guard.release - result - } else - false + if (!bounded) { + super.tryTransfer(e,timeout,unit) + } else { + if (guard.tryAcquire(timeout,unit)) { + val result = try { + super.tryTransfer(e) + } catch { + case e => guard.release; throw e + } + if (!result) guard.release + result + } else false + } } override def transfer(e: E): Unit = { - if (guard.tryAcquire) { - try { - super.transfer(e) - } catch { - case e => guard.release; throw e + if (!bounded) { + super.transfer(e) + } else { + if (guard.tryAcquire) { + try { + super.transfer(e) + } catch { + case e => guard.release; throw e + } } } } @@ -134,7 +174,8 @@ class BoundedTransferQueue[E <: AnyRef](val capacity: Int) extends LinkedTransfe def next = it.next def remove { it.remove - guard.release //Assume remove worked if no exception was thrown + if (bounded) + guard.release //Assume remove worked if no exception was thrown } } } diff --git a/akka-actor/src/main/scala/util/LockUtil.scala b/akka-actor/src/main/scala/util/LockUtil.scala index 885e11def7..ee7f4f0efc 100644 --- a/akka-actor/src/main/scala/util/LockUtil.scala +++ b/akka-actor/src/main/scala/util/LockUtil.scala @@ -5,6 +5,7 @@ package se.scalablesolutions.akka.util import java.util.concurrent.locks.{ReentrantReadWriteLock, ReentrantLock} +import java.util.concurrent.atomic.AtomicBoolean /** * @author Jonas Bonér @@ -58,3 +59,50 @@ class ReadWriteGuard { } } +/** + * A very simple lock that uses CCAS (Compare Compare-And-Swap) + * Does not keep track of the owner and isn't Reentrant, so don't nest and try to stick to the if*-methods + */ +class SimpleLock { + val acquired = new AtomicBoolean(false) + + def ifPossible(perform: () => Unit): Boolean = { + if (tryLock()) { + try { + perform + } finally { + unlock() + } + true + } else false + } + + def ifPossibleYield[T](perform: () => T): Option[T] = { + if (tryLock()) { + try { + Some(perform()) + } finally { + unlock() + } + } else None + } + + def ifPossibleApply[T,R](value: T)(function: (T) => R): Option[R] = { + if (tryLock()) { + try { + Some(function(value)) + } finally { + unlock() + } + } else None + } + + def tryLock() = { + if (acquired.get) false + else acquired.compareAndSet(false,true) + } + + def unlock() { + acquired.set(false) + } +} \ No newline at end of file From f20e0ee030106f2745691276efe48f9129c52a0c Mon Sep 17 00:00:00 2001 From: Viktor Klang Date: Fri, 10 Sep 2010 18:25:24 +0200 Subject: [PATCH 09/25] Added more safeguards to the WorkStealers tests --- ...asedEventDrivenWorkStealingDispatcher.scala | 1 - ...EventDrivenWorkStealingDispatcherSpec.scala | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala index c97c16c238..10afb1bfb6 100644 --- a/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala +++ b/akka-actor/src/main/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcher.scala @@ -200,7 +200,6 @@ class ExecutorBasedEventDrivenWorkStealingDispatcher( else { new LinkedBlockingDeque[MessageInvocation](mailboxCapacity) with MessageQueue with Runnable { def enqueue(handle: MessageInvocation): Unit = this.add(handle) - def dequeue: MessageInvocation = this.poll() def run = { diff --git a/akka-actor/src/test/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcherSpec.scala b/akka-actor/src/test/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcherSpec.scala index cde57a0544..3285e450c6 100644 --- a/akka-actor/src/test/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcherSpec.scala +++ b/akka-actor/src/test/scala/dispatch/ExecutorBasedEventDrivenWorkStealingDispatcherSpec.scala @@ -5,11 +5,10 @@ import org.scalatest.junit.JUnitSuite import org.junit.Test -import se.scalablesolutions.akka.dispatch.Dispatchers - import java.util.concurrent.{TimeUnit, CountDownLatch} import se.scalablesolutions.akka.actor.{IllegalActorStateException, Actor} import Actor._ +import se.scalablesolutions.akka.dispatch.{MessageQueue, Dispatchers} object ExecutorBasedEventDrivenWorkStealingDispatcherSpec { val delayableActorDispatcher = Dispatchers.newExecutorBasedEventDrivenWorkStealingDispatcher("pooled-dispatcher") @@ -18,7 +17,7 @@ object ExecutorBasedEventDrivenWorkStealingDispatcherSpec { class DelayableActor(name: String, delay: Int, finishedCounter: CountDownLatch) extends Actor { self.dispatcher = delayableActorDispatcher - var invocationCount = 0 + @volatile var invocationCount = 0 self.id = name def receive = { @@ -61,10 +60,14 @@ class ExecutorBasedEventDrivenWorkStealingDispatcherSpec extends JUnitSuite with val slow = actorOf(new DelayableActor("slow", 50, finishedCounter)).start val fast = actorOf(new DelayableActor("fast", 10, finishedCounter)).start + var sentToFast = 0 + for (i <- 1 to 100) { // send most work to slow actor - if (i % 20 == 0) + if (i % 20 == 0) { fast ! i + sentToFast += 1 + } else slow ! i } @@ -72,13 +75,18 @@ class ExecutorBasedEventDrivenWorkStealingDispatcherSpec extends JUnitSuite with // now send some messages to actors to keep the dispatcher dispatching messages for (i <- 1 to 10) { Thread.sleep(150) - if (i % 2 == 0) + if (i % 2 == 0) { fast ! i + sentToFast += 1 + } else slow ! i } finishedCounter.await(5, TimeUnit.SECONDS) + fast.mailbox.asInstanceOf[MessageQueue].isEmpty must be(true) + slow.mailbox.asInstanceOf[MessageQueue].isEmpty must be(true) + fast.actor.asInstanceOf[DelayableActor].invocationCount must be > sentToFast fast.actor.asInstanceOf[DelayableActor].invocationCount must be > (slow.actor.asInstanceOf[DelayableActor].invocationCount) slow.stop From 8494f8b1fc1d981083873bf39ce4190b6a40eb7e Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Fri, 10 Sep 2010 23:08:25 +0530 Subject: [PATCH 10/25] changes for ticket #343. Test harness runs for both Redis and Mongo --- .../src/main/scala/CassandraStorage.scala | 2 +- .../src/main/scala/Storage.scala | 339 +++++++++++++---- .../src/main/scala/MongoStorage.scala | 2 +- .../src/main/scala/MongoStorageBackend.scala | 103 ++--- .../src/test/scala/MongoStorageSpec.scala | 4 +- .../src/test/scala/MongoTicket343Spec.scala | 347 +++++++++++++++++ .../src/main/scala/RedisStorage.scala | 2 +- .../src/test/scala/RedisTicket343Spec.scala | 351 ++++++++++++++++++ project/build/AkkaProject.scala | 2 +- 9 files changed, 1039 insertions(+), 113 deletions(-) create mode 100644 akka-persistence/akka-persistence-mongo/src/test/scala/MongoTicket343Spec.scala create mode 100644 akka-persistence/akka-persistence-redis/src/test/scala/RedisTicket343Spec.scala diff --git a/akka-persistence/akka-persistence-cassandra/src/main/scala/CassandraStorage.scala b/akka-persistence/akka-persistence-cassandra/src/main/scala/CassandraStorage.scala index be5fc4f4c7..0c6f239ef7 100644 --- a/akka-persistence/akka-persistence-cassandra/src/main/scala/CassandraStorage.scala +++ b/akka-persistence/akka-persistence-cassandra/src/main/scala/CassandraStorage.scala @@ -29,7 +29,7 @@ object CassandraStorage extends Storage { * * @author Jonas Bonér */ -class CassandraPersistentMap(id: String) extends PersistentMap[Array[Byte], Array[Byte]] { +class CassandraPersistentMap(id: String) extends PersistentMapBinary { val uuid = id val storage = CassandraStorageBackend } diff --git a/akka-persistence/akka-persistence-common/src/main/scala/Storage.scala b/akka-persistence/akka-persistence-common/src/main/scala/Storage.scala index ccaf7518f1..4d9ff48a60 100644 --- a/akka-persistence/akka-persistence-common/src/main/scala/Storage.scala +++ b/akka-persistence/akka-persistence-common/src/main/scala/Storage.scala @@ -7,9 +7,11 @@ package se.scalablesolutions.akka.persistence.common import se.scalablesolutions.akka.stm._ import se.scalablesolutions.akka.stm.TransactionManagement.transaction import se.scalablesolutions.akka.util.Logging -import se.scalablesolutions.akka.AkkaException -class StorageException(message: String) extends AkkaException(message) +// FIXME move to 'stm' package + add message with more info +class NoTransactionInScopeException extends RuntimeException + +class StorageException(message: String) extends RuntimeException(message) /** * Example Scala usage. @@ -80,24 +82,90 @@ trait Storage { */ trait PersistentMap[K, V] extends scala.collection.mutable.Map[K, V] with Transactional with Committable with Abortable with Logging { - protected val newAndUpdatedEntries = TransactionalMap[K, V]() - protected val removedEntries = TransactionalVector[K]() protected val shouldClearOnCommit = Ref[Boolean]() + // operations on the Map + trait Op + case object GET extends Op + case object PUT extends Op + case object REM extends Op + case object UPD extends Op + + // append only log: records all mutating operations + protected val appendOnlyTxLog = TransactionalVector[LogEntry]() + + case class LogEntry(key: K, value: Option[V], op: Op) + + // need to override in subclasses e.g. "sameElements" for Array[Byte] + def equal(k1: K, k2: K): Boolean = k1 == k2 + + // Seqable type that's required for maintaining the log of distinct keys affected in current transaction + type T <: Equals + + // converts key K to the Seqable type Equals + def toEquals(k: K): T + + // keys affected in the current transaction + protected val keysInCurrentTx = TransactionalMap[T, K]() + + protected def addToListOfKeysInTx(key: K): Unit = + keysInCurrentTx += (toEquals(key), key) + + protected def clearDistinctKeys = keysInCurrentTx.clear + + protected def filterTxLogByKey(key: K): IndexedSeq[LogEntry] = + appendOnlyTxLog filter(e => equal(e.key, key)) + + // need to get current value considering the underlying storage as well as the transaction log + protected def getCurrentValue(key: K): Option[V] = { + + // get all mutating entries for this key for this tx + val txEntries = filterTxLogByKey(key) + + // get the snapshot from the underlying store for this key + val underlying = try { + storage.getMapStorageEntryFor(uuid, key) + } catch { case e: Exception => None } + + if (txEntries.isEmpty) underlying + else replay(txEntries, key, underlying) + } + + // replay all tx entries for key k with seed = initial + private def replay(txEntries: IndexedSeq[LogEntry], key: K, initial: Option[V]): Option[V] = { + import scala.collection.mutable._ + + val m = initial match { + case None => Map.empty[K, V] + case Some(v) => Map((key, v)) + } + txEntries.foreach {case LogEntry(k, v, o) => o match { + case PUT => m.put(k, v.get) + case REM => m -= k + case UPD => m.update(k, v.get) + }} + m get key + } + // to be concretized in subclasses val storage: MapStorageBackend[K, V] def commit = { - if (shouldClearOnCommit.isDefined && shouldClearOnCommit.get) storage.removeMapStorageFor(uuid) - removedEntries.toList.foreach(key => storage.removeMapStorageFor(uuid, key)) - storage.insertMapStorageEntriesFor(uuid, newAndUpdatedEntries.toList) - newAndUpdatedEntries.clear - removedEntries.clear + // if (shouldClearOnCommit.isDefined && shouldClearOnCommit.get) storage.removeMapStorageFor(uuid) + + appendOnlyTxLog.foreach { case LogEntry(k, v, o) => o match { + case PUT => storage.insertMapStorageEntryFor(uuid, k, v.get) + case UPD => storage.insertMapStorageEntryFor(uuid, k, v.get) + case REM => storage.removeMapStorageFor(uuid, k) + }} + + appendOnlyTxLog.clear + clearDistinctKeys } def abort = { - newAndUpdatedEntries.clear - removedEntries.clear + appendOnlyTxLog.clear + clearDistinctKeys shouldClearOnCommit.swap(false) } @@ -118,68 +186,84 @@ trait PersistentMap[K, V] extends scala.collection.mutable.Map[K, V] override def put(key: K, value: V): Option[V] = { register - newAndUpdatedEntries.put(key, value) + val curr = getCurrentValue(key) + appendOnlyTxLog add LogEntry(key, Some(value), PUT) + addToListOfKeysInTx(key) + curr } override def update(key: K, value: V) = { register - newAndUpdatedEntries.update(key, value) + val curr = getCurrentValue(key) + appendOnlyTxLog add LogEntry(key, Some(value), UPD) + addToListOfKeysInTx(key) + curr } override def remove(key: K) = { register - removedEntries.add(key) - newAndUpdatedEntries.get(key) + val curr = getCurrentValue(key) + appendOnlyTxLog add LogEntry(key, None, REM) + addToListOfKeysInTx(key) + curr } - def slice(start: Option[K], count: Int): List[Tuple2[K, V]] = + def slice(start: Option[K], count: Int): List[(K, V)] = slice(start, None, count) - def slice(start: Option[K], finish: Option[K], count: Int): List[Tuple2[K, V]] = try { - storage.getMapStorageRangeFor(uuid, start, finish, count) - } catch { case e: Exception => Nil } + def slice(start: Option[K], finish: Option[K], count: Int): List[(K, V)] override def clear = { register + appendOnlyTxLog.clear + clearDistinctKeys shouldClearOnCommit.swap(true) } override def contains(key: K): Boolean = try { - newAndUpdatedEntries.contains(key) || - storage.getMapStorageEntryFor(uuid, key).isDefined + filterTxLogByKey(key) match { + case Seq() => // current tx doesn't use this + storage.getMapStorageEntryFor(uuid, key).isDefined // check storage + case txs => // present in log + txs.last.op != REM // last entry cannot be a REM + } } catch { case e: Exception => false } + protected def existsInStorage(key: K): Option[V] = try { + storage.getMapStorageEntryFor(uuid, key) + } catch { + case e: Exception => None + } + override def size: Int = try { - storage.getMapStorageSizeFor(uuid) - } catch { case e: Exception => 0 } + // partition key set affected in current tx into those which r added & which r deleted + val (keysAdded, keysRemoved) = keysInCurrentTx.map { + case (kseq, k) => ((kseq, k), getCurrentValue(k)) + }.partition(_._2.isDefined) - override def get(key: K): Option[V] = { - if (newAndUpdatedEntries.contains(key)) { - newAndUpdatedEntries.get(key) - } - else try { - storage.getMapStorageEntryFor(uuid, key) - } catch { case e: Exception => None } + // keys which existed in storage but removed in current tx + val inStorageRemovedInTx = + keysRemoved.keySet + .map(_._2) + .filter(k => existsInStorage(k).isDefined) + .size + + // all keys in storage + val keysInStorage = + storage.getMapStorageFor(uuid) + .map { case (k, v) => toEquals(k) } + .toSet + + // (keys that existed UNION keys added ) - (keys removed) + (keysInStorage union keysAdded.keySet.map(_._1)).size - inStorageRemovedInTx + } catch { + case e: Exception => 0 } - def iterator = elements + // get must consider underlying storage & current uncommitted tx log + override def get(key: K): Option[V] = getCurrentValue(key) - override def elements: Iterator[Tuple2[K, V]] = { - new Iterator[Tuple2[K, V]] { - private val originalList: List[Tuple2[K, V]] = try { - storage.getMapStorageFor(uuid) - } catch { - case e: Throwable => Nil - } - private var elements = newAndUpdatedEntries.toList union originalList.reverse - override def next: Tuple2[K, V]= synchronized { - val element = elements.head - elements = elements.tail - element - } - override def hasNext: Boolean = synchronized { !elements.isEmpty } - } - } + def iterator: Iterator[Tuple2[K, V]] private def register = { if (transaction.get.isEmpty) throw new NoTransactionInScopeException @@ -187,6 +271,95 @@ trait PersistentMap[K, V] extends scala.collection.mutable.Map[K, V] } } +trait PersistentMapBinary extends PersistentMap[Array[Byte], Array[Byte]] { + import scala.collection.mutable.ArraySeq + + type T = ArraySeq[Byte] + def toEquals(k: Array[Byte]) = ArraySeq(k: _*) + override def equal(k1: Array[Byte], k2: Array[Byte]): Boolean = k1 sameElements k2 + + object COrdering { + implicit object ArraySeqOrdering extends Ordering[ArraySeq[Byte]] { + def compare(o1: ArraySeq[Byte], o2: ArraySeq[Byte]) = + new String(o1.toArray) compare new String(o2.toArray) + } + } + + import scala.collection.immutable.{TreeMap, SortedMap} + private def replayAllKeys: SortedMap[ArraySeq[Byte], Array[Byte]] = { + import COrdering._ + + // need ArraySeq for ordering + val fromStorage = + TreeMap(storage.getMapStorageFor(uuid).map { case (k, v) => (ArraySeq(k: _*), v) }: _*) + + val (keysAdded, keysRemoved) = keysInCurrentTx.map { + case (_, k) => (k, getCurrentValue(k)) + }.partition(_._2.isDefined) + + val inStorageRemovedInTx = + keysRemoved.keySet + .filter(k => existsInStorage(k).isDefined) + .map(k => ArraySeq(k: _*)) + + (fromStorage -- inStorageRemovedInTx) ++ keysAdded.map { case (k, Some(v)) => (ArraySeq(k: _*), v) } + } + + override def slice(start: Option[Array[Byte]], finish: Option[Array[Byte]], count: Int): List[(Array[Byte], Array[Byte])] = try { + val newMap = replayAllKeys + + if (newMap isEmpty) List[(Array[Byte], Array[Byte])]() + + val startKey = + start match { + case Some(bytes) => Some(ArraySeq(bytes: _*)) + case None => None + } + + val endKey = + finish match { + case Some(bytes) => Some(ArraySeq(bytes: _*)) + case None => None + } + + ((startKey, endKey, count): @unchecked) match { + case ((Some(s), Some(e), _)) => + newMap.range(s, e) + .toList + .map(e => (e._1.toArray, e._2)) + .toList + case ((Some(s), None, c)) if c > 0 => + newMap.from(s) + .iterator + .take(count) + .map(e => (e._1.toArray, e._2)) + .toList + case ((Some(s), None, _)) => + newMap.from(s) + .toList + .map(e => (e._1.toArray, e._2)) + .toList + case ((None, Some(e), _)) => + newMap.until(e) + .toList + .map(e => (e._1.toArray, e._2)) + .toList + } + } catch { case e: Exception => Nil } + + override def iterator: Iterator[(Array[Byte], Array[Byte])] = { + new Iterator[(Array[Byte], Array[Byte])] { + private var elements = replayAllKeys + override def next: (Array[Byte], Array[Byte]) = synchronized { + val (k, v) = elements.head + elements = elements.tail + (k.toArray, v) + } + override def hasNext: Boolean = synchronized { !elements.isEmpty } + } + } +} + /** * Implements a template for a concrete persistent transactional vector based storage. * @@ -198,42 +371,83 @@ trait PersistentVector[T] extends IndexedSeq[T] with Transactional with Committa protected val removedElems = TransactionalVector[T]() protected val shouldClearOnCommit = Ref[Boolean]() + // operations on the Vector + trait Op + case object ADD extends Op + case object UPD extends Op + case object POP extends Op + + // append only log: records all mutating operations + protected val appendOnlyTxLog = TransactionalVector[LogEntry]() + + case class LogEntry(index: Option[Int], value: Option[T], op: Op) + + // need to override in subclasses e.g. "sameElements" for Array[Byte] + def equal(v1: T, v2: T): Boolean = v1 == v2 + val storage: VectorStorageBackend[T] def commit = { - for (element <- newElems) storage.insertVectorStorageEntryFor(uuid, element) - for (entry <- updatedElems) storage.updateVectorStorageEntryFor(uuid, entry._1, entry._2) - newElems.clear - updatedElems.clear + for(entry <- appendOnlyTxLog) { + entry match { + case LogEntry(_, Some(v), ADD) => storage.insertVectorStorageEntryFor(uuid, v) + case LogEntry(Some(i), Some(v), UPD) => storage.updateVectorStorageEntryFor(uuid, i, v) + case LogEntry(_, _, POP) => //.. + } + } + appendOnlyTxLog.clear } def abort = { - newElems.clear - updatedElems.clear - removedElems.clear + appendOnlyTxLog.clear shouldClearOnCommit.swap(false) } + private def replay: List[T] = { + import scala.collection.mutable.ArrayBuffer + var elemsStorage = ArrayBuffer(storage.getVectorStorageRangeFor(uuid, None, None, storage.getVectorStorageSizeFor(uuid)).reverse: _*) + + for(entry <- appendOnlyTxLog) { + entry match { + case LogEntry(_, Some(v), ADD) => elemsStorage += v + case LogEntry(Some(i), Some(v), UPD) => elemsStorage.update(i, v) + case LogEntry(_, _, POP) => elemsStorage = elemsStorage.drop(1) + } + } + elemsStorage.toList.reverse + } + def +(elem: T) = add(elem) def add(elem: T) = { register - newElems + elem + appendOnlyTxLog + LogEntry(None, Some(elem), ADD) } def apply(index: Int): T = get(index) def get(index: Int): T = { - if (newElems.size > index) newElems(index) - else storage.getVectorStorageEntryFor(uuid, index) + if (appendOnlyTxLog.isEmpty) { + storage.getVectorStorageEntryFor(uuid, index) + } else { + val curr = replay + curr(index) + } } override def slice(start: Int, finish: Int): IndexedSeq[T] = slice(Some(start), Some(finish)) def slice(start: Option[Int], finish: Option[Int], count: Int = 0): IndexedSeq[T] = { - val buffer = new scala.collection.mutable.ArrayBuffer[T] - storage.getVectorStorageRangeFor(uuid, start, finish, count).foreach(buffer.append(_)) - buffer + val curr = replay + val s = if (start.isDefined) start.get else 0 + val cnt = + if (finish.isDefined) { + val f = finish.get + if (f >= s) (f - s) else count + } + else count + if (s == 0 && cnt == 0) List().toIndexedSeq + else curr.slice(s, s + cnt).toIndexedSeq } /** @@ -241,12 +455,13 @@ trait PersistentVector[T] extends IndexedSeq[T] with Transactional with Committa */ def pop: T = { register + appendOnlyTxLog + LogEntry(None, None, POP) throw new UnsupportedOperationException("PersistentVector::pop is not implemented") } def update(index: Int, newElem: T) = { register - storage.updateVectorStorageEntryFor(uuid, index, newElem) + appendOnlyTxLog + LogEntry(Some(index), Some(newElem), UPD) } override def first: T = get(0) @@ -260,7 +475,7 @@ trait PersistentVector[T] extends IndexedSeq[T] with Transactional with Committa } } - def length: Int = storage.getVectorStorageSizeFor(uuid) + newElems.length + def length: Int = replay.length private def register = { if (transaction.get.isEmpty) throw new NoTransactionInScopeException diff --git a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala index 79cacfeb07..83e47e3ba5 100644 --- a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala +++ b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorage.scala @@ -29,7 +29,7 @@ object MongoStorage extends Storage { * * @author Debasish Ghosh */ -class MongoPersistentMap(id: String) extends PersistentMap[Array[Byte], Array[Byte]] { +class MongoPersistentMap(id: String) extends PersistentMapBinary { val uuid = id val storage = MongoStorageBackend } diff --git a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala index 847c226630..d51ff17dab 100644 --- a/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala +++ b/akka-persistence/akka-persistence-mongo/src/main/scala/MongoStorageBackend.scala @@ -64,51 +64,60 @@ private[akka] object MongoStorageBackend extends coll.remove(q) } - private def queryFor[T](name: String)(body: (MongoDBObject, MongoDBObject) => T): T = { - val q: DBObject = MongoDBObject(KEY -> name) - val dbo = coll.findOne(q).getOrElse { throw new NoSuchElementException(name + " not present") } - body(q, dbo) + + private def queryFor[T](name: String)(body: (MongoDBObject, Option[DBObject]) => T): T = { + val q = MongoDBObject(KEY -> name) + body(q, coll.findOne(q)) } def removeMapStorageFor(name: String, key: Array[Byte]): Unit = queryFor(name) { (q, dbo) => - dbo -= new String(key) - coll.update(q, dbo, true, false) + dbo.foreach { d => + d -= new String(key) + coll.update(q, d, true, false) + } } def getMapStorageEntryFor(name: String, key: Array[Byte]): Option[Array[Byte]] = queryFor(name) { (q, dbo) => - dbo.get(new String(key)).asInstanceOf[Option[Array[Byte]]] + dbo.map { d => + Option(d.get(new String(key))).asInstanceOf[Option[Array[Byte]]] + }.getOrElse(None) } def getMapStorageSizeFor(name: String): Int = queryFor(name) { (q, dbo) => - dbo.size - 2 // need to exclude object id and our KEY + dbo.map { d => + d.size - 2 // need to exclude object id and our KEY + }.getOrElse(0) } def getMapStorageFor(name: String): List[(Array[Byte], Array[Byte])] = queryFor(name) { (q, dbo) => - for { - (k, v) <- dbo.toList - if k != "_id" && k != KEY - } yield (k.getBytes, v.asInstanceOf[Array[Byte]]) + dbo.map { d => + for { + (k, v) <- d.toList + if k != "_id" && k != KEY + } yield (k.getBytes, v.asInstanceOf[Array[Byte]]) + }.getOrElse(List.empty[(Array[Byte], Array[Byte])]) } def getMapStorageRangeFor(name: String, start: Option[Array[Byte]], finish: Option[Array[Byte]], count: Int): List[(Array[Byte], Array[Byte])] = queryFor(name) { (q, dbo) => - // get all keys except the special ones - val keys = - dbo.keySet - .toList - .filter(k => k != "_id" && k != KEY) - .sortWith(_ < _) + dbo.map { d => + // get all keys except the special ones + val keys = d.keys + .filter(k => k != "_id" && k != KEY) + .toList + .sortWith(_ < _) - // if the supplied start is not defined, get the head of keys - val s = start.map(new String(_)).getOrElse(keys.head) + // if the supplied start is not defined, get the head of keys + val s = start.map(new String(_)).getOrElse(keys.head) - // if the supplied finish is not defined, get the last element of keys - val f = finish.map(new String(_)).getOrElse(keys.last) + // if the supplied finish is not defined, get the last element of keys + val f = finish.map(new String(_)).getOrElse(keys.last) - // slice from keys: both ends inclusive - val ks = keys.slice(keys.indexOf(s), scala.math.min(count, keys.indexOf(f) + 1)) - ks.map(k => (k.getBytes, dbo.get(k).get.asInstanceOf[Array[Byte]])) + // slice from keys: both ends inclusive + val ks = keys.slice(keys.indexOf(s), scala.math.min(count, keys.indexOf(f) + 1)) + ks.map(k => (k.getBytes, d.get(k).asInstanceOf[Array[Byte]])) + }.getOrElse(List.empty[(Array[Byte], Array[Byte])]) } def insertVectorStorageEntryFor(name: String, element: Array[Byte]) = { @@ -147,12 +156,16 @@ private[akka] object MongoStorageBackend extends } def updateVectorStorageEntryFor(name: String, index: Int, elem: Array[Byte]) = queryFor(name) { (q, dbo) => - dbo += ((index.toString, elem)) - coll.update(q, dbo, true, false) + dbo.foreach { d => + d += ((index.toString, elem)) + coll.update(q, d, true, false) + } } def getVectorStorageEntryFor(name: String, index: Int): Array[Byte] = queryFor(name) { (q, dbo) => - dbo(index.toString).asInstanceOf[Array[Byte]] + dbo.map { d => + d(index.toString).asInstanceOf[Array[Byte]] + }.getOrElse(Array.empty[Byte]) } /** @@ -162,24 +175,26 @@ private[akka] object MongoStorageBackend extends * if start == 0 and finish == 0, return an empty collection */ def getVectorStorageRangeFor(name: String, start: Option[Int], finish: Option[Int], count: Int): List[Array[Byte]] = queryFor(name) { (q, dbo) => - val ls = dbo.filter { case (k, v) => k != KEY && k != "_id" } + dbo.map { d => + val ls = d.filter { case (k, v) => k != KEY && k != "_id" } .toSeq .sortWith((e1, e2) => (e1._1.toInt < e2._1.toInt)) .map(_._2) - val st = start.getOrElse(0) - val cnt = - if (finish.isDefined) { - val f = finish.get - if (f >= st) (f - st) else count - } - else count - if (st == 0 && cnt == 0) List() - ls.slice(st, st + cnt).asInstanceOf[List[Array[Byte]]] + val st = start.getOrElse(0) + val cnt = + if (finish.isDefined) { + val f = finish.get + if (f >= st) (f - st) else count + } + else count + if (st == 0 && cnt == 0) List() + ls.slice(st, st + cnt).asInstanceOf[List[Array[Byte]]] + }.getOrElse(List.empty[Array[Byte]]) } def getVectorStorageSizeFor(name: String): Int = queryFor(name) { (q, dbo) => - dbo.size - 2 + dbo.map { d => d.size - 2 }.getOrElse(0) } def insertRefStorageFor(name: String, element: Array[Byte]) = { @@ -201,11 +216,9 @@ private[akka] object MongoStorageBackend extends } } - def getRefStorageFor(name: String): Option[Array[Byte]] = try { - queryFor(name) { (q, dbo) => - dbo.get(REF).asInstanceOf[Option[Array[Byte]]] - } - } catch { - case e: java.util.NoSuchElementException => None + def getRefStorageFor(name: String): Option[Array[Byte]] = queryFor(name) { (q, dbo) => + dbo.map { d => + Option(d.get(REF)).asInstanceOf[Option[Array[Byte]]] + }.getOrElse(None) } } 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 fb2034b6c1..e9576cc152 100644 --- a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala +++ b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoStorageSpec.scala @@ -46,7 +46,7 @@ class MongoStorageSpec extends new String(getMapStorageEntryFor("t1", "odersky".getBytes).get) should equal("scala") getMapStorageEntryFor("t1", "torvalds".getBytes) should equal(None) - evaluating { getMapStorageEntryFor("t2", "torvalds".getBytes) } should produce [NoSuchElementException] + getMapStorageEntryFor("t2", "torvalds".getBytes) should equal(None) getMapStorageFor("t1").map { case (k, v) => (new String(k), new String(v)) } should equal (l) @@ -54,7 +54,7 @@ class MongoStorageSpec extends getMapStorageSizeFor("t1") should equal(2) removeMapStorageFor("t1") - evaluating { getMapStorageSizeFor("t1") } should produce [NoSuchElementException] + getMapStorageSizeFor("t1") should equal(0) } it("should do proper range queries") { diff --git a/akka-persistence/akka-persistence-mongo/src/test/scala/MongoTicket343Spec.scala b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoTicket343Spec.scala new file mode 100644 index 0000000000..3b160c8c50 --- /dev/null +++ b/akka-persistence/akka-persistence-mongo/src/test/scala/MongoTicket343Spec.scala @@ -0,0 +1,347 @@ +package se.scalablesolutions.akka.persistence.mongo + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith + +import se.scalablesolutions.akka.actor.{Actor, ActorRef} +import se.scalablesolutions.akka.config.OneForOneStrategy +import Actor._ +import se.scalablesolutions.akka.stm.global._ +import se.scalablesolutions.akka.config.ScalaConfig._ +import se.scalablesolutions.akka.util.Logging + +import MongoStorageBackend._ + +case class GET(k: String) +case class SET(k: String, v: String) +case class REM(k: String) +case class CONTAINS(k: String) +case object MAP_SIZE +case class MSET(kvs: List[(String, String)]) +case class REMOVE_AFTER_PUT(kvsToAdd: List[(String, String)], ksToRem: List[String]) +case class CLEAR_AFTER_PUT(kvsToAdd: List[(String, String)]) +case class PUT_WITH_SLICE(kvsToAdd: List[(String, String)], start: String, cnt: Int) +case class PUT_REM_WITH_SLICE(kvsToAdd: List[(String, String)], ksToRem: List[String], start: String, cnt: Int) + +case class VADD(v: String) +case class VUPD(i: Int, v: String) +case class VUPD_AND_ABORT(i: Int, v: String) +case class VGET(i: Int) +case object VSIZE +case class VGET_AFTER_VADD(vsToAdd: List[String], isToFetch: List[Int]) +case class VADD_WITH_SLICE(vsToAdd: List[String], start: Int, cnt: Int) + +object Storage { + class MongoSampleMapStorage extends Actor { + self.lifeCycle = Some(LifeCycle(Permanent)) + val FOO_MAP = "akka.sample.map" + + private var fooMap = atomic { MongoStorage.getMap(FOO_MAP) } + + def receive = { + case SET(k, v) => + atomic { + fooMap += (k.getBytes, v.getBytes) + } + self.reply((k, v)) + + case GET(k) => + val v = atomic { + fooMap.get(k.getBytes).map(new String(_)).getOrElse(k + " Not found") + } + self.reply(v) + + case REM(k) => + val v = atomic { + fooMap -= k.getBytes + } + self.reply(k) + + case CONTAINS(k) => + val v = atomic { + fooMap contains k.getBytes + } + self.reply(v) + + case MAP_SIZE => + val v = atomic { + fooMap.size + } + self.reply(v) + + case MSET(kvs) => atomic { + kvs.foreach {kv => fooMap += (kv._1.getBytes, kv._2.getBytes) } + } + self.reply(kvs.size) + + case REMOVE_AFTER_PUT(kvs2add, ks2rem) => atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + + ks2rem.foreach {k => + fooMap -= k.getBytes + }} + self.reply(fooMap.size) + + case CLEAR_AFTER_PUT(kvs2add) => atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + fooMap.clear + } + self.reply(true) + + case PUT_WITH_SLICE(kvs2add, from, cnt) => + val v = atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + fooMap.slice(Some(from.getBytes), cnt) + } + self.reply(v: List[(Array[Byte], Array[Byte])]) + + case PUT_REM_WITH_SLICE(kvs2add, ks2rem, from, cnt) => + val v = atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + ks2rem.foreach {k => + fooMap -= k.getBytes + } + fooMap.slice(Some(from.getBytes), cnt) + } + self.reply(v: List[(Array[Byte], Array[Byte])]) + } + } + + class MongoSampleVectorStorage extends Actor { + self.lifeCycle = Some(LifeCycle(Permanent)) + val FOO_VECTOR = "akka.sample.vector" + + private var fooVector = atomic { MongoStorage.getVector(FOO_VECTOR) } + + def receive = { + case VADD(v) => + val size = + atomic { + fooVector + v.getBytes + fooVector length + } + self.reply(size) + + case VGET(index) => + val ind = + atomic { + fooVector get index + } + self.reply(ind) + + case VGET_AFTER_VADD(vs, is) => + val els = + atomic { + vs.foreach(fooVector + _.getBytes) + (is.foldRight(List[Array[Byte]]())(fooVector.get(_) :: _)).map(new String(_)) + } + self.reply(els) + + case VUPD_AND_ABORT(index, value) => + val l = + atomic { + fooVector.update(index, value.getBytes) + // force fail + fooVector get 100 + } + self.reply(index) + + case VADD_WITH_SLICE(vs, s, c) => + val l = + atomic { + vs.foreach(fooVector + _.getBytes) + fooVector.slice(Some(s), None, c) + } + self.reply(l.map(new String(_))) + } + } +} + +import Storage._ + +@RunWith(classOf[JUnitRunner]) +class MongoTicket343Spec extends + Spec with + ShouldMatchers with + BeforeAndAfterAll with + BeforeAndAfterEach { + + + override def beforeAll { + MongoStorageBackend.drop + println("** destroyed database") + } + + override def beforeEach { + MongoStorageBackend.drop + println("** destroyed database") + } + + override def afterEach { + MongoStorageBackend.drop + println("** destroyed database") + } + + describe("Ticket 343 Issue #1") { + it("remove after put should work within the same transaction") { + val proc = actorOf[MongoSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + (proc !! MSET(List(("dg", "1"), ("mc", "2"), ("nd", "3")))).getOrElse("Mset failed") should equal(3) + + (proc !! GET("dg")).getOrElse("Get failed") should equal("1") + (proc !! GET("mc")).getOrElse("Get failed") should equal("2") + (proc !! GET("nd")).getOrElse("Get failed") should equal("3") + + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(4) + + val add = List(("a", "1"), ("b", "2"), ("c", "3")) + val rem = List("a", "debasish") + (proc !! REMOVE_AFTER_PUT(add, rem)).getOrElse("REMOVE_AFTER_PUT failed") should equal(5) + + (proc !! GET("debasish")).getOrElse("debasish not found") should equal("debasish Not found") + (proc !! GET("a")).getOrElse("a not found") should equal("a Not found") + + (proc !! GET("b")).getOrElse("b not found") should equal("2") + + (proc !! CONTAINS("b")).getOrElse("b not found") should equal(true) + (proc !! CONTAINS("debasish")).getOrElse("debasish not found") should equal(false) + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(5) + proc.stop + } + } + + describe("Ticket 343 Issue #2") { + it("clear after put should work within the same transaction") { + val proc = actorOf[MongoSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + val add = List(("a", "1"), ("b", "2"), ("c", "3")) + (proc !! CLEAR_AFTER_PUT(add)).getOrElse("CLEAR_AFTER_PUT failed") should equal(true) + + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + proc.stop + } + } + + describe("Ticket 343 Issue #3") { + it("map size should change after the transaction") { + val proc = actorOf[MongoSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + (proc !! MSET(List(("dg", "1"), ("mc", "2"), ("nd", "3")))).getOrElse("Mset failed") should equal(3) + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(4) + + (proc !! GET("dg")).getOrElse("Get failed") should equal("1") + (proc !! GET("mc")).getOrElse("Get failed") should equal("2") + (proc !! GET("nd")).getOrElse("Get failed") should equal("3") + proc.stop + } + } + + describe("slice test") { + it("should pass") { + val proc = actorOf[MongoSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + // (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + (proc !! MSET(List(("dg", "1"), ("mc", "2"), ("nd", "3")))).getOrElse("Mset failed") should equal(3) + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(4) + + (proc !! PUT_WITH_SLICE(List(("ec", "1"), ("tb", "2"), ("mc", "10")), "dg", 3)).get.asInstanceOf[List[(Array[Byte], Array[Byte])]].map { case (k, v) => (new String(k), new String(v)) } should equal(List(("dg", "1"), ("ec", "1"), ("mc", "10"))) + + (proc !! PUT_REM_WITH_SLICE(List(("fc", "1"), ("gb", "2"), ("xy", "10")), List("tb", "fc"), "dg", 5)).get.asInstanceOf[List[(Array[Byte], Array[Byte])]].map { case (k, v) => (new String(k), new String(v)) } should equal(List(("dg", "1"), ("ec", "1"), ("gb", "2"), ("mc", "10"), ("nd", "3"))) + proc.stop + } + } + + describe("Ticket 343 Issue #4") { + it("vector get should not ignore elements that were in vector before transaction") { + + val proc = actorOf[MongoSampleVectorStorage] + proc.start + + // add 4 elements in separate transactions + (proc !! VADD("debasish")).getOrElse("VADD failed") should equal(1) + (proc !! VADD("maulindu")).getOrElse("VADD failed") should equal(2) + (proc !! VADD("ramanendu")).getOrElse("VADD failed") should equal(3) + (proc !! VADD("nilanjan")).getOrElse("VADD failed") should equal(4) + + new String((proc !! VGET(0)).get.asInstanceOf[Array[Byte]] ) should equal("nilanjan") + new String((proc !! VGET(1)).get.asInstanceOf[Array[Byte]] ) should equal("ramanendu") + new String((proc !! VGET(2)).get.asInstanceOf[Array[Byte]] ) should equal("maulindu") + new String((proc !! VGET(3)).get.asInstanceOf[Array[Byte]] ) should equal("debasish") + + // now add 3 more and do gets in the same transaction + (proc !! VGET_AFTER_VADD(List("a", "b", "c"), List(0, 2, 4))).get.asInstanceOf[List[String]] should equal(List("c", "a", "ramanendu")) + proc.stop + } + } + + describe("Ticket 343 Issue #6") { + it("vector update should not ignore transaction") { + val proc = actorOf[MongoSampleVectorStorage] + proc.start + + // add 4 elements in separate transactions + (proc !! VADD("debasish")).getOrElse("VADD failed") should equal(1) + (proc !! VADD("maulindu")).getOrElse("VADD failed") should equal(2) + (proc !! VADD("ramanendu")).getOrElse("VADD failed") should equal(3) + (proc !! VADD("nilanjan")).getOrElse("VADD failed") should equal(4) + + evaluating { + (proc !! VUPD_AND_ABORT(0, "virat")).getOrElse("VUPD_AND_ABORT failed") + } should produce [Exception] + + // update aborts and hence values will remain unchanged + new String((proc !! VGET(0)).get.asInstanceOf[Array[Byte]] ) should equal("nilanjan") + proc.stop + } + } + + describe("Ticket 343 Issue #5") { + it("vector slice() should not ignore elements added in current transaction") { + val proc = actorOf[MongoSampleVectorStorage] + proc.start + + // add 4 elements in separate transactions + (proc !! VADD("debasish")).getOrElse("VADD failed") should equal(1) + (proc !! VADD("maulindu")).getOrElse("VADD failed") should equal(2) + (proc !! VADD("ramanendu")).getOrElse("VADD failed") should equal(3) + (proc !! VADD("nilanjan")).getOrElse("VADD failed") should equal(4) + + // slice with no new elements added in current transaction + (proc !! VADD_WITH_SLICE(List(), 2, 2)).getOrElse("VADD_WITH_SLICE failed") should equal(Vector("maulindu", "debasish")) + + // slice with new elements added in current transaction + (proc !! VADD_WITH_SLICE(List("a", "b", "c", "d"), 2, 2)).getOrElse("VADD_WITH_SLICE failed") should equal(Vector("b", "a")) + proc.stop + } + } +} diff --git a/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorage.scala b/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorage.scala index c92761beea..1eca775567 100644 --- a/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorage.scala +++ b/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorage.scala @@ -36,7 +36,7 @@ object RedisStorage extends Storage { * * @author Debasish Ghosh */ -class RedisPersistentMap(id: String) extends PersistentMap[Array[Byte], Array[Byte]] { +class RedisPersistentMap(id: String) extends PersistentMapBinary { val uuid = id val storage = RedisStorageBackend } diff --git a/akka-persistence/akka-persistence-redis/src/test/scala/RedisTicket343Spec.scala b/akka-persistence/akka-persistence-redis/src/test/scala/RedisTicket343Spec.scala new file mode 100644 index 0000000000..de236b9a5a --- /dev/null +++ b/akka-persistence/akka-persistence-redis/src/test/scala/RedisTicket343Spec.scala @@ -0,0 +1,351 @@ +package se.scalablesolutions.akka.persistence.redis + +import org.scalatest.Spec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith + +import se.scalablesolutions.akka.actor.{Actor} +import se.scalablesolutions.akka.config.OneForOneStrategy +import Actor._ +import se.scalablesolutions.akka.persistence.common.PersistentVector +import se.scalablesolutions.akka.stm.global._ +import se.scalablesolutions.akka.config.ScalaConfig._ +import se.scalablesolutions.akka.util.Logging + +import RedisStorageBackend._ + +case class GET(k: String) +case class SET(k: String, v: String) +case class REM(k: String) +case class CONTAINS(k: String) +case object MAP_SIZE +case class MSET(kvs: List[(String, String)]) +case class REMOVE_AFTER_PUT(kvsToAdd: List[(String, String)], ksToRem: List[String]) +case class CLEAR_AFTER_PUT(kvsToAdd: List[(String, String)]) +case class PUT_WITH_SLICE(kvsToAdd: List[(String, String)], start: String, cnt: Int) +case class PUT_REM_WITH_SLICE(kvsToAdd: List[(String, String)], ksToRem: List[String], start: String, cnt: Int) + +case class VADD(v: String) +case class VUPD(i: Int, v: String) +case class VUPD_AND_ABORT(i: Int, v: String) +case class VGET(i: Int) +case object VSIZE +case class VGET_AFTER_VADD(vsToAdd: List[String], isToFetch: List[Int]) +case class VADD_WITH_SLICE(vsToAdd: List[String], start: Int, cnt: Int) + +object Storage { + class RedisSampleMapStorage extends Actor { + self.lifeCycle = Some(LifeCycle(Permanent)) + val FOO_MAP = "akka.sample.map" + + private var fooMap = atomic { RedisStorage.getMap(FOO_MAP) } + + def receive = { + case SET(k, v) => + atomic { + fooMap += (k.getBytes, v.getBytes) + } + self.reply((k, v)) + + case GET(k) => + val v = atomic { + fooMap.get(k.getBytes) + } + self.reply(v.collect {case byte => new String(byte)}.getOrElse(k + " Not found")) + + case REM(k) => + val v = atomic { + fooMap -= k.getBytes + } + self.reply(k) + + case CONTAINS(k) => + val v = atomic { + fooMap contains k.getBytes + } + self.reply(v) + + case MAP_SIZE => + val v = atomic { + fooMap.size + } + self.reply(v) + + case MSET(kvs) => + atomic { + kvs.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + } + self.reply(kvs.size) + + case REMOVE_AFTER_PUT(kvs2add, ks2rem) => + val v = + atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + + ks2rem.foreach {k => + fooMap -= k.getBytes + } + fooMap.size + } + self.reply(v) + + case CLEAR_AFTER_PUT(kvs2add) => + atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + fooMap.clear + } + self.reply(true) + + case PUT_WITH_SLICE(kvs2add, from, cnt) => + val v = + atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + fooMap.slice(Some(from.getBytes), cnt) + } + self.reply(v: List[(Array[Byte], Array[Byte])]) + + case PUT_REM_WITH_SLICE(kvs2add, ks2rem, from, cnt) => + val v = + atomic { + kvs2add.foreach {kv => + fooMap += (kv._1.getBytes, kv._2.getBytes) + } + ks2rem.foreach {k => + fooMap -= k.getBytes + } + fooMap.slice(Some(from.getBytes), cnt) + } + self.reply(v: List[(Array[Byte], Array[Byte])]) + } + } + + class RedisSampleVectorStorage extends Actor { + self.lifeCycle = Some(LifeCycle(Permanent)) + val FOO_VECTOR = "akka.sample.vector" + + private var fooVector = atomic { RedisStorage.getVector(FOO_VECTOR) } + + def receive = { + case VADD(v) => + val size = + atomic { + fooVector + v.getBytes + fooVector length + } + self.reply(size) + + case VGET(index) => + val ind = + atomic { + fooVector get index + } + self.reply(ind) + + case VGET_AFTER_VADD(vs, is) => + val els = + atomic { + vs.foreach(fooVector + _.getBytes) + (is.foldRight(List[Array[Byte]]())(fooVector.get(_) :: _)).map(new String(_)) + } + self.reply(els) + + case VUPD_AND_ABORT(index, value) => + val l = + atomic { + fooVector.update(index, value.getBytes) + // force fail + fooVector get 100 + } + self.reply(index) + + case VADD_WITH_SLICE(vs, s, c) => + val l = + atomic { + vs.foreach(fooVector + _.getBytes) + fooVector.slice(Some(s), None, c) + } + self.reply(l.map(new String(_))) + } + } +} + +import Storage._ + +@RunWith(classOf[JUnitRunner]) +class RedisTicket343Spec extends + Spec with + ShouldMatchers with + BeforeAndAfterAll with + BeforeAndAfterEach { + + override def beforeAll { + flushDB + println("** destroyed database") + } + + override def afterEach { + flushDB + println("** destroyed database") + } + + describe("Ticket 343 Issue #1") { + it("remove after put should work within the same transaction") { + val proc = actorOf[RedisSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + (proc !! MSET(List(("dg", "1"), ("mc", "2"), ("nd", "3")))).getOrElse("Mset failed") should equal(3) + + (proc !! GET("dg")).getOrElse("Get failed") should equal("1") + (proc !! GET("mc")).getOrElse("Get failed") should equal("2") + (proc !! GET("nd")).getOrElse("Get failed") should equal("3") + + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(4) + + val add = List(("a", "1"), ("b", "2"), ("c", "3")) + val rem = List("a", "debasish") + (proc !! REMOVE_AFTER_PUT(add, rem)).getOrElse("REMOVE_AFTER_PUT failed") should equal(5) + + (proc !! GET("debasish")).getOrElse("debasish not found") should equal("debasish Not found") + (proc !! GET("a")).getOrElse("a not found") should equal("a Not found") + + (proc !! GET("b")).getOrElse("b not found") should equal("2") + + (proc !! CONTAINS("b")).getOrElse("b not found") should equal(true) + (proc !! CONTAINS("debasish")).getOrElse("debasish not found") should equal(false) + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(5) + proc.stop + } + } + + describe("Ticket 343 Issue #2") { + it("clear after put should work within the same transaction") { + val proc = actorOf[RedisSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + val add = List(("a", "1"), ("b", "2"), ("c", "3")) + (proc !! CLEAR_AFTER_PUT(add)).getOrElse("CLEAR_AFTER_PUT failed") should equal(true) + + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + proc.stop + } + } + + describe("Ticket 343 Issue #3") { + it("map size should change after the transaction") { + val proc = actorOf[RedisSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + (proc !! MSET(List(("dg", "1"), ("mc", "2"), ("nd", "3")))).getOrElse("Mset failed") should equal(3) + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(4) + + (proc !! GET("dg")).getOrElse("Get failed") should equal("1") + (proc !! GET("mc")).getOrElse("Get failed") should equal("2") + (proc !! GET("nd")).getOrElse("Get failed") should equal("3") + proc.stop + } + } + + describe("slice test") { + it("should pass") { + val proc = actorOf[RedisSampleMapStorage] + proc.start + + (proc !! SET("debasish", "anshinsoft")).getOrElse("Set failed") should equal(("debasish", "anshinsoft")) + (proc !! GET("debasish")).getOrElse("Get failed") should equal("anshinsoft") + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(1) + + (proc !! MSET(List(("dg", "1"), ("mc", "2"), ("nd", "3")))).getOrElse("Mset failed") should equal(3) + (proc !! MAP_SIZE).getOrElse("Size failed") should equal(4) + + (proc !! PUT_WITH_SLICE(List(("ec", "1"), ("tb", "2"), ("mc", "10")), "dg", 3)).get.asInstanceOf[List[(Array[Byte], Array[Byte])]].map { case (k, v) => (new String(k), new String(v)) } should equal(List(("dg", "1"), ("ec", "1"), ("mc", "10"))) + + (proc !! PUT_REM_WITH_SLICE(List(("fc", "1"), ("gb", "2"), ("xy", "10")), List("tb", "fc"), "dg", 5)).get.asInstanceOf[List[(Array[Byte], Array[Byte])]].map { case (k, v) => (new String(k), new String(v)) } should equal(List(("dg", "1"), ("ec", "1"), ("gb", "2"), ("mc", "10"), ("nd", "3"))) + proc.stop + } + } + + describe("Ticket 343 Issue #4") { + it("vector get should not ignore elements that were in vector before transaction") { + val proc = actorOf[RedisSampleVectorStorage] + proc.start + + // add 4 elements in separate transactions + (proc !! VADD("debasish")).getOrElse("VADD failed") should equal(1) + (proc !! VADD("maulindu")).getOrElse("VADD failed") should equal(2) + (proc !! VADD("ramanendu")).getOrElse("VADD failed") should equal(3) + (proc !! VADD("nilanjan")).getOrElse("VADD failed") should equal(4) + + new String((proc !! VGET(0)).get.asInstanceOf[Array[Byte]] ) should equal("nilanjan") + new String((proc !! VGET(1)).get.asInstanceOf[Array[Byte]] ) should equal("ramanendu") + new String((proc !! VGET(2)).get.asInstanceOf[Array[Byte]] ) should equal("maulindu") + new String((proc !! VGET(3)).get.asInstanceOf[Array[Byte]] ) should equal("debasish") + + // now add 3 more and do gets in the same transaction + (proc !! VGET_AFTER_VADD(List("a", "b", "c"), List(0, 2, 4))).get.asInstanceOf[List[String]] should equal(List("c", "a", "ramanendu")) + proc.stop + } + } + + describe("Ticket 343 Issue #6") { + it("vector update should not ignore transaction") { + val proc = actorOf[RedisSampleVectorStorage] + proc.start + + // add 4 elements in separate transactions + (proc !! VADD("debasish")).getOrElse("VADD failed") should equal(1) + (proc !! VADD("maulindu")).getOrElse("VADD failed") should equal(2) + (proc !! VADD("ramanendu")).getOrElse("VADD failed") should equal(3) + (proc !! VADD("nilanjan")).getOrElse("VADD failed") should equal(4) + + evaluating { + (proc !! VUPD_AND_ABORT(0, "virat")).getOrElse("VUPD_AND_ABORT failed") + } should produce [Exception] + + // update aborts and hence values will remain unchanged + new String((proc !! VGET(0)).get.asInstanceOf[Array[Byte]] ) should equal("nilanjan") + proc.stop + } + } + + describe("Ticket 343 Issue #5") { + it("vector slice() should not ignore elements added in current transaction") { + val proc = actorOf[RedisSampleVectorStorage] + proc.start + + // add 4 elements in separate transactions + (proc !! VADD("debasish")).getOrElse("VADD failed") should equal(1) + (proc !! VADD("maulindu")).getOrElse("VADD failed") should equal(2) + (proc !! VADD("ramanendu")).getOrElse("VADD failed") should equal(3) + (proc !! VADD("nilanjan")).getOrElse("VADD failed") should equal(4) + + // slice with no new elements added in current transaction + (proc !! VADD_WITH_SLICE(List(), 2, 2)).getOrElse("VADD_WITH_SLICE failed") should equal(Vector("maulindu", "debasish")) + + // slice with new elements added in current transaction + (proc !! VADD_WITH_SLICE(List("a", "b", "c", "d"), 2, 2)).getOrElse("VADD_WITH_SLICE failed") should equal(Vector("b", "a")) + proc.stop + } + } +} diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index 0eb289a290..0ee2b18108 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -482,7 +482,7 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { val commons_codec = Dependencies.commons_codec val redis = Dependencies.redis - override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil + // override def testOptions = TestFilter((name: String) => name.endsWith("Test")) :: Nil } // ------------------------------------------------------------------------------------------------------------------- From 5a2529b10569f1eb05f83d5d6663d6731f091ac3 Mon Sep 17 00:00:00 2001 From: Debasish Ghosh Date: Sat, 11 Sep 2010 01:03:48 +0530 Subject: [PATCH 11/25] redis keys are no longer base64-ed. Though values are --- .../src/main/scala/RedisStorageBackend.scala | 95 ++++++++----------- 1 file changed, 38 insertions(+), 57 deletions(-) diff --git a/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorageBackend.scala b/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorageBackend.scala index 9200393ef9..61595ec21f 100644 --- a/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorageBackend.scala +++ b/akka-persistence/akka-persistence-redis/src/main/scala/RedisStorageBackend.scala @@ -96,12 +96,12 @@ private [akka] object RedisStorageBackend extends *