Request and Session Access in Lift Futures

In one of my previous posts I described how you can bind Futures to page elements in Lift 2. Lift 3.0.0 that has been released two weeks ago handles this type of bindings out of the box. What’s more, it’s possible to wrap any RestHelper response in a Future or LAFuture to have it handled asynchronously using container continuations. One of the next milestones on a way for even better async support in Lift is an elimination of a recurring pain point in many projects – access to container’s request and session in Future / LAFuture. Although some discussion on this is still in progress, you can already incorporate this code into your projects. In this post, I’d like to give you an overview of what you may expect in the next Lift releases about it and how to bring now session-aware futures to your Lift 3 and Lift 2 projects.

As an example, say that you want to lazily render a list of users. First, you execute database query asynchronously and get Future[Seq[User]] as a result of this operation. If you use Lift 2, incorporate FutureBinds that I described in one my previous posts or if you use Lift3, import net.liftweb.http._ and do:

1
2
3
4
5
6
7
val allUsers: Future[Seq[User]] = ... // retrieve users from DB

".user-list-async-container" #> allUsers.map { users =>
  ".user-list" #> users.map { user =>
    ".user" #> userBindings(user)
  }
}

As long as Future[Seq[User]] does not complete, .user-list-async-container shows a spinner. As soon as Future completes, bindings get executed and spinner is replaced with .user-list content.

There is one important limitation here. Since userBindings calls are not executed in snippet’s thread, you don’t have an access to LiftSession or Req from this method. Why would you need it? Say that your user list has two columns. The first one is a username and the second one is a user role. User role string should be localized depending on a language of the user seeing the list. The obvious choice is to use S ? for this type of operation. Unfortunately, S ? fails when no Req is available in scope because it can’t evaluate user’s locale. You can always overcome this by evaluating map of all possible roles and their names in advance, in snippet’s thread. However, what if you had 10 more columns in the user list, all requiring localized values? Here, FutureWithSession and LAFutureWithSession come to the rescue.

The idea is that if you have Req and LiftSession available in scope creating or transforming Future or LAFuture, future execution thread and all chained transformation methods’ threads will have an access to that Req and LiftSession. This works out of the box, all you need to do, is to create such a session/request-aware future. All other futures created as a result of transformations like map, flatMap will be also session/request-aware. Request and session will be also available in all Future/LAFuture interface methods, like onComplete, recoverWith and so forth. The only requirement is that Req and LiftSession must be available in thread creating future or calling these methods. For security reasons, initial Req and LiftSession are not automatically propagated to all threads processing given future – they must be available in calling code’s scope.

Since, as already said, this is not yet a part of Lift, you need to copy-paste FutureWithSession to your project. You may also want to look at a specification for this class.

There is a similar class for LAFuture named LAFutureWithSession here but you can’t manually incorporate it into your codebase since implementation required some changes in LAFuture itself. In this case, you need to wait for next Lift releases. The good thing is that you can easily transform LAFuture to Future and the opposite way, so the lack of the LAFutureWithSession in your project is not really a blocker.

Session-aware future can be created with FutureWithSession.withCurrentSession. I’ve prepared a sample application on GitHub demonstrating usage of this code:

1
2
3
4
"#scala-future *" #> FutureWithSession.withCurrentSession {
  Thread.sleep(1000)
  S ? ("general-futureCompleted", date)
}.map(s => s"$s in request from = ${S.request.map(_.userAgent).openOrThrowException("No request!")}")

In the exable above, we create session/request-aware Future with FutureWithSession.withCurrentSession. Method that is executed in a separate thread has an acceess to Req, so it can safely call S ?. Also, code inside that map method has access to Req so it can call S.request safely. This is all possible because Req and LiftSession are available within snippet’s thread, future is created as session-aware with FutureWithSession.withCurrentSession and map is called from Req/LiftSession-aware scope.

In case of LAFuture you will most probably use (this is still upcoming in on of the next Lift releases) LAFutureWithSession.withCurrentSession or even better just S.sessionFuture:

1
".content" #> S.sessionFuture { SessionVar.is }

Below you can find a source code of the FutureWithSession. You can incorporate it into your project even today. I am not sure yet when this will be an official part of Lift together with LAFuture equivalent – some discussion is still in progress, but this code is production – tested and can be safely used in your projects. I’ll post updates about an official Lift support for this feature.

FutureWithSession.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package net.liftweb.http

import net.liftweb.common.Full

import scala.concurrent.duration.Duration
import scala.concurrent.{CanAwait, ExecutionContext, Future}
import scala.util.Try

