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