Long-running requests with Progress Bar in Dancer / AnyEvent
For an application of mine which does very long-running requests (the server needs to communicate with a slow backend over the internet), I wanted to show the user a progress bar. Also, it should not just display some progress, but the real deal (communicate with the server to find out the current progress), because the browser already just displays some progress.
So, to outline my idea: When the user sends a request to the route, let’s call it /long, Dancer would immediately return a page with a little javascript that periodically calls /progress and updates the progress bar. When /progress returns undef, the operation is done and the user gets redirected to the /success route handler.
Since Dancer is not asynchronous by nature, this requires using AnyEvent (see my previous posts). You have to be aware of how AnyEvent works: Just calling a timer which does the long-running operation once will not be sufficient, as the timer execution blocks and Dancer won't handle requests while AnyEvent is in control. Instead, you should cut it into little blocks. In my case, this is easy, since I’m doing HTTP requests and AnyEvent::HTTP handles them just fine.
Alright, let’s dive into the code. I will start with the Dancer part:
package poc; use Dancer ':syntax'; use AnyEvent;our $VERSION = '0.1';
set serializer => 'JSON';
my %long_ops;
We have just included AnyEvent, set the default serializer to JSON (very convenient in the javascript later) and defined a %long_ops hash.
get '/long' => sub { session 'progress' => 0; session 'progresslimit' => 5; $long_ops{foo} = { cnt => 0, guard => AnyEvent->timer(after => 1, interval => 1, cb => sub { my $cnt = $long_ops{foo}->{cnt} + 1; $long_ops{foo}->{cnt} = $cnt; if ($cnt == 5) { undef $long_ops{foo}->{guard}; session 'progress' => undef; } else { session 'progress' => $cnt; debug "progress is $cnt"; } } }; template 'index'; };
The /long route is where all the magic happens. First we set the 'progress' and 'progresslimit' session variables to 0 and 5, meaning that progress starts at 0 and can reach 5 at max. Afterwards, we initialize our data structure (note that due to this being a proof-of-concept we just use 'foo' instead of a reasonable per-client identifier) with the current status (cnt) and a guard for the AnyEvent timer. As I explained in previous posts, AnyEvent objects fall out of scope if you don’t keep a reference to them.
The timer will be triggered 5 times and all it does is increasing the progress on every iteration. As soon as it reaches 5, it will undef the guard and thereby disable itself.
The route returns the template 'index' at which we will take a look in a second.
get '/progress' => sub { { progress => session('progress'), progresslimit => session('progresslimit') } };
The /progress route returns a hash with the current and maximum progress. Due to the default serializer being set to JSON, Dancer will automatically serialize the hash to JSON.
Now that we have the server side, you can already test it with curl:
$ curl --cookie-jar /tmp/cookies http://localhost:8000/long $ while [ 1 ] ; do curl -b /tmp/cookies http://localhost:8000/progress; sleep 1; done
Now we just need to do the same in javascript. Of course, we will make use of the bundled jQuery that comes with Dancer, so besides modifying views/index.tt, no other changes are required.
<h1>Long-running request demo</h1><div id="progress">Progress not yet initialized</div>
<script type="text/javascript">
function pollProgress() {
$.getJSON('/progress', function(data) {
if (data.progress === null) {
return;
}
$('#progress').text('progress: ' + data.progress + ' / ' + data.progresslimit);
setTimeout(pollProgress, 1000);
});
}
$(document).ready(function() {
pollProgress();
});
</script>
Since this is not a jQuery tutorial, I will cover only briefly what I’m doing here: When the document is loaded, pollProgress() will be called to get the initial progress (current = 0, max = 5). This is done by calling jQuery’s getJSON helper with an appropriate callback function. The callback exits if progress is null (undef in Perl) or otherwise updates the div with id progress and schedules the next call of pollProgress() in one second.
That’s it for now. I think I’m going to make a Dancer::Plugin out of this so that you can use it easily. Comments/Improvements very welcome!
My last post seemingly got eaten somewhere.
My question is - why use Javascript when a META Refresh tag can do the same, but without the security implications that Javascript brings with it?
I guess you could also use the meta refresh tag. The advantage of the javascript solution is that it doesn’t cause a visible refresh in your browser and thus doesn’t disturb you.
Since this is intended for an intranet application anyways, javascript security implications are not much of a deal here. After all it’s a matter of taste, which solution you prefer, I guess.
Firstly, you missed an end ")" in your code example.
The most important thing is you don't even mention that your example code requires Twiggy to work. Same thing happens in your Dancer::Plugin::Progress document on CPAN.
I did notice your previous post which mentioned how you glue the AnyEvent and Dancer after hours of investigation.
Anyway, thank you for sharing the inspiration of "Progressing Bar".
Firstly, you missed an end ")" in your code example.
The most important thing is you don't even mention that your example code requires Twiggy to work. Same thing happens in your Dancer::Plugin::Progress document on CPAN.
I did notice your previous post which mentioned how you glue the AnyEvent and Dancer after hours of investigation.
Anyway, thank you for sharing the inspiration of "Progressing Bar".
Firstly, you missed an end ")" in your code example.
The most important thing is you don't even mention that your example code requires Twiggy to work. Same thing happens in your Dancer::Plugin::Progress document on CPAN.
I did notice your previous post which mentioned how you glue the AnyEvent and Dancer after hours of investigation.
Anyway, thank you for sharing the inspiration of "Progressing Bar".