Back to Home

Introduction to web application development on PSGI / Plack. Part 3. Starman

perl · web development and programming

Introduction to web application development on PSGI / Plack. Part 3. Starman

  • Tutorial
Continuation of the series of articles on PSGI / Plack. A preforking Starman PSGI server is discussed in more detail.

With the permission of the author of the article and the editor-in-chief of the journal PragmaticPerl.com . The original article is located here.

Starman?


The author of this server (Tatsuhiko Miyagawa) says the following about him:

“The name Starman is taken from the song Star HA Otoko of the Japanese rock band Unicorn (Yes, Unicorn). David Bowie also has a song of the same name, Starman - the name of the character of the cult Japanese game Earthbound, the name of a musical theme from Super Mario Brothers.

I'm tired of naming Perl modules like HTTP :: Server :: PSGI :: How :: Its :: Written :: With :: What :: Module, and as a result, people call it HSPHIWWWM in IRC. This is poorly pronounced and creates problems for beginners. Yes, maybe I'm an oversight. Time will tell. ”

They sorted out the name. Now we will deal with the server itself.


Preforking?


Starman’s preforking model is similar to the most powerful Unix servers. It uses a model of pre-running processes. He also automatically restarts the pool of workers and removes his zombie processes.

Plack application


This time the Plack application will be completely elementary:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    mount "/" => builder { $app };
};

When developing under Starman, you need to understand one very important point of its work. Consider, for example, a database connection. Very often, in order to save time and lines of code, connection initialization is taken to the very beginning of the script. This applies to CGI and sometimes FastCGI. This is not the case with PSGI. And that's why. When the server starts, this code will be executed exactly once for each worker. And the danger of the situation is that at first, until the connection crashes either by timeout, or for some other reason, the application will work as usual. In the case of asynchronous servers, at the beginning of the application code, you can initialize the connection pool (connection! = Connection pool).

In order to confirm or refute this, we will make changes to the application code. Add the following line to the beginning of the code, after imports:

warn 'AFTER IMPORT';


Now the application should look like:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
warn 'AFTER IMPORT';
sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    mount "/" => builder { $app };
};

For the purity of the experiment, we will run starman with one worker with the following command:

starman --port 8080 --workers 1 app.psgi


Where app.psgi is the application.

Immediately after the launch is complete, we see the following picture in STDERR:

noxx@noxx-inferno ~/perl/psgi $ starman --port 8080 app.psgi --workers 1
2013/06/02-15:05:31 Starman::Server (type Net::Server::PreFork) starting! pid(4204)
Resolved [*]:8080 to [::]:8080, IPv6
Not including resolved host [0.0.0.0] IPv4 because it will be handled by [::] IPv6
Binding to TCP port 8080 on host :: with IPv6
Setting gid to "1000 1000 4 24 27 30 46 107 125 1000 1001"
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.

If you send a request to localhost: 8080 /, you can make sure that nothing new has appeared in STDERR, and the server is responding normally.
In order to make sure that worker is really one, run the following command:

ps uax | grep starman


Result:

noxx      4204  0.6  0.1  57836 11264 pts/3    S+   15:05   0:00 starman master --port 8080 app.psgi --workers 1
noxx      4205  0.2  0.1  64708 13164 pts/3    S+   15:05   0:00 starman worker --port 8080 app.psgi --workers 1
noxx      4213  0.0  0.0  13580   940 pts/4    S+   15:05   0:00 grep --colour=auto starman

There are two processes. But in fact, the worker is only one of them. Let's do another experiment. Run starman with three workers.

starman --port 8080 --workers 3 app.psgi


Result:

2013/06/02-15:11:08 Starman::Server (type Net::Server::PreFork) starting! pid(4219)
Resolved [*]:8080 to [::]:8080, IPv6
Not including resolved host [0.0.0.0] IPv4 because it will be handled by [::] IPv6
Binding to TCP port 8080 on host :: with IPv6
Setting gid to "1000 1000 4 24 27 30 46 107 125 1000 1001"
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.


All right. Now let's look at the list of processes. It looks like this for me:

noxx      4219  0.1  0.1  57836 11264 pts/3    S+   15:11   0:00 starman master --port 8080 app.psgi --workers 3
noxx      4220  0.0  0.1  64460 12756 pts/3    S+   15:11   0:00 starman worker --port 8080 app.psgi --workers 3
noxx      4221  0.0  0.1  64460 12920 pts/3    S+   15:11   0:00 starman worker --port 8080 app.psgi --workers 3
noxx      4222  0.0  0.1  64460 12756 pts/3    S+   15:11   0:00 starman worker --port 8080 app.psgi --workers 3
noxx      4224  0.0  0.0  13580   936 pts/4    S+   15:12   0:00 grep --colour=auto starman


