Время: часы, дедлайны и календарные даты
Программы задают три разных вопроса и все три называют «временем»: сколько это заняло, какое сегодня число и сколько прошло в мире, пока машина спала. В kola у каждого вопроса своё показание со своей единицей измерения, а чтение часов считается полномочием, которое функции нужно явно передать. Глава разбирает обе стороны, а затем библиотеку std::time для календаря и меток времени в формате RFC 3339.
Часов, до которых можно дотянуться, нет
В C функцию clock_gettime можно вызвать откуда угодно, в Python — time.time(). В kola таких глобальных функций нет. Доступ к внешнему миру приходит в виде капабилити: обычного значения, которое даёт один вид доступа, например право читать часы. Полный набор капабилити (sys) среда выполнения передаёт в main и больше никуда, так что sys.clock попадает в функцию, только если кто-то передал его аргументом. Clock — это маркер полномочия: его можно бросить на любом пути, но никакой код не может его создать.
Кроме того, функция объявляет свои эффекты после типа результата: !{} значит, что она только вычисляет, !{io} — что она обращается к внешнему миру, !{io, block} — что она ещё и может ждать. На практике это значит, что сигнатура и есть аудит. Функция, которая не принимает &Clock и не объявляет io, не может прочитать время, и рецензент убедится в этом, не читая тела.
use std::log;
fn timed_work(clock: &Clock, n: u64) -> u64 !{io}
req n <= 100000
{
let t0 = clock.now();
let mut acc: u64 = 0;
let mut i: u64 = 0;
while i < n {
acc = acc +% i;
i = i + 1;
}
let t1 = clock.now();
t1 - t0
}
fn checksum(b: &[u8]) -> u64 !{} {
let mut h: u64 = 0;
let mut i: usize = 0;
while i < b.len {
h = h *% 31u64 +% widen[u64](b[i]);
i = i + 1;
}
h
}
pub fn main(sys: Sys) -> u8 !{io, log} {
let ns = timed_work(&sys.clock, 1000);
let h = checksum(&b"kola");
log::info(f"elapsed_ns={ns} checksum={h}");
0
}
test the_stopwatch_measures_a_duration (mut sim: Sim) {
// виртуальные часы сдвигаются на 1000 нс за чтение, два чтения дают 1000
assert timed_work(&sim.clock, 10) == 1000u64;
}Стоит пропустить любую из двух половин, и компилятор назовёт недостающую. Здесь checksum пытается прочитать часы, которых ей никто не давал:
fn checksum(b: &[u8]) -> u64 !{} {
let t = clock.now();
widen[u64](b.len) +% t
}Чекер отвечает error[card §1] с пометкой «unknown name» для clock: такого имени в области видимости нет. Если передать Clock параметром, но оставить ряд !{}, ошибка станет card §6: функция выполняет io (Clock::now), но не объявляет его, а строка подсказки предлагает «add io to the effect clause, or hand the work to a caller that has it».
Три показания
Clock отвечает на три вопроса, и за ними стоят разные часы операционной системы. Монотонные часы идут только вперёд и с постоянной скоростью, но их нулевая точка произвольна: ими удобно мерить длительность и бесполезно — дату. Настенные часы показывают календарное время, но система может в любой момент перевести их вперёд или назад (когда NTP поправляет время или администратор выставляет его вручную), поэтому разность двух настенных показаний может оказаться бессмыслицей. Каждое показание — метод часов.
Показание | Единица и начало отсчёта | Для чего |
|---|---|---|
| наносекунды с момента создания этой капабилити; монотонные | Секундомер: длительности, тайм-ауты, ограничение частоты, задержки. |
| наносекунды с 1970-01-01 UTC, разрешение — микросекунда; со знаком | Дата: срок действия сертификатов, метки в журнале, всё, что читает человек или другая сторона соединения. |
| наносекунды от того же начала, что и | Аренды и сроки истечения, которые спящая машина не должна продлевать. |
Настенное показание знаковое, чтобы можно было представить моменты до 1970 года: так арифметика дат остаётся всюду определённой. Из таблицы стоит запомнить и другое: now — секундомер. Его ноль — момент создания капабилити, а не загрузка системы и тем более не 1970 год.
now_boot никогда не меньше now. Эти показания берутся из разных часов ОС (в Linux CLOCK_MONOTONIC и CLOCK_BOOTTIME, в Darwin CLOCK_UPTIME_RAW и CLOCK_MONOTONIC, причём там именно последние учитывают сон), а POSIX никак не упорядочивает одни относительно других, поэтому среда выполнения подтягивает now_boot к now. В итоге разность now_boot - now не может уйти в минус и действительно равна времени, проведённому во сне. В freestanding-сборке (программе, которая работает прямо на железе, без операционной системы) now_boot совпадает с now: усыплять её некому.
Ещё два показания измеряют процессорное время, а не прошедшее: clock.cpu_ns() для всего процесса и clock.thread_cpu_ns() для текущего потока ОС. Их значения абсолютные и не имеют общего начала отсчёта, так что смысл имеет только разность двух показаний. В Windows их разрешение равно такту планировщика (около 15,6 мс), и короткая фаза может дать нулевую разность.
use std::log;
fn readings(clock: &Clock) -> (u64, i64, u64) !{io} {
let mono = clock.now(); // нс с момента создания капабилити
let wall = clock.now_wall_ns(); // нс с 1970-01-01
let boot = clock.now_boot(); // нс, включая сон машины
(mono, wall, boot)
}
// Аренда по секундомеру, учитывающему сон, с насыщающей арифметикой:
// показание часов, которое программа не контролирует, не должно вызвать трап.
pub fn lease_deadline_ns(c: &Clock, ttl_ms: u64) -> u64 !{io}
req ttl_ms <= 3600000
{
let now = c.now_boot();
let ttl_ns = ttl_ms * 1000000;
if now > NO_DEADLINE - ttl_ns { NO_DEADLINE } else { now + ttl_ns }
}
pub fn main(sys: Sys) -> u8 !{io, log} {
let (mono, wall, boot) = readings(&sys.clock);
let d = lease_deadline_ns(&sys.clock, 30000);
let live = d > boot;
log::info(f"mono={mono} wall={wall} boot={boot} lease_live={live}");
0
}
test the_three_readings_under_sim (mut sim: Sim) {
let (mono, wall, boot) = readings(&sim.clock);
// now и now_boot берутся из одного виртуального счётчика, 1000 нс за чтение.
// Настенный счётчик отдельный и сдвигается на 1 мс за чтение.
assert mono == 1000u64;
assert boot == 2000u64;
assert wall == 1000000i64;
}
test a_lease_is_ahead_of_the_reading_it_came_from (mut sim: Sim) {
let base = sim.clock.now_boot();
let d = lease_deadline_ns(&sim.clock, 1000);
assert d > base;
assert d == 2000u64 + 1000000000u64;
}Ради аренды now_boot и существует. Аренда — это право на ограниченное время, например блокировка, которая истекает, если владелец её не продлил. У сервиса, который меряет срок аренды по now, есть тихая ошибка: когда машина засыпает, эти часы останавливаются, и аренда продлевается ровно на тот промежуток, когда машины не было рядом, чтобы её обновить.
Секундомер — не дата
Вот классическая ошибка, и совершить её легко, потому что каждая строка выглядит разумно. Фоновый сервис выпускает сертификаты X.509 (те самые, которыми TLS подтверждает подлинность сервера), и в каждом есть поле notBefore — момент, с которого сертификат действителен. Показание часов у сервиса уже есть, он переводит clock.now() в нужные единицы и подставляет в notBefore. Арифметика верна, а начало отсчёта — нет: сертификат теперь утверждает, что действует с 1970 года плюс время работы процесса. Каждая сторона проверяет now < not_before, и каждое TLS-рукопожатие с таким сервисом проваливается.
use std::log;
use std::time::instant;
use std::time::instant::Instant;
// НЕВЕРНО: clock.now отсчитывает время от создания капабилити.
fn broken_timestamp(clock: &Clock) -> Instant !{io} {
let ms = clock.now() / 1000000u64;
Instant { micros: widen[i64](ms) * 1000i64 }
}
// ВЕРНО: instant_now читает настенные часы и превращает показание в Instant.
fn real_timestamp(clock: &Clock) -> Instant !{io} {
instant::instant_now(clock)
}
// Секунды Unix для API, которым они нужны (срок действия X.509, `exp` в JWT).
fn unix_seconds(clock: &Clock) -> u64 !{io} {
narrow[u64](clock.now_wall_ns() / 1000000000i64).unwrap()
}
pub fn main(sys: Sys) -> u8 !{io, log} {
let bad = instant::instant_to_datetime(broken_timestamp(&sys.clock)).date.year;
let good = instant::instant_to_datetime(real_timestamp(&sys.clock)).date.year;
let secs = unix_seconds(&sys.clock);
log::info(f"stopwatch as a date: year {bad}; wall clock: year {good}; unix {secs}");
0
}
test both_spellings_look_identical_under_sim (mut sim: Sim) {
// Виртуальные настенные часы стартуют с эпохи, поэтому здесь ошибочное
// и правильное показания совпадают. Никакой симулированный тест их не различит.
let bad = instant::instant_to_datetime(broken_timestamp(&sim.clock));
let good = instant::instant_to_datetime(real_timestamp(&sim.clock));
assert bad.date.year == 1970i64;
assert good.date.year == 1970i64;
}Если запустить программу по-настоящему, ошибочный вариант покажет 1970 год, а правильный — текущий. Тест внизу объясняет, почему такая ошибка переживает хороший набор тестов: под --sim виртуальные настенные часы стартуют с эпохи, оба показания попадают в январь 1970 года, и тест проходит в любом случае. Различить их могут только настоящие часы, а программа, чей main не запускает ни один тест, с ними не встречается.
У той же ошибки есть второй вариант: правильный источник и неверный делитель. Локальная переменная называется now_ns, на деле хранит микросекунды и делится на 1e9. Оба значения — i64, а о смысле чисел компилятор своего мнения не имеет.
Сон
clock.sleep(ms: u64) !{io, block} — единственная операция Clock, которая ждёт, и поэтому единственная, у которой помимо io есть block. Когда поток kola блокируется на операции с капабилити, среда выполнения переключается на другой поток, у которого есть работа, так что спящий поток не мешает соседям (см. Потоки и каналы). Если объявить io, но не block, чекер сообщит, что функция выполняет block (Clock::sleep), но не объявляет его.
use std::log;
// Ограниченный повтор с экспоненциальной задержкой.
pub fn retry_backoff(clock: &Clock, attempts: u64) -> u64 !{io, block}
req attempts <= 8
{
let mut i: u64 = 0;
let mut delay: u64 = 1;
while i < attempts {
clock.sleep(delay);
delay = delay * 2;
i = i + 1;
}
delay
}
pub fn main(sys: Sys) -> u8 !{io, log, block} {
let t0 = sys.clock.now();
let d = retry_backoff(&sys.clock, 8);
let t1 = sys.clock.now();
let waited_ms = (t1 - t0) / 1000000;
log::info(f"next_delay={d}ms waited~{waited_ms}ms");
0
}
test backoff_is_exact_under_sim (mut sim: Sim) {
let t0 = sim.clock.now();
let d = retry_backoff(&sim.clock, 8);
let t1 = sim.clock.now();
assert d == 256u64;
// 1+2+...+128 = 255 мс сна плюс 1000 нс на само чтение t1
assert t1 - t0 == 255001000u64;
}
test zero_is_a_legal_no_wait (mut sim: Sim) {
let t0 = sim.clock.now();
sim.clock.sleep(0);
let t1 = sim.clock.now();
assert t1 - t0 == 1000u64;
}kola run на этом файле действительно ждёт около 255 мс и может сообщить лишь примерное время. kola test укладывается в миллисекунды и проверяет прошедшее время с точностью до наносекунды, потому что симулированные часы идут по правилам, а не по факту ожидания. Поэтому набор тестов, полный путей с тайм-аутами, может оставаться быстрым и при этом делать точные проверки.
Freestanding-сборка отказывается от sleep: на голом железе нет планировщика, которому можно уступить процессор, и ждать нужно на собственном таймере. Отказ приходит от компилятора C в виде необъявленного идентификатора, имя которого само всё объясняет: k_clock_sleep_UNAVAILABLE_FREESTANDING__no_scheduler_to_yield_to__park_on_your_own_timer. Остальные показания в freestanding-сборке работают как обычно.
Симулированные часы
Тест, объявивший (mut sim: Sim), получает симулированный набор капабилити, которым управляет случайное зерно (seed) — число, фиксирующее все случайные решения в прогоне, — а sim.clock в нём виртуальные. Правила короткие, и знать их полезно, чтобы не проверять того, чего симуляция дать не может:
Каждое чтение сдвигает часы. Чтение
nowилиnow_bootприбавляет к монотонному счётчику 1000 нс, так что секундомер вокруг пустого кода покажет 1000, а не 0.nowиnow_bootделят этот единственный счётчик. Модели сна в симуляторе нет, так что не пишите тест, который ждёт их расхождения.У
now_wall_nsсвой счётчик, начинающийся с эпохи. Каждое чтение сдвигает его на 1 мс; первое чтение возвращает1000000.Сон сдвигает оба счётчика ровно на свой аргумент и чтением не считается:
sleep(10)добавляет 10 000 000 нс,sleep(0)не добавляет ничего.Когда потоков несколько, каждое ограниченное ожидание (
sleep,recvиз канала,wait_readableна сокете) назначено на абсолютный момент виртуального времени. Первым срабатывает самое раннее, и виртуальное время перескакивает к нему, поэтому 30-секундное ожидание рядом с 700-секундным сном соседа длится 30 секунд. При равенстве первым идёт поток с меньшим номером, и одно и то же зерно даёт одну и ту же трассу.cpu_nsиthread_cpu_nsтоже берутся из монотонной последовательности.
Всё выводится из зерна, поэтому падение воспроизводится бит в бит с --seed N. C-бэкенд реализует те же правила независимо, а kola test запускает оба исполнителя и сравнивает результаты, так что точная проверка времени — это настоящая сверка двух реализаций.
std::time: календарь, эпоха, формат передачи
Всё, что было выше, касалось капабилити. Превратить число в дату — задача библиотеки, и std::time делит её на три модуля.
std::time::civil— календарь как чистые данные:Date,Time,DateTime; конструкторыmk_dateиmk_time, возвращающиеOption;is_leap_year,days_in_month,days_from_civilиcivil_from_days. Все функции —!{}, об эпохах и часах они ничего не знают.std::time::instant— мост к эпохе.InstantиDuration— это знаковоеi64с микросекундами от 1970 года.instant_to_datetimeиdatetime_to_instantпереводят в календарь и обратно;duration_add,duration_sub,instant_diff,instant_ltиinstant_leотвечают за арифметику;pg_micros_to_unix_microsи родственные функции переводят в эпоху PostgreSQL (2000-01-01) и обратно. Только одна функция здесь обращается к капабилити:instant_now(clock: &Clock) -> Instant, которая делит настенное показание до микросекунд.std::time::iso8601— кодек для передачи, строгое подмножество RFC 3339 над байтами.parse_rfc3339возвращаетResult[(DateTime, Option[i32]), TimeErr], гдеOption[i32]— смещение от UTC в минутах (NoneдляZ), аwrite_rfc3339выполняет обратное.TimeErr— этоBadFormat,BadDateилиBadTime. Строчныеtиz, пробел вместоTи пониженная точность отвергаются: парсер, принимающий больше, чем выдаёт парный ему писатель, не может гарантировать круговое преобразование, а каждое лишнее написание — это поверхность для атаки (см. Сериализация и кодировки).
use std::log;
use std::text::utf8;
use std::time::civil;
use std::time::instant;
use std::time::iso8601;
use std::time::civil::DateTime;
use std::time::instant::Instant;
fn christmas_2026() -> DateTime !{} {
// Корректность проверяется один раз, при создании: mk_date возвращает
// Option, так что 30 февраля никогда не станет значением Date.
let d = civil::mk_date(2026i64, 12u32, 25u32).unwrap();
let t = civil::mk_time(9u32, 30u32, 0u32, 0u32).unwrap();
DateTime { date: d, time: t }
}
fn render[r](out: &r mut Vec[u8], dt: DateTime) !{alloc(r)} {
let z: Option[i32] = None;
iso8601::write_rfc3339(out, dt, z);
}
pub fn main(sys: Sys) -> u8 !{io, log, alloc(heap)} {
let dt = christmas_2026();
let us = instant::datetime_to_instant(dt).micros;
region r {
let buf: &r mut Vec[u8] = alloc in r mut Vec::new();
render(buf, dt);
let s = utf8::to_str(buf).unwrap();
log::info(f"{s} = {us} us since the epoch");
}
0
}
test parse_then_format_round_trips {
region r {
// Недоверенные байты получают Result, а не трап.
let (dt, off) = iso8601::parse_rfc3339(&b"2026-12-25T09:30:00Z").unwrap();
assert dt.date.year == 2026i64;
assert dt.date.month == 12u32;
assert dt.time.hour == 9u32;
assert off == None;
let out: &r mut Vec[u8] = alloc in r mut Vec::new();
iso8601::write_rfc3339(out, dt, off);
let s = utf8::to_str(out).unwrap();
assert s == "2026-12-25T09:30:00Z";
}
}
test the_calendar_is_pure_data {
assert civil::is_leap_year(2024i64);
assert !civil::is_leap_year(2100i64);
assert civil::days_in_month(2024i64, 2u32) == 29u32;
assert civil::mk_date(2026i64, 2u32, 30u32) == None;
assert civil::days_from_civil(1970i64, 1u32, 1u32) == 0i64;
}
test the_epoch_bridge_is_total {
let epoch = instant::instant_to_datetime(Instant { micros: 0i64 });
assert epoch.date.year == 1970i64;
// Микросекунда до эпохи: любое i64 соответствует календарному моменту.
let before = instant::instant_to_datetime(Instant { micros: 0i64 - 1 });
assert before.date.year == 1969i64;
assert before.time.hour == 23u32;
}instant_to_datetime определена всюду: каждое i64 соответствует ровно одному календарному моменту, без Option и без трапа, а моменты до 1970 года получаются правильными, потому что модуль делит с округлением вниз, а не обычным для kola /, отбрасывающим дробную часть к нулю. Форматер пишет в растущий байтовый буфер Vec[u8], выделенный в регионе — блоке, вся память которого освобождается на закрывающей скобке; поэтому ряд эффектов у него !{alloc(r)}. У построения сертификатов в std::crypto::x509_build есть своя datetime_from_unix(secs: u64), которая принимает секунды; передавайте ей результат показанного выше unix_seconds.
Дедлайны живут на ожидающих операциях
Код, связанный со временем, чаще всего занят дедлайнами, а они задаются не на Clock, а на операциях, которые ждут:
rx.recv(timeout_ms: u64) -> Recv[T]принимает из канала и отвечаетMsg(v),TimeoutилиClosed, не смешивая «отправитель медлит» и «отправителя больше нет». См. Потоки и каналы.c.wait_readable(timeout_ms)иc.wait_writable(timeout_ms)ждут готовности сокета, файла или устройства и отвечаютOk(true), если готово, иOk(false), если время ожидания вышло.c.wait_ready(timeout_ms)за одну проверку отвечаетData,ClosedилиTimeout. См. Сеть.
Каждый timeout_ms — это u64, который выжидается полностью. Тайм-аут 0 означает «проверь сейчас и скажи», и именно это спрашивает цикл обслуживания после каждого законченного запроса. Чтобы ждать вовсе без дедлайна, передайте NO_DEADLINE — зарезервированный литерал, равный наибольшему u64. Осторожнее на стыке с POSIX: там нулевой SO_RCVTIMEO означает «без тайм-аута», то есть прямо противоположное мгновенной проверке в kola.
Тайм-аут ограничивает одно ожидание, а не цикл из них. Собеседник, который отвечает чуть раньше каждого тайм-аута, удержит цикл ограниченных ожиданий навсегда. Настоящий дедлайн — это один clock.now(), взятый до цикла и сравниваемый на каждой итерации.
Последняя ловушка ждёт со стороны файловой системы. stat возвращает FileInfo { size, mtime_ns, kind }, и в симулированной файловой системе mtime_ns — детерминированно растущая метка, а не настенное время. Никогда не сравнивайте её с clock.now_wall_ns(). Это та же ошибка категорий, что и с сертификатом: два целых числа одного типа и порядка величины, но с разным началом отсчёта.