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