88// the Business Source License, use of this software will be governed
99// by the Apache License, Version 2.0.
1010
11- use anyhow:: Context ;
1211use bytes:: { Buf , BufMut } ;
1312
1413use restate_clock:: RoughTimestamp ;
@@ -130,11 +129,14 @@ impl KeyDecode for EntryId {
130129 }
131130}
132131
133- // RoughTimestamp is encoded as a u64 in big-endian order to enable forward compatibility
134- // with potential future higher-precision restate-epoch based timestamp.
132+ // The on-disk value is milliseconds-since-restate-epoch, quantized to whole
133+ // seconds by the current seconds-precision `RoughTimestamp`. Storing in the
134+ // millisecond domain keeps the format forward-compatible with a future
135+ // higher-precision ms timestamp using the same u64 slot — no migration, and
136+ // sort order is preserved across the boundary.
135137impl KeyEncode for RoughTimestamp {
136138 fn encode < B : BufMut > ( & self , target : & mut B ) {
137- target. put_u64 ( self . as_u32 ( ) as u64 ) ;
139+ target. put_u64 ( self . as_u32 ( ) as u64 * 1_000 ) ;
138140 }
139141
140142 fn serialized_length ( & self ) -> usize {
@@ -144,11 +146,12 @@ impl KeyEncode for RoughTimestamp {
144146
145147impl KeyDecode for RoughTimestamp {
146148 fn decode < B : Buf > ( source : & mut B ) -> crate :: Result < Self > {
147- let raw = source. get_u64 ( ) ;
148- Ok ( Self :: new (
149- raw. try_into ( )
150- . context ( "RoughTimestamp needs to fit into u32" ) ?,
151- ) )
149+ let raw_ms = source. get_u64 ( ) ;
150+ // Floor to seconds. Clamp so that forward-written ms values beyond
151+ // `RoughTimestamp`'s range saturate at MAX instead of truncating
152+ // on the `as u32` cast.
153+ let secs = ( raw_ms / 1_000 ) . min ( u32:: MAX as u64 ) as u32 ;
154+ Ok ( Self :: new ( secs) )
152155 }
153156}
154157
@@ -190,3 +193,55 @@ impl KeyEncode for EntryKey {
190193 Self :: serialized_length_fixed ( )
191194 }
192195}
196+
197+ #[ cfg( test) ]
198+ mod tests {
199+ use super :: * ;
200+
201+ #[ test]
202+ fn rough_timestamp_ms_codec ( ) {
203+ // Round-trip preserves seconds across the representable range, and
204+ // encoded bytes are always an exact multiple of 1_000 (i.e. the
205+ // encoder only ever emits whole-second ms values, so the `*1_000` /
206+ // `/1_000` pair is lossless for every `RoughTimestamp`).
207+ for & secs in & [ 0u32 , 1 , 100 , 1_000 , u32:: MAX / 2 , u32:: MAX - 1 ] {
208+ let mut buf = Vec :: new ( ) ;
209+ RoughTimestamp :: new ( secs) . encode ( & mut buf) ;
210+
211+ let raw = u64:: from_be_bytes ( buf. as_slice ( ) . try_into ( ) . unwrap ( ) ) ;
212+ assert_eq ! (
213+ raw % 1_000 ,
214+ 0 ,
215+ "encoded value must be a multiple of 1_000 (got {raw} for {secs}s)"
216+ ) ;
217+ assert_eq ! (
218+ raw,
219+ secs as u64 * 1_000 ,
220+ "encoded ms must equal seconds * 1_000"
221+ ) ;
222+
223+ let mut slice = buf. as_slice ( ) ;
224+ let decoded = RoughTimestamp :: decode ( & mut slice) . expect ( "decode ok" ) ;
225+ assert_eq ! ( decoded. as_u32( ) , secs, "round-trip failed for {secs}s" ) ;
226+ }
227+
228+ // Forward-compat: sub-second ms values floor to the correct second.
229+ let mut sub_second = Vec :: new ( ) ;
230+ sub_second. put_u64 ( 5_999 ) ;
231+ let mut slice = sub_second. as_slice ( ) ;
232+ assert_eq ! (
233+ RoughTimestamp :: decode( & mut slice) . unwrap( ) . as_u32( ) ,
234+ 5 ,
235+ "ms value should floor to whole seconds on decode"
236+ ) ;
237+
238+ // Forward-compat: ms values beyond RoughTimestamp::MAX clamp to MAX.
239+ let mut over_max = Vec :: new ( ) ;
240+ over_max. put_u64 ( u64:: MAX ) ;
241+ let mut slice = over_max. as_slice ( ) ;
242+ assert_eq ! (
243+ RoughTimestamp :: decode( & mut slice) . unwrap( ) ,
244+ RoughTimestamp :: MAX ,
245+ ) ;
246+ }
247+ }
0 commit comments