Lift’s comet implementation and multiple tabs.

Opening two tabs and looking at a Comet page in lift leads to the web browser polling the page every 100ms. I posted a solution to the Lift mailing list for this, complete with some example source:

I recently implemented a comet application using Lift (quakewidget.com). The application use case was sending events to up to several thousand clients at fairly infrequent intervals, maybe once every minute at most. Everything worked fine except for when users would open multiple tabs on the application, which was a frequent occurrence.

I’ve read some of the history of this topic on the list and understand that in the case of multiple tabs looking at the same comet stream, the comet implementation will switch to having each tab poll the server every 100ms. I understand the reasoning behind this is to not cause connection starvation for older browsers with a hard per-server connection limit, and to provide low message latency on a site with very frequent updates, but for my use case I would like to suggest a slightly different implementation that I think would work better and is pretty tried in tested in similar message queue situations.

The idea is to use an exponential backoff algorithm to avoid polling the server if messages are infrequent. When a browser tab is long-polling the server and another browser tab connects to start a comet session, the browser will disconnect the first browser tab, and if the first browser tab has not received any messages during it’s connection to the server it will increase the amount of time it waits before trying back by a random increment, up to a LiftRules defined maximum value. For a chat site, this might be 3000ms, for my site it might be 10s. This way, the speed of the ping-ponging back and forth between browser tabs is slowed down if there aren’t a lot of messages arriving. When a message arrives for the particular tab, the polling delay will be reset back down to the minimum, 100ms.

Here’s some more about exponential backoff:
http://en.wikipedia.org/wiki/Exponential_backoff

How it’s used on github for ajax polling:
http://github.com/blog/467-smart-js-polling

A way to implement this in the framework would be for the comet implementation to keep track of the retry delay in the client generated javascript and increment it by a small random amount for every connection that did not receive a message. Each partial update sent to the client would need to reset the comet poll timeout to its minimum value. This could be implemented using a combination of a custom LiftRules.renderCometScript and sending a PartialUpdate with each message to reset the success retry timeout, but I think this is something that’s better handled by the framework.

Here’s my implementation:

<!--
 * Implements backoff policy for comet server
 * In your main template you have to setup the variable with
-->
<script>
   var retryTime=0;
</script>
object CometBackoffRetry { 
  //backoff Increment
  val retryInc=2000;
  // Minimum repoll time
  val retryMin=100;  
  //Maximum repoll time
  val retryMax=10000;
  
  def cometHandler = JsCmds.Run("""
      function lift_handlerSuccessFunc() {  
      	  if (retryTime < """+retryMin+""") { retryTime = """ + retryMin +"""; }  
    	  setTimeout("lift_cometEntry();",retryTime);
          retryTime=retryTime+"""+new Random().nextInt(retryInc)+""";
		  if (retryTime >"""+retryMax+""") { retryTime= """+retryMax+"""; }
      }
      function lift_handlerFailureFunc() {setTimeout("lift_cometEntry();",""" 
		  + LiftRules.cometFailureRetryTimeout + """);}
      function lift_cometEntry() {""" +
                    LiftRules.jsArtifacts.comet(AjaxInfo(JE.JsRaw("lift_toWatch"),
                                                             "GET",
                                                             LiftRules.cometGetTimeout,
                                                             false,
                                                             "script",
                                                             Full("lift_handlerSuccessFunc"),
                                                             Full("lift_handlerFailureFunc"))) + " } \n" +
                        LiftRules.jsArtifacts.onLoad(new JsCmd() {
        def toJsCmd = "lift_handlerSuccessFunc()"
      }).toJsCmd)

      /**
       * Add this to each partial update from your comet server
       * e.g partialUpdate(List(CometBackoffRetry.resetRetryTimeout, ...
       **/
      def resetRetryTimeout : JsRaw = {  JsRaw("retryTime="+retryMin+";") }

}
class Boot {

// ... Init Code Goes Here ...

    /**
     * If the user opens multiple tabs, Lift starts ping ponging between the tabs every 100ms
     * Workaround here increases the retry time while there are no messages
     * See: http://markmail.org/message/y6vd2ug7yoi2drf7
     * Also: http://www.mail-archive.com/liftweb@googlegroups.com/msg11318.html
     **/
    LiftRules.renderCometScript= session => CometBackoffRetry.cometHandler
}

Lift 2.0 + Scala 2.8 + Maven, in Eclipse!

