1use crate::utils::{Backoff, ResetTimerBackoff};
6
7use backon::BackoffBuilder;
8use educe::Educe;
9use futures::{Stream, StreamExt, stream::BoxStream};
10use kube_client::{
11 Api, Error as ClientErr,
12 api::{ListParams, Resource, ResourceExt, VersionMatch, WatchEvent, WatchParams},
13 core::{ObjectList, Selector, metadata::PartialObjectMeta},
14 error::Status,
15};
16use serde::de::DeserializeOwned;
17#[cfg(test)]
18#[path = "stub_watcher.rs"]
19mod stub_watcher;
20
21use std::{
22 clone::Clone,
23 collections::VecDeque,
24 fmt::{Debug, Display},
25 future::{self, Future},
26 time::Duration,
27};
28use thiserror::Error;
29use tracing::{debug, error, warn};
30
31#[derive(Debug, Error)]
38pub enum Error {
39 #[error("failed to perform initial object list: {0}")]
41 InitialListFailed(#[source] kube_client::Error),
42
43 #[error("failed to start watching object: {0}")]
45 WatchStartFailed(#[source] kube_client::Error),
46
47 #[error("error returned by apiserver during watch: {0}")]
49 WatchError(#[source] Box<Status>),
50
51 #[error("watch stream failed: {0}")]
53 WatchFailed(#[source] kube_client::Error),
54
55 #[error("no metadata.resourceVersion in watch result (does resource support watch?)")]
57 NoResourceVersion,
58}
59
60pub type Result<T, E = Error> = std::result::Result<T, E>;
62
63#[derive(Debug, Clone)]
64pub enum Event<K> {
66 Apply(K),
68 Delete(K),
79 Init,
84 InitApply(K),
92 InitDone,
100}
101
102impl<K> Display for Event<K>
103where
104 K: ResourceExt,
105{
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 let desc = match self {
108 Event::Init => "Init".into(),
109 Event::InitApply(obj) => format!("InitApply({})", obj.name_any()),
110 Event::InitDone => "InitDone".into(),
111 Event::Apply(obj) => format!("Apply({})", obj.name_any()),
112 Event::Delete(obj) => format!("Delete({})", obj.name_any()),
113 };
114 write!(f, "{desc}")
115 }
116}
117
118impl<K> Event<K> {
119 #[must_use]
137 pub fn modify(mut self, mut f: impl FnMut(&mut K)) -> Self {
138 match &mut self {
139 Self::Apply(obj) | Self::Delete(obj) | Self::InitApply(obj) => (f)(obj),
140 Self::Init | Self::InitDone => {} }
142 self
143 }
144}
145
146#[derive(Educe, Default)]
147#[educe(Debug)]
148enum State<K> {
150 #[default]
152 Empty,
153 InitPage {
155 continue_token: Option<String>,
156 objects: VecDeque<K>,
157 last_bookmark: Option<String>,
158 },
159 InitialWatch {
162 #[educe(Debug(ignore))]
163 stream: BoxStream<'static, kube_client::Result<WatchEvent<K>>>,
164 },
165 InitListed { resource_version: String },
167 Watching {
174 resource_version: String,
175 #[educe(Debug(ignore))]
176 stream: BoxStream<'static, kube_client::Result<WatchEvent<K>>>,
177 },
178}
179
180trait ApiMode: Send + Sync {
183 type Value: Clone + Send + 'static;
184
185 fn list(
186 &self,
187 lp: &ListParams,
188 ) -> impl Future<Output = kube_client::Result<ObjectList<Self::Value>>> + Send;
189 fn watch(
190 &self,
191 wp: &WatchParams,
192 version: &str,
193 ) -> impl Future<
194 Output = kube_client::Result<BoxStream<'static, kube_client::Result<WatchEvent<Self::Value>>>>,
195 > + Send;
196}
197
198struct FullObject<K> {
201 api: Api<K>,
202}
203
204#[derive(Clone, Default, Debug, PartialEq)]
206pub enum ListSemantic {
207 #[default]
217 MostRecent,
218
219 Any,
228}
229
230#[derive(Clone, Default, Debug, PartialEq)]
233pub enum InitialListStrategy {
234 #[default]
239 ListWatch,
240 StreamingList,
245}
246
247#[derive(Clone, Debug, PartialEq)]
249pub struct Config {
250 pub label_selector: Option<String>,
254
255 pub field_selector: Option<String>,
259
260 pub timeout: Option<u32>,
267
268 pub list_semantic: ListSemantic,
274
275 pub initial_list_strategy: InitialListStrategy,
286
287 pub page_size: Option<u32>,
296
297 pub bookmarks: bool,
302}
303
304impl Default for Config {
305 fn default() -> Self {
306 Self {
307 bookmarks: true,
308 label_selector: None,
309 field_selector: None,
310 timeout: None,
311 list_semantic: ListSemantic::default(),
312 page_size: Some(500),
315 initial_list_strategy: InitialListStrategy::ListWatch,
316 }
317 }
318}
319
320impl Config {
330 #[must_use]
335 pub fn timeout(mut self, timeout_secs: u32) -> Self {
336 self.timeout = Some(timeout_secs);
337 self
338 }
339
340 #[must_use]
346 pub fn fields(mut self, field_selector: &str) -> Self {
347 self.field_selector = Some(field_selector.to_string());
348 self
349 }
350
351 #[must_use]
356 pub fn labels(mut self, label_selector: &str) -> Self {
357 self.label_selector = Some(label_selector.to_string());
358 self
359 }
360
361 #[must_use]
377 pub fn labels_from(mut self, selector: &Selector) -> Self {
378 self.label_selector = Some(selector.to_string());
379 self
380 }
381
382 #[must_use]
386 pub fn list_semantic(mut self, semantic: ListSemantic) -> Self {
387 self.list_semantic = semantic;
388 self
389 }
390
391 #[must_use]
395 pub fn any_semantic(self) -> Self {
396 self.list_semantic(ListSemantic::Any)
397 }
398
399 #[must_use]
404 pub fn disable_bookmarks(mut self) -> Self {
405 self.bookmarks = false;
406 self
407 }
408
409 #[must_use]
416 pub fn page_size(mut self, page_size: u32) -> Self {
417 self.page_size = Some(page_size);
418 self
419 }
420
421 #[must_use]
424 pub fn streaming_lists(mut self) -> Self {
425 self.initial_list_strategy = InitialListStrategy::StreamingList;
426 self
427 }
428
429 fn to_list_params(&self) -> ListParams {
431 let (resource_version, version_match) = match self.list_semantic {
432 ListSemantic::Any => (Some("0".into()), Some(VersionMatch::NotOlderThan)),
433 ListSemantic::MostRecent => (None, None),
434 };
435 ListParams {
436 label_selector: self.label_selector.clone(),
437 field_selector: self.field_selector.clone(),
438 version_match,
439 resource_version,
440 limit: self.page_size,
442 continue_token: None,
443 ..Default::default()
444 }
445 }
446
447 fn to_watch_params(&self, phase: WatchPhase) -> WatchParams {
449 WatchParams {
450 label_selector: self.label_selector.clone(),
451 field_selector: self.field_selector.clone(),
452 timeout: self.timeout,
453 bookmarks: self.bookmarks,
454 send_initial_events: phase == WatchPhase::Initial
455 && self.initial_list_strategy == InitialListStrategy::StreamingList,
456 }
457 }
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
465enum WatchPhase {
466 Initial,
468 Resumed,
470}
471
472impl<K> ApiMode for FullObject<K>
473where
474 K: Clone + Debug + DeserializeOwned + Send + 'static,
475{
476 type Value = K;
477
478 async fn list(&self, lp: &ListParams) -> kube_client::Result<ObjectList<Self::Value>> {
479 self.api.list(lp).await
480 }
481
482 async fn watch(
483 &self,
484 wp: &WatchParams,
485 version: &str,
486 ) -> kube_client::Result<BoxStream<'static, kube_client::Result<WatchEvent<Self::Value>>>> {
487 self.api.watch(wp, version).await.map(StreamExt::boxed)
488 }
489}
490
491struct MetaOnly<K> {
494 api: Api<K>,
495}
496
497impl<K> ApiMode for MetaOnly<K>
498where
499 K: Clone + Debug + DeserializeOwned + Send + 'static,
500{
501 type Value = PartialObjectMeta<K>;
502
503 async fn list(&self, lp: &ListParams) -> kube_client::Result<ObjectList<Self::Value>> {
504 self.api.list_metadata(lp).await
505 }
506
507 async fn watch(
508 &self,
509 wp: &WatchParams,
510 version: &str,
511 ) -> kube_client::Result<BoxStream<'static, kube_client::Result<WatchEvent<Self::Value>>>> {
512 self.api.watch_metadata(wp, version).await.map(StreamExt::boxed)
513 }
514}
515
516const WATCH_IDLE_TIMEOUT_MARGIN: Duration = Duration::from_secs(5);
522
523async fn next_with_idle_timeout<S, T>(stream: &mut S, timeout: Option<u32>) -> Option<T>
530where
531 S: Stream<Item = T> + Unpin,
532{
533 if timeout == Some(0) {
534 return stream.next().await;
535 }
536
537 let idle_timeout = Duration::from_secs(u64::from(timeout.unwrap_or(290))) + WATCH_IDLE_TIMEOUT_MARGIN;
538 match tokio::time::timeout(idle_timeout, stream.next()).await {
539 Ok(item) => item,
540 Err(_elapsed) => {
541 debug!(
542 timeout_secs = idle_timeout.as_secs(),
543 "watch stream idle timeout, reconnecting"
544 );
545 None
546 }
547 }
548}
549
550#[allow(clippy::too_many_lines)] async fn step_trampolined<A>(
556 api: &A,
557 wc: &Config,
558 state: State<A::Value>,
559) -> (Option<Result<Event<A::Value>>>, State<A::Value>)
560where
561 A: ApiMode,
562 A::Value: Resource + 'static,
563{
564 match state {
565 State::Empty => match wc.initial_list_strategy {
566 InitialListStrategy::ListWatch => (Some(Ok(Event::Init)), State::InitPage {
567 continue_token: None,
568 objects: VecDeque::default(),
569 last_bookmark: None,
570 }),
571 InitialListStrategy::StreamingList => {
572 match api.watch(&wc.to_watch_params(WatchPhase::Initial), "0").await {
573 Ok(stream) => (Some(Ok(Event::Init)), State::InitialWatch { stream }),
574 Err(err) => {
575 if std::matches!(err, ClientErr::Api(ref status) if status.is_forbidden()) {
576 warn!("watch initlist error with 403: {err:?}");
577 } else {
578 debug!("watch initlist error: {err:?}");
579 }
580 (Some(Err(Error::WatchStartFailed(err))), State::default())
581 }
582 }
583 }
584 },
585 State::InitPage {
586 continue_token,
587 mut objects,
588 last_bookmark,
589 } => {
590 if let Some(next) = objects.pop_front() {
591 return (Some(Ok(Event::InitApply(next))), State::InitPage {
592 continue_token,
593 objects,
594 last_bookmark,
595 });
596 }
597 if continue_token.is_none()
599 && let Some(resource_version) = last_bookmark
600 {
601 return (Some(Ok(Event::InitDone)), State::InitListed { resource_version });
603 }
604 let mut lp = wc.to_list_params();
605 lp.continue_token = continue_token;
606 match api.list(&lp).await {
607 Ok(list) => {
608 let last_bookmark = list.metadata.resource_version.filter(|s| !s.is_empty());
609 let continue_token = list.metadata.continue_.filter(|s| !s.is_empty());
610 if last_bookmark.is_none() && continue_token.is_none() {
611 return (Some(Err(Error::NoResourceVersion)), State::Empty);
612 }
613 (None, State::InitPage {
616 continue_token,
617 objects: list.items.into_iter().collect(),
618 last_bookmark,
619 })
620 }
621 Err(err) => {
622 if std::matches!(err, ClientErr::Api(ref status) if status.is_forbidden()) {
623 warn!("watch list error with 403: {err:?}");
624 } else {
625 debug!("watch list error: {err:?}");
626 }
627 (Some(Err(Error::InitialListFailed(err))), State::Empty)
628 }
629 }
630 }
631 State::InitialWatch { mut stream } => {
632 match next_with_idle_timeout(&mut stream, wc.timeout).await {
633 Some(Ok(WatchEvent::Added(obj) | WatchEvent::Modified(obj))) => {
634 (Some(Ok(Event::InitApply(obj))), State::InitialWatch { stream })
635 }
636 Some(Ok(WatchEvent::Deleted(_obj))) => {
637 error!("got deleted event during initial watch. this is a bug");
640 (None, State::InitialWatch { stream })
641 }
642 Some(Ok(WatchEvent::Bookmark(bm))) => {
643 let marks_initial_end = bm.metadata.annotations.contains_key("k8s.io/initial-events-end");
644 if marks_initial_end {
645 (Some(Ok(Event::InitDone)), State::Watching {
646 resource_version: bm.metadata.resource_version,
647 stream,
648 })
649 } else {
650 (None, State::InitialWatch { stream })
651 }
652 }
653 Some(Ok(WatchEvent::Error(err))) => {
654 let new_state = if err.code == 410 {
656 State::default()
657 } else {
658 State::InitialWatch { stream }
659 };
660 if err.code == 403 {
661 warn!("watcher watchevent error 403: {err:?}");
662 } else {
663 debug!("error watchevent error: {err:?}");
664 }
665 (Some(Err(Error::WatchError(err.boxed()))), new_state)
666 }
667 Some(Err(err)) => {
668 if std::matches!(err, ClientErr::Api(ref status) if status.is_forbidden()) {
669 warn!("watcher error 403: {err:?}");
670 } else {
671 debug!("watcher error: {err:?}");
672 }
673 (Some(Err(Error::WatchFailed(err))), State::InitialWatch { stream })
674 }
675 None => (None, State::default()),
676 }
677 }
678 State::InitListed { resource_version } => {
679 match api
680 .watch(&wc.to_watch_params(WatchPhase::Resumed), &resource_version)
681 .await
682 {
683 Ok(stream) => (None, State::Watching {
684 resource_version,
685 stream,
686 }),
687 Err(err) => {
688 if std::matches!(err, ClientErr::Api(ref status) if status.is_forbidden()) {
689 warn!("watch initlist error with 403: {err:?}");
690 } else {
691 debug!("watch initlist error: {err:?}");
692 }
693 (Some(Err(Error::WatchStartFailed(err))), State::InitListed {
694 resource_version,
695 })
696 }
697 }
698 }
699 State::Watching {
700 resource_version,
701 mut stream,
702 } => match next_with_idle_timeout(&mut stream, wc.timeout).await {
703 Some(Ok(WatchEvent::Added(obj) | WatchEvent::Modified(obj))) => {
704 let resource_version = obj.resource_version().unwrap_or_default();
705 if resource_version.is_empty() {
706 (Some(Err(Error::NoResourceVersion)), State::default())
707 } else {
708 (Some(Ok(Event::Apply(obj))), State::Watching {
709 resource_version,
710 stream,
711 })
712 }
713 }
714 Some(Ok(WatchEvent::Deleted(obj))) => {
715 let resource_version = obj.resource_version().unwrap_or_default();
716 if resource_version.is_empty() {
717 (Some(Err(Error::NoResourceVersion)), State::default())
718 } else {
719 (Some(Ok(Event::Delete(obj))), State::Watching {
720 resource_version,
721 stream,
722 })
723 }
724 }
725 Some(Ok(WatchEvent::Bookmark(bm))) => (None, State::Watching {
726 resource_version: bm.metadata.resource_version,
727 stream,
728 }),
729 Some(Ok(WatchEvent::Error(err))) => {
730 let new_state = if err.code == 410 {
732 State::default()
733 } else {
734 State::Watching {
735 resource_version,
736 stream,
737 }
738 };
739 if err.code == 403 {
740 warn!("watcher watchevent error 403: {err:?}");
741 } else {
742 debug!("error watchevent error: {err:?}");
743 }
744 (Some(Err(Error::WatchError(err.boxed()))), new_state)
745 }
746 Some(Err(err)) => {
747 if std::matches!(err, ClientErr::Api(ref status) if status.is_forbidden()) {
748 warn!("watcher error 403: {err:?}");
749 } else {
750 debug!("watcher error: {err:?}");
751 }
752 (Some(Err(Error::WatchFailed(err))), State::Watching {
753 resource_version,
754 stream,
755 })
756 }
757 None => (None, State::InitListed { resource_version }),
758 },
759 }
760}
761
762async fn step<A>(
764 api: &A,
765 config: &Config,
766 mut state: State<A::Value>,
767) -> (Result<Event<A::Value>>, State<A::Value>)
768where
769 A: ApiMode,
770 A::Value: Resource + 'static,
771{
772 loop {
773 match step_trampolined(api, config, state).await {
774 (Some(result), new_state) => return (result, new_state),
775 (None, new_state) => state = new_state,
776 }
777 }
778}
779
780#[doc(alias = "informer")]
830pub fn watcher<K: Resource + Clone + DeserializeOwned + Debug + Send + 'static>(
831 api: Api<K>,
832 watcher_config: Config,
833) -> impl Stream<Item = Result<Event<K>>> + Send {
834 watcher_inner(FullObject { api }, watcher_config)
835}
836
837fn watcher_inner<A>(api: A, watcher_config: Config) -> impl Stream<Item = Result<Event<A::Value>>> + Send
840where
841 A: ApiMode,
842 A::Value: Resource,
843{
844 futures::stream::unfold(
845 (api, watcher_config, State::default()),
846 |(api, watcher_config, state)| async {
847 let (event, state) = step(&api, &watcher_config, state).await;
848 Some((event, (api, watcher_config, state)))
849 },
850 )
851}
852
853#[deprecated(
904 since = "3.1.0",
905 note = "Use `watcher(Api::<PartialObjectMeta<K>>::all(client), config)` instead. \
906 `Api<PartialObjectMeta<K>>` now automatically uses metadata-only requests."
907)]
908pub fn metadata_watcher<K: Resource + Clone + DeserializeOwned + Debug + Send + 'static>(
909 api: Api<K>,
910 watcher_config: Config,
911) -> impl Stream<Item = Result<Event<PartialObjectMeta<K>>>> + Send {
912 watcher_inner(MetaOnly { api }, watcher_config)
913}
914
915pub fn watch_object<K: Resource + Clone + DeserializeOwned + Debug + Send + 'static>(
927 api: Api<K>,
928 name: &str,
929) -> impl Stream<Item = Result<Option<K>>> + Send + use<K> {
930 let fields = format!("metadata.name={name}");
933 watcher(api, Config::default().fields(&fields))
934 .scan(false, |obj_seen, event| {
942 if matches!(event, Ok(Event::Init)) {
943 *obj_seen = false;
944 } else if matches!(event, Ok(Event::InitApply(_))) {
945 *obj_seen = true;
946 }
947 future::ready(Some((*obj_seen, event)))
948 })
949 .filter_map(|(obj_seen, event)| async move {
950 match event {
951 Ok(Event::Apply(obj) | Event::InitApply(obj)) => Some(Ok(Some(obj))),
953 Ok(Event::Delete(_)) => Some(Ok(None)),
955 Ok(Event::InitDone) if !obj_seen => Some(Ok(None)),
957 Ok(Event::Init | Event::InitDone) => None,
959 Err(err) => Some(Err(err)),
961 }
962 })
963}
964
965pub struct ExponentialBackoff {
967 inner: backon::ExponentialBackoff,
968 builder: backon::ExponentialBuilder,
969}
970
971impl ExponentialBackoff {
972 fn new(min_delay: Duration, max_delay: Duration, factor: f32, enable_jitter: bool) -> Self {
973 let builder = backon::ExponentialBuilder::default()
974 .with_min_delay(min_delay)
975 .with_max_delay(max_delay)
976 .with_factor(factor)
977 .without_max_times();
978
979 let builder = if enable_jitter {
980 builder.with_jitter()
981 } else {
982 builder
983 };
984
985 Self {
986 inner: builder.build(),
987 builder,
988 }
989 }
990}
991
992impl Backoff for ExponentialBackoff {
993 fn reset(&mut self) {
994 self.inner = self.builder.build();
995 }
996}
997
998impl Iterator for ExponentialBackoff {
999 type Item = Duration;
1000
1001 fn next(&mut self) -> Option<Self::Item> {
1002 self.inner.next()
1003 }
1004}
1005
1006impl From<backon::ExponentialBuilder> for ExponentialBackoff {
1007 fn from(builder: backon::ExponentialBuilder) -> Self {
1008 Self {
1009 inner: builder.build(),
1010 builder,
1011 }
1012 }
1013}
1014
1015pub struct DefaultBackoff(Strategy);
1026type Strategy = ResetTimerBackoff<ExponentialBackoff>;
1027
1028impl Default for DefaultBackoff {
1029 fn default() -> Self {
1030 Self(ResetTimerBackoff::new(
1031 ExponentialBackoff::new(Duration::from_millis(800), Duration::from_secs(30), 2.0, true),
1032 Duration::from_secs(120),
1033 ))
1034 }
1035}
1036
1037impl Iterator for DefaultBackoff {
1038 type Item = Duration;
1039
1040 fn next(&mut self) -> Option<Self::Item> {
1041 self.0.next()
1042 }
1043}
1044
1045impl Backoff for DefaultBackoff {
1046 fn reset(&mut self) {
1047 self.0.reset();
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use std::{collections::BTreeMap, time::Duration};
1054
1055 use futures::StreamExt;
1056 use k8s_openapi::{api::core::v1::ConfigMap, apimachinery::pkg::apis::meta::v1::ObjectMeta};
1057 use kube_client::{
1058 api::{TypeMeta, WatchEvent, WatchParams},
1059 core::{
1060 Status,
1061 watch::{Bookmark, BookmarkMeta},
1062 },
1063 };
1064
1065 use crate::watcher::{
1066 ApiMode, Config, ExponentialBackoff, State, WatchPhase, next_with_idle_timeout, step,
1067 stub_watcher::{
1068 Recording, Sequence, SequenceStep, TestMode, error_indicates_graceful_watch_seq_exhaustion,
1069 },
1070 watcher_inner,
1071 };
1072
1073 use super::stub_watcher::ResultPage;
1074
1075 fn config_map(name: &str, resource_version: &str) -> ConfigMap {
1076 ConfigMap {
1077 metadata: ObjectMeta {
1078 name: Some(name.to_string()),
1079 resource_version: Some(resource_version.to_string()),
1080 ..ObjectMeta::default()
1081 },
1082 ..ConfigMap::default()
1083 }
1084 }
1085
1086 #[tokio::test(start_paused = true)]
1087 async fn test_mode_watch_waits_before_returning_next_event() {
1088 let api = TestMode::new(vec![], vec![Sequence::new(vec![
1089 SequenceStep::Wait(Duration::from_millis(50)),
1090 SequenceStep::List(vec![Ok(WatchEvent::Added(config_map("a", "1")))].into()),
1091 ])]);
1092 let mut stream = api.watch(&WatchParams::default(), "0").await.unwrap();
1093
1094 assert!(futures::poll!(stream.next()).is_pending());
1095 tokio::time::advance(Duration::from_millis(49)).await;
1096 assert!(futures::poll!(stream.next()).is_pending());
1097 tokio::time::advance(Duration::from_millis(1)).await;
1098
1099 assert!(matches!(
1100 stream.next().await,
1101 Some(Ok(WatchEvent::Added(config_map))) if config_map.metadata.name.as_deref() == Some("a")
1102 ));
1103 }
1104
1105 #[allow(clippy::too_many_lines)]
1106 #[tokio::test]
1107 async fn streaming_list_resync() {
1108 const LAST_VALID_RESOURCE_VERSION: &str = "5";
1109 let mut recoverable_err = Box::new(Status::default());
1110 recoverable_err.code = 502;
1111 recoverable_err.message = "some err".to_string();
1112 recoverable_err.reason = "Something went wrong".to_string();
1113 let api = TestMode::new(vec![], vec![
1114 Sequence::new(vec![
1115 SequenceStep::List(
1116 vec![
1117 Ok(WatchEvent::Added(config_map("a", "2"))),
1118 Ok(WatchEvent::Added(config_map("b", "3"))),
1119 Ok(WatchEvent::Added(config_map("c", "4"))),
1120 Ok(WatchEvent::Bookmark(Bookmark {
1126 types: TypeMeta::resource::<ConfigMap>(),
1127 metadata: BookmarkMeta {
1128 resource_version: "4".to_string(),
1129 annotations: BTreeMap::from_iter(vec![(
1130 "k8s.io/initial-events-end".into(),
1131 "true".into(),
1132 )]),
1133 },
1134 })),
1135 ]
1136 .into(),
1137 ),
1138 SequenceStep::List(
1139 vec![
1140 Ok(WatchEvent::Modified(config_map("a", LAST_VALID_RESOURCE_VERSION))),
1141 Ok(WatchEvent::Error(recoverable_err)),
1142 ]
1143 .into(),
1144 ),
1145 SequenceStep::List(
1148 vec![Ok(WatchEvent::Modified(config_map(
1149 "a",
1150 LAST_VALID_RESOURCE_VERSION,
1151 )))]
1152 .into(),
1153 ),
1154 ]),
1155 Sequence::new(vec![SequenceStep::List(
1158 vec![
1159 Ok(WatchEvent::Added(config_map("a", "5"))),
1160 Ok(WatchEvent::Bookmark(Bookmark {
1161 types: TypeMeta::resource::<ConfigMap>(),
1162 metadata: BookmarkMeta {
1163 resource_version: "5".to_string(),
1164 annotations: BTreeMap::from_iter(vec![(
1165 "k8s.io/initial-events-end".into(),
1166 "true".into(),
1167 )]),
1168 },
1169 })),
1170 ]
1171 .into(),
1172 )]),
1173 ]);
1174
1175 let config = Config::default()
1176 .timeout(1)
1177 .streaming_lists()
1178 .labels("app=test")
1179 .fields("metadata.name!=ignored");
1180 let mut stream = std::pin::pin!(watcher_inner(api.clone(), config));
1183
1184 for expected in [
1185 "Init",
1186 "InitApply(a)",
1187 "InitApply(b)",
1188 "InitApply(c)",
1189 "InitDone",
1190 "Apply(a)",
1191 "error returned by apiserver during watch: some err: Something went wrong",
1192 "Apply(a)",
1193 "Apply(a)",
1194 ] {
1195 let event = stream.next().await.expect("watcher stream should not end");
1196 let repr = match event {
1197 Ok(event) => event.to_string(),
1198 Err(err) => err.to_string(),
1199 };
1200 assert_eq!(repr, expected);
1201 }
1202
1203 let event = tokio::time::timeout(Duration::from_millis(100), stream.next())
1204 .await
1205 .expect("watcher should return when the TestMode sequence is exhausted")
1206 .expect("watcher stream should not end");
1207 dbg!(&event);
1211 assert!(event.is_err_and(|err| error_indicates_graceful_watch_seq_exhaustion(&err)),);
1212
1213 let records = api.get_recordings();
1214 match &records[..] {
1215 [
1216 Recording::Watch(initial_params, initial_version),
1217 Recording::Watch(resumed_params, resumed_version),
1218 Recording::Watch(exhausted_params, exhausted_version),
1219 ] => {
1220 assert_eq!(initial_version, "0");
1221 assert_eq!(initial_params.label_selector.as_deref(), Some("app=test"));
1222 assert_eq!(
1223 initial_params.field_selector.as_deref(),
1224 Some("metadata.name!=ignored")
1225 );
1226 assert!(initial_params.send_initial_events);
1227
1228 for (watch_params, watch_version) in [
1229 (resumed_params, resumed_version),
1230 (exhausted_params, exhausted_version),
1231 ] {
1232 assert_eq!(watch_version, LAST_VALID_RESOURCE_VERSION);
1233 assert_eq!(watch_params.label_selector.as_deref(), Some("app=test"));
1234 assert_eq!(
1235 watch_params.field_selector.as_deref(),
1236 Some("metadata.name!=ignored")
1237 );
1238 assert!(!watch_params.send_initial_events);
1239 }
1240 }
1241 _ => panic!("unexpected API call sequence {records:?}"),
1242 }
1243 }
1244
1245 #[tokio::test]
1246 async fn streaming_list_init() {
1247 let api = TestMode::new(vec![], vec![Sequence::new(vec![
1248 SequenceStep::List(
1249 vec![
1250 Ok(WatchEvent::Added(config_map("a", "2"))),
1251 Ok(WatchEvent::Added(config_map("b", "3"))),
1252 Ok(WatchEvent::Added(config_map("c", "4"))),
1253 Ok(WatchEvent::Bookmark(Bookmark {
1259 types: TypeMeta::resource::<ConfigMap>(),
1260 metadata: BookmarkMeta {
1261 resource_version: "145".to_string(),
1262 annotations: BTreeMap::from_iter(vec![(
1263 "k8s.io/initial-events-end".into(),
1264 "true".into(),
1265 )]),
1266 },
1267 })),
1268 ]
1269 .into(),
1270 ),
1271 SequenceStep::List(
1272 vec![
1273 Ok(WatchEvent::Modified(config_map("a", "5"))),
1274 Ok(WatchEvent::Deleted(config_map("b", "7"))),
1275 Ok(WatchEvent::Modified(config_map("c", "9"))),
1276 ]
1277 .into(),
1278 ),
1279 ])]);
1280
1281 let config = Config::default()
1282 .timeout(1)
1283 .streaming_lists()
1284 .labels("app=test")
1285 .fields("metadata.name!=ignored");
1286 let mut state = State::default();
1287 for expected in [
1288 "Init",
1289 "InitApply(a)",
1290 "InitApply(b)",
1291 "InitApply(c)",
1292 "InitDone",
1293 "Apply(a)",
1294 "Delete(b)",
1295 "Apply(c)",
1296 ] {
1297 let (event, next) = step(&api, &config, state).await;
1298 state = next;
1299 assert_eq!(event.unwrap().to_string(), expected);
1300 }
1301
1302 let (event, _) = tokio::time::timeout(Duration::from_millis(100), step(&api, &config, state))
1303 .await
1304 .expect("step should return when the TestMode sequence is exhausted");
1305 assert!(event.is_err_and(|err| error_indicates_graceful_watch_seq_exhaustion(&err)));
1309
1310 let records = api.get_recordings();
1311 match &records[..] {
1312 [
1313 Recording::Watch(initial_params, initial_version),
1314 Recording::Watch(resumed_params, resumed_version),
1315 ] => {
1316 assert_eq!(initial_version, "0");
1317 assert_eq!(initial_params.label_selector.as_deref(), Some("app=test"));
1318 assert_eq!(
1319 initial_params.field_selector.as_deref(),
1320 Some("metadata.name!=ignored")
1321 );
1322 assert!(initial_params.send_initial_events);
1323
1324 assert_eq!(resumed_version, "9");
1325 assert_eq!(resumed_params.label_selector.as_deref(), Some("app=test"));
1326 assert_eq!(
1327 resumed_params.field_selector.as_deref(),
1328 Some("metadata.name!=ignored")
1329 );
1330 assert!(!resumed_params.send_initial_events);
1331 }
1332 _ => panic!("unexpected API call sequence {records:?}"),
1333 }
1334 }
1335
1336 #[tokio::test]
1337 async fn streaming_list_initial_stream_eof_before_bookmark_drops_partial_objects() {
1338 let api = TestMode::new(vec![], vec![
1339 Sequence::new(vec![SequenceStep::List(
1340 vec![
1341 Ok(WatchEvent::Added(config_map("a", "1"))),
1342 Ok(WatchEvent::Added(config_map("b", "2"))),
1343 ]
1344 .into(),
1345 )]),
1346 Sequence::new(vec![SequenceStep::List(
1347 vec![
1348 Ok(WatchEvent::Added(config_map("a", "1"))),
1349 Ok(WatchEvent::Bookmark(Bookmark {
1350 types: TypeMeta::resource::<ConfigMap>(),
1351 metadata: BookmarkMeta {
1352 resource_version: "1".to_string(),
1353 annotations: BTreeMap::from_iter(vec![(
1354 "k8s.io/initial-events-end".into(),
1355 "true".into(),
1356 )]),
1357 },
1358 })),
1359 ]
1360 .into(),
1361 )]),
1362 ]);
1363
1364 let config = Config::default().streaming_lists();
1365 let (reader, mut writer) = crate::reflector::store::<ConfigMap>();
1366 let mut state = State::default();
1367 for expected in [
1368 "Init",
1369 "InitApply(a)",
1370 "InitApply(b)",
1371 "Init",
1372 "InitApply(a)",
1373 "InitDone",
1374 ] {
1375 let (event, next) = step(&api, &config, state).await;
1376 state = next;
1377 let event = event.unwrap();
1378 assert_eq!(event.to_string(), expected);
1379 writer.apply_watcher_event(&event);
1380 }
1381
1382 let mut names: Vec<_> = reader
1383 .state()
1384 .iter()
1385 .filter_map(|cm| cm.metadata.name.clone())
1386 .collect();
1387 names.sort();
1388 assert_eq!(names, ["a".to_string()]);
1389
1390 let records = api.get_recordings();
1391 match &records[..] {
1392 [
1393 Recording::Watch(first, first_version),
1394 Recording::Watch(second, second_version),
1395 ] => {
1396 assert_eq!(first_version, "0");
1397 assert!(first.send_initial_events);
1398 assert_eq!(second_version, "0");
1399 assert!(second.send_initial_events);
1400 }
1401 _ => panic!("unexpected API call sequence {records:?}"),
1402 }
1403 }
1404
1405 #[tokio::test]
1406 async fn streaming_list_initial_watch_410_before_bookmark_drops_partial_objects() {
1407 let mut gone = Box::new(Status::default());
1408 gone.code = 410;
1409 gone.message = "expired".to_string();
1410 gone.reason = "Expired".to_string();
1411 let api = TestMode::new(vec![], vec![
1412 Sequence::new(vec![SequenceStep::List(
1413 vec![
1414 Ok(WatchEvent::Added(config_map("a", "1"))),
1415 Ok(WatchEvent::Added(config_map("b", "2"))),
1416 Ok(WatchEvent::Error(gone)),
1417 ]
1418 .into(),
1419 )]),
1420 Sequence::new(vec![SequenceStep::List(
1421 vec![
1422 Ok(WatchEvent::Added(config_map("a", "1"))),
1423 Ok(WatchEvent::Bookmark(Bookmark {
1424 types: TypeMeta::resource::<ConfigMap>(),
1425 metadata: BookmarkMeta {
1426 resource_version: "1".to_string(),
1427 annotations: BTreeMap::from_iter(vec![(
1428 "k8s.io/initial-events-end".into(),
1429 "true".into(),
1430 )]),
1431 },
1432 })),
1433 ]
1434 .into(),
1435 )]),
1436 ]);
1437
1438 let config = Config::default().streaming_lists();
1439 let (reader, mut writer) = crate::reflector::store::<ConfigMap>();
1440 let mut state = State::default();
1441 for expected in [
1442 "Init",
1443 "InitApply(a)",
1444 "InitApply(b)",
1445 "error returned by apiserver during watch: expired: Expired",
1446 "Init",
1447 "InitApply(a)",
1448 "InitDone",
1449 ] {
1450 let (event, next) = step(&api, &config, state).await;
1451 state = next;
1452 let repr = match &event {
1453 Ok(event) => event.to_string(),
1454 Err(err) => err.to_string(),
1455 };
1456 assert_eq!(repr, expected);
1457 if let Ok(event) = &event {
1458 writer.apply_watcher_event(event);
1459 }
1460 }
1461
1462 let mut names: Vec<_> = reader
1463 .state()
1464 .iter()
1465 .filter_map(|cm| cm.metadata.name.clone())
1466 .collect();
1467 names.sort();
1468 assert_eq!(names, ["a".to_string()]);
1469
1470 let records = api.get_recordings();
1471 match &records[..] {
1472 [
1473 Recording::Watch(first, first_version),
1474 Recording::Watch(second, second_version),
1475 ] => {
1476 assert_eq!(first_version, "0");
1477 assert!(first.send_initial_events);
1478 assert_eq!(second_version, "0");
1479 assert!(second.send_initial_events);
1480 }
1481 _ => panic!("unexpected API call sequence {records:?}"),
1482 }
1483 }
1484
1485 #[tokio::test]
1486 async fn listwatch_paginates_initial_list_before_starting_watch() {
1487 let api = TestMode::new(
1488 vec![
1489 Ok(ResultPage::empty()
1490 .items(vec![config_map("a", "1")])
1491 .continue_token("first".into())
1492 .into()),
1493 Ok(ResultPage::empty()
1494 .items(vec![config_map("b", "2")])
1495 .resource_version("2".into())
1496 .continue_token("second".into())
1497 .into()),
1498 Ok(ResultPage::empty()
1499 .items(vec![config_map("c", "3")])
1500 .resource_version("3".into())
1501 .into()),
1502 ],
1503 vec![Sequence::new(vec![SequenceStep::List(
1504 vec![Ok(WatchEvent::Added(config_map("d", "4")))].into(),
1505 )])],
1506 );
1507
1508 let config = Config::default()
1509 .page_size(1)
1510 .labels("app=test")
1511 .fields("metadata.name!=ignored");
1512 let mut state = State::default();
1513
1514 let (event, next) = step(&api, &config, state).await;
1515 state = next;
1516 assert_eq!(event.unwrap().to_string(), "Init");
1517
1518 let (event, next) = step(&api, &config, state).await;
1519 state = next;
1520 assert_eq!(event.unwrap().to_string(), "InitApply(a)");
1521
1522 let (event, next) = step(&api, &config, state).await;
1523 state = next;
1524 assert_eq!(event.unwrap().to_string(), "InitApply(b)");
1525
1526 let (event, next) = step(&api, &config, state).await;
1527 state = next;
1528 assert_eq!(event.unwrap().to_string(), "InitApply(c)");
1529
1530 let (event, next) = step(&api, &config, state).await;
1531 state = next;
1532 assert_eq!(event.unwrap().to_string(), "InitDone");
1533
1534 let (event, _) = step(&api, &config, state).await;
1535 assert_eq!(event.unwrap().to_string(), "Apply(d)");
1536
1537 let records = api.get_recordings();
1538 match &records[..] {
1539 [
1540 Recording::List(first),
1541 Recording::List(second),
1542 Recording::List(third),
1543 Recording::Watch(watch_params, watch_version),
1544 ] => {
1545 assert_eq!(first.continue_token.as_deref(), None);
1546 assert_eq!(second.continue_token.as_deref(), Some("first"));
1547 assert_eq!(third.continue_token.as_deref(), Some("second"));
1548
1549 assert_eq!(first.limit, Some(1));
1550 assert_eq!(first.label_selector.as_deref(), Some("app=test"));
1551 assert_eq!(first.field_selector.as_deref(), Some("metadata.name!=ignored"));
1552
1553 assert_eq!(watch_version, "3");
1554 assert_eq!(watch_params.label_selector.as_deref(), Some("app=test"));
1555 assert_eq!(
1556 watch_params.field_selector.as_deref(),
1557 Some("metadata.name!=ignored")
1558 );
1559 assert!(!watch_params.send_initial_events);
1560 }
1561 _ => panic!("unexpected API call sequence"),
1562 }
1563 }
1564
1565 #[test]
1566 fn to_watch_params_initial_phase_with_streaming_list_sets_send_initial_events() {
1567 let config = Config::default().streaming_lists();
1568 let params = config.to_watch_params(WatchPhase::Initial);
1569 assert!(params.send_initial_events);
1570 }
1571
1572 #[test]
1573 fn to_watch_params_resumed_phase_with_streaming_list_does_not_set_send_initial_events() {
1574 let config = Config::default().streaming_lists();
1575 let params = config.to_watch_params(WatchPhase::Resumed);
1576 assert!(!params.send_initial_events);
1577 }
1578
1579 #[test]
1580 fn to_watch_params_listwatch_mode_does_not_set_send_initial_events() {
1581 let config = Config::default(); let params_initial = config.to_watch_params(WatchPhase::Initial);
1583 let params_resumed = config.to_watch_params(WatchPhase::Resumed);
1584 assert!(!params_initial.send_initial_events);
1585 assert!(!params_resumed.send_initial_events);
1586 }
1587
1588 fn approx_eq(a: Duration, b: Duration) -> bool {
1589 a.abs_diff(b) < Duration::from_micros(100)
1590 }
1591
1592 #[test]
1593 fn exponential_backoff_without_jitter() {
1594 let mut backoff =
1595 ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(1), 2.0, false);
1596
1597 assert!(approx_eq(backoff.next().unwrap(), Duration::from_millis(100)));
1598 assert!(approx_eq(backoff.next().unwrap(), Duration::from_millis(200)));
1599 assert!(approx_eq(backoff.next().unwrap(), Duration::from_millis(400)));
1600 }
1601
1602 #[test]
1603 fn exponential_backoff_with_jitter_applies_randomness() {
1604 let mut backoff =
1605 ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(1), 2.0, true);
1606
1607 let delays: Vec<_> = (0..5).filter_map(|_| backoff.next()).collect();
1608
1609 for d in &delays {
1611 assert!(*d > Duration::ZERO);
1612 }
1613
1614 let exact_values = [
1616 Duration::from_millis(100),
1617 Duration::from_millis(200),
1618 Duration::from_millis(400),
1619 Duration::from_millis(800),
1620 Duration::from_secs(1),
1621 ];
1622
1623 let all_exact = delays
1624 .iter()
1625 .zip(exact_values.iter())
1626 .all(|(d, e)| approx_eq(*d, *e));
1627
1628 assert!(
1629 !all_exact,
1630 "With jitter enabled, delays should not all match exact exponential values"
1631 );
1632 }
1633
1634 #[tokio::test(start_paused = true)]
1635 async fn idle_timeout_returns_item_when_stream_has_data() {
1636 let mut stream = futures::stream::iter(vec![1, 2, 3]);
1637 let result = next_with_idle_timeout(&mut stream, Some(290)).await;
1638 assert_eq!(result, Some(1));
1639 }
1640
1641 #[tokio::test(start_paused = true)]
1642 async fn zero_timeout_does_not_trigger_idle_timeout() {
1643 let mut stream = futures::stream::once(async {
1644 tokio::time::sleep(Duration::from_secs(6)).await;
1645 42
1646 })
1647 .boxed();
1648
1649 let result = next_with_idle_timeout(&mut stream, Some(0)).await;
1650 assert_eq!(result, Some(42));
1651 }
1652
1653 #[tokio::test(start_paused = true)]
1654 async fn idle_timeout_returns_none_on_dead_connection() {
1655 let mut stream = futures::stream::pending::<i32>();
1656 let result = next_with_idle_timeout(&mut stream, Some(290)).await;
1658 assert_eq!(result, None);
1659 }
1660
1661 #[tokio::test(start_paused = true)]
1662 async fn idle_timeout_returns_none_when_stream_ends() {
1663 let mut stream = futures::stream::empty::<i32>();
1664 let result = next_with_idle_timeout(&mut stream, Some(290)).await;
1665 assert_eq!(result, None);
1666 }
1667}