Permalink
Browse files

! routing: Added `detach` directive which executes its inner route in…

… a future. Removed `detachTo` directive. Fixes #240.

The `detachTo` directive was removed and replaced with `detach` which executes its inner route in a `Future` (as opposed to
executing it inside an actor). In order to migrate, users will have to replace their use of `detachTo`, which most commonly
was used with `singleRequestServiceActor`, with a parameterless call to detach (i.e. `detach()`) if they have either an
implicit `ExecutionContext` or `ActorRefFactory` in scope. Additionally, it is also possible to call detach with an explicit `ExecutionContext` (e.g. `detach(ec)`).
  • Loading branch information...
andresilva committed Jul 23, 2013
1 parent ea7f609 commit ead4a70e08e3c98ff1d1eddfcd8ec8a1e9f56279
@@ -74,7 +74,7 @@ class HttpServiceExamplesSpec extends Specification with Specs2RouteTest {
// unmarshal with in-scope unmarshaller
entity(as[Order]) { order =>
// transfer to newly spawned actor
- detachTo(singleRequestServiceActor) {
+ detach() {
complete {
// ... write order to DB
"Order received"
@@ -0,0 +1,6 @@
+.. _-detach-:
+
+detach
+========
+
+(todo)
@@ -1,6 +0,0 @@
-.. _-detachTo-:
-
-detachTo
-========
-
-(todo)
@@ -6,7 +6,7 @@ ExecutionDirectives
.. toctree::
:maxdepth: 1
- detachTo
+ detach
dynamic
dynamicIf
handleExceptions
@@ -4,7 +4,8 @@ Exception Handling
==================
Exceptions thrown during route execution bubble up throw the route structure up to the next enclosing
-``handleExceptions`` directive, the main ``runRoute`` wrapper or the ``receive`` function of a ``detachTo`` actor.
+``handleExceptions`` directive, the main ``runRoute`` wrapper or the ``onFailure`` callback of a
+future created by ``detach``.
Similarly to the way that :ref:`Rejections` are handled the ``handleExceptions`` directive delegates the actual job of
converting a list of rejections to its argument, an ExceptionHandler__, which is defined like this::
@@ -27,4 +28,4 @@ have put somewhere into your route structure.
Here is an example:
.. includecode:: ../code/docs/ExceptionHandlerExamplesSpec.scala
- :snippet: example-1
+ :snippet: example-1
@@ -33,7 +33,7 @@ Directive Description
:ref:`-delete-` Rejects all non-DELETE requests
:ref:`-deleteCookie-` Adds a ``Set-Cookie`` header expiring the given cookie to all ``HttpResponse``
replies of its inner Route
-:ref:`-detachTo-` Executes its inner Route in the context of the actor returned by a given function
+:ref:`-detach-` Executes its inner Route in a ``Future``
:ref:`-dynamic-` Rebuilds its inner Route for every request anew
:ref:`-dynamicIf-` Conditionally rebuilds its inner Route for every request anew
:ref:`-encodeResponse-` Compresses responses coming back from its inner Route using a given Decoder
@@ -44,8 +44,8 @@ trait DemoService extends HttpService {
complete("PONG!")
} ~
path("stream1") {
- // we detach in order to move the blocking code inside the simpleStringStream off the service actor
- detachTo(singleRequestServiceActor) {
+ // we detach in order to move the blocking code inside the simpleStringStream into a future
+ detach() {
complete(simpleStringStream)
}
} ~
@@ -146,4 +146,4 @@ trait DemoService extends HttpService {
file
}
-}
+}
@@ -47,8 +47,8 @@ trait DemoService extends HttpService {
complete("PONG!")
} ~
path("stream1") {
- // we detach in order to move the blocking code inside the simpleStringStream off the service actor
- detachTo(singleRequestServiceActor) {
+ // we detach in order to move the blocking code inside the simpleStringStream into a future
+ detach() {
complete(simpleStringStream)
}
} ~
@@ -185,4 +185,4 @@ trait DemoService extends HttpService {
def in[U](duration: FiniteDuration)(body: => U): Unit =
actorSystem.scheduler.scheduleOnce(duration)(body)
-}
+}
@@ -16,6 +16,8 @@
package spray.routing
+import spray.http.StatusCodes.InternalServerError
+
class ExecutionDirectivesSpec extends RoutingSpec {
"the 'dynamicIf' directive" should {
@@ -32,4 +34,25 @@ class ExecutionDirectivesSpec extends RoutingSpec {
expect(dynamicRoute, "xxxx")
}
}
-}
+
+ "the 'detach directive" should {
+ "handle exceptions thrown inside its inner future" in {
+
+ implicit val exceptionHandler = ExceptionHandler {
+ case e: ArithmeticException ctx
+ ctx.complete(InternalServerError, "Oops.")
+ }
+
+ val route = get {
+ detach() {
+ complete((3 / 0).toString)
+ }
+ }
+
+ Get() ~> route ~> check {
+ status === InternalServerError
+ entityAs[String] === "Oops."
+ }
+ }
+ }
+}
@@ -17,6 +17,7 @@
package spray.routing
package directives
+import scala.concurrent.{ ExecutionContext, Future }
import scala.util.control.NonFatal
import akka.actor._
@@ -82,30 +83,32 @@ trait ExecutionDirectives {
}
/**
- * Executes its inner Route in the context of the actor returned by the given function.
- * Note that the parameter function is re-evaluated for every request anew.
+ * Executes its inner Route in a `Future`.
*/
- def detachTo(serviceActor: Route ActorRef): Directive0 =
- mapInnerRoute { route ctx serviceActor(route) ! ctx }
-
- /**
- * Returns a function creating a new SingleRequestServiceActor for a given Route.
- */
- def singleRequestServiceActor(implicit refFactory: ActorRefFactory): Route ActorRef =
- route refFactory.actorOf(Props(new SingleRequestServiceActor(route)))
+ def detach(dm: DetachMagnet): Directive0 = {
+ import dm._
+ mapInnerRoute { inner
+ ctx
+ Future(inner(ctx)).onFailure { case e ctx.failWith(e) }
+ }
+ }
}
object ExecutionDirectives extends ExecutionDirectives
-/**
- * An HttpService actor that reacts to an incoming RequestContext message by running it in the given Route
- * before shutting itself down.
- */
-class SingleRequestServiceActor(route: Route) extends Actor {
- def receive = {
- case ctx: RequestContext
- try route(ctx)
- catch { case NonFatal(e) ctx.failWith(e) }
- finally context.stop(self)
- }
-}
+class DetachMagnet()(implicit val ec: ExecutionContext)
+
+object DetachMagnet {
+ implicit def fromUnit(u: Unit)(implicit dm2: DetachMagnet2) = new DetachMagnet()(dm2.ec)
+ implicit def fromExecutionContext(ec: ExecutionContext) = new DetachMagnet()(ec)
+}
+
+class DetachMagnet2(val ec: ExecutionContext)
+
+object DetachMagnet2 extends DetachMagnet2LowerPriorityImplicits {
+ implicit def fromImplicitExecutionContext(implicit ec: ExecutionContext) = new DetachMagnet2(ec)
+}
+
+private[directives] abstract class DetachMagnet2LowerPriorityImplicits {
+ implicit def fromImplicitRefFactory(implicit factory: ActorRefFactory) = new DetachMagnet2(factory.dispatcher)
+}
@@ -35,7 +35,7 @@ trait FileAndResourceDirectives {
/**
* Completes GET requests with the content of the given file. The actual I/O operation is
- * running detached in the context of a newly spawned actor, so it doesn't block the current thread (but potentially
+ * running detached in a `Future`, so it doesn't block the current thread (but potentially
* some other thread !). If the file cannot be found or read the request is rejected.
*/
def getFromFile(fileName: String)(implicit settings: RoutingSettings, resolver: ContentTypeResolver,
@@ -44,7 +44,7 @@ trait FileAndResourceDirectives {
/**
* Completes GET requests with the content of the given file. The actual I/O operation is
- * running detached in the context of a newly spawned actor, so it doesn't block the current thread (but potentially
+ * running detached in a `Future`, so it doesn't block the current thread (but potentially
* some other thread !). If the file cannot be found or read the request is rejected.
*/
def getFromFile(file: File)(implicit settings: RoutingSettings, resolver: ContentTypeResolver,
@@ -53,12 +53,12 @@ trait FileAndResourceDirectives {
/**
* Completes GET requests with the content of the given file. The actual I/O operation is
- * running detached in the context of a newly spawned actor, so it doesn't block the current thread (but potentially
+ * running detached in a `Future`, so it doesn't block the current thread (but potentially
* some other thread !). If the file cannot be found or read the request is rejected.
*/
def getFromFile(file: File, contentType: ContentType)(implicit settings: RoutingSettings,
refFactory: ActorRefFactory): Route =
- (get & detachTo(singleRequestServiceActor)) {
+ (get & detach()) {
respondWithLastModifiedHeader(file.lastModified) {
if (file.isFile && file.canRead) {
implicit val bufferMarshaller = BasicMarshallers.byteArrayMarshaller(contentType)
@@ -77,7 +77,7 @@ trait FileAndResourceDirectives {
/**
* Completes GET requests with the content of the given resource. The actual I/O operation is
- * running detached in the context of a newly spawned actor, so it doesn't block the current thread (but potentially
+ * running detached in a `Future`, so it doesn't block the current thread (but potentially
* some other thread !).
* If the file cannot be found or read the Route rejects the request.
*/
@@ -86,13 +86,13 @@ trait FileAndResourceDirectives {
/**
* Completes GET requests with the content of the given resource. The actual I/O operation is
- * running detached in the context of a newly spawned actor, so it doesn't block the current thread (but potentially
+ * running detached in a `Future`, so it doesn't block the current thread (but potentially
* some other thread !).
* If the file cannot be found or read the Route rejects the request.
*/
def getFromResource(resourceName: String, contentType: ContentType)(implicit refFactory: ActorRefFactory): Route =
if (!resourceName.endsWith("/"))
- (get & detachTo(singleRequestServiceActor)) {
+ (get & detach()) {
val theClassLoader = actorSystem(refFactory).dynamicAccess.classLoader
theClassLoader.getResource(resourceName) match {
case null reject
@@ -110,7 +110,7 @@ trait FileAndResourceDirectives {
* Completes GET requests with the content of a file underneath the given directory.
* The unmatchedPath of the [[spray.RequestContext]] is first transformed by the given pathRewriter function before
* being appended to the given directoryName to build the final fileName.
- * The actual I/O operation is running detached in the context of a newly spawned actor, so it doesn't block the
+ * The actual I/O operation is running detached in a `Future`, so it doesn't block the
* current thread. If the file cannot be read the Route rejects the request.
*/
def getFromDirectory(directoryName: String)(implicit settings: RoutingSettings, resolver: ContentTypeResolver,
@@ -127,7 +127,7 @@ trait FileAndResourceDirectives {
*/
def listDirectoryContents(directories: String*)(implicit renderer: Marshaller[DirectoryListing],
refFactory: ActorRefFactory): Route =
- (get & detachTo(singleRequestServiceActor)) {
+ (get & detach()) {
unmatchedPath { path
val pathString = path.toString
val dirs = directories.map(new File(_, pathString)).filter(dir dir.isDirectory && dir.canRead)
@@ -157,7 +157,8 @@ trait FileAndResourceDirectives {
* Same as "getFromDirectory" except that the file is not fetched from the file system but rather from a
* "resource directory".
*/
- def getFromResourceDirectory(directoryName: String)(implicit resolver: ContentTypeResolver, refFactory: ActorRefFactory): Route = {
+ def getFromResourceDirectory(directoryName: String)(implicit resolver: ContentTypeResolver,
+ refFactory: ActorRefFactory): Route = {
val base = if (directoryName.isEmpty) "" else withTrailingSlash(directoryName)
unmatchedPath { path
getFromResource(base + stripLeadingSlash(path).toString)
@@ -254,4 +255,4 @@ object DirectoryListing {
if (settings.renderVanityFooter) sb.append(html(4)).append(DateTime.now.toIsoLikeDateTimeString).append(html(5))
sb.append(html(6)).toString
}
-}
+}

0 comments on commit ead4a70

Please sign in to comment.