I got the latest Scala 2.8 and Lift 2.0 all working with Eclipse and Maven.  Here’s the relevant pom.xml :


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>com.myproject</groupId>
 <artifactId>myproject</artifactId>
 <version>1.0-SNAPSHOT</version>
 <packaging>war</packaging>
 <name>myproject</name>
 <inceptionYear>2007</inceptionYear>
 <properties>
 <scala.version>2.8.0.RC6</scala.version>
 </properties>

 <repositories>
 <repository>
 <id>scala-tools.org</id>
 <name>Scala-Tools Maven2 Repository</name>
 <url>http://scala-tools.org/repo-releases</url>
 </repository>
 <repository>
 <id>scala-tools.org snap</id>
 <name>Scala-Tools Maven2 Repository</name>
 <url>http://scala-tools.org/repo-snapshots</url>
 </repository>
 </repositories>

 <pluginRepositories>
 <pluginRepository>
 <id>scala-tools.org</id>
 <name>Scala-Tools Maven2 Repository</name>
 <url>http://scala-tools.org/repo-releases</url>
 </pluginRepository>
 <pluginRepository>
 <id>scala-tools.org snap</id>
 <name>Scala-Tools Maven2 Repository</name>
 <url>http://scala-tools.org/repo-snapshots</url>
 </pluginRepository>
 </pluginRepositories>

 <dependencies>
 <dependency>
 <groupId>postgresql</groupId>
 <artifactId>postgresql</artifactId>
 <version>8.4-701.jdbc4</version>
 </dependency>
 <dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.12</version>
 </dependency>

 <dependency>
 <groupId>org.scala-lang</groupId>
 <artifactId>scala-library</artifactId>
 <version>${scala.version}</version>
 </dependency>
 <dependency>
 <groupId>net.liftweb</groupId>
 <artifactId>lift-core</artifactId>
 <version>2.0-scala280-SNAPSHOT</version>
 </dependency>
 <dependency>
 <groupId>net.liftweb</groupId>
 <artifactId>lift-mapper</artifactId>
 <version>2.0-scala280-SNAPSHOT</version> <!-- or 1.1-SNAPSHOT, etc -->
 </dependency>
 <dependency>
 <groupId>org.apache.derby</groupId>
 <artifactId>derby</artifactId>
 <version>10.4.2.0</version>
 </dependency>
 <dependency>
 <groupId>javax.servlet</groupId>
 <artifactId>servlet-api</artifactId>
 <version>2.5</version>
 <scope>provided</scope>
 </dependency>
 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>4.5</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>jetty</artifactId>
 <version>[6.1.6,)</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>org.scala-tools.testing</groupId>
 <artifactId>specs</artifactId>
 <version>1.6.1-2.8.0.Beta1-RC6</version>
 <scope>test</scope>
 </dependency>
 <!-- for LiftConsole -->
 <dependency>
 <groupId>org.scala-lang</groupId>
 <artifactId>scala-compiler</artifactId>
 <version>${scala.version}</version>
 <scope>test</scope>
 </dependency>
 </dependencies>

 <build>
 <sourceDirectory>src/main/scala</sourceDirectory>
 <testSourceDirectory>src/test/scala</testSourceDirectory>
 <plugins>
 <plugin>
 <groupId>org.scala-tools</groupId>
 <artifactId>maven-scala-plugin</artifactId>
 <version>2.12</version>
 <executions>
 <execution>
 <goals>
 <goal>compile</goal>
 <goal>testCompile</goal>
 </goals>
 </execution>
 </executions>
 <configuration>
 <scalaVersion>${scala.version}</scalaVersion>
 </configuration>
 </plugin>
 <plugin>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>maven-jetty-plugin</artifactId>
 <configuration>
 <contextPath>/</contextPath>
 <scanIntervalSeconds>5</scanIntervalSeconds>
 </configuration>
 </plugin>
 <plugin>
 <groupId>net.sf.alchim</groupId>
 <artifactId>yuicompressor-maven-plugin</artifactId>
 <executions>
 <execution>
 <goals>
 <goal>compress</goal>
 </goals>
 </execution>
 </executions>
 <configuration>
 <nosuffix>true</nosuffix>
 </configuration>
 </plugin>
 <plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-eclipse-plugin</artifactId>
 <configuration>
 <downloadSources>true</downloadSources>
 <excludes>
 <exclude>org.scala-lang:scala-library</exclude>
 </excludes>
 <classpathContainers>
 <classpathContainer>ch.epfl.lamp.sdt.launching.SCALA_CONTAINER</classpathContainer>
 </classpathContainers>
 <projectnatures>
 <java.lang.String>ch.epfl.lamp.sdt.core.scalanature</java.lang.String>
 <java.lang.String>org.eclipse.jdt.core.javanature</java.lang.String>
 </projectnatures>
 <buildcommands>
 <java.lang.String>ch.epfl.lamp.sdt.core.scalabuilder</java.lang.String>
 </buildcommands>
 </configuration>
 </plugin>
 <plugin>
 <groupId>org.kohsuke.jetty</groupId>
 <artifactId>jetty-maven-plugin</artifactId>
 <version>7.0.0pre1</version>
 <configuration></configuration>
 </plugin>
 </plugins>
 </build>
 <reporting>
 <plugins>
 <plugin>
 <groupId>org.scala-tools</groupId>
 <artifactId>maven-scala-plugin</artifactId>
 <configuration>
 <scalaVersion>${scala.version}</scalaVersion>
 </configuration>
 </plugin>
 </plugins>
 </reporting>
</project>