1use std::fmt;
9use std::marker::PhantomData;
10use std::sync::Arc;
11
12use datadog_agent_remote_config::{
13 decode_json, ApplyError, ConfigId, JsonError, ProductDecoder, RemoteConfigurationClient, Subscription,
14};
15use serde::de::value::MapAccessDeserializer;
16use serde::de::{self, IgnoredAny, MapAccess, Visitor};
17use serde::{Deserialize, Deserializer};
18
19#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
23struct SamplerEnvConfig {
24 #[serde(
26 default,
27 rename = "priority_sampler_target_TPS",
28 alias = "priority_sampler_target_tps"
29 )]
30 priority_sampler_target_tps: Option<f64>,
31
32 #[serde(default, rename = "errors_sampler_target_TPS", alias = "errors_sampler_target_tps")]
34 errors_sampler_target_tps: Option<f64>,
35
36 #[serde(default)]
38 rare_sampler_enabled: Option<bool>,
39}
40
41#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
43struct EnvAndConfig {
44 #[serde(default, deserialize_with = "null_as_default")]
46 env: String,
47
48 #[serde(default, deserialize_with = "null_or_object")]
50 config: SamplerEnvConfig,
51}
52
53#[derive(Clone, Debug, Default, PartialEq)]
75pub struct SamplerConfig {
76 all_envs: SamplerEnvConfig,
78
79 by_env: Vec<EnvAndConfig>,
81}
82
83impl<'de> Deserialize<'de> for SamplerConfig {
84 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
85 deserializer.deserialize_map(SamplerConfigVisitor)
86 }
87}
88
89struct SamplerConfigVisitor;
91
92impl<'de> Visitor<'de> for SamplerConfigVisitor {
93 type Value = SamplerConfig;
94
95 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str("a sampling configuration object")
97 }
98
99 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
100 #[derive(Deserialize)]
101 #[serde(field_identifier, rename_all = "snake_case")]
102 enum Field {
103 AllEnvs,
104 ByEnv,
105 #[serde(other)]
106 Unknown,
107 }
108
109 let mut all_envs = None;
110 let mut by_env = None;
111 while let Some(field) = map.next_key()? {
112 match field {
113 Field::AllEnvs if all_envs.is_some() => return Err(de::Error::duplicate_field("all_envs")),
114 Field::AllEnvs => {
115 all_envs = Some(
116 map.next_value::<Option<Object<SamplerEnvConfig>>>()?
117 .unwrap_or_default()
118 .0,
119 )
120 }
121 Field::ByEnv if by_env.is_some() => return Err(de::Error::duplicate_field("by_env")),
122 Field::ByEnv => {
123 let entries = map.next_value::<Option<Vec<Option<Object<EnvAndConfig>>>>>()?;
125 by_env = Some(
126 entries
127 .unwrap_or_default()
128 .into_iter()
129 .map(|entry| entry.unwrap_or_default().0)
130 .collect(),
131 );
132 }
133 Field::Unknown => {
134 map.next_value::<IgnoredAny>()?;
135 }
136 }
137 }
138 Ok(SamplerConfig {
139 all_envs: all_envs.unwrap_or_default(),
140 by_env: by_env.unwrap_or_default(),
141 })
142 }
143}
144
145#[derive(Default)]
150struct Object<T>(T);
151
152impl<'de, T: Deserialize<'de>> Deserialize<'de> for Object<T> {
153 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
154 struct ObjectVisitor<T>(PhantomData<T>);
155
156 impl<'de, T: Deserialize<'de>> Visitor<'de> for ObjectVisitor<T> {
157 type Value = T;
158
159 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.write_str("an object")
161 }
162
163 fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
164 T::deserialize(MapAccessDeserializer::new(map))
165 }
166 }
167
168 deserializer.deserialize_map(ObjectVisitor(PhantomData)).map(Self)
169 }
170}
171
172fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
174where
175 D: Deserializer<'de>,
176 T: Default + Deserialize<'de>,
177{
178 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
179}
180
181fn null_or_object<'de, D, T>(deserializer: D) -> Result<T, D::Error>
183where
184 D: Deserializer<'de>,
185 T: Default + Deserialize<'de>,
186{
187 Ok(null_as_default::<D, Object<T>>(deserializer)?.0)
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
192pub struct SamplerSettings {
193 pub target_traces_per_second: f64,
195
196 pub errors_per_second: f64,
198
199 pub rare_sampler_enabled: bool,
201}
202
203impl SamplerConfig {
204 pub fn resolve(&self, env: &str, static_settings: &SamplerSettings) -> SamplerSettings {
230 let for_env = self
231 .by_env
232 .iter()
233 .rfind(|entry| entry.env == env)
234 .map(|entry| &entry.config);
235
236 let pick = |field: fn(&SamplerEnvConfig) -> Option<f64>, fallback: f64| {
237 for_env
238 .and_then(field)
239 .or_else(|| field(&self.all_envs))
240 .unwrap_or(fallback)
241 };
242 SamplerSettings {
243 target_traces_per_second: pick(
244 |c| c.priority_sampler_target_tps,
245 static_settings.target_traces_per_second,
246 ),
247 errors_per_second: pick(|c| c.errors_sampler_target_tps, static_settings.errors_per_second),
248 rare_sampler_enabled: for_env
249 .and_then(|c| c.rare_sampler_enabled)
250 .or(self.all_envs.rare_sampler_enabled)
251 .unwrap_or(static_settings.rare_sampler_enabled),
252 }
253 }
254}
255
256#[derive(Clone, Debug, PartialEq)]
258pub enum RemoteSampling {
259 Unassigned,
264
265 Remote {
269 id: ConfigId,
271
272 config: SamplerConfig,
274 },
275}
276
277#[derive(Debug)]
281pub(super) enum SamplingError {
282 InvalidPayload(JsonError),
284
285 TooManyConfigurations {
287 assigned: usize,
289 },
290
291 NoValidConfiguration,
293}
294
295impl fmt::Display for SamplingError {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 match self {
298 Self::InvalidPayload(error) => error.fmt(f),
299 Self::TooManyConfigurations { assigned } => write!(
300 f,
301 "APM_SAMPLING carries {assigned} configurations; expected at most one."
302 ),
303 Self::NoValidConfiguration => f.write_str("The assigned APM_SAMPLING configuration is not valid."),
304 }
305 }
306}
307
308impl std::error::Error for SamplingError {}
309
310impl ApplyError for SamplingError {
311 fn apply_error(&self) -> String {
312 self.to_string()
313 }
314}
315
316#[derive(Clone, Debug)]
329pub struct TraceSamplingSubscription {
330 subscription: Subscription<RemoteSampling, SamplingError>,
331}
332
333impl TraceSamplingSubscription {
334 pub fn new(client: &RemoteConfigurationClient) -> Result<Self, datadog_agent_remote_config::Error> {
340 Ok(Self::from_subscription(client.subscribe::<SamplingDecoder>()?))
341 }
342
343 pub(super) fn from_subscription(subscription: Subscription<RemoteSampling, SamplingError>) -> Self {
345 Self { subscription }
346 }
347
348 pub fn current(&self) -> Option<Arc<RemoteSampling>> {
356 self.subscription.current()
357 }
358}
359
360impl Default for TraceSamplingSubscription {
361 fn default() -> Self {
362 Self::from_subscription(Subscription::inert())
363 }
364}
365
366#[derive(Default)]
372pub(super) struct SamplingDecoder {
373 assigned: usize,
374 chosen: Option<(ConfigId, SamplerConfig)>,
375}
376
377impl ProductDecoder for SamplingDecoder {
378 const PRODUCT: &'static str = "APM_SAMPLING";
379
380 type Snapshot = RemoteSampling;
381 type Error = SamplingError;
382
383 fn decode(&mut self, id: &ConfigId, payload: &[u8]) -> Result<(), Self::Error> {
384 self.assigned += 1;
386 let config = decode_json::<Option<SamplerConfig>>(payload)
388 .map_err(SamplingError::InvalidPayload)?
389 .unwrap_or_default();
390 self.chosen = Some((id.clone(), config));
391 Ok(())
392 }
393
394 fn build(self) -> Result<Self::Snapshot, Self::Error> {
395 if self.assigned > 1 {
396 return Err(SamplingError::TooManyConfigurations {
398 assigned: self.assigned,
399 });
400 }
401 match self.chosen {
402 Some((id, config)) => Ok(RemoteSampling::Remote { id, config }),
403 None if self.assigned == 0 => Ok(RemoteSampling::Unassigned),
404 None => Err(SamplingError::NoValidConfiguration),
405 }
406 }
407
408 fn keeps_previous(snapshot: &Self::Snapshot) -> bool {
409 matches!(snapshot, RemoteSampling::Unassigned)
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use datadog_agent_remote_config::TestPublisher;
416
417 use super::*;
418 use crate::sampling::test_util::TestSamplingPublisher;
419
420 const STATIC: SamplerSettings = SamplerSettings {
421 target_traces_per_second: 41.0,
422 errors_per_second: 41.0,
423 rare_sampler_enabled: true,
424 };
425
426 fn decode_all(assignment: &[(&str, &str)]) -> (Vec<Result<(), String>>, Result<RemoteSampling, String>) {
427 let mut decoder = SamplingDecoder::default();
428 let verdicts = assignment
429 .iter()
430 .map(|(id, payload)| {
431 decoder
432 .decode(&ConfigId::new(*id), payload.as_bytes())
433 .map_err(|e| e.apply_error())
434 })
435 .collect();
436 (verdicts, decoder.build().map_err(|e| e.apply_error()))
437 }
438
439 fn parse(json: &str) -> SamplerConfig {
440 decode_json(json.as_bytes()).expect("payload should decode")
441 }
442
443 fn env_config(priority: Option<f64>, errors: Option<f64>, rare: Option<bool>) -> SamplerEnvConfig {
444 SamplerEnvConfig {
445 priority_sampler_target_tps: priority,
446 errors_sampler_target_tps: errors,
447 rare_sampler_enabled: rare,
448 }
449 }
450
451 #[test]
452 fn payload_uses_the_agent_field_names() {
453 let config = parse(
454 r#"{"all_envs":{"priority_sampler_target_TPS":1.5,"errors_sampler_target_TPS":2.5,
455 "rare_sampler_enabled":true},
456 "by_env":[{"env":"prod","config":{"priority_sampler_target_TPS":3}}]}"#,
457 );
458 assert_eq!(config.all_envs, env_config(Some(1.5), Some(2.5), Some(true)));
459 assert_eq!(
460 config.by_env,
461 [EnvAndConfig {
462 env: "prod".to_owned(),
463 config: env_config(Some(3.0), None, None)
464 }]
465 );
466 }
467
468 #[test]
469 fn payload_accepts_lowercase_tps_field_names() {
470 let config = parse(r#"{"all_envs":{"priority_sampler_target_tps":1,"errors_sampler_target_tps":2}}"#);
471 assert_eq!(config.all_envs, env_config(Some(1.0), Some(2.0), None));
472 }
473
474 #[test]
475 fn payload_treats_null_and_absent_fields_as_unset() {
476 assert_eq!(parse(r#"{"all_envs":null,"by_env":null}"#), SamplerConfig::default());
477 assert_eq!(
478 parse(r#"{"by_env":[{"env":"prod","config":null}]}"#).by_env,
479 [EnvAndConfig {
480 env: "prod".to_owned(),
481 config: SamplerEnvConfig::default()
482 }]
483 );
484 assert_eq!(
485 parse(
486 r#"{"all_envs":{"priority_sampler_target_TPS":null,"errors_sampler_target_TPS":null,
487 "rare_sampler_enabled":null}}"#
488 ),
489 SamplerConfig::default()
490 );
491 assert_eq!(parse("{}"), SamplerConfig::default());
492 }
493
494 #[test]
495 fn payload_honors_explicit_zero_and_false() {
496 let config = parse(
497 r#"{"all_envs":{"priority_sampler_target_TPS":0,"errors_sampler_target_TPS":0,
498 "rare_sampler_enabled":false}}"#,
499 );
500 assert_eq!(config.all_envs, env_config(Some(0.0), Some(0.0), Some(false)));
501 assert_eq!(
502 config.resolve("", &STATIC),
503 SamplerSettings {
504 target_traces_per_second: 0.0,
505 errors_per_second: 0.0,
506 rare_sampler_enabled: false,
507 }
508 );
509 }
510
511 #[test]
512 fn payload_ignores_unknown_fields() {
513 let config = parse(r#"{"future":1,"all_envs":{"rare_sampler_enabled":true,"other":"x"}}"#);
514 assert_eq!(config.all_envs, env_config(None, None, Some(true)));
515 }
516
517 #[test]
520 fn resolve_prefers_env_then_all_envs_then_static() {
521 let all_envs = env_config(Some(42.0), Some(42.0), Some(true));
522 let for_env = |env: &str, config: SamplerEnvConfig| EnvAndConfig {
523 env: env.to_owned(),
524 config,
525 };
526 let settings = |priority, errors, rare| SamplerSettings {
527 target_traces_per_second: priority,
528 errors_per_second: errors,
529 rare_sampler_enabled: rare,
530 };
531
532 let cases = [
533 (
534 "env entry beats all_envs",
535 all_envs.clone(),
536 vec![for_env("agent-env", env_config(Some(43.0), Some(43.0), Some(false)))],
537 settings(43.0, 43.0, false),
538 ),
539 (
540 "all_envs applies without an env entry",
541 all_envs.clone(),
542 vec![for_env("other-env", env_config(Some(43.0), Some(43.0), Some(false)))],
543 settings(42.0, 42.0, true),
544 ),
545 (
546 "static applies when neither sets a field",
547 SamplerEnvConfig::default(),
548 vec![],
549 STATIC,
550 ),
551 (
552 "a field the env entry leaves unset falls back to all_envs",
553 all_envs.clone(),
554 vec![for_env("agent-env", env_config(Some(43.0), None, None))],
555 settings(43.0, 42.0, true),
556 ),
557 (
558 "a field neither sets falls back to static per field",
559 env_config(None, Some(42.0), None),
560 vec![for_env("agent-env", env_config(Some(43.0), None, Some(false)))],
561 settings(43.0, 42.0, false),
562 ),
563 (
564 "the last matching env entry wins",
565 SamplerEnvConfig::default(),
566 vec![
567 for_env("agent-env", env_config(Some(43.0), Some(43.0), Some(false))),
568 for_env("agent-env", env_config(Some(44.0), None, None)),
569 ],
570 settings(44.0, 41.0, true),
571 ),
572 ];
573
574 for (name, all_envs, by_env, expected) in cases {
575 let config = SamplerConfig { all_envs, by_env };
576 assert_eq!(config.resolve("agent-env", &STATIC), expected, "{name}");
577 }
578 }
579
580 #[test]
581 fn resolve_compares_the_payload_env_as_given() {
582 let config = SamplerConfig {
583 all_envs: SamplerEnvConfig::default(),
584 by_env: vec![EnvAndConfig {
585 env: "Agent-Env".to_owned(),
586 config: env_config(Some(43.0), None, None),
587 }],
588 };
589 assert_eq!(config.resolve("agent-env", &STATIC).target_traces_per_second, 41.0);
590 assert_eq!(config.resolve("Agent-Env", &STATIC).target_traces_per_second, 43.0);
591 }
592
593 #[test]
594 fn empty_assignment_is_unassigned() {
595 assert_eq!(decode_all(&[]).1, Ok(RemoteSampling::Unassigned));
596 }
597
598 #[test]
599 fn single_valid_configuration_is_chosen() {
600 let (verdicts, snapshot) = decode_all(&[("a", r#"{"all_envs":{"rare_sampler_enabled":false}}"#)]);
601 assert_eq!(verdicts, [Ok(())]);
602 assert_eq!(
603 snapshot,
604 Ok(RemoteSampling::Remote {
605 id: ConfigId::new("a"),
606 config: SamplerConfig {
607 all_envs: env_config(None, None, Some(false)),
608 by_env: vec![],
609 },
610 })
611 );
612 }
613
614 #[test]
615 fn null_payload_is_a_configuration_with_nothing_set() {
616 let (verdicts, snapshot) = decode_all(&[("a", "null")]);
617 assert_eq!(verdicts, [Ok(())]);
618 assert_eq!(
619 snapshot,
620 Ok(RemoteSampling::Remote {
621 id: ConfigId::new("a"),
622 config: SamplerConfig::default(),
623 })
624 );
625 }
626
627 #[test]
628 fn null_env_is_the_empty_env() {
629 let (_, snapshot) = decode_all(&[(
630 "a",
631 r#"{"by_env":[{"env":null,"config":{"priority_sampler_target_TPS":3}}]}"#,
632 )]);
633 let Ok(RemoteSampling::Remote { config, .. }) = snapshot else {
634 panic!("the configuration should be chosen: {snapshot:?}");
635 };
636 assert_eq!(
637 config.by_env,
638 [EnvAndConfig {
639 env: String::new(),
640 config: env_config(Some(3.0), None, None)
641 }]
642 );
643 }
644
645 #[test]
646 fn null_by_env_element_is_an_empty_entry() {
647 let (_, snapshot) = decode_all(&[(
648 "a",
649 r#"{"by_env":[null,{"env":"prod","config":{"priority_sampler_target_TPS":3}}]}"#,
650 )]);
651 let Ok(RemoteSampling::Remote { config, .. }) = snapshot else {
652 panic!("the configuration should be chosen: {snapshot:?}");
653 };
654 assert_eq!(
655 config.by_env,
656 [
657 EnvAndConfig::default(),
658 EnvAndConfig {
659 env: "prod".to_owned(),
660 config: env_config(Some(3.0), None, None)
661 }
662 ]
663 );
664 }
665
666 #[test]
667 fn invalid_payload_error_gives_only_its_position() {
668 let (verdicts, snapshot) = decode_all(&[("a", r#"{"all_envs":{"rare_sampler_enabled":"secret"}}"#)]);
669 assert_eq!(
670 verdicts,
671 [Err("Payload is malformed JSON at line 1, column 44.".to_owned())]
672 );
673 assert_eq!(
674 snapshot,
675 Err("The assigned APM_SAMPLING configuration is not valid.".to_owned())
676 );
677 }
678
679 #[test]
680 fn more_than_one_configuration_is_rejected_with_the_count() {
681 let (verdicts, snapshot) = decode_all(&[("a", "{}"), ("b", "not json"), ("c", "{}")]);
682 assert_eq!(
683 verdicts,
684 [
685 Ok(()),
686 Err("Payload is malformed JSON at line 1, column 2.".to_owned()),
687 Ok(())
688 ]
689 );
690 assert_eq!(
691 snapshot,
692 Err("APM_SAMPLING carries 3 configurations; expected at most one.".to_owned())
693 );
694 }
695
696 #[test]
697 fn configurations_that_fail_to_decode_count_toward_the_limit() {
698 let (_, snapshot) = decode_all(&[("a", "not json"), ("b", "{}")]);
699 assert_eq!(
700 snapshot,
701 Err("APM_SAMPLING carries 2 configurations; expected at most one.".to_owned())
702 );
703 }
704
705 #[test]
706 fn default_subscription_never_has_an_update() {
707 assert_eq!(TraceSamplingSubscription::default().current(), None);
708 }
709
710 #[test]
711 fn new_subscribes_to_apm_sampling_until_the_last_clone_drops() {
712 let client = RemoteConfigurationClient::without_agent();
713
714 let subscription = TraceSamplingSubscription::new(&client).expect("the product is not yet subscribed");
715 assert_eq!(client.subscribed_products(), ["APM_SAMPLING"]);
716 assert!(matches!(
717 TraceSamplingSubscription::new(&client),
718 Err(datadog_agent_remote_config::Error::AlreadySubscribed { product }) if product == "APM_SAMPLING"
719 ));
720
721 let clone = subscription.clone();
723 drop(subscription);
724 assert!(TraceSamplingSubscription::new(&client).is_err());
725
726 drop(clone);
727 assert!(client.subscribed_products().is_empty());
728 let _resubscribed = TraceSamplingSubscription::new(&client).expect("the product is free again");
729 assert_eq!(client.subscribed_products(), ["APM_SAMPLING"]);
730 }
731
732 #[test]
733 fn subscription_has_no_update_before_one_is_accepted() {
734 let (_publisher, subscription) = TestSamplingPublisher::new();
735 assert_eq!(subscription.current(), None);
736 }
737
738 #[test]
739 fn clones_observe_accepted_updates() {
740 let (publisher, subscription) = TestSamplingPublisher::new();
741 let clone = subscription.clone();
742
743 publisher.assign([("a", r#"{"all_envs":{"rare_sampler_enabled":true}}"#)]);
744 let first = subscription.current().expect("a configuration is accepted");
745 assert!(matches!(&*first, RemoteSampling::Remote { id, .. } if id == &ConfigId::new("a")));
746 assert!(Arc::ptr_eq(
747 &first,
748 &clone.current().expect("clones see the same update")
749 ));
750 }
751
752 #[test]
757 fn payload_rejects_non_objects_where_the_agent_requires_an_object() {
758 let invalid = [
759 "[]",
760 "[null,null]",
761 "5",
762 r#""text""#,
763 "true",
764 r#"{"all_envs":[0,0,false]}"#,
765 r#"{"all_envs":[]}"#,
766 r#"{"all_envs":5}"#,
767 r#"{"by_env":{}}"#,
768 r#"{"by_env":[[]]}"#,
769 r#"{"by_env":[["prod",{}]]}"#,
770 r#"{"by_env":[5]}"#,
771 r#"{"by_env":[{"env":"prod","config":[0,0,false]}]}"#,
772 r#"{"by_env":[{"env":"prod","config":"x"}]}"#,
773 r#"{"by_env":[{"env":5}]}"#,
774 ];
775 for payload in invalid {
776 let (verdicts, snapshot) = decode_all(&[("a", payload)]);
777 assert!(
778 matches!(&verdicts[..], [Err(reason)] if reason.starts_with("Payload is malformed JSON at line 1")),
779 "{payload}: {verdicts:?}"
780 );
781 assert!(snapshot.is_err(), "{payload}: {snapshot:?}");
782 }
783
784 let valid = [
785 "null",
786 "{}",
787 r#"{"all_envs":null,"by_env":null}"#,
788 r#"{"all_envs":{}}"#,
789 r#"{"by_env":[]}"#,
790 r#"{"by_env":[null,{},{"env":null,"config":null},{"env":"other","config":{}}]}"#,
791 ];
792 for payload in valid {
793 let (verdicts, snapshot) = decode_all(&[("a", payload)]);
794 assert_eq!(verdicts, [Ok(())], "{payload}");
795 let Ok(RemoteSampling::Remote { config, .. }) = snapshot else {
796 panic!("{payload} should be chosen: {snapshot:?}");
797 };
798 assert_eq!(
799 config.resolve("prod", &STATIC),
800 STATIC,
801 "{payload} sets nothing for `prod`"
802 );
803 }
804 }
805
806 #[test]
807 fn payload_rejects_a_duplicate_field() {
808 let (verdicts, _) = decode_all(&[("a", r#"{"by_env":[],"by_env":[]}"#)]);
809 assert!(matches!(&verdicts[..], [Err(_)]), "{verdicts:?}");
810 }
811
812 #[test]
817 fn withdrawal_and_rejection_keep_the_last_accepted_configuration() {
818 let empty = std::iter::empty::<(&str, &str)>;
819 let current_id = |subscription: &TraceSamplingSubscription| match &*subscription.current().expect("accepted") {
820 RemoteSampling::Remote { id, .. } => Some(id.clone()),
821 RemoteSampling::Unassigned => None,
822 };
823 let (publisher, subscription) = TestSamplingPublisher::new();
824
825 publisher.assign(empty());
826 assert_eq!(current_id(&subscription), None, "nothing has been assigned yet");
827
828 publisher.assign([("a", "{}")]);
829 let accepted = subscription.current().expect("a configuration is accepted");
830 for (name, assignment) in [
831 ("an empty assignment", vec![]),
832 ("an array payload", vec![("b", "[]")]),
833 ("an array settings value", vec![("b", r#"{"all_envs":[0,0,false]}"#)]),
834 ("more than one configuration", vec![("b", "{}"), ("c", "{}")]),
835 ] {
836 publisher.assign(assignment);
837 assert!(
838 Arc::ptr_eq(&accepted, &subscription.current().expect("still accepted")),
839 "{name} keeps the last accepted configuration"
840 );
841 }
842
843 publisher.assign([("b", "{}")]);
844 publisher.assign(empty());
845 assert_eq!(
846 current_id(&subscription),
847 Some(ConfigId::new("b")),
848 "the newest configuration is kept"
849 );
850 }
851
852 #[test]
853 fn rejected_assignment_keeps_the_last_update() {
854 let (publisher, subscription) = TestSamplingPublisher::new();
855 publisher.assign([("a", "{}")]);
856 let accepted = subscription.current().expect("a configuration is accepted");
857
858 publisher.assign([("a", "{}"), ("b", "{}")]);
859 assert!(Arc::ptr_eq(
860 &accepted,
861 &subscription.current().expect("the earlier update is retained")
862 ));
863 }
864
865 #[tokio::test]
866 async fn an_empty_assignment_after_a_rejection_clears_it_and_notifies_with_the_last_configuration() {
867 let (publisher, mut subscription) = TestPublisher::<RemoteSampling, SamplingError>::new();
868 publisher.assign::<SamplingDecoder>([("a", "{}")]);
869 let accepted = subscription
870 .changed()
871 .await
872 .expect("one configuration should be accepted");
873
874 publisher.assign::<SamplingDecoder>([("a", "[]")]);
875 assert!(subscription.changed().await.is_err(), "the array payload is rejected");
876
877 publisher.assign::<SamplingDecoder>(std::iter::empty::<(&str, &str)>());
878 let kept = subscription.changed().await.expect("the empty assignment is accepted");
879 assert!(Arc::ptr_eq(&accepted, &kept));
880 }
881}