1use crate::codes::FILTER_OP_VERSION;
2use crate::error::InvalidError;
3use crate::filter::catalog::CatalogPosition;
4use crate::filter::expr::ConsumerFilter;
5use crate::limits::{
6 MAX_FILTER_PREVIEW_EXAMINED, MAX_FILTER_PREVIEW_RECORDS, MAX_FILTER_SAMPLE_BYTES,
7 MAX_FILTER_SAMPLE_HEADERS, MAX_FILTER_SOURCE_NAME_BYTES, MAX_FILTERED_PAGE_BYTES,
8 MAX_FILTERED_PAGE_EXAMINED, MAX_FILTERED_PAGE_RECORDS, MAX_FILTERED_WAIT_CANCEL_IDS,
9 MAX_FILTERED_WAIT_MS, MAX_FILTERED_WAIT_PARTITIONS,
10};
11use crate::result::ResultCode;
12use crate::schema::Digest32;
13use crate::validate::Validate;
14use serde::ser::SerializeStruct;
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use std::collections::BTreeSet;
17use std::fmt;
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub struct FilterSource {
22 pub stream: String,
23 pub topic: String,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum FilterConsumer {
30 Consumer(String),
31 Group(String),
32 GroupId(u64),
33}
34
35impl fmt::Display for FilterConsumer {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::Consumer(name) | Self::Group(name) => formatter.write_str(name),
39 Self::GroupId(id) => write!(formatter, "{id}"),
40 }
41 }
42}
43
44impl FilterConsumer {
45 pub const fn is_group(&self) -> bool {
46 matches!(self, Self::Group(_) | Self::GroupId(_))
47 }
48}
49
50impl Serialize for FilterConsumer {
51 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
52 let mut state = serializer.serialize_struct("FilterConsumer", 2)?;
53 match self {
54 Self::Consumer(name) => {
55 state.serialize_field("kind", "consumer")?;
56 state.serialize_field("name", name)?;
57 }
58 Self::Group(name) => {
59 state.serialize_field("kind", "group")?;
60 state.serialize_field("name", name)?;
61 }
62 Self::GroupId(id) => {
63 state.serialize_field("kind", "group_id")?;
64 state.serialize_field("id", id)?;
65 }
66 }
67 state.end()
68 }
69}
70
71impl<'de> Deserialize<'de> for FilterConsumer {
72 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
73 #[derive(Deserialize)]
74 #[serde(tag = "kind", rename_all = "snake_case")]
75 enum Selector {
76 Consumer { name: String },
77 Group { name: String },
78 GroupId { id: u64 },
79 }
80 Ok(match Selector::deserialize(deserializer)? {
81 Selector::Consumer { name } => Self::Consumer(name),
82 Selector::Group { name } => Self::Group(name),
83 Selector::GroupId { id } => Self::GroupId(id),
84 })
85 }
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum FilterRef {
92 Inline(ConsumerFilter),
95 Revision { filter_id: u32, revision: u32 },
97 Bound,
100 Group,
104}
105
106#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum FilteredStart {
110 Next,
112 First,
113 Last,
114 Offset(u64),
115 Timestamp(u64),
117 Continue(Continuation),
119}
120
121#[derive(
125 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display,
126)]
127#[serde(rename_all = "snake_case")]
128#[strum(serialize_all = "snake_case")]
129pub enum ExecutionMode {
130 #[default]
131 Filtered,
132 Unfiltered,
133}
134
135impl ExecutionMode {
136 pub const fn is_filtered(&self) -> bool {
137 matches!(self, Self::Filtered)
138 }
139}
140
141fn validate_policy_identity(
143 mode: ExecutionMode,
144 digest: Option<&Digest32>,
145) -> Result<(), InvalidError> {
146 match (mode, digest) {
147 (ExecutionMode::Filtered, Some(digest)) => digest.validate(),
148 (ExecutionMode::Filtered, None) => {
149 Err(InvalidError::new("a filtered policy names its digest"))
150 }
151 (ExecutionMode::Unfiltered, None) => Ok(()),
152 (ExecutionMode::Unfiltered, Some(_)) => {
153 Err(InvalidError::new("an unfiltered policy carries no digest"))
154 }
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
163pub struct SourceGeneration {
164 pub stream_id: u32,
165 pub stream_created_at_micros: u64,
166 pub topic_id: u32,
167 pub topic_created_at_micros: u64,
168 pub partition_id: u32,
169 pub partition_incarnation: u64,
170}
171
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
176pub struct AckContext {
177 pub owner_generation: u64,
179 pub metadata_op: u64,
181}
182
183#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
185pub struct Continuation {
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub group_id: Option<u64>,
188 pub next_scan_offset: u64,
189 pub generation: SourceGeneration,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub digest: Option<Digest32>,
193 #[serde(default)]
194 pub mode: ExecutionMode,
195 #[serde(default)]
198 pub policy_generation: u64,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
207pub struct FilteredWaitRequest {
208 pub v: u32,
209 pub source: FilterSource,
210 pub consumer: FilterConsumer,
211 pub partitions: Vec<FilteredWaitPartition>,
212 pub max_wait_ms: u32,
213 pub wait_id: u64,
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219pub struct FilteredWaitCancel {
220 pub v: u32,
221 pub wait_ids: Vec<u64>,
222}
223
224#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
227pub struct FilteredWaitCancelled {
228 pub cancelled: Vec<u64>,
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub struct FilteredWaitPartition {
234 pub partition_id: u32,
235 pub offset: u64,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub struct FilteredWaitResult {
242 pub ready: Vec<u32>,
243}
244
245#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
247pub struct FilteredPollRequest {
248 pub v: u32,
249 pub source: FilterSource,
250 pub partition_id: u32,
251 pub consumer: FilterConsumer,
252 pub filter: FilterRef,
253 pub start: FilteredStart,
254 pub count: u32,
256 pub max_reply_bytes: u32,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub max_examined: Option<u32>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub min_catalog_position: Option<CatalogPosition>,
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
272#[serde(rename_all = "snake_case")]
273#[strum(serialize_all = "snake_case")]
274pub enum StopReason {
275 Filled,
277 Budget,
279 EndOfVisible,
281 Fault,
283 OversizedRecord,
285}
286
287#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
289#[serde(rename_all = "snake_case")]
290#[strum(serialize_all = "snake_case")]
291pub enum FaultReason {
292 MissingSchema,
294 SchemaNotAllowed,
296 SchemaMismatch,
298 Malformed,
300 TooLarge,
302 TooDeep,
304 ForeignCodec,
306 TypeMismatch,
308}
309
310impl FaultReason {
311 pub const fn is_foreign(self) -> bool {
314 matches!(
315 self,
316 FaultReason::ForeignCodec
317 | FaultReason::MissingSchema
318 | FaultReason::SchemaNotAllowed
319 | FaultReason::SchemaMismatch
320 )
321 }
322}
323
324#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
326pub struct RecordFault {
327 pub offset: u64,
328 pub reason: FaultReason,
329}
330
331#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
333pub struct FilterDecodeLimits {
334 pub max_payload_bytes: u32,
335 pub max_depth: u32,
336}
337
338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
340pub struct AppliedPolicy {
341 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub group_id: Option<u64>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub digest: Option<Digest32>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub filter_id: Option<u32>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
349 pub revision: Option<u32>,
350 #[serde(default)]
351 pub mode: ExecutionMode,
352 #[serde(default)]
354 pub policy_generation: u64,
355}
356
357impl AppliedPolicy {
358 #[must_use]
360 pub fn filtered(group_id: Option<u64>, digest: Digest32) -> Self {
361 Self {
362 group_id,
363 digest: Some(digest),
364 filter_id: None,
365 revision: None,
366 mode: ExecutionMode::Filtered,
367 policy_generation: 0,
368 }
369 }
370
371 #[must_use]
373 pub const fn unfiltered(group_id: u64, policy_generation: u64) -> Self {
374 Self {
375 group_id: Some(group_id),
376 digest: None,
377 filter_id: None,
378 revision: None,
379 mode: ExecutionMode::Unfiltered,
380 policy_generation,
381 }
382 }
383}
384
385impl Validate for AppliedPolicy {
386 fn validate(&self) -> Result<(), InvalidError> {
387 validate_policy_identity(self.mode, self.digest.as_ref())?;
388 if self.mode == ExecutionMode::Unfiltered {
389 if self.group_id.is_none() {
390 return Err(InvalidError::new("only a consumer group runs unfiltered"));
391 }
392 if self.filter_id.is_some() || self.revision.is_some() {
393 return Err(InvalidError::new(
394 "an unfiltered policy has no filter reference",
395 ));
396 }
397 }
398 Ok(())
399 }
400}
401
402#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
414pub struct FilteredPage {
415 pub v: u32,
416 pub partition_id: u32,
417 pub policy: AppliedPolicy,
418 pub generation: SourceGeneration,
419 pub ack_context: AckContext,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub next_scan_offset: Option<u64>,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub safe_ack_offset: Option<u64>,
424 pub frontier: u64,
427 pub examined: u32,
428 pub matched: u32,
429 pub stop: StopReason,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub fault: Option<RecordFault>,
432 #[serde(default, skip_serializing_if = "Vec::is_empty")]
435 pub unevaluated: Vec<RecordFault>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub evaluation_limits: Option<Box<FilterDecodeLimits>>,
438 #[serde(with = "crate::encoding::bin_bytes")]
439 pub records: Vec<u8>,
440}
441
442impl FilteredPage {
443 pub fn next_start(&self, original: &FilteredStart) -> FilteredStart {
446 match self.next_scan_offset {
447 Some(next_scan_offset) => FilteredStart::Continue(Continuation {
448 group_id: self.policy.group_id,
449 next_scan_offset,
450 generation: self.generation,
451 digest: self.policy.digest.clone(),
452 mode: self.policy.mode,
453 policy_generation: self.policy.policy_generation,
454 }),
455 None => original.clone(),
456 }
457 }
458}
459
460#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
464pub struct FilteredAck {
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub group_id: Option<u64>,
467 pub v: u32,
468 pub source: FilterSource,
469 pub partition_id: u32,
470 pub consumer: FilterConsumer,
471 pub generation: SourceGeneration,
472 pub ack_context: AckContext,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub digest: Option<Digest32>,
476 pub offset: u64,
477 #[serde(default)]
478 pub mode: ExecutionMode,
479 #[serde(default)]
482 pub policy_generation: u64,
483}
484
485#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
487pub struct AckReceipt {
488 pub partition_id: u32,
489 pub offset: u64,
490 pub generation: SourceGeneration,
491}
492
493#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
496pub struct FilterPreviewRequest {
497 pub v: u32,
498 pub source: FilterSource,
499 pub partition_id: u32,
500 pub filter: FilterRef,
502 pub from_offset: u64,
503 pub max_examined: u32,
504 pub max_records: u32,
505 #[serde(default)]
507 pub explain: bool,
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
512#[serde(rename_all = "snake_case")]
513#[strum(serialize_all = "snake_case")]
514pub enum Verdict {
515 Selected,
516 Rejected,
517 Fault,
518}
519
520#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
522pub struct PreviewRecord {
523 pub offset: u64,
524 pub timestamp_micros: u64,
525 pub verdict: Verdict,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub fault: Option<FaultReason>,
528 pub payload_text: String,
530 #[serde(default)]
531 pub payload_truncated: bool,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub explanation: Option<FilterExplanation>,
534}
535
536#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
538pub struct FilterPreview {
539 pub v: u32,
540 pub partition_id: u32,
541 pub policy: AppliedPolicy,
542 pub examined: u32,
543 pub matched: u32,
544 pub faults: u32,
545 pub stop: StopReason,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub next_offset: Option<u64>,
548 pub frontier: u64,
549 pub records: Vec<PreviewRecord>,
552}
553
554#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
557#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
558pub enum HeaderScalar {
559 Bool(bool),
560 Int(i64),
561 Uint(u64),
562 Float(f64),
563 String(String),
564 Raw(#[serde(with = "crate::encoding::bin_bytes")] Vec<u8>),
565}
566
567#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
569pub struct FilterHeader {
570 pub key: String,
571 pub value: HeaderScalar,
572}
573
574#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
577pub struct FilterTestRequest {
578 pub v: u32,
579 pub filter: FilterRef,
581 #[serde(with = "crate::encoding::bin_bytes")]
582 pub payload: Vec<u8>,
583 #[serde(default, skip_serializing_if = "Vec::is_empty")]
584 pub headers: Vec<FilterHeader>,
585}
586
587#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
589#[serde(rename_all = "snake_case")]
590#[strum(serialize_all = "snake_case")]
591pub enum Truth {
592 Match,
593 NoMatch,
594 Unknown,
595}
596
597#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
600pub struct ExplainNode {
601 pub label: String,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub truth: Option<Truth>,
604 #[serde(default, skip_serializing_if = "Vec::is_empty")]
605 pub children: Vec<ExplainNode>,
606}
607
608#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
610pub struct FilterExplanation {
611 pub verdict: Verdict,
612 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub fault: Option<FaultReason>,
614 pub root: ExplainNode,
615}
616
617#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
619pub struct FilterTestResult {
620 pub v: u32,
621 pub policy: AppliedPolicy,
622 pub explanation: FilterExplanation,
623}
624
625#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
627pub struct FilterValidation {
628 pub v: u32,
629 pub digest: Digest32,
630 pub reads_payload: bool,
632 pub reads_headers: bool,
634}
635
636#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
639#[serde(rename_all = "snake_case")]
640#[strum(serialize_all = "snake_case")]
641#[non_exhaustive]
642pub enum FilterErrorReason {
643 InvalidRequest,
645 Unsupported,
647 VersionSkew,
649 NotFound,
651 Conflict,
654 SourceChanged,
657 MembershipStale,
659 CatalogUnavailable,
661 RevisionDisabled,
663 TooLarge,
665 Unauthenticated,
666 Forbidden,
667 Unavailable,
669 CapacityExhausted,
671 Backend,
673 #[serde(other)]
676 Unknown,
677}
678
679impl FilterErrorReason {
680 pub const fn code(self) -> ResultCode {
681 match self {
682 Self::InvalidRequest => ResultCode::InvalidArgument,
683 Self::Unsupported => ResultCode::Unsupported,
684 Self::VersionSkew => ResultCode::VersionSkew,
685 Self::NotFound => ResultCode::NotFound,
686 Self::Conflict => ResultCode::Conflict,
687 Self::SourceChanged => ResultCode::StaleGeneration,
688 Self::MembershipStale => ResultCode::Stale,
689 Self::CatalogUnavailable | Self::Unavailable | Self::RevisionDisabled => {
690 ResultCode::Unavailable
691 }
692 Self::TooLarge => ResultCode::TooLarge,
693 Self::Unauthenticated => ResultCode::Unauthenticated,
694 Self::Forbidden => ResultCode::Forbidden,
695 Self::CapacityExhausted => ResultCode::ResourceLimit,
696 Self::Backend | Self::Unknown => ResultCode::Backend,
697 }
698 }
699}
700
701#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
703#[error("{reason}: {message}")]
704pub struct FilterError {
705 pub code: ResultCode,
706 pub reason: FilterErrorReason,
707 pub message: String,
708}
709
710impl FilterError {
711 pub fn new(reason: FilterErrorReason, message: impl Into<String>) -> Self {
712 Self {
713 code: reason.code(),
714 reason,
715 message: message.into(),
716 }
717 }
718
719 pub fn invalid(error: InvalidError) -> Self {
720 Self::new(FilterErrorReason::InvalidRequest, error.0)
721 }
722
723 pub fn check_version(v: u32) -> Result<(), Self> {
731 validate_version(v).map_err(|error| Self::new(FilterErrorReason::VersionSkew, error.0))
732 }
733}
734
735#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
737#[serde(rename_all = "snake_case")]
738pub enum FilterOutcome {
739 Page(FilteredPage),
740 Acknowledged(AckReceipt),
741 Preview(FilterPreview),
742 Tested(FilterTestResult),
743 Validated(FilterValidation),
744 Ready(FilteredWaitResult),
745 Cancelled(FilteredWaitCancelled),
746}
747
748#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
750#[serde(rename_all = "snake_case")]
751#[allow(clippy::large_enum_variant)]
753pub enum FilterReply {
754 Ok(FilterOutcome),
755 Err(FilterError),
756}
757
758fn validate_version(v: u32) -> Result<(), InvalidError> {
759 if v == FILTER_OP_VERSION {
760 Ok(())
761 } else {
762 Err(InvalidError::new(format!(
763 "filter request version {v} is not supported, expected {FILTER_OP_VERSION}"
764 )))
765 }
766}
767
768pub(crate) fn validate_name(label: &str, name: &str) -> Result<(), InvalidError> {
769 if name.is_empty() || name.len() > MAX_FILTER_SOURCE_NAME_BYTES {
770 return Err(InvalidError::new(format!(
771 "{label} must be 1..={MAX_FILTER_SOURCE_NAME_BYTES} bytes"
772 )));
773 }
774 Ok(())
775}
776
777impl Validate for FilterSource {
778 fn validate(&self) -> Result<(), InvalidError> {
779 validate_name("stream", &self.stream)?;
780 validate_name("topic", &self.topic)
781 }
782}
783
784impl Validate for FilterConsumer {
785 fn validate(&self) -> Result<(), InvalidError> {
786 match self {
787 Self::Consumer(name) => validate_name("consumer", name),
788 Self::Group(name) => validate_name("consumer group", name),
789 Self::GroupId(id) => u32::try_from(*id).map(|_| ()).map_err(|_| {
790 InvalidError::new("consumer group id exceeds the native offset-key range")
791 }),
792 }
793 }
794}
795
796impl Validate for FilterRef {
797 fn validate(&self) -> Result<(), InvalidError> {
798 match self {
799 Self::Inline(filter) => filter.validate(),
800 Self::Revision { .. } | Self::Bound | Self::Group => Ok(()),
801 }
802 }
803}
804
805impl Validate for FilteredWaitRequest {
806 fn validate(&self) -> Result<(), InvalidError> {
807 validate_version(self.v)?;
808 self.source.validate()?;
809 self.consumer.validate()?;
810 if self.partitions.is_empty() || self.partitions.len() > MAX_FILTERED_WAIT_PARTITIONS {
811 return Err(InvalidError::new(format!(
812 "a filtered wait names 1..={MAX_FILTERED_WAIT_PARTITIONS} partitions"
813 )));
814 }
815 let mut seen = BTreeSet::new();
816 if !self
817 .partitions
818 .iter()
819 .all(|partition| seen.insert(partition.partition_id))
820 {
821 return Err(InvalidError::new(
822 "a filtered wait names each partition once",
823 ));
824 }
825 if self.max_wait_ms > MAX_FILTERED_WAIT_MS {
826 return Err(InvalidError::new(format!(
827 "max_wait_ms must be 0..={MAX_FILTERED_WAIT_MS}"
828 )));
829 }
830 Ok(())
831 }
832}
833
834impl Validate for FilteredWaitCancel {
835 fn validate(&self) -> Result<(), InvalidError> {
836 validate_version(self.v)?;
837 if self.wait_ids.is_empty() || self.wait_ids.len() > MAX_FILTERED_WAIT_CANCEL_IDS {
838 return Err(InvalidError::new(format!(
839 "a filtered wait cancel names 1..={MAX_FILTERED_WAIT_CANCEL_IDS} waits"
840 )));
841 }
842 let mut seen = BTreeSet::new();
843 if !self.wait_ids.iter().all(|wait_id| seen.insert(*wait_id)) {
844 return Err(InvalidError::new(
845 "a filtered wait cancel names each wait once",
846 ));
847 }
848 Ok(())
849 }
850}
851
852impl Validate for FilteredPollRequest {
853 fn validate(&self) -> Result<(), InvalidError> {
854 validate_version(self.v)?;
855 self.source.validate()?;
856 self.consumer.validate()?;
857 self.filter.validate()?;
858 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) && !self.consumer.is_group() {
859 return Err(InvalidError::new(
860 "a `bound` or `group` filter needs a consumer group",
861 ));
862 }
863 if self.count == 0 || self.count > MAX_FILTERED_PAGE_RECORDS {
864 return Err(InvalidError::new(format!(
865 "count must be 1..={MAX_FILTERED_PAGE_RECORDS}"
866 )));
867 }
868 if let Some(max_examined) = self.max_examined
869 && (max_examined == 0 || max_examined > MAX_FILTERED_PAGE_EXAMINED)
870 {
871 return Err(InvalidError::new(format!(
872 "max_examined must be 1..={MAX_FILTERED_PAGE_EXAMINED}"
873 )));
874 }
875 if self.max_reply_bytes == 0 || self.max_reply_bytes > MAX_FILTERED_PAGE_BYTES {
876 return Err(InvalidError::new(format!(
877 "max_reply_bytes must be 1..={MAX_FILTERED_PAGE_BYTES}"
878 )));
879 }
880 if let FilteredStart::Continue(continuation) = &self.start {
881 validate_policy_identity(continuation.mode, continuation.digest.as_ref())?;
882 if continuation.mode == ExecutionMode::Unfiltered && !self.consumer.is_group() {
883 return Err(InvalidError::new("only a consumer group runs unfiltered"));
884 }
885 if self.consumer.is_group() != continuation.group_id.is_some() {
886 return Err(InvalidError::new(
887 "a group continuation must retain its group identity",
888 ));
889 }
890 if continuation.generation.partition_id != self.partition_id {
891 return Err(InvalidError::new(
892 "a continuation belongs to another partition",
893 ));
894 }
895 }
896 Ok(())
897 }
898}
899
900impl Validate for FilteredAck {
901 fn validate(&self) -> Result<(), InvalidError> {
902 validate_version(self.v)?;
903 self.source.validate()?;
904 self.consumer.validate()?;
905 validate_policy_identity(self.mode, self.digest.as_ref())?;
906 if self.mode == ExecutionMode::Unfiltered && !self.consumer.is_group() {
907 return Err(InvalidError::new("only a consumer group runs unfiltered"));
908 }
909 if self.consumer.is_group() != self.group_id.is_some() {
910 return Err(InvalidError::new(
911 "a group acknowledgment must retain its group identity",
912 ));
913 }
914 if self.generation.partition_id != self.partition_id {
915 return Err(InvalidError::new(
916 "an acknowledgment names another partition's generation",
917 ));
918 }
919 Ok(())
920 }
921}
922
923impl Validate for FilterPreviewRequest {
924 fn validate(&self) -> Result<(), InvalidError> {
925 validate_version(self.v)?;
926 self.source.validate()?;
927 self.filter.validate()?;
928 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) {
929 return Err(InvalidError::new(
930 "a preview executes an inline filter or a saved revision",
931 ));
932 }
933 if self.max_examined == 0 || self.max_examined > MAX_FILTER_PREVIEW_EXAMINED {
934 return Err(InvalidError::new(format!(
935 "max_examined must be 1..={MAX_FILTER_PREVIEW_EXAMINED}"
936 )));
937 }
938 if self.max_records == 0 || self.max_records > MAX_FILTER_PREVIEW_RECORDS {
939 return Err(InvalidError::new(format!(
940 "max_records must be 1..={MAX_FILTER_PREVIEW_RECORDS}"
941 )));
942 }
943 Ok(())
944 }
945}
946
947impl Validate for FilterTestRequest {
948 fn validate(&self) -> Result<(), InvalidError> {
949 validate_version(self.v)?;
950 self.filter.validate()?;
951 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) {
952 return Err(InvalidError::new(
953 "a sample test executes an inline filter or a saved revision",
954 ));
955 }
956 if self.payload.len() > MAX_FILTER_SAMPLE_BYTES {
957 return Err(InvalidError::new(format!(
958 "a sample payload exceeds {MAX_FILTER_SAMPLE_BYTES}B"
959 )));
960 }
961 if self.headers.len() > MAX_FILTER_SAMPLE_HEADERS {
962 return Err(InvalidError::new(format!(
963 "a sample carries more than {MAX_FILTER_SAMPLE_HEADERS} headers"
964 )));
965 }
966 for header in &self.headers {
967 validate_name("sample header key", &header.key)?;
968 let bytes = match &header.value {
969 HeaderScalar::String(value) => value.len(),
970 HeaderScalar::Raw(value) => value.len(),
971 _ => 0,
972 };
973 if bytes > crate::limits::MAX_FILTER_STRING_BYTES {
974 return Err(InvalidError::new(
975 "a sample header value exceeds its byte limit",
976 ));
977 }
978 }
979 Ok(())
980 }
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986 use crate::filter::expr::FilterExpr;
987 use crate::query::CmpOp;
988
989 fn wait_request() -> FilteredWaitRequest {
990 FilteredWaitRequest {
991 v: FILTER_OP_VERSION,
992 source: FilterSource {
993 stream: "orbit".to_owned(),
994 topic: "fleet".to_owned(),
995 },
996 consumer: FilterConsumer::Group("desk".to_owned()),
997 partitions: vec![
998 FilteredWaitPartition {
999 partition_id: 0,
1000 offset: 12,
1001 },
1002 FilteredWaitPartition {
1003 partition_id: 1,
1004 offset: 0,
1005 },
1006 ],
1007 max_wait_ms: 5_000,
1008 wait_id: 9,
1009 }
1010 }
1011
1012 #[test]
1013 fn given_a_wait_cancel_when_encoded_as_json_then_should_round_trip_with_its_reply() {
1014 let cancel = FilteredWaitCancel {
1015 v: FILTER_OP_VERSION,
1016 wait_ids: vec![9, 11],
1017 };
1018 cancel.validate().expect("valid");
1019 let json = serde_json::to_string(&cancel).expect("encodes");
1020 assert_eq!(
1021 serde_json::from_str::<FilteredWaitCancel>(&json).expect("decodes"),
1022 cancel
1023 );
1024 let cancelled = FilterReply::Ok(FilterOutcome::Cancelled(FilteredWaitCancelled {
1025 cancelled: vec![9],
1026 }));
1027 let json = serde_json::to_string(&cancelled).expect("encodes");
1028 assert_eq!(
1029 serde_json::from_str::<FilterReply>(&json).expect("decodes"),
1030 cancelled
1031 );
1032 }
1033
1034 #[test]
1035 fn given_wait_cancels_out_of_range_when_validated_then_should_be_rejected() {
1036 let cancel = |wait_ids: Vec<u64>| FilteredWaitCancel {
1037 v: FILTER_OP_VERSION,
1038 wait_ids,
1039 };
1040 let too_many = cancel((0..=MAX_FILTERED_WAIT_CANCEL_IDS as u64).collect());
1041 let old_version = FilteredWaitCancel {
1042 v: FILTER_OP_VERSION + 1,
1043 ..cancel(vec![1])
1044 };
1045 for request in [
1046 cancel(Vec::new()),
1047 cancel(vec![4, 4]),
1048 too_many,
1049 old_version,
1050 ] {
1051 assert!(request.validate().is_err(), "{request:?} must be rejected");
1052 }
1053 cancel((0..MAX_FILTERED_WAIT_CANCEL_IDS as u64).collect())
1054 .validate()
1055 .expect("the cap is accepted");
1056 }
1057
1058 #[test]
1059 fn given_a_wait_request_when_encoded_as_json_then_should_round_trip() {
1060 let request = wait_request();
1061 request.validate().expect("valid");
1062 let json = serde_json::to_string(&request).expect("encodes");
1063 assert_eq!(
1064 serde_json::from_str::<FilteredWaitRequest>(&json).expect("decodes"),
1065 request
1066 );
1067 let ready = FilterReply::Ok(FilterOutcome::Ready(FilteredWaitResult { ready: vec![1] }));
1068 let json = serde_json::to_string(&ready).expect("encodes");
1069 assert_eq!(
1070 serde_json::from_str::<FilterReply>(&json).expect("decodes"),
1071 ready
1072 );
1073 }
1074
1075 #[test]
1076 fn given_wait_requests_out_of_range_when_validated_then_should_be_rejected() {
1077 let no_partitions = FilteredWaitRequest {
1078 partitions: Vec::new(),
1079 ..wait_request()
1080 };
1081 let too_long = FilteredWaitRequest {
1082 max_wait_ms: MAX_FILTERED_WAIT_MS + 1,
1083 ..wait_request()
1084 };
1085 let mut repeated = wait_request();
1086 repeated.partitions[1].partition_id = 0;
1087 let too_many = FilteredWaitRequest {
1088 partitions: (0..=MAX_FILTERED_WAIT_PARTITIONS as u32)
1089 .map(|partition_id| FilteredWaitPartition {
1090 partition_id,
1091 offset: 0,
1092 })
1093 .collect(),
1094 ..wait_request()
1095 };
1096 for request in [no_partitions, too_long, repeated, too_many] {
1097 assert!(request.validate().is_err(), "{request:?} must be rejected");
1098 }
1099 FilteredWaitRequest {
1100 max_wait_ms: 0,
1101 ..wait_request()
1102 }
1103 .validate()
1104 .expect("a zero wait answers at once");
1105 }
1106
1107 fn generation() -> SourceGeneration {
1108 SourceGeneration {
1109 stream_id: 1,
1110 stream_created_at_micros: 10,
1111 topic_id: 2,
1112 topic_created_at_micros: 20,
1113 partition_id: 0,
1114 partition_incarnation: 30,
1115 }
1116 }
1117
1118 fn request() -> FilteredPollRequest {
1119 FilteredPollRequest {
1120 v: FILTER_OP_VERSION,
1121 source: FilterSource {
1122 stream: "orbit".to_owned(),
1123 topic: "fleet_changes".to_owned(),
1124 },
1125 partition_id: 0,
1126 consumer: FilterConsumer::Group("anomaly-desk".to_owned()),
1127 filter: FilterRef::Bound,
1128 start: FilteredStart::Next,
1129 count: 100,
1130 max_reply_bytes: 1024 * 1024,
1131 max_examined: None,
1132 min_catalog_position: None,
1133 }
1134 }
1135
1136 #[test]
1137 fn given_a_group_read_when_validated_then_should_pass() {
1138 request().validate().expect("valid");
1139 }
1140
1141 #[test]
1142 fn given_out_of_range_reads_when_validated_then_should_be_rejected() {
1143 let mut bound_consumer = request();
1144 bound_consumer.consumer = FilterConsumer::Consumer("one".to_owned());
1145 let mut zero = request();
1146 zero.count = 0;
1147 let mut huge = request();
1148 huge.max_reply_bytes = MAX_FILTERED_PAGE_BYTES + 1;
1149 let mut other_partition = request();
1150 other_partition.start = FilteredStart::Continue(Continuation {
1151 group_id: None,
1152 next_scan_offset: 7,
1153 generation: SourceGeneration {
1154 partition_id: 5,
1155 ..generation()
1156 },
1157 digest: Some(Digest32::new([1; 32])),
1158 mode: ExecutionMode::Filtered,
1159 policy_generation: 0,
1160 });
1161 let mut group_consumer = request();
1162 group_consumer.filter = FilterRef::Group;
1163 group_consumer.consumer = FilterConsumer::Consumer("one".to_owned());
1164 let mut over_examined = request();
1165 over_examined.max_examined = Some(MAX_FILTERED_PAGE_EXAMINED + 1);
1166 let mut zero_examined = request();
1167 zero_examined.max_examined = Some(0);
1168 let mut unfiltered_with_digest = request();
1169 unfiltered_with_digest.start = FilteredStart::Continue(Continuation {
1170 group_id: Some(3),
1171 next_scan_offset: 7,
1172 generation: generation(),
1173 digest: Some(Digest32::new([1; 32])),
1174 mode: ExecutionMode::Unfiltered,
1175 policy_generation: 1,
1176 });
1177 for request in [
1178 bound_consumer,
1179 zero,
1180 huge,
1181 other_partition,
1182 group_consumer,
1183 over_examined,
1184 zero_examined,
1185 unfiltered_with_digest,
1186 ] {
1187 assert!(request.validate().is_err(), "{request:?} must be rejected");
1188 }
1189 }
1190
1191 #[test]
1192 fn given_an_automatic_group_read_with_an_examined_bound_when_validated_then_should_pass() {
1193 let mut automatic = request();
1194 automatic.filter = FilterRef::Group;
1195 automatic.max_examined = Some(100);
1196 automatic.min_catalog_position = Some(CatalogPosition {
1197 partition_id: 0,
1198 offset: 41,
1199 operation_id: Some(11),
1200 revision: None,
1201 });
1202 automatic.validate().expect("valid");
1203 }
1204
1205 #[test]
1206 fn given_an_unfiltered_policy_when_validated_then_should_need_a_group_and_no_digest() {
1207 AppliedPolicy::unfiltered(3, 1).validate().expect("valid");
1208 assert!(
1209 AppliedPolicy::filtered(Some(3), Digest32::new([1; 32]))
1210 .validate()
1211 .is_ok()
1212 );
1213 let mut no_group = AppliedPolicy::unfiltered(3, 1);
1214 no_group.group_id = None;
1215 assert!(no_group.validate().is_err());
1216 let mut with_digest = AppliedPolicy::unfiltered(3, 1);
1217 with_digest.digest = Some(Digest32::new([1; 32]));
1218 assert!(with_digest.validate().is_err());
1219 let mut with_reference = AppliedPolicy::unfiltered(3, 1);
1220 with_reference.filter_id = Some(1);
1221 assert!(with_reference.validate().is_err());
1222 with_reference.filter_id = None;
1223 with_reference.revision = Some(1);
1224 assert!(with_reference.validate().is_err());
1225 let mut without_digest = AppliedPolicy::filtered(None, Digest32::new([1; 32]));
1226 without_digest.digest = None;
1227 assert!(without_digest.validate().is_err());
1228 }
1229
1230 #[test]
1231 fn given_a_page_when_asked_for_the_next_start_then_should_continue_or_repeat() {
1232 let page = FilteredPage {
1233 v: FILTER_OP_VERSION,
1234 partition_id: 0,
1235 policy: AppliedPolicy {
1236 group_id: None,
1237 digest: Some(Digest32::new([9; 32])),
1238 filter_id: Some(3),
1239 revision: Some(1),
1240 mode: ExecutionMode::Filtered,
1241 policy_generation: 0,
1242 },
1243 generation: generation(),
1244 ack_context: AckContext {
1245 owner_generation: 4,
1246 metadata_op: 40,
1247 },
1248 next_scan_offset: Some(1397),
1249 safe_ack_offset: Some(1396),
1250 frontier: 2000,
1251 examined: 397,
1252 matched: 3,
1253 stop: StopReason::Filled,
1254 fault: None,
1255 unevaluated: Vec::new(),
1256 evaluation_limits: None,
1257 records: Vec::new(),
1258 };
1259 assert_eq!(
1260 page.next_start(&FilteredStart::Next),
1261 FilteredStart::Continue(Continuation {
1262 group_id: None,
1263 next_scan_offset: 1397,
1264 generation: generation(),
1265 digest: Some(Digest32::new([9; 32])),
1266 mode: ExecutionMode::Filtered,
1267 policy_generation: 0,
1268 })
1269 );
1270 let empty = FilteredPage {
1271 next_scan_offset: None,
1272 safe_ack_offset: None,
1273 ..page
1274 };
1275 assert_eq!(empty.next_start(&FilteredStart::Last), FilteredStart::Last);
1276 }
1277
1278 #[test]
1279 fn given_error_reasons_when_classified_then_should_map_to_shared_codes() {
1280 assert_eq!(
1281 FilterError::new(FilterErrorReason::SourceChanged, "recreated").code,
1282 ResultCode::StaleGeneration
1283 );
1284 assert_eq!(FilterErrorReason::MembershipStale.code(), ResultCode::Stale);
1285 assert_eq!(FilterErrorReason::Conflict.code(), ResultCode::Conflict);
1286 assert_eq!(
1287 FilterErrorReason::CatalogUnavailable.code(),
1288 ResultCode::Unavailable
1289 );
1290 }
1291
1292 #[test]
1293 fn given_samples_and_previews_when_validated_then_should_reject_group_selectors() {
1294 let inline = FilterRef::Inline(ConsumerFilter::json(FilterExpr::pred(
1295 "kind",
1296 CmpOp::Eq,
1297 "satellites",
1298 )));
1299 let test = FilterTestRequest {
1300 v: FILTER_OP_VERSION,
1301 filter: inline.clone(),
1302 payload: br#"{"kind":"satellites"}"#.to_vec(),
1303 headers: Vec::new(),
1304 };
1305 test.validate().expect("valid sample");
1306 let mut preview = FilterPreviewRequest {
1307 v: FILTER_OP_VERSION,
1308 source: request().source,
1309 partition_id: 0,
1310 filter: inline,
1311 from_offset: 0,
1312 max_examined: 100,
1313 max_records: 10,
1314 explain: false,
1315 };
1316 preview.validate().expect("valid preview");
1317 for selector in [FilterRef::Bound, FilterRef::Group] {
1318 let sample = FilterTestRequest {
1319 filter: selector.clone(),
1320 ..test.clone()
1321 };
1322 assert!(sample.validate().is_err());
1323 preview.filter = selector;
1324 assert!(preview.validate().is_err());
1325 }
1326 }
1327
1328 #[test]
1329 fn given_oversized_sample_headers_when_validated_then_should_reject_before_evaluation() {
1330 let mut request = FilterTestRequest {
1331 v: FILTER_OP_VERSION,
1332 filter: FilterRef::Inline(ConsumerFilter::headers_only(FilterExpr::header(
1333 "priority",
1334 CmpOp::Eq,
1335 "critical",
1336 ))),
1337 payload: Vec::new(),
1338 headers: vec![FilterHeader {
1339 key: "priority".to_owned(),
1340 value: HeaderScalar::Raw(vec![0; crate::limits::MAX_FILTER_STRING_BYTES + 1]),
1341 }],
1342 };
1343 assert!(request.validate().is_err());
1344 request.headers[0].value = HeaderScalar::String("critical".to_owned());
1345 request.validate().expect("bounded header");
1346 request.headers[0].key.clear();
1347 assert!(request.validate().is_err());
1348 }
1349}