Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

AJAX Request deduplication, take 2 #1328

Merged
merged 8 commits into from

7 participants

@Shadowfiend
Owner

Opening a separate issue for this to keep the discussion separate, as the approach is completely different.

The new approach does away with continuations altogether. Things are fairly simple:

  • We once again carry a sequence number on the AJAX URI.
  • A request for a sequence number we haven't seen yet is processed the normal way. When it completes and returns its response, that response is used to satisfy a future that has been associated with that sequence number.
  • Future requests for that sequence number wait on the future to return a result, but they time out after the ajaxPostTimeout. Note that the original request does not time out (on the server). It runs to completion.
  • To deal with the above, AJAX requests associated with comet actors no longer time out after the cometProcessingTimeout; instead, they block until the comet processes the request and returns a result. Otherwise, we would not be able to properly deduplicate comet-related AJAX requests.
  • Because of the way AJAX requests are queued up (I had misunderstood this the first time), info for a given sequence number is kept around until we know it can be discarded. To deal with this, the AJAX URI also carries a number indicating how many AJAX requests are still queued on the client. Only when this number reaches 0 do we clear out the information related to existing sequence numbers, as that is the only moment at which we are certain that every sequence number in our list has either received its response on the client or has been given up on by the client.
  • The other situation in which information associated with a given sequence number is evicted is if it remains untouched for the length of the functionLifespan.

Note: this request also includes the commit that fixes issue #1327, as that fix was developed in tandem to this branch.

Shadowfiend added some commits
@Shadowfiend Shadowfiend Move uriSuffix extraction into lift_ajaxHandler.
By having it in doAjaxCycle, there were situations where the uri suffix
could get lost. The most obvious one was when a long-running ajax
request was occurring, and two AJAX requests were queued during that
time frame. This would result in the first request getting the second
request's uriSuffix, and the second request getting no suffix at all.

We now immediately put the uriSuffix into the sending data when
lift_ajaxHandler is called.
c9279a4
@Shadowfiend Shadowfiend Encode an AJAX request version in the request URI.
The AJAX request version has two components: the actual version number
and a count of queued requests. This is presented as two base-36 values
after a dash. So a request now looks like:

/ajax_request/F<page version>-v8

Where v indicates request number 31 and 8 indicates that there are 8
queued requests that have not been handled yet.
9878065
@Shadowfiend Shadowfiend Drop the timeout on comet-related AJAX requests.
While this will tie up a request thread for longer, it means we can
reliably say that when the AJAX request thread completes, it will
actually have the correct response to the original request.
d2ec29f
@Shadowfiend Shadowfiend Add tracking for AJAX requests in LiftSession.
Request info consists of three things:
 - The request version.
 - A future for the response to the request, satisfied by the first
   request for this version when the response is ready.
 - A lastSeen timestamp, used to expire the entry after the usual
   function lifespan.

LiftSession.withAjaxRequests exposes the request list, which is a Map
mapping a page version to the list of AjaxRequestInfos currently being
tracked for that page. AjaxRequestInfos are cleaned up according to
their lastSeen timestamp, which is updated the same way as those of
functions on the page.
26bf7a0
@Shadowfiend Shadowfiend Implement the meat of AJAX deduplication.
AJAX requests now come in with a two-part version number, one a sequence
number for the ajax request's count in the overall list of requests sent
by the client, and one a number indicating how many other requests are
queued up on the client.

If this is the first request seen with its sequence number, we record
the request in the session and run regular AJAX request handling. When
that request handling is complete, it satisfies the future that is
recorded in the session. Subsequent requests for a given sequence number
wait on the future from the first request up to the ajax post timeout,
then fail.

