Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
! routing: Added detach directive which executes its inner route in…
Browse files Browse the repository at this point in the history
… 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 26, 2013
1 parent ea7f609 commit ead4a70
Show file tree
Hide file tree
Showing 11 changed files with 79 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. _-detach-:

detach
========

(todo)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ExecutionDirectives
.. toctree::
:maxdepth: 1

detachTo
detach
dynamic
dynamicIf
handleExceptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
} ~
Expand Down Expand Up @@ -146,4 +146,4 @@ trait DemoService extends HttpService {
file
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
} ~
Expand Down Expand Up @@ -185,4 +185,4 @@ trait DemoService extends HttpService {

def in[U](duration: FiniteDuration)(body: => U): Unit =
actorSystem.scheduler.scheduleOnce(duration)(body)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package spray.routing

import spray.http.StatusCodes.InternalServerError

class ExecutionDirectivesSpec extends RoutingSpec {

"the 'dynamicIf' directive" should {
Expand All @@ -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."
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package spray.routing
package directives

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.control.NonFatal
import akka.actor._

Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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.
*/
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.