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,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub max_examined: Option<u32>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub min_catalog_position: Option<CatalogPosition>,
267}
268
269#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
271#[serde(rename_all = "snake_case")]
272#[strum(serialize_all = "snake_case")]
273pub enum StopReason {
274 Filled,
276 Budget,
278 EndOfVisible,
280 Fault,
282 OversizedRecord,
284}
285
286#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
288#[serde(rename_all = "snake_case")]
289#[strum(serialize_all = "snake_case")]
290pub enum FaultReason {
291 MissingSchema,
293 SchemaNotAllowed,
295 SchemaMismatch,
297 Malformed,
299 TooLarge,
301 TooDeep,
303 ForeignCodec,
305 TypeMismatch,
307}
308
309impl FaultReason {
310 pub const fn is_foreign(self) -> bool {
313 matches!(
314 self,
315 FaultReason::ForeignCodec
316 | FaultReason::MissingSchema
317 | FaultReason::SchemaNotAllowed
318 | FaultReason::SchemaMismatch
319 )
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
325pub struct RecordFault {
326 pub offset: u64,
327 pub reason: FaultReason,
328}
329
330#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
332pub struct FilterDecodeLimits {
333 pub max_payload_bytes: u32,
334 pub max_depth: u32,
335}
336
337#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
339pub struct AppliedPolicy {
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub group_id: Option<u64>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub digest: Option<Digest32>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub filter_id: Option<u32>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub revision: Option<u32>,
349 #[serde(default)]
350 pub mode: ExecutionMode,
351 #[serde(default)]
353 pub policy_generation: u64,
354}
355
356impl AppliedPolicy {
357 #[must_use]
359 pub fn filtered(group_id: Option<u64>, digest: Digest32) -> Self {
360 Self {
361 group_id,
362 digest: Some(digest),
363 filter_id: None,
364 revision: None,
365 mode: ExecutionMode::Filtered,
366 policy_generation: 0,
367 }
368 }
369
370 #[must_use]
372 pub const fn unfiltered(group_id: u64, policy_generation: u64) -> Self {
373 Self {
374 group_id: Some(group_id),
375 digest: None,
376 filter_id: None,
377 revision: None,
378 mode: ExecutionMode::Unfiltered,
379 policy_generation,
380 }
381 }
382}
383
384impl Validate for AppliedPolicy {
385 fn validate(&self) -> Result<(), InvalidError> {
386 validate_policy_identity(self.mode, self.digest.as_ref())?;
387 if self.mode == ExecutionMode::Unfiltered {
388 if self.group_id.is_none() {
389 return Err(InvalidError::new("only a consumer group runs unfiltered"));
390 }
391 if self.filter_id.is_some() || self.revision.is_some() {
392 return Err(InvalidError::new(
393 "an unfiltered policy has no filter reference",
394 ));
395 }
396 }
397 Ok(())
398 }
399}
400
401#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
413pub struct FilteredPage {
414 pub v: u32,
415 pub partition_id: u32,
416 pub policy: AppliedPolicy,
417 pub generation: SourceGeneration,
418 pub ack_context: AckContext,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub next_scan_offset: Option<u64>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub safe_ack_offset: Option<u64>,
423 pub frontier: u64,
426 pub examined: u32,
427 pub matched: u32,
428 pub stop: StopReason,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub fault: Option<RecordFault>,
431 #[serde(default, skip_serializing_if = "Vec::is_empty")]
434 pub unevaluated: Vec<u64>,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub evaluation_limits: Option<Box<FilterDecodeLimits>>,
437 #[serde(with = "crate::encoding::bin_bytes")]
438 pub records: Vec<u8>,
439}
440
441impl FilteredPage {
442 pub fn next_start(&self, original: &FilteredStart) -> FilteredStart {
445 match self.next_scan_offset {
446 Some(next_scan_offset) => FilteredStart::Continue(Continuation {
447 group_id: self.policy.group_id,
448 next_scan_offset,
449 generation: self.generation,
450 digest: self.policy.digest.clone(),
451 mode: self.policy.mode,
452 policy_generation: self.policy.policy_generation,
453 }),
454 None => original.clone(),
455 }
456 }
457}
458
459#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
463pub struct FilteredAck {
464 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub group_id: Option<u64>,
466 pub v: u32,
467 pub source: FilterSource,
468 pub partition_id: u32,
469 pub consumer: FilterConsumer,
470 pub generation: SourceGeneration,
471 pub ack_context: AckContext,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub digest: Option<Digest32>,
475 pub offset: u64,
476 #[serde(default)]
477 pub mode: ExecutionMode,
478 #[serde(default)]
481 pub policy_generation: u64,
482}
483
484#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
486pub struct AckReceipt {
487 pub partition_id: u32,
488 pub offset: u64,
489 pub generation: SourceGeneration,
490}
491
492#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
495pub struct FilterPreviewRequest {
496 pub v: u32,
497 pub source: FilterSource,
498 pub partition_id: u32,
499 pub filter: FilterRef,
501 pub from_offset: u64,
502 pub max_examined: u32,
503 pub max_records: u32,
504 #[serde(default)]
506 pub explain: bool,
507}
508
509#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
511#[serde(rename_all = "snake_case")]
512#[strum(serialize_all = "snake_case")]
513pub enum Verdict {
514 Selected,
515 Rejected,
516 Fault,
517}
518
519#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
521pub struct PreviewRecord {
522 pub offset: u64,
523 pub timestamp_micros: u64,
524 pub verdict: Verdict,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub fault: Option<FaultReason>,
527 pub payload_text: String,
529 #[serde(default)]
530 pub payload_truncated: bool,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub explanation: Option<FilterExplanation>,
533}
534
535#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
537pub struct FilterPreview {
538 pub v: u32,
539 pub partition_id: u32,
540 pub policy: AppliedPolicy,
541 pub examined: u32,
542 pub matched: u32,
543 pub faults: u32,
544 pub stop: StopReason,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub next_offset: Option<u64>,
547 pub frontier: u64,
548 pub records: Vec<PreviewRecord>,
551}
552
553#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
556#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
557pub enum HeaderScalar {
558 Bool(bool),
559 Int(i64),
560 Uint(u64),
561 Float(f64),
562 String(String),
563 Raw(#[serde(with = "crate::encoding::bin_bytes")] Vec<u8>),
564}
565
566#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
568pub struct FilterHeader {
569 pub key: String,
570 pub value: HeaderScalar,
571}
572
573#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
576pub struct FilterTestRequest {
577 pub v: u32,
578 pub filter: FilterRef,
580 #[serde(with = "crate::encoding::bin_bytes")]
581 pub payload: Vec<u8>,
582 #[serde(default, skip_serializing_if = "Vec::is_empty")]
583 pub headers: Vec<FilterHeader>,
584}
585
586#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
588#[serde(rename_all = "snake_case")]
589#[strum(serialize_all = "snake_case")]
590pub enum Truth {
591 Match,
592 NoMatch,
593 Unknown,
594}
595
596#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
599pub struct ExplainNode {
600 pub label: String,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub truth: Option<Truth>,
603 #[serde(default, skip_serializing_if = "Vec::is_empty")]
604 pub children: Vec<ExplainNode>,
605}
606
607#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
609pub struct FilterExplanation {
610 pub verdict: Verdict,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub fault: Option<FaultReason>,
613 pub root: ExplainNode,
614}
615
616#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
618pub struct FilterTestResult {
619 pub v: u32,
620 pub policy: AppliedPolicy,
621 pub explanation: FilterExplanation,
622}
623
624#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
626pub struct FilterValidation {
627 pub v: u32,
628 pub digest: Digest32,
629 pub reads_payload: bool,
631 pub reads_headers: bool,
633}
634
635#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
638#[serde(rename_all = "snake_case")]
639#[strum(serialize_all = "snake_case")]
640#[non_exhaustive]
641pub enum FilterErrorReason {
642 InvalidRequest,
644 Unsupported,
646 VersionSkew,
648 NotFound,
650 Conflict,
653 SourceChanged,
656 MembershipStale,
658 CatalogUnavailable,
660 RevisionDisabled,
662 TooLarge,
664 Unauthenticated,
665 Forbidden,
666 Unavailable,
668 CapacityExhausted,
670 Backend,
672 #[serde(other)]
675 Unknown,
676}
677
678impl FilterErrorReason {
679 pub const fn code(self) -> ResultCode {
680 match self {
681 Self::InvalidRequest => ResultCode::InvalidArgument,
682 Self::Unsupported => ResultCode::Unsupported,
683 Self::VersionSkew => ResultCode::VersionSkew,
684 Self::NotFound => ResultCode::NotFound,
685 Self::Conflict => ResultCode::Conflict,
686 Self::SourceChanged => ResultCode::StaleGeneration,
687 Self::MembershipStale => ResultCode::Stale,
688 Self::CatalogUnavailable | Self::Unavailable | Self::RevisionDisabled => {
689 ResultCode::Unavailable
690 }
691 Self::TooLarge => ResultCode::TooLarge,
692 Self::Unauthenticated => ResultCode::Unauthenticated,
693 Self::Forbidden => ResultCode::Forbidden,
694 Self::CapacityExhausted => ResultCode::ResourceLimit,
695 Self::Backend | Self::Unknown => ResultCode::Backend,
696 }
697 }
698}
699
700#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
702#[error("{reason}: {message}")]
703pub struct FilterError {
704 pub code: ResultCode,
705 pub reason: FilterErrorReason,
706 pub message: String,
707}
708
709impl FilterError {
710 pub fn new(reason: FilterErrorReason, message: impl Into<String>) -> Self {
711 Self {
712 code: reason.code(),
713 reason,
714 message: message.into(),
715 }
716 }
717
718 pub fn invalid(error: InvalidError) -> Self {
719 Self::new(FilterErrorReason::InvalidRequest, error.0)
720 }
721
722 pub fn check_version(v: u32) -> Result<(), Self> {
730 validate_version(v).map_err(|error| Self::new(FilterErrorReason::VersionSkew, error.0))
731 }
732}
733
734#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
736#[serde(rename_all = "snake_case")]
737pub enum FilterOutcome {
738 Page(FilteredPage),
739 Acknowledged(AckReceipt),
740 Preview(FilterPreview),
741 Tested(FilterTestResult),
742 Validated(FilterValidation),
743 Ready(FilteredWaitResult),
744 Cancelled(FilteredWaitCancelled),
745}
746
747#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
749#[serde(rename_all = "snake_case")]
750#[allow(clippy::large_enum_variant)]
752pub enum FilterReply {
753 Ok(FilterOutcome),
754 Err(FilterError),
755}
756
757fn validate_version(v: u32) -> Result<(), InvalidError> {
758 if v == FILTER_OP_VERSION {
759 Ok(())
760 } else {
761 Err(InvalidError::new(format!(
762 "filter request version {v} is not supported, expected {FILTER_OP_VERSION}"
763 )))
764 }
765}
766
767pub(crate) fn validate_name(label: &str, name: &str) -> Result<(), InvalidError> {
768 if name.is_empty() || name.len() > MAX_FILTER_SOURCE_NAME_BYTES {
769 return Err(InvalidError::new(format!(
770 "{label} must be 1..={MAX_FILTER_SOURCE_NAME_BYTES} bytes"
771 )));
772 }
773 Ok(())
774}
775
776impl Validate for FilterSource {
777 fn validate(&self) -> Result<(), InvalidError> {
778 validate_name("stream", &self.stream)?;
779 validate_name("topic", &self.topic)
780 }
781}
782
783impl Validate for FilterConsumer {
784 fn validate(&self) -> Result<(), InvalidError> {
785 match self {
786 Self::Consumer(name) => validate_name("consumer", name),
787 Self::Group(name) => validate_name("consumer group", name),
788 Self::GroupId(id) => u32::try_from(*id).map(|_| ()).map_err(|_| {
789 InvalidError::new("consumer group id exceeds the native offset-key range")
790 }),
791 }
792 }
793}
794
795impl Validate for FilterRef {
796 fn validate(&self) -> Result<(), InvalidError> {
797 match self {
798 Self::Inline(filter) => filter.validate(),
799 Self::Revision { .. } | Self::Bound | Self::Group => Ok(()),
800 }
801 }
802}
803
804impl Validate for FilteredWaitRequest {
805 fn validate(&self) -> Result<(), InvalidError> {
806 validate_version(self.v)?;
807 self.source.validate()?;
808 self.consumer.validate()?;
809 if self.partitions.is_empty() || self.partitions.len() > MAX_FILTERED_WAIT_PARTITIONS {
810 return Err(InvalidError::new(format!(
811 "a filtered wait names 1..={MAX_FILTERED_WAIT_PARTITIONS} partitions"
812 )));
813 }
814 let mut seen = BTreeSet::new();
815 if !self
816 .partitions
817 .iter()
818 .all(|partition| seen.insert(partition.partition_id))
819 {
820 return Err(InvalidError::new(
821 "a filtered wait names each partition once",
822 ));
823 }
824 if self.max_wait_ms > MAX_FILTERED_WAIT_MS {
825 return Err(InvalidError::new(format!(
826 "max_wait_ms must be 0..={MAX_FILTERED_WAIT_MS}"
827 )));
828 }
829 Ok(())
830 }
831}
832
833impl Validate for FilteredWaitCancel {
834 fn validate(&self) -> Result<(), InvalidError> {
835 validate_version(self.v)?;
836 if self.wait_ids.is_empty() || self.wait_ids.len() > MAX_FILTERED_WAIT_CANCEL_IDS {
837 return Err(InvalidError::new(format!(
838 "a filtered wait cancel names 1..={MAX_FILTERED_WAIT_CANCEL_IDS} waits"
839 )));
840 }
841 let mut seen = BTreeSet::new();
842 if !self.wait_ids.iter().all(|wait_id| seen.insert(*wait_id)) {
843 return Err(InvalidError::new(
844 "a filtered wait cancel names each wait once",
845 ));
846 }
847 Ok(())
848 }
849}
850
851impl Validate for FilteredPollRequest {
852 fn validate(&self) -> Result<(), InvalidError> {
853 validate_version(self.v)?;
854 self.source.validate()?;
855 self.consumer.validate()?;
856 self.filter.validate()?;
857 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) && !self.consumer.is_group() {
858 return Err(InvalidError::new(
859 "a `bound` or `group` filter needs a consumer group",
860 ));
861 }
862 if self.count == 0 || self.count > MAX_FILTERED_PAGE_RECORDS {
863 return Err(InvalidError::new(format!(
864 "count must be 1..={MAX_FILTERED_PAGE_RECORDS}"
865 )));
866 }
867 if let Some(max_examined) = self.max_examined
868 && (max_examined == 0 || max_examined > MAX_FILTERED_PAGE_EXAMINED)
869 {
870 return Err(InvalidError::new(format!(
871 "max_examined must be 1..={MAX_FILTERED_PAGE_EXAMINED}"
872 )));
873 }
874 if self.max_reply_bytes == 0 || self.max_reply_bytes > MAX_FILTERED_PAGE_BYTES {
875 return Err(InvalidError::new(format!(
876 "max_reply_bytes must be 1..={MAX_FILTERED_PAGE_BYTES}"
877 )));
878 }
879 if let FilteredStart::Continue(continuation) = &self.start {
880 validate_policy_identity(continuation.mode, continuation.digest.as_ref())?;
881 if continuation.mode == ExecutionMode::Unfiltered && !self.consumer.is_group() {
882 return Err(InvalidError::new("only a consumer group runs unfiltered"));
883 }
884 if self.consumer.is_group() != continuation.group_id.is_some() {
885 return Err(InvalidError::new(
886 "a group continuation must retain its group identity",
887 ));
888 }
889 if continuation.generation.partition_id != self.partition_id {
890 return Err(InvalidError::new(
891 "a continuation belongs to another partition",
892 ));
893 }
894 }
895 Ok(())
896 }
897}
898
899impl Validate for FilteredAck {
900 fn validate(&self) -> Result<(), InvalidError> {
901 validate_version(self.v)?;
902 self.source.validate()?;
903 self.consumer.validate()?;
904 validate_policy_identity(self.mode, self.digest.as_ref())?;
905 if self.mode == ExecutionMode::Unfiltered && !self.consumer.is_group() {
906 return Err(InvalidError::new("only a consumer group runs unfiltered"));
907 }
908 if self.consumer.is_group() != self.group_id.is_some() {
909 return Err(InvalidError::new(
910 "a group acknowledgment must retain its group identity",
911 ));
912 }
913 if self.generation.partition_id != self.partition_id {
914 return Err(InvalidError::new(
915 "an acknowledgment names another partition's generation",
916 ));
917 }
918 Ok(())
919 }
920}
921
922impl Validate for FilterPreviewRequest {
923 fn validate(&self) -> Result<(), InvalidError> {
924 validate_version(self.v)?;
925 self.source.validate()?;
926 self.filter.validate()?;
927 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) {
928 return Err(InvalidError::new(
929 "a preview executes an inline filter or a saved revision",
930 ));
931 }
932 if self.max_examined == 0 || self.max_examined > MAX_FILTER_PREVIEW_EXAMINED {
933 return Err(InvalidError::new(format!(
934 "max_examined must be 1..={MAX_FILTER_PREVIEW_EXAMINED}"
935 )));
936 }
937 if self.max_records == 0 || self.max_records > MAX_FILTER_PREVIEW_RECORDS {
938 return Err(InvalidError::new(format!(
939 "max_records must be 1..={MAX_FILTER_PREVIEW_RECORDS}"
940 )));
941 }
942 Ok(())
943 }
944}
945
946impl Validate for FilterTestRequest {
947 fn validate(&self) -> Result<(), InvalidError> {
948 validate_version(self.v)?;
949 self.filter.validate()?;
950 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) {
951 return Err(InvalidError::new(
952 "a sample test executes an inline filter or a saved revision",
953 ));
954 }
955 if self.payload.len() > MAX_FILTER_SAMPLE_BYTES {
956 return Err(InvalidError::new(format!(
957 "a sample payload exceeds {MAX_FILTER_SAMPLE_BYTES}B"
958 )));
959 }
960 if self.headers.len() > MAX_FILTER_SAMPLE_HEADERS {
961 return Err(InvalidError::new(format!(
962 "a sample carries more than {MAX_FILTER_SAMPLE_HEADERS} headers"
963 )));
964 }
965 for header in &self.headers {
966 validate_name("sample header key", &header.key)?;
967 let bytes = match &header.value {
968 HeaderScalar::String(value) => value.len(),
969 HeaderScalar::Raw(value) => value.len(),
970 _ => 0,
971 };
972 if bytes > crate::limits::MAX_FILTER_STRING_BYTES {
973 return Err(InvalidError::new(
974 "a sample header value exceeds its byte limit",
975 ));
976 }
977 }
978 Ok(())
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985 use crate::filter::expr::FilterExpr;
986 use crate::query::CmpOp;
987
988 fn wait_request() -> FilteredWaitRequest {
989 FilteredWaitRequest {
990 v: FILTER_OP_VERSION,
991 source: FilterSource {
992 stream: "orbit".to_owned(),
993 topic: "fleet".to_owned(),
994 },
995 consumer: FilterConsumer::Group("desk".to_owned()),
996 partitions: vec![
997 FilteredWaitPartition {
998 partition_id: 0,
999 offset: 12,
1000 },
1001 FilteredWaitPartition {
1002 partition_id: 1,
1003 offset: 0,
1004 },
1005 ],
1006 max_wait_ms: 5_000,
1007 wait_id: 9,
1008 }
1009 }
1010
1011 #[test]
1012 fn given_a_wait_cancel_when_encoded_as_json_then_should_round_trip_with_its_reply() {
1013 let cancel = FilteredWaitCancel {
1014 v: FILTER_OP_VERSION,
1015 wait_ids: vec![9, 11],
1016 };
1017 cancel.validate().expect("valid");
1018 let json = serde_json::to_string(&cancel).expect("encodes");
1019 assert_eq!(
1020 serde_json::from_str::<FilteredWaitCancel>(&json).expect("decodes"),
1021 cancel
1022 );
1023 let cancelled = FilterReply::Ok(FilterOutcome::Cancelled(FilteredWaitCancelled {
1024 cancelled: vec![9],
1025 }));
1026 let json = serde_json::to_string(&cancelled).expect("encodes");
1027 assert_eq!(
1028 serde_json::from_str::<FilterReply>(&json).expect("decodes"),
1029 cancelled
1030 );
1031 }
1032
1033 #[test]
1034 fn given_wait_cancels_out_of_range_when_validated_then_should_be_rejected() {
1035 let cancel = |wait_ids: Vec<u64>| FilteredWaitCancel {
1036 v: FILTER_OP_VERSION,
1037 wait_ids,
1038 };
1039 let too_many = cancel((0..=MAX_FILTERED_WAIT_CANCEL_IDS as u64).collect());
1040 let old_version = FilteredWaitCancel {
1041 v: FILTER_OP_VERSION + 1,
1042 ..cancel(vec![1])
1043 };
1044 for request in [
1045 cancel(Vec::new()),
1046 cancel(vec![4, 4]),
1047 too_many,
1048 old_version,
1049 ] {
1050 assert!(request.validate().is_err(), "{request:?} must be rejected");
1051 }
1052 cancel((0..MAX_FILTERED_WAIT_CANCEL_IDS as u64).collect())
1053 .validate()
1054 .expect("the cap is accepted");
1055 }
1056
1057 #[test]
1058 fn given_a_wait_request_when_encoded_as_json_then_should_round_trip() {
1059 let request = wait_request();
1060 request.validate().expect("valid");
1061 let json = serde_json::to_string(&request).expect("encodes");
1062 assert_eq!(
1063 serde_json::from_str::<FilteredWaitRequest>(&json).expect("decodes"),
1064 request
1065 );
1066 let ready = FilterReply::Ok(FilterOutcome::Ready(FilteredWaitResult { ready: vec![1] }));
1067 let json = serde_json::to_string(&ready).expect("encodes");
1068 assert_eq!(
1069 serde_json::from_str::<FilterReply>(&json).expect("decodes"),
1070 ready
1071 );
1072 }
1073
1074 #[test]
1075 fn given_wait_requests_out_of_range_when_validated_then_should_be_rejected() {
1076 let no_partitions = FilteredWaitRequest {
1077 partitions: Vec::new(),
1078 ..wait_request()
1079 };
1080 let too_long = FilteredWaitRequest {
1081 max_wait_ms: MAX_FILTERED_WAIT_MS + 1,
1082 ..wait_request()
1083 };
1084 let mut repeated = wait_request();
1085 repeated.partitions[1].partition_id = 0;
1086 let too_many = FilteredWaitRequest {
1087 partitions: (0..=MAX_FILTERED_WAIT_PARTITIONS as u32)
1088 .map(|partition_id| FilteredWaitPartition {
1089 partition_id,
1090 offset: 0,
1091 })
1092 .collect(),
1093 ..wait_request()
1094 };
1095 for request in [no_partitions, too_long, repeated, too_many] {
1096 assert!(request.validate().is_err(), "{request:?} must be rejected");
1097 }
1098 FilteredWaitRequest {
1099 max_wait_ms: 0,
1100 ..wait_request()
1101 }
1102 .validate()
1103 .expect("a zero wait answers at once");
1104 }
1105
1106 fn generation() -> SourceGeneration {
1107 SourceGeneration {
1108 stream_id: 1,
1109 stream_created_at_micros: 10,
1110 topic_id: 2,
1111 topic_created_at_micros: 20,
1112 partition_id: 0,
1113 partition_incarnation: 30,
1114 }
1115 }
1116
1117 fn request() -> FilteredPollRequest {
1118 FilteredPollRequest {
1119 v: FILTER_OP_VERSION,
1120 source: FilterSource {
1121 stream: "orbit".to_owned(),
1122 topic: "fleet_changes".to_owned(),
1123 },
1124 partition_id: 0,
1125 consumer: FilterConsumer::Group("anomaly-desk".to_owned()),
1126 filter: FilterRef::Bound,
1127 start: FilteredStart::Next,
1128 count: 100,
1129 max_reply_bytes: 1024 * 1024,
1130 max_examined: None,
1131 min_catalog_position: None,
1132 }
1133 }
1134
1135 #[test]
1136 fn given_a_group_read_when_validated_then_should_pass() {
1137 request().validate().expect("valid");
1138 }
1139
1140 #[test]
1141 fn given_out_of_range_reads_when_validated_then_should_be_rejected() {
1142 let mut bound_consumer = request();
1143 bound_consumer.consumer = FilterConsumer::Consumer("one".to_owned());
1144 let mut zero = request();
1145 zero.count = 0;
1146 let mut huge = request();
1147 huge.max_reply_bytes = MAX_FILTERED_PAGE_BYTES + 1;
1148 let mut other_partition = request();
1149 other_partition.start = FilteredStart::Continue(Continuation {
1150 group_id: None,
1151 next_scan_offset: 7,
1152 generation: SourceGeneration {
1153 partition_id: 5,
1154 ..generation()
1155 },
1156 digest: Some(Digest32::new([1; 32])),
1157 mode: ExecutionMode::Filtered,
1158 policy_generation: 0,
1159 });
1160 let mut group_consumer = request();
1161 group_consumer.filter = FilterRef::Group;
1162 group_consumer.consumer = FilterConsumer::Consumer("one".to_owned());
1163 let mut over_examined = request();
1164 over_examined.max_examined = Some(MAX_FILTERED_PAGE_EXAMINED + 1);
1165 let mut zero_examined = request();
1166 zero_examined.max_examined = Some(0);
1167 let mut unfiltered_with_digest = request();
1168 unfiltered_with_digest.start = FilteredStart::Continue(Continuation {
1169 group_id: Some(3),
1170 next_scan_offset: 7,
1171 generation: generation(),
1172 digest: Some(Digest32::new([1; 32])),
1173 mode: ExecutionMode::Unfiltered,
1174 policy_generation: 1,
1175 });
1176 for request in [
1177 bound_consumer,
1178 zero,
1179 huge,
1180 other_partition,
1181 group_consumer,
1182 over_examined,
1183 zero_examined,
1184 unfiltered_with_digest,
1185 ] {
1186 assert!(request.validate().is_err(), "{request:?} must be rejected");
1187 }
1188 }
1189
1190 #[test]
1191 fn given_an_automatic_group_read_with_an_examined_bound_when_validated_then_should_pass() {
1192 let mut automatic = request();
1193 automatic.filter = FilterRef::Group;
1194 automatic.max_examined = Some(100);
1195 automatic.min_catalog_position = Some(CatalogPosition {
1196 partition_id: 0,
1197 offset: 41,
1198 operation_id: Some(11),
1199 });
1200 automatic.validate().expect("valid");
1201 }
1202
1203 #[test]
1204 fn given_an_unfiltered_policy_when_validated_then_should_need_a_group_and_no_digest() {
1205 AppliedPolicy::unfiltered(3, 1).validate().expect("valid");
1206 assert!(
1207 AppliedPolicy::filtered(Some(3), Digest32::new([1; 32]))
1208 .validate()
1209 .is_ok()
1210 );
1211 let mut no_group = AppliedPolicy::unfiltered(3, 1);
1212 no_group.group_id = None;
1213 assert!(no_group.validate().is_err());
1214 let mut with_digest = AppliedPolicy::unfiltered(3, 1);
1215 with_digest.digest = Some(Digest32::new([1; 32]));
1216 assert!(with_digest.validate().is_err());
1217 let mut with_reference = AppliedPolicy::unfiltered(3, 1);
1218 with_reference.filter_id = Some(1);
1219 assert!(with_reference.validate().is_err());
1220 with_reference.filter_id = None;
1221 with_reference.revision = Some(1);
1222 assert!(with_reference.validate().is_err());
1223 let mut without_digest = AppliedPolicy::filtered(None, Digest32::new([1; 32]));
1224 without_digest.digest = None;
1225 assert!(without_digest.validate().is_err());
1226 }
1227
1228 #[test]
1229 fn given_a_page_when_asked_for_the_next_start_then_should_continue_or_repeat() {
1230 let page = FilteredPage {
1231 v: FILTER_OP_VERSION,
1232 partition_id: 0,
1233 policy: AppliedPolicy {
1234 group_id: None,
1235 digest: Some(Digest32::new([9; 32])),
1236 filter_id: Some(3),
1237 revision: Some(1),
1238 mode: ExecutionMode::Filtered,
1239 policy_generation: 0,
1240 },
1241 generation: generation(),
1242 ack_context: AckContext {
1243 owner_generation: 4,
1244 metadata_op: 40,
1245 },
1246 next_scan_offset: Some(1397),
1247 safe_ack_offset: Some(1396),
1248 frontier: 2000,
1249 examined: 397,
1250 matched: 3,
1251 stop: StopReason::Filled,
1252 fault: None,
1253 unevaluated: Vec::new(),
1254 evaluation_limits: None,
1255 records: Vec::new(),
1256 };
1257 assert_eq!(
1258 page.next_start(&FilteredStart::Next),
1259 FilteredStart::Continue(Continuation {
1260 group_id: None,
1261 next_scan_offset: 1397,
1262 generation: generation(),
1263 digest: Some(Digest32::new([9; 32])),
1264 mode: ExecutionMode::Filtered,
1265 policy_generation: 0,
1266 })
1267 );
1268 let empty = FilteredPage {
1269 next_scan_offset: None,
1270 safe_ack_offset: None,
1271 ..page
1272 };
1273 assert_eq!(empty.next_start(&FilteredStart::Last), FilteredStart::Last);
1274 }
1275
1276 #[test]
1277 fn given_error_reasons_when_classified_then_should_map_to_shared_codes() {
1278 assert_eq!(
1279 FilterError::new(FilterErrorReason::SourceChanged, "recreated").code,
1280 ResultCode::StaleGeneration
1281 );
1282 assert_eq!(FilterErrorReason::MembershipStale.code(), ResultCode::Stale);
1283 assert_eq!(FilterErrorReason::Conflict.code(), ResultCode::Conflict);
1284 assert_eq!(
1285 FilterErrorReason::CatalogUnavailable.code(),
1286 ResultCode::Unavailable
1287 );
1288 }
1289
1290 #[test]
1291 fn given_samples_and_previews_when_validated_then_should_reject_group_selectors() {
1292 let inline = FilterRef::Inline(ConsumerFilter::json(FilterExpr::pred(
1293 "kind",
1294 CmpOp::Eq,
1295 "satellites",
1296 )));
1297 let test = FilterTestRequest {
1298 v: FILTER_OP_VERSION,
1299 filter: inline.clone(),
1300 payload: br#"{"kind":"satellites"}"#.to_vec(),
1301 headers: Vec::new(),
1302 };
1303 test.validate().expect("valid sample");
1304 let mut preview = FilterPreviewRequest {
1305 v: FILTER_OP_VERSION,
1306 source: request().source,
1307 partition_id: 0,
1308 filter: inline,
1309 from_offset: 0,
1310 max_examined: 100,
1311 max_records: 10,
1312 explain: false,
1313 };
1314 preview.validate().expect("valid preview");
1315 for selector in [FilterRef::Bound, FilterRef::Group] {
1316 let sample = FilterTestRequest {
1317 filter: selector.clone(),
1318 ..test.clone()
1319 };
1320 assert!(sample.validate().is_err());
1321 preview.filter = selector;
1322 assert!(preview.validate().is_err());
1323 }
1324 }
1325
1326 #[test]
1327 fn given_oversized_sample_headers_when_validated_then_should_reject_before_evaluation() {
1328 let mut request = FilterTestRequest {
1329 v: FILTER_OP_VERSION,
1330 filter: FilterRef::Inline(ConsumerFilter::headers_only(FilterExpr::header(
1331 "priority",
1332 CmpOp::Eq,
1333 "critical",
1334 ))),
1335 payload: Vec::new(),
1336 headers: vec![FilterHeader {
1337 key: "priority".to_owned(),
1338 value: HeaderScalar::Raw(vec![0; crate::limits::MAX_FILTER_STRING_BYTES + 1]),
1339 }],
1340 };
1341 assert!(request.validate().is_err());
1342 request.headers[0].value = HeaderScalar::String("critical".to_owned());
1343 request.validate().expect("bounded header");
1344 request.headers[0].key.clear();
1345 assert!(request.validate().is_err());
1346 }
1347}