When we get a request coming in with a 0 count for pending requests on
the client, we clear out all other pending requests in the session's
list, since that means none of them will be getting a chance to report
their responses again.
82824bf
...kit/src/main/scala/net/liftweb/http/LiftServlet.scala
((5 lines not shown))
+ * Tracks the two aspects of an AJAX version: the sequence number,
+ * whose sole purpose is to identify requests that are retries for the
+ * same resource, and pending requests, which indicates how many
+ * requests are still queued for this particular page version on the
+ * client. The latter is used to expire result data for sequence
+ * numbers that are no longer needed.
+ */
+ private case class AjaxVersionInfo(renderVersion:String, sequenceNumber:Int, pendingRequests:Int)
+ private object AjaxVersions {
+ def unapply(ajaxPathPart: String) : Option[AjaxVersionInfo] = {
+ val dash = ajaxPathPart.indexOf("-")
+ if (dash > -1 && ajaxPathPart.length > dash + 2)
+ Some(
+ AjaxVersionInfo(ajaxPathPart.substring(0, dash),
+ ajaxPathPart.charAt(dash + 1),
+ Integer.parseInt(ajaxPathPart.substring(dash + 2, dash + 3), 36))
@fmpwizard Owner

Does this mean we cannot have more than 9 pending req?

@Shadowfiend Owner

35, but yeah. I was thinking about making the last “digit” a single value (since 0 is the only value for it that we're interested in) and allowing an arbitrary number before that so that we can queue up as many requests as needed (though we'd trigger some interesting performance issues if you ended up with too many requests stored on the server…).

@fmpwizard Owner

This is the only thing I would change, I see your point that having that many queue'd request would be a "bad thing", but it may also be hard to debug if we are not parsing the right value here (but it's way too late for me to even follow what would happen)

That being said, my mac is back, so I should be able to try this branch on my work app and another app I have.

@Shadowfiend Owner

Totally fair. I'll try and make that change tomorrow.

@dpp Owner
dpp added a note

I'd really like to not have a limit on queued items.

@Shadowfiend Owner

Yep. I'll make the sequence number arbitrarily large.

@andreak Owner
andreak added a note
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@fmpwizard
Owner

+1

...kit/src/main/scala/net/liftweb/http/LiftServlet.scala
@@ -423,75 +423,223 @@ class LiftServlet extends Loggable {
}
}
+ /**
+ * Tracks the two aspects of an AJAX version: the sequence number,
+ * whose sole purpose is to identify requests that are retries for the
+ * same resource, and pending requests, which indicates how many
+ * requests are still queued for this particular page version on the
+ * client. The latter is used to expire result data for sequence
+ * numbers that are no longer needed.
+ */
+ private case class AjaxVersionInfo(renderVersion:String, sequenceNumber:Int, pendingRequests:Int)
+ private object AjaxVersions {
+ def unapply(ajaxPathPart: String) : Option[AjaxVersionInfo] = {
+ val dash = ajaxPathPart.indexOf("-")
@dcbriccetti Collaborator

In this modern age of typography, it may be good to distinguish between hyphens (-) and the various dashes: en dash (–) and em dash (—). This code here appears to be dealing with a hyphen.

@Shadowfiend Owner

This is machine-handled and -generated, so no fear of typography here. ASCII only :)

@dcbriccetti Collaborator

Sorry, I could have been more clear: “hyphen” would be a better variable name, since the code isn’t dealing with dashes. A dash is something completely different from a hypen, and modern programs do deal with dashes sometimes, so it may be good to be precise in the language, and call a hyphen a hyphen.

@Shadowfiend Owner

Oho, I see! Fair point, fair point. I'll make that change, or maybe simply rename the variable to separator, which is a more functional name anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Shadowfiend added some commits
@Shadowfiend Shadowfiend Track sequence numbers of arbitrary length.
We track sequence numbers as Longs now, and the client side encodes the
sequence number as an arbitrarily-long base-36 number.
fd2bbba
@Shadowfiend Shadowfiend Guard for JS number size overflows.
If you hit the maximum of a JS integer, math starts getting wonky. We
make sure at that point we wrap to 0.
3c3a404
@Shadowfiend
Owner

Sequence numbers should now be able to take on an arbitrary length. We're limited by JavaScript's maximum positive Integer value (approximately 9007199254740992), and when we detect that we wrap back to 0.

@lkuczera
Collaborator
@Shadowfiend
Owner

We could, but I'd rather not. My reasoning is simple: we're not detecting the max integer via a hardcoded number, we're doing it by checking whether math breaks at a given point. Finding the min integer would be a pain, plus we'd have to deal with the possibility of a negative on the server as well, which would complicate the code without, I think, a clear benefit.

@fmpwizard
Owner

I think that having a limit of 9007199254740992 would pretty much mean unlimited on our case :) And if we get a bug report because that limit is too small, I'll be very happy to know about that user.

@dpp
Owner
dpp commented

Let's think of the practical issues here. If there are 9007199254740992 requests, somebody is out of memory. At 10ms per request, it would take 28 million years to make all those requests.

10 or 35 requests stacked up is something I can see happening (especially in a game where the user is clicking a button). 1,000 requests stacked up looks like a bug in the client or server code.

So, when I said, "no limits" I meant "no limits that would be hit absent a bug in the code".

@jeppenejsum
Owner

Not sure how easy this would be (and may not be needed for this PR), but I would love to have some test cases for the basic request/response handling so we make sure that things like the *Vars & handling of thread locals are working as expected

@Shadowfiend Shadowfiend Make AjaxRequestInfo track a Long version id.
Not sure how I got this to compile locally without this change...
f785ddb
@dpp dpp merged commit cbf51d4 into master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 24, 2012
  1. @Shadowfiend

    Move uriSuffix extraction into lift_ajaxHandler.

    Shadowfiend authored
    By having it in doAjaxCycle, there were situations where the uri suffix
    could get lost. The most obvious one was when a long-running ajax
    request was occurring, and two AJAX requests were queued during that
    time frame. This would result in the first request getting the second
    request's uriSuffix, and the second request getting no suffix at all.
    
    We now immediately put the uriSuffix into the sending data when
    lift_ajaxHandler is called.
  2. @Shadowfiend

    Encode an AJAX request version in the request URI.

    Shadowfiend authored
    The AJAX request version has two components: the actual version number
    and a count of queued requests. This is presented as two base-36 values
    after a dash. So a request now looks like:
    
    /ajax_request/F<page version>-v8
    
    Where v indicates request number 31 and 8 indicates that there are 8
    queued requests that have not been handled yet.
  3. @Shadowfiend

    Drop the timeout on comet-related AJAX requests.

    Shadowfiend authored
    While this will tie up a request thread for longer, it means we can
    reliably say that when the AJAX request thread completes, it will
    actually have the correct response to the original request.
  4. @Shadowfiend

    Add tracking for AJAX requests in LiftSession.

    Shadowfiend authored
    Request info consists of three things:
     - The request version.
     - A future for the response to the request, satisfied by the first
       request for this version when the response is ready.
     - A lastSeen timestamp, used to expire the entry after the usual
       function lifespan.
    
    LiftSession.withAjaxRequests exposes the request list, which is a Map
    mapping a page version to the list of AjaxRequestInfos currently being
    tracked for that page. AjaxRequestInfos are cleaned up according to
    their lastSeen timestamp, which is updated the same way as those of
    functions on the page.
  5. @Shadowfiend

    Implement the meat of AJAX deduplication.

    Shadowfiend authored
    AJAX requests now come in with a two-part version number, one a sequence
    number for the ajax request's count in the overall list of requests sent
    by the client, and one a number indicating how many other requests are
    queued up on the client.
    
    If this is the first request seen with its sequence number, we record
    the request in the session and run regular AJAX request handling. When
    that request handling is complete, it satisfies the future that is
    recorded in the session. Subsequent requests for a given sequence number
    wait on the future from the first request up to the ajax post timeout,
    then fail.
    
    When we get a request coming in with a 0 count for pending requests on
    the client, we clear out all other pending requests in the session's
    list, since that means none of them will be getting a chance to report
    their responses again.
Commits on Sep 28, 2012
  1. @Shadowfiend

    Track sequence numbers of arbitrary length.

    Shadowfiend authored
    We track sequence numbers as Longs now, and the client side encodes the
    sequence number as an arbitrarily-long base-36 number.
  2. @Shadowfiend

    Guard for JS number size overflows.

    Shadowfiend authored
    If you hit the maximum of a JS integer, math starts getting wonky. We
    make sure at that point we wrap to 0.
Commits on Oct 2, 2012
  1. @Shadowfiend

    Make AjaxRequestInfo track a Long version id.

    Shadowfiend authored
    Not sure how I got this to compile locally without this change...
This page is out of date. Refresh to see the latest.
View
250 web/webkit/src/main/scala/net/liftweb/http/LiftServlet.scala
@@ -423,75 +423,223 @@ class LiftServlet extends Loggable {
}
}
+ /**
+ * Tracks the two aspects of an AJAX version: the sequence number,
+ * whose sole purpose is to identify requests that are retries for the
+ * same resource, and pending requests, which indicates how many
+ * requests are still queued for this particular page version on the
+ * client. The latter is used to expire result data for sequence
+ * numbers that are no longer needed.
+ */
+ private case class AjaxVersionInfo(renderVersion:String, sequenceNumber:Long, pendingRequests:Int)
+ private object AjaxVersions {
+ def unapply(ajaxPathPart: String) : Option[AjaxVersionInfo] = {
+ val separator = ajaxPathPart.indexOf("-")
+ if (separator > -1 && ajaxPathPart.length > separator + 2)
+ Some(
+ AjaxVersionInfo(ajaxPathPart.substring(0, separator),
+ java.lang.Long.parseLong(ajaxPathPart.substring(separator + 1, ajaxPathPart.length - 1), 36),
+ Integer.parseInt(ajaxPathPart.substring(ajaxPathPart.length - 1), 36))
+ )
+ else
+ None
+ }
+ }
+ /**
+ * Extracts two versions from a given AJAX path:
+ * - The RenderVersion, which is used for GC purposes.
+ * - The requestVersions, which let 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[AjaxVersionInfo]) => T): T = {
+ path match {
+ case first :: AjaxVersions(versionInfo @ AjaxVersionInfo(renderVersion, _, _)) :: _ =>
+ RenderVersion.doWith(renderVersion)(f(Full(versionInfo)))
+ case _ => f(Empty)
+ }
+ }
+
+ /**
+ * 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(liftSession: LiftSession,
+ requestState: Req): Box[LiftResponse] = {
+ try {
+ requestState.param("__lift__GC") match {
+ case Full(_) =>
+ liftSession.updateFuncByOwner(RenderVersion.get, 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))
+ })
+
+ val what2 = what.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)
+ case s => Nil
+ }
+
+ val ret: LiftResponse = what2 match {
+ case (json: JsObj) :: Nil => JsonResponse(json)
+ case (jv: JValue) :: Nil => JsonResponse(jv)
+ case (js: JsCmd) :: xs => {
+ (JsCommands(S.noticesToJsCmd :: Nil) &
+ ((js :: xs).flatMap {
+ case js: JsCmd => List(js)
+ case _ => Nil
+ }.reverse) &
+ S.jsToAppend).toResponse
+ }
+
+ case (n: Node) :: _ => XmlResponse(n)
+ case (ns: NodeSeq) :: _ => XmlResponse(Group(ns))
+ case (r: LiftResponse) :: _ => r
+ case _ => JsCommands(S.noticesToJsCmd :: JsCmds.Noop :: S.jsToAppend).toResponse
+ }
+
+ 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);
+ }
+ }
+
+ // Retry requests will stop trying to wait for the original request to
+ // complete 500ms after the client's timeout. This is because, while
+ // we want the original thread to complete so that it can provide an
+ // answer for future retries, we don't want retries tying up resources
+ // when the client won't receive the response anyway.
+ private lazy val ajaxPostTimeout: Long = LiftRules.ajaxPostTimeout * 1000L + 500L
+ /**
+ * Kick off AJAX handling. Extracts relevant versions and handles the
+ * begin/end servicing requests. Then checks whether to wait on an
+ * existing request for this same version to complete or whether to
+ * do the actual processing.
+ */
private def handleAjax(liftSession: LiftSession,
requestState: Req): Box[LiftResponse] = {
- extractVersion(requestState.path.partPath) {
-
+ extractVersions(requestState.path.partPath) { versionInfo =>
LiftRules.cometLogger.debug("AJAX Request: " + liftSession.uniqueId + " " + requestState.params)
tryo {
LiftSession.onBeginServicing.foreach(_(liftSession, requestState))
}
- val ret = try {
- requestState.param("__lift__GC") match {
- case Full(_) =>
- liftSession.updateFuncByOwner(RenderVersion.get, 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))
- })
-
- val what2 = what.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)
- case s => Nil
+ // Here, a Left[LAFuture] indicates a future that needs to be
+ // *satisfied*, meaning we will run the request processing.
+ // A Right[LAFuture] indicates a future we need to *wait* on,
+ // meaning we will return the result of whatever satisfies the
+ // future.
+ val nextAction:Either[LAFuture[Box[LiftResponse]], LAFuture[Box[LiftResponse]]] =
+ versionInfo match {
+ case Full(AjaxVersionInfo(_, handlerVersion, pendingRequests)) =>
+ val renderVersion = RenderVersion.get
+
+ liftSession.withAjaxRequests { currentAjaxRequests =>
+ // Create a new future, put it in the request list, and return
+ // the associated info with the future that needs to be
+ // satisfied by the current request handler.
+ def newRequestInfo = {
+ val info = AjaxRequestInfo(handlerVersion, new LAFuture[Box[LiftResponse]], millis)
+
+ val existing = currentAjaxRequests.getOrElseUpdate(renderVersion, Nil)
+ currentAjaxRequests += (renderVersion -> (info :: existing))
+
+ info
}
- val ret: LiftResponse = what2 match {
- case (json: JsObj) :: Nil => JsonResponse(json)
- case (jv: JValue) :: Nil => JsonResponse(jv)
- case (js: JsCmd) :: xs => {
- (JsCommands(S.noticesToJsCmd :: Nil) &
- ((js :: xs).flatMap {
- case js: JsCmd => List(js)
- case _ => Nil
- }.reverse) &
- S.jsToAppend).toResponse
- }
+ val infoList = currentAjaxRequests.get(renderVersion)
+ val (requestInfo, result) =
+ infoList
+ .flatMap { entries =>
+ entries
+ .find(_.requestVersion == handlerVersion)
+ .map { entry =>
+ (entry, Right(entry.responseFuture))
+ }
+ }
+ .getOrElse {
+ val entry = newRequestInfo
- case (n: Node) :: _ => XmlResponse(n)
- case (ns: NodeSeq) :: _ => XmlResponse(Group(ns))
- case (r: LiftResponse) :: _ => r
- case _ => JsCommands(S.noticesToJsCmd :: JsCmds.Noop :: S.jsToAppend).toResponse
- }
+ (entry, Left(entry.responseFuture))
+ }
- LiftRules.cometLogger.debug("AJAX Response: " + liftSession.uniqueId + " " + ret)
+ // If there are no other pending requests, we can
+ // invalidate all the render version's AJAX entries except
+ // for the current one, as the client is no longer looking
+ // to retry any of them.
+ if (pendingRequests == 0) {
+ // Satisfy anyone waiting on futures for invalid
+ // requests with a failure.
+ for {
+ list <- infoList
+ entry <- list if entry.requestVersion != handlerVersion
+ } {
+ entry.responseFuture.satisfy(Failure("Request no longer pending."))
+ }
- Full(ret)
- } finally {
- if (S.functionMap.size > 0) {
- liftSession.updateFunctionMap(S.functionMap, RenderVersion.get, millis)
- S.clearFunctionMap
+ currentAjaxRequests += (renderVersion -> List(requestInfo))
}
+
+ result
}
+
+ case _ =>
+ // Create a future that processes the ajax response
+ // immediately. This runs if we don't have a handler
+ // version, which happens in cases like AJAX requests for
+ // Lift GC that don't go through the de-duping pipeline.
+ // Because we always return a Left here, the ajax processing
+ // always runs for this type of request.
+ Left(new LAFuture[Box[LiftResponse]])
}
- } catch {
- case foc: LiftFlowOfControlException => throw foc
- case e => S.assertExceptionThrown() ; NamedPF.applyBox((Props.mode, requestState, e), LiftRules.exceptionHandler.toList);
- }
+
+ val ret:Box[LiftResponse] =
+ nextAction match {
+ case Left(future) =>
+ val result = runAjax(liftSession, requestState)
+ future.satisfy(result)
+
+ result
+
+ case Right(future) =>
+ val ret = future.get(ajaxPostTimeout) openOr Failure("AJAX retry timeout.")
+
+ ret
+ }
+
tryo {
LiftSession.onEndServicing.foreach(_(liftSession, requestState, ret))
}
+
ret
}
}
View
51 web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala
@@ -500,6 +500,15 @@ private final case class PostPageFunctions(renderVersion: String,
}
/**
+ * The responseFuture will be satisfied by the original request handling
+ * thread when the response has been calculated. Retries will wait for the
+ * future to be satisfied in order to return the proper response.
+ */
+private[http] final case class AjaxRequestInfo(requestVersion: Long,
+ responseFuture: LAFuture[Box[LiftResponse]],
+ lastSeen: Long)
+
+/**
* The LiftSession class containg the session state information
*/
class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
@@ -558,6 +567,20 @@ 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 an entry for every AJAX request we don't *know*
+ * has completed successfully or been discarded by the client.
+ *
+ * See LiftServlet.handleAjax for how we determine we no longer need
+ * to hold a reference to an AJAX request.
+ */
+ private var ajaxRequests = scala.collection.mutable.Map[String,List[AjaxRequestInfo]]()
+
+ private[http] def withAjaxRequests[T](fn: (scala.collection.mutable.Map[String, List[AjaxRequestInfo]]) => T) = {
+ ajaxRequests.synchronized { fn(ajaxRequests) }
+ }
+
+ /**
* The synchronization lock for the postPageFunctions
*/
private val postPageLock = new Object
@@ -709,10 +732,8 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
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(li: List[_]) => li
+ a.!?(ActionMessageSet(f.map(i => buildFunc(i)), state)) match {
case li: List[_] => li
- case Empty => Full(a.cometProcessingTimeoutHandler())
case other => Nil
})
case _ => f.map(i => buildFunc(i).apply())
@@ -836,6 +857,22 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
}
}
+ withAjaxRequests { currentAjaxRequests =>
+ for {
+ (version, requestInfos) <- currentAjaxRequests
+ } {
+ val remaining =
+ requestInfos.filter { info =>
+ (now - info.lastSeen) <= LiftRules.unusedFunctionsLifeTime
+ }
+
+ if (remaining.length > 0)
+ currentAjaxRequests += (version -> remaining)
+ else
+ currentAjaxRequests -= version
+ }
+ }
+
synchronized {
messageCallback.foreach {
case (k, f) =>
@@ -963,6 +1000,14 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
} postPageFunctions += (ownerName -> funcInfo.updateLastSeen)
}
+ withAjaxRequests { currentAjaxRequests =>
+ currentAjaxRequests.get(ownerName).foreach { requestInfos =>
+ val updated = requestInfos.map(_.copy(lastSeen = time))
+
+ currentAjaxRequests += (ownerName -> updated)
+ }
+ }
+
synchronized {
(0 /: messageCallback)((l, v) => l + (v._2.owner match {
case Full(owner) if (owner == ownerName) =>
View
44 web/webkit/src/main/scala/net/liftweb/http/js/ScriptRenderer.scala
@@ -46,6 +46,18 @@ object ScriptRenderer {
toSend.onSuccess = theSuccess;
toSend.onFailure = theFailure;
toSend.responseType = responseType;
+ toSend.version = liftAjax.lift_ajaxVersion++;
+
+ // Make sure we wrap when we hit JS max int.
+ var version = liftAjax.lift_ajaxVersion
+ if ((version - (version + 1) != -1) || (version - (version - 1) != 1))
+ liftAjax.lift_ajaxVersion = 0;
+
+ if (liftAjax.lift_uriSuffix) {
+ theData += '&' + liftAjax.lift_uriSuffix;
+ toSend.theData = theData;
+ liftAjax.lift_uriSuffix = undefined;
+ }
liftAjax.lift_ajaxQueue.push(toSend);
liftAjax.lift_ajaxQueueSort();
@@ -102,7 +114,8 @@ object ScriptRenderer {
},
lift_registerGC: function() {
- var data = "__lift__GC=_"
+ var data = "__lift__GC=_",
+ version = null;
""" + LiftRules.jsArtifacts.ajax(AjaxInfo(JE.JsRaw("data"),
"POST",
LiftRules.ajaxPostTimeout,
@@ -160,13 +173,10 @@ object ScriptRenderer {
aboutToSend.responseType.toLowerCase() === "json") {
liftAjax.lift_actualJSONCall(aboutToSend.theData, successFunc, failureFunc);
} else {
- var theData = aboutToSend.theData;
- if (liftAjax.lift_uriSuffix) {
- theData += '&' + liftAjax.lift_uriSuffix;
- aboutToSend.theData = theData;
- liftAjax.lift_uriSuffix = undefined;
- }
- liftAjax.lift_actualAjaxCall(theData, successFunc, failureFunc);
+ var theData = aboutToSend.theData,
+ version = aboutToSend.version;
+
+ liftAjax.lift_actualAjaxCall(theData, version, successFunc, failureFunc);
}
}
}
@@ -180,17 +190,22 @@ object ScriptRenderer {
setTimeout("liftAjax.lift_doAjaxCycle();", 200);
},
- addPageName: function(url) {
- return """ + {
- if (LiftRules.enableLiftGC) {
- "url.replace('" + LiftRules.ajaxPath + "', '" + LiftRules.ajaxPath + "/'+lift_page);"
+ lift_ajaxVersion: 0,
+
+ addPageNameAndVersion: function(url, version) {
+ """ + {
+ if (LiftRules.enableLiftGC) { """
+ var replacement = '""" + LiftRules.ajaxPath + """/'+lift_page;
+ if (version)
+ replacement += ('-'+version.toString(36)) + (liftAjax.lift_ajaxQueue.length > 35 ? 35 : liftAjax.lift_ajaxQueue.length).toString(36);
+ return url.replace('""" + LiftRules.ajaxPath + """', replacement);"""
} else {
- "url;"
+ "return url;"
}
} + """
},
- lift_actualAjaxCall: function(data, onSuccess, onFailure) {
+ lift_actualAjaxCall: function(data, version, onSuccess, onFailure) {
""" +
LiftRules.jsArtifacts.ajax(AjaxInfo(JE.JsRaw("data"),
"POST",
@@ -201,6 +216,7 @@ object ScriptRenderer {
},
lift_actualJSONCall: function(data, onSuccess, onFailure) {
+ var version = null;
""" +
LiftRules.jsArtifacts.ajax(AjaxInfo(JE.JsRaw("data"),
"POST",
View
2  web/webkit/src/main/scala/net/liftweb/http/js/jquery/JQueryArtifacts.scala
@@ -97,7 +97,7 @@ trait JQueryArtifacts extends JSArtifacts {
def ajax(data: AjaxInfo): String = {
"jQuery.ajax(" + toJson(data, S.contextPath,
prefix =>
- JsRaw("liftAjax.addPageName(" + S.encodeURL(prefix + "/" + LiftRules.ajaxPath + "/").encJs + ")")) + ");"
+ JsRaw("liftAjax.addPageNameAndVersion(" + S.encodeURL(prefix + "/" + LiftRules.ajaxPath + "/").encJs + ", version)")) + ");"
}
/**
Something went wrong with that request. Please try again.