Herokuのappを二つ使って交互起動で常時運用(Sinatra版)
この記事は2016/5/31までの内容です。
Twitter bot の運用に heroku を使っている [twitter:@riocampos] です。Web app はあまり使っていません。
でも記載しましたが、 Free dyno だと18時間起動/6時間強制睡眠を強要されるようになります(現状ではまだ24時間起動出来ていますが、そろそろ強要されそう)。
ということで対策を講じました。2つの app を使って交互に起動させる、という手段です*1。
process type として web を使います。それ以外では通信が行えないのでダメです。が、web だと30分の間にアクセスが生じないとプロセスが止められるのでその対策も行っています。
また、希望の時間帯に app を止めるために Process Scheduler を使っています(もちろん実行時間を一日18時間までに設定)。
heroku で複数の app を登録する方法
はこちらを見てください。
複数の app を登録する場合 - Herokuのapp作成手順(buildpack-multiを使う場合も含む) - Qiita
alternateapps.rb
こちらで相手側との通信を行います。
簡単に言えば「自分が起きたら相手に知らせる」「相手から知らせがあったら定期実行している作業を止める(グローバル変数を使う)」ということで交互起動を実現しています。
具体的には http://anotherappurl/update へのベーシック認証アクセスを行うことで「起動したよ」通知としています。
(現状だと意味はないのですが)相手からの通知があった場合に REST 的に JSON でレスポンスを返しています。
なお、環境変数にベーシック認証の ID/PW や自分/相手のURLを登録しています(ベーシック認証の ID/PW を共用していますが、もちろん別個にした方が安全ですね)。ローカルで実行確認するときに dotenv gem を使いました。
環境変数を heroku に登録するには Setting up config vars for a deployed application のように heroku config:set
コマンドを使います。
#!/usr/bin/env ruby # coding: utf-8 require 'sinatra/base' require 'sinatra/json' require 'dotenv' require 'open-uri' require 'json' $stdout.sync = true $update = nil Dotenv.load certs = [ENV['BASIC_AUTH_USERNAME'], ENV['BASIC_AUTH_PASSWORD']] begin JSON.parse(open(ENV['ANOTHER_SITE_URL'], {:http_basic_authentication => certs}).read) rescue Errno::ECONNREFUSED, OpenURI::HTTPError puts "Another app does not wake." end require './schedule' #ここで通常のアプリ(ただしjoinしてないもの)を読み込む class App < Sinatra::Base use Rack::Auth::Basic do |username, password| username == ENV['BASIC_AUTH_USERNAME'] && password == ENV['BASIC_AUTH_PASSWORD'] end configure do # register Sinatra::Reloader # enable :logging set :port, ENV['PORT'].to_i end get '/update' do $update = true puts "Recieve access from another app. Stop the cron processes." JSON.dump({status: 'Update'}) end get '/*' do 404 end run! if app_file == $0 end
ちなみに Heroku のポート番号は環境変数 PORT に入っています。
schedule.rb
こちらが定期実行している作業のスクリプト例です。
私は cron 作業を行わせるのに rufus-scheduler gem を常用しているので、ここでもそれを使って書いています。ここでは定期作業の例として毎時0/10/20/30/40/50分に時刻をログ出力させています。相手の app から知らせがあるとグローバル変数 $update が true になるので、定期作業ブロックの冒頭に next if $update
を置くことで定期作業を止めています。
また、scheduler.every '15m'
のブロックで自分に15分おきにアクセスしており、それにより「Heroku ルータに30分アクセスがないとプロセスを落とす」という web process type の弊害を回避しています。もちろん Thread.start してsleep 15*60 したloopブロックでアクセスしてもいい(はず)です。
require 'rufus-scheduler' require 'open-uri' scheduler = Rufus::Scheduler.new scheduler.every '15m' do begin open(ENV['MY_SITE_WAKE_URL']).read rescue OpenURI::HTTPError => ex puts 'wake up access.' # puts ex.backtrace.first + ": #{ex.message} (#{ex.class})" end end scheduler.cron '0 */10 * * * *' do next if $update puts Time.now end
なお、こちらのスクリプトを参考にして Thread を使った場合、末尾に thread.join させたくなる(また rufus-scheduler でも scheduler.join させたくなる)のですが、そうするとドツボに嵌まりますのでご注意ください。参考:Sinatra内でloopなど定期作業を行うには - 別館 子子子子子子(ねこのここねこ)
補足
この app ではごく単純に「相手が起動したらこちらは定期作業をそれ以降実行しない」ようにしていますが、もちろん
get '/down' do $update = nil JSON.dump({status: 'Down'}) end
のような map を追加しておけば、相手から http://myappurl/down へアクセスがあれば定期作業を再開出来ます。適当に機能追加してもいいでしょう。
補足2
web プロセスを使っているので、スクリプトの起動から60秒以内に Sinatra が動作している状況をつくる必要があります。もし起動にそれ以上掛かっている場合には
Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch Stopping process with SIGKILL Process exited with status 137
のように、エラーメッセージが出た後に終了してしまいます。ご注意下さい。
参考
- open-uri でのベーシック認証の方法
singleton method OpenURI.open_uri
open_uri(name, mode = 'r', perm = nil, options = {})
-> StringIO
- :http_basic_authentication
HTTP の Basic 認証のためのユーザ名とパスワードを、文字列の配列 ["user", "password"] として与えます。
- Sinatra でのベーシック認証サイト設定方法
- その他いろいろ
ひとりごと
とうとうグローバル変数を使ってしまった…。
*1:本当はお金を払うべきなんだけどな(汗