Skip to content

[rust] Move from chrono (deprecated) to jiff - #1567

Merged
brandur merged 2 commits into
masterfrom
rust-jiff
Nov 2, 2026
Merged

brandur merged 2 commits into
masterfrom
rust-jiff

Conversation

@brandur

@brandur brandur commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

🤖 It was called to my attention last week that it turns out that what's
been traditionally the main date/time library in Rust, chrono, is
actually soft deprecated [1]. The chrono maintainer recommends jiff [2]
as a replacement.

I figure that since River Rust is still quite new and River Pro for Rust
is still unreleased, it's the best time ever to make this jump. Although
annoying, jiff does seem to be very thoughtfully designed, with heavy
inspiration from Temporal in JavaScript.

[1] chronotope/chrono#1768
[2] https://crates.io/crates/jiff

Compatibility changes:

  • Public timestamps move from Chrono to jiff::Timestamp, re-exported through riverqueue::jiff.
  • The timestamp range shrinks from approximately ±262,000 years to Jiff's Timestamp::MIN through Timestamp::MAX. Timestamps outside years -9999 to 9999 are rejected, including stored database values outside that range.
  • The optional chrono-tz feature is renamed to tzdb.
  • CronTimeZone remains Clone but no longer implements Copy; its fixed and named variants use jiff::tz::Offset and jiff::tz::TimeZone.
  • CronSchedule::next_after takes &jiff::Zoned and returns Option<jiff::Zoned>, preserving the reference time zone.
  • PostgresReindexSchedule::DailyUtc takes jiff::civil::Time.

Persisted database formats and canonical timestamp JSON remain unchanged; existing databases require no schema migration. The migration guide includes examples for replacing Chrono types.

@brandur

brandur commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

@bgentry This really sucks, but chrono's never going to get less deprecated, so the best time to switch is still probably now.

Jiff does seem a little better designed, and there was some nice simplification opportunities. For example, previously River had to do some pretty gnarly stuff to get specific RFC3339 formatting:

pub(crate) fn format_rfc3339_nano(timestamp: DateTime<Utc>) -> String {
    let mut formatted = timestamp.to_rfc3339_opts(SecondsFormat::Secs, true);
    let nanos = timestamp.nanosecond() % 1_000_000_000;
    if nanos > 0 {
        let fraction = format!("{nanos:09}");
        formatted.insert(formatted.len() - 1, '.');
        formatted.insert_str(formatted.len() - 1, fraction.trim_end_matches('0'));
    }
    formatted
}

Now it's just timestamp.to_string() because jiff's accepted that RFC3339 is the canonical format for timestamp strings.

Codex had some ideas about potentially putting in a date/time indirection layer to River, but that's definitely got some tradeoffs. I think the BurntSushi crates (which jiff is) have a pretty good maintenance track record, so it might be worth being a bit more optimistic about this one.

@brandur
brandur requested a review from bgentry October 31, 2026 03:52
@brandur brandur changed the title Move from chrono (deprecated) to jiff [rust] Move from chrono (deprecated) to jiff Nov 1, 2026
Comment thread rust/riverqueue/src/database/sqlite.rs Resolved
let utc = naive.checked_sub_signed(chrono::Duration::seconds(offset))?;
let corrected = offset_at(utc);
fn resolve_local(zone: &TimeZone, naive: DateTime) -> Option<Zoned> {
let local = naive.to_zoned(TimeZone::UTC).ok()?.timestamp();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Idiom nit: building a UTC Zoned just to get its timestamp can be TimeZone::UTC.to_timestamp(naive).ok()? (or Offset::UTC.to_timestamp(naive)). parse_rfc3339 in attempt_error.rs does the same round trip through to_zoned and could use Offset::to_timestamp too.

This one predates the PR: in the block below, if corrected != offset { offset = corrected; } is equivalent to offset = corrected;.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Left these unchanged. Direct UTC-to-timestamp conversion is a valid simplification, but the current round trips are correct. The conditional offset assignment is also confirmed to predate this PR, so it remains outside the regression fixes.

let today = date
.to_datetime(time)
.to_zoned(jiff::tz::TimeZone::UTC)
.expect("valid UTC date")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Nit: the .to_zoned(UTC).expect("valid UTC date").timestamp() chain appears twice. TimeZone::UTC.to_timestamp(datetime) (or a small local closure) would remove the duplicate and the second expect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Left this small duplication unchanged. The direct conversion would be a stylistic simplification, but no behavior regression was identified in these two daily-schedule conversions. The separate interval-overflow issue is fixed in e66b027.

Comment thread rust/riverqueue/src/unique.rs Resolved
Comment thread rust/riverqueue/src/client/mod.rs Resolved
mod support;

use chrono::{DateTime, Duration, TimeZone, Utc};
use jiff::{SignedDuration as Duration, Timestamp};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Nit: aliasing SignedDuration as Duration (here and in tests/sqlite_storage.rs) is confusing next to std::time::Duration, which River's public API uses for delays. Using SignedDuration by name would avoid that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Left these aliases unchanged. The alias itself is new, but both test files already used Duration for Chrono's signed duration type. Keeping that local naming convention avoids unrelated test churn; neither file imports std::time::Duration under the same name.

@bgentry

bgentry commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Coverage gaps worth closing, beyond the inline notes:

  • SQLite encoding at Timestamp::MIN/MAX (MAX currently panics).
  • Range-edge arithmetic in the rescuer's retry delay, the cleaner's job and queue retention horizons, and the reindexer's Interval, asserting that they saturate instead of panicking.
  • cancel_attempted rejecting forms Go rejects (zone annotations, lowercase designators, offsets with seconds, a space separator).
  • Postgres decoding of a timestamptz past year 9999 or infinity, asserting a decode error rather than a panic. Postgres can store years up to 294276, and the new docs say Rust can't read those.
  • next_after with a non-UTC reference zone combined with a schedule that has its own zone, checking which zone the result is in. Today that's only covered with a fixed offset.

@bgentry

bgentry commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

🤖 PR hygiene: the title has the [rust] prefix now, but the commit subject (Move from chrono (deprecated) to jiff) doesn't, which matters if the merge keeps the commit message. The description covers only the motivation. It would help reviewers and users to call out the behavior changes there too:

  • the timestamp range shrinks from about ±262k years to ±9999;
  • chrono-tz is renamed to tzdb;
  • CronTimeZone loses Copy;
  • next_after now takes Zoned.

Separately, the CHANGELOG entry is one long "Breaking" bullet; splitting it into a few bullets and stating the new range explicitly ("timestamps outside years -9999 to 9999 are rejected") would make it easier to scan.

It was called to my attention last week that it turns out that what's
been traditionally the main date/time library in Rust, chrono, is
actually soft deprecated [1]. The chrono maintainer recommends jiff [2]
as a replacement.

I figure that since River Rust is still quite new and River Pro for Rust
is still unreleased, it's the best time ever to make this jump. Although
annoying, jiff does seem to be very thoughtfully designed, with heavy
inspiration from Temporal in JavaScript.

[1] chronotope/chrono#1768
[2] https://crates.io/crates/jiff
Handle SQLite timestamp boundaries without panicking, preserve stored
negative-year timestamps, and retain column names in unexpected-NULL errors.
Saturate retry, retention, and reindex arithmetic at Jiff's timestamp bounds.
Reject newly accepted Temporal syntax in cancellation timestamps while
preserving Chrono's existing parsing leniency.

Make jiff-sqlx optional and Postgres-only. Add boundary and decode-error
coverage, clarify go_time's behavior and the breaking changelog entries,
and move the Chrono migration guide out of the README into a separate
published documentation page.

Validation:
- make test/rust with Postgres and SQLite: 577 passed, 1 ignored
- make lint/rust
- make doc/rust
@brandur

brandur commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Follow-up on the coverage suggestions, addressed in e66b027:

  • Added SQLite MIN/MAX encoding and decoding coverage. This caught both the MAX rounding panic and a new failure to decode the existing negative-year format; both are fixed.
  • Added regression tests for rescuer retry delays, the shared job/queue retention horizon, and reindex intervals using values that fit Chrono's range but exceed Jiff's. Timestamp overflow now saturates.
  • Added cancellation tests rejecting the newly accepted annotations and expanded offset forms. Lowercase designators and a space separator were already accepted by Chrono, so tests preserve that preexisting behavior.
  • Added Postgres coverage for year 10000, infinity, and -infinity. These already return decode errors; no decoder change was needed.
  • Left the additional named-reference-zone case out. The non-UTC reference-zone behavior is already tested with a fixed offset, and named-zone scheduling has Go fixture coverage. No regression was identified that required extending that combination here.

Validation passed: make test/rust with Postgres and SQLite (577 passed, 1 ignored), make lint/rust, and make doc/rust.

@brandur

brandur commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Addressed the PR hygiene feedback. Both commits now start with [rust]: 01f6a4f and e66b027. The PR description now lists the timestamp-range change, chrono-tz → tzdb, loss of CronTimeZone: Copy, and the Zoned signature of next_after. The changelog entry is split into separate breaking-change bullets and explicitly states that timestamps outside years -9999 to 9999 are rejected, with Timestamp::MIN/MAX as the exact bounds.

The migration guide has also moved to a separate document, linked from the README and included in generated documentation.

@brandur
brandur requested a review from bgentry November 2, 2026 00:27
@brandur

brandur commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Thx!

@brandur
brandur merged commit 77e1985 into master Nov 2, 2026
67 of 68 checks passed
@brandur
brandur deleted the rust-jiff branch November 2, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants