2012/08/28

番組予約タスクの重複解決

チューナが1つしかないんで、時間が重なっていると何方かの録画が失敗するわけですね。これをどうにかしたいなぁということで、取り組んでおりましたが・・・

という感じの話と

外部タスクと、Redmineのフック処理でコードを共有化する際に行ったこと

について。

やりたいこと
  1. 絶対録画したいやつを、自動予約や不意の予約が重なっても保護されること。
  2. マニュアル操作での予約で重複があることがすぐに分かるようにすること
1は、チケットのプライオリティ属性を使えばいいかと。重複している番組チケットのプライオリティが高いものが優先されるという処理を何処かで行えば良い。

2は、Redmineのフックスクリプト内で重複を確認して、重複していることをチケットのステータスで知らせるようにすれば良い。

結論から先に言うと、1はまあ出来たかな。2はどうやってもうまく動かない。
もう1歩か2歩先のスキルが必要な感じだ。ということで完全ではないけれど、こうしたいのだ!というところまでは形になったと思うので、そこまでの考えや手順を記録しておく。
(願わくば、どなたか初心者にも分かるようにご教授賜りたい気分です)

思惑
  • 重複確認は、前記事で構築した’VideoTimeline’レコードを使えば簡単にできるはず。
  • 重複が発生する可能性があるのは、自動予約タスクとRedmine操作での予約の時。
  • 重複したチケットのうち負けたチケットは録画が実行されないように。
  • 実行されないけれど、予約されており、無効状態。
  • 無効/有効はJenkinsジョブのアクティブ・非アクティブと連動する。
  • 外部タスクからと、Redmine内部からの処理で重複する部分を共有したい。
共通部分の分離
video-jobs.rb
# -*- coding: utf-8 -*-

# Jenkins接続
Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

def get_job_name id
  "record_#{id}"
end

# ジョブ作成
def create_job info

  jobname = get_job_name info[:issue_id]
  job = Hudson::Job.get jobname
  job.delete if job
  job = Hudson::Job.create jobname

  # 設定
  config = REXML::Document.new(Hudson::Job.get("record_0").config)

  # コマンド
  commands = [
              "/opt/task/ready.rb %d" % info[:issue_id],
              "/opt/task/record2.sh %d %d %s" % [
                                                 info[:channel],
                                                 info[:duration],
                                                 info[:output]
                                                ],
              "/opt/task/record2.rb %d" % info[:issue_id]
             ]
  config.elements.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }
  # 時間
  config.elements["/project/triggers/hudson.triggers.TimerTrigger/spec"].text =
    "%s %s %s %s *" % [
                       info[:datetime][10..11],
                       info[:datetime][8..9],
                       info[:datetime][6..7],
                       info[:datetime][4..5]
                      ]
  job.update config.to_s
  job.enable
  return true
end

def entry_job issue, time, channel

  # 1分前にジョブがスタートするように
  datetime = Time.parse( issue.start_date.strftime("%Y%m%d") + format("%04d",time) ) - 60
  duration = issue.estimated_hours.to_i * 60

  # 時間調整
  nowtime = Time.now
  if datetime < nowtime
    if datetime + duration < nowtime
      # 終わってる・・・
      issue.tracker_id = 1
      return
    else
      # 時間が過ぎてる!後半だけでも!
      duration = duration - (nowtime - datetime) - 60
      datetime = nowtime + 60
    end
  end

  info = {
    # チケット番号
    :issue_id => issue.id,
    # 録画開始日時
    :datetime => datetime.strftime("%Y%m%d%H%M"),
    # 録画時間
    :duration => duration,
    # チャンネル
    :channel => channel,
    # 保存場所
    :output => "/opt/videos/%d" % issue.id
  }

  # ジョブ作成
  if create_job info
    # 実行中
    issue.status_id = 2
  end

  # 予約トラッカー
  issue.tracker_id = 2

  # 重複?
  if duplicated? issue
    disable_job issue
  else
    issue.save
  end

  puts "ID: #{issue.id}"

  update_timeline issue
end

def delete_job issue
  job = Hudson::Job.get get_job_name issue.id
  job.delete unless job.nil?

  update_timeline issue
end

def disable_job issue
  job = Hudson::Job.get get_job_name issue.id
  job.disable unless job.nil?
  # 無効
  issue.status_id = 8
  issue.save
end

def enable_job issue
  job = Hudson::Job.get get_job_name issue.id
  job.enable unless job.nil?
  # 実行中に復活
  if issue.status_id != 2
    issue.status_id = 2
    issue.save
  end
end

# タイムライン更新
def update_timeline issue
  vt = VideoTimeline.where(:issue_id =>issue.id).first
  if vt
    vt.reserved = issue.tracker_id == 2
    vt.save
  end
end

def duplicated? issue
  vt = VideoTimeline.where(:issue_id =>issue.id).first
  vts = VideoTimeline.where("reserved = true and start_on <= '%s' and end_on >= '%s'" % [vt.end_on, vt.start_on])
  return vts && vts.size>0
end

def priority vt
#  ActiveRecord::Base.lock_optimistically = false
  vts = VideoTimeline.where("reserved = true and start_on <= '%s' and end_on >= '%s'" % [vt.end_on, vt.start_on])
  if vts
    # ソート
    issues = Issue.find( vts.map(&:issue_id), :order => "priority_id desc, estimated_hours" )
    if issues && issues.size>0
      # 有効
      enable_job issues.shift
      # 無効
      issues.each {|issue| disable_job issue } if issues.size>0
    end
  end
