8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Railsでサービス構築するときは時間を制御できる仕組みを入れておくと大変はかどる

Last updated at Posted at 2016-12-06

Rubyコードの開発中に、時間を変更するときには、timecop gem が便利である。

timecop - Github
https://github.com/travisjeffery/timecop

例えば、 Timecop.travel(t) を実行した時点で、そのRubyインスタンスの現在時刻が変わり、かつ、tの時点から時間が進む。
Timecop.freeze(t) を実行すると、現在時刻がtで固定となる。
などなど、いくつかのメソッドが用意されている。
tはTimeクラスのインスタンス)
Railsと組み合わせると、開発チームが共用で使っている開発サーバの時間を変更・偽装・シミュレート出来て便利である。ちょっとしたサービスなら、時間をからめたテストが必要だったり、イベントやキャンペーンの確認に使えるだろう。
Timecop.travel を活用するためのちょっとしたスニペットをご紹介。

ページを作ろう

timecop単体では、UIは提供していない。
ちょっとした規模のサービスやアプリケーションなら、開発用のデバッグページがあったり、管理用のページがあるだろう。
Viewから時間を変更できるような画面を作っておけば、開発者以外のチームメンバーでも簡単に時間を変えられる。
当然ながら、本番環境では、本記事のスニペットは全部無効になるように実装しておくのが良い。

class DebugController < ApplicationController
  def update_time
    if params[:time].present?
      Timecop.travel(Time.parse(params[:time]))
    else
      Timecop.return
    end
  end
end

例外に備えよう

とは言え、時間をごまかしたくない例外もある。
例えば、aws-sdkだ。aws-sdk を使ってS3にアクセスするとき、ほんとうの時間とリクエスト時のタイムスタンプのズレが大きいと、下記のようなエラーが出る。

The difference between the request time and the current time is too large. (Aws::S3::Errors::RequestTimeTooSkewed)

他にも、なにか外部サービスを利用していて時間をごまかしたくない時にどうするか・・・。aws-sdkにモンキーパッチを当てようにも、Time.now を参照している箇所が多すぎる。
下記のような黒魔術で、呼び出し元によってtimecopを無効化させることができる。

class Time
  class << self
    def now_wrap
      if (caller || []).first.match('aws-sdk')
        now_without_mock_time # return real time
      else
        mock_time || now_without_mock_time # return mock time or real time
      end
    end
    alias_method :now, :now_wrap
  end
end
  • 現時点のtimecopのバージョンでは、非公開APIだが内部的に mock_time メソッドと now_without_mock_time メソッドをTimeクラスに生やしているのでこれを利用して現実世界の正しい時刻を得る。
  • Rubyの Kernel#caller を使ってコールスタック(呼び出し元)にaws-sdkが含まれるか判定する。呼び出し元によって暗黙的に動作を変えるのは、極悪と言っても良いプログラミングスタイルである。通常の状況でヤッてはいけない。

再起動に備えよう

timecop単体では、永続化機能は提供していない。ページを作るだけでは不十分で、デプロイ契機で揮発する。
ファイルシステムでも、DBでも、なんでも良いので、永続化させちゃえば良い。

設定するところ

# 入力値を保存
redis.set("timecop:travel_time", params[:time])
# 現在のリアル時刻も一緒に保存
redis.set("timecop:travel_started_time", Time.now_without_mock_time.to_s)
# アプリケーションサーバを再起動させる。下記コマンドは Unicornな想定。
system 'kill -s USR2 `cat /var/www/hogehoge-pj/shared/tmp/pids/unicorn.pid` &'
  • 現代のアプリケーションは、マルチプロセスで動くことは当たり前である。Unicornもワーカープロセスの数を自由に設定できるし、開発環境でもマルチで動作検証しておくべきだろう。複数のプロセスにtimecopを一気に適用させるには、再起動させるのが手っ取り早い。再起動の方法は環境に依存する。

Rails起動時(application.rbとか)にやること

tt = redis.get("timecop:travel_time")
tst = redis.get("timecop:travel_started_time")
if tt && tst
  a = Time.parse(tt)
  b = Time.parse(tst)
  # ここで時間変更。設定した日次に、経過時間を加算する
  Timecop.travel(a + (Time.now_without_mock_time - b).to_i.seconds)
end

起動時に毎回同じ時刻に .travel するのではなく、差分計算をして経過時間をシミュレートしたほうが、非エンジニア的にはうれしい。


まとめ

Ruby は標準のメソッドもばんばん書き換えられるので、たまに苦しめられるけど、楽しい。
今回は timecop gem を紹介したが、これを使わずにDIYするのも容易いと思う。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?