環境: circleci/ruby:2.4.2-stretch-node-browsers, circleci/postgres:10.12
とあるアプリケーションで、新しくCircleCIを設定してRspecを動かしてみたら、エラーが大量に出ました。
expected: 2020-03-25 12:36:20.477148202 +0900
got: 2020-03-25 12:36:20.477148000 +0900
Rubyはナノ秒(小数点以下9桁)まで時刻を保持し、PostgreSQLはマイクロ秒(小数点以下6桁)までしか保持しないので、比較が失敗しまくっていると判明。ひえー、どうすんのこれ。助けて!
用語
参照: Orders_of_magnitude_(time)
名前 | 単位 | 例 |
---|---|---|
ミリ秒 | 10-3 秒 | 0.123 |
マイクロ秒 | 10-6 秒 | 0.123456 |
ナノ秒 | 10-9 秒 | 0.123456789 |
以下、3桁ずつピコ 10-12、フェムト 10-15、アット 10-18、ゼプト 10-21、ヨクト 10-24と続きます。
Macでは
Mac(rbenvでインストール)では、RubyのTime#nsecはナノ秒(9桁)を表しますが、実際はマイクロ秒(6桁)で切り取られるので、Postgresとの違いに気づきませんでした。
t = Time.now
t.to_f #=> 1585394326.856343
t.nsec #=> 856343000
Ubuntuだとナノ秒までしっかり入ります。
t = Time.now
t.to_f #=> 1585394640.8082483
t.nsec #=> 808248257
対策
Time#<=> を上書きしてミリ秒で比較することでエラーを減らすことができました。<=> を上書きするだけで、== も != も < も対策できます。Time#<=> を上書きすると、ActiveSupport::TimeWithZone の比較にも使われます。
マイクロ秒で比較(floor(6))ではうまく行きませんでした。
if ENV['CIRCLECI']
class Time
alias_method :old_cmp, :<=>
def <=>(val)
if val.try(:acts_like_time?)
self.to_f.floor(3) <=> val.to_f.floor(3)
else
old_cmp(val)
end
end
end
end
ちなみにRubyには時刻の小数点以下を丸めるメソッド Time#round があり、Ruby 2.7 では切り落としを行う Time#floorが追加されています。
今後やりたいこと
- 上記の対策よりもうまい方法を考える。
- Rubyのソースコードをチェックしたり、Linuxでコンパイルしてみたりする(たぶんC言語の#defineに違いがある)。