bashでシグナルを使った同期

最近こんなの書きました。

やりたいことは、

  • webサーバーでリクエストを受け取ったら少量のデータ(メッセージ)をDBに格納してすぐレスポンスを返す
  • 同時にバックグランドで複雑な処理をしてDBに反映する
  • バックグラウンドで処理中に複数のメッセージがあった場合はまとめて処理できるようにする
  • リクエストがなくても一定時間毎にバックグランド処理を実行したい

です。

Producer-Consumer的な?
簡単なメッセージキュー?のようなものをbashだけでできます。
(リクエストを受けるのとバックグランドでの処理は別途必要です)

syncrun.sh


#! /bin/bash

function wakeup {
    wakeup=yes
}

function stop {
    stop=yes
}

function loop {
    while true; do
        sleep $sec
        kill -USR1 $mypid
    done
}

# スクリプト終了時にバックグランドで起動したプロセスとpidファイルを削除
function clean {
    kill "$looppid"
    rm -f "$pidfile"
}

sec=60
pidfile=/tmp/syncrun.pid

# オプションの解析
while getopts "s:p:" opt; do
  case $opt in
    s) sec="${OPTARG}" ;;
    p) pidfile="${OPTARG}" ;;
  esac
done

shift $(( $OPTIND - 1 ))

wakeup=yes
stop=no

mypid=$$
echo "mypid=${mypid}"
echo $mypid > $pidfile

# ユーザー定義の割り込み
trap wakeup USR1

# Ctrl+C とか、端末が切り離されたとき
trap stop HUP TERM INT

# スクリプトの終了時
trap clean EXIT

# バックグラウドでループ (新たにプロセスが起動する)
loop &

# ループプロセスのpid
looppid=$!

# "stop"フラグが立っていたらループを抜けて終了
while [[ $stop != yes ]]; do

    # シグナルがあったら指定されたコマンドを実行
    # コマンド実行中にシグナルがたつことがあるので、実行した後もフラグチェック
    # フォアグランドのコマンド実行中はトラップされない。終了時にトラップされる
    while [[ $wakeup == yes ]]; do
        wakeup=no
        command "$@"
    done

    wait # 起動したプロセスの終了を待つ。シグナルで割り込まれる
done

-s オプションで 最大の待機時間、 -p オプションで pid記録するファイルを指定します。
残りの引数は実行するコマンドとその引数です。

最大10秒待機、pidを/tmp/syncrun.pid に記録するように指定して起動

$ bash syncrun.sh -s 10 -p /tmp/syncrun.pid echo hoge

daemon化する場合

$ setsid bash syncrun.sh -s 10 -p /tmp/syncrun.pid echo hoge

待機を中断して処理をさせる

$ kill -USR1 $(cat /tmp/syncrun.pid)

終了

$ kill -HUP $(cat /tmp/syncrun.pid)

使い道としては、DBにとりあえず入れたログを一定時間、一定件数で集計するとか。
特にログをもとに木構造のデータをDB上に構築していくような場合で、木の変更は同期的にやりたいけど、排他ロックはかけたくない、みたいな時に使えるかも。
うまくやればリアルタイムっぽいランキング集計とかできるかもしれないです。

あとはアップロードされた動画の変換処理とかですね。

upload-video.php

<php
function get_converter_pid() {
    (略) // pidファイルを読んで返す
}

function notify_uploaded() {
    if (($pid = get_converter_pid()) == false) {
        return false;
    }

    posix_kill($pid, SIGUSR1);
}

(略)

move_uploaded_file ($_FILES['videofile']['temp_name'], $dest);
$stmt = $pdo->prepare('insert into videos (file, state, date) values(:file, "uploaded", :date)');
$stmt->execute(array(':file' => $dest, ':date' => date('Y-m-d H:i:s')));

notify_uploaded();

(略)

convert-video.php


function fetch_uploaded_video() {
    (略)
}

/*
    return true if state changed or return false
*/
function update_video_state($video_id, $state) {
    (略)
}

function update_converted_video($video_id, $file, $state='converted') {
    (略)
}

/*
    変換された動画のファイルのパスを返す
*/
function convert_video_format($in_filename) {
    (略) // ffmpeg とかつかって全力で変換します
}

list($video_id, $video_file) = fetch_uploaded_video();

if (!empty($video_id) && update_video_state($video_id, "processing")) {
    $out_file = convert_video_format($video_file);
    update_converted_video($video_id, $out_file);
}

(略)
$ setsid bash syncrun.sh -s 60 -p /tmp/videoconverter.pid php /PATH/TO/convert_video.php

シグナル以外にも、名前付きパイプ(FIFO)を使うとか、
inotify でディレクトリを監視するとか、
他にもやりかたがありますです。

ファイルがアップロードされたらrsyncするとかは、inotify版でやったほうが楽だと思いますです。

しかしそもそもbashでやる必要があったのか(笑)