1.はじめに
ジュークボックスサーバーを造ったのですが、言っても再生リスト順に再生開始、停止させているだけです。ただ、リクエスト元のgoogleAssistantからはジュークボックスの状態とは(だいたい)無関係に要求を投げて来るので、適時応答を返してあげる必要があります。
その仕掛けとしてフルフィルメントとジュークボックスの間にキューを置いて、同期しないようにしています。
ジュークボックスは定期的にキューを参照してリクエストがあればその要求に応じた動作をする――わけですが、キューの参照間隔(下図の①)が長いとレスポンスが悪くなり、キューサイズも有限ですからパンクする可能性が高まります。
キューの参照間隔を大きく左右するのはgoogleHomePlayerの実行動作になります。そもそも1曲の再生が終わるまで次の曲に移れません。実装では無駄にCPU利用率が上昇しないように1秒以下のスリープを入れていたりするのですが、曲の再生では3分くらいはかかるのでスパンが2桁ほど違います。
つまり、以下2点を実装する必要があります。
1)曲の再生終了は拾わなければならない
2)キューの読み込みサイクルは曲の再生長以下にしなければならない
ここまで長く書いてきましたが、ぶっちゃけ子プロセスの非同期実行管理が必要になったということで、本稿はその実装について説明する内容となります。
2.PHPでのFork&Exec
2.1.子プロセスの非同期実行
PHPで子プロセスを実行するにはexec()
関数やsystem()
関数などがありますが、どちらも実行した子プロセスが終了するまで後続処理ができません。それはそれで有用な機能ですが、今回の用途には合いません。
非同期実行させるためには、古典的なfork()
&exec()
を行う必要があります。PHPではpcntl_fork()
とpcntl_exec()
で実現されます。fork()
が呼ばれると、その時点の実行状態のまま自プロセスの複製が子プロセスとして生成されます。オリジナルのプロセスか、複製プロセスかはfork()
(pcntl_fork()
)関数の戻り値で識別できて、オリジナル(親プロセス)であれば複製プロセスのプロセスID(PID)、複製プロセス側では0が返されます。
exec()
(pcntl_exec()
)が呼ばれると、呼び出したプロセスは引数で与えた実行ファイルにPIDはそのままで切り替わります。
今回はジュークボックスサーバーがGoogleHomePlayerをバックグラウンドプロセスとして実行させるために使います。
2.2.子プロセスの終了監視
Fork&Execで実行された子プロセスの終了監視にはpcntl_wait()
かpcntl_waitpid()
が使えます。使い分けに悩みますが、今回は子プロセスが単体なので丁寧にpcntl_waitpid()
を使うことにしました。関数の名前からして指定したPIDのプロセスが実行終了するまで待たせるもののようですが、今回は待ちに入って実行が止まるのは困るのでWNOHANG
フラグを指定します。
ただ、問題は返り値の扱いです。PHPのドキュメントには次のようにあります。
pcntl_waitpid() は、終了した子プロセスの プロセス ID を返します。エラーの場合は -1、WNOHANG が使用され、 子プロセスが利用できない場合に 0 を返します。
そのため、戻り値に子プロセスのPIDを期待して実行中かどうかの判定を行ったところ、曲が次々切り替わって行きます。$status
に終了ステータスが格納されるとあるのですが、設定はされていないようです。
どういう動きをするのかドキュメントからは読み切れなかったので、確認のためにfork&execの後に1秒間隔でpcntl_waitpid()
を呼び出してみたところ、実行中は0、終了直後あたりでPIDが戻り、それ以降はー1が戻りました。今回は実行中かどうかを知りたいので、戻り値が0かどうかで真偽判定するようにしました。
2.3.実装
ジュークボックスから見るとGoogleHomePlayerはオブジェクトとして振る舞うことになるので、クラスとして扱うようにしました。
class childProcess {
public $pid;
public $argv;
public function __construct(){
$this->preExec();
}
public function exec($dst, $mp3Path){
$this->argv = array(gpd_playModuleJs, $dst, $mp3Path);
if(($this->pid = pcntl_fork()) == 0){
pcntl_exec("node", $this->argv);
}
}
public function is_running(){
if($this->pid == 0) return false;
$rc = pcntl_waitpid($this->pid, $status, WNOHANG);
if($rc == -1){
$this->preExec();
}
return ($rc == 0);
}
public function preExec(){
$this->pid = 0;
}
public function kill(){
if($this->pid > 0){
posix_kill($this->pid, SIGKILL);
$this->preExec();
}
}
}
実行本体はchildProcess::exec()
で、引数として再生対象のgoogleHomeのIPアドレスとmp3のURIを与えるようにしています。
実行中かどうかの判定はchildProcess::is_running()
です。他には実行中のGoogleHomePlayerを停止するchildProcess::kill()
もありますが、説明は不用だと思います。ただ、楽曲再生そのものはgoogleHomeで行われているのでプロセスを停止しても再生は続きます。再生を止めるには前稿で触れましたが無音のmp3を再生させるのが手っ取り早いように思います。
3.おわりに
自分だけで使う分にはpcntl_fork()&pcntl_exec()ではなくexec()だけでも十分使えるのですが、動作確認のために一度再生を始めると曲が終わるまで次に移れないのでそれはそれでストレスになります。
それと、どこにも説明していませんが、ジュークボックスに再生させるmp3のパスを次々渡し、ジュークボックス内部に再生リストを作成する機能を持たせているのですが、再生中にその機能が使えないのは地味に不便でした。
ジュークボックスは最終的にはsystemdからサービス起動できるようにするつもりで、その時にpcntl_fork()がまた出て来るはずですが、そちらはあまり特筆するようなことは無い様に思っています。