Skip to content

Loading…

AJAX request deduplication. #1309

Merged
merged 9 commits into from

3 participants

@Shadowfiend
Lift Web Framework member

Regular AJAX requests now carry a single-character version identifier.
Server-side, if we've already seen a request for that identifier (meaning
this is a retry), we make the request wait for the original processing code
to finish before returning the response from there. If the original request
has already completed, we return the existing response for that request.

Continuations are used to wait for the completion of the initial request when
needed and available.

Shadowfiend added some commits
@Shadowfiend Shadowfiend Add !< to LiftCometActor.
CometActor had the function by virtue of extending Lift's actor traits;
however, the LiftCometActor trait that is often used to reference it
didn't define that the method would be present. We add it there so it
can be referred to anywhere that LiftCometActor is passed around instead
of CometActor.

!< returns an LAFuture for a message reply.
4b61567
@Shadowfiend Shadowfiend Include a version on the end of the Ajax GUID.
AJAX requests currently carry a GUID. We now add a single-character
version indicator. This version increments for distinct AJAX requests.
In particular, it does NOT increment during AJAX retries, so that a
retry of an existing request can be identified as being part of the same
attempt on the server.

Not all AJAX requests carry this version identifier. In particular, Lift
GC requests do not carry a version identifier. This is because these
are going to be handled in a streamlined handler. If a Lift GC request
doesn't make it through, we can retry it as many times as we want and
it'll just remark stuff in the session.

liftAjax.addPageNameAndVersion appends both the page GUID and the
version number. The version number is encoded in base-36.
bbd703d
@Shadowfiend Shadowfiend Rename continuation-related code to clarify Comet association.
We're about to add some AJAX-related continuation code, so we want it to
be clear the existing stuff is for comets.
0f7b081
@Shadowfiend Shadowfiend AJAX version-based deduplication.
The meat of the deal. Based on the AJAX version appended to the request
GUID, we determine whether we've already seen this request. If so, we
wait for the original request to complete before returning the resulting
value.  If we already completed the request before, we return the same
answer without re-running the associated parameters. AJAX requests that
need to wait are put into continuations if available.
ba0b313
@Shadowfiend Shadowfiend Delimit version in GUID string by a dash.
Before we were relying on the expected length of the funcName,
determined by calling nextFuncName. Because funcName isn't *always* the
same length, we switch instead ot putting a - between the GUID and the
version identifier in the path. We then look for it when extracting the
version identifier.
1c46530
@Shadowfiend Shadowfiend Move ajax request list into LiftSession with lastSeen.
LiftSession is in charge of managing cleanup of non-recently-seen pages
and such, so it needs to know about the AjaxRequestInfo list to clean it
up when a given request hasn't been needed in sufficiently long.
7d89340
@Shadowfiend
Lift Web Framework member

This is some pretty hairy stuff and required reworking a decent bit of the AJAX handling, so please test it and go over it a bit. I'm going to run some more tests myself (I'm testing the two basic AJAX processing scenarios (within and without a comet) each with two different delay potentials (no delay and 9s of delay) at https://github.com/Shadowfiend/lift-tests , amongst other things.

Once I get some feedback from you folks, I'll probably also put it out on OpenStudy and make sure things run smoothly there.

@Shadowfiend Shadowfiend Don't suspend requests too early for the first request.
We were suspending the request before we got a chance to kick off the
processing for the actual response to be run.
70ffcba
@Shadowfiend
Lift Web Framework member

Hm. Also I guess I screwed up the pull request attaching to an existing ticket. This would resolve issue #956.

@fmpwizard

Why do you use Box[Box[LiftResponse]] instead of just Box[LiftResponse] ?

Could we run into a situation where we would get a Full(Failure(x,y,z)) ? and then think that the response is still fine?

Lift Web Framework member

So, the ajax runner returns a Box[LiftResponse] as it did before these changes. An Empty or Failure at the top level of the Box[Box[]] means we haven't calculated a response and therefore it should be calculated. A Full() means the runAjax function calculated a response, and that is what the is. The response won't be fine, it will just already be precomputed as a Failure. Does that make sense?

Lift Web Framework member

It does, thanks for the clarification, would you mind adding it as a comment?

Lift Web Framework member

Sure. Yeah I inlined the relevant comment in the returns under the collect. But I think I'll centralize it up top.

@fmpwizard
Lift Web Framework member

I was going to ask about how safe it was to use val funcLength = Helpers.nextFuncName.length , glad you went the path of being explicit about the two values.

