1use crate::{ApiClientWrapper, ApiError, Result, dyn_err};
3use futures::{StreamExt, TryStreamExt, stream};
4use prost::Message;
5use std::{
6 collections::{HashMap, HashSet, VecDeque},
7 future::Future,
8};
9use tonic::Code;
10use xmtp_common::{RetryableError, retry_async};
11use xmtp_configuration::*;
12use xmtp_proto::{
13 api::grpc_status,
14 api_client::XmtpBackendClient,
15 backend_v1 as wire,
16 types::{
17 CanonicalEnvelope, Cursor, IncomingBatchLimits, OrderedEnvelopeBatch, Topic, TopicCursor,
18 },
19};
20
21pub const MAX_PUBLISH_CHUNKS_IN_FLIGHT: usize = 4;
22pub const MAX_READ_CHUNKS_IN_FLIGHT: usize = 4;
23
24#[derive(Debug)]
26pub struct OrderedQueryPage {
27 pub batches: Vec<OrderedEnvelopeBatch>,
29 pub has_more: bool,
31}
32
33#[derive(Clone, Debug)]
34struct PublishEnvelope {
35 envelope: wire::ClientEnvelope,
36 canonical: CanonicalEnvelope,
37 topic: Topic,
38}
39
40#[derive(Clone, Debug)]
42pub struct PublishUnit {
43 envelopes: Vec<PublishEnvelope>,
44}
45impl PublishUnit {
46 pub fn new_within(
50 envelopes: Vec<wire::ClientEnvelope>,
51 limits: &LimitsConfiguration,
52 ) -> Result<Self> {
53 if envelopes.is_empty() {
54 return Err(ApiError::InvalidRequest("empty publish unit"));
55 }
56 let envelopes = envelopes
57 .into_iter()
58 .map(|envelope| {
59 let parsed = xmtp_mls_validation::parse_envelope(envelope)?;
60 if parsed.canonical.bytes.len() > limits.max_envelope_bytes {
61 return Err(ApiError::EnvelopeTooLarge);
62 }
63 Ok(PublishEnvelope {
64 envelope: parsed.envelope,
65 canonical: parsed.canonical,
66 topic: parsed.topic,
67 })
68 })
69 .collect::<Result<Vec<_>>>()?;
70 let unit = Self { envelopes };
71 let mut measure = PublishMeasure::new(limits);
72 measure.add(&unit);
73 if !measure.fits() {
74 return Err(ApiError::UnitTooLarge);
75 }
76 Ok(unit)
77 }
78
79 pub fn single_within(
80 envelope: wire::ClientEnvelope,
81 limits: &LimitsConfiguration,
82 ) -> Result<Self> {
83 Self::new_within(vec![envelope], limits)
84 }
85
86 pub fn new(envelopes: Vec<wire::ClientEnvelope>) -> Result<Self> {
89 Self::new_within(envelopes, &LimitsConfiguration::default())
90 }
91
92 pub fn single(envelope: wire::ClientEnvelope) -> Result<Self> {
93 Self::new(vec![envelope])
94 }
95}
96
97pub(crate) fn request(units: &[PublishUnit]) -> wire::PublishRequest {
98 wire::PublishRequest {
99 envelopes: units
100 .iter()
101 .flat_map(|unit| unit.envelopes.iter().map(|e| e.envelope.clone()))
102 .collect(),
103 }
104}
105struct PublishMeasure<'a> {
107 bytes: usize,
108 topics: HashSet<&'a Topic>,
109 limits: &'a LimitsConfiguration,
110}
111impl<'a> PublishMeasure<'a> {
112 fn new(limits: &'a LimitsConfiguration) -> Self {
113 Self {
114 bytes: 0,
115 topics: HashSet::new(),
116 limits,
117 }
118 }
119
120 fn add(&mut self, unit: &'a PublishUnit) {
121 for envelope in &unit.envelopes {
122 let len = envelope.canonical.bytes.len();
123 self.bytes += 1 + prost::length_delimiter_len(len) + len;
124 self.topics.insert(&envelope.topic);
125 }
126 }
127
128 fn fits(&self) -> bool {
129 self.bytes <= self.limits.max_request_bytes
130 && self.topics.len() <= self.limits.max_publish_topics
131 }
132}
133
134pub fn chunk_publish_within<'a>(
137 units: &'a [PublishUnit],
138 limits: &LimitsConfiguration,
139) -> Result<Vec<&'a [PublishUnit]>> {
140 let mut chunks = Vec::new();
141 let mut start = 0;
142 let mut measure = PublishMeasure::new(limits);
143 for (end, unit) in units.iter().enumerate() {
144 measure.add(unit);
145 if !measure.fits() {
146 if start == end {
147 return Err(ApiError::UnitTooLarge);
148 }
149 chunks.push(&units[start..end]);
150 start = end;
151 measure = PublishMeasure::new(limits);
152 measure.add(unit);
153 if !measure.fits() {
154 return Err(ApiError::UnitTooLarge);
155 }
156 }
157 }
158 if start < units.len() {
159 chunks.push(&units[start..]);
160 }
161 Ok(chunks)
162}
163
164pub fn chunk_publish(units: &[PublishUnit]) -> Result<Vec<&[PublishUnit]>> {
166 chunk_publish_within(units, &LimitsConfiguration::default())
167}
168
169pub(crate) fn size_error(error: &(dyn std::error::Error + 'static)) -> bool {
170 let Some(status) = grpc_status(error) else {
171 return false;
172 };
173 match status.code() {
174 Code::OutOfRange | Code::ResourceExhausted => true,
175 Code::InvalidArgument => {
176 tonic_types::pb::Status::decode(status.details()).is_ok_and(|details| {
177 details.details.iter().any(|detail| {
178 detail.type_url == "type.googleapis.com/xmtp.backend.v1.PublishError"
179 && wire::PublishError::decode(detail.value.as_slice()).is_ok_and(|error| {
180 error.reason == wire::publish_error::Reason::TooLarge as i32
181 })
182 })
183 })
184 }
185 _ => false,
186 }
187}
188
189#[derive(Debug, thiserror::Error)]
191enum CallError<E> {
192 #[error(transparent)]
193 Resize(E),
194 #[error(transparent)]
195 Retry(E),
196}
197impl<E: RetryableError> RetryableError for CallError<E> {
198 fn is_retryable(&self) -> bool {
199 match self {
200 Self::Resize(_) => false,
201 Self::Retry(error) => error.is_retryable(),
202 }
203 }
204}
205
206impl<C> ApiClientWrapper<C> {
207 pub(crate) async fn retry_call<T, E, F, Fut>(
208 &self,
209 mut call: F,
210 resize: bool,
211 ) -> std::result::Result<T, E>
212 where
213 E: RetryableError + 'static,
214 F: FnMut() -> Fut,
215 Fut: Future<Output = std::result::Result<T, E>>,
216 {
217 retry_async!(
218 self.retry_strategy,
219 (async {
220 call().await.map_err(|error| {
221 if resize && size_error(&error) {
222 CallError::Resize(error)
223 } else {
224 CallError::Retry(error)
225 }
226 })
227 })
228 )
229 .map_err(|error| match error {
230 CallError::Resize(error) | CallError::Retry(error) => error,
231 })
232 }
233}
234
235impl<C: XmtpBackendClient> ApiClientWrapper<C> {
236 #[xmtp_common::rpc_span]
238 pub async fn publish_units(&self, units: Vec<PublishUnit>) -> Result<Vec<wire::EnvelopeMeta>> {
239 let chunks = chunk_publish_within(&units, self.limits())?
240 .into_iter()
241 .map(<[_]>::to_vec)
242 .collect::<Vec<_>>();
243 let mut responses: Vec<_> = stream::iter(chunks.into_iter().enumerate().map(
244 |(index, chunk)| async move {
245 Ok::<_, ApiError>((index, self.publish_chunk(&chunk).await?))
246 },
247 ))
248 .buffer_unordered(MAX_PUBLISH_CHUNKS_IN_FLIGHT)
249 .try_collect()
250 .await?;
251 responses.sort_by_key(|(index, _)| *index);
252 Ok(responses.into_iter().flat_map(|(_, metas)| metas).collect())
253 }
254
255 async fn publish_chunk(&self, units: &[PublishUnit]) -> Result<Vec<wire::EnvelopeMeta>> {
256 let mut pending = VecDeque::from([units]);
257 let mut metas = Vec::new();
258 while let Some(units) = pending.pop_front() {
259 let request = request(units);
260 match self
261 .retry_call(|| self.api_client.publish(request.clone()), units.len() > 1)
262 .await
263 {
264 Ok(response) => {
265 let expected: Vec<_> = units.iter().flat_map(|unit| &unit.envelopes).collect();
266 if response.envelope_metas.len() != expected.len() {
267 return Err(ApiError::InvalidResponse("publish metadata count"));
268 }
269 for (meta, envelope) in response.envelope_metas.iter().zip(expected) {
270 let (topic, _, _) =
271 xmtp_api_backend::envelope::metadata(meta, envelope.topic.kind())?;
272 if topic != envelope.topic {
273 return Err(ApiError::InvalidResponse("publish topic"));
274 }
275 }
276 metas.extend(response.envelope_metas);
277 }
278 Err(error) if size_error(&error) && units.len() > 1 => {
279 let (left, right) = units.split_at(units.len() / 2);
280 pending.push_front(right);
281 pending.push_front(left);
282 }
283 Err(error) => return Err(dyn_err(error)),
284 }
285 }
286 Ok(metas)
287 }
288
289 pub async fn query_ordered_page(
292 &self,
293 cursors: TopicCursor,
294 limit: u32,
295 limits: IncomingBatchLimits,
296 ) -> Result<OrderedQueryPage> {
297 if limit == 0
298 || limit as usize > self.limits().max_query_limit
299 || limits.max_rows == 0
300 || limits.max_bytes == 0
301 {
302 return Err(ApiError::InvalidRequest("query limit"));
303 }
304 let topics: Vec<_> = cursors.into_iter().collect();
305 let mut pending: VecDeque<_> = topics
306 .chunks(self.limits().max_query_topics)
307 .map(|topics| (topics.to_vec(), limit))
308 .collect();
309 let mut page = OrderedQueryPage {
310 batches: Vec::new(),
311 has_more: false,
312 };
313 let mut remaining = limits;
314 while let Some((topics, requested_limit)) = pending.pop_front() {
315 if remaining.max_rows == 0 || remaining.max_bytes == 0 {
316 page.has_more = true;
317 break;
318 }
319 let requested_limit =
320 requested_limit.min(remaining.max_rows.min(u32::MAX as usize) as u32);
321 let request = wire::QueryRequest {
322 queries: topics
323 .iter()
324 .map(|(topic, cursor)| wire::TopicQuery {
325 topic: Some(wire::Topic {
326 topic: topic.cloned_vec(),
327 }),
328 cursor: Some((*cursor).into()),
329 })
330 .collect(),
331 limit: requested_limit,
332 };
333 let response = match self
334 .retry_call(
335 || self.api_client.query(request.clone()),
336 requested_limit > 1 || topics.len() > 1,
337 )
338 .await
339 {
340 Ok(response) => response,
341 Err(error) if size_error(&error) && requested_limit > 1 => {
342 pending.push_front((topics, (requested_limit / 2).max(1)));
343 continue;
344 }
345 Err(error) if size_error(&error) && topics.len() > 1 => {
346 let (left, right) = topics.split_at(topics.len() / 2);
347 pending.push_front((right.to_vec(), requested_limit));
348 pending.push_front((left.to_vec(), requested_limit));
349 continue;
350 }
351 Err(error) => return Err(dyn_err(error)),
352 };
353 let has_more = response
354 .continuation
355 .ok_or(ApiError::InvalidResponse("query continuation"))?
356 .has_more;
357 if has_more && response.envelopes.is_empty() {
358 return Err(ApiError::InvalidResponse("query has more without progress"));
359 }
360 if response.envelopes.len() > requested_limit as usize {
361 return Err(ApiError::InvalidResponse("query response limit"));
362 }
363 let mut positions = topics.iter().cloned().collect();
364 let batches = match xmtp_api_backend::envelope::ordered_batches(
365 &mut positions,
366 response.envelopes,
367 remaining,
368 ) {
369 Ok(batches) => batches,
370 Err(xmtp_api_backend::envelope::EnvelopeError::Capacity) if requested_limit > 1 => {
371 pending.push_front((topics, (requested_limit / 2).max(1)));
372 continue;
373 }
374 Err(xmtp_api_backend::envelope::EnvelopeError::Capacity)
375 if !page.batches.is_empty() =>
376 {
377 page.has_more = true;
378 break;
379 }
380 Err(error) => return Err(error.into()),
381 };
382 for batch in &batches {
383 remaining.max_rows -= batch.envelopes.len();
384 remaining.max_bytes -= batch
385 .envelopes
386 .iter()
387 .map(Message::encoded_len)
388 .sum::<usize>();
389 }
390 page.has_more |= has_more;
391 page.batches.extend(batches);
392 }
393 Ok(page)
394 }
395
396 pub async fn newest_topic_cursors(&self, topics: Vec<Topic>) -> Result<TopicCursor> {
399 let mut cursors: TopicCursor = topics
400 .iter()
401 .cloned()
402 .map(|topic| (topic, Cursor(0)))
403 .collect();
404 for result in self.newest(topics, false).await? {
405 let topic = Topic::parse(
406 &result
407 .topic
408 .ok_or(ApiError::InvalidResponse("newest topic"))?
409 .topic,
410 )?;
411 let meta = result
412 .meta
413 .ok_or(ApiError::InvalidResponse("newest metadata"))?;
414 let (_, cursor, _) = xmtp_api_backend::envelope::metadata(&meta, topic.kind())?;
415 cursors.insert(topic, cursor);
416 }
417 Ok(cursors)
418 }
419
420 pub async fn query_all(
422 &self,
423 cursors: TopicCursor,
424 limit: u32,
425 ) -> Result<Vec<wire::ServerEnvelope>> {
426 if limit == 0 || limit as usize > self.limits().max_query_limit {
427 return Err(ApiError::InvalidRequest("query limit"));
428 }
429 let topics: Vec<_> = cursors.into_iter().collect();
430 let results: Vec<Vec<_>> = stream::iter(
431 topics
432 .chunks(self.limits().max_query_topics)
433 .map(<[_]>::to_vec)
434 .collect::<Vec<_>>()
435 .into_iter()
436 .map(|chunk| self.query_chunk(chunk, limit)),
437 )
438 .buffer_unordered(MAX_READ_CHUNKS_IN_FLIGHT)
439 .try_collect()
440 .await?;
441 Ok(results.into_iter().flatten().collect())
442 }
443
444 async fn query_chunk(
445 &self,
446 topics: Vec<(Topic, Cursor)>,
447 limit: u32,
448 ) -> Result<Vec<wire::ServerEnvelope>> {
449 let mut pending = VecDeque::from([(topics, limit)]);
450 let mut output = Vec::new();
451 while let Some((mut topics, mut limit)) = pending.pop_front() {
452 loop {
453 let request = wire::QueryRequest {
454 queries: topics
455 .iter()
456 .map(|(topic, cursor)| wire::TopicQuery {
457 topic: Some(wire::Topic {
458 topic: topic.cloned_vec(),
459 }),
460 cursor: Some((*cursor).into()),
461 })
462 .collect(),
463 limit,
464 };
465 match self
466 .retry_call(
467 || self.api_client.query(request.clone()),
468 limit > 1 || topics.len() > 1,
469 )
470 .await
471 {
472 Ok(response) => {
473 let has_more = response
474 .continuation
475 .ok_or(ApiError::InvalidResponse("query continuation"))?
476 .has_more;
477 let mut cursors: HashMap<_, _> = topics
478 .iter_mut()
479 .map(|(topic, cursor)| (topic.clone(), cursor))
480 .collect();
481 let mut advanced = false;
482 for envelope in &response.envelopes {
483 let meta = envelope
484 .meta
485 .as_ref()
486 .ok_or(ApiError::InvalidResponse("query metadata"))?;
487 let topic = Topic::parse(
488 &meta
489 .topic
490 .as_ref()
491 .ok_or(ApiError::InvalidResponse("query topic"))?
492 .topic,
493 )?;
494 let cursor = cursors
495 .get_mut(&topic)
496 .ok_or(ApiError::InvalidResponse("unrequested query topic"))?;
497 let sequence = meta
498 .cursor
499 .as_ref()
500 .ok_or(ApiError::InvalidResponse("query cursor"))?
501 .sequence_id;
502 if sequence <= cursor.0 || sequence > i64::MAX as u64 {
503 return Err(ApiError::InvalidResponse(
504 "query cursor did not advance",
505 ));
506 }
507 **cursor = Cursor(sequence);
508 advanced = true;
509 }
510 output.extend(response.envelopes);
511 if !has_more {
512 break;
513 }
514 if !advanced {
515 return Err(ApiError::InvalidResponse(
516 "query has more without progress",
517 ));
518 }
519 }
520 Err(error) if size_error(&error) && limit > 1 => {
521 limit = (limit / 2).max(1);
522 }
523 Err(error) if size_error(&error) && topics.len() > 1 => {
524 let right = topics.split_off(topics.len() / 2);
525 pending.push_front((right, limit));
526 }
527 Err(error) => return Err(dyn_err(error)),
528 }
529 }
530 }
531 Ok(output)
532 }
533
534 pub(crate) async fn newest(
535 &self,
536 topics: Vec<Topic>,
537 full: bool,
538 ) -> Result<Vec<wire::query_newest_response::Result>> {
539 let topics: Vec<_> = topics
540 .into_iter()
541 .collect::<HashSet<_>>()
542 .into_iter()
543 .collect();
544 let cap = if full {
545 self.limits().max_newest_full_topics
546 } else {
547 self.limits().max_newest_metadata_topics
548 };
549 let results: Vec<Vec<_>> = stream::iter(
550 topics
551 .chunks(cap)
552 .map(<[_]>::to_vec)
553 .collect::<Vec<_>>()
554 .into_iter()
555 .map(|chunk| async move { self.newest_chunk(&chunk, full).await }),
556 )
557 .buffer_unordered(MAX_READ_CHUNKS_IN_FLIGHT)
558 .try_collect()
559 .await?;
560 Ok(results.into_iter().flatten().collect())
561 }
562
563 async fn newest_chunk(
564 &self,
565 topics: &[Topic],
566 full: bool,
567 ) -> Result<Vec<wire::query_newest_response::Result>> {
568 let mut pending = VecDeque::from([topics]);
569 let mut output = Vec::new();
570 while let Some(topics) = pending.pop_front() {
571 let request = wire::QueryNewestRequest {
572 topics: topics
573 .iter()
574 .map(|topic| wire::Topic {
575 topic: topic.cloned_vec(),
576 })
577 .collect(),
578 include_full_envelope: full,
579 };
580 match self
581 .retry_call(
582 || self.api_client.query_newest(request.clone()),
583 topics.len() > 1,
584 )
585 .await
586 {
587 Ok(response) => {
588 let mut seen = HashSet::new();
589 for result in &response.results {
590 let topic = Topic::parse(
591 &result
592 .topic
593 .as_ref()
594 .ok_or(ApiError::InvalidResponse("newest topic"))?
595 .topic,
596 )?;
597 if !topics.contains(&topic) || !seen.insert(topic.clone()) {
598 return Err(ApiError::InvalidResponse(
599 "unrequested or duplicate newest topic",
600 ));
601 }
602 let meta = result
603 .meta
604 .as_ref()
605 .ok_or(ApiError::InvalidResponse("newest metadata"))?;
606 let (meta_topic, _, _) =
607 xmtp_api_backend::envelope::metadata(meta, topic.kind())?;
608 if meta_topic != topic || (full && result.envelope.is_none()) {
609 return Err(ApiError::InvalidResponse("newest envelope"));
610 }
611 }
612 output.extend(response.results);
613 }
614 Err(error) if size_error(&error) && topics.len() > 1 => {
615 let (left, right) = topics.split_at(topics.len() / 2);
616 pending.push_front(right);
617 pending.push_front(left);
618 }
619 Err(error) => return Err(dyn_err(error)),
620 }
621 }
622 Ok(output)
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use xmtp_mls_validation::test_utils::{
630 GroupMessageKind, group_message_envelope, inline_welcome_envelope,
631 };
632
633 #[xmtp_common::test(unwrap_try = true)]
634 fn running_publish_measure_matches_encoded_mixed_request() {
635 let units = [
636 PublishUnit::single(inline_welcome_envelope([1; 32]))?,
637 PublishUnit::new(vec![
638 group_message_envelope([2; 16], GroupMessageKind::Proposal, [0; 127]),
639 group_message_envelope([2; 16], GroupMessageKind::Commit, [0; 16384]),
640 ])?,
641 PublishUnit::single(inline_welcome_envelope([1; 32]))?,
642 PublishUnit::single(inline_welcome_envelope([3; 32]))?,
643 ];
644 let limits = LimitsConfiguration::default();
645 let mut measure = PublishMeasure::new(&limits);
646 for (index, unit) in units.iter().enumerate() {
647 measure.add(unit);
648 assert_eq!(measure.bytes, request(&units[..=index]).encoded_len());
649 }
650 assert_eq!(measure.topics.len(), 3);
651 }
652}