/**
  * Decorates `Future` instance to allow access to session and request resources. Should be created with
  * `FutureWithSession.withCurrentSession` that takes the current Lift request and session and make it available
  * to all transformation methods and initial `Future` execution body. Each transformation method returns
  * `FutureWithSession`, thus, they can be all chained together.
  *
  * It's important to bear in mind that each chained method requires current thread's `LiftSession` to be available.
  * `FutureWithSession` does _not_ propagate initial session or request to all chained methods.
  *
  * @see FutureWithSession.withCurrentSession
  *
  * @param delegate original `Future` instance that will be enriched with session and request access
  */
private[http] class FutureWithSession[T](private[this] val delegate: Future[T]) extends Future[T] {

  import FutureWithSession.withCurrentSession

  override def isCompleted: Boolean = delegate.isCompleted

  override def value: Option[Try[T]] = delegate.value

  override def result(atMost: Duration)(implicit permit: CanAwait): T = delegate.result(atMost)

  override def ready(atMost: Duration)(implicit permit: CanAwait) = {
    delegate.ready(atMost)
    this
  }

  override def onComplete[U](f: (Try[T]) => U)(implicit executor:ExecutionContext): Unit = {
    val sessionFn = withCurrentSession(f)
    delegate.onComplete(sessionFn)
  }

  override def map[S](f: T => S)(implicit executor: ExecutionContext): FutureWithSession[S] = {
    val sessionFn = withCurrentSession(f)
    new FutureWithSession(delegate.map(sessionFn))
  }

  override def flatMap[S](f: T => Future[S])(implicit executor: ExecutionContext): FutureWithSession[S] = {
    val sessionFn = withCurrentSession(f)
    new FutureWithSession(delegate.flatMap(sessionFn))
  }

  override def andThen[U](pf: PartialFunction[Try[T], U])(implicit executor: ExecutionContext): FutureWithSession[T] = {
    val sessionFn = withCurrentSession(pf)
    new FutureWithSession(delegate.andThen {
      case t => sessionFn(t)
    })
  }

  override def failed: FutureWithSession[Throwable] = {
    new FutureWithSession(delegate.failed)
  }

  override def fallbackTo[U >: T](that: Future[U]): FutureWithSession[U] = {
    new FutureWithSession[U](delegate.fallbackTo(that))
  }

  override def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): FutureWithSession[U] = {
    val sessionFn = withCurrentSession(pf)
    new FutureWithSession(delegate.recover {
      case t => sessionFn(t)
    })
  }

  override def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])(implicit executor: ExecutionContext): FutureWithSession[U] = {
    val sessionFn = withCurrentSession(pf)
    new FutureWithSession(delegate.recoverWith {
      case t => sessionFn(t)
    })
  }

  override def transform[S](s: T => S, f: Throwable => Throwable)(implicit executor: ExecutionContext): Future[S] = {
    val sessionSuccessFn = withCurrentSession(s)
    val sessionFailureFn = withCurrentSession(f)

    new FutureWithSession(delegate.transform(s => sessionSuccessFn(s), f => sessionFailureFn(f)))
  }

  override def zip[U](that: Future[U]): Future[(T, U)] = {
    new FutureWithSession(delegate.zip(that))
  }
}

object FutureWithSession {

  /**
    * Creates `Future` instance aware of current request and session. Each `Future` returned by chained
    * transformation method (e.g. `map`, `flatMap`) will be also request/session-aware. However, it's
    * important to bear in mind that initial request and session are not propagated to chained methods.
    * It's required that current execution thread for chained method has its own request/session available
    * if reading/writing some data to it as a part of chained method execution.
    */
  def withCurrentSession[T](task: => T)(implicit executionContext: ExecutionContext): Future[T] = {
    FutureWithSession(task)
  }

  private def apply[T](task: => T)(implicit executionContext: ExecutionContext): FutureWithSession[T] = {
    S.session match {
      case Full(_) =>
        val sessionFn = withCurrentSession(() => task)
        new FutureWithSession(Future[T](sessionFn()))

      case _ =>
        new FutureWithSession(Future.failed[T](
          new IllegalStateException("LiftSession not available in this thread context")
        ))
    }
  }

  private def withCurrentSession[T](task: () => T): () => T = {
    val session = S.session openOrThrowException "LiftSession not available in this thread context"
    session.buildDeferredFunction(task)
  }

  private def withCurrentSession[A,T](task: (A)=>T): (A)=>T = {
    val session = S.session openOrThrowException "LiftSession not available in this thread context"
    session.buildDeferredFunction(task)
  }
}

Comments

Author

photo

View Piotr Dyraga's LinkedIn profile  Piotr Dyraga
Senior software engineering consultant experienced in a wide range of projects (banking, logistics, computer networks and others). Please feel free to contact me if you are looking for top-notch development services for your project.

Recent Posts