Lift Web Framework member

Yeah, it was a bit of a hack at first to get things going while I figured out a better way of dealing with it :)

@fmpwizard

If we use a mutable.Map , couldn't this be a val? It compiles locally if I change it to a val.

Lift Web Framework member

That's an excellent point actually. This was left over from when it was an immutable, but I wanted to use the map as its own synchronization point.

@fmpwizard
Lift Web Framework member

It looks very good, I didn't see anything clearly wrong with it, I just did a publish-local and early this week I'll try it with our app at work as well as another application I have been working on for some time that makes use of some ajax and comet.

@Shadowfiend
Lift Web Framework member

Made the changes you pointed out!

@fmpwizard
Lift Web Framework member

Thanks! I'll post on the mailing list once I get to try this branch on my two applications.

@dpp dpp merged commit aae6082 into master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 18, 2012
  1. @Shadowfiend

    Add !< to LiftCometActor.

    Shadowfiend committed
    CometActor had the function by virtue of extending Lift's actor traits;
    however, the LiftCometActor trait that is often used to reference it
    didn't define that the method would be present. We add it there so it
    can be referred to anywhere that LiftCometActor is passed around instead
    of CometActor.
    
    !< returns an LAFuture for a message reply.
  2. @Shadowfiend

    Include a version on the end of the Ajax GUID.

    Shadowfiend committed
    AJAX requests currently carry a GUID. We now add a single-character
    version indicator. This version increments for distinct AJAX requests.
    In particular, it does NOT increment during AJAX retries, so that a
    retry of an existing request can be identified as being part of the same
    attempt on the server.
    
    Not all AJAX requests carry this version identifier. In particular, Lift
    GC requests do not carry a version identifier. This is because these
    are going to be handled in a streamlined handler. If a Lift GC request
    doesn't make it through, we can retry it as many times as we want and
    it'll just remark stuff in the session.
    
    liftAjax.addPageNameAndVersion appends both the page GUID and the
    version number. The version number is encoded in base-36.
  3. @Shadowfiend

    Rename continuation-related code to clarify Comet association.

    Shadowfiend committed
    We're about to add some AJAX-related continuation code, so we want it to
    be clear the existing stuff is for comets.
  4. @Shadowfiend

    AJAX version-based deduplication.

    Shadowfiend committed
    The meat of the deal. Based on the AJAX version appended to the request
    GUID, we determine whether we've already seen this request. If so, we
    wait for the original request to complete before returning the resulting
    value.  If we already completed the request before, we return the same
    answer without re-running the associated parameters. AJAX requests that
    need to wait are put into continuations if available.
  5. @Shadowfiend

    Delimit version in GUID string by a dash.

    Shadowfiend committed
    Before we were relying on the expected length of the funcName,
    determined by calling nextFuncName. Because funcName isn't *always* the
    same length, we switch instead ot putting a - between the GUID and the
    version identifier in the path. We then look for it when extracting the
    version identifier.
  6. @Shadowfiend

    Move ajax request list into LiftSession with lastSeen.

    Shadowfiend committed
    LiftSession is in charge of managing cleanup of non-recently-seen pages
    and such, so it needs to know about the AjaxRequestInfo list to clean it
    up when a given request hasn't been needed in sufficiently long.
  7. @Shadowfiend

    Don't suspend requests too early for the first request.

    Shadowfiend committed
    We were suspending the request before we got a chance to kick off the
    processing for the actual response to be run.
Commits on Aug 20, 2012
  1. @Shadowfiend

    Make ajaxRequests a val in LiftSession.

    Shadowfiend committed
    It's a mutable Map, so there's no need for it to be a var.
  2. @Shadowfiend
