1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! `Store` SET-family write path: encoding pick (`Int` / `ArcBulk` /
//! inline `Str`), NX/XX guards, the F1 single-probe `maxmemory == 0`
//! fast path, and the v1.25 A.3 bio-drop hand-off of displaced values.
//! Split out of `string.rs` (GET family + INCR) to keep both under the
//! 500-LOC house cap; verbatim from before the move.
use crate::value::{BULK_THRESHOLD, SmallBytes, Value};
use crate::{Entry, Store, deadline_at, now_ns};
use crate::util::parse_canonical_i64;
use std::sync::Arc;
use std::time::Duration;
/// L2 + L1: pick the optimal encoding for `bytes` at SET time:
/// 1. Canonical i64 ASCII → `Value::Int(n)` (smallest + INCR fast path)
/// 2. > [`BULK_THRESHOLD`] bytes → `Value::ArcBulk(Arc<[u8]>)` (lets the
/// > reactor reply path borrow the bytes for `writev` zero-copy GET)
/// 3. Else → `Value::Str(SmallBytes::from_slice(bytes))` (inline-cache-
/// line storage, beats Arc indirection for small values)
#[inline]
fn pick_value_for_set(bytes: &[u8]) -> Value {
if let Some(n) = parse_canonical_i64(bytes) {
return Value::Int(n);
}
if bytes.len() > BULK_THRESHOLD {
return Value::ArcBulk(Arc::new(Box::<[u8]>::from(bytes)));
}
Value::Str(SmallBytes::from_slice(bytes))
}
#[inline]
pub(crate) fn pick_value_for_set_owned(bytes: Vec<u8>) -> Value {
if let Some(n) = parse_canonical_i64(&bytes) {
return Value::Int(n);
}
if bytes.len() > BULK_THRESHOLD {
// v1.29 Option A — `Arc::new(box)` is zero-copy when `len ==
// capacity` (shrink-to-fit no-ops). See `Value::ArcBulk` doc.
return Value::ArcBulk(Arc::new(bytes.into_boxed_slice()));
}
Value::Str(SmallBytes::from_vec(bytes))
}
impl Store {
/// `SET` — overwrites any existing value/type. NX/XX guards; clears TTL.
/// Takes an owned `Vec` so a >22 B value's allocation is adopted as-is
/// (no copy). For callers holding a borrowed slice, prefer
/// [`Self::set_slice`] — it skips the `to_vec` entirely for values that
/// inline.
pub fn set(
&mut self,
key: &[u8],
value: Vec<u8>,
expire: Option<Duration>,
nx: bool,
xx: bool,
) -> bool {
self.set_value(key, pick_value_for_set_owned(value), expire, nx, xx)
}
/// [`Self::set`] for a borrowed value. Values ≤ 22 B store inline in the
/// entry — zero allocator traffic, where `set(key, value.to_vec(), …)`
/// paid a malloc for the `Vec` and a free when the inline copy dropped
/// it (the dominant overwrite-SET pattern). Larger values pay the same
/// single allocation either way.
pub fn set_slice(
&mut self,
key: &[u8],
value: &[u8],
expire: Option<Duration>,
nx: bool,
xx: bool,
) -> bool {
self.set_value(key, pick_value_for_set(value), expire, nx, xx)
}
fn set_value(
&mut self,
key: &[u8],
new_value: Value,
expire: Option<Duration>,
nx: bool,
xx: bool,
) -> bool {
// F1 (v1.25): single-probe overwrite-SET fast path for default
// `maxmemory == 0` (the bench and production-common case). Goes
// through `kevy_map::RawEntryMut` (B.1, commit 1a9f9af) so the
// Occupied arm mutates the entry in place and returns owned
// (delta, ttl_delta) — no escaping reference, no second probe.
// Overwrite path drops from 2 probes (live_entry_mut: get+get_mut)
// to 1 probe. New-key + expired-removed paths still pay the
// insert_entry probe (same as before).
if self.maxmemory == 0 {
return self.set_value_no_evict(key, new_value, expire, nx, xx);
}
self.set_value_evict(key, new_value, expire, nx, xx)
}
/// Eviction path (maxmemory > 0) of [`Self::set_value`]: keeps the
/// 2-probe shape so `live_entry_mut`'s touch_on_access bookkeeping runs.
fn set_value_evict(
&mut self,
key: &[u8],
new_value: Value,
expire: Option<Duration>,
nx: bool,
xx: bool,
) -> bool {
let expire_at = expire.map(|d| deadline_at(now_ns(), d));
let key_heap = crate::key_heap_bytes_for(key);
#[allow(clippy::single_match_else)]
// Phase 1: in-place overwrite or remember we need to insert.
// The old value (if any) is taken via `mem::replace` so the bio
// hand-off in phase 2 happens AFTER `self.live_entry_mut`'s
// borrow on `self.map` is released — without splitting the
// borrow we couldn't call `self.maybe_offload_drop`.
let (outcome, old_value) = match self.live_entry_mut(key) {
Some(e) => {
if nx {
return false;
}
let (delta, ttl_delta, old) =
overwrite_in_place(e, new_value, expire_at, key_heap);
(Ok((delta, ttl_delta)), Some(old))
}
None => {
if xx {
return false;
}
(Err(Entry::new(new_value, expire_at)), None)
}
};
match outcome {
Ok((delta, ttl_delta)) => {
self.apply_weight_delta(delta);
self.adjust_expires(ttl_delta);
}
Err(entry) => {
self.insert_entry(SmallBytes::from_slice(key), entry);
}
}
// Phase 2: hand the old value off if heavy. Done last so the
// critical mutation + bookkeeping commit before any (sub-µs in
// steady state) channel send.
if let Some(old) = old_value {
self.maybe_offload_drop(old);
}
true
}
/// Single-probe overwrite-SET via `kevy_map::RawEntryMut` for the
/// `maxmemory == 0` fast path (F1, v1.25). Skips the `live_entry_mut`
/// 2-probe shape: Occupied arm mutates in place + returns owned
/// (delta, ttl_delta); Expired arm removes via raw-entry handle + falls
/// through to insert; Vacant arm goes to insert.
fn set_value_no_evict(
&mut self,
key: &[u8],
new_value: Value,
expire: Option<Duration>,
nx: bool,
xx: bool,
) -> bool {
let expire_at = expire.map(|d| deadline_at(now_ns(), d));
let key_heap = crate::key_heap_bytes_for(key);
// Hold new_value behind Option so the multi-arm consumption
// (overwrite arm vs insert-after-expired arm) is moved-once.
let mut value_slot = Some(new_value);
let outcome = self.set_probe_no_evict(key, &mut value_slot, expire_at, key_heap, nx, xx);
// Phase 2: bookkeeping + maybe insert. Borrow on self.map is gone.
let old_value: Option<Value> = match outcome {
SetOutcome::Refused { drop_first } => {
if let Some(old) = drop_first {
// No insert; the expired `Value` still needs
// to drop. Ship via the bio path on its way out.
self.maybe_offload_drop(old);
}
return false;
}
SetOutcome::Updated { delta, ttl_delta, old } => {
self.apply_weight_delta(delta);
self.adjust_expires(ttl_delta);
Some(old)
}
SetOutcome::ExpiredThenInsert { old } => {
let entry = Entry::new(value_slot.take().unwrap(), expire_at);
self.insert_entry(SmallBytes::from_slice(key), entry);
Some(old)
}
SetOutcome::NeedInsert => {
let entry = Entry::new(value_slot.take().unwrap(), expire_at);
self.insert_entry(SmallBytes::from_slice(key), entry);
None
}
};
// Phase 3: ship the displaced Value if any. Last so the keyspace
// commit precedes any (sub-µs steady-state) channel send.
if let Some(old) = old_value {
self.maybe_offload_drop(old);
}
true
}
/// Phase 1 of [`Self::set_value_no_evict`]: probe + decide. Four
/// outcomes — overwrite-finished (delta + ttl_delta + the displaced
/// old `Value` to maybe ship to the bio thread), an expired-removed
/// `Entry` whose `Value` ditto needs offload, needs-insert with no
/// prior value, or refused by an NX/XX guard.
#[inline]
fn set_probe_no_evict(
&mut self,
key: &[u8],
value_slot: &mut Option<Value>,
expire_at: Option<u64>,
key_heap: u64,
nx: bool,
xx: bool,
) -> SetOutcome {
use kevy_map::RawEntryMut;
let (uc, cn) = (self.cached_clock, self.cached_ns);
match self.map.raw_entry_mut(key) {
RawEntryMut::Occupied(mut occ) => {
// Decide via shared ref first so we don't touch the entry
// (avoiding a needless cache-line dirtying) on the
// is_expired+remove path.
let expired = occ.get().is_expired(uc, cn);
if expired {
let old = occ.remove();
// borrow on self.map released by remove(self).
self.note_expired_removed(&old);
if xx {
return SetOutcome::Refused { drop_first: Some(old.value) };
}
SetOutcome::ExpiredThenInsert { old: old.value }
} else {
if nx {
return SetOutcome::Refused { drop_first: None };
}
// v1.25 A.3: take the old Value before overwriting
// so phase 2 can hand it to the bio thread instead
// of dropping inline (the Axis I tail amplifier).
let (delta, ttl_delta, old) = overwrite_in_place(
occ.get_mut(),
value_slot.take().unwrap(),
expire_at,
key_heap,
);
SetOutcome::Updated { delta, ttl_delta, old }
}
}
RawEntryMut::Vacant(_) => {
if xx {
return SetOutcome::Refused { drop_first: None };
}
SetOutcome::NeedInsert
}
}
}
/// Bookkeeping for an entry removed because the SET probe found it
/// expired: subtract its weight, decrement the TTL gauge, bump the
/// expired counter.
#[inline]
fn note_expired_removed(&mut self, old: &Entry) {
self.used_memory = self
.used_memory
.saturating_sub(old.weight() + crate::value::ENTRY_OVERHEAD);
if old.expire_at_ns.is_some() {
self.adjust_expires(-1);
}
self.expired_keys_total = self.expired_keys_total.saturating_add(1);
}
}
/// Phase-1 verdict of the SET probe (see [`Store::set_probe_no_evict`]).
enum SetOutcome {
Updated { delta: i64, ttl_delta: i64, old: Value },
ExpiredThenInsert { old: Value },
NeedInsert,
/// An NX/XX guard stopped the write; `drop_first` carries an
/// expired-removed `Value` that still needs the bio-drop hand-off.
Refused { drop_first: Option<Value> },
}
/// Overwrite one live entry's value + TTL in place; returns
/// `(weight_delta, ttl_delta, displaced_old_value)`. Shared by the
/// eviction-path SET ([`Store::set_value`]) and the `maxmemory == 0`
/// single-probe SET ([`Store::set_probe_no_evict`]) — the displaced
/// `Value` is returned (v1.25 A.3) so the caller can hand it to the
/// bio thread AFTER the keyspace borrow is released rather than
/// dropping inline (the Drop of a `Value::ArcBulk` over the heap-heavy
/// threshold is the Axis I tail amplifier).
#[inline]
fn overwrite_in_place(
e: &mut Entry,
new_value: Value,
expire_at: Option<u64>,
key_heap: u64,
) -> (i64, i64, Value) {
let had_ttl = e.expire_at_ns.is_some();
let old = std::mem::replace(&mut e.value, new_value);
e.expire_at_ns = expire_at.and_then(crate::pack_deadline);
let new_w = key_heap + e.value.weight();
let delta = new_w as i64 - e.weight() as i64;
let ttl_delta = i64::from(e.expire_at_ns.is_some()) - i64::from(had_ttl);
e.set_weight(new_w);
(delta, ttl_delta, old)
}