2012年9月10日月曜日

予約の登録、待機、実行

レコーダーネタまだ一杯ある。
というわけで、予約を登録して、実行まで待機して、実行するまでの流れについて。

予約の登録

予約はTwitterで「うぽって録画して」みたいにつぶやくだけである。
中はどうなってるかというと Programs テーブルから要求された単語を検索して、見つかったら Reserves テーブルに追加する。

word = "うぽって"
descs = Groonga['Programs'].select do |record| 
 (record.description =~ word) & (record.stop > Time.now)
end
titles = Groonga['Programs'].select do |record|
  target = record.match_target do |match_record|
    match_record.title * 5
  end
  (target =~ word) & (record.stop > Time.now)
end

programs = descs.union!(titles).sort([ {:key => "_score", :order => "descending" }, 'start', { :key => 'channel.type', :order => "descending" } ])

programs.each do |program|
  unless Groonga['Reserves'].has_key?(program._key)
    reserve = Groonga['Reserves'].add(program._key, {} )
    reserve.title = program.title
    reserve.start = program.start
    # 省略
    break
  else
    # 予約済み
  end
end

ほんとは一連の流れを排他で処理しなきゃならないけどそれはまた今度。まぁこんな感じ。
検索のスコアリングは好き好きあるだろうけどこの例だとタイトルでのヒットは番組詳細に対して5倍のスコアで算出するようにしてる。

予約実行までの待機

予約から実行までの流れをざっくり考えてみる。もっとも単純なのはきっとこう。

  1. 予約リストを開始時間でソートして1番目の開始時間まで sleep
  2. sleepを抜けたら別スレッドで録画開始
  3. 録画開始した予約に実行中フラグを立てて 1 に戻る

でもどう考えてもこんな簡単なのではだめで、予約は任意のタイミングで追加されるし、その都度待機時間を調整しなくてはならない。
それを考慮すると、方法は2つ浮かんできて、

  • 予約リストを監視する
  • 予約リストが更新されたという通知を受け取り、待機をやめる

ようするにパッシブかアクティブかのどちらか。
予約リストの監視をするにはおそらく 短時間 sleep を挟んだ無限ループで行うことになるが、これはどうもスマートではないし、十分なマージンをとって録画を開始しなきゃならない。(まぁどのみちマージンはあったほうがいいのだが)

では、通知を受け取る方法はどうやるのか。
inotify を使うとかもあるけど、簡単なIPCは名前付きパイプでできるし、pipe のIOをselectで待ってれば、タイムアウト付きで待機できる。
次の録画開始までの待機時間をタイムアウトに設定すればうまいことやれそうである。
実際のコードはこんな感じ

シェルで名前付きパイプをつくっとく

$ mkfifo -m 600 var/run/recorder

Ruby のコード

REC_MERGIN = 10 # 10秒マージン
IPC_PIPE = 'var/run/recorder'

ipc = open(IPC_PIPE, "r")

loop do
  reserve = Groonga['Reserves'].select do |r| (r.running == false) end.sort(['start']).first
  wait = reserve.start - Time.now + REC_MERGIN
  ret = IO::select([ipc], nil, nil, wait > 0 ? wait : 0.1)
  if ret
    ipc.gets
    puts "io event"
  end
  reserve = Groonga['Reserves'].select do |r| (r.running == false) end.sort(['start']).first
  if reserve and reserve.start <= Time.now + REC_MERGIN
    start_recording(reserve)
  end
end

適当だけどこんな感じ。
別のスレッドやプロセスから "var/run/recorder" に対して "\n" を書けば select が起きて処理が進むし、でなければ次の録画開始時間まで待機する。
start_recording() は中で reserve.running = true にして 別スレッド生成。
案外簡単。

※ 他プロセスが絡まない待機なら timeout(sec) { Queue.pop } や observer なんかでもいいかもしれない。

あとは、待機時間が大変長い時に適当にEPG取得するような処理いれたり(いつ予約が入るかわからないからいつでも殺せるような仕組みで)したらよい。

注意しなきゃならないのは、外部プログラムで録画していると何が起こるかわからないのでちゃんと例外をキャッチして情報を得られるようにしておかないと後で辛い。
あとは例外でプロセスが死なないようにうまく retry したり。

録画開始から終了までの話はまた別エントリで。

0 件のコメント:

コメントを投稿