奇怪的 datetime.utcnow() 错误

Weird datetime.utcnow() bug

考虑这个简单的 Python 脚本:

$ cat test_utc.py
from datetime import datetime

for i in range(10_000_000):
    first = datetime.utcnow()
    second = datetime.utcnow()

    assert first <= second, f"{first=} {second=} {i=}"

当我 运行 它从 shell 像 python test_utc.py 它完成 w/o 错误,正如预期的那样。但是,当我 运行 它在 Docker 容器中时,断言失败:

$ docker run -it --rm -v "$PWD":/code -w /code python:3.10.4 python test_utc.py
Traceback (most recent call last):
  File "/code/test_utc.py", line 7, in <module>
    assert first <= second, f"{first=} {second=} {i=}"
AssertionError: first=datetime.datetime(2022, 5, 24, 19, 5, 1, 861308) second=datetime.datetime(2022, 5, 24, 19, 5, 1, 818270) i=1818860

怎么可能?

P.S。一位同事报告说,将范围参数增加到 100_000_000 也会使其在 mac 上的 shell 中失败(但对我而言不是)。

utcnow refers to now refers to today refers to fromtimestamp refers to time,表示:

While this function normally returns non-decreasing values, it can return a lower value than a previous call if the system clock has been set back between the two calls.

utcnow code也说明了它的用法time:

def utcnow(cls):
    "Construct a UTC datetime from time.time()."
    t = _time.time()
    return cls.utcfromtimestamp(t)

这样的系统时钟更新也是 monotonic 存在的原因,即:

Return the value (in fractional seconds) of a monotonic clock, i.e. a clock that cannot go backwards. The clock is not affected by system clock updates.

utcnow没有这样的保证。

您的计算机没有完美的时钟,有时它会通过互联网与更准确的时钟同步,可能会向后调整。例如参见 [​​=32=].

而且看起来 Docker 使情况变得更糟,请参阅 Docker 博客中的示例 Addressing Time Drift in Docker Desktop for Mac。摘录:

macOS doesn’t have native container support. The helper VM has its own internal clock, separate from the host’s clock. When the two clocks drift apart then suddenly commands which rely on the time, or on file timestamps, may start to behave differently

最后,您可以增加在发生向后更新时捕捉到向后更新的机会。如果一个不是发生在 firstsecond 之间,而是发生在 second 和下一个 first 之间,您就会错过它!下面的代码修复了这个问题,也是 micro-optimized(包括删除 utcnow 中间人)所以它检查得更快/更频繁:

import time
from itertools import repeat

def function():
    n = 10_000_000
    reps = repeat(1, n)
    now = time.time
    first = now()
    for _ in reps:
        second = now()
        assert first <= second, f"{first=} {second=} i={n - sum(reps)}"
        first = second
function()