1use crate::error::LaserError;
2use crate::filters::FilterPreviewBuilder;
3use crate::filters::reader::{FilteredReaderBuilder, SourceIncarnation};
4use crate::laser::Laser;
5use crate::stream::Topic;
6use crate::stream::transport::ConsumerBuilder;
7use iggy::prelude::{ConsumerGroupClient, ConsumerGroupDetails, Identifier, IggyError};
8use laser_wire::authz::scoped_resource;
9use laser_wire::filter::{
10 CatalogPosition, ConsumerFilter, FilterBinding, FilterConsumer, FilterError, FilterErrorReason,
11 FilterGroupIdentity, FilterGroupRef, FilterHeader, FilterMutation, FilterRef,
12 FilterRevisionPage, FilterRevisionRef, FilterSource, FilterTestResult, GroupFilterSpec,
13};
14use laser_wire::validate::Validate;
15use std::sync::{Arc, Mutex};
16
17const GROUP_FILTER_LOOKUP_PAGE: u32 = 8;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub(crate) enum GroupTarget {
24 Name(String),
25 Id(u64),
26}
27
28#[derive(Clone)]
34pub struct ConsumerGroup {
35 topic: Topic,
36 target: GroupTarget,
37 configured_at: Arc<Mutex<Option<CatalogPosition>>>,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ConsumerGroupInfo {
48 pub id: u32,
50 pub name: String,
51 pub identity: FilterGroupIdentity,
54 pub filter: Option<FilterBinding>,
57}
58
59impl ConsumerGroup {
60 pub(crate) fn new(topic: Topic, target: GroupTarget) -> Self {
61 Self {
62 topic,
63 target,
64 configured_at: Arc::new(Mutex::new(None)),
65 }
66 }
67
68 pub fn name(&self) -> Option<&str> {
70 match &self.target {
71 GroupTarget::Name(name) => Some(name),
72 GroupTarget::Id(_) => None,
73 }
74 }
75
76 pub const fn id(&self) -> Option<u64> {
78 match self.target {
79 GroupTarget::Id(id) => Some(id),
80 GroupTarget::Name(_) => None,
81 }
82 }
83
84 pub const fn topic(&self) -> &Topic {
85 &self.topic
86 }
87
88 pub fn create(&self) -> CreateConsumerGroup {
92 CreateConsumerGroup {
93 group: self.clone(),
94 policy: None,
95 operation_id: None,
96 }
97 }
98
99 pub fn filter(&self) -> GroupFilter {
102 GroupFilter {
103 group: self.clone(),
104 }
105 }
106
107 pub fn consumer(&self) -> ConsumerBuilder {
113 ConsumerBuilder::group(self.clone())
114 }
115
116 pub fn reader(&self) -> Result<FilteredReaderBuilder<'_>, LaserError> {
124 self.reader_with(FilterRef::Group)
125 }
126
127 pub async fn info(&self) -> Result<ConsumerGroupInfo, LaserError> {
130 let details = self.native().await?;
131 let identity = self.identity_of(details.id).await?;
132 let filter = if self.laser().capabilities().await.filters.catalog {
133 self.filter().get().await?
134 } else {
135 None
136 };
137 if filter
138 .as_ref()
139 .is_some_and(|binding| binding.identity != identity)
140 {
141 return Err(FilterError::new(
142 FilterErrorReason::SourceChanged,
143 "the group incarnation changed while its policy was inspected",
144 )
145 .into());
146 }
147 Ok(ConsumerGroupInfo {
148 id: details.id,
149 name: details.name,
150 identity,
151 filter,
152 })
153 }
154
155 pub(crate) fn reader_with(
156 &self,
157 filter: FilterRef,
158 ) -> Result<FilteredReaderBuilder<'_>, LaserError> {
159 Ok(FilteredReaderBuilder::new(
160 self.laser(),
161 self.source()?,
162 self.selector(),
163 filter,
164 self.catalog_position(),
165 ))
166 }
167
168 pub(crate) async fn ensure_native(&self) -> Result<(), LaserError> {
170 let GroupTarget::Name(name) = &self.target else {
171 return Err(LaserError::Invalid(
172 "a consumer group is created by name, address it with consumer_group(name)"
173 .to_owned(),
174 ));
175 };
176 let stream = Identifier::named(self.topic.stream()?)?;
177 let topic = Identifier::named(self.topic.name())?;
178 match self
179 .laser()
180 .client()
181 .create_consumer_group(&stream, &topic, name)
182 .await
183 {
184 Ok(_) | Err(IggyError::ConsumerGroupNameAlreadyExists(..)) => Ok(()),
185 Err(error) => Err(error.into()),
186 }
187 }
188
189 pub(crate) fn laser(&self) -> &Laser {
190 self.topic.laser()
191 }
192
193 pub(crate) fn selector(&self) -> FilterConsumer {
194 match &self.target {
195 GroupTarget::Name(name) => FilterConsumer::Group(name.clone()),
196 GroupTarget::Id(id) => FilterConsumer::GroupId(*id),
197 }
198 }
199
200 pub(crate) fn source(&self) -> Result<FilterSource, LaserError> {
201 Ok(FilterSource {
202 stream: self.topic.stream()?.to_owned(),
203 topic: self.topic.name().to_owned(),
204 })
205 }
206
207 pub(crate) fn catalog_position(&self) -> Option<CatalogPosition> {
208 *self
209 .configured_at
210 .lock()
211 .unwrap_or_else(std::sync::PoisonError::into_inner)
212 }
213
214 pub(crate) async fn native_name(&self) -> Result<String, LaserError> {
215 match &self.target {
216 GroupTarget::Name(name) => Ok(name.clone()),
217 GroupTarget::Id(_) => Ok(self.native().await?.name),
218 }
219 }
220
221 fn remember_position(&self, position: Option<CatalogPosition>) {
222 let Some(position) = position else {
223 return;
224 };
225 let mut configured_at = self
226 .configured_at
227 .lock()
228 .unwrap_or_else(std::sync::PoisonError::into_inner);
229 if configured_at.is_none_or(|known| {
230 position.operation_id.is_some() && known.operation_id != position.operation_id
231 || known.partition_id != position.partition_id
232 || known.offset < position.offset
233 }) {
234 *configured_at = Some(position);
235 }
236 }
237
238 async fn group_ref(&self) -> Result<FilterGroupRef, LaserError> {
241 let source = self.source()?;
242 let group = match &self.target {
243 GroupTarget::Name(name) => name.clone(),
244 GroupTarget::Id(_) => self.native().await?.name,
245 };
246 Ok(FilterGroupRef {
247 stream: source.stream,
248 topic: source.topic,
249 group,
250 })
251 }
252
253 async fn native(&self) -> Result<ConsumerGroupDetails, LaserError> {
254 let stream = Identifier::named(self.topic.stream()?)?;
255 let topic = Identifier::named(self.topic.name())?;
256 let group = match &self.target {
257 GroupTarget::Name(name) => Identifier::named(name)?,
258 GroupTarget::Id(id) => Identifier::numeric(
259 u32::try_from(*id)
260 .map_err(|_| LaserError::Config("consumer group id exceeds 32 bits"))?,
261 )?,
262 };
263 self.laser()
264 .client()
265 .get_consumer_group(&stream, &topic, &group)
266 .await?
267 .ok_or_else(|| {
268 FilterError::new(
269 FilterErrorReason::NotFound,
270 format!("consumer group {} does not exist", self.selector()),
271 )
272 .into()
273 })
274 }
275
276 async fn identity_of(&self, group_id: u32) -> Result<FilterGroupIdentity, LaserError> {
277 let source = self.source()?;
278 Ok(SourceIncarnation::read(&self.laser().client(), &source)
279 .await?
280 .identity(u64::from(group_id)))
281 }
282}
283
284pub struct CreateConsumerGroup {
286 group: ConsumerGroup,
287 policy: Option<GroupFilterSpec>,
288 operation_id: Option<u128>,
289}
290
291impl CreateConsumerGroup {
292 #[must_use]
295 pub fn filter(mut self, filter: ConsumerFilter) -> Self {
296 self.policy = Some(GroupFilterSpec::Definition(filter));
297 self
298 }
299
300 #[must_use]
302 pub fn policy(mut self, policy: GroupFilterSpec) -> Self {
303 self.policy = Some(policy);
304 self
305 }
306
307 #[must_use]
311 pub const fn operation_id(mut self, operation_id: u128) -> Self {
312 self.operation_id = Some(operation_id);
313 self
314 }
315
316 pub async fn build(self) -> Result<ConsumerGroupInfo, LaserError> {
326 let group = self.group;
327 if let Some(policy) = &self.policy {
328 FilterMutation::ConfigureGroup {
329 group: group.group_ref().await?,
330 policy: policy.clone(),
331 expected_identity: None,
332 }
333 .validate()?;
334 group.laser().filters().catalog_capabilities().await?;
335 }
336 group.ensure_native().await?;
337 let details = group.native().await?;
338 let identity = group.identity_of(details.id).await?;
339 let filter = match self.policy {
340 Some(policy) => match group
341 .filter()
342 .configure_scoped_as(self.operation_id, policy, identity)
343 .await
344 {
345 Ok(binding) => Some(binding),
346 Err(source) => {
347 return Err(LaserError::ConsumerGroupSetup {
348 group_id: details.id,
349 name: details.name,
350 identity,
351 source: Box::new(source),
352 });
353 }
354 },
355 None if group.laser().capabilities().await.filters.catalog => {
356 group.filter().get().await?
357 }
358 None => None,
359 };
360 Ok(ConsumerGroupInfo {
361 id: details.id,
362 name: details.name,
363 identity,
364 filter,
365 })
366 }
367}
368
369pub struct GroupFilter {
372 group: ConsumerGroup,
373}
374
375impl GroupFilter {
376 pub async fn configure(&self, filter: ConsumerFilter) -> Result<FilterBinding, LaserError> {
382 self.configure_as(None, GroupFilterSpec::Definition(filter))
383 .await
384 }
385
386 pub async fn configure_with(
389 &self,
390 policy: GroupFilterSpec,
391 ) -> Result<FilterBinding, LaserError> {
392 self.configure_as(None, policy).await
393 }
394
395 pub async fn configure_as(
399 &self,
400 operation_id: Option<u128>,
401 policy: GroupFilterSpec,
402 ) -> Result<FilterBinding, LaserError> {
403 let details = self.group.native().await?;
404 let identity = self.group.identity_of(details.id).await?;
405 self.configure_scoped_as(operation_id, policy, identity)
406 .await
407 }
408
409 async fn configure_scoped_as(
410 &self,
411 operation_id: Option<u128>,
412 policy: GroupFilterSpec,
413 identity: FilterGroupIdentity,
414 ) -> Result<FilterBinding, LaserError> {
415 let group = self.group.group_ref().await?;
416 let (binding, position) = self
417 .group
418 .laser()
419 .filters()
420 .configure_group(group, policy, operation_id, identity)
421 .await?;
422 if binding.identity != identity {
423 return Err(FilterError::new(
424 FilterErrorReason::SourceChanged,
425 "configured binding identifies another group incarnation",
426 )
427 .into());
428 }
429 self.group.remember_position(position);
430 Ok(binding)
431 }
432
433 pub async fn get(&self) -> Result<Option<FilterBinding>, LaserError> {
435 self.group.laser().filters().catalog_capabilities().await?;
436 let details = self.group.native().await?;
437 let identity = self.group.identity_of(details.id).await?;
438 let group = self.group.group_ref().await?;
439 match self.group.laser().filters().binding(group).await {
440 Ok(binding) if binding.identity == identity => Ok(Some(binding)),
441 Ok(_) => Err(FilterError::new(
442 FilterErrorReason::SourceChanged,
443 "the group incarnation changed while its policy was inspected",
444 )
445 .into()),
446 Err(error) if error.is_not_found() => Ok(None),
447 Err(error) => Err(error),
448 }
449 }
450
451 pub async fn revisions(
453 &self,
454 page: u32,
455 page_size: u32,
456 ) -> Result<FilterRevisionPage, LaserError> {
457 let active = self.active().await?;
458 self.group
459 .laser()
460 .filters()
461 .revisions(active.filter_id, page, page_size)
462 .await
463 }
464
465 pub async fn revise(
469 &self,
470 expected_revision: u32,
471 filter: ConsumerFilter,
472 ) -> Result<FilterRevisionRef, LaserError> {
473 let active = self.active().await?;
474 self.group
475 .laser()
476 .filters()
477 .revise(active.filter_id, expected_revision, filter)
478 .await
479 }
480
481 pub async fn set_revision_enabled(
485 &self,
486 revision: u32,
487 enabled: bool,
488 ) -> Result<(), LaserError> {
489 let active = self.active().await?;
490 self.group
491 .laser()
492 .filters()
493 .set_revision_enabled(active.filter_id, revision, enabled)
494 .await
495 }
496
497 pub async fn delete(&self) -> Result<bool, LaserError> {
502 self.group.laser().filters().catalog_capabilities().await?;
503 let details = self.group.native().await?;
504 let identity = self.group.identity_of(details.id).await?;
505 let name = group_filter_name(&identity);
506 let scoped_name = scoped_resource(&self.group.source()?.stream, &name);
507 let filters = self.group.laser().filters();
508 let mut before_id = None;
509 loop {
510 let page = filters
511 .list_before(&name, before_id, GROUP_FILTER_LOOKUP_PAGE)
512 .await?;
513 let next = page.items.last().map(|summary| summary.id);
514 let count = page.items.len();
515 if let Some(own) = page
516 .items
517 .into_iter()
518 .find(|summary| summary.name == name || summary.name == scoped_name)
519 {
520 let position = filters.drop_filter(own.id).await?;
521 self.group.remember_position(position);
522 return Ok(true);
523 }
524 let Some(next) = next else {
525 return Ok(false);
526 };
527 if before_id.is_some_and(|before| next >= before) {
528 return Err(FilterError::new(
529 FilterErrorReason::CatalogUnavailable,
530 "the catalog lookup cursor did not advance",
531 )
532 .into());
533 }
534 if count as u64 >= u64::from(page.total) {
535 return Ok(false);
536 }
537 before_id = Some(next);
538 }
539 }
540
541 pub async fn release(&self) -> Result<FilterBinding, LaserError> {
544 let active = self.active().await?;
545 let (released, position) = self.group.laser().filters().release_group(active).await?;
546 self.group.remember_position(position);
547 Ok(released)
548 }
549
550 pub async fn preview(&self, partition_id: u32) -> Result<FilterPreviewBuilder<'_>, LaserError> {
554 let active = self.active().await?;
555 let source = self.group.source()?;
556 Ok(self.group.laser().filters().preview(
557 source.stream,
558 source.topic,
559 partition_id,
560 FilterRef::Revision {
561 filter_id: active.filter_id,
562 revision: active.revision,
563 },
564 ))
565 }
566
567 pub async fn test(
570 &self,
571 payload: impl Into<Vec<u8>>,
572 headers: Vec<FilterHeader>,
573 ) -> Result<FilterTestResult, LaserError> {
574 let active = self.active().await?;
575 self.group
576 .laser()
577 .filters()
578 .test(
579 FilterRef::Revision {
580 filter_id: active.filter_id,
581 revision: active.revision,
582 },
583 payload,
584 headers,
585 )
586 .await
587 }
588
589 async fn active(&self) -> Result<FilterBinding, LaserError> {
590 self.get().await?.ok_or_else(|| {
591 FilterError::new(
592 FilterErrorReason::NotFound,
593 "the consumer group has no filter policy",
594 )
595 .into()
596 })
597 }
598}
599
600fn group_filter_name(identity: &FilterGroupIdentity) -> String {
604 format!(
605 "group:{}:{}:{}:{}:{}",
606 identity.stream_id,
607 identity.stream_created_at_micros,
608 identity.topic_id,
609 identity.topic_created_at_micros,
610 identity.group_id
611 )
612}