2012/08/11

Jenkinsジョブ開始からの時間調整

Redmineのプラグイン対応でチケット操作で即時予約実行ができるようになったので
Jenkins側で回すバックエンドスクリプトを頻繁に回す必要がなくなった。

この辺のタスク整理をしていたら、さほど注視してなかった録画が開始されるまでのタイムラグが気になってきた。この辺の対応をやっておくと何かと便利そうなのと、簡単に対応出来そうなので、スクリプト整理と同時にやってしまおう。今回はそこら辺の話です。

reserve2.rb廃止
以前の記事で作ったジョブ登録スクリプトを一時間ごとに実行していたけど、不要になった。

クエリー予約へ統合
自動予約はクエリーからの予約だけで良くなったので、以前の記事で作ったquery.rbにジョブ登録までの機能を統合した。

録画ジョブは録画1分前に起動
Jenkinsジョブが実際の起動する時間を観測すると指定時間より28秒程度遅く起動しているようだ。どうしてこんなにずれるの?という深いところは置いておいて、
起動が思った以上に指定時間より遅いので、1分前に起動して実際の録画時間になるまでウエイトするような処理を加えることにした。
その時間を使って録画開始までの前処理をちょっと入れたり、どうせなら便利に使おう。ざっくり30秒くらいは別の用途に使えるはずだ。

Jenkinsの待機時間設定
Jenkinsでジョブごとに秒指定は出来ないと書いたが、ジョブ起動から何秒待つかという待機時間を秒設定することが出きる。が、ジョブ起動からの相対値なので今回のような用途には向かない。さらに、これをジョブに設定していない場合、Jenkinsのシステム設定の待機時間が使われてしまうので、システムの待機時間を0にしておくか、録画ジョブのテンプレートに0を設定しておく。念のため両方共0設定。

時刻同期設定
以前の記事に追記したntpの設定もしっかりやっておく。手持ちの電波時計と比べてもずれは無いみたいだ。

時間待ちをする ready.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'active_record'

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

class Issue < ActiveRecord::Base
end
class CustomValues < ActiveRecord::Base
end

abort "no arguments" if ARGV.size==0
issue = Issue.find(ARGV[0])
abort "no ticket" if issue.nil?

# 録画トラッカー
issue.tracker_id = 3
issue.save

time = CustomValues.first( :conditions => {:customized_id => issue.id,
                             :custom_field_id => 1}).value.to_s
start_time = Time.parse issue.start_date.strftime("%Y%m%d") + format("%04d",time)

wait_time = (start_time - Time.now) - 4 # 4秒前に録画開始(外から設定できるといいかな)

# 開始待ち
if wait_time > 1
  p 'waiting...%d secs' % wait_time
  sleep( wait_time )
end
ウエイトして時間調整する前処理で、チケットのトラッカーを予約から録画にする程度のことをやっている。いずれ似たような事前処理が増えていくかも。
このready.rbが終わって次のrecrd2.shで録画が開始されるまでの時間が4秒くらいかかる。そのために4秒前に終わるように調整している。

録画ジョブの実行シェルは3つになった
ready.rb ジョブ起動した時に最初に動く
record2.sh 録画を行う
record2.rb チケットへファイルのアタッチをする
3つのシェルコマンドが登録できるように'record_0'ジョブテンプレートも直す。

対応したクエリー予約スクリプト query2.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-

require 'active_record'
require 'rest_client'
require 'hudson-remote-api'

key = 'api-key'
api = "http://localhost/projects/%s/issues.xml?key="+key+"&query_id="

# レコード
class Issue < ActiveRecord::Base
end
class CustomValues < ActiveRecord::Base
end
class Query < ActiveRecord::Base
end
class Project < ActiveRecord::Base
end

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

# project識別名を合成
api = api % Project.first(1)[0].identifier.to_s

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

