【问题标题】:Ruby Time.zone.now and accounting for database precisionRuby Time.zone.now 并考虑数据库精度
【发布时间】:2019-07-26 10:27:07
【问题描述】:

我在 Rails 3 测试套件中进行了一个测试,该测试生成了一个数字断言,这些断言比较了在我的本地机器上通过但在我们的 CI 管道中失败的时间戳。该测试在 Postgres 数据库时间戳字段中存储一个时间戳,精度为 6,并将存储的值与原始时间戳进行比较,非常类似于以下示例:

tmp_time = Time.zone.now
u = User.find(1)
u.updated_at = tmp_time
u.save!
u.reload

assert_equal u.updated_at.to_i, tmp_time.to_i # passes...
assert_equal u.updated_at, tmp_time # fails...
assert_equal u.updated_at.to_f, tmp_time.to_f # fails...

我认为问题与 Ruby 的时间表示比存储的值具有更高的精度有关。

除了在比较中不太精确之外,补偿由于精度引起的值的细微差异的最佳方法是什么?我们考虑过覆盖 Time.zone.now,但相信这会导致下游问题。

提前致谢。

【问题讨论】:

  • 这个测试首先可能有什么用处?好像你不信任时间戳。
  • 你能在 CI 管道中输出u.updated_at.to_ftmp_time.to_f 的值吗?
  • “在我的本地机器上通过,但在我们的 CI 管道中失败” – 如果这是由于 Postgres,你应该在本地机器上遇到同样的问题。或者您是否在本地和您的 CI 上运行不同的 Postgres 版本/数据库模式?

标签: ruby-on-rails ruby postgresql


【解决方案1】:

当您调用.save! 时,会发生对数据库的实际写入。时间戳由数据库写入,该数据库更新存储在 updated_at 中的实际数据,这些数据不是由 ActiveRecord Ruby 对象写入的(除非您明确地使用 u.update_attribute(updated_at: tmp_time) 这样做,这在大多数情况下会破坏时间戳点。

因此,当您实例化 Ruby Time 对象时,内存中的时间将与数据库记录的时间不匹配,这将在几纳秒之后。转换Time.new.to_i 不是很准确。虽然.to_f 通常“足够接近”,但时间平等几乎是不可能的。这可以用一个多线程的例子来说明:

@times = []
def test_time
  t1 = Thread.new{ @times << Time.now }
  t2 = Thread.new{ @times << Time.now }
  t1.join; t2.join
end
test_time

puts @times.each(&:to_s)
# they may appear the same depending on default `.to_s` format
# 10/29/2021 2:20PM
# 10/29/2021 2:20PM

@times.map(&:to_i)
=>[1635531636,1635531636] # same problem

@times.map(&:to_f)
=>[1635531636.989422, 1635531636.989532] # here's often enough precision but... 

# under the hood times[1] == times[2] will use the most precise nsec method
@times.map(&:nsec)
=>[989422129, 989531969]

另见See docs on Time#nsec

【讨论】:

  • 冻结时间在这里有什么帮助?当前时间仅检索一次,并从那里的局部变量中引用,该变量不会改变。
  • @LeninRajRajasekaran 我已经更新了我的答案,谢谢。
【解决方案2】:

问题可能不在于数据库中的精度,而是在您定义 tmp_time 和保存时之间经过了一小段时间。

您可以看到 Time 的 .to_f 表示立即发生变化:

irb(main):011:0> 2.times.map { Time.now.to_f }
=> [1551755487.5737898, 1551755487.573792]

当您使用.to_i 时,通常看不到这种差异,因为它会四舍五入到最接近的秒数。

正如另一个答案所提到的,您可以使用Timecop 来解决这个问题:

irb(main):013:0> Timecop.freeze { 2.times.map { Time.now.to_f } }
=> [1551755580.12368, 1551755580.12368]

【讨论】:

  • Time.zone.now 在 OP 的测试中仅使用一次,而不是您的示例中的两次。 OP 将其存储在一个变量中,并将其作为一个常量引用,该常量在断言过程中不会改变。
  • 保存时内部调用,确定updated_at
  • 对了。明白了。所以问题是我们不能自己设置updated_at。 Rails 总是覆盖它们。
  • @LeninRajRajasekaran 这可能不是真的。 u.save! 不会覆盖手动设置的时间戳,这无论如何都是不好的做法。事实上,整个测试似乎毫无意义,除非它发现了一些我们都没有意识到的核心 Rails 错误。
  • @lacostenycoder 你说得对,这个测试似乎毫无意义,但是在某个时间点锁定测试有有效的用例。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-18
  • 1970-01-01
  • 2013-10-23
  • 1970-01-01
相关资源
最近更新 更多