This page is out of date. Refresh to see the latest.
View
6 web/webkit/src/main/scala/net/liftweb/http/CometActor.scala
@@ -400,6 +400,12 @@ trait LiftCometActor extends TypedActor[Any, Any] with ForwardableActor[Any, Any
}
/**
+ * Asynchronous message send. Send-and-receive eventually. Returns a Future for the reply message.
+ */
+ def !<(msg: Any): LAFuture[Any]
+
+
+ /**
* Override in sub-class to customise timeout for the render()-method for the specific comet
*/
def cometRenderTimeout = LiftRules.cometRenderTimeout
View
332 web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala
@@ -416,48 +416,135 @@ class LiftServlet extends Loggable {
toReturn
}
- private def extractVersion[T](path: List[String])(f: => T): T = {
+ private object AjaxVersions {
+ def unapply(ajaxPathPart: String) : Option[(String,Int)] = {
+ val dash = ajaxPathPart.indexOf("-")
+ if (dash > -1 && ajaxPathPart.length > dash + 1)
+ Some(
+ (ajaxPathPart.substring(0, dash),
+ ajaxPathPart.charAt(dash + 1))
+ )
+ else
+ None
+ }
+ }
+ /**
+ * Extracts two versions from a given AJAX path:
+ * - The RenderVersion, which is used for GC purposes.
+ * - The requestVersion, which lets us determine if this is
+ * a request we've already dealt with or are currently dealing
+ * with (so we don't rerun the associated handler). See
+ * handleVersionedAjax for more.
+ *
+ * The requestVersion is passed to the function that is passed in.
+ */
+ private def extractVersions[T](path: List[String])(f: (Box[Int]) => T): T = {
path match {
- case first :: second :: _ => RenderVersion.doWith(second)(f)
- case _ => f
+ case first :: AjaxVersions(renderVersion, requestVersion) :: _ =>
+ RenderVersion.doWith(renderVersion)(f(Full(requestVersion)))
+ case _ => f(Empty)
}
}
- private def handleAjax(liftSession: LiftSession,
- requestState: Req): Box[LiftResponse] = {
- extractVersion(requestState.path.partPath) {
+ /**
+ * An actor that manages AJAX continuations from container (Jetty style).
+ */
+ class AjaxContinuationActor(request: Req, session: LiftSession,
+ onBreakout: Box[LiftResponse] => Unit) extends LiftActor {
+ private var response: Box[LiftResponse] = Empty
+ private var done = false
- LiftRules.cometLogger.debug("AJAX Request: " + liftSession.uniqueId + " " + requestState.params)
- tryo {
- LiftSession.onBeginServicing.foreach(_(liftSession, requestState))
- }
+ def messageHandler = {
+ case AjaxResponseComplete(completeResponse) =>
+ response = completeResponse
+ LAPinger.schedule(this, BreakOut(), 5 millis)
- val ret = try {
- requestState.param("__lift__GC") match {
- case Full(_) =>
- liftSession.updateFuncByOwner(RenderVersion.get, millis)
- Full(JavaScriptResponse(js.JsCmds.Noop))
+ case BreakOut() if ! done =>
+ done = true
+ session.exitComet(this)
+ onBreakout(response)
- case _ =>
- try {
- val what = flatten(try {
- liftSession.runParams(requestState)
- } catch {
- case ResponseShortcutException(_, Full(to), _) =>
- import js.JsCmds._
- List(RedirectTo(to))
- })
+ case _ =>
+ }
+
+ override def toString = "AJAX Continuation Actor"
+ }
+
+ private case class AjaxResponseComplete(response: Box[LiftResponse])
+
+ private lazy val ajaxPostTimeout: Long = LiftRules.ajaxPostTimeout * 1000L
- val what2 = what.flatMap {
+ /**
+ * Runs the actual AJAX processing. This includes handling __lift__GC,
+ * or running the parameters in the session. onComplete is run when the
+ * AJAX request has completed with a response that is meant for the
+ * user. In cases where the request is taking too long to respond,
+ * an LAFuture may be used to delay the real response (and thus the
+ * invocation of onComplete) while this function returns an empty
+ * response.
+ */
+ private def runAjax(requestState: Req,
+ liftSession: LiftSession,
+ onCompleteFn: Box[(LiftResponse)=>Unit]): Box[LiftResponse] = {
+ // We don't want to call the onComplete function if we're returning
+ // an error response, as if there is a comet timeout while
+ // processing. This is because the onComplete function is meant to
+ // indicate a successful completion, and will short-circuit any
+ // other AJAX requests with the same version with the same
+ // response. Error responses are specific to the AJAX request,
+ // rather than to the version of the AJAX request. Successful
+ // responses are for all AJAX requests with the same version.
+ var callCompleteFn = true
+
+ val ret = try {
+ requestState.param("__lift__GC") match {
+ case Full(_) =>
+ val renderVersion = RenderVersion.get
+ liftSession.updateFuncByOwner(renderVersion, millis)
+
+ Full(JavaScriptResponse(js.JsCmds.Noop))
+
+ case _ =>
+ try {
+ val what = flatten(try {
+ liftSession.runParams(requestState)
+ } catch {
+ case ResponseShortcutException(_, Full(to), _) =>
+ import js.JsCmds._
+ List(RedirectTo(to))
+ })
+
+ def processResponse(response: List[Any]): LiftResponse = {
+ val what2 = response.flatMap {
case js: JsCmd => List(js)
case jv: JValue => List(jv)
case n: NodeSeq => List(n)
case js: JsCommands => List(js)
case r: LiftResponse => List(r)
+
+ // This strange case comes from ajax requests run in
+ // a comet context that don't complete in the
+ // cometTimeout time frame. The js is the result of the
+ // comet processing timeout handler, while the future
+ // gives us access to the eventual result of the ajax
+ // request.
+ case (js, future: LAFuture[List[Any]]) if onCompleteFn.isDefined =>
+ // Wait for the future on a separate thread.
+ Schedule.schedule(() => {
+ val result = flatten(future.get)
+ onCompleteFn.foreach(_(processResponse(result)))
+ }, 0 seconds)
+
+ // But this request is done for. Return the comet
+ // processing timeout result, but don't mark the
+ // request complete; that happens whenever we satisfy
+ // the future above.
+ callCompleteFn = false
+ List(js)
case s => Nil
}
- val ret: LiftResponse = what2 match {
+ what2 match {
case (json: JsObj) :: Nil => JsonResponse(json)
case (jv: JValue) :: Nil => JsonResponse(jv)
case (js: JsCmd) :: xs => {
@@ -474,21 +561,180 @@ class LiftServlet extends Loggable {
case (r: LiftResponse) :: _ => r
case _ => JsCommands(S.noticesToJsCmd :: JsCmds.Noop :: S.jsToAppend).toResponse
}
+ }
- LiftRules.cometLogger.debug("AJAX Response: " + liftSession.uniqueId + " " + ret)
+ val ret: LiftResponse = processResponse(what)
- Full(ret)
- } finally {
- if (S.functionMap.size > 0) {
- liftSession.updateFunctionMap(S.functionMap, RenderVersion.get, millis)
- S.clearFunctionMap
+ LiftRules.cometLogger.debug("AJAX Response: " + liftSession.uniqueId + " " + ret)
+
+ Full(ret)
+ } finally {
+ if (S.functionMap.size > 0) {
+ liftSession.updateFunctionMap(S.functionMap, RenderVersion.get, millis)
+ S.clearFunctionMap
+ }
+ }
+ }
+ } catch {
+ case foc: LiftFlowOfControlException => throw foc
+ case e => S.assertExceptionThrown() ; NamedPF.applyBox((Props.mode, requestState, e), LiftRules.exceptionHandler.toList);
+ }
+
+ for {
+ response <- ret if callCompleteFn
+ onComplete <- onCompleteFn
+ } {
+ onComplete(response)
+ }
+
+ ret
+ }
+
+ /**
+ * Handles the details of versioned AJAX requests. If the version is
+ * Empty, runs the request through a normal AJAX flow with no
+ * continuations or special handling. Repeated calls will cause
+ * repeated evaluation.
+ *
+ * If the version is Full, the request is tracked. Subsequent requests
+ * for the same version will suspend until a response is available,
+ * then they will return the response.
+ *
+ * cont is the AjaxContinuationActor for this request, and
+ * suspendRequest is the function that will suspend the request.
+ * These are present to support non-continuation containers; these
+ * will present a no-op to suspendRequest.
+ */
+ private def handleVersionedAjax(handlerVersion: Box[Int],
+ cont: AjaxContinuationActor,
+ suspendRequest: () => Any,
+ requestState: Req,
+ liftSession: LiftSession): Box[LiftResponse] = {
+ handlerVersion match {
+ case Full(handlerVersion) =>
+ val renderVersion = RenderVersion.get
+
+ // An Empty in toReturn indicates there is no precomputed response for
+ // this AJAX request/version. Note that runAjax returns a
+ // Box[LiftResponse]. So we can have a Full(Empty) that indicates
+ // runAjax has computed a response, and that response was an Empty.
+ // That's why we have a double Box here. If we get an Empty back,
+ // we compute the actual response by calling runAjax below.
+ val toReturn: Box[Box[LiftResponse]] =
+ liftSession.withAjaxRequests { currentAjaxRequests =>
+ currentAjaxRequests.get(renderVersion).collect {
+ case AjaxRequestInfo(storedVersion, _, pendingActors, _) if handlerVersion != storedVersion =>
+ // Break out of any actors for the stale version.
+ pendingActors.foreach(_ ! BreakOut())
+
+ // Evict the older version's info.
+ currentAjaxRequests +=
+ (renderVersion ->
+ AjaxRequestInfo(handlerVersion, Empty, cont :: Nil, millis))
+
+ Empty
+
+ case AjaxRequestInfo(storedVersion, existingResponseBox @ Full(_), _, _) =>
+ existingResponseBox // return the Full response Box
+
+ case AjaxRequestInfo(storedVersion, _, pendingActors, _) =>
+ currentAjaxRequests +=
+ (renderVersion ->
+ AjaxRequestInfo(handlerVersion, Empty, cont :: pendingActors, millis))
+
+ suspendRequest()
+
+ Full(Full(EmptyResponse))
+ } openOr {
+ currentAjaxRequests +=
+ (renderVersion ->
+ AjaxRequestInfo(handlerVersion, Empty, cont :: Nil, millis))
+
+ Empty
+ }
+ }
+
+ toReturn or {
+ val result = Full(runAjax(requestState, liftSession, Full((result: LiftResponse) => {
+ // When we get the response, synchronizedly check that the
+ // versions are still the same in the map, and, if so, update
+ // any waiting actors then clear the actor list and update the
+ // request info to include the response in case any other
+ // requests come in with this version.
+ liftSession.withAjaxRequests { currentAjaxRequests =>
+ currentAjaxRequests.get(renderVersion).collect {
+ case AjaxRequestInfo(storedVersion, _, pendingActors, _) if storedVersion == handlerVersion =>
+ pendingActors.foreach(_ ! AjaxResponseComplete(Full(result)))
+ currentAjaxRequests +=
+ (renderVersion ->
+ AjaxRequestInfo(handlerVersion, Full(Full(result)), Nil, millis))
}
}
+ })))
+
+ suspendRequest()
+
+ result
+ } openOr Empty
+
+ case _ =>
+ runAjax(requestState, liftSession, Empty)
+ }
+ }
+
+ /**
+ * Kick off AJAX handling. Extracts relevant versions and handles the
+ * begin/end servicing requests, as well as generation of
+ * ContinuationActors and choosing between continuation and
+ * continuationless request handling.
+ */
+ private def handleAjax(liftSession: LiftSession,
+ requestState: Req): Box[LiftResponse] = {
+ extractVersions(requestState.path.partPath) { handlerVersion =>
+ LiftRules.cometLogger.debug("AJAX Request: " + liftSession.uniqueId + " " + requestState.params)
+ tryo {
+ LiftSession.onBeginServicing.foreach(_(liftSession, requestState))
+ }
+
+ def suspendingActor = {
+ new AjaxContinuationActor(requestState, liftSession,
+ response => {
+ requestState.request.resume((requestState, S.init(requestState, liftSession)
+ (response.map(LiftRules.performTransform) openOr EmptyResponse)))})
+ }
+
+ def waitingActorForFuture(future: LAFuture[Box[LiftResponse]]) = {
+ new AjaxContinuationActor(requestState, liftSession,
+ response => future.satisfy(response))
+ }
+
+ val possibleFuture =
+ if (requestState.request.suspendResumeSupport_?)
+ Empty
+ else
+ Full(new LAFuture[Box[LiftResponse]])
+ val (cont, suspendRequest) =
+ possibleFuture.map { f =>
+ (waitingActorForFuture(f), () => ())
+ } openOr {
+ (suspendingActor, () => requestState.request.suspend(ajaxPostTimeout + 500L))
}
- } catch {
- case foc: LiftFlowOfControlException => throw foc
- case e => S.assertExceptionThrown() ; NamedPF.applyBox((Props.mode, requestState, e), LiftRules.exceptionHandler.toList);
+
+ val ret: Box[LiftResponse] = try {
+ liftSession.enterComet(cont -> requestState)
+
+ val result: Box[LiftResponse] =
+ handleVersionedAjax(handlerVersion, cont, suspendRequest, requestState, liftSession)
+
+ possibleFuture.map(_.get(ajaxPostTimeout) match {
+ case Full(response) => response
+ case _ => Empty
+ }) openOr result
+ } finally {
+ if (! requestState.request.suspendResumeSupport_?)
+ liftSession.exitComet(cont)
}
+
tryo {
LiftSession.onEndServicing.foreach(_(liftSession, requestState, ret))
}
@@ -496,10 +742,10 @@ class LiftServlet extends Loggable {
}
}
-/**
- * An actor that manages continuations from container (Jetty style)
+ /**
+ * An actor that manages comet continuations from container (Jetty style)
*/
- class ContinuationActor(request: Req, session: LiftSession,
+ class CometContinuationActor(request: Req, session: LiftSession,
actors: List[(LiftCometActor, Long)],
onBreakout: List[AnswerRender] => Unit) extends LiftActor {
private var answers: List[AnswerRender] = Nil
@@ -529,15 +775,15 @@ class LiftServlet extends Loggable {
case _ =>
}
- override def toString = "Actor dude " + seqId
+ override def toString = "Continuation Actor dude " + seqId
}
private object BeginContinuation
private lazy val cometTimeout: Long = (LiftRules.cometRequestTimeout openOr 120) * 1000L
- private def setupContinuation(request: Req, session: LiftSession, actors: List[(LiftCometActor, Long)]): Any = {
- val cont = new ContinuationActor(request, session, actors,
+ private def setupCometContinuation(request: Req, session: LiftSession, actors: List[(LiftCometActor, Long)]): Any = {
+ val cont = new CometContinuationActor(request, session, actors,
answers => request.request.resume(
(request, S.init(request, session)
(LiftRules.performTransform(
@@ -566,7 +812,7 @@ class LiftServlet extends Loggable {
if (actors.isEmpty) Left(Full(new JsCommands(LiftRules.noCometSessionCmd.vend :: js.JE.JsRaw("lift_toWatch = {};").cmd :: Nil).toResponse))
else requestState.request.suspendResumeSupport_? match {
case true => {
- setupContinuation(requestState, sessionActor, actors)
+ setupCometContinuation(requestState, sessionActor, actors)
Left(Full(EmptyResponse))
}
@@ -614,7 +860,7 @@ class LiftServlet extends Loggable {
private def handleNonContinuationComet(request: Req, session: LiftSession, actors: List[(LiftCometActor, Long)],
originalRequest: Req): () => Box[LiftResponse] = () => {
val f = new LAFuture[List[AnswerRender]]
- val cont = new ContinuationActor(request, session, actors,
+ val cont = new CometContinuationActor(request, session, actors,
answers => f.satisfy(answers))
try {
View
60 web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala
@@ -500,6 +500,16 @@ private final case class PostPageFunctions(renderVersion: String,
}
/**
+ * existingResponse is Empty if we have no response for this request
+ * yet. pendingActors is a list of actors who want to be notified when
+ * this response is received.
+ */
+private[http] final case class AjaxRequestInfo(requestVersion:Int,
+ existingResponse:Box[Box[LiftResponse]],
+ pendingActors:List[LiftActor],
+ lastSeen: Long)
+
+/**
* The LiftSession class containg the session state information
*/
class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
@@ -558,6 +568,16 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
private var postPageFunctions: Map[String, PostPageFunctions] = Map()
/**
+ * A list of AJAX requests that may or may not be pending for this
+ * session. There is up to one entry per RenderVersion.
+ */
+ private val ajaxRequests = scala.collection.mutable.Map[String,AjaxRequestInfo]()
+
+ private[http] def withAjaxRequests[T](fn: (scala.collection.mutable.Map[String, AjaxRequestInfo]) => T): T = {
+ ajaxRequests.synchronized { fn(ajaxRequests) }
+ }
+
+ /**
* The synchronization lock for the postPageFunctions
*/
private val postPageLock = new Object
@@ -675,7 +695,6 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
* Executes the user's functions based on the query parameters
*/
def runParams(state: Req): List[Any] = {
-
val toRun = {
// get all the commands, sorted by owner,
(state.uploadedFiles.map(_.name) ::: state.paramNames).distinct.
@@ -708,13 +727,28 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
val f = toRun.filter(_.owner == w)
w match {
// if it's going to a CometActor, batch up the commands
- case Full(id) if asyncById.contains(id) => asyncById.get(id).toList.flatMap(a =>
- a.!?(a.cometProcessingTimeout, ActionMessageSet(f.map(i => buildFunc(i)), state)) match {
+ case Full(id) if asyncById.contains(id) => asyncById.get(id).toList.flatMap(a => {
+ val future =
+ a.!<(ActionMessageSet(f.map(i => buildFunc(i)), state))
+
+ def processResult(result: Any): List[Any] = result match {
case Full(li: List[_]) => li
case li: List[_] => li
- case Empty => Full(a.cometProcessingTimeoutHandler())
+ // We return the future so it can, from AJAX requests, be
+ // satisfied and update the pending ajax request map.
+ case Empty =>
+ val processingFuture = new LAFuture[Any]
+ // Wait for and process the future on a separate thread.
+ Schedule.schedule(() => {
+ processingFuture.satisfy(processResult(future.get))
+ }, 0 seconds)
+ List((a.cometProcessingTimeoutHandler, processingFuture))
case other => Nil
- })
+ }
+
+ processResult(future.get(a.cometProcessingTimeout))
+ })
+
case _ => f.map(i => buildFunc(i).apply())
}
}
@@ -836,6 +870,15 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
}
}
+ withAjaxRequests { currentAjaxRequests =>
+ for {
+ (version, requestInfo) <- currentAjaxRequests
+ if (now - requestInfo.lastSeen) > LiftRules.unusedFunctionsLifeTime
+ } {
+ currentAjaxRequests -= version
+ }
+ }
+
synchronized {
messageCallback.foreach {
case (k, f) =>
@@ -963,6 +1006,13 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
} postPageFunctions += (ownerName -> funcInfo.updateLastSeen)
}
+ withAjaxRequests { currentAjaxRequests =>
+ currentAjaxRequests.get(ownerName).foreach {
+ case info: AjaxRequestInfo =>
+ currentAjaxRequests += (ownerName -> info.copy(lastSeen = time))
+ }
+ }
+
synchronized {
(0 /: messageCallback)((l, v) => l + (v._2.owner match {
case Full(owner) if (owner == ownerName) =>
View
31 web/webkit/src/main/scala/net/liftweb/http/js/JSArtifacts.scala
@@ -112,24 +112,24 @@ trait JSArtifacts {
}
/**
- * The companion module for AjaxInfo that provides
+ * The companion module for AjaxInfodd that provides
* different construction schemes
*/
object AjaxInfo {
def apply(data: JsExp, post: Boolean) =
- new AjaxInfo(data, if (post) "POST" else "GET", 1000, false, "script", Empty, Empty)
+ new AjaxInfo(data, if (post) "POST" else "GET", 1000, false, "script", Empty, Empty, true)
def apply(data: JsExp,
dataType: String,
post: Boolean) =
- new AjaxInfo(data, if (post) "POST" else "GET", 1000, false, dataType, Empty, Empty)
+ new AjaxInfo(data, if (post) "POST" else "GET", 1000, false, dataType, Empty, Empty, true)
def apply(data: JsExp) =
- new AjaxInfo(data, "POST", 1000, false, "script", Empty, Empty)
+ new AjaxInfo(data, "POST", 1000, false, "script", Empty, Empty, true)
def apply(data: JsExp,
dataType: String) =
- new AjaxInfo(data, "POST", 1000, false, dataType, Empty, Empty)
+ new AjaxInfo(data, "POST", 1000, false, dataType, Empty, Empty, true)
def apply(data: JsExp,
post: Boolean,
@@ -142,7 +142,23 @@ object AjaxInfo {
false,
"script",
Full(successFunc),
- Full(failFunc))
+ Full(failFunc),
+ true)
+
+ def apply(data: JsExp,
+ post: Boolean,
+ timeout: Long,
+ successFunc: String,
+ failFunc: String,
+ includeVersion: Boolean) =
+ new AjaxInfo(data,
+ if (post) "POST" else "GET",
+ timeout,
+ false,
+ "script",
+ Full(successFunc),
+ Full(failFunc),
+ includeVersion)
}
/**
@@ -150,5 +166,6 @@ object AjaxInfo {
*/
case class AjaxInfo(data: JsExp, action: String, timeout: Long,
cache: Boolean, dataType: String,
- successFunc: Box[String], failFunc: Box[String])
+ successFunc: Box[String], failFunc: Box[String],
+ includeVersion: Boolean)
View
23 web/webkit/src/main/scala/net/liftweb/http/js/ScriptRenderer.scala
@@ -107,8 +107,7 @@ object ScriptRenderer {
"POST",
LiftRules.ajaxPostTimeout,
false, "script",
- Full("liftAjax.lift_successRegisterGC"), Full("liftAjax.lift_failRegisterGC"))) +
- """
+ Full("liftAjax.lift_successRegisterGC"), Full("liftAjax.lift_failRegisterGC"), false)) + """
},
@@ -132,6 +131,7 @@ object ScriptRenderer {
aboutToSend.onSuccess(data);
}
liftAjax.lift_doCycleQueueCnt++;
+ liftAjax.lift_ajaxVersion++;
liftAjax.lift_doAjaxCycle();
};
@@ -145,6 +145,7 @@ object ScriptRenderer {
queue.push(aboutToSend);
liftAjax.lift_ajaxQueueSort();
} else {
+ liftAjax.lift_ajaxVersion++;
if (aboutToSend.onFailure) {
aboutToSend.onFailure();
} else {
@@ -180,6 +181,18 @@ object ScriptRenderer {
setTimeout("liftAjax.lift_doAjaxCycle();", 200);
},
+ lift_ajaxVersion: 0,
+
+ addPageNameAndVersion: function(url) {
+ return """ + {
+ if (LiftRules.enableLiftGC) {
+ "url.replace('" + LiftRules.ajaxPath + "', '" + LiftRules.ajaxPath + "/'+lift_page+('-'+liftAjax.lift_ajaxVersion%36).toString(36));"
+ } else {
+ "url;"
+ }
+ } + """
+ },
+
addPageName: function(url) {
return """ + {
if (LiftRules.enableLiftGC) {
@@ -196,7 +209,7 @@ object ScriptRenderer {
"POST",
LiftRules.ajaxPostTimeout,
false, "script",
- Full("onSuccess"), Full("onFailure"))) +
+ Full("onSuccess"), Full("onFailure"), true)) +
"""
},
@@ -206,7 +219,7 @@ object ScriptRenderer {
"POST",
LiftRules.ajaxPostTimeout,
false, "json",
- Full("onSuccess"), Full("onFailure"))) +
+ Full("onSuccess"), Full("onFailure"), true)) +
"""
}
};
@@ -279,7 +292,7 @@ object ScriptRenderer {
false,
"script",
Full("liftComet.lift_handlerSuccessFunc"),
- Full("liftComet.lift_handlerFailureFunc"))) +
+ Full("liftComet.lift_handlerFailureFunc"), false)) +
"""
}
}
View
2 web/webkit/src/main/scala/net/liftweb/http/js/extcore/ExtCoreArtifacts.scala
@@ -148,7 +148,7 @@ object ExtCoreArtifacts extends JSArtifacts {
}
private def toJson(info: AjaxInfo, server: String, path: String => JsExp): String =
- (("url : liftAjax.addPageName(" + path(server).toJsCmd + ")" ) ::
+ (("url : liftAjax.addPageNameAndVersion(" + path(server).toJsCmd + ")" ) ::
"params : " + info.data.toJsCmd ::
("method : " + info.action.encJs) ::
("dataType : " + info.dataType.encJs) ::
View
8 web/webkit/src/main/scala/net/liftweb/http/js/jquery/JQueryArtifacts.scala
@@ -95,9 +95,15 @@ trait JQueryArtifacts extends JSArtifacts {
* attributes described by data parameter
*/
def ajax(data: AjaxInfo): String = {
+ val versionIncluder =
+ if (data.includeVersion)
+ "liftAjax.addPageNameAndVersion"
+ else
+ "liftAjax.addPageName"
+
"jQuery.ajax(" + toJson(data, S.contextPath,
prefix =>
- JsRaw("liftAjax.addPageName(" + S.encodeURL(prefix + "/" + LiftRules.ajaxPath + "/").encJs + ")")) + ");"
+ JsRaw(versionIncluder + "(" + S.encodeURL(prefix + "/" + LiftRules.ajaxPath + "/").encJs + ")")) + ");"
}
/**
View
8 web/webkit/src/main/scala/net/liftweb/http/js/yui/YUIArtifacts.scala
@@ -126,9 +126,15 @@ object YUIArtifacts extends JSArtifacts {
* attributes described by data parameter
*/
def ajax(data: AjaxInfo): String = {
+ val versionIncluder =
+ if (data.includeVersion)
+ "liftAjax.addPageNameAndVersion"
+ else
+ "liftAjax.addPageName"
+
val url = S.encodeURL(S.contextPath + "/" + LiftRules.ajaxPath + "/")
- "url = YAHOO.lift.buildURI(liftAjax.addPageName(" + url.encJs + ") , " + data.data.toJsCmd + ");" +
+ "url = YAHOO.lift.buildURI(" + versionIncluder + "(" + url.encJs + ") , " + data.data.toJsCmd + ");" +
"YAHOO.util.Connect.asyncRequest(" + data.action.encJs + ", url, " + toJson(data) + ");"
}
Something went wrong with that request. Please try again.