Skip to main content

rocketmq_client_rust/consumer/store/
offset_store.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::collections::HashSet;
17#[cfg(test)]
18use std::sync::atomic::AtomicUsize;
19#[cfg(test)]
20use std::sync::atomic::Ordering;
21#[cfg(test)]
22use std::sync::Arc;
23#[cfg(test)]
24use std::sync::Mutex;
25use std::time::Duration;
26
27use rocketmq_model::common::message::message_queue::MessageQueue;
28
29use crate::consumer::store::local_file_offset_store::LocalFileOffsetStore;
30use crate::consumer::store::read_offset_type::ReadOffsetType;
31use crate::consumer::store::remote_broker_offset_store::RemoteBrokerOffsetStore;
32
33/// The active offset storage backend for a message consumer.
34///
35/// Exactly one variant is active during the lifetime of a consumer:
36/// - [`Remote`][OffsetStore::Remote] is used in clustering mode, where offsets are coordinated
37///   through the broker.
38/// - [`Local`][OffsetStore::Local] is used in broadcasting mode, where offsets are stored in a
39///   local file on the consumer host.
40pub enum OffsetStore {
41    /// Offsets persisted to and retrieved from the remote broker.
42    Remote(RemoteBrokerOffsetStore),
43    /// Offsets persisted to and retrieved from a local file.
44    Local(LocalFileOffsetStore),
45    #[cfg(test)]
46    Test(TestOffsetStore),
47}
48
49#[cfg(test)]
50#[derive(Clone, Default)]
51pub struct TestOffsetStore {
52    offsets: Arc<Mutex<HashMap<MessageQueue, i64>>>,
53    persisted_offsets: Arc<Mutex<HashMap<MessageQueue, i64>>>,
54    persist_all_count: Arc<AtomicUsize>,
55}
56
57impl OffsetStore {
58    /// Returns an [`OffsetStore`] backed by remote broker offset storage.
59    pub fn new_with_remote(remote_broker_offset_store: RemoteBrokerOffsetStore) -> Self {
60        Self::Remote(remote_broker_offset_store)
61    }
62
63    /// Returns an [`OffsetStore`] backed by local file offset storage.
64    pub fn new_with_local(local_file_offset_store: LocalFileOffsetStore) -> Self {
65        Self::Local(local_file_offset_store)
66    }
67
68    #[cfg(test)]
69    pub(crate) fn new_test() -> Self {
70        Self::Test(TestOffsetStore::default())
71    }
72
73    #[cfg(test)]
74    pub(crate) fn test_persist_all_count(&self) -> usize {
75        match self {
76            Self::Test(store) => store.persist_all_count.load(Ordering::Acquire),
77            Self::Remote(_) | Self::Local(_) => 0,
78        }
79    }
80
81    #[cfg(test)]
82    pub(crate) fn test_persisted_offset(&self, mq: &MessageQueue) -> Option<i64> {
83        match self {
84            Self::Test(store) => store.persisted_offset(mq),
85            Self::Remote(_) | Self::Local(_) => None,
86        }
87    }
88
89    /// Asynchronously loads persisted offsets from the underlying storage backend.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the underlying storage backend fails to read or deserialize
94    /// the persisted offset data.
95    pub async fn load(&self) -> crate::ClientResult<()> {
96        match self {
97            Self::Remote(store) => store.load().await,
98            Self::Local(store) => store.load().await,
99            #[cfg(test)]
100            Self::Test(_) => Ok(()),
101        }
102    }
103
104    /// Asynchronously updates the stored offset for the given queue.
105    ///
106    /// When `increase_only` is `true`, the offset is updated only if the new value is
107    /// greater than the currently stored value.
108    pub async fn update_offset(&self, mq: &MessageQueue, offset: i64, increase_only: bool) {
109        match self {
110            Self::Remote(store) => store.update_offset(mq, offset, increase_only).await,
111            Self::Local(store) => store.update_offset(mq, offset, increase_only).await,
112            #[cfg(test)]
113            Self::Test(store) => store.update_offset(mq, offset, increase_only),
114        }
115    }
116
117    /// Asynchronously updates the stored offset for the given queue and prevents further
118    /// updates until the freeze is lifted.
119    pub async fn update_and_freeze_offset(&self, mq: &MessageQueue, offset: i64) {
120        match self {
121            Self::Remote(store) => store.update_and_freeze_offset(mq, offset).await,
122            Self::Local(store) => store.update_and_freeze_offset(mq, offset).await,
123            #[cfg(test)]
124            Self::Test(store) => store.update_offset(mq, offset, false),
125        }
126    }
127
128    /// Asynchronously reads the current offset for the given queue according to the
129    /// specified read strategy.
130    ///
131    /// Returns `0` if no offset has been recorded for the queue.
132    pub async fn read_offset(&self, mq: &MessageQueue, type_: ReadOffsetType) -> i64 {
133        match self {
134            Self::Remote(store) => store.read_offset(mq, type_).await,
135            Self::Local(store) => store.read_offset(mq, type_).await,
136            #[cfg(test)]
137            Self::Test(store) => store.read_offset(mq),
138        }
139    }
140
141    /// Asynchronously persists the offsets for all queues in `mqs` to the underlying
142    /// storage backend.
143    pub async fn persist_all(&self, mqs: &HashSet<MessageQueue>) {
144        match self {
145            Self::Remote(store) => store.persist_all(mqs).await,
146            Self::Local(store) => store.persist_all(mqs).await,
147            #[cfg(test)]
148            Self::Test(store) => store.persist_all(mqs),
149        }
150    }
151
152    /// Asynchronously persists the offset for the specified queue to the underlying
153    /// storage backend.
154    pub async fn persist(&self, mq: &MessageQueue) {
155        match self {
156            Self::Remote(store) => store.persist(mq).await,
157            Self::Local(store) => store.persist(mq).await,
158            #[cfg(test)]
159            Self::Test(store) => store.persist(mq),
160        }
161    }
162
163    /// Asynchronously removes the stored offset entry for the specified queue.
164    pub async fn remove_offset(&self, mq: &MessageQueue) {
165        match self {
166            Self::Remote(store) => store.remove_offset(mq).await,
167            Self::Local(store) => store.remove_offset(mq).await,
168            #[cfg(test)]
169            Self::Test(store) => store.remove_offset(mq),
170        }
171    }
172
173    /// Asynchronously returns a snapshot of the offset table filtered by the given topic.
174    pub async fn clone_offset_table(&self, topic: &str) -> HashMap<MessageQueue, i64> {
175        match self {
176            Self::Remote(store) => store.clone_offset_table(topic).await,
177            Self::Local(store) => store.clone_offset_table(topic).await,
178            #[cfg(test)]
179            Self::Test(store) => store.clone_offset_table(topic),
180        }
181    }
182
183    /// Asynchronously pushes the given offset for the specified queue to the broker.
184    ///
185    /// When `is_oneway` is `true`, the request is sent without waiting for an acknowledgement.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the broker is unreachable or rejects the offset commit request.
190    pub async fn update_consume_offset_to_broker(
191        &self,
192        mq: &MessageQueue,
193        offset: i64,
194        is_oneway: bool,
195    ) -> crate::ClientResult<()> {
196        match self {
197            Self::Remote(store) => store.update_consume_offset_to_broker(mq, offset, is_oneway).await,
198            Self::Local(store) => store.update_consume_offset_to_broker(mq, offset, is_oneway).await,
199            #[cfg(test)]
200            Self::Test(store) => {
201                store.update_offset(mq, offset, false);
202                Ok(())
203            }
204        }
205    }
206
207    /// Gracefully shuts down any background resources owned by the offset store.
208    pub async fn shutdown(&self, timeout: Duration) -> bool {
209        match self {
210            Self::Remote(_) => true,
211            Self::Local(store) => store.shutdown_with_timeout(timeout).await,
212            #[cfg(test)]
213            Self::Test(_) => true,
214        }
215    }
216}
217
218#[cfg(test)]
219impl TestOffsetStore {
220    fn update_offset(&self, mq: &MessageQueue, offset: i64, increase_only: bool) {
221        let mut offsets = self.offsets.lock().expect("test offset store lock");
222        if increase_only {
223            offsets
224                .entry(mq.clone())
225                .and_modify(|current| {
226                    if offset > *current {
227                        *current = offset;
228                    }
229                })
230                .or_insert(offset);
231        } else {
232            offsets.insert(mq.clone(), offset);
233        }
234    }
235
236    fn read_offset(&self, mq: &MessageQueue) -> i64 {
237        self.offsets
238            .lock()
239            .expect("test offset store lock")
240            .get(mq)
241            .copied()
242            .unwrap_or(-1)
243    }
244
245    fn persist_all(&self, mqs: &HashSet<MessageQueue>) {
246        let offsets = self.offsets.lock().expect("test offset store lock");
247        let mut persisted_offsets = self.persisted_offsets.lock().expect("test persisted offset store lock");
248        for mq in mqs {
249            if let Some(offset) = offsets.get(mq) {
250                persisted_offsets.insert(mq.clone(), *offset);
251            }
252        }
253        self.persist_all_count.fetch_add(1, Ordering::AcqRel);
254    }
255
256    fn persist(&self, mq: &MessageQueue) {
257        self.persist_all(&HashSet::from([mq.clone()]));
258    }
259
260    fn remove_offset(&self, mq: &MessageQueue) {
261        self.offsets.lock().expect("test offset store lock").remove(mq);
262    }
263
264    fn persisted_offset(&self, mq: &MessageQueue) -> Option<i64> {
265        self.persisted_offsets
266            .lock()
267            .expect("test persisted offset store lock")
268            .get(mq)
269            .copied()
270    }
271
272    fn clone_offset_table(&self, topic: &str) -> HashMap<MessageQueue, i64> {
273        self.offsets
274            .lock()
275            .expect("test offset store lock")
276            .iter()
277            .filter(|(mq, _)| mq.topic() == topic)
278            .map(|(mq, offset)| (mq.clone(), *offset))
279            .collect()
280    }
281}
282
283/// Defines the contract for a consumer offset storage backend.
284///
285/// Implementations are responsible for loading, updating, persisting, and querying
286/// per-queue consume offsets. The two standard backends are
287/// [`RemoteBrokerOffsetStore`] and [`LocalFileOffsetStore`].
288pub(crate) trait OffsetStoreTrait {
289    /// Asynchronously loads persisted offsets from the underlying storage.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if the underlying storage backend fails to read or deserialize
294    /// the persisted offset data.
295    async fn load(&self) -> crate::ClientResult<()>;
296
297    /// Asynchronously updates the stored offset for the given queue.
298    ///
299    /// When `increase_only` is `true`, the offset is updated only if the new value is
300    /// greater than the currently stored value.
301    async fn update_offset(&self, mq: &MessageQueue, offset: i64, increase_only: bool);
302
303    /// Asynchronously updates the stored offset for the given queue and prevents further
304    /// updates until the freeze is lifted.
305    async fn update_and_freeze_offset(&self, mq: &MessageQueue, offset: i64);
306
307    /// Asynchronously reads the current offset for the given queue according to the
308    /// specified read strategy.
309    ///
310    /// Returns `0` if no offset has been recorded for the queue.
311    async fn read_offset(&self, mq: &MessageQueue, type_: ReadOffsetType) -> i64;
312
313    /// Asynchronously persists the offsets for all queues in `mqs` to the underlying storage.
314    async fn persist_all(&self, mqs: &HashSet<MessageQueue>);
315
316    /// Asynchronously persists the offset for the specified queue to the underlying storage.
317    async fn persist(&self, mq: &MessageQueue);
318
319    /// Asynchronously removes the stored offset entry for the specified queue.
320    async fn remove_offset(&self, mq: &MessageQueue);
321
322    /// Asynchronously returns a snapshot of the offset table filtered by the given topic.
323    async fn clone_offset_table(&self, topic: &str) -> HashMap<MessageQueue, i64>;
324
325    /// Asynchronously pushes the given offset for the specified queue to the broker.
326    ///
327    /// When `is_oneway` is `true`, the request is sent without waiting for an acknowledgement.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if the broker is unreachable or rejects the offset commit request.
332    async fn update_consume_offset_to_broker(
333        &self,
334        mq: &MessageQueue,
335        offset: i64,
336        is_oneway: bool,
337    ) -> crate::ClientResult<()>;
338}