3

tl;dr

I'm streaming content with Dancer2's keywords delayed, flush, content, done and it blocks the server while streaming until I call done. How can I avoid that?

More detailed

I wrote a Dancer2 application that calls an external command (a shell/Perl script) when the user hits a specific button. I want the output of that command to appear in the browser in a <pre> section and I want to refresh that section as new lines arrive. Similar to a tail -f but in the browser. My external command runs several seconds, possibly up to 10 minutes, so I run it asynchronously.

My first approach was to completely detach the program from the Webserver using double-fork, exec, ssid, and closing/reopening the program's STDIN/OUT/ERR so that the command's output goes to a temporary logfile. Then I set up AJAX calls (one per second) so that my Dancer2 application reads the new lines from the logfile and returns them to the client until the PID of the external command would disappear. This worked like a charm until the moment when my "external command" issued ssh commands to contact some other server and return that output as well. ssh doesn't work properly when run without a terminal and the ssh did not produce any output. Think of ssh $host "ls -l", which gave no output.

So I switched to Dancer2's delayed mechanism like shown in the code below. I took the CSV example from the Dancer2 docs as a template. Again, it works like a charm, but while the command is running and new lines appear in the browser, the server is blocked. When I click some other link on the Webpage, I only see an hour glass until the command is over. It looks like the server is single-process and single threaded.

index.tt

<script>
    function start_command( event ) {

        $('#out_win').empty();
        var last_response_len = false;
        $.ajax({
            url: '/ajax/start',
            xhrFields:  {
                onprogress: function(evt){
                    /* make "this_response" only contain the new lines: */
                    var this_response;
                    var response = evt.currentTarget.response;
                    if ( last_response_len === false ) {
                        this_response = response;
                        last_response_len = response.length;
                    } else {
                        this_response = response.substring(last_response_len);
                        last_response_len = response.length;
                    }

                    /* add those new lines to <pre> and scroll down */
                    var pre = $('#out_win');
                    pre.append(this_response); 
                    pre.scrollTop(pre.prop('scrollHeight'));
                }
            },
            success: function(result, textStatus, jqXHR) {
                alert("Done streaming, result="+result);
            },
            error: function( jqXHR, textStatus, errorThrown ) {
                alert("error; status=" + textStatus);
            },
        });

        event.preventDefault();
    }
</script>

<div class="container">

    <div>   <%# Links %>
        <a href="javascript:start_command();">Start external command</a></br>
        <a href="/other">Show some other page</a>
    <div>

    <div>   <%# output window %>
        <pre class="pre-scrollable" id="out_win"></pre>
    </div>

</div>

Streaming.pm

package Streaming;
use Dancer2;

################################################################
#
################################################################
get '/' => sub {
    template 'index';
};

################################################################
#
################################################################
get '/ajax/start' => sub {
    delayed {
        flush;    # streaming content

        # "stream" something. Actually I start an external program here
        # with open(..., "$pgm |") and stream its output line my line.
        foreach my $line ( 1 .. 10 ) {
            content "This is line $line\n";
            sleep(1);
        }
        done;    # close user connection
    }
    on_error => sub {
        my ($error) = @_;
        warning 'Failed to stream to user: ' . request->remote_address;
    };
};

true;

I'm using

  • Dancer2 0.204002 as installed via apt install libdancer2-perl on
  • Ubuntu 17.04
  • no further Webserver, i.e. I'm using the server that ships with Dancer2, started with plackup -a bin/app.psgi
  • jQuery 2.2.4
  • Bootstrap 3.3.7
  • Perl 5.18+

My questions are:

  • Am I doing something completely wrong wrt. the delayed keyword?
  • Or am I using the wrong Webserver for this task? I'd love to stick with the Webserver that comes with Dancer2 because it's so simple to use and I don't need a throughput like e.g. Google does. We will have 1-3 users a day and mostly not at the same time, but a blocking Webapplication would be a no-go.
PerlDuck
  • 5,610
  • 3
  • 20
  • 39
  • https://metacpan.org/favorite/leaderboard => (dancer vs dancer2 vs mojolicious rank) – mpapec May 26 '17 at 19:00
  • @Сухой27 So you're telling me I should abandon Dancer2 and switch to another framework? I always thought Mojolicious vs. Dancer2 is just like Mercedes vs. BMW or Boeing vs. Airbus. ;-) – PerlDuck May 26 '17 at 19:05
  • It is. Mojo currently has the bigger userbase, because they are better at marketing and they have a shiny raptor that spews a rainbow. :) – simbabque May 26 '17 at 19:10
  • 1
    The default Plack server that `plackup` starts is a single process. If this is during development, that's fine. For production you don't want that. You'd use something like Gazelle or Starman, so you have a bunch of processes. – simbabque May 26 '17 at 19:12
  • @simbabque not to mention non-blocking from early on, websockets and what not (so perhaps marketing is not in the first place:) ). – mpapec May 26 '17 at 19:13
  • @Сухой27 yeah, Mojo has that. But it's a different product. I think you can't compare them. It fills a different niche in my opinion. I'm not saying Mojo is bad, just that it's different. D2 is a micro framework. Mojo brings one too, but D2 is mostly superior to Mojo::Lite in my opinion. As soon as we are talking about Mojolicious though, we can't compare it to D2. Maybe to Catalyst, but the different approaches also make that hard. A lot of stuff is super easy with Mojo, yes. But some stuff gets messy fast I believe. – simbabque May 26 '17 at 19:16
  • @PerlDuck honestly I can't see why I would use Dancer, as features are favoring Mojolicious. – mpapec May 26 '17 at 19:17
  • @Сухой27 somehow that sounds like someone saying _I don't understand why you use Perl. Ruby/Python/Node.js are so much better_. Let's not do that please. Or if we do, let's do it with a few beers and a few laptops at a conference. :) – simbabque May 26 '17 at 19:17
  • ok, since you mentioned nodejs, what should I use under perl to match node async/non blocking and websockets? Frameworks which lack these nowadays are IMO not worth exploring. – mpapec May 26 '17 at 19:20
  • and these features are obviously important (hint golang, elixir, and not just nodejs) – mpapec May 26 '17 at 19:22
  • 1
    Uh, did I just open Pandora's Box? However, I just introduced D2 at work and they wouldn't especially appreciate it if I'd tell them to switch. I like D2 and now quite familiar with it. For a test I installed `starman` as a Webserver and now my async/delayed script is not blocking any longer. – PerlDuck May 26 '17 at 19:23
  • I'm happy that worked out. Don't worry about the discussion. Pick the tools that you are familiar with to get the job done. Also, go home. It's Friday night and you should not be writing new code at $work at ten in the evening. ;) – simbabque May 26 '17 at 20:54
  • @Сухой27 I mentioned those because that's what I have to deal with every time I go to a meetup here in Berlin and disclose that I do Perl for a living. That is, if the other party knows what that is, because a lot of them are too young or too focused on front-end and don't even know that there is a Perl on there shiny macbooks. :) – simbabque May 26 '17 at 20:56
  • No worries, @simbabque. I wasn't at work when asking this. My private curiosity often just matches the requirements at work :-). To summarize: I simply need another Webserver (e.g. Starman) for my task, but my use of `delayed` is basically correct, right? I found [Dave Cross' answer to a related question](https://stackoverflow.com/a/43762085/5830574) which points to the same direction. Would Apache2 and FCGI (instead of Starman) work as well? – PerlDuck May 28 '17 at 13:03
  • 1
    Yes, that would work. I have not used the async stuff, so I can't say if that's correct, but it doesn't look wrong to me. If it works, it must be at least a bit correct. :) – simbabque May 28 '17 at 13:28

0 Answers0