One master, three workers.

We figured out the execution order. Now add another warning.

warn 'IN BUILDER'


The application is as follows:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
warn 'AFTER IMPORT';
sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};


For one worker process, the output looks like this (start command: starman --port 8080 --workers 1 app.psgi):

2013/06/02-17:33:27 Starman::Server (type Net::Server::PreFork) starting! pid(4430)
Resolved [*]:8080 to [::]:8080, IPv6
Not including resolved host [0.0.0.0] IPv4 because it will be handled by [::] IPv6
Binding to TCP port 8080 on host :: with IPv6
Setting gid to "1000 1000 4 24 27 30 46 107 125 1000 1001"
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.


If we run the application with three workers, we will see the following picture in STDERR:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 23.


By making a request to localhost: 8080 /, you can easily make sure that nothing new has appeared in STDERR.

The following conclusions can be made:

This action will be performed when the application starts. This is true both for the start of the script and for the builder section, if any.
This action will not be performed on server requests.
Starman workflows start sequentially.
This makes it possible to construct heavy objects both at the start of the script and in the builder part.

And now let's add another warning of the following form to the code:

warn 'REQUEST';


And let's bring the application to the following form:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
warn 'AFTER IMPORT';
sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    warn 'REQUEST';
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};


Now run the application with one workflow (starman --port 8080 --workers 1 app.psgi). So far, nothing has changed:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24.


But it’s worth making a request for a new entry to appear in STDERR.

REQUEST at /home/noxx/perl/psgi/app.psgi line 16.


Summarize. With each request to starman, only the code of the application itself will be executed (it is worth remembering return sub ...), but at startup this code will not be executed.

And now, let's say one process has fallen. Add the following line to return sub ...:

die("DIED");


As a result, you should get an application of the following form:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
warn 'AFTER IMPORT';
sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    warn 'REQUEST';
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    die("DIED");
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};

We launch the application with one workflow, make a request. The application naturally crashes. But the result is curious, although logical. The application did not crash, only two notifications appeared in STDERR:

REQUEST at /home/noxx/perl/psgi/app.psgi line 16.
DIED at /home/noxx/perl/psgi/app.psgi line 21.


Now replace die ('DIED'); on exit 1 ;. Run Starman, make a request to localhost: 8080 /. Now the workflow has fallen. This can be seen from STDERR, which will now look like this:

REQUEST at /home/noxx/perl/psgi/app.psgi line 16, <$read> line 7.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 8.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 26, <$read> line 8.


After each request, the workflow will fall, but the master process will raise it.

Leave Starman for a while. Let's try to run this application, for example, under Twiggy. If this server is not installed, then it's time to install it. The package is called Twiggy.

After installing Twiggy, run our application with the following command:
twiggy --port 8080 app.psgi


And we will make a request. All like Starman, except for one feature. The server has fallen.

noxx@noxx-inferno ~/perl/psgi $ twiggy --port 8080 app.psgi
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 26.
REQUEST at /home/noxx/perl/psgi/app.psgi line 16, <> line 5.
noxx@noxx-inferno ~/perl/psgi $


Of course, this is because Twiggy lacks a master process and there is no one to raise a fallen worker. And now from here follows a very important point that must be taken into account. Before restarting the server, you need to make sure that its code is correct and does not contain syntax errors. If you try to launch an application that contains an error using Starman, several events will occur in the following order:

  • Starman will start the master process, check if it can start workflows.
  • Starman will start workflows and pass the application code to execution.
  • Workflows begin to fall, and the master begins to pick them up.
  • The load increases incredibly and in a very short period of time.


Runtime errors are not so critical. Let's remove the drops from the application, bringing it almost to its initial form:

use strict;
use warnings;
use Plack;
use Plack::Builder;
use Plack::Request;
warn 'AFTER IMPORT';
sub body {
    return 'body';
}
sub body2 {
    return shift;
}
my $app = sub {
    warn 'REQUEST';
    my $env = shift;
    my $req = Plack::Request->new($env);
    my $res = $req->new_response(200);
    $res->body(body());
    return $res->finalize();
};
my $main_app = builder {
    warn 'IN BUILDER';
    mount "/" => builder { $app };
};

And try to do the following in this order:
  • We bring the application to its original form.
  • Run it using Starman.
  • We will make a request.
  • Change the application code and save it.
  • Without restarting the application, we will make a request for it again.

Result:

curl localhost:8080/
body

We save the application, change the body function. Let now, for example, it returns nobody. We make a request - the result, if we did not restart the server, is as follows:
curl localhost:8080/
body

But it is worth making a restart, as everything changes:

curl localhost:8080/
nobody


Another important conclusion. In order for the application to be updated, changing the files is not enough. You must restart the server. Or send a special signal to the master process.

Starman and signals


Imagine that we have a large PSGI application that cannot be stopped, because we have pretty heavy libraries that load into memory, say, ten seconds.

We repeat the previous chain of actions, but with one change. Add the sending of signals.

The signal that tells Starman to reread is SIGHUP.

The command to send this signal looks like this:

kill -s SIGHUP [pid]

You can get the pid value with the following command:

ps uax | grep starman | grep master


Command output example:

noxx      6214  0.8  0.1  54852 10288 pts/3    S+   19:17   0:00 starman master --port 8080 --workers 1 app.psgi

pid = 6214. We

check the response request. Replace nobody back with body and run the application.

Result:
curl localhost:8080
body
kill -s SIGHUP 6214
curl localhost:8080
nobody

Meanwhile, in STDERR Starman we can see the following:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24.
REQUEST at /home/noxx/perl/psgi/app.psgi line 16.
Sending children hup signal
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 2.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24, <$read> line 2.
REQUEST at /home/noxx/perl/psgi/app.psgi line 16, <$read> line 2.

Thus, there are two ways to update a PSGI application. Which one to choose depends on the task.

Suppose you need another workflow. It can be added in two ways. Restart the server with the necessary parameter (--workers) or send a signal. The signal to add one workflow is TTIN, and to delete it is TTOU. If we want to completely stop the server safely, we can use the QUIT signal.

So. Launch our application with one workflow:
starman --port 8080 --workers 1


Then we add two processes by running the following command twice:
kill -s TTIN 6214


Starman Process List:

noxx      6214  0.0  0.1  54852 10304 pts/3    S+   19:17   0:00 starman master --port 8080 --workers 1 app.psgi
noxx      6221  0.0  0.1  64724 13188 pts/3    S+   19:19   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6233  0.0  0.1  64476 12872 pts/3    S+   19:26   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6239  2.0  0.1  64480 12872 pts/3    S+   19:29   0:00 starman worker --port 8080 --workers 1 app.psgi

STDERR is already familiar:

AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 4.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24, <$read> line 4.
AFTER IMPORT at /home/noxx/perl/psgi/app.psgi line 7, <$read> line 4.
IN BUILDER at /home/noxx/perl/psgi/app.psgi line 24, <$read> line 4.


Then we remove one process:

kill -s TTOU 6214


We can see that the team had an effect by looking at the list of processes:
noxx      6214  0.0  0.1  54852 10304 pts/3    S+   19:17   0:00 starman master --port 8080 --workers 1 app.psgi
noxx      6221  0.0  0.1  64724 13188 pts/3    S+   19:19   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6233  0.0  0.1  64476 12872 pts/3    S+   19:26   0:00 starman worker --port 8080 --workers 1 app.psgi
noxx      6238  0.0  0.0  13584   936 pts/4    S+   19:29   0:00 grep --colour=auto starman

But in STDERR this will not be displayed.

And now we will complete the work of our application by sending him a QUIT signal.

kill -s QUIT 6214

The server writes to STDERR:

2013/06/02-19:32:15 Received QUIT. Running a graceful shutdown
Sending children hup signal
2013/06/02-19:32:15 Worker processes cleaned up
2013/06/02-19:32:15 Server closing!


And shuts down.

That's all you need to know about Starman in order to get started.

One more important detail remains. When you start Starman, you can specify the necessary module through the -M switch to boot through the master process. But then the following restriction begins to work. Modules loaded via -M (-MDBI -MDBIx :: Class) will not be re-read during SIGHUP.

Another useful server option is -I. It allows you to specify the path to Perl modules before starting the master process. Starman can also work with Unix sockets, but this feature will be discussed in more detail in the following articles, starting with the article on the deployment and administration of Plack.

And finally, the -E flag, which sets the environment variable (PLACK_ENV) to the passed state.

The next article will be devoted to the asynchronous PSGI server - Twiggy.

Dmitry Shamatrin

Read Next