timestamp.py
180 lines
| 5.7 KiB
| text/x-python
|
PythonLexer
Simon Sapin
|
r49079 | # Copyright Mercurial Contributors | ||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Simon Sapin
|
r49079 | |||
Simon Sapin
|
r49081 | import functools | ||
r49202 | import os | |||
Simon Sapin
|
r49079 | import stat | ||
Raphaël Gomès
|
r52949 | import time | ||
from typing import Optional, Tuple | ||||
Simon Sapin
|
r49079 | |||
r49226 | from .. import error | |||
Simon Sapin
|
r49079 | |||
rangemask = 0x7FFFFFFF | ||||
Simon Sapin
|
r49081 | @functools.total_ordering | ||
Simon Sapin
|
r49079 | class timestamp(tuple): | ||
""" | ||||
Simon Sapin
|
r49081 | A Unix timestamp with optional nanoseconds precision, | ||
Simon Sapin
|
r49079 | modulo 2**31 seconds. | ||
Simon Sapin
|
r49271 | A 3-tuple containing: | ||
Simon Sapin
|
r49079 | |||
`truncated_seconds`: seconds since the Unix epoch, | ||||
truncated to its lower 31 bits | ||||
`subsecond_nanoseconds`: number of nanoseconds since `truncated_seconds`. | ||||
Simon Sapin
|
r49081 | When this is zero, the sub-second precision is considered unknown. | ||
Simon Sapin
|
r49271 | |||
`second_ambiguous`: whether this timestamp is still "reliable" | ||||
(see `reliable_mtime_of`) if we drop its sub-second component. | ||||
Simon Sapin
|
r49079 | """ | ||
def __new__(cls, value): | ||||
r49227 | truncated_seconds, subsec_nanos, second_ambiguous = value | |||
value = (truncated_seconds & rangemask, subsec_nanos, second_ambiguous) | ||||
Simon Sapin
|
r49079 | return super(timestamp, cls).__new__(cls, value) | ||
Simon Sapin
|
r49081 | def __eq__(self, other): | ||
r49226 | raise error.ProgrammingError( | |||
'timestamp should never be compared directly' | ||||
Simon Sapin
|
r49081 | ) | ||
def __gt__(self, other): | ||||
r49226 | raise error.ProgrammingError( | |||
'timestamp should never be compared directly' | ||||
) | ||||
Simon Sapin
|
r49081 | |||
Simon Sapin
|
r49079 | |||
Raphaël Gomès
|
r52949 | def get_fs_now(vfs) -> Optional[timestamp]: | ||
r49202 | """return a timestamp for "now" in the current vfs | |||
This will raise an exception if no temporary files could be created. | ||||
""" | ||||
tmpfd, tmpname = vfs.mkstemp() | ||||
try: | ||||
return mtime_of(os.fstat(tmpfd)) | ||||
finally: | ||||
os.close(tmpfd) | ||||
vfs.unlink(tmpname) | ||||
Raphaël Gomès
|
r52949 | def zero() -> timestamp: | ||
Simon Sapin
|
r49079 | """ | ||
Returns the `timestamp` at the Unix epoch. | ||||
""" | ||||
return tuple.__new__(timestamp, (0, 0)) | ||||
Raphaël Gomès
|
r52949 | def mtime_of(stat_result: os.stat_result) -> timestamp: | ||
Simon Sapin
|
r49079 | """ | ||
Takes an `os.stat_result`-like object and returns a `timestamp` object | ||||
for its modification time. | ||||
""" | ||||
Simon Sapin
|
r49082 | try: | ||
# TODO: add this attribute to `osutil.stat` objects, | ||||
# see `mercurial/cext/osutil.c`. | ||||
# | ||||
# This attribute is also not available on Python 2. | ||||
nanos = stat_result.st_mtime_ns | ||||
except AttributeError: | ||||
# https://docs.python.org/2/library/os.html#os.stat_float_times | ||||
# "For compatibility with older Python versions, | ||||
# accessing stat_result as a tuple always returns integers." | ||||
secs = stat_result[stat.ST_MTIME] | ||||
Simon Sapin
|
r49079 | |||
Simon Sapin
|
r49082 | subsec_nanos = 0 | ||
else: | ||||
billion = int(1e9) | ||||
secs = nanos // billion | ||||
subsec_nanos = nanos % billion | ||||
Simon Sapin
|
r49079 | |||
r49227 | return timestamp((secs, subsec_nanos, False)) | |||
r49224 | ||||
Raphaël Gomès
|
r52949 | def reliable_mtime_of( | ||
stat_result: os.stat_result, present_mtime: timestamp | ||||
) -> Optional[timestamp]: | ||||
Raphaël Gomès
|
r52950 | """Wrapper for `make_mtime_reliable` for stat objects""" | ||
file_mtime = mtime_of(stat_result) | ||||
return make_mtime_reliable(file_mtime, present_mtime) | ||||
def make_mtime_reliable( | ||||
file_timestamp: timestamp, present_mtime: timestamp | ||||
) -> Optional[timestamp]: | ||||
Simon Sapin
|
r49271 | """Same as `mtime_of`, but return `None` or a `Timestamp` with | ||
`second_ambiguous` set if the date might be ambiguous. | ||||
r49224 | ||||
A modification time is reliable if it is older than "present_time" (or | ||||
Simon Sapin
|
r49250 | sufficiently in the future). | ||
r49224 | ||||
Otherwise a concurrent modification might happens with the same mtime. | ||||
""" | ||||
Raphaël Gomès
|
r52950 | file_second = file_timestamp[0] | ||
file_ns = file_timestamp[1] | ||||
r49224 | boundary_second = present_mtime[0] | |||
r49232 | boundary_ns = present_mtime[1] | |||
r49224 | # If the mtime of the ambiguous file is younger (or equal) to the starting | |||
# point of the `status` walk, we cannot garantee that another, racy, write | ||||
# will not happen right after with the same mtime and we cannot cache the | ||||
# information. | ||||
# | ||||
r49232 | # However if the mtime is far away in the future, this is likely some | |||
r49224 | # mismatch between the current clock and previous file system operation. So | |||
# mtime more than one days in the future are considered fine. | ||||
r49232 | if boundary_second == file_second: | |||
if file_ns and boundary_ns: | ||||
if file_ns < boundary_ns: | ||||
return timestamp((file_second, file_ns, True)) | ||||
return None | ||||
elif boundary_second < file_second < (3600 * 24 + boundary_second): | ||||
r49224 | return None | |||
else: | ||||
Raphaël Gomès
|
r52950 | return file_timestamp | ||
Raphaël Gomès
|
r52952 | |||
FS_TICK_WAIT_TIMEOUT = 0.1 # 100 milliseconds | ||||
def wait_until_fs_tick(vfs) -> Optional[Tuple[timestamp, bool]]: | ||||
"""Wait until the next update from the filesystem time by writing in a loop | ||||
a new temporary file inside the working directory and checking if its time | ||||
differs from the first one observed. | ||||
Returns `None` if we are unable to get the filesystem time, | ||||
`(timestamp, True)` if we've timed out waiting for the filesystem clock | ||||
to tick, and `(timestamp, False)` if we've waited successfully. | ||||
On Linux, your average tick is going to be a "jiffy", or 1/HZ. | ||||
HZ is your kernel's tick rate (if it has one configured) and the value | ||||
is the one returned by `grep 'CONFIG_HZ=' /boot/config-$(uname -r)`, | ||||
again assuming a normal setup. | ||||
In my case (Alphare) at the time of writing, I get `CONFIG_HZ=250`, | ||||
which equates to 4ms. | ||||
This might change with a series that could make it to Linux 6.12: | ||||
https://lore.kernel.org/all/20241002-mgtime-v10-8-d1c4717f5284@kernel.org | ||||
""" | ||||
start = time.monotonic() | ||||
try: | ||||
old_fs_time = get_fs_now(vfs) | ||||
new_fs_time = get_fs_now(vfs) | ||||
while ( | ||||
new_fs_time[0] == old_fs_time[0] | ||||
and new_fs_time[1] == old_fs_time[1] | ||||
): | ||||
if time.monotonic() - start > FS_TICK_WAIT_TIMEOUT: | ||||
return (old_fs_time, True) | ||||
new_fs_time = get_fs_now(vfs) | ||||
except OSError: | ||||
return None | ||||
else: | ||||
return (new_fs_time, False) | ||||