-
Notifications
You must be signed in to change notification settings - Fork 582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a 'worker_starting' event. #1755
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm leaning towards thinking this might be a good addition since it is a question that comes up again and again. I don't like the event name though.
My review is indeed early, so I will come back to this PR when it has matured more.
lib/Mojo/Server/Daemon.pm
Outdated
@@ -35,6 +35,7 @@ sub run { | |||
my $loop = $self->ioloop; | |||
my $int = $loop->recurring(1 => sub { }); | |||
local $SIG{INT} = local $SIG{TERM} = sub { $loop->stop }; | |||
$self->app->worker_starting; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this "before_worker_start" ? And I wonder if we need a method for this...
@kraih: Is it bad to emit events from outside the app? As in $self->app->plugins->emit_hook(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could rename the event as "before_worker_start" to be consistent with the other hooks, indeed!
I like it better myself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jhthorsen Yes, that's bad encapsulation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jhthorsen @kraih I am sorry, I do not follow your logic.
Why is it bad encapsulation to send advertised events to an object so that it can react appropriately if it wants? How is this violating encapsulation at all? Calling $app->app_starting
is no more violating encapsulation than calling $app->server
is. Or am I totally misunderstanding what you are talking about here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now you're crossing two layers instead of just one. The server knowing about a specific set of methods it can call on the app instance it manages is very different from it digging into very specific app properties that would normally only be reached via delegation by the app. It makes implementing new servers much much harder. Just because it is public doesn't make it part of the "server <-> app" protocol.
Done with all the changes following the initial review. As of now, the hook is called "before_worker_start". However, I'm offering a new suggestion: "before_app_start". Indeed, this is not a hook just for pre-forked servers, it is a generic hook that applications can use to initialize global state before they start processing requests. It just happens to also "do the right thing" in pre-forked environments. |
Normally i'd be against this, since |
I believe this hook should really be named "before_app_start". And I still need to write tests in |
I like this name better. My only comment is just thinking about how this PR will ensure that the hook will get called for each worker start, even those [re]starting later; and my very possibly flawed reaction to Also, a possible contradiction: is it really |
Good point. But Yeah, naming is really hard. |
The name should reference that it is run on the start of each worker process, not the application. before_app_start would be misleading for both these reasons. |
I have made also patches to make sure the new hook is run before every possible application start I could see, so that behaviour can be controlled in a standard way: regardless of whether you are going to use pre-forking, or run in a single daemon, or via |
But it is for every worker, which makes it different from "before application start", which only occurs once. |
That's the idea. I can see that it works for my application, which already relies on the new hook here: the code gets triggered every time a new child is starting. My logging vouches for it 😄. The trick is... how to write good tests for it. That's why I haven't touched |
Yes, it is for every worker. That's why I had named it |
my $app = $self->app; | ||
$app->worker_starting if ref $app; | ||
return $self; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now inconsistent with before_server_start
. Also a blocker for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is it inconsistent exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mojo::Server::CGI
is excluded from deployment hooks like before_server_start
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this is also a deployment hook you are working on. It does not get emitted for commands like ./myapp.pl eval -V 'app->mode'
after all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kraih you are right, something is not working as expected yet. I only discovered Mojo 2 days ago, hence I apologize for being a slow learner.
I really wish to have a global hook that triggers possibly costly initialization for the application, only once and regardless of how the application is actually started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And that makes it inconsistent with before_server_start
. Which does not run for ./myapp.pl cgi
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a blocker too. We might have to consider adding the before_server_start
hook to cgi first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you try to link the fate of before_server_start
with that of before_app_start
? They have different semantics now.
We could add before_server_start
to CGI, but that would be another PR, because, for me, the two events have different semantics, even if, at first, when I started toying with this idea, I emitted one close to the other...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, i think before_server_start
should be added to Mojo::Server::CGI
after all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And done. 81a2753...8e1611e
d50deba
to
4f0b5e4
Compare
4f0b5e4
to
b70e98e
Compare
This event is always called before the application starts, regardless of whether it is invoked via morbo, is started in daemon mode from the command line, via hypnotoad in pre-forking mode, as a CGI application or as a PSGI. In pre-forked children, it avoids having to say: Mojo::IOLoop->next_tick(sub ($ioloop) { app->log->info("Worker $$ star...ALL GLORY TO THE HYPNOTOAD!"); }); to get this special behaviour. It is way nicer to be able to say: app->hook(before_app_start => sub ($app) { $app->log->info("App in $$ star...ALL GLORY TO MOJOLICIOUS!"); }); to get that behaviour. It is more consistent with the over hooks, and also it is a better abstraction than having to know the internals of the IOLoop to be able to get such a hooking point in pre-forked processes. Now the application has a unique and consistent entry point where it can initialize its global state, regardless of how it is being launched.
b70e98e
to
aadad52
Compare
This is the standard width for that project.
So, at this stage, I believe I have answered all the review comments so far. I'm letting you review the current changes now, awaiting more feedback. Note that the semantics of the This means we do not need to evaluate code that could lie in |
@@ -35,6 +35,7 @@ sub run { | |||
my $loop = $self->ioloop; | |||
my $int = $loop->recurring(1 => sub { }); | |||
local $SIG{INT} = local $SIG{TERM} = sub { $loop->stop }; | |||
$self->app->app_starting; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should trigger twice now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It did, hence the PID protection in Mojolicious
.
If I remove this line, then morbo ./myapp.pl
is not triggering the event!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that's a different problem though. This is a newly designed feature, so it really shouldn't use workarounds like that.
# Ensure we run 'before_app_start' only once per process | ||
$self->{_started} //= {}; | ||
return if exists $self->{_started}->{$$}; | ||
$self->{_started}->{$$} = undef; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be completely unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not. See above comment... If I do not do this, then there is a duplicate invocation (as you guessed) with having run() and start() both invoke the event...
Yet if both do not, then the event is not always emitted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a workaround, not a solution for the actual problem (which is that morbo initialises the listener in the parent process before the application is ever loaded).
$counter_server->{$pid}++; | ||
$c->rendered(201); | ||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Way too overcomplicated for a simple test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do the absolute minimum necessary to test the feature.
|
||
is $foreign, 0, 'no before_app_start ran in foreign processes'; | ||
|
||
kill 'TERM', $dpid; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a mess. Anything that risks making our fork tests less reliable is a blocker i'm afraid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More constructively: Please make a self contained subtest (all new tests are required to use subtests), and make sure that it waits until the forked process is stopped safely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not familiar with the subtest
thing. t/mojo/prefork.t
did not use these.
Are you suggesting that I put all the preforking tests into a single subtest?
With my little webapp, I can collect statistics about all the tests done and make sure everything is working as expected: that callbacks are invoked the expected amount of time and in the right processes. Since we have many concurrent processes, I thought it would be nicer to cleanly abstract the statistics collection into a single tracking service that every sub-process knows how to reach and which has a simple REST API.
I can add a waitpid()
call at the end to make sure we cleanup properly, but I have added a failsafe in the webapp by instantiating a callback every 0.5 second to check whether the child has been re-parented to init
(sorry, one should say systemd
nowadays). Did I do that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please leave the existing tests alone, and treat everything new as a separate new subtest. We will convert the rest of prefork.t
to subtests in time. Make the test app no more complicated than absolutely necessary. Every line that looks unnecessary will be questioned and might end up as a blocker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion is an ongoing process independent of this PR. #1520
t/mojo/daemon.t
Outdated
my $whatever = 'Failed!'; | ||
$app->hook( | ||
before_app_start => sub { | ||
my ($app) = shift; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use shift in list context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. I meant ($app) = @_;
but as your earlier comment indicates, you use $app = shift
when there is just one argument expected.
before_app_start => sub { | ||
my ($app) = shift; | ||
my $ok = ref $app && $app->isa('Mojolicious'); | ||
$whatever = $ok ? 'Whatever!' : 'Wrong type'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Way too complicated again. Why have special case that is now untested?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean "untested"? This is tested indeed... if I change the 'Whatever!'
string, the tests are failing!
Or did you mean something I don't get?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Wrong type
variant appears untested and rather pointless. You could just ref the app and use that as test value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or better yet, call a method in $app
and use that, like ->moniker
or so.
t/mojo/prefork.t
Outdated
); | ||
$prefork->app->hook( | ||
before_app_start => sub { | ||
my ($app) = @_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't parameters always passed as a LIST in Perl? Even if there is a single one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but we use the convention my $app = shift
for single arguments. It became an idiom because it used to be a little faster with some versions of Perl (i don't actually remember the details, ask on IRC if you want to know more).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I remember that discussion on p5p at the time. It was a loooong time ago. In my own code, I just don't care and prefer to say ($x) = @_;
even if there is just one argument because I favor readability over hypothetical performance. My only exception is for my $self = shift;
which I always use on a line by itself, even if there are following arguments.
But Mojolicious
is not my code and I will follow your standards by amending this code once again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it was up to me alone we would just be using signatures everywhere. 😉
$self->{_started} //= {}; | ||
return if exists $self->{_started}->{$$}; | ||
$self->{_started}->{$$} = undef; | ||
$self->plugins->emit_hook(before_app_start => $self); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the new name misleading. It implies that it runs before app->start
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree here. In line with other comment from @s1037989 yesterday.
Naming is hard. What would you choose? before_app_serving
? Or perhaps drop the before_
and say start
? Or init
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after_server_start
, consistent with before_server_start
? 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but that would mean it has comparable semantics to some "server starting" event and... remember... there is no such thing for CGI yet by your own admission... 😀
That being said, I have absolutely no stake in this naming. I just want to have this new functionality in standard Mojolicious and not just in my own forked framework. Forking Open-Source projects is always... bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that Mojolicious gets forked so much that we had to adopt an official policy for it. https://docs.mojolicious.org/Mojolicious/Guides/Contributing#FORK-POLICY
66b4c97
to
2058775
Compare
This pull request is now in conflicts. Could you fix it @rmanfredi? 🙏 |
I've been thinking about this for a bit, and there are more edge cases that have not been considered yet. The best solution i can think of would be something like: diff --git a/lib/Mojo/Server/Daemon.pm b/lib/Mojo/Server/Daemon.pm
index f85982841..98ddcee24 100644
--- a/lib/Mojo/Server/Daemon.pm
+++ b/lib/Mojo/Server/Daemon.pm
@@ -54,6 +54,7 @@ sub start {
elsif (!@{$self->acceptors}) {
$self->app->server($self);
$self->_listen($_) for @{$self->listen};
+ $self->ioloop->next_tick(sub { $self->app->server($self, {after_start => 1}) });
}
return $self; |
PR seems abandoned. Given our growing review backlog, i'm closing this to make room for other PRs. |
This event (
before_app_start
) is always called before the application starts, regardlessof whether it is invoked via
morbo
, is started in daemon mode from thecommand line or via
hypnotoad
in pre-forking mode.It avoids having to say:
to get this special behaviour. It is way nicer to be able to say:
to get that behaviour.
It is more consistent with the over hooks, and also it is a better
abstraction than having to know the internals of the IOLoop to be
able to get such a hooking point in pre-forked processes.
Now the application has a unique and consistent entry point where it can
initialize its global state.