Skip to main content

kevy_store/stream/
claim.rs

1//! `XCLAIM` / `XAUTOCLAIM` impls — split out of `stream/group.rs` so
2//! both files stay under the project's ≤500-LOC rule. Owns the
3//! `AutoclaimResult` return type alongside the methods that produce it.
4
5use super::group::{ConsumerGroup, ensure_consumer};
6use super::{EntryBatch, PelEntry, StreamData, StreamId, XClaimOpts};
7use crate::StoreError;
8#[cfg(not(feature = "std"))]
9use crate::nostd_prelude::*;
10use crate::value::SmallBytes;
11
12/// Snapshot of `XAUTOCLAIM` work in progress: cursor for the next
13/// call, IDs successfully transferred, and IDs skipped because the
14/// stream has since deleted them.
15#[derive(Debug)]
16pub struct AutoclaimResult {
17    /// Where the next `XAUTOCLAIM` should resume. `0-0` when the scan
18    /// reached the end of the pending list.
19    pub next_cursor: StreamId,
20    /// Entries transferred to the claiming consumer, in stream order.
21    pub claimed_ids: Vec<StreamId>,
22    /// Entries that were pending but no longer exist — deleted from the
23    /// stream while a consumer still held them. XAUTOCLAIM drops them from
24    /// the pending list and reports them here rather than claiming a
25    /// message with no body.
26    pub deleted_ids: Vec<StreamId>,
27}
28
29impl StreamData {
30    /// `XCLAIM key group consumer min-idle-ms id [id ...] [...]`.
31    /// Returns the IDs successfully claimed (the dispatcher decides
32    /// whether to emit JUSTID or full entries).
33    pub fn claim(
34        &mut self,
35        group: &[u8],
36        new_owner: &[u8],
37        ids: &[StreamId],
38        opts: &XClaimOpts,
39        now_ms: u64,
40    ) -> Result<Vec<StreamId>, StoreError> {
41        let Some(g) = self.groups.get_mut(group) else {
42            return Err(StoreError::NoSuchKey);
43        };
44        let new_owner_smb = SmallBytes::from_slice(new_owner);
45        ensure_consumer(g, &new_owner_smb, now_ms);
46        let mut claimed = Vec::new();
47        for id in ids {
48            if !claim_one(g, &self.entries, *id, &new_owner_smb, opts, now_ms) {
49                continue;
50            }
51            claimed.push(*id);
52        }
53        Ok(claimed)
54    }
55
56    /// `XAUTOCLAIM key group consumer min-idle-ms start [COUNT n]
57    /// [JUSTID]`. Walks the PEL from `start` onward, claiming the
58    /// first `count` entries whose idle ≥ `min_idle_ms`. Returns
59    /// `(next_cursor_id, claimed_ids, deleted_ids)`.
60    #[allow(clippy::too_many_arguments)]
61    pub fn autoclaim(
62        &mut self,
63        group: &[u8],
64        new_owner: &[u8],
65        min_idle_ms: u64,
66        start: StreamId,
67        count: usize,
68        justid: bool,
69        now_ms: u64,
70    ) -> Result<AutoclaimResult, StoreError> {
71        let opts = XClaimOpts {
72            min_idle_ms,
73            idle_override_ms: None,
74            time_override_ms: None,
75            retrycount_override: None,
76            force: false,
77            justid,
78        };
79        let candidates: Vec<StreamId> = {
80            let Some(g) = self.groups.get(group) else {
81                return Err(StoreError::NoSuchKey);
82            };
83            g.pel
84                .range(start..=StreamId::MAX)
85                .filter(|(_, p)| now_ms.saturating_sub(p.delivery_time_ms) >= min_idle_ms)
86                .take(count)
87                .map(|(id, _)| *id)
88                .collect()
89        };
90        let next_cursor = candidates.last().map_or(StreamId::MIN, |id| id.next());
91        let claimed = self.claim(group, new_owner, &candidates, &opts, now_ms)?;
92        let mut deleted = Vec::new();
93        for id in &candidates {
94            if !self.entries.contains_key(id) && !claimed.contains(id) {
95                deleted.push(*id);
96            }
97        }
98        Ok(AutoclaimResult { next_cursor, claimed_ids: claimed, deleted_ids: deleted })
99    }
100
101    /// Field-value payload list pairing with `ids` (from
102    /// [`Self::claim`] / [`Self::autoclaim`]). Skips IDs that were
103    /// XDELed between claim and emit.
104    pub fn payloads_for(&self, ids: &[StreamId]) -> EntryBatch {
105        ids.iter()
106            .filter_map(|id| {
107                self.entries
108                    .get(id)
109                    .map(|fv| (*id, fv.iter().map(|(f, v)| (f.to_vec(), v.to_vec())).collect()))
110            })
111            .collect()
112    }
113}
114
115/// Attempt one XCLAIM. Returns `true` if the entry was successfully
116/// transferred to `new_owner`. The `entries` ref is the stream's
117/// entry map (passed in to avoid an extra `&mut self` borrow when
118/// `claim` is called over a slice of IDs).
119fn claim_one(
120    g: &mut ConsumerGroup,
121    entries: &alloc::collections::BTreeMap<StreamId, Vec<(SmallBytes, SmallBytes)>>,
122    id: StreamId,
123    new_owner: &SmallBytes,
124    opts: &XClaimOpts,
125    now_ms: u64,
126) -> bool {
127    let entry_present = g.pel.contains_key(&id);
128    if !entry_present && !opts.force {
129        return false;
130    }
131    if !entries.contains_key(&id) {
132        if let Some(p) = g.pel.remove(&id)
133            && let Some(cs) = g.consumers.get_mut(p.consumer.as_slice())
134        {
135            cs.pel_count = cs.pel_count.saturating_sub(1);
136        }
137        return false;
138    }
139    if let Some(existing) = g.pel.get(&id) {
140        let idle = now_ms.saturating_sub(existing.delivery_time_ms);
141        if idle < opts.min_idle_ms {
142            return false;
143        }
144    }
145    let new_dt = opts
146        .time_override_ms
147        .or_else(|| opts.idle_override_ms.map(|i| now_ms.saturating_sub(i)))
148        .unwrap_or(now_ms);
149    let new_dc = opts.retrycount_override.unwrap_or_else(|| {
150        let base = g.pel.get(&id).map_or(0, |p| p.delivery_count);
151        if opts.justid { base.max(1) } else { base.saturating_add(1) }
152    });
153    let prev = g.pel.insert(
154        id,
155        PelEntry { consumer: new_owner.clone(), delivery_time_ms: new_dt, delivery_count: new_dc },
156    );
157    transfer_ownership_counts(g, prev.as_ref(), new_owner);
158    true
159}
160
161fn transfer_ownership_counts(
162    g: &mut ConsumerGroup,
163    prev: Option<&PelEntry>,
164    new_owner: &SmallBytes,
165) {
166    match prev {
167        Some(p) if p.consumer != *new_owner => {
168            if let Some(cs) = g.consumers.get_mut(p.consumer.as_slice()) {
169                cs.pel_count = cs.pel_count.saturating_sub(1);
170            }
171            if let Some(cs) = g.consumers.get_mut(new_owner.as_slice()) {
172                cs.pel_count = cs.pel_count.saturating_add(1);
173            }
174        }
175        Some(_) => {}
176        None => {
177            if let Some(cs) = g.consumers.get_mut(new_owner.as_slice()) {
178                cs.pel_count = cs.pel_count.saturating_add(1);
179            }
180        }
181    }
182}