# ジョブ作成
def create_job info

  # Config
  return false if !Hudson::Job.list.include?("record_0")
  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["/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]
                      ]
  # コマンド登録
  config.elements.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }

  # 作成
  job_name = "record_%d" % info[:issue_id]
  job = Hudson::Job.get job_name
  job = Hudson::Job.create job_name if job.nil?

  # 設定
  job.update config.to_s
  job.enable
  return true
end

# 予約クエリー検索
reserve = []
queries = Query.find :all, :conditions => { :project_id =>1, :is_public =>1 }
queries.each {|query|

  # REST-APIでクエリー実行
  doc = REXML::Document.new RestClient.get api+query.id.to_s

  # REXMLでトラバース
  doc.elements.each('issues/issue') {|issue|
    if issue.elements['tracker'].attributes['id'].to_i == 1 &&
        issue.elements['status'].attributes['id'].to_i == 1
      # 予約チケットID収集
      reserve << issue.elements['id'].text.to_i
    end
  }
}

Issue.find(reserve).each {|issue|
  channel = CustomValues.first( :conditions => {
                                  :customized_id => issue.id,
                                  :custom_field_id => 2}).value.to_i
  time = CustomValues.first( :conditions => {
                               :customized_id => issue.id,
                               :custom_field_id => 1}).value.to_i

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

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

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

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

  puts "ID:%d [%s]" % [issue.id, issue.subject]
}


以前の記事で作ったチケット操作での予約処理の方もジョブ登録機能を持つので直す。

対応したフックスクリプト video_hooks.rb
# -*- coding: utf-8 -*-

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

# ジョブ作成
def create_job info

  # Config
  return false if !Hudson::Job.list.include?("record_0")
  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["/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]
                      ]
  # コマンド登録
  config.elements.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }

  # 作成
  job_name = "record_%d" % info[:issue_id]
  job = Hudson::Job.get job_name
  job = Hudson::Job.create job_name if job.nil?

  # 設定
  job.update config.to_s
  job.enable
  return true
end

# ジョブ削除
def delete_job id

  job = Hudson::Job.get "record_#{id}"
  if !job.nil?
    job.delete
    return true
  end
  return false
end

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
      do_cancel issue if issue.tracker_id == 1 || issue.status_id == 5
      do_reserve issue if issue.tracker_id == 2
    end
  end

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

  # 予約?
  def do_reserve issue

    time = issue.custom_field_values[0].to_s
    channel = issue.custom_field_values[1].to_s

    # 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.to_i,
      # 保存場所
      :output => "/opt/videos/%d" % issue.id
    }

    # ジョブ登録してチケットを実行中に更新
    if create_job info
      issue.status_id = 2
    end
  end

end

create_job関数の部分は共通になったので外部モジュール化ができそうだ。だけど何処に置くと使いやすいかなぁ。redmineプラグインのlibに置く? 外部タスクの所に置く? どっちを$LOAD_PATHに追加してrequireする?
この辺が固まってないんで、今のところはどっちにも記述しておくけど、こういうのがバグの元なんだよね。

駆け込み録画!
この記事を書いているときに突然思い立って追加したのがvideo_hooksスクリプト内の時間調整のところ。もう開始時間は過ぎてしまったんだけど、途中からでも残りを録画したいという予約時間調整をする部分。

ビデオファイル名をチケットIDに変更!
今まではファイル名はYYYYMMDDmmhh.mp4とかだったんだけれど、一分前処理とかしちゃったもんだから、番組情報と実際の時間が合わなくなった。もう思い切ってファイル名=チケット番号ってことにした。そうしたらあちこちシンプルになった。

今までのものもチケットにアタッチされているので、アタッチファイルからファイル名取得してれば、コンパチビリティも問題なさそう。

あちこちにバグがあって、時々変なことになるけど概ね良い感じに録画ができているかな。オリンピック録画チケットも着々と増えている。失敗もあるけど多くの原因はチューナデバイスの競合だ。これはこれで解決しなくては。

0 件のコメント:

コメントを投稿