
Introduction to web application development on PSGI / Plack. Part 4. Asynchrony
With the permission of the author and editor-in-chief of PragmaticPerl.com, I am publishing this article.
The original article can be read here .
Continuation of the series of articles on the development of PSGI / Plack. We deal with asynchrony.
In previous articles, we examined the main aspects of development for PSGI / Plack, which, in principle, are sufficient for developing applications of almost any complexity.
We figured out what PSGI is, figured out how Plack works, then we figured out how the basic components of Plack are arranged (Plack :: Builder, Plack :: Request, Plack :: Middleware). Then we looked at Starman, which is a good PSGI server, ready for use in production.
Everything that was considered earlier was related to the development of a runtime model called synchronous. Now consider the asynchronous model.
The synchronous model is simple and straightforward. Everything happens one after another in a certain order. This is called the execution process. Consider one interpreter process that, say, runs a loop, one of the elements of which is the input of user information. The next iteration of the loop will not be completed until the previous one is completed, which includes waiting for the user to enter data. This is a synchronous model.
While the user does not enter anything, the program expects input and does nothing useful. This situation is called blocking the execution process. In this case, a simple program simply utilizes processor time. But if in the process of waiting for the user the program does something else, waiting for input, then the process becomes asynchronous, and the situation, accordingly, becomes non-blocking.
Consider an example bar. A simple bar or pub where customers sit and drink beer. There are many customers. The bar has two waiters - Bob and Joe. They work in two different ways. Bob approaches the customers, takes the order, goes to the bar, orders the bartender a glass of beer, waits for the bartender to pour the glass, takes it to the client, the situation repeats. Bob works synchronously. Joe does something completely different. He takes the order from the client, goes to the bartender, tells him: “Hey, pour a glass of% beername%,” then he goes to take the order from the next client. As soon as the bartender pours the glass, he calls Joe, who picks up the glass and takes it to the client.
In this case, Bob works synchronously, and Joe, respectively, asynchronously. Joe's model of work is event-driven. This is the most popular model of asynchronous systems. In our case, waiting for input is the time required to fill the glass with beer, the event manager is the bartender, and the event is the bartender’s cry “% beername% poured”.
Now, readers who have never worked with asynchronous systems should have a question. “But why, in fact, do synchronous things if asynchrony is faster and more convenient?”.
This is a very popular misconception, but it is not. Asynchronous solutions also have a number of problems and disadvantages. There are many places where you can read that asynchronous solutions are more productive than synchronous ones. Yes and no.
Back to the waiters. Bob works leisurely, tells jokes to the bartender, measures glasses, and Joe constantly wobbles like crazy. The load on Joe, of course, is higher, because he does much more at the same time. Bob’s load is minimal, as long as there are no customers. As soon as there are a lot of customers, they start loudly demanding their beer and rushing Bob. The load on the client’s side is increasing, but Bob continues to work at the same pace, he doesn’t care, he’s not going to give up his scheme of work, and even let the sky collapse.
So, from here we can conclude that asynchrony is not bad, but it should be understood that the asynchronous system will be constantly under load. The load, in principle, will be the same as for the synchronous system, but with one difference. The synchronous system is subject to peak loads, and the asynchronous system “spreads” these loads according to execution time.
Well and most importantly, we must not forget that any system can perform as many tasks at once as many processor cores are available to the process.
Classic Plack application (skip the builder section):
It can be seen from the code that the $ app scalar contains a link to a function that returns a valid PSGI response (an array reference). So this is a reference to a function that returns a reference to an array. Here you can add asynchrony, but this will not work out, because the executable process will be blocked.
A PSGI application, which is a reference to a function that returns a reference to an array, must be executed to the end, and only then release the execution thread.
Naturally, this code will work correctly on any PSGI server, as it is synchronous. Any asynchronous server can execute synchronous code, but a synchronous server cannot execute asynchronous code. The code above is synchronous. In a previous article, we touched a bit on a PSGI server like Twiggy. I recommend installing it if you do not already have it. There are several ways to do this. Using cpan (cpan install Twiggy), using cpanm (cpanm Twiggy), or use github.
Twiggy is an asynchronous server. The author of Twiggy and Starman is the same - @miyagawa.
About Twiggy @miyagawa says the following: “PSGI / Plack is an HTTP server based on AnyEvent.”
Twiggy is a supermodel from the 60s, which, as many people think, marked the beginning of the “thin” fashion. the server is very “light”, “thin”, “small”, the name was not chosen by chance.
The deferred response PSGI application is presented in the documentation as follows:
We’ll figure out how it works in order to understand how to use it further and write our own application that works with deferred response.
An application is a reference to a function that returns a function that will be executed after some conditions (callback) are met. As a result, the application is a function reference that returns a function reference. That's all there is to understand. The server, if the PSGI environment variable “psgi.streaming” is set, will try to perform this operation in non-blocking mode, i.e. asynchronously.
So how does it work?
If you run such an application on Starman, then there will be no difference, but if we use a delayed response on an asynchronous server, the execution process will look like this.
If the model were synchronous, then the server would not be able to accept a single request until the previous one worked.
We will write the application using the deferred response mechanism. The application will look like this:
Now launch the application using both Starman and Twiggy.
The command to launch using Starman does not change with us and looks like this:
To run using Twiggy:
Now we will make a request first to one server, then to another.
Request to Starman:
Request to Twiggy:
So far, there are no differences, and the servers work out the same way.
Now let's do a simple experiment with Twiggy and Starman. Imagine that we need to write an application that will do something at the request of the client, and after the completion of the operation, report on the work done. But, because we don’t need to keep the client, we will use to simulate the execution of anything AnyEvent-> timer () for Twiggy, sleep 5 for Starman. In general, sleep is not the best option here, but we do not have another, because code with AnyEvent in Starman will not work.
So, we are implementing two options.
Blocking:
No matter how we run it, at least with the help of Starman, at least with the help of Twiggy, the result will always be the same. Run it, for starters, using Starman with the following command:
Note: for the purity of the experiment, you must use Starman with one workflow.
Turning to the server from different terminals at the same time, we can see how this application is executed. First, the worker will take the first request and begin to execute it. At this point, the second request will be queued. As soon as the first request is fully completed, the server will begin to process the next request.
In total, two requests will be executed for approximately 10 seconds (the second is launched for processing only after the first). If the request is 3, then the approximate execution time will be 18 seconds. This situation is called blocking.
If you run the previous example using Twiggy, the result will be the same for sure. Now the question may arise why an asynchronous server is needed if it is blocked and Starman works the same way.
The fact is that in order for something to work asynchronously, you need a mechanism that will provide asynchrony, an event loop, for example.
Twiggy is built around an AnyEvent mechanism that starts when the server starts. We can use it immediately after the server starts. It is possible to use Coro, an article on which will also be mandatory.
Now we ’ll write a code that will not work with Starman and get a ready-made asynchronous application.
Let's tidy up the code and make the application asynchronous. As a result, we should get something like the following:
It is worth recalling that there will always be locks , where they will depend on the code writing. The less time the server will be locked, the better.
How it works?
The timer starts first. The main point is that in return sub {...} you must assign an observer object (AnyEvent-> timer (...)) to the variable that was declared before return sub {...}, or use condvar. Otherwise, the timer will never be executed, because AnyEvent considers that the function is complete and does not need to do anything. After the timer expires, an event occurs, the function is executed, and the server returns the result. If three requests are made from different terminals, for example, then they will all be executed asynchronously, and a response will be returned when the timer event fires. But the most important thing here is that blocking does not occur. This is evidenced by the result of three queries made from different terminals, the output of STDERR:
The server was launched by the following command:
And the queries were executed using curl:
Recall that the preforking server in the classic form is synchronous. The simultaneity of requests is processed using a certain number of workers. Those. if you run the previous synchronous code:
with several workers, it turns out that two requests will be executed simultaneously. But this is not a matter of asynchrony, but the fact that each request is processed by its own workflow. This is how Starman, the preforking PSGI server, works.
Take an asynchronous example:
Start by the following command:
and repeat the experiment with two simultaneous queries.
Indeed, Twiggy works as a single process, but nothing prevents it from performing other useful actions while waiting. This is asynchrony.
This example was used solely to demonstrate how a delayed response can be used. For a better understanding of Twiggy’s operating principles, it is recommended that you read the articles on AnyEvent in previous issues of the magazine (“Everything You Wanted to Know About AnyEvent, but Were Afraid to Ask” and “AnyEvent and fork”).
At the moment, there are a fairly large number of PSGI servers that support event loops. Namely:
Any technology has its own nuances. It is necessary to decide which approach to use based on the data for each specific task, but not to use the asynchronous approach everywhere, because it is fashionable.
Dmitry Shamatrin
The original article can be read here .
Continuation of the series of articles on the development of PSGI / Plack. We deal with asynchrony.
In previous articles, we examined the main aspects of development for PSGI / Plack, which, in principle, are sufficient for developing applications of almost any complexity.
We figured out what PSGI is, figured out how Plack works, then we figured out how the basic components of Plack are arranged (Plack :: Builder, Plack :: Request, Plack :: Middleware). Then we looked at Starman, which is a good PSGI server, ready for use in production.
Nuance
Everything that was considered earlier was related to the development of a runtime model called synchronous. Now consider the asynchronous model.
Synchronism and asynchrony
The synchronous model is simple and straightforward. Everything happens one after another in a certain order. This is called the execution process. Consider one interpreter process that, say, runs a loop, one of the elements of which is the input of user information. The next iteration of the loop will not be completed until the previous one is completed, which includes waiting for the user to enter data. This is a synchronous model.
While the user does not enter anything, the program expects input and does nothing useful. This situation is called blocking the execution process. In this case, a simple program simply utilizes processor time. But if in the process of waiting for the user the program does something else, waiting for input, then the process becomes asynchronous, and the situation, accordingly, becomes non-blocking.
Go to the bar
Consider an example bar. A simple bar or pub where customers sit and drink beer. There are many customers. The bar has two waiters - Bob and Joe. They work in two different ways. Bob approaches the customers, takes the order, goes to the bar, orders the bartender a glass of beer, waits for the bartender to pour the glass, takes it to the client, the situation repeats. Bob works synchronously. Joe does something completely different. He takes the order from the client, goes to the bartender, tells him: “Hey, pour a glass of% beername%,” then he goes to take the order from the next client. As soon as the bartender pours the glass, he calls Joe, who picks up the glass and takes it to the client.
In this case, Bob works synchronously, and Joe, respectively, asynchronously. Joe's model of work is event-driven. This is the most popular model of asynchronous systems. In our case, waiting for input is the time required to fill the glass with beer, the event manager is the bartender, and the event is the bartender’s cry “% beername% poured”.
Problem
Now, readers who have never worked with asynchronous systems should have a question. “But why, in fact, do synchronous things if asynchrony is faster and more convenient?”.
This is a very popular misconception, but it is not. Asynchronous solutions also have a number of problems and disadvantages. There are many places where you can read that asynchronous solutions are more productive than synchronous ones. Yes and no.
Back to the waiters. Bob works leisurely, tells jokes to the bartender, measures glasses, and Joe constantly wobbles like crazy. The load on Joe, of course, is higher, because he does much more at the same time. Bob’s load is minimal, as long as there are no customers. As soon as there are a lot of customers, they start loudly demanding their beer and rushing Bob. The load on the client’s side is increasing, but Bob continues to work at the same pace, he doesn’t care, he’s not going to give up his scheme of work, and even let the sky collapse.
So, from here we can conclude that asynchrony is not bad, but it should be understood that the asynchronous system will be constantly under load. The load, in principle, will be the same as for the synchronous system, but with one difference. The synchronous system is subject to peak loads, and the asynchronous system “spreads” these loads according to execution time.
Well and most importantly, we must not forget that any system can perform as many tasks at once as many processor cores are available to the process.
Asynchronous PSGI / Plack
Classic Plack application (skip the builder section):
my $app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = $req->new_response(200);
$res->body('body');
return $res->finalize();
};
It can be seen from the code that the $ app scalar contains a link to a function that returns a valid PSGI response (an array reference). So this is a reference to a function that returns a reference to an array. Here you can add asynchrony, but this will not work out, because the executable process will be blocked.
A PSGI application, which is a reference to a function that returns a reference to an array, must be executed to the end, and only then release the execution thread.
Naturally, this code will work correctly on any PSGI server, as it is synchronous. Any asynchronous server can execute synchronous code, but a synchronous server cannot execute asynchronous code. The code above is synchronous. In a previous article, we touched a bit on a PSGI server like Twiggy. I recommend installing it if you do not already have it. There are several ways to do this. Using cpan (cpan install Twiggy), using cpanm (cpanm Twiggy), or use github.
Twiggy
Twiggy is an asynchronous server. The author of Twiggy and Starman is the same - @miyagawa.
About Twiggy @miyagawa says the following: “PSGI / Plack is an HTTP server based on AnyEvent.”
Twiggy is a supermodel from the 60s, which, as many people think, marked the beginning of the “thin” fashion. the server is very “light”, “thin”, “small”, the name was not chosen by chance.
Deferred response
The deferred response PSGI application is presented in the documentation as follows:
my $app = sub {
my $env = shift;
return sub {
my $responder = shift;
fetch_content_from_server(sub {
my $content = shift;
$responder->([ 200, $headers, [ $content ] ]);
});
};
};
We’ll figure out how it works in order to understand how to use it further and write our own application that works with deferred response.
An application is a reference to a function that returns a function that will be executed after some conditions (callback) are met. As a result, the application is a function reference that returns a function reference. That's all there is to understand. The server, if the PSGI environment variable “psgi.streaming” is set, will try to perform this operation in non-blocking mode, i.e. asynchronously.
So how does it work?
If you run such an application on Starman, then there will be no difference, but if we use a delayed response on an asynchronous server, the execution process will look like this.
- The server receives the request.
- The server requests data from somewhere, where it comes from for a long time (function fetch_content_from_server).
- Then, while waiting for a response, it can accept more requests.
If the model were synchronous, then the server would not be able to accept a single request until the previous one worked.
We will write the application using the deferred response mechanism. The application will look like this:
use strict;
use Plack;
my $app = sub {
my $env = shift;
return sub {
my $responder = shift;
my $body = "ok\n";
$responder->([ 200, [], [ $body ] ]);
}
}
Now launch the application using both Starman and Twiggy.
The command to launch using Starman does not change with us and looks like this:
starman --port 8080 app.psgi
To run using Twiggy:
twiggy --port 8081 app.psgi
Now we will make a request first to one server, then to another.
Request to Starman:
curl localhost:8080/
ok
Request to Twiggy:
curl localhost:8081/
ok
So far, there are no differences, and the servers work out the same way.
Now let's do a simple experiment with Twiggy and Starman. Imagine that we need to write an application that will do something at the request of the client, and after the completion of the operation, report on the work done. But, because we don’t need to keep the client, we will use to simulate the execution of anything AnyEvent-> timer () for Twiggy, sleep 5 for Starman. In general, sleep is not the best option here, but we do not have another, because code with AnyEvent in Starman will not work.
So, we are implementing two options.
Blocking:
use strict;
sub {
my $env = shift;
return sub {
my $responder = shift;
sleep 5;
warn 'Hi';
$responder->([ 200, [ 'Content-Type' => 'text/json'], [ 'Hi' ] ]);
}
}
No matter how we run it, at least with the help of Starman, at least with the help of Twiggy, the result will always be the same. Run it, for starters, using Starman with the following command:
starman --port 8080 --workers=1 app.psgi
Note: for the purity of the experiment, you must use Starman with one workflow.
Turning to the server from different terminals at the same time, we can see how this application is executed. First, the worker will take the first request and begin to execute it. At this point, the second request will be queued. As soon as the first request is fully completed, the server will begin to process the next request.
In total, two requests will be executed for approximately 10 seconds (the second is launched for processing only after the first). If the request is 3, then the approximate execution time will be 18 seconds. This situation is called blocking.
Asynchronous code
If you run the previous example using Twiggy, the result will be the same for sure. Now the question may arise why an asynchronous server is needed if it is blocked and Starman works the same way.
The fact is that in order for something to work asynchronously, you need a mechanism that will provide asynchrony, an event loop, for example.
Twiggy is built around an AnyEvent mechanism that starts when the server starts. We can use it immediately after the server starts. It is possible to use Coro, an article on which will also be mandatory.
Now we ’ll write a code that will not work with Starman and get a ready-made asynchronous application.
Let's tidy up the code and make the application asynchronous. As a result, we should get something like the following:
sub {
my $env = shift;
return sub {
my $respond = shift;
$env->{timer} = AnyEvent->timer(
after => 5,
cb => sub {
warn 'Hi' . time() . "\n";
$respond->([200, [], ['Hi' . time() . "\n"]]);
}
);
}
}
It is worth recalling that there will always be locks , where they will depend on the code writing. The less time the server will be locked, the better.
How it works?
The timer starts first. The main point is that in return sub {...} you must assign an observer object (AnyEvent-> timer (...)) to the variable that was declared before return sub {...}, or use condvar. Otherwise, the timer will never be executed, because AnyEvent considers that the function is complete and does not need to do anything. After the timer expires, an event occurs, the function is executed, and the server returns the result. If three requests are made from different terminals, for example, then they will all be executed asynchronously, and a response will be returned when the timer event fires. But the most important thing here is that blocking does not occur. This is evidenced by the result of three queries made from different terminals, the output of STDERR:
twiggy --port 8080 app.psgi
Hi1372613810
Hi1372613811
Hi1372613812
The server was launched by the following command:
twiggy --port 8080 app.psgi
And the queries were executed using curl:
curl localhost:8080
Recall that the preforking server in the classic form is synchronous. The simultaneity of requests is processed using a certain number of workers. Those. if you run the previous synchronous code:
use strict;
sub {
my $env = shift;
return sub {
my $responder = shift;
sleep 5;
warn 'Hi';
$responder->([ 200, [ 'Content-Type' => 'text/json'], [ 'Hi' ] ]);
}
}
with several workers, it turns out that two requests will be executed simultaneously. But this is not a matter of asynchrony, but the fact that each request is processed by its own workflow. This is how Starman, the preforking PSGI server, works.
Take an asynchronous example:
sub {
my $env = shift;
return sub {
my $respond = shift;
$env->{timer} = AnyEvent->timer(
after => 5,
cb => sub {
warn 'Hi' . time() . "\n";
$respond->([200, [], ['Hi' . time() . "\n"]]);
}
);
}
}
Start by the following command:
twiggy --port 8080 app.psgi
and repeat the experiment with two simultaneous queries.
Indeed, Twiggy works as a single process, but nothing prevents it from performing other useful actions while waiting. This is asynchrony.
This example was used solely to demonstrate how a delayed response can be used. For a better understanding of Twiggy’s operating principles, it is recommended that you read the articles on AnyEvent in previous issues of the magazine (“Everything You Wanted to Know About AnyEvent, but Were Afraid to Ask” and “AnyEvent and fork”).
At the moment, there are a fairly large number of PSGI servers that support event loops. Namely:
- Feersum is an asynchronous XS server with unrealistic performance, based on EV.
- Twiggy is an asynchronous server based on AnyEvent.
- Twiggy :: TLS is the same Twiggy, but with ssl support.
- Twiggy :: Prefork is the same Twiggy, but with workers.
- Monoceros - a young server, hybrid, has both synchronous and asynchronous parts.
- Corona is an asynchronous server based on Coro.
conclusions
Any technology has its own nuances. It is necessary to decide which approach to use based on the data for each specific task, but not to use the asynchronous approach everywhere, because it is fashionable.
Dmitry Shamatrin