如何从Mojolicious应用中启动预加载的Mojolicious应用实例?
我负责开发一个规模较大的Mojolicious应用,该应用编译耗时数秒。
该应用的部分测试套件基于Playwright编写,目前每个测试用例都会创建干净的数据库,并通过@mojolicious/server-starter启动一个Mojolicious应用实例。
随着应用编译耗时增加,大幅扩展Playwright测试套件已变得不现实,我希望在不放弃当前独立测试数据库和Mojolicious应用实例所提供的隔离性的前提下解决此问题。
为此,我目前的思路是开发一个小型Perl应用,它可以预加载这个大型Mojolicious应用,并能响应Playwright测试套件的请求,在开放端口上启动一个带有干净数据库的大型应用新实例。
我希望通过HTTP与这个小型Perl应用通信,主要是出于便捷性考虑;同时希望该小型应用使用Mojolicious实现HTTP通信,这既便捷又与现有代码库保持一致。
我尝试过一些简单的实现方案,大致代码如下:
use TheBigApp; $app->routes->post('/spawn-child')->to(cb => sub ($c) { my ($sock, $port) = new_listen_socket(); if (my $pid = fork()) { # record pid to later be able to shut it down or whatever $c->render(json => { url => "http://localhost:$port" }) } else { my $bapp = TheBigApp->new($c->req->json); my $s = Mojo::Server::Daemon->new(listen => ...); $s->app($bapp); $bapp->start; return; } });
但所有此类实现都因IOLoop等单例组件出现问题,即使在子进程中重写IOLoop->singleton以返回带有新反应器的新实例,子进程似乎仍在监听与父进程相同的套接字。
是否有更底层的Mojolicious API可实现此需求?或者不使用Mojolicious实现小型父进程以规避问题会更简单?
方案1:修复Mojolicious父子进程的单例隔离问题
你的核心问题是fork后子进程继承了父进程的Mojo全局单例(比如IOLoop、监听套接字),导致端口冲突或资源竞争。可以通过在子进程中完全重置Mojo的全局状态来解决:
- 子进程中重置IOLoop单例,避免复用父进程的反应器
- 关闭父进程继承的所有监听套接字
- 独立初始化新的应用实例和Daemon服务
修正后的代码示例:
use Mojo::Base 'Mojolicious'; use TheBigApp; use Mojo::IOLoop; use Mojo::Server::Daemon; sub startup ($self) { $self->routes->post('/spawn-child')->to(cb => sub ($c) { # 让系统自动分配空闲端口 my $listen = 'http://*:0'; my $daemon_parent = Mojo::Server::Daemon->new(listen => [$listen]); $daemon_parent->start->stop; # 启动后立即停止,获取分配的端口 my $actual_listen = $daemon_parent->listen->[0]; my ($port) = $actual_listen =~ /:(\d+)$/; if (my $pid = fork()) { # 父进程记录PID和端口,用于后续管理 $self->attr(child_processes => sub { {} }) unless $self->has('child_processes'); $self->child_processes->{$pid} = { port => $port, url => "http://localhost:$port" }; $c->render(json => { url => "http://localhost:$port", pid => $pid }); } else { # 子进程:完全重置Mojo全局状态 # 1. 重置IOLoop单例 Mojo::IOLoop->singleton(Mojo::IOLoop->new); # 2. 关闭父进程所有监听套接字(避免子进程继承后监听相同端口) if (my $server = Mojo::Server->singleton) { $_->close for @{$server->acceptors}; $server->acceptors([]); } # 3. 初始化干净的应用实例和数据库 my $app_config = $c->req->json || {}; my $big_app = TheBigApp->new(%$app_config); # 这里添加初始化干净数据库的逻辑,比如调用$big_app->setup_clean_db() # 4. 启动独立的Daemon服务 my $daemon = Mojo::Server::Daemon->new( app => $big_app, listen => [$actual_listen], silent => 1 # 可选:关闭日志输出避免干扰父进程 ); $daemon->run; # 用run代替start,确保子进程进入独立事件循环 exit 0; # 确保子进程退出时清理 } }); # 可选:添加停止子进程的接口 $self->routes->post('/stop-child/:pid')->to(cb => sub ($c) { my $pid = $c->param('pid'); if (exists $self->child_processes->{$pid}) { kill 'TERM', $pid; delete $self->child_processes->{$pid}; $c->render(json => { status => 'ok' }); } else { $c->render(json => { error => 'Process not found' }, status => 404); } }); } app->start;
关键注意点:
- 子进程必须重置
Mojo::IOLoop的单例,不能复用父进程的反应器 - 必须关闭父进程的
acceptors套接字,防止子进程监听相同端口 - 用
Daemon->run()代替app->start(),因为start()会调用全局Mojo::Server的逻辑,而run()是Daemon实例独立的事件循环 - 数据库初始化逻辑要放在子进程中,确保每个实例用独立的干净数据库
方案2:用非Mojolicious的轻量管理进程(备选)
如果不想处理Mojolicious的单例问题,可以用纯Perl写一个简单的HTTP管理进程,父进程不运行Mojolicious事件循环,fork后子进程不会继承任何Mojolicious单例状态:
use strict; use warnings; use Plack::Request; use HTTP::Server::PSGI; use TheBigApp; use Mojo::Server::Daemon; my %child_processes; my $app = sub { my $env = shift; my $req = Plack::Request->new($env); if ($req->path eq '/spawn-child' && $req->method eq 'POST') { my $listen = 'http://*:0'; my $daemon_parent = Mojo::Server::Daemon->new(listen => [$listen]); $daemon_parent->start->stop; my $actual_listen = $daemon_parent->listen->[0]; my ($port) = $actual_listen =~ /:(\d+)$/; my $pid = fork(); if ($pid) { $child_processes{$pid} = $port; return [200, ['Content-Type' => 'application/json'], ["{\"url\":\"http://localhost:$port\",\"pid\":$pid}"]]; } else { # 子进程直接启动应用,无Mojolicious单例冲突 my $big_app = TheBigApp->new($req->json || {}); $big_app->setup_clean_db(); # 初始化干净数据库 my $daemon = Mojo::Server::Daemon->new(app => $big_app, listen => [$actual_listen]); $daemon->run; exit 0; } } elsif ($req->path =~ m{/stop-child/(\d+)} && $req->method eq 'POST') { my $pid = $1; if (exists $child_processes{$pid}) { kill 'TERM', $pid; delete $child_processes{$pid}; return [200, ['Content-Type' => 'application/json'], ["{\"status\":\"ok\"}"]]; } else { return [404, ['Content-Type' => 'application/json'], ["{\"error\":\"Process not found\"}"]]; } } else { return [404, ['Content-Type' => 'text/plain'], ['Not Found']]; } }; HTTP::Server::PSGI->new(port => 8080)->run($app);
内容的提问来源于stack exchange,提问作者rafl