#  ActiveRecord::Base.lock_optimistically = true
# Issue変更フック内で別のIssueに対する操作を行おうとすると
# ActiveRecord::StaleObjectError (Attempted to update a stale object: Issue)
# というエラーが出てしまう。このエラーを出ないようにすることが、上記の設定で可能だが
# Issue.find自体が正常動作しないようなので、結局別のIssueに対する操作ができそうにない。
# 従って、こっち側は外部タスク専用とする。
end
こんな感じで、Jenkinsジョブ周りの処理とか、チケットステータス更新とか、タイムラインから重複を確認する関数とかをひとまとめにした。
ここでのポイントは、処理内容ではなくて外部タスクからとRedmine内部フックからの両方から使えるようにするということ。

requireを使わない。Redmineプラグイン側はGemfileで指定する。
外部タスク側で必要なrequireやロード、初期化を行う。

という感じにすると、共有がしやすいようだ。

Redmineフック video_hooks.rb
# -*- coding: utf-8 -*-
# $: << "/opt/task"
# $: << "."
# require 'video-jobs'
load '/opt/task/video-jobs.rb'

class VideoHooks < Redmine::Hook::ViewListener

  # チケット更新
  def controller_issues_edit_before_save(context)
    edit_hook context[:issue]
  end

  # バルク更新(チケットごとに呼ばれる)
  def controller_issues_bulk_edit_before_save(context)
    edit_hook context[:issue]
  end

  def edit_hook issue
    # 番組内のチケット
    if issue.project_id == 1
      if issue.tracker_id == 1 || issue.status_id == 5
        do_cancel issue
      elsif issue.tracker_id == 2
        time = issue.custom_field_values[0].to_s
        channel = issue.custom_field_values[1].to_s
        entry_job issue, time.to_i, channel.to_i
      end
    end
  end

  # キャンセル
  def do_cancel issue
    # ジョブ削除して、録画キャンセルチケットに
    delete_job issue
    issue.status_id = 5 if issue.status_id > 1
    issue.tracker_id = 1
  end

end
ほとんどの処理を外(video-jobs.rb)に出してしまったので、以前書いたフックスクリプトより格段にシンプルになった。ここでのポイントは、requireで'video-jobs'をロードするんじゃなくて、直接loadでパス指定で読んでいるところでしょうか。

たいした事じゃないけれど、$: << 'パス'ってやってロードパスに追加して、requireするってのをよく見かける。Jenkinsジョブとして動作させる事を考えるとカレント'.'を追加しても意味が無い。フルパスを指定するなら、requireである必要ないし、そもそもロードしたいのはrubyスクリプトなので、直接loadするのが自然だと思う。
無理してrequireを使う例が多い気がするのは、気のせいか?

外部タスク priority.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'rubygems'
require 'active_record'
require 'hudson-remote-api'
load '/opt/task/video-jobs.rb'

# レコード
class Issue < ActiveRecord::Base;end
class VideoTimeline < ActiveRecord::Base; end

ActiveRecord::Base.establish_connection(
                                        :adapter => 'mysql2',
                                        :host => 'localhost',
                                        :username => 'redmine',
                                        :password => 'redmine',
                                        :database => 'redmine'
                                        )

# clean dust
VideoTimeline.where( :reserved => true ).each {|vt|
  issue = Issue.find( vt.issue_id )
  vt.reserved = false unless issue.tracker_id == 2
  vt.save
}

# main
VideoTimeline.where( :reserved => true ).each {|vt| priority vt }
これは、自動予約タスク処理の直後に動かして、重複している予約チケットの優先順位から上位のものだけを生かして、それ以外は無効にするスクリプト。
こちらは思い通りに動作している。

思い通りにいかなかった事
話が戻るけど、Redmineのフックスクリプトで、チケット予約を行った際に重複をチェックして重複していたら、どうのこうのという処理を行いたかった。その辺の処理がvideo-jobs.rb内のduplicated?関数とpriority関数。

priority関数内にもコメントしてあるけど、Redmineのチケット更新処理フック内で別のチケットの更新とかアクセスとかしようとするとロック機構が働いてエラーが出てしまうようなのだ。そのエラーを回避する事はスクリプト内に書いてある通り出来たけれど、チケット検索処理自体が正常に動作しなくなるみたい。
この辺色々やってみたけどどうしても解決しないんで、ちょっとあきらめた。
外部タスクだけで活用する事にした。

仕方ないと思って、今更新中のチケットが重複しているかどうかだけをチェックしてステータス変更するくらいなら出来るだろうと思ったけど、なぜかduplicated?関数が正常動作しない。処理はされているようだけれど、正しい結果が得られない。トレースしてもスルーされてしまう感じ。こっちも原因がつかめていない。

と、不完全な状態ではあるけれど、ハングはしなくなったし、重複確認もリアルタイムは出来てないけどpriority.rbは動作しているので半分くらいは解決したかな。

この辺はかなーり高いスキルとじっくり取り組み時間が必要かも。ちょっと別の事に取り組もうかな。

0 件のコメント:

コメントを投稿