Skip to main content

ProductDecoder

Trait ProductDecoder 

Source
pub trait ProductDecoder:
    Default
    + Send
    + 'static {
    type Snapshot: Send + Sync + 'static;
    type Error: ApplyError + Send + Sync + 'static;

    const PRODUCT: &'static str;

    // Required methods
    fn decode(
        &mut self,
        id: &ConfigId,
        payload: &[u8],
    ) -> Result<(), Self::Error>;
    fn build(self) -> Result<Self::Snapshot, Self::Error>;

    // Provided method
    fn keeps_previous(snapshot: &Self::Snapshot) -> bool { ... }
}
Expand description

Decodes one product’s assigned configurations into a snapshot.

A decoder names the product it decodes, so subscribing with a decoder cannot attach it to the wrong product. Define a decoder in the crate that owns its snapshot type. A decoder for a product whose type has no other owner belongs in this crate instead; otherwise this crate knows no product’s payload.

To evaluate an assignment, the client constructs a decoder with Default, calls decode once per assigned configuration in ascending ConfigId order, and then calls build. On success, keeps_previous decides whether to replace or retain the current snapshot. Ascending order is part of the contract, so a reduction that keeps the last valid configuration is well defined.

A product is decoded again only when its assigned configurations or their contents change, so a rejected assignment is not retried until it changes. When the Agent reports its configuration expired, every product that had configurations is decoded as an empty assignment; a product that already had none is not decoded again. The client then asks the Agent for the full assignment until a response is not expired, so configurations that return are decoded as new.

A rejected snapshot leaves subscribers with the last accepted one, so an implementation must build a snapshot from an empty assignment; see build.

Decoding runs on the client’s worker task and delays polling for every product while it runs, so implementations MUST NOT block. A panic in decode or build is caught: the client discards the decoder, rejects every configuration in the assignment, and publishes nothing, so subscribers keep the last accepted snapshot and are not notified.

§Design

A product’s assignment is several configurations, each with its own payload, and the protocol carries an apply status for each one separately. Decoding therefore accumulates configuration by configuration rather than consuming the assignment as a whole: a configuration that decode rejects is attributed to itself and skipped, and the decoder still builds from the rest. Because the client drives the loop, that attribution cannot be forgotten or misdirected. Whether a rejected configuration invalidates the whole snapshot remains the decoder’s choice: build may reject the snapshot, for example when a configuration it requires was rejected.

Accumulating into the decoder, rather than returning one decoded value per configuration, is what lets a product whose configurations have different shapes hold each of them in a field of its own type instead of funneling them through a shared enum.

§Examples

A product that accepts at most one configuration records every configuration it is given and rejects more than one in build, since only build sees the whole assignment:

use datadog_agent_remote_config::{ConfigId, ProductDecoder};

/// Holds the product's single configuration, if it has one.
#[derive(Default)]
struct SingleDecoder {
    payloads: Vec<(ConfigId, Vec<u8>)>,
}

impl ProductDecoder for SingleDecoder {
    const PRODUCT: &'static str = "EXAMPLE_SINGLE";

    type Snapshot = Option<Vec<u8>>;
    type Error = String;

    fn decode(&mut self, id: &ConfigId, payload: &[u8]) -> Result<(), Self::Error> {
        self.payloads.push((id.clone(), payload.to_vec()));
        Ok(())
    }

    fn build(mut self) -> Result<Self::Snapshot, Self::Error> {
        if self.payloads.len() > 1 {
            return Err(format!("Expected at most one configuration; got {}.", self.payloads.len()));
        }
        Ok(self.payloads.pop().map(|(_, payload)| payload))
    }
}

let mut decoder = SingleDecoder::default();
decoder.decode(&ConfigId::new("a"), b"one").unwrap();
decoder.decode(&ConfigId::new("b"), b"two").unwrap();
assert!(decoder.build().is_err());

Required Associated Constants§

Source

const PRODUCT: &'static str

The product this decoder decodes, by its protocol name, such as APM_SEMANTIC_CORE_DD.

The client requests this product from the Agent and keys the product’s subscription by this name. The client can only subscribe once per product: attempting a second subscription to the same product while the first is live returns Error::AlreadySubscribed, whichever decoder it uses.

Required Associated Types§

Source

type Snapshot: Send + Sync + 'static

The type that holds the product’s decoded configuration.

build produces it from the product’s configurations. keeps_previous decides whether subscribers receive this snapshot or retain the previous one.

Source

type Error: ApplyError + Send + Sync + 'static

The error this product’s decoding and validation produces.

Use String when there is nothing richer to report; a product that wants to describe a failure more precisely for its own diagnostics defines its own type and implements ApplyError for it.

Required Methods§

Source

fn decode(&mut self, id: &ConfigId, payload: &[u8]) -> Result<(), Self::Error>

Accumulates one of the product’s assigned configurations.

The client calls this at most once per distinct id within a single snapshot, then calls build once. A decoder may therefore hold one slot per configuration ID without a second configuration silently displacing the first.

§Errors

Returns Self::Error to reject this configuration alone. The client attributes the rejection to it and skips it, and still decodes the product’s remaining configurations.

Source

fn build(self) -> Result<Self::Snapshot, Self::Error>

Validates the accumulated configurations and produces a snapshot.

Check requirements across configurations here, including whether a required configuration is present when the decoder receives a non-empty assignment. The client also calls this without calling decode when no configurations are assigned (after the backend removes the last one or the Agent reports its configuration expired), or when every assigned configuration was rejected for sharing an ID.

An empty assignment to the decoder means “no configuration,” even if files were assigned but all collided. This method MUST build a snapshot representing that state. If the product should retain its previous configuration on withdrawal, use keeps_previous rather than rejecting the assignment.

The client acknowledges successfully decoded configurations only when this method succeeds.

§Errors

Returns Self::Error to reject the snapshot. The client rejects every successfully decoded configuration with this error’s apply reason; configurations rejected by decode keep their own errors. No new snapshot is published, and subscribers retain the last accepted snapshot.

This method cannot reject selected configurations while publishing the rest. Selective rejection belongs in decode.

Provided Methods§

Source

fn keeps_previous(snapshot: &Self::Snapshot) -> bool

Controls whether an unassigned-marker update preserves the last accepted configuration.

Remote Configuration assigns configuration files to each product. When the backend withdraws the last file, or the Agent reports that its cached configuration has expired, the product’s assignment becomes empty: there is no longer a remote configuration assigned to it.

The decoder must still represent this valid state. Its build can return a marker such as an Unassigned variant in its snapshot type, meaning “no remote configuration is assigned.” This is a value the decoder creates, not an error or a special payload in a file. Another decoder might instead build a snapshot containing its local defaults. Whether either result should replace the previous configuration depends on the product’s rules for withdrawal.

Use this hook to retain the last accepted configuration when the product becomes unassigned. The client calls it for every successfully built snapshot; your decoder identifies its product’s unassigned marker. Return true only for that marker when withdrawal should preserve the previous configuration, and false for ordinary configurations.

  • false (the default): publish this snapshot, replacing the previous one.
  • true: accept this unassigned-marker update without replacing the previous snapshot. Later configuration updates still return false and replace it normally. If no snapshot exists yet, publish the marker.

Trace sampling (APM_SAMPLING) keeps its last sampling settings on withdrawal. Its decoder returns true for Unassigned and false for every actual configuration. Even an assigned {} or JSON null returns false: it is a configuration with no overrides, which restores local settings, not a withdrawal.

The semantic registry (APM_SEMANTIC_CORE_DD) must instead revert to embedded mappings on withdrawal. Its decoder builds an embedded-registry snapshot and uses the default false, so that snapshot replaces the remote registry. Consumers, including APM stats, then use the embedded mappings.

Retention happens here because subscriptions hold only the latest snapshot, not a queue. If configuration A is followed by an Unassigned marker before a sampler reads either, publishing the marker would erase A. Retaining A lets the sampler apply it even after withdrawal. New concrete configurations can still replace one another.

Both return values accept the update, clear any earlier rejection, notify subscribers, and acknowledge configuration verdicts normally. Retaining a snapshot does not mean its configuration remains remotely assigned. The call runs inside the same panic guard as build.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.

Implementors§