Skip to content

Make your app not block

Stefan Adams edited this page Jan 18, 2014 · 1 revision

The key is that you need to design your entire app top to bottom to use non-blocking, and generally if you've done that there are no places where you block without knowing it, only places where you block but for a very explicit reason (and sometimes that reason can be something as simple as "there is no non-blocking module that does what I need and I can't write one yet")

And part of designing your app top to bottom to use non-blocking is to understand that different modules you use will or will not block. Often easily evidenced by the fact that the Module's API expects a callback or not.

For example,

$dbh->do("SELECT * FROM table");

will block for as long as do() takes to send the query to the database and to get the response back from the database. There's no reason that code execution would be able to continue beyond the do() until do() is done or else why run the do() in the first place! The results would be inaccessible.

As a more full example, this is a simple app that blocks as the connect() method of Socket.pm blocks trying to access a hostname that is unreachable.

use Mojolicious::Lite;
use Socket;

get '/' => {text => 'Hello, World!'};
get '/connect' => sub {
  my $self = shift;

  my $ip = '192.168.0.52';
  my $port = '6335'; 

  socket(SOCKET, PF_INET, SOCK_STREAM, getprotobyname('tcp'));

  $self->app->log->info("Connecting to $ip:$port");
  if (!connect(SOCKET, sockaddr_in($port, inet_aton($ip)))) {  # Blocks for 3 seconds
    $self->render(text => 'KO');
  } else {
    $self->render(text => 'OK');
    close SOCKET || die "close: $!";
  }
};

app->start;

The process handling the HTTP request for access to the /connect route will block for 3 seconds. This process can't do anything else. So resolution #1 is to give your preforking Mojo web server more processes to handle the load and make sure that each handles only one concurrent connection and resolution #2 is to rewrite this code using methods that don't block.

So with connect() as above ... there's no reason to continue on to the next code line when connect() isn't done because the next line might be to read from the socket but it hasn't even connected yet!

Of course, you can't magically make connect() have a callback and call it like connect(cb => sub { ... }) but you could choose a different module or write your own implementation. In the case of connect() just use Mojo::IOLoop->client instead. Of course, it's not intended to be a drop-in replacement so some recoding will be necessary beyond changing Socket::connect() to Mojo::IOLoop->client()

use Mojolicious::Lite;

get '/' => {text => 'Hello, World!'};
get '/connect' => sub {
  my $self = shift;
  $self->render_later;

  my $ip = '192.168.0.52';
  my $port = '6335'; 

  $self->app->log->info("Connecting to $ip:$port");
  Mojo::IOLoop->client({ port => $port, address => $ip } => sub {
    # It may take 3 seconds to get here...
    my ($loop, $error, $stream) = (@_);
    
    if(defined($error)) {
      $self->render(text => 'KO');
    } else {
      $self->render(text => 'OK');
    }
  });
  # ... but this process can still go on and do what it needs to do
  # In this case, returning so that it can handle more requests
};

app->start;