Moving updated-at timestamps to unique millisecond times (#17489)

To allow the ability to have guaranteed-unique cursors, we make two
important updates:
* Add new updated_at_ms and created_at_ms columns that are in
millisecond precision
* Guarantee uniqueness -- if multiple items are inserted at the same
millisecond, bump the new one by one millisecond until it becomes unique

This lets us use single-number cursors for forwards and backwards paging
through resultsets and guarantee that the cursor is a fixed point to do
(timestamp > cursor) and get new items only.

This updated implementation is backwards-compatible since multiple
appservers can be running and won't handle the previous method well.
This commit is contained in:
David de Regt
2026-04-14 11:55:34 -04:00
committed by GitHub
Unverified
parent 61fe23159e
commit 4f2fc3e3fa
13 changed files with 470 additions and 167 deletions
+23 -7
View File
@@ -1,6 +1,5 @@
use anyhow::Result;
use chrono::DateTime;
use chrono::Timelike;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::openai_models::ReasoningEffort;
@@ -300,7 +299,7 @@ impl ThreadMetadata {
}
fn canonicalize_datetime(dt: DateTime<Utc>) -> DateTime<Utc> {
dt.with_nanosecond(0).unwrap_or(dt)
epoch_millis_to_datetime(datetime_to_epoch_millis(dt)).unwrap_or(dt)
}
#[derive(Debug)]
@@ -389,8 +388,8 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
Ok(Self {
id: ThreadId::try_from(id)?,
rollout_path: PathBuf::from(rollout_path),
created_at: epoch_seconds_to_datetime(created_at)?,
updated_at: epoch_seconds_to_datetime(updated_at)?,
created_at: epoch_millis_to_datetime(created_at)?,
updated_at: epoch_millis_to_datetime(updated_at)?,
source,
agent_nickname,
agent_role,
@@ -423,13 +422,30 @@ pub(crate) fn anchor_from_item(item: &ThreadMetadata, sort_key: SortKey) -> Opti
Some(Anchor { ts, id })
}
pub(crate) fn datetime_to_epoch_millis(dt: DateTime<Utc>) -> i64 {
dt.timestamp_millis()
}
pub(crate) fn datetime_to_epoch_seconds(dt: DateTime<Utc>) -> i64 {
dt.timestamp()
}
pub(crate) fn epoch_seconds_to_datetime(secs: i64) -> Result<DateTime<Utc>> {
DateTime::<Utc>::from_timestamp(secs, 0)
.ok_or_else(|| anyhow::anyhow!("invalid unix timestamp: {secs}"))
pub(crate) fn epoch_millis_to_datetime(value: i64) -> Result<DateTime<Utc>> {
// Values older than 2020 if interpreted as milliseconds are legacy second-precision rows.
// Convert them in memory so old state DBs keep ordering correctly after new writes use ms.
const MIN_EPOCH_MILLIS: i64 = 1_577_836_800_000;
let millis = if value < MIN_EPOCH_MILLIS {
value.saturating_mul(1000)
} else {
value
};
DateTime::<Utc>::from_timestamp_millis(millis)
.ok_or_else(|| anyhow::anyhow!("invalid unix timestamp millis: {value}"))
}
pub(crate) fn epoch_seconds_to_datetime(value: i64) -> Result<DateTime<Utc>> {
DateTime::<Utc>::from_timestamp(value, 0)
.ok_or_else(|| anyhow::anyhow!("invalid unix timestamp seconds: {value}"))
}
/// Statistics about a backfill operation.