Skip to content

Commit 2f6c9e4

Browse files
Address PR review: improve error context, guards, and testability
- Pass store_name to serialize_entry for meaningful error messages - Add attempt number to upsert_partner_id race-retry log - Add explicit backwards-timestamp guard in update_last_seen (previously absorbed silently into debounce check) - Reuse store handle and serialized data in create_or_revive CAS loop instead of re-opening and re-serializing - Add comment explaining intentional dual store handles in upsert_partner_id (get() opens its own; outer is for writes) - Document tombstone created-field reset as intentional design - Add PartialEq derive to all schema types for test ergonomics
1 parent 735e38c commit 2f6c9e4

2 files changed

Lines changed: 43 additions & 21 deletions

File tree

crates/trusted-server-core/src/ec/kv.rs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,18 @@ impl KvIdentityGraph {
7272
}
7373

7474
/// Serializes an entry body and metadata for insertion.
75-
fn serialize_entry(entry: &KvEntry) -> Result<(String, String), Report<TrustedServerError>> {
75+
fn serialize_entry(
76+
entry: &KvEntry,
77+
store_name: &str,
78+
) -> Result<(String, String), Report<TrustedServerError>> {
7679
let body = serde_json::to_string(entry).change_context(TrustedServerError::KvStore {
77-
store_name: String::new(),
80+
store_name: store_name.to_owned(),
7881
message: "Failed to serialize KV entry body".to_owned(),
7982
})?;
8083
let meta = KvMetadata::from_entry(entry);
8184
let meta_str =
8285
serde_json::to_string(&meta).change_context(TrustedServerError::KvStore {
83-
store_name: String::new(),
86+
store_name: store_name.to_owned(),
8487
message: "Failed to serialize KV entry metadata".to_owned(),
8588
})?;
8689
Ok((body, meta_str))
@@ -169,7 +172,7 @@ impl KvIdentityGraph {
169172
/// key already exists (`ItemPreconditionFailed`).
170173
pub fn create(&self, ec_hash: &str, entry: &KvEntry) -> Result<(), Report<TrustedServerError>> {
171174
let store = self.open_store()?;
172-
let (body, meta_str) = Self::serialize_entry(entry)?;
175+
let (body, meta_str) = Self::serialize_entry(entry, &self.store_name)?;
173176
let created = Self::try_insert_add(&store, ec_hash, &body, &meta_str, &self.store_name)?;
174177
if created {
175178
Ok(())
@@ -231,9 +234,11 @@ impl KvIdentityGraph {
231234
ec_hash: &str,
232235
entry: &KvEntry,
233236
) -> Result<(), Report<TrustedServerError>> {
234-
// Try create first — fast path for new entries.
237+
// Serialize once and reuse across the fast path and CAS loop.
235238
let store = self.open_store()?;
236-
let (body, meta_str) = Self::serialize_entry(entry)?;
239+
let (body, meta_str) = Self::serialize_entry(entry, &self.store_name)?;
240+
241+
// Try create first — fast path for new entries.
237242
if Self::try_insert_add(&store, ec_hash, &body, &meta_str, &self.store_name)? {
238243
return Ok(());
239244
}
@@ -253,8 +258,6 @@ impl KvIdentityGraph {
253258

254259
// Tombstone — CAS overwrite to revive.
255260
log::info!("create_or_revive: reviving tombstone for '{ec_hash}'");
256-
let store = self.open_store()?;
257-
let (body, meta_str) = Self::serialize_entry(entry)?;
258261

259262
let mut current_gen = generation;
260263
for attempt in 0..MAX_CAS_RETRIES {
@@ -325,6 +328,10 @@ impl KvIdentityGraph {
325328
uid: &str,
326329
synced: u64,
327330
) -> Result<(), Report<TrustedServerError>> {
331+
// Open store once for write operations. Note: `self.get()` opens
332+
// its own handle internally — this is intentional since `KVStore::open`
333+
// is a cheap name lookup, and keeping the read/write APIs independent
334+
// simplifies the method signatures.
328335
let store = self.open_store()?;
329336

330337
for attempt in 0..MAX_CAS_RETRIES {
@@ -336,7 +343,7 @@ impl KvIdentityGraph {
336343
"upsert_partner_id: no entry for '{ec_hash}', creating minimal entry"
337344
);
338345
let minimal = KvEntry::minimal(partner_id, uid, synced);
339-
let (min_body, min_meta) = Self::serialize_entry(&minimal)?;
346+
let (min_body, min_meta) = Self::serialize_entry(&minimal, &self.store_name)?;
340347
if Self::try_insert_add(
341348
&store,
342349
ec_hash,
@@ -348,7 +355,9 @@ impl KvIdentityGraph {
348355
}
349356
// Key appeared between get() and create — re-read on next iteration.
350357
log::debug!(
351-
"upsert_partner_id: minimal create raced for '{ec_hash}', retrying"
358+
"upsert_partner_id: minimal create raced for '{ec_hash}', retrying (attempt {}/{})",
359+
attempt + 1,
360+
MAX_CAS_RETRIES,
352361
);
353362
continue;
354363
}
@@ -377,7 +386,7 @@ impl KvIdentityGraph {
377386
},
378387
);
379388

380-
let (body, meta_str) = Self::serialize_entry(&entry)?;
389+
let (body, meta_str) = Self::serialize_entry(&entry, &self.store_name)?;
381390

382391
match store
383392
.build_insert()
@@ -448,18 +457,27 @@ impl KvIdentityGraph {
448457
return Ok(());
449458
}
450459

460+
// Guard against stale/out-of-order timestamps.
461+
if timestamp <= entry.last_seen {
462+
log::trace!(
463+
"update_last_seen: stale timestamp for '{ec_hash}' (stored={}, incoming={timestamp})",
464+
entry.last_seen,
465+
);
466+
return Ok(());
467+
}
468+
451469
// Debounce: skip if the stored value is recent enough.
452-
if timestamp.saturating_sub(entry.last_seen) < LAST_SEEN_DEBOUNCE_SECS {
470+
if timestamp - entry.last_seen < LAST_SEEN_DEBOUNCE_SECS {
453471
log::trace!(
454472
"update_last_seen: debounced for '{ec_hash}' (delta={}s)",
455-
timestamp.saturating_sub(entry.last_seen),
473+
timestamp - entry.last_seen,
456474
);
457475
return Ok(());
458476
}
459477

460478
entry.last_seen = timestamp;
461479
let store = self.open_store()?;
462-
let (body, meta_str) = Self::serialize_entry(&entry)?;
480+
let (body, meta_str) = Self::serialize_entry(&entry, &self.store_name)?;
463481

464482
store
465483
.build_insert()
@@ -493,7 +511,7 @@ impl KvIdentityGraph {
493511
) -> Result<(), Report<TrustedServerError>> {
494512
let store = self.open_store()?;
495513
let entry = KvEntry::tombstone(current_timestamp());
496-
let (body, meta_str) = Self::serialize_entry(&entry)?;
514+
let (body, meta_str) = Self::serialize_entry(&entry, &self.store_name)?;
497515

498516
store
499517
.build_insert()
@@ -557,7 +575,7 @@ mod tests {
557575
fn serialize_entry_produces_valid_json() {
558576
let entry = KvEntry::tombstone(1000);
559577
let (body, meta) =
560-
KvIdentityGraph::serialize_entry(&entry).expect("should serialize entry");
578+
KvIdentityGraph::serialize_entry(&entry, "test-store").expect("should serialize entry");
561579

562580
// Verify body is valid JSON.
563581
let _: KvEntry =

crates/trusted-server-core/src/ec/kv_types.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub const SCHEMA_VERSION: u8 = 1;
2020
///
2121
/// **KV key:** 64-character hex hash (the stable prefix from the EC ID).
2222
/// **KV value:** JSON-serialized `KvEntry` (max ~5KB).
23-
#[derive(Debug, Clone, Serialize, Deserialize)]
23+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2424
pub struct KvEntry {
2525
/// Schema version — always [`SCHEMA_VERSION`].
2626
pub v: u8,
@@ -41,7 +41,7 @@ pub struct KvEntry {
4141
}
4242

4343
/// Consent state within a KV entry.
44-
#[derive(Debug, Clone, Serialize, Deserialize)]
44+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4545
pub struct KvConsent {
4646
/// Raw TCF v2 consent string.
4747
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -56,7 +56,7 @@ pub struct KvConsent {
5656
}
5757

5858
/// Geo location within a KV entry.
59-
#[derive(Debug, Clone, Serialize, Deserialize)]
59+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6060
pub struct KvGeo {
6161
/// ISO 3166-1 alpha-2 country code (e.g. `"US"`).
6262
pub country: String,
@@ -66,7 +66,7 @@ pub struct KvGeo {
6666
}
6767

6868
/// A synced partner user ID within a KV entry.
69-
#[derive(Debug, Clone, Serialize, Deserialize)]
69+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7070
pub struct KvPartnerId {
7171
/// The partner's user identifier.
7272
pub uid: String,
@@ -78,7 +78,7 @@ pub struct KvPartnerId {
7878
///
7979
/// Fastly KV metadata is limited to 2048 bytes and can be read without
8080
/// streaming the full body. Used by batch sync for fast consent checks.
81-
#[derive(Debug, Clone, Serialize, Deserialize)]
81+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8282
pub struct KvMetadata {
8383
/// Mirrors [`KvConsent::ok`] — `false` means tombstone.
8484
pub ok: bool,
@@ -144,6 +144,10 @@ impl KvEntry {
144144
///
145145
/// Sets `consent.ok = false`, clears all partner IDs, and uses a
146146
/// placeholder geo. The caller should apply a 24-hour TTL when writing.
147+
///
148+
/// **Note:** The original `created` timestamp is intentionally not
149+
/// preserved — reading the existing entry first would add latency on
150+
/// the consent-withdrawal hot path, and the tombstone expires in 24h.
147151
#[must_use]
148152
pub fn tombstone(now: u64) -> Self {
149153
Self {

0 commit comments

Comments
 (0)