Skip to main content

rocketmq_client_rust/consumer/store/
remote_broker_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;
17use std::sync::Arc;
18
19use cheetah_string::CheetahString;
20use rocketmq_model::common::message::message_queue::MessageQueue;
21use rocketmq_model::common::mix_all;
22use rocketmq_protocol::protocol::header::namesrv::topic_operation_header::TopicRequestHeader;
23use rocketmq_protocol::protocol::header::query_consumer_offset_request_header::QueryConsumerOffsetRequestHeader;
24use rocketmq_protocol::protocol::header::update_consumer_offset_header::UpdateConsumerOffsetRequestHeader;
25use rocketmq_transport::api::RpcRequestHeader;
26use tokio::sync::Mutex;
27use tracing::error;
28use tracing::info;
29use tracing::warn;
30
31use crate::consumer::store::controllable_offset::ControllableOffset;
32use crate::consumer::store::offset_store::OffsetStoreTrait;
33use crate::consumer::store::read_offset_type::ReadOffsetType;
34use crate::factory::mq_client_instance::MQClientInstance;
35
36pub struct RemoteBrokerOffsetStore {
37    client_instance: Arc<MQClientInstance>,
38    group_name: CheetahString,
39    offset_table: Arc<Mutex<HashMap<MessageQueue, ControllableOffset>>>,
40    persisted_offset_table: Arc<Mutex<HashMap<MessageQueue, i64>>>,
41}
42
43impl RemoteBrokerOffsetStore {
44    pub fn new(client_instance: Arc<MQClientInstance>, group_name: CheetahString) -> Self {
45        Self {
46            client_instance,
47            group_name,
48            offset_table: Arc::new(Mutex::new(HashMap::with_capacity(64))),
49            persisted_offset_table: Arc::new(Mutex::new(HashMap::with_capacity(64))),
50        }
51    }
52
53    async fn offset_needs_persist(&self, mq: &MessageQueue, offset: i64) -> bool {
54        let persisted_offset_table = self.persisted_offset_table.lock().await;
55        match persisted_offset_table.get(mq) {
56            Some(&persisted_offset) => persisted_offset != offset,
57            None => true,
58        }
59    }
60
61    async fn mark_offset_persisted(&self, mq: &MessageQueue, offset: i64) {
62        let mut persisted_offset_table = self.persisted_offset_table.lock().await;
63        persisted_offset_table.insert(mq.clone(), offset);
64    }
65
66    async fn fetch_consume_offset_from_broker(&self, mq: &MessageQueue) -> crate::ClientResult<i64> {
67        let broker_name = self.client_instance.get_broker_name_from_message_queue(mq).await;
68        let mut find_broker_result = self
69            .client_instance
70            .find_broker_address_in_subscribe(&broker_name, mix_all::MASTER_ID, true)
71            .await;
72
73        if find_broker_result.is_none() {
74            self.client_instance
75                .update_topic_route_info_from_name_server_topic(mq.topic())
76                .await;
77            let broker_name = self.client_instance.get_broker_name_from_message_queue(mq).await;
78            find_broker_result = self
79                .client_instance
80                .find_broker_address_in_subscribe(&broker_name, mix_all::MASTER_ID, false)
81                .await;
82        }
83        if let Some(find_broker_result) = find_broker_result {
84            let request_header = QueryConsumerOffsetRequestHeader {
85                consumer_group: self.group_name.clone(),
86                topic: mq.topic().clone(),
87                queue_id: mq.queue_id(),
88                set_zero_if_not_found: None,
89                topic_request_header: Some(TopicRequestHeader {
90                    lo: None,
91                    rpc: Some(RpcRequestHeader {
92                        namespace: None,
93                        namespaced: None,
94                        broker_name: Some(mq.broker_name().clone()),
95                        oneway: None,
96                    }),
97                }),
98            };
99            let Some(mq_client_api_impl) = self.client_instance.mq_client_api_impl.load_full() else {
100                return Err(crate::ClientError::not_initialized("MQClientAPIImpl"));
101            };
102            mq_client_api_impl
103                .query_consumer_offset(find_broker_result.broker_addr.as_str(), request_header, 5_000)
104                .await
105        } else {
106            Err(mq_client_err!(format!("broker not found, {}", mq.broker_name())))
107        }
108    }
109}
110
111impl OffsetStoreTrait for RemoteBrokerOffsetStore {
112    async fn load(&self) -> crate::ClientResult<()> {
113        Ok(())
114    }
115
116    async fn update_offset(&self, mq: &MessageQueue, offset: i64, increase_only: bool) {
117        let mut offset_table = self.offset_table.lock().await;
118        let offset_old = offset_table
119            .entry(mq.clone())
120            .or_insert_with(|| ControllableOffset::new(offset));
121        if increase_only {
122            offset_old.update(offset, true);
123        } else {
124            offset_old.update_unconditionally(offset);
125        }
126    }
127
128    async fn update_and_freeze_offset(&self, mq: &MessageQueue, offset: i64) {
129        let mut offset_table = self.offset_table.lock().await;
130        offset_table
131            .entry(mq.clone())
132            .or_insert_with(|| ControllableOffset::new(offset))
133            .update_and_freeze(offset);
134    }
135
136    async fn read_offset(&self, mq: &MessageQueue, type_: ReadOffsetType) -> i64 {
137        match type_ {
138            ReadOffsetType::ReadFromMemory => {
139                let offset_table = self.offset_table.lock().await;
140                if let Some(offset) = offset_table.get(mq) {
141                    return offset.get_offset();
142                }
143                -1
144            }
145            ReadOffsetType::MemoryFirstThenStore => {
146                let memory_offset = {
147                    let offset_table = self.offset_table.lock().await;
148                    offset_table.get(mq).map(ControllableOffset::get_offset)
149                };
150                if let Some(offset) = memory_offset {
151                    return offset;
152                }
153                match self.fetch_consume_offset_from_broker(mq).await {
154                    Ok(value) => {
155                        self.update_offset(mq, value, false).await;
156                        value
157                    }
158                    Err(error)
159                        if error.broker_response_code()
160                            == Some(rocketmq_protocol::code::response_code::ResponseCode::QueryNotFound as i32) =>
161                    {
162                        -1
163                    }
164                    Err(_) => {
165                        warn!("fetchConsumeOffsetFromBroker exception: {:?}", mq);
166                        -2
167                    }
168                }
169            }
170            ReadOffsetType::ReadFromStore => match self.fetch_consume_offset_from_broker(mq).await {
171                Ok(value) => {
172                    self.update_offset(mq, value, false).await;
173                    value
174                }
175                Err(error)
176                    if error.broker_response_code()
177                        == Some(rocketmq_protocol::code::response_code::ResponseCode::QueryNotFound as i32) =>
178                {
179                    -1
180                }
181                Err(_) => {
182                    warn!("fetchConsumeOffsetFromBroker exception: {:?}", mq);
183                    -2
184                }
185            },
186        }
187    }
188
189    async fn persist_all(&self, mqs: &HashSet<MessageQueue>) {
190        if mqs.is_empty() {
191            return;
192        }
193        let mut unused_mq = HashSet::new();
194        let mut used_mq = Vec::new();
195        let mut offset_table = self.offset_table.lock().await;
196        for (mq, offset) in offset_table.iter_mut() {
197            if mqs.contains(mq) {
198                let offset = offset.get_offset();
199                used_mq.push((mq.clone(), offset));
200            } else {
201                unused_mq.insert(mq.clone());
202            }
203        }
204        for mq in &unused_mq {
205            offset_table.remove(mq);
206            info!("remove unused mq, {}, {}", mq, self.group_name);
207        }
208        drop(offset_table);
209        if !unused_mq.is_empty() {
210            let mut persisted_offset_table = self.persisted_offset_table.lock().await;
211            for mq in &unused_mq {
212                persisted_offset_table.remove(mq);
213            }
214        }
215
216        for (mq, offset) in used_mq {
217            if !self.offset_needs_persist(&mq, offset).await {
218                continue;
219            }
220            match self.update_consume_offset_to_broker(&mq, offset, true).await {
221                Ok(_) => {
222                    self.mark_offset_persisted(&mq, offset).await;
223                    info!(
224                        "[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",
225                        self.group_name, self.client_instance.client_id, mq, offset
226                    );
227                }
228                Err(e) => {
229                    error!("updateConsumeOffsetToBroker exception, {},{}", mq, e);
230                }
231            }
232        }
233    }
234
235    async fn persist(&self, mq: &MessageQueue) {
236        let offset_table = self.offset_table.lock().await;
237        let Some(offset) = offset_table.get(mq).map(ControllableOffset::get_offset) else {
238            return;
239        };
240        drop(offset_table);
241        if !self.offset_needs_persist(mq, offset).await {
242            return;
243        }
244        match self.update_consume_offset_to_broker(mq, offset, true).await {
245            Ok(_) => {
246                self.mark_offset_persisted(mq, offset).await;
247                info!(
248                    "[persist] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",
249                    self.group_name, self.client_instance.client_id, mq, offset
250                );
251            }
252            Err(e) => {
253                error!("updateConsumeOffsetToBroker exception, {},{}", mq, e);
254            }
255        }
256    }
257
258    async fn remove_offset(&self, mq: &MessageQueue) {
259        let offset_table_size = {
260            let mut offset_table = self.offset_table.lock().await;
261            offset_table.remove(mq);
262            offset_table.len()
263        };
264        let mut persisted_offset_table = self.persisted_offset_table.lock().await;
265        persisted_offset_table.remove(mq);
266        info!(
267            "remove unnecessary messageQueue offset. group={}, mq={}, offsetTableSize={}",
268            mq, self.group_name, offset_table_size
269        );
270    }
271
272    async fn clone_offset_table(&self, topic: &str) -> HashMap<MessageQueue, i64> {
273        let offset_table = self.offset_table.lock().await;
274        offset_table
275            .iter()
276            .filter(|(mq, _)| mq.topic_str() == topic)
277            .map(|(mq, offset)| (mq.clone(), offset.get_offset()))
278            .collect()
279    }
280
281    async fn update_consume_offset_to_broker(
282        &self,
283        mq: &MessageQueue,
284        offset: i64,
285        is_oneway: bool,
286    ) -> crate::ClientResult<()> {
287        let broker_name = self.client_instance.get_broker_name_from_message_queue(mq).await;
288        let mut find_broker_result = self
289            .client_instance
290            .find_broker_address_in_subscribe(&broker_name, mix_all::MASTER_ID, false)
291            .await;
292
293        if find_broker_result.is_none() {
294            self.client_instance
295                .update_topic_route_info_from_name_server_topic(mq.topic())
296                .await;
297            let broker_name = self.client_instance.get_broker_name_from_message_queue(mq).await;
298            find_broker_result = self
299                .client_instance
300                .find_broker_address_in_subscribe(&broker_name, mix_all::MASTER_ID, false)
301                .await;
302        }
303
304        if let Some(find_broker_result) = find_broker_result {
305            let request_header = UpdateConsumerOffsetRequestHeader {
306                consumer_group: self.group_name.clone(),
307                topic: mq.topic().clone(),
308                queue_id: mq.queue_id(),
309                commit_offset: offset,
310                topic_request_header: Some(TopicRequestHeader {
311                    lo: None,
312                    rpc: Some(RpcRequestHeader {
313                        namespace: None,
314                        namespaced: None,
315                        broker_name: Some(mq.broker_name().clone()),
316                        oneway: None,
317                    }),
318                }),
319            };
320            let Some(mq_client_api_impl) = self.client_instance.mq_client_api_impl.load_full() else {
321                return Err(crate::ClientError::not_initialized("MQClientAPIImpl"));
322            };
323            if is_oneway {
324                mq_client_api_impl
325                    .update_consumer_offset_oneway(find_broker_result.broker_addr.as_str(), request_header, 5_000)
326                    .await?;
327            } else {
328                mq_client_api_impl
329                    .update_consumer_offset(&find_broker_result.broker_addr, request_header, 5_000)
330                    .await?;
331            };
332            Ok(())
333        } else {
334            Err(mq_client_err!(format!("broker not found, {}", mq.broker_name())))
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::base::client_config::ClientConfig;
343
344    fn new_store() -> RemoteBrokerOffsetStore {
345        let runtime = crate::runtime::test_client_runtime("remote-broker-offset-store-test");
346        let client_instance = MQClientInstance::new_arc(
347            ClientConfig::default(),
348            0,
349            "remote-offset-coalescing-test",
350            None,
351            runtime.component("instance"),
352            runtime.telemetry_handle().clone(),
353            runtime.pool().request_future_holder(),
354        );
355        RemoteBrokerOffsetStore::new(client_instance, CheetahString::from_static_str("group-a"))
356    }
357
358    fn message_queue() -> MessageQueue {
359        MessageQueue::from_parts("topic-a", "broker-a", 0)
360    }
361
362    #[tokio::test]
363    async fn persisted_offset_coalescing_skips_only_identical_offsets() {
364        let store = new_store();
365        let mq = message_queue();
366
367        assert!(store.offset_needs_persist(&mq, 10).await);
368
369        store.mark_offset_persisted(&mq, 10).await;
370
371        assert!(!store.offset_needs_persist(&mq, 10).await);
372        assert!(store.offset_needs_persist(&mq, 9).await);
373        assert!(store.offset_needs_persist(&mq, 11).await);
374    }
375
376    #[tokio::test]
377    async fn remove_offset_clears_persisted_coalescing_state() {
378        let store = new_store();
379        let mq = message_queue();
380
381        store.mark_offset_persisted(&mq, 10).await;
382        assert!(!store.offset_needs_persist(&mq, 10).await);
383
384        store.remove_offset(&mq).await;
385
386        assert!(store.offset_needs_persist(&mq, 10).await);
387    }
388
389    #[tokio::test]
390    async fn read_from_memory_distinguishes_absent_queue_from_stored_zero_offset() {
391        let store = new_store();
392        let absent_mq = message_queue();
393        let zero_mq = MessageQueue::from_parts("topic-a", "broker-a", 1);
394
395        assert_eq!(store.read_offset(&absent_mq, ReadOffsetType::ReadFromMemory).await, -1);
396
397        store.update_offset(&zero_mq, 0, false).await;
398        assert_eq!(store.read_offset(&zero_mq, ReadOffsetType::ReadFromMemory).await, 0);
399    }
400
401    #[tokio::test]
402    async fn memory_first_then_store_returns_cached_value_without_network_fallback() {
403        let store = new_store();
404        let mq = message_queue();
405        store.update_offset(&mq, 42, false).await;
406
407        let offset = store.read_offset(&mq, ReadOffsetType::MemoryFirstThenStore).await;
408
409        assert_eq!(offset, 42);
410    }
411
412    #[tokio::test]
413    async fn clone_offset_table_filters_by_exact_topic_with_overlapping_broker_and_queue_ids() {
414        let store = new_store();
415        let topic_a_q0 = MessageQueue::from_parts("topic-a", "broker-a", 0);
416        let topic_b_q0 = MessageQueue::from_parts("topic-b", "broker-a", 0);
417
418        store.update_offset(&topic_a_q0, 5, false).await;
419        store.update_offset(&topic_b_q0, 9, false).await;
420
421        let table = store.clone_offset_table("topic-a").await;
422
423        assert_eq!(table.len(), 1);
424        assert_eq!(table.get(&topic_a_q0), Some(&5));
425
426        let missing = store.clone_offset_table("topic-missing").await;
427        assert!(missing.is_empty());
428    }
429
430    #[tokio::test]
431    async fn clone_offset_table_snapshot_is_isolated_from_later_store_mutations() {
432        let store = new_store();
433        let mq = message_queue();
434        store.update_offset(&mq, 1, false).await;
435
436        let mut snapshot = store.clone_offset_table("topic-a").await;
437        snapshot.insert(mq.clone(), 999);
438        assert_eq!(store.read_offset(&mq, ReadOffsetType::ReadFromMemory).await, 1);
439
440        let snapshot_before_mutation = store.clone_offset_table("topic-a").await;
441        store.update_offset(&mq, 2, false).await;
442        store.remove_offset(&mq).await;
443
444        assert_eq!(snapshot_before_mutation.get(&mq), Some(&1));
445    }
446}