tag:blogger.com,1999:blog-16543641899494027532024-03-13T03:02:43.054-07:00benqua technical small talkbenquahttp://www.blogger.com/profile/10810612975707018734noreply@blogger.comBlogger2125tag:blogger.com,1999:blog-1654364189949402753.post-58708624633620149802012-04-14T09:03:00.000-07:002012-11-30T00:14:10.237-08:00Hub: Interesting but undocumented feature of Playframework 2<h3>
<span style="color: red; font-family: Helvetica Neue;">WARNING: </span></h3>
<div>
<span style="font-family: Helvetica Neue;">hub are depreciated in Play2.1. They are replace by Concurrent.broadcast (which is also not documented). I did a very small example that demonstrate the concept and post it on github: </span><a href="https://github.com/benqua/BroadcasterInPlay2">https://github.com/benqua/BroadcasterInPlay2</a></div>
<div>
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<h3>
<span style="font-family: 'Helvetica Neue';">The Problem</span></h3>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">Write a stream based (server sent event, comet or WebSocket) web application where many clients need to get the same information (news feed or any kind of dashboard for example).</span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">Why push the same information to all clients? Because this information may be costly to build or it is important every client gets the exact same, time dependent, information.</span></div>
<div style="margin-bottom: 0in;">
<br />
<h3>
<span style="font-family: 'Helvetica Neue';">The Concepts</span></h3>
</div>
<span style="font-family: 'Helvetica Neue';">To deal with information stream, Play provides many new (to me) interesting concepts, like </span><a href="https://github.com/playframework/Play20/wiki/Iteratees" style="font-family: 'Helvetica Neue';" target="_blank">Iteratees</a><span style="font-family: 'Helvetica Neue';">, </span><a href="https://github.com/playframework/Play20/wiki/Enumerators" style="font-family: 'Helvetica Neue';" target="_blank">Enumerators</a><span style="font-family: 'Helvetica Neue';"> and </span><a href="https://github.com/playframework/Play20/wiki/Enumeratees" style="font-family: 'Helvetica Neue';" target="_blank">Enumartees</a><span style="font-family: 'Helvetica Neue';">. Check the documentation. </span><br />
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">Basically we want to push an enumerator (think: “input data stream”) to many web clients.</span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">To do so, Play2 provides an undocumented object called “<a href="https://github.com/playframework/Play20/blob/master/framework/src/play/src/main/scala/play/api/libs/iteratee/Concurrent.scala" target="_blank">hub</a>”. Lets see it in action.</span><br />
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<div style="margin-bottom: 0in;">
<h3>
<span style="font-family: 'Helvetica Neue';">The Code</span></h3>
</div>
<span style="font-family: 'Helvetica Neue';">For this example, we will use “</span><a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/" style="font-family: 'Helvetica Neue';" target="_blank">Server Sent Events</a><span style="font-family: 'Helvetica Neue';">” to push information from the server to many clients.</span><br />
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">First, a simple example <b>without hub</b>, the code of the controller could be:</span></div>
<div style="margin-bottom: 0in;">
<pre>
//no hub: each client gets its own event
def serverevents = Action {
val timestep = 1000
//getmtime is an Enumarator that provides
// a new number (time) every second
val getmtime = Enumerator.fromCallback{ () =>
Promise.timeout( {
//time in tenth of sec
val currentTime = java.lang.System.currentTimeMillis() / 100
Logger.debug(currentTime toString)
Some(currentTime +":ds")},
timestep, TimeUnit.MILLISECONDS )
}
// the getmtime enumerator is pushed through the Enumerattee returned
// by the apply method of the EventSource object,
// to be transformed into a Server Sent Event.
// The event is then sent to the client.
Ok.stream(getmtime &> EventSource()).as("text/event-stream")
}
</pre>
</div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">If you run this example in two browsers, you will see that they get different results. It is also obvious, from the log in the Play console, that a new time list is generated for each client. </span><br />
<span style="font-family: 'Helvetica Neue';">There is one line per client per second in the log.</span><br />
<span style="font-family: 'Helvetica Neue';">If this small function (getting current time in milliseconds) were resource intensive, resource usage would grow linearly with the number of client. There is one call to the function per client every time step.</span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">Now, let's <b>add the hub </b>and refactor a little bit the controller code:</span><br />
<pre>
val timestep = 1000
val timeEnum = Enumerator.fromCallback{ () =>
Promise.timeout( {
val currentTime = java.lang.System.currentTimeMillis()
Logger.debug(currentTime toString)
Some(currentTime +":ds")},
timestep, TimeUnit.MILLISECONDS )
}
// now, the hub that let us
// "share" the timeEnum between all clients
val hub = Concurrent.hub[String](timeEnum)
//with hub: one event generation for all clients;
def serverhubevents = Action {
implicit val encoder = Comet.CometMessage.stringMessages
Ok.stream(hub.getPatchCord &> EventSource()).as("text/event-stream")
}</pre>
</div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';"><br /></span>
<span style="font-family: 'Helvetica Neue';">Note the use of the magic and undocumented hub.getPatchCord function which, at least conceptually, returns a copy of the timeEnum Enumerator.</span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">With this code, all clients will get the exact same output. The (potentially) resource intensive function that feed the timeEnum would be called only once every time step, independently of the number of clients.</span><br />
<span style="font-family: 'Helvetica Neue';"><br /></span>
<h3>
<span style="font-family: 'Helvetica Neue';">Conclusion</span></h3>
<span style="font-family: 'Helvetica Neue';">Hubs are a great way to multiplex an answer to many clients. </span></div>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">Sadly, there is no documentation for the moment.</span><br />
<br />
<span style="font-family: 'Helvetica Neue';"><br /></span></div>
<h3>
<span style="font-family: 'Helvetica Neue';">Open Questions</span></h3>
<div style="margin-bottom: 0in;">
</div>
<span style="font-family: 'Helvetica Neue';">I don't understand everything in my own (based on “try and test”) code :( Hopefully, someone can shed some light on the two below open points:</span><br />
<div style="margin-bottom: 0in;">
<ol>
<li><span style="font-family: 'Helvetica Neue';">First, a Scala question: why do I have to add the “implicit” parameter in the hub version while it is not necessary in the version without hub?</span></li>
<li><span style="font-family: 'Helvetica Neue';">In the first example, if I remove the Enumerator definition from the end point function and put it in the controller object scope, I get the exact same result. In this case, shouldn't the Enumerator be shared between the clients? I don't know exactly what should happen, but I don't understand why each client gets its own enumerator:</span></li>
</ol>
</div>
<div style="margin-bottom: 0in;">
<pre>
val timestep = 1000
val timeEnum = Enumerator.fromCallback{ () =>
Promise.timeout( {
val currentTime = java.lang.System.currentTimeMillis()
Logger.debug(currentTime toString)
Some(currentTime +":ds")},
timestep, TimeUnit.MILLISECONDS )
}
//why do each client have its own timeEnum
// (declared in the controller object scope)??
def servereventsshared = Action {
implicit val encoder = Comet.CometMessage.stringMessages
Ok.stream(timeEnum &> EventSource()).as("text/event-stream")
}
</pre>
<span style="font-family: 'Helvetica Neue';"><br /></span>
<span style="font-family: 'Helvetica Neue';">This gives the exact same result as the version </span><span style="font-family: 'Helvetica Neue';">without hub</span><span style="font-family: 'Helvetica Neue';">. </span><br />
<span style="font-family: 'Helvetica Neue';"><br /></span>
<span style="font-family: 'Helvetica Neue';">Last question: how will the server know that a client browser window has been closed and that the stream shouldn't be pushed to this client anymore? no way... ?</span></div>
<div style="margin-bottom: 0in;">
</div>
<h3>
<span style="font-family: 'Helvetica Neue';">Thanks</span></h3>
<div style="margin-bottom: 0in;">
<span style="font-family: 'Helvetica Neue';">Thanks to Gaëtan Renaudeau who wrote a very interesting <a href="http://blog.greweb.fr/2012/03/play-painter-how-ive-improved-the-30-minutes-prototyped-version/" target="_blank">experiment</a> and blog post using, among other things, hub. His experiment raises my attention to this undocumented Play2 feature.</span></div>
<br />benquahttp://www.blogger.com/profile/10810612975707018734noreply@blogger.com2tag:blogger.com,1999:blog-1654364189949402753.post-67619374356486412812011-11-09T03:36:00.000-08:002012-04-14T07:22:08.492-07:00Some technical notesWhen I play with new technologies, I sometimes take a few personal notes.<br />
These notes have nothing confidential and can maybe sometimes be useful to someone else playing with the same technologies; hence, I decided to share them.<br />
<br />
Feel free to comment but do not expect anything else than what you found.benquahttp://www.blogger.com/profile/10810612975707018734noreply@blogger.com0