From 60ea539d0b0bd468dd8815ad1da47b73ec753939 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 15:24:59 -0700 Subject: [PATCH 01/18] feat(forwarder): support forwarder_outdated_file_in_days for stale retry file cleanup Adds forwarder_outdated_file_in_days (default 10) to RetryConfiguration. When disk persistence is enabled, ADP now removes retry-*.json files older than the configured number of days each time it starts, preventing unbounded disk growth after long outages. Set to 0 to disable. Matches the core Agent's behavior in comp/forwarder/defaultforwarder/default_forwarder.go. Closes #1360 Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 11 ++ Cargo.toml | 1 + .../configuration/dogstatsd.md | 1 + lib/saluki-components/Cargo.toml | 1 + .../src/common/datadog/io.rs | 4 + .../src/common/datadog/retry.rs | 169 ++++++++++++++++++ .../src/config_registry/datadog/forwarder.rs | 12 ++ .../config_registry/datadog/unsupported.rs | 14 -- 8 files changed, 199 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62c894a277..711f6eea8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1360,6 +1360,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -4095,6 +4105,7 @@ dependencies = [ "facet", "faster-hex", "figment", + "filetime", "float-cmp", "fnv", "foldhash 0.2.0", diff --git a/Cargo.toml b/Cargo.toml index b2ab748ced..5f3c6b5483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ zstd = { version = "0.13.3", default-features = false } bitmask-enum = { version = "2.2", default-features = false } facet = { version = "0.46.0", default-features = false, features = ["std"] } figment = { version = "0.10", default-features = false } +filetime = { version = "0.2", default-features = false } foldhash = { version = "0.2", default-features = false, features = ["std"] } headers = { version = "0.4", default-features = false } http = { version = "1", default-features = false } diff --git a/docs/agent-data-plane/configuration/dogstatsd.md b/docs/agent-data-plane/configuration/dogstatsd.md index c6851c7265..077c54bfda 100644 --- a/docs/agent-data-plane/configuration/dogstatsd.md +++ b/docs/agent-data-plane/configuration/dogstatsd.md @@ -32,6 +32,7 @@ tracking. | `dogstatsd_windows_pipe_security_descriptor` | Windows named pipe ACL descriptor | [#1466] | | `forwarder_http_protocol` | HTTP version (auto/http1) | [#1361] | | `forwarder_outdated_file_in_days` | Retry file retention (days) | [#1360] | +| `log_format_rfc3339` | Use RFC3339 timestamp format | [#1373] | | `serializer_experimental_use_v3_api.*` | V3 metrics API migration flags | [#1468] | | `sslkeylogfile` | TLS key log file path | [#1372] | | `tls_handshake_timeout` | HTTP TLS handshake timeout | [#178] | diff --git a/lib/saluki-components/Cargo.toml b/lib/saluki-components/Cargo.toml index 9056455e58..3e5cd69f5e 100644 --- a/lib/saluki-components/Cargo.toml +++ b/lib/saluki-components/Cargo.toml @@ -88,6 +88,7 @@ zstd = { workspace = true } [dev-dependencies] derive-where = { workspace = true } +filetime = { workspace = true } proptest = { workspace = true } rcgen = { workspace = true, features = ["crypto", "aws_lc_rs", "pem"] } rustls = { workspace = true } diff --git a/lib/saluki-components/src/common/datadog/io.rs b/lib/saluki-components/src/common/datadog/io.rs index 61eaf11b19..0600b9eb7d 100644 --- a/lib/saluki-components/src/common/datadog/io.rs +++ b/lib/saluki-components/src/common/datadog/io.rs @@ -37,6 +37,7 @@ use super::{ config::ForwarderConfiguration, endpoints::{EndpointRoute, ResolvedEndpoint, RoutableEndpoint}, middleware::{for_resolved_endpoint, with_allow_arbitrary_tags, with_version_info}, + retry::remove_outdated_retry_files, telemetry::{ComponentTelemetry, SharedTransactionQueueTelemetry, TransactionQueueTelemetry}, transaction::{Metadata, Transaction, TransactionBody}, METRIC_INTAKE_PATHS, @@ -392,6 +393,9 @@ async fn run_endpoint_io_loop( // If the storage size is set, enable disk persistence for the retry queue. if config.retry().storage_max_size_bytes() > 0 { + // Remove stale retry files before opening the queue. + remove_outdated_retry_files(config.retry().storage_path(), config.retry().outdated_file_in_days()).await; + retry_queue = retry_queue .with_disk_persistence( PathBuf::from(config.retry().storage_path()), diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index fbbd9adcc5..fd16d93076 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -44,6 +44,10 @@ const fn default_storage_max_disk_ratio() -> f64 { 0.8 } +const fn default_outdated_file_in_days() -> u32 { + 10 +} + /// Datadog Agent-specific forwarder retry configuration. #[derive(Clone, Deserialize, Facet)] #[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))] @@ -130,6 +134,19 @@ pub struct RetryConfiguration { rename = "forwarder_storage_max_disk_ratio" )] storage_max_disk_ratio: f64, + + /// Maximum age in days for retry files on disk before they are deleted at startup. + /// + /// When disk persistence is enabled, ADP removes any `retry-*.json` files in + /// `forwarder_storage_path` that are older than this many days each time it starts. This + /// prevents unbounded disk growth from stale retry data left behind after long outages. + /// + /// Set to `0` to disable age-based cleanup. Defaults to 10. + #[serde( + default = "default_outdated_file_in_days", + rename = "forwarder_outdated_file_in_days" + )] + outdated_file_in_days: u32, } impl RetryConfiguration { @@ -180,6 +197,11 @@ impl RetryConfiguration { self.storage_max_disk_ratio } + /// Returns the maximum age in days for retry files on disk before they are deleted at startup. + pub const fn outdated_file_in_days(&self) -> u32 { + self.outdated_file_in_days + } + /// Creates a new [`DefaultHttpRetryPolicy`] based on the forwarder configuration. /// /// If a [`GenericConfiguration`] is supplied, the policy captures it and checks whether @@ -209,6 +231,73 @@ impl RetryConfiguration { } } +/// Deletes `retry-*.json` files in `storage_path` whose filesystem mtime exceeds `max_age_days`. +/// +/// Called once at startup, before disk persistence is opened, to prevent stale retry data from +/// accumulating after long outages. Errors and unrecognised files are skipped non-fatally. +/// Does nothing if `max_age_days` is 0 or if `storage_path` does not exist. +pub(super) async fn remove_outdated_retry_files(storage_path: &std::path::Path, max_age_days: u32) { + if max_age_days == 0 { + return; + } + + let mut dir = match tokio::fs::read_dir(storage_path).await { + Ok(d) => d, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, + Err(e) => { + tracing::warn!(path = %storage_path.display(), error = %e, "Failed to open retry storage directory for age-based cleanup."); + return; + } + }; + + let cutoff = std::time::SystemTime::now() + .checked_sub(std::time::Duration::from_secs(max_age_days as u64 * 24 * 3600)) + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + + let mut removed = 0u32; + loop { + let entry = match dir.next_entry().await { + Ok(Some(e)) => e, + Ok(None) => break, + Err(e) => { + tracing::warn!(error = %e, "Error reading retry storage directory during age-based cleanup."); + continue; + } + }; + + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + // Only consider retry files written by ADP. + if !name_str.starts_with("retry-") || !name_str.ends_with(".json") { + continue; + } + + let mtime = match entry.metadata().await.and_then(|m| m.modified()) { + Ok(t) => t, + Err(e) => { + tracing::debug!(file = %name_str, error = %e, "Could not read mtime; skipping."); + continue; + } + }; + + if mtime < cutoff { + match tokio::fs::remove_file(entry.path()).await { + Ok(()) => { + tracing::debug!(file = %name_str, "Removed outdated retry file."); + removed += 1; + } + Err(e) => { + tracing::warn!(file = %name_str, error = %e, "Failed to remove outdated retry file."); + } + } + } + } + + if removed > 0 { + tracing::info!(count = removed, max_age_days, "Removed outdated retry files from disk."); + } +} + fn secrets_in_use(config: &GenericConfiguration) -> bool { matches!(config.try_get_typed::("secret_refresh_on_api_key_failure_interval"), Ok(Some(value)) if value > 0) || matches!(config.try_get_typed::("secret_backend_command"), Ok(Some(value)) if !value.trim().is_empty()) @@ -420,4 +509,84 @@ mod tests { // The same policy instance must now retry 403 because the predicate reads the live cached secrets flag. assert!(would_retry(&mut policy, ok_response(StatusCode::FORBIDDEN))); } + + mod outdated_files { + use std::{ + path::Path, + time::{Duration, SystemTime}, + }; + + use tempfile::TempDir; + use tokio::fs; + + use super::super::remove_outdated_retry_files; + + async fn write_file(dir: &Path, name: &str) { + fs::write(dir.join(name), b"{}").await.unwrap(); + } + + async fn set_mtime_old(dir: &Path, name: &str, days_old: u32) { + let path = dir.join(name); + let old_time = SystemTime::now() + .checked_sub(Duration::from_secs(days_old as u64 * 24 * 3600)) + .unwrap(); + let ft = filetime::FileTime::from_system_time(old_time); + filetime::set_file_mtime(&path, ft).unwrap(); + } + + async fn names_in(dir: &Path) -> Vec { + let mut entries = fs::read_dir(dir).await.unwrap(); + let mut names = Vec::new(); + while let Some(e) = entries.next_entry().await.unwrap() { + names.push(e.file_name().to_string_lossy().into_owned()); + } + names.sort(); + names + } + + #[tokio::test] + async fn removes_old_retry_files_only() { + let dir = TempDir::new().unwrap(); + let path = dir.path(); + + write_file(path, "retry-old-1.json").await; + write_file(path, "retry-old-2.json").await; + write_file(path, "retry-recent.json").await; + write_file(path, "other-file.json").await; // non-retry file, must not be touched + + set_mtime_old(path, "retry-old-1.json", 15).await; + set_mtime_old(path, "retry-old-2.json", 11).await; + // retry-recent.json and other-file.json keep their current mtime + + remove_outdated_retry_files(path, 10).await; + + let remaining = names_in(path).await; + assert!(!remaining.contains(&"retry-old-1.json".to_string())); + assert!(!remaining.contains(&"retry-old-2.json".to_string())); + assert!(remaining.contains(&"retry-recent.json".to_string())); + assert!(remaining.contains(&"other-file.json".to_string())); + } + + #[tokio::test] + async fn zero_days_disables_cleanup() { + let dir = TempDir::new().unwrap(); + let path = dir.path(); + + write_file(path, "retry-old.json").await; + set_mtime_old(path, "retry-old.json", 100).await; + + remove_outdated_retry_files(path, 0).await; + + let remaining = names_in(path).await; + assert!(remaining.contains(&"retry-old.json".to_string())); + } + + #[tokio::test] + async fn nonexistent_directory_is_noop() { + let dir = TempDir::new().unwrap(); + let missing = dir.path().join("does-not-exist"); + // Must not panic or error. + remove_outdated_retry_files(&missing, 10).await; + } + } } diff --git a/lib/saluki-components/src/config_registry/datadog/forwarder.rs b/lib/saluki-components/src/config_registry/datadog/forwarder.rs index 6d7fe99767..c66423a0f1 100644 --- a/lib/saluki-components/src/config_registry/datadog/forwarder.rs +++ b/lib/saluki-components/src/config_registry/datadog/forwarder.rs @@ -321,4 +321,16 @@ crate::declare_annotations! { test_json: None, pipeline_affinity: PipelineAffinity::CrossCutting, }; + + /// `forwarder_outdated_file_in_days`—maximum age in days for retry files before deletion at startup. + FORWARDER_OUTDATED_FILE_IN_DAYS = SalukiAnnotation { + schema: &schema::FORWARDER_OUTDATED_FILE_IN_DAYS, + support_level: SupportLevel::Full, + additional_yaml_paths: &[], + env_var_override: None, + used_by: &[structs::FORWARDER_CONFIGURATION], + value_type_override: None, + test_json: None, + pipeline_affinity: PipelineAffinity::CrossCutting, + }; } diff --git a/lib/saluki-components/src/config_registry/datadog/unsupported.rs b/lib/saluki-components/src/config_registry/datadog/unsupported.rs index 2cb1e030a4..809c3e443f 100644 --- a/lib/saluki-components/src/config_registry/datadog/unsupported.rs +++ b/lib/saluki-components/src/config_registry/datadog/unsupported.rs @@ -149,20 +149,6 @@ crate::declare_annotations! { pipeline_affinity: PipelineAffinity::CrossCutting, }; - /// `forwarder_outdated_file_in_days` - retry file retention period. - FORWARDER_OUTDATED_FILE_IN_DAYS = SalukiAnnotation { - schema: &schema::FORWARDER_OUTDATED_FILE_IN_DAYS, - // Retry file retention not implemented. #1360 - support_level: SupportLevel::Incompatible(Severity::Medium), - additional_yaml_paths: &[], - env_var_override: None, - used_by: &[], - value_type_override: None, - test_json: None, - // The forwarder is potentially used by any pipeline. - pipeline_affinity: PipelineAffinity::CrossCutting, - }; - /// `forwarder_retry_queue_capacity_time_interval_sec` - retry queue time-based capacity. FORWARDER_RETRY_QUEUE_CAPACITY_TIME_INTERVAL_SEC = SalukiAnnotation { schema: &schema::FORWARDER_RETRY_QUEUE_CAPACITY_TIME_INTERVAL_SEC, From e4fd5b450a0aeb60bea146b1f26f3514214965dc Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 15:30:10 -0700 Subject: [PATCH 02/18] fix: replace 'mtime' with 'modification time' in comments, fix PR title scope Co-Authored-By: Claude Sonnet 4.6 --- .../src/common/datadog/retry.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index fd16d93076..f9a52c410c 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -231,7 +231,7 @@ impl RetryConfiguration { } } -/// Deletes `retry-*.json` files in `storage_path` whose filesystem mtime exceeds `max_age_days`. +/// Deletes `retry-*.json` files in `storage_path` whose filesystem modification time exceeds `max_age_days`. /// /// Called once at startup, before disk persistence is opened, to prevent stale retry data from /// accumulating after long outages. Errors and unrecognised files are skipped non-fatally. @@ -272,15 +272,15 @@ pub(super) async fn remove_outdated_retry_files(storage_path: &std::path::Path, continue; } - let mtime = match entry.metadata().await.and_then(|m| m.modified()) { + let file_mod_time = match entry.metadata().await.and_then(|m| m.modified()) { Ok(t) => t, Err(e) => { - tracing::debug!(file = %name_str, error = %e, "Could not read mtime; skipping."); + tracing::debug!(file = %name_str, error = %e, "Could not read modification time; skipping."); continue; } }; - if mtime < cutoff { + if file_mod_time < cutoff { match tokio::fs::remove_file(entry.path()).await { Ok(()) => { tracing::debug!(file = %name_str, "Removed outdated retry file."); @@ -525,7 +525,7 @@ mod tests { fs::write(dir.join(name), b"{}").await.unwrap(); } - async fn set_mtime_old(dir: &Path, name: &str, days_old: u32) { + async fn set_file_age(dir: &Path, name: &str, days_old: u32) { let path = dir.join(name); let old_time = SystemTime::now() .checked_sub(Duration::from_secs(days_old as u64 * 24 * 3600)) @@ -554,9 +554,9 @@ mod tests { write_file(path, "retry-recent.json").await; write_file(path, "other-file.json").await; // non-retry file, must not be touched - set_mtime_old(path, "retry-old-1.json", 15).await; - set_mtime_old(path, "retry-old-2.json", 11).await; - // retry-recent.json and other-file.json keep their current mtime + set_file_age(path, "retry-old-1.json", 15).await; + set_file_age(path, "retry-old-2.json", 11).await; + // retry-recent.json and other-file.json are freshly created, so they are not outdated. remove_outdated_retry_files(path, 10).await; @@ -573,7 +573,7 @@ mod tests { let path = dir.path(); write_file(path, "retry-old.json").await; - set_mtime_old(path, "retry-old.json", 100).await; + set_file_age(path, "retry-old.json", 100).await; remove_outdated_retry_files(path, 0).await; From e53f52b703dc3d0b034a055597b3d51775335ef2 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 15:41:43 -0700 Subject: [PATCH 03/18] fix(config): set ValueType::Integer override for forwarder_outdated_file_in_days Schema type: number maps to Float in smoke tests, causing injection of 1.5 which fails to deserialize into u32. Explicit Integer override makes the smoke test inject 42 instead. Co-Authored-By: Claude Sonnet 4.6 --- lib/saluki-components/src/config_registry/datadog/forwarder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/saluki-components/src/config_registry/datadog/forwarder.rs b/lib/saluki-components/src/config_registry/datadog/forwarder.rs index c66423a0f1..e34379a072 100644 --- a/lib/saluki-components/src/config_registry/datadog/forwarder.rs +++ b/lib/saluki-components/src/config_registry/datadog/forwarder.rs @@ -329,7 +329,7 @@ crate::declare_annotations! { additional_yaml_paths: &[], env_var_override: None, used_by: &[structs::FORWARDER_CONFIGURATION], - value_type_override: None, + value_type_override: Some(ValueType::Integer), test_json: None, pipeline_affinity: PipelineAffinity::CrossCutting, }; From fc738328882212b3dcc716f2c20370b2588a0559 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 15:54:27 -0700 Subject: [PATCH 04/18] fix: address code review findings in remove_outdated_retry_files - Scan the per-queue subdirectory (storage_path/{queue_id}) not the root; retry files live at storage_path/{queue_id}/retry-*.json - Use the filename-embedded creation timestamp via decode_timestamped_filename (now pub-exported from saluki-io) instead of filesystem mtime, which can be reset by backup/restore tools - break (not continue) on next_entry() error to avoid potential infinite loop on macOS with persistent readdir errors - Downgrade ENOENT on remove_file to debug; it indicates a concurrent sibling endpoint task already deleted the same file - Update tests to use valid filename-encoded timestamps; remove filetime dep Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 11 -- Cargo.toml | 1 - lib/saluki-components/Cargo.toml | 1 - .../src/common/datadog/io.rs | 6 +- .../src/common/datadog/retry.rs | 111 +++++++++--------- lib/saluki-io/src/net/util/retry/mod.rs | 4 +- lib/saluki-io/src/net/util/retry/queue/mod.rs | 2 +- .../src/net/util/retry/queue/persisted.rs | 2 +- 8 files changed, 64 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 711f6eea8a..62c894a277 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1360,16 +1360,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -4105,7 +4095,6 @@ dependencies = [ "facet", "faster-hex", "figment", - "filetime", "float-cmp", "fnv", "foldhash 0.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5f3c6b5483..b2ab748ced 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,6 @@ zstd = { version = "0.13.3", default-features = false } bitmask-enum = { version = "2.2", default-features = false } facet = { version = "0.46.0", default-features = false, features = ["std"] } figment = { version = "0.10", default-features = false } -filetime = { version = "0.2", default-features = false } foldhash = { version = "0.2", default-features = false, features = ["std"] } headers = { version = "0.4", default-features = false } http = { version = "1", default-features = false } diff --git a/lib/saluki-components/Cargo.toml b/lib/saluki-components/Cargo.toml index 3e5cd69f5e..9056455e58 100644 --- a/lib/saluki-components/Cargo.toml +++ b/lib/saluki-components/Cargo.toml @@ -88,7 +88,6 @@ zstd = { workspace = true } [dev-dependencies] derive-where = { workspace = true } -filetime = { workspace = true } proptest = { workspace = true } rcgen = { workspace = true, features = ["crypto", "aws_lc_rs", "pem"] } rustls = { workspace = true } diff --git a/lib/saluki-components/src/common/datadog/io.rs b/lib/saluki-components/src/common/datadog/io.rs index 0600b9eb7d..24f57987aa 100644 --- a/lib/saluki-components/src/common/datadog/io.rs +++ b/lib/saluki-components/src/common/datadog/io.rs @@ -393,8 +393,10 @@ async fn run_endpoint_io_loop( // If the storage size is set, enable disk persistence for the retry queue. if config.retry().storage_max_size_bytes() > 0 { - // Remove stale retry files before opening the queue. - remove_outdated_retry_files(config.retry().storage_path(), config.retry().outdated_file_in_days()).await; + // Remove stale retry files before opening the queue. Pass the per-queue subdirectory + // (storage_path/{queue_id}) because that is where with_disk_persistence writes files. + let queue_path = config.retry().storage_path().join(&queue_id); + remove_outdated_retry_files(&queue_path, config.retry().outdated_file_in_days()).await; retry_queue = retry_queue .with_disk_persistence( diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index f9a52c410c..0d153a7563 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -8,7 +8,7 @@ use facet::Facet; use http::StatusCode; use saluki_config::GenericConfiguration; use saluki_io::net::util::retry::{ - DefaultHttpRetryPolicy, ExponentialBackoff, HttpRetryPredicate, StandardHttpClassifier, + decode_timestamped_filename, DefaultHttpRetryPolicy, ExponentialBackoff, HttpRetryPredicate, StandardHttpClassifier, }; use serde::Deserialize; use tracing::debug; @@ -231,28 +231,33 @@ impl RetryConfiguration { } } -/// Deletes `retry-*.json` files in `storage_path` whose filesystem modification time exceeds `max_age_days`. +/// Deletes `retry-*.json` files in `queue_path` whose creation timestamp exceeds `max_age_days`. /// -/// Called once at startup, before disk persistence is opened, to prevent stale retry data from -/// accumulating after long outages. Errors and unrecognised files are skipped non-fatally. -/// Does nothing if `max_age_days` is 0 or if `storage_path` does not exist. -pub(super) async fn remove_outdated_retry_files(storage_path: &std::path::Path, max_age_days: u32) { +/// Called at startup, before disk persistence is opened, to prevent stale retry data from +/// accumulating after long outages. `queue_path` must be the per-queue subdirectory (i.e. +/// `forwarder_storage_path/{queue_id}`), not the storage root. Age is determined by the +/// creation timestamp embedded in each filename, not filesystem mtime. Errors are skipped +/// non-fatally. Does nothing if `max_age_days` is 0 or `queue_path` does not exist. +pub(super) async fn remove_outdated_retry_files(queue_path: &std::path::Path, max_age_days: u32) { if max_age_days == 0 { return; } - let mut dir = match tokio::fs::read_dir(storage_path).await { + let mut dir = match tokio::fs::read_dir(queue_path).await { Ok(d) => d, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, Err(e) => { - tracing::warn!(path = %storage_path.display(), error = %e, "Failed to open retry storage directory for age-based cleanup."); + tracing::warn!(path = %queue_path.display(), error = %e, "Failed to open retry queue directory for age-based cleanup."); return; } }; - let cutoff = std::time::SystemTime::now() - .checked_sub(std::time::Duration::from_secs(max_age_days as u64 * 24 * 3600)) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + // Cutoff as nanoseconds since Unix epoch, matching the u128 returned by decode_timestamped_filename. + let now_ns = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000); let mut removed = 0u32; loop { @@ -260,34 +265,31 @@ pub(super) async fn remove_outdated_retry_files(storage_path: &std::path::Path, Ok(Some(e)) => e, Ok(None) => break, Err(e) => { - tracing::warn!(error = %e, "Error reading retry storage directory during age-based cleanup."); - continue; + tracing::warn!(error = %e, "Error reading retry queue directory during age-based cleanup."); + break; } }; - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - // Only consider retry files written by ADP. - if !name_str.starts_with("retry-") || !name_str.ends_with(".json") { - continue; - } - - let file_mod_time = match entry.metadata().await.and_then(|m| m.modified()) { - Ok(t) => t, - Err(e) => { - tracing::debug!(file = %name_str, error = %e, "Could not read modification time; skipping."); - continue; - } + // decode_timestamped_filename returns None for any file that is not a valid ADP retry file. + let file_ts = match decode_timestamped_filename(&entry.path()) { + Some(ts) => ts, + None => continue, }; - if file_mod_time < cutoff { + if file_ts < cutoff_ns { + let name_str = entry.file_name(); + let name = name_str.to_string_lossy(); match tokio::fs::remove_file(entry.path()).await { Ok(()) => { - tracing::debug!(file = %name_str, "Removed outdated retry file."); + tracing::debug!(file = %name, "Removed outdated retry file."); removed += 1; } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // Concurrent cleanup from a sibling endpoint task already deleted it. + tracing::debug!(file = %name, "Retry file already removed by concurrent cleanup."); + } Err(e) => { - tracing::warn!(file = %name_str, error = %e, "Failed to remove outdated retry file."); + tracing::warn!(file = %name, error = %e, "Failed to remove outdated retry file."); } } } @@ -511,27 +513,21 @@ mod tests { } mod outdated_files { - use std::{ - path::Path, - time::{Duration, SystemTime}, - }; + use std::path::Path; + use chrono::{Duration, Utc}; use tempfile::TempDir; use tokio::fs; use super::super::remove_outdated_retry_files; - async fn write_file(dir: &Path, name: &str) { - fs::write(dir.join(name), b"{}").await.unwrap(); + fn retry_filename_days_old(days_old: i64, nonce: u64) -> String { + let ts = Utc::now() - Duration::days(days_old); + format!("retry-{}-{}.json", ts.format("%Y%m%d%H%M%S%f"), nonce) } - async fn set_file_age(dir: &Path, name: &str, days_old: u32) { - let path = dir.join(name); - let old_time = SystemTime::now() - .checked_sub(Duration::from_secs(days_old as u64 * 24 * 3600)) - .unwrap(); - let ft = filetime::FileTime::from_system_time(old_time); - filetime::set_file_mtime(&path, ft).unwrap(); + async fn write_file(dir: &Path, name: &str) { + fs::write(dir.join(name), b"{}").await.unwrap(); } async fn names_in(dir: &Path) -> Vec { @@ -549,22 +545,25 @@ mod tests { let dir = TempDir::new().unwrap(); let path = dir.path(); - write_file(path, "retry-old-1.json").await; - write_file(path, "retry-old-2.json").await; - write_file(path, "retry-recent.json").await; - write_file(path, "other-file.json").await; // non-retry file, must not be touched + let old_1 = retry_filename_days_old(15, 100000001); + let old_2 = retry_filename_days_old(11, 100000002); + let recent = retry_filename_days_old(1, 100000003); - set_file_age(path, "retry-old-1.json", 15).await; - set_file_age(path, "retry-old-2.json", 11).await; - // retry-recent.json and other-file.json are freshly created, so they are not outdated. + write_file(path, &old_1).await; + write_file(path, &old_2).await; + write_file(path, &recent).await; + write_file(path, "other-file.json").await; // not a valid retry filename, must not be touched remove_outdated_retry_files(path, 10).await; let remaining = names_in(path).await; - assert!(!remaining.contains(&"retry-old-1.json".to_string())); - assert!(!remaining.contains(&"retry-old-2.json".to_string())); - assert!(remaining.contains(&"retry-recent.json".to_string())); - assert!(remaining.contains(&"other-file.json".to_string())); + assert!(!remaining.contains(&old_1), "15-day-old file should be removed"); + assert!(!remaining.contains(&old_2), "11-day-old file should be removed"); + assert!(remaining.contains(&recent), "1-day-old file should be kept"); + assert!( + remaining.contains(&"other-file.json".to_string()), + "non-retry file must not be touched" + ); } #[tokio::test] @@ -572,13 +571,13 @@ mod tests { let dir = TempDir::new().unwrap(); let path = dir.path(); - write_file(path, "retry-old.json").await; - set_file_age(path, "retry-old.json", 100).await; + let old = retry_filename_days_old(100, 100000004); + write_file(path, &old).await; remove_outdated_retry_files(path, 0).await; let remaining = names_in(path).await; - assert!(remaining.contains(&"retry-old.json".to_string())); + assert!(remaining.contains(&old), "cleanup disabled; file should survive"); } #[tokio::test] diff --git a/lib/saluki-io/src/net/util/retry/mod.rs b/lib/saluki-io/src/net/util/retry/mod.rs index d8bf1b6a92..7f5a884ce9 100644 --- a/lib/saluki-io/src/net/util/retry/mod.rs +++ b/lib/saluki-io/src/net/util/retry/mod.rs @@ -11,7 +11,9 @@ mod policy; pub use self::policy::{NoopRetryPolicy, RollingExponentialBackoffRetryPolicy}; mod queue; -pub use self::queue::{DiskUsageRetrieverImpl, EventContainer, PushResult, RetryQueue, Retryable}; +pub use self::queue::{ + decode_timestamped_filename, DiskUsageRetrieverImpl, EventContainer, PushResult, RetryQueue, Retryable, +}; /// A batteries-included retry policy suitable for HTTP-based clients. pub type DefaultHttpRetryPolicy = diff --git a/lib/saluki-io/src/net/util/retry/queue/mod.rs b/lib/saluki-io/src/net/util/retry/queue/mod.rs index 9226453f6b..192a5fd16c 100644 --- a/lib/saluki-io/src/net/util/retry/queue/mod.rs +++ b/lib/saluki-io/src/net/util/retry/queue/mod.rs @@ -5,7 +5,7 @@ use serde::{de::DeserializeOwned, Serialize}; use tracing::debug; mod persisted; -pub use self::persisted::DiskUsageRetrieverImpl; +pub use self::persisted::{decode_timestamped_filename, DiskUsageRetrieverImpl}; use self::persisted::{DiskUsageRetriever, DiskUsageRetrieverWrapper, PersistedQueue}; /// A container that holds events. diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index bc048082d0..ebbbc56d15 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -407,7 +407,7 @@ fn generate_timestamped_filename() -> (PathBuf, u128) { (filename, now_ts) } -fn decode_timestamped_filename(path: &Path) -> Option { +pub fn decode_timestamped_filename(path: &Path) -> Option { let filename = path.file_stem()?.to_str()?; let mut filename_parts = filename.split('-'); From 21118631061d82174a33a0289cfda32eb3b27c07 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 15:57:52 -0700 Subject: [PATCH 05/18] docs: fix spelling in remove_outdated_retry_files doc comment Co-Authored-By: Claude Sonnet 4.6 --- lib/saluki-components/src/common/datadog/retry.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index 0d153a7563..32e910ee6b 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -234,10 +234,10 @@ impl RetryConfiguration { /// Deletes `retry-*.json` files in `queue_path` whose creation timestamp exceeds `max_age_days`. /// /// Called at startup, before disk persistence is opened, to prevent stale retry data from -/// accumulating after long outages. `queue_path` must be the per-queue subdirectory (i.e. -/// `forwarder_storage_path/{queue_id}`), not the storage root. Age is determined by the -/// creation timestamp embedded in each filename, not filesystem mtime. Errors are skipped -/// non-fatally. Does nothing if `max_age_days` is 0 or `queue_path` does not exist. +/// accumulating after long outages. `queue_path` must be the per-queue subdirectory under +/// `forwarder_storage_path`, not the storage root. Age is determined by the creation +/// timestamp embedded in each filename, not the filesystem modification time. Errors are +/// skipped non-fatally. Does nothing if `max_age_days` is 0 or `queue_path` does not exist. pub(super) async fn remove_outdated_retry_files(queue_path: &std::path::Path, max_age_days: u32) { if max_age_days == 0 { return; From 25911d9c6bc56c54fb87ef3b7d60cd7661d569c1 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 16:15:19 -0700 Subject: [PATCH 06/18] refactor(forwarder): move outdated retry file cleanup into PersistedQueue::from_root_path Cleanup now lives inside saluki-io alongside the filename format it depends on. No cross-crate exports needed. RetryQueue::with_disk_persistence and PersistedQueue::from_root_path each gain a max_age_days: u32 parameter; io.rs passes outdated_file_in_days() at the single call site. Co-Authored-By: Claude Sonnet 4.6 --- .../src/common/datadog/io.rs | 7 +- .../src/common/datadog/retry.rs | 148 +----------------- lib/saluki-io/src/net/util/retry/mod.rs | 4 +- lib/saluki-io/src/net/util/retry/queue/mod.rs | 6 +- .../src/net/util/retry/queue/persisted.rs | 148 +++++++++++++++++- 5 files changed, 153 insertions(+), 160 deletions(-) diff --git a/lib/saluki-components/src/common/datadog/io.rs b/lib/saluki-components/src/common/datadog/io.rs index 24f57987aa..43eba194ba 100644 --- a/lib/saluki-components/src/common/datadog/io.rs +++ b/lib/saluki-components/src/common/datadog/io.rs @@ -37,7 +37,6 @@ use super::{ config::ForwarderConfiguration, endpoints::{EndpointRoute, ResolvedEndpoint, RoutableEndpoint}, middleware::{for_resolved_endpoint, with_allow_arbitrary_tags, with_version_info}, - retry::remove_outdated_retry_files, telemetry::{ComponentTelemetry, SharedTransactionQueueTelemetry, TransactionQueueTelemetry}, transaction::{Metadata, Transaction, TransactionBody}, METRIC_INTAKE_PATHS, @@ -393,11 +392,6 @@ async fn run_endpoint_io_loop( // If the storage size is set, enable disk persistence for the retry queue. if config.retry().storage_max_size_bytes() > 0 { - // Remove stale retry files before opening the queue. Pass the per-queue subdirectory - // (storage_path/{queue_id}) because that is where with_disk_persistence writes files. - let queue_path = config.retry().storage_path().join(&queue_id); - remove_outdated_retry_files(&queue_path, config.retry().outdated_file_in_days()).await; - retry_queue = retry_queue .with_disk_persistence( PathBuf::from(config.retry().storage_path()), @@ -406,6 +400,7 @@ async fn run_endpoint_io_loop( Arc::new(DiskUsageRetrieverImpl::new(PathBuf::from( config.retry().storage_path(), ))), + config.retry().outdated_file_in_days(), ) .await .unwrap_or_else(|e| { diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index 32e910ee6b..8d171f41cd 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -8,7 +8,7 @@ use facet::Facet; use http::StatusCode; use saluki_config::GenericConfiguration; use saluki_io::net::util::retry::{ - decode_timestamped_filename, DefaultHttpRetryPolicy, ExponentialBackoff, HttpRetryPredicate, StandardHttpClassifier, + DefaultHttpRetryPolicy, ExponentialBackoff, HttpRetryPredicate, StandardHttpClassifier, }; use serde::Deserialize; use tracing::debug; @@ -231,75 +231,6 @@ impl RetryConfiguration { } } -/// Deletes `retry-*.json` files in `queue_path` whose creation timestamp exceeds `max_age_days`. -/// -/// Called at startup, before disk persistence is opened, to prevent stale retry data from -/// accumulating after long outages. `queue_path` must be the per-queue subdirectory under -/// `forwarder_storage_path`, not the storage root. Age is determined by the creation -/// timestamp embedded in each filename, not the filesystem modification time. Errors are -/// skipped non-fatally. Does nothing if `max_age_days` is 0 or `queue_path` does not exist. -pub(super) async fn remove_outdated_retry_files(queue_path: &std::path::Path, max_age_days: u32) { - if max_age_days == 0 { - return; - } - - let mut dir = match tokio::fs::read_dir(queue_path).await { - Ok(d) => d, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, - Err(e) => { - tracing::warn!(path = %queue_path.display(), error = %e, "Failed to open retry queue directory for age-based cleanup."); - return; - } - }; - - // Cutoff as nanoseconds since Unix epoch, matching the u128 returned by decode_timestamped_filename. - let now_ns = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000); - - let mut removed = 0u32; - loop { - let entry = match dir.next_entry().await { - Ok(Some(e)) => e, - Ok(None) => break, - Err(e) => { - tracing::warn!(error = %e, "Error reading retry queue directory during age-based cleanup."); - break; - } - }; - - // decode_timestamped_filename returns None for any file that is not a valid ADP retry file. - let file_ts = match decode_timestamped_filename(&entry.path()) { - Some(ts) => ts, - None => continue, - }; - - if file_ts < cutoff_ns { - let name_str = entry.file_name(); - let name = name_str.to_string_lossy(); - match tokio::fs::remove_file(entry.path()).await { - Ok(()) => { - tracing::debug!(file = %name, "Removed outdated retry file."); - removed += 1; - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - // Concurrent cleanup from a sibling endpoint task already deleted it. - tracing::debug!(file = %name, "Retry file already removed by concurrent cleanup."); - } - Err(e) => { - tracing::warn!(file = %name, error = %e, "Failed to remove outdated retry file."); - } - } - } - } - - if removed > 0 { - tracing::info!(count = removed, max_age_days, "Removed outdated retry files from disk."); - } -} - fn secrets_in_use(config: &GenericConfiguration) -> bool { matches!(config.try_get_typed::("secret_refresh_on_api_key_failure_interval"), Ok(Some(value)) if value > 0) || matches!(config.try_get_typed::("secret_backend_command"), Ok(Some(value)) if !value.trim().is_empty()) @@ -511,81 +442,4 @@ mod tests { // The same policy instance must now retry 403 because the predicate reads the live cached secrets flag. assert!(would_retry(&mut policy, ok_response(StatusCode::FORBIDDEN))); } - - mod outdated_files { - use std::path::Path; - - use chrono::{Duration, Utc}; - use tempfile::TempDir; - use tokio::fs; - - use super::super::remove_outdated_retry_files; - - fn retry_filename_days_old(days_old: i64, nonce: u64) -> String { - let ts = Utc::now() - Duration::days(days_old); - format!("retry-{}-{}.json", ts.format("%Y%m%d%H%M%S%f"), nonce) - } - - async fn write_file(dir: &Path, name: &str) { - fs::write(dir.join(name), b"{}").await.unwrap(); - } - - async fn names_in(dir: &Path) -> Vec { - let mut entries = fs::read_dir(dir).await.unwrap(); - let mut names = Vec::new(); - while let Some(e) = entries.next_entry().await.unwrap() { - names.push(e.file_name().to_string_lossy().into_owned()); - } - names.sort(); - names - } - - #[tokio::test] - async fn removes_old_retry_files_only() { - let dir = TempDir::new().unwrap(); - let path = dir.path(); - - let old_1 = retry_filename_days_old(15, 100000001); - let old_2 = retry_filename_days_old(11, 100000002); - let recent = retry_filename_days_old(1, 100000003); - - write_file(path, &old_1).await; - write_file(path, &old_2).await; - write_file(path, &recent).await; - write_file(path, "other-file.json").await; // not a valid retry filename, must not be touched - - remove_outdated_retry_files(path, 10).await; - - let remaining = names_in(path).await; - assert!(!remaining.contains(&old_1), "15-day-old file should be removed"); - assert!(!remaining.contains(&old_2), "11-day-old file should be removed"); - assert!(remaining.contains(&recent), "1-day-old file should be kept"); - assert!( - remaining.contains(&"other-file.json".to_string()), - "non-retry file must not be touched" - ); - } - - #[tokio::test] - async fn zero_days_disables_cleanup() { - let dir = TempDir::new().unwrap(); - let path = dir.path(); - - let old = retry_filename_days_old(100, 100000004); - write_file(path, &old).await; - - remove_outdated_retry_files(path, 0).await; - - let remaining = names_in(path).await; - assert!(remaining.contains(&old), "cleanup disabled; file should survive"); - } - - #[tokio::test] - async fn nonexistent_directory_is_noop() { - let dir = TempDir::new().unwrap(); - let missing = dir.path().join("does-not-exist"); - // Must not panic or error. - remove_outdated_retry_files(&missing, 10).await; - } - } } diff --git a/lib/saluki-io/src/net/util/retry/mod.rs b/lib/saluki-io/src/net/util/retry/mod.rs index 7f5a884ce9..d8bf1b6a92 100644 --- a/lib/saluki-io/src/net/util/retry/mod.rs +++ b/lib/saluki-io/src/net/util/retry/mod.rs @@ -11,9 +11,7 @@ mod policy; pub use self::policy::{NoopRetryPolicy, RollingExponentialBackoffRetryPolicy}; mod queue; -pub use self::queue::{ - decode_timestamped_filename, DiskUsageRetrieverImpl, EventContainer, PushResult, RetryQueue, Retryable, -}; +pub use self::queue::{DiskUsageRetrieverImpl, EventContainer, PushResult, RetryQueue, Retryable}; /// A batteries-included retry policy suitable for HTTP-based clients. pub type DefaultHttpRetryPolicy = diff --git a/lib/saluki-io/src/net/util/retry/queue/mod.rs b/lib/saluki-io/src/net/util/retry/queue/mod.rs index 192a5fd16c..4be7388122 100644 --- a/lib/saluki-io/src/net/util/retry/queue/mod.rs +++ b/lib/saluki-io/src/net/util/retry/queue/mod.rs @@ -5,7 +5,7 @@ use serde::{de::DeserializeOwned, Serialize}; use tracing::debug; mod persisted; -pub use self::persisted::{decode_timestamped_filename, DiskUsageRetrieverImpl}; +pub use self::persisted::DiskUsageRetrieverImpl; use self::persisted::{DiskUsageRetriever, DiskUsageRetrieverWrapper, PersistedQueue}; /// A container that holds events. @@ -124,7 +124,7 @@ where /// If there is an error initializing the disk persistence layer, an error is returned. pub async fn with_disk_persistence( mut self, root_path: PathBuf, max_disk_size_bytes: u64, storage_max_disk_ratio: f64, - disk_usage_retriever: Arc, + disk_usage_retriever: Arc, max_age_days: u32, ) -> Result { // Make sure the root storage path is non-empty, as otherwise we can't generate a valid path // for the persisted entries in this retry queue. @@ -138,6 +138,7 @@ where max_disk_size_bytes, storage_max_disk_ratio, DiskUsageRetrieverWrapper::new(disk_usage_retriever), + max_age_days, ) .await?; self.persisted_pending = Some(persisted_pending); @@ -434,6 +435,7 @@ mod tests { u64::MAX, 1.0, Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + 0, ) .await .expect("should not fail to create retry queue with disk persistence"); diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index ebbbc56d15..c67d39fcb7 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -111,13 +111,18 @@ where /// shrink the directory to fit the given maximum size, an error is returned. pub async fn from_root_path( root_path: PathBuf, max_on_disk_bytes: u64, storage_max_disk_ratio: f64, - disk_usage_retriever: DiskUsageRetrieverWrapper, + disk_usage_retriever: DiskUsageRetrieverWrapper, max_age_days: u32, ) -> Result { // Make sure the directory exists first. create_directory_recursive(root_path.clone()) .await .with_error_context(|| format!("Failed to create retry directory '{}'.", root_path.display()))?; + // Remove stale retry files before loading state. This must run after directory creation + // but before refresh_entry_state so the queue doesn't load files that are about to be + // removed. + remove_outdated_retry_files(&root_path, max_age_days).await; + let mut persisted_requests = Self { root_path: root_path.clone(), entries: Vec::new(), @@ -407,7 +412,7 @@ fn generate_timestamped_filename() -> (PathBuf, u128) { (filename, now_ts) } -pub fn decode_timestamped_filename(path: &Path) -> Option { +fn decode_timestamped_filename(path: &Path) -> Option { let filename = path.file_stem()?.to_str()?; let mut filename_parts = filename.split('-'); @@ -454,6 +459,61 @@ async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> { .error_context("Failed to spawn directory creation blocking task.")? } +/// Deletes files in `queue_path` whose filename-embedded creation timestamp is older than +/// `max_age_days`. Does nothing if `max_age_days` is 0 or the directory does not exist. +async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { + if max_age_days == 0 { + return; + } + let mut dir = match tokio::fs::read_dir(queue_path).await { + Ok(d) => d, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, + Err(e) => { + warn!(path = %queue_path.display(), error = %e, "Failed to open retry queue directory for age-based cleanup."); + return; + } + }; + let now_ns = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000); + let mut removed = 0u32; + loop { + let entry = match dir.next_entry().await { + Ok(Some(e)) => e, + Ok(None) => break, + Err(e) => { + warn!(error = %e, "Error reading retry queue directory during age-based cleanup."); + break; + } + }; + let file_ts = match decode_timestamped_filename(&entry.path()) { + Some(ts) => ts, + None => continue, + }; + if file_ts < cutoff_ns { + let name_str = entry.file_name(); + let name = name_str.to_string_lossy(); + match tokio::fs::remove_file(entry.path()).await { + Ok(()) => { + debug!(file = %name, "Removed outdated retry file."); + removed += 1; + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + debug!(file = %name, "Retry file already removed by concurrent cleanup."); + } + Err(e) => { + warn!(file = %name, error = %e, "Failed to remove outdated retry file."); + } + } + } + } + if removed > 0 { + info!(count = removed, max_age_days, "Removed outdated retry files from disk."); + } +} + #[cfg(test)] mod tests { use rand::RngExt as _; @@ -518,6 +578,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -557,6 +618,7 @@ mod tests { 1, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -587,6 +649,7 @@ mod tests { 32, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -636,6 +699,7 @@ mod tests { 80, 0.35, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -694,6 +758,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -742,6 +807,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -780,6 +846,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -809,6 +876,7 @@ mod tests { 32, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), + 0, ) .await .expect("should not fail to create persisted queue"); @@ -840,4 +908,80 @@ mod tests { assert_eq!(data, actual); assert_eq!(0, files_in_dir(&root_path).await); } + + mod outdated_file_cleanup { + use std::path::Path; + + use chrono::{Duration, Utc}; + use tempfile::TempDir; + use tokio::fs; + + use super::super::remove_outdated_retry_files; + + fn retry_filename_days_old(days_old: i64, nonce: u64) -> String { + let ts = Utc::now() - Duration::days(days_old); + format!("retry-{}-{}.json", ts.format("%Y%m%d%H%M%S%f"), nonce) + } + + async fn write_file(dir: &Path, name: &str) { + fs::write(dir.join(name), b"{}").await.unwrap(); + } + + async fn names_in(dir: &Path) -> Vec { + let mut entries = fs::read_dir(dir).await.unwrap(); + let mut names = Vec::new(); + while let Some(e) = entries.next_entry().await.unwrap() { + names.push(e.file_name().to_string_lossy().into_owned()); + } + names.sort(); + names + } + + #[tokio::test] + async fn removes_old_retry_files_only() { + let dir = TempDir::new().unwrap(); + let path = dir.path(); + + let old_1 = retry_filename_days_old(15, 100000001); + let old_2 = retry_filename_days_old(11, 100000002); + let recent = retry_filename_days_old(1, 100000003); + + write_file(path, &old_1).await; + write_file(path, &old_2).await; + write_file(path, &recent).await; + write_file(path, "other-file.json").await; // not a valid retry filename, must not be touched + + remove_outdated_retry_files(path, 10).await; + + let remaining = names_in(path).await; + assert!(!remaining.contains(&old_1), "15-day-old file should be removed"); + assert!(!remaining.contains(&old_2), "11-day-old file should be removed"); + assert!(remaining.contains(&recent), "1-day-old file should be kept"); + assert!( + remaining.contains(&"other-file.json".to_string()), + "non-retry file must not be touched" + ); + } + + #[tokio::test] + async fn zero_days_disables_cleanup() { + let dir = TempDir::new().unwrap(); + let path = dir.path(); + + let old = retry_filename_days_old(100, 100000004); + write_file(path, &old).await; + + remove_outdated_retry_files(path, 0).await; + + let remaining = names_in(path).await; + assert!(remaining.contains(&old), "cleanup disabled; file should survive"); + } + + #[tokio::test] + async fn nonexistent_directory_is_noop() { + let dir = TempDir::new().unwrap(); + let missing = dir.path().join("does-not-exist"); + remove_outdated_retry_files(&missing, 10).await; + } + } } From 90346a4729f3b017d1f3bb7a9da79ecc283bf4bb Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 16:17:10 -0700 Subject: [PATCH 07/18] fix(forwarder): match agent behavior: max_age_days=0 deletes all retry files The core Agent's FileRemovalPolicy with outdatedFileDayCount=0 sets the cutoff to now, deleting all retry files. Remove the early-return guard (which incorrectly documented 0 as "disable") to match that behavior. Update the test and field doc accordingly. Co-Authored-By: Claude Sonnet 4.6 --- .../src/common/datadog/retry.rs | 9 ++++--- .../src/net/util/retry/queue/persisted.rs | 27 +++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index 8d171f41cd..4836713614 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -137,11 +137,12 @@ pub struct RetryConfiguration { /// Maximum age in days for retry files on disk before they are deleted at startup. /// - /// When disk persistence is enabled, ADP removes any `retry-*.json` files in - /// `forwarder_storage_path` that are older than this many days each time it starts. This - /// prevents unbounded disk growth from stale retry data left behind after long outages. + /// When disk persistence is enabled, ADP removes any `retry-*.json` files in the + /// per-queue subdirectory of `forwarder_storage_path` that are older than this many days + /// each time it starts. This prevents unbounded disk growth from stale retry data left + /// behind after long outages. /// - /// Set to `0` to disable age-based cleanup. Defaults to 10. + /// Setting this to `0` removes all retry files on startup. Defaults to 10. #[serde( default = "default_outdated_file_in_days", rename = "forwarder_outdated_file_in_days" diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index c67d39fcb7..9bfd64991a 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -460,11 +460,11 @@ async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> { } /// Deletes files in `queue_path` whose filename-embedded creation timestamp is older than -/// `max_age_days`. Does nothing if `max_age_days` is 0 or the directory does not exist. +/// `max_age_days`. Does nothing if the directory does not exist. +/// +/// Setting `max_age_days` to `0` deletes all retry files (cutoff = now), matching the behavior +/// of the core Agent's `FileRemovalPolicy` with `outdatedFileDayCount = 0`. async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { - if max_age_days == 0 { - return; - } let mut dir = match tokio::fs::read_dir(queue_path).await { Ok(d) => d, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, @@ -964,17 +964,28 @@ mod tests { } #[tokio::test] - async fn zero_days_disables_cleanup() { + async fn zero_days_removes_all_retry_files() { + // max_age_days=0 sets cutoff=now, so all retry files (which were created before now) + // are deleted — matching the core Agent's FileRemovalPolicy behavior with + // outdatedFileDayCount=0. let dir = TempDir::new().unwrap(); let path = dir.path(); - let old = retry_filename_days_old(100, 100000004); - write_file(path, &old).await; + let just_created = retry_filename_days_old(0, 100000004); + write_file(path, &just_created).await; + write_file(path, "other-file.json").await; remove_outdated_retry_files(path, 0).await; let remaining = names_in(path).await; - assert!(remaining.contains(&old), "cleanup disabled; file should survive"); + assert!( + !remaining.contains(&just_created), + "0-day cutoff should delete all retry files" + ); + assert!( + remaining.contains(&"other-file.json".to_string()), + "non-retry file must survive" + ); } #[tokio::test] From b297f13f6c45e71e9548a2da8ef2e196051e1cd3 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 16:23:07 -0700 Subject: [PATCH 08/18] docs/test: apply review suggestions - Simplify doc last line to just 'Defaults to 10.' - Use 10 (not 0) in with_disk_persistence test call for clarity Co-Authored-By: Claude Sonnet 4.6 --- lib/saluki-components/src/common/datadog/retry.rs | 2 +- lib/saluki-io/src/net/util/retry/queue/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/saluki-components/src/common/datadog/retry.rs b/lib/saluki-components/src/common/datadog/retry.rs index 4836713614..abfe073faf 100644 --- a/lib/saluki-components/src/common/datadog/retry.rs +++ b/lib/saluki-components/src/common/datadog/retry.rs @@ -142,7 +142,7 @@ pub struct RetryConfiguration { /// each time it starts. This prevents unbounded disk growth from stale retry data left /// behind after long outages. /// - /// Setting this to `0` removes all retry files on startup. Defaults to 10. + /// Defaults to 10. #[serde( default = "default_outdated_file_in_days", rename = "forwarder_outdated_file_in_days" diff --git a/lib/saluki-io/src/net/util/retry/queue/mod.rs b/lib/saluki-io/src/net/util/retry/queue/mod.rs index 4be7388122..6c466564dc 100644 --- a/lib/saluki-io/src/net/util/retry/queue/mod.rs +++ b/lib/saluki-io/src/net/util/retry/queue/mod.rs @@ -435,7 +435,7 @@ mod tests { u64::MAX, 1.0, Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), - 0, + 10, ) .await .expect("should not fail to create retry queue with disk persistence"); From 387cc7773faba946dc5a5c312591276fb36f8270 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 16:25:45 -0700 Subject: [PATCH 09/18] test: flatten outdated file cleanup tests into persisted module test suite Remove the nested mod, match the style of storage_ratio_exceeded and other existing tests (tempfile::tempdir, flat helpers, files_in_dir). Co-Authored-By: Claude Sonnet 4.6 --- .../src/net/util/retry/queue/persisted.rs | 124 +++++++----------- 1 file changed, 49 insertions(+), 75 deletions(-) diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index 9bfd64991a..862a6101f1 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -909,90 +909,64 @@ mod tests { assert_eq!(0, files_in_dir(&root_path).await); } - mod outdated_file_cleanup { - use std::path::Path; + fn retry_filename_days_old(days_old: i64, nonce: u64) -> String { + let ts = chrono::Utc::now() - chrono::Duration::days(days_old); + format!("retry-{}-{}.json", ts.format("%Y%m%d%H%M%S%f"), nonce) + } - use chrono::{Duration, Utc}; - use tempfile::TempDir; - use tokio::fs; + #[tokio::test] + async fn outdated_retry_files_are_removed_on_startup() { + let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); + let root_path = temp_dir.path().to_path_buf(); - use super::super::remove_outdated_retry_files; + let old_1 = retry_filename_days_old(15, 100000001); + let old_2 = retry_filename_days_old(11, 100000002); + let recent = retry_filename_days_old(1, 100000003); + let non_retry = "other-file.json"; - fn retry_filename_days_old(days_old: i64, nonce: u64) -> String { - let ts = Utc::now() - Duration::days(days_old); - format!("retry-{}-{}.json", ts.format("%Y%m%d%H%M%S%f"), nonce) + for name in [&old_1, &old_2, &recent, non_retry] { + tokio::fs::write(root_path.join(name), b"{}").await.unwrap(); } - async fn write_file(dir: &Path, name: &str) { - fs::write(dir.join(name), b"{}").await.unwrap(); - } + // 4 files before cleanup; after removing files older than 10 days, 2 should remain + // (the 1-day-old retry file and the non-retry file). + assert_eq!(4, files_in_dir(&root_path).await); + remove_outdated_retry_files(&root_path, 10).await; + assert_eq!(2, files_in_dir(&root_path).await); - async fn names_in(dir: &Path) -> Vec { - let mut entries = fs::read_dir(dir).await.unwrap(); - let mut names = Vec::new(); - while let Some(e) = entries.next_entry().await.unwrap() { - names.push(e.file_name().to_string_lossy().into_owned()); - } - names.sort(); - names - } + // The non-retry file must survive regardless of age. + assert!(root_path.join(non_retry).exists(), "non-retry file must not be touched"); + assert!(root_path.join(&recent).exists(), "1-day-old retry file should be kept"); + } - #[tokio::test] - async fn removes_old_retry_files_only() { - let dir = TempDir::new().unwrap(); - let path = dir.path(); - - let old_1 = retry_filename_days_old(15, 100000001); - let old_2 = retry_filename_days_old(11, 100000002); - let recent = retry_filename_days_old(1, 100000003); - - write_file(path, &old_1).await; - write_file(path, &old_2).await; - write_file(path, &recent).await; - write_file(path, "other-file.json").await; // not a valid retry filename, must not be touched - - remove_outdated_retry_files(path, 10).await; - - let remaining = names_in(path).await; - assert!(!remaining.contains(&old_1), "15-day-old file should be removed"); - assert!(!remaining.contains(&old_2), "11-day-old file should be removed"); - assert!(remaining.contains(&recent), "1-day-old file should be kept"); - assert!( - remaining.contains(&"other-file.json".to_string()), - "non-retry file must not be touched" - ); - } + #[tokio::test] + async fn zero_age_removes_all_retry_files() { + // max_age_days=0 sets cutoff=now, matching the core Agent's FileRemovalPolicy with + // outdatedFileDayCount=0 — all retry files are deleted, non-retry files are untouched. + let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); + let root_path = temp_dir.path().to_path_buf(); - #[tokio::test] - async fn zero_days_removes_all_retry_files() { - // max_age_days=0 sets cutoff=now, so all retry files (which were created before now) - // are deleted — matching the core Agent's FileRemovalPolicy behavior with - // outdatedFileDayCount=0. - let dir = TempDir::new().unwrap(); - let path = dir.path(); - - let just_created = retry_filename_days_old(0, 100000004); - write_file(path, &just_created).await; - write_file(path, "other-file.json").await; - - remove_outdated_retry_files(path, 0).await; - - let remaining = names_in(path).await; - assert!( - !remaining.contains(&just_created), - "0-day cutoff should delete all retry files" - ); - assert!( - remaining.contains(&"other-file.json".to_string()), - "non-retry file must survive" - ); - } + let retry_file = retry_filename_days_old(0, 100000004); + let non_retry = "other-file.json"; - #[tokio::test] - async fn nonexistent_directory_is_noop() { - let dir = TempDir::new().unwrap(); - let missing = dir.path().join("does-not-exist"); - remove_outdated_retry_files(&missing, 10).await; + for name in [&retry_file, non_retry] { + tokio::fs::write(root_path.join(name), b"{}").await.unwrap(); } + + remove_outdated_retry_files(&root_path, 0).await; + + assert!( + !root_path.join(&retry_file).exists(), + "retry file should be deleted with 0-day cutoff" + ); + assert!(root_path.join(non_retry).exists(), "non-retry file must survive"); + } + + #[tokio::test] + async fn outdated_file_cleanup_is_noop_for_missing_directory() { + let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); + let missing = temp_dir.path().join("does-not-exist"); + // Must not panic or error. + remove_outdated_retry_files(&missing, 10).await; } } From 76e6fb7eeb8a2eaf638881189668b6490aca6e90 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 17:20:41 -0700 Subject: [PATCH 10/18] test: rewrite cleanup tests to go through PersistedQueue::from_root_path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test via the public API (from_root_path) rather than calling the private remove_outdated_retry_files directly. Uses make_persisted_queue helper, FakeData, and DiskUsageRetrieverImpl — consistent with storage_ratio_exceeded and other tests in the module. Co-Authored-By: Claude Sonnet 4.6 --- .../src/net/util/retry/queue/persisted.rs | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index 862a6101f1..74254224be 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -909,64 +909,63 @@ mod tests { assert_eq!(0, files_in_dir(&root_path).await); } - fn retry_filename_days_old(days_old: i64, nonce: u64) -> String { - let ts = chrono::Utc::now() - chrono::Duration::days(days_old); - format!("retry-{}-{}.json", ts.format("%Y%m%d%H%M%S%f"), nonce) + async fn make_persisted_queue(root_path: PathBuf, max_age_days: u32) -> PersistedQueue { + PersistedQueue::::from_root_path( + root_path.clone(), + 1024 * 1024, + 0.8, + DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path))), + max_age_days, + ) + .await + .expect("should not fail to create persisted queue") } #[tokio::test] - async fn outdated_retry_files_are_removed_on_startup() { + async fn persisted_queue_removes_outdated_files_on_initialization() { + let data = FakeData::random(); + let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let old_1 = retry_filename_days_old(15, 100000001); - let old_2 = retry_filename_days_old(11, 100000002); - let recent = retry_filename_days_old(1, 100000003); - let non_retry = "other-file.json"; + // Pre-seed a year-2000 retry file containing valid data that would be loaded as an entry + // if it were not cleaned up first. + let stale_content = serde_json::to_vec(&data).unwrap(); + tokio::fs::write( + root_path.join("retry-20000101000000000000000-100000000.json"), + &stale_content, + ) + .await + .unwrap(); - for name in [&old_1, &old_2, &recent, non_retry] { - tokio::fs::write(root_path.join(name), b"{}").await.unwrap(); - } + assert_eq!(1, files_in_dir(&root_path).await); - // 4 files before cleanup; after removing files older than 10 days, 2 should remain - // (the 1-day-old retry file and the non-retry file). - assert_eq!(4, files_in_dir(&root_path).await); - remove_outdated_retry_files(&root_path, 10).await; - assert_eq!(2, files_in_dir(&root_path).await); + // Initialize the queue with a 10-day age limit. + // The stale file must be deleted before entries are loaded. + let queue = make_persisted_queue(root_path.clone(), 10).await; - // The non-retry file must survive regardless of age. - assert!(root_path.join(non_retry).exists(), "non-retry file must not be touched"); - assert!(root_path.join(&recent).exists(), "1-day-old retry file should be kept"); + assert_eq!(0, files_in_dir(&root_path).await); + assert!(queue.is_empty()); } #[tokio::test] - async fn zero_age_removes_all_retry_files() { - // max_age_days=0 sets cutoff=now, matching the core Agent's FileRemovalPolicy with - // outdatedFileDayCount=0 — all retry files are deleted, non-retry files are untouched. + async fn persisted_queue_zero_age_removes_all_retry_files_on_initialization() { + // max_age_days=0 sets cutoff=now, matching the core Agent's FileRemovalPolicy behavior + // with outdatedFileDayCount=0 — all retry files are removed on startup. + let data = FakeData::random(); + let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let retry_file = retry_filename_days_old(0, 100000004); - let non_retry = "other-file.json"; - - for name in [&retry_file, non_retry] { - tokio::fs::write(root_path.join(name), b"{}").await.unwrap(); - } + // Seed with a freshly-written entry via a normal queue. + let mut seeding_queue = make_persisted_queue(root_path.clone(), 10).await; + let _ = seeding_queue.push(data).await.expect("should not fail to push data"); + assert_eq!(1, files_in_dir(&root_path).await); - remove_outdated_retry_files(&root_path, 0).await; + // Re-open with max_age_days=0: the just-written file must also be deleted. + let queue = make_persisted_queue(root_path.clone(), 0).await; - assert!( - !root_path.join(&retry_file).exists(), - "retry file should be deleted with 0-day cutoff" - ); - assert!(root_path.join(non_retry).exists(), "non-retry file must survive"); - } - - #[tokio::test] - async fn outdated_file_cleanup_is_noop_for_missing_directory() { - let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); - let missing = temp_dir.path().join("does-not-exist"); - // Must not panic or error. - remove_outdated_retry_files(&missing, 10).await; + assert_eq!(0, files_in_dir(&root_path).await); + assert!(queue.is_empty()); } } From 4262122d7ec2c0dbbd64d7bcba1b3c3604126e8c Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 17:50:59 -0700 Subject: [PATCH 11/18] test(datadog): use default max_age_days=10 in non-cleanup queue tests Co-Authored-By: Claude Sonnet 4.6 --- .../src/net/util/retry/queue/persisted.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index 74254224be..fd504a146a 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -578,7 +578,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -618,7 +618,7 @@ mod tests { 1, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -649,7 +649,7 @@ mod tests { 32, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -699,7 +699,7 @@ mod tests { 80, 0.35, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -758,7 +758,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -807,7 +807,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -846,7 +846,7 @@ mod tests { 1024, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); @@ -876,7 +876,7 @@ mod tests { 32, 0.8, DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 0, + 10, ) .await .expect("should not fail to create persisted queue"); From 3e14624c495c7a5718f3673d3d74742d7e21d3fd Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 20:51:26 -0400 Subject: [PATCH 12/18] Apply suggestion from @jszwedko --- lib/saluki-io/src/net/util/retry/queue/persisted.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index fd504a146a..9fb8ba9c34 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -461,9 +461,6 @@ async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> { /// Deletes files in `queue_path` whose filename-embedded creation timestamp is older than /// `max_age_days`. Does nothing if the directory does not exist. -/// -/// Setting `max_age_days` to `0` deletes all retry files (cutoff = now), matching the behavior -/// of the core Agent's `FileRemovalPolicy` with `outdatedFileDayCount = 0`. async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { let mut dir = match tokio::fs::read_dir(queue_path).await { Ok(d) => d, From d24e244c4389c33914dbbdbe41e9ad8be075084a Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Fri, 29 May 2026 17:53:54 -0700 Subject: [PATCH 13/18] test(datadog): inline from_root_path calls and add epoch-edge-case comment Co-Authored-By: Claude Sonnet 4.6 --- .../src/net/util/retry/queue/persisted.rs | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index 9fb8ba9c34..9969c81d5c 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -472,7 +472,7 @@ async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { }; let now_ns = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() + .unwrap_or_default() // clock before epoch: treat cutoff as 0, skipping all deletions .as_nanos(); let cutoff_ns = now_ns.saturating_sub(max_age_days as u128 * 24 * 3600 * 1_000_000_000); let mut removed = 0u32; @@ -906,18 +906,6 @@ mod tests { assert_eq!(0, files_in_dir(&root_path).await); } - async fn make_persisted_queue(root_path: PathBuf, max_age_days: u32) -> PersistedQueue { - PersistedQueue::::from_root_path( - root_path.clone(), - 1024 * 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path))), - max_age_days, - ) - .await - .expect("should not fail to create persisted queue") - } - #[tokio::test] async fn persisted_queue_removes_outdated_files_on_initialization() { let data = FakeData::random(); @@ -939,7 +927,15 @@ mod tests { // Initialize the queue with a 10-day age limit. // The stale file must be deleted before entries are loaded. - let queue = make_persisted_queue(root_path.clone(), 10).await; + let queue = PersistedQueue::::from_root_path( + root_path.clone(), + 1024 * 1024, + 0.8, + DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), + 10, + ) + .await + .expect("should not fail to create persisted queue"); assert_eq!(0, files_in_dir(&root_path).await); assert!(queue.is_empty()); @@ -955,12 +951,28 @@ mod tests { let root_path = temp_dir.path().to_path_buf(); // Seed with a freshly-written entry via a normal queue. - let mut seeding_queue = make_persisted_queue(root_path.clone(), 10).await; + let mut seeding_queue = PersistedQueue::::from_root_path( + root_path.clone(), + 1024 * 1024, + 0.8, + DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), + 10, + ) + .await + .expect("should not fail to create persisted queue"); let _ = seeding_queue.push(data).await.expect("should not fail to push data"); assert_eq!(1, files_in_dir(&root_path).await); // Re-open with max_age_days=0: the just-written file must also be deleted. - let queue = make_persisted_queue(root_path.clone(), 0).await; + let queue = PersistedQueue::::from_root_path( + root_path.clone(), + 1024 * 1024, + 0.8, + DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), + 0, + ) + .await + .expect("should not fail to create persisted queue"); assert_eq!(0, files_in_dir(&root_path).await); assert!(queue.is_empty()); From 85ebb260ae907f4bb07524f052ba85198f4034a1 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 1 Jun 2026 10:01:28 -0700 Subject: [PATCH 14/18] refactor(datadog): address webern's review feedback on forwarder_outdated_file_in_days - Extract PersistedQueueArgs struct to clean up from_root_path signature - Move max_age_days onto PersistedQueue as a field; remove_stale_files takes no args - Expose remove_stale_files as a public method returning count of removed files - Callers (with_disk_persistence) explicitly call remove_stale_files and handle errors - remove_outdated_retry_files propagates dir open/scan errors instead of swallowing them - with_disk_persistence takes PersistedQueueArgs directly Co-Authored-By: Claude Sonnet 4.6 --- .../src/common/datadog/io.rs | 16 +- lib/saluki-io/src/net/util/retry/mod.rs | 4 +- lib/saluki-io/src/net/util/retry/queue/mod.rs | 51 ++-- .../src/net/util/retry/queue/persisted.rs | 245 +++++++++++------- 4 files changed, 180 insertions(+), 136 deletions(-) diff --git a/lib/saluki-components/src/common/datadog/io.rs b/lib/saluki-components/src/common/datadog/io.rs index 43eba194ba..5ecc3ca209 100644 --- a/lib/saluki-components/src/common/datadog/io.rs +++ b/lib/saluki-components/src/common/datadog/io.rs @@ -20,7 +20,7 @@ use saluki_io::net::{ client::http::{into_client_body, HttpClient, HttpClientBuilder}, util::{ middleware::{RetryCircuitBreakerError, RetryCircuitBreakerLayer}, - retry::{DiskUsageRetrieverImpl, PushResult, RetryQueue, Retryable}, + retry::{DiskUsageRetrieverImpl, PersistedQueueArgs, PushResult, RetryQueue, Retryable}, }, }; use saluki_metrics::MetricsBuilder; @@ -393,15 +393,15 @@ async fn run_endpoint_io_loop( // If the storage size is set, enable disk persistence for the retry queue. if config.retry().storage_max_size_bytes() > 0 { retry_queue = retry_queue - .with_disk_persistence( - PathBuf::from(config.retry().storage_path()), - config.retry().storage_max_size_bytes(), - config.retry().storage_max_disk_ratio(), - Arc::new(DiskUsageRetrieverImpl::new(PathBuf::from( + .with_disk_persistence(PersistedQueueArgs { + root_path: PathBuf::from(config.retry().storage_path()), + max_on_disk_bytes: config.retry().storage_max_size_bytes(), + storage_max_disk_ratio: config.retry().storage_max_disk_ratio(), + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(PathBuf::from( config.retry().storage_path(), ))), - config.retry().outdated_file_in_days(), - ) + max_age_days: config.retry().outdated_file_in_days(), + }) .await .unwrap_or_else(|e| { error!(endpoint_url, error = %e, "Failed to initialize disk persistence for retry queue. Transactions will not be persisted."); diff --git a/lib/saluki-io/src/net/util/retry/mod.rs b/lib/saluki-io/src/net/util/retry/mod.rs index d8bf1b6a92..cfa226bdf0 100644 --- a/lib/saluki-io/src/net/util/retry/mod.rs +++ b/lib/saluki-io/src/net/util/retry/mod.rs @@ -11,7 +11,9 @@ mod policy; pub use self::policy::{NoopRetryPolicy, RollingExponentialBackoffRetryPolicy}; mod queue; -pub use self::queue::{DiskUsageRetrieverImpl, EventContainer, PushResult, RetryQueue, Retryable}; +pub use self::queue::{ + DiskUsageRetriever, DiskUsageRetrieverImpl, EventContainer, PersistedQueueArgs, PushResult, RetryQueue, Retryable, +}; /// A batteries-included retry policy suitable for HTTP-based clients. pub type DefaultHttpRetryPolicy = diff --git a/lib/saluki-io/src/net/util/retry/queue/mod.rs b/lib/saluki-io/src/net/util/retry/queue/mod.rs index 6c466564dc..a25a8fc758 100644 --- a/lib/saluki-io/src/net/util/retry/queue/mod.rs +++ b/lib/saluki-io/src/net/util/retry/queue/mod.rs @@ -1,12 +1,12 @@ -use std::{collections::VecDeque, path::PathBuf, sync::Arc}; +use std::collections::VecDeque; use saluki_error::{generic_error, GenericError}; use serde::{de::DeserializeOwned, Serialize}; -use tracing::debug; +use tracing::{debug, info, warn}; mod persisted; -pub use self::persisted::DiskUsageRetrieverImpl; -use self::persisted::{DiskUsageRetriever, DiskUsageRetrieverWrapper, PersistedQueue}; +use self::persisted::PersistedQueue; +pub use self::persisted::{DiskUsageRetriever, DiskUsageRetrieverImpl, PersistedQueueArgs}; /// A container that holds events. /// @@ -117,30 +117,27 @@ where /// provides priority to the most recent entries added to the queue, but allows for bursting over the configured /// in-memory size limit without having to immediately discard entries. /// - /// Files are stored in a subdirectory, with the same name as the given queue name, within the given `root_path`. + /// Files are stored in a subdirectory, with the same name as the given queue name, within `args.root_path`. /// /// # Errors /// /// If there is an error initializing the disk persistence layer, an error is returned. - pub async fn with_disk_persistence( - mut self, root_path: PathBuf, max_disk_size_bytes: u64, storage_max_disk_ratio: f64, - disk_usage_retriever: Arc, max_age_days: u32, - ) -> Result { + pub async fn with_disk_persistence(mut self, mut args: PersistedQueueArgs) -> Result { // Make sure the root storage path is non-empty, as otherwise we can't generate a valid path // for the persisted entries in this retry queue. - if root_path.as_os_str().is_empty() { + if args.root_path.as_os_str().is_empty() { return Err(generic_error!("Storage path cannot be empty.")); } - let queue_root_path = root_path.join(&self.queue_name); - let persisted_pending = PersistedQueue::from_root_path( - queue_root_path, - max_disk_size_bytes, - storage_max_disk_ratio, - DiskUsageRetrieverWrapper::new(disk_usage_retriever), - max_age_days, - ) - .await?; + args.root_path = args.root_path.join(&self.queue_name); + let mut persisted_pending = PersistedQueue::from_root_path(args).await?; + match persisted_pending.remove_stale_files().await { + Ok(removed) if removed > 0 => { + info!(count = removed, "Removed outdated retry files from disk."); + } + Ok(_) => {} + Err(e) => warn!(error = %e, "Failed to remove stale retry files."), + } self.persisted_pending = Some(persisted_pending); Ok(self) } @@ -282,7 +279,7 @@ where #[cfg(test)] mod tests { - use std::path::Path; + use std::{path::Path, sync::Arc}; use rand::RngExt as _; use rand_distr::Alphanumeric; @@ -430,13 +427,13 @@ mod tests { assert_eq!(0, file_count_recursive(&root_path)); let mut retry_queue = RetryQueue::::new("test".to_string(), u64::MAX) - .with_disk_persistence( - root_path.clone(), - u64::MAX, - 1.0, - Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), - 10, - ) + .with_disk_persistence(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: u64::MAX, + storage_max_disk_ratio: 1.0, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 10, + }) .await .expect("should not fail to create retry queue with disk persistence"); diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index 9969c81d5c..3c425924a3 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -85,6 +85,23 @@ impl DiskUsageRetrieverWrapper { } } +/// Arguments for constructing a [`PersistedQueue`]. +pub struct PersistedQueueArgs { + /// Root path under which the queue directory is created. + pub root_path: PathBuf, + /// Maximum total bytes the queue may occupy on disk. + pub max_on_disk_bytes: u64, + /// Maximum fraction of the disk that may be used before writes stop. + pub storage_max_disk_ratio: f64, + /// Provider for total- and available-disk-space queries. + pub disk_usage_retriever: Arc, + /// Maximum age of retry files in days; files older than this are removed on startup. + /// + /// Setting this to `0` removes all retry files (cutoff = now), matching the behavior of the + /// core Agent's `FileRemovalPolicy` with `outdatedFileDayCount = 0`. + pub max_age_days: u32, +} + pub struct PersistedQueue { root_path: PathBuf, entries: Vec, @@ -92,6 +109,7 @@ pub struct PersistedQueue { max_on_disk_bytes: u64, storage_max_disk_ratio: f64, disk_usage_retriever: DiskUsageRetrieverWrapper, + max_age_days: u32, entries_dropped: u64, _entry: PhantomData, } @@ -100,36 +118,39 @@ impl PersistedQueue where T: EventContainer + DeserializeOwned + Serialize, { - /// Creates a new `PersistedQueue` instance from the given root path and maximum size. + /// Creates a new `PersistedQueue` instance from the given arguments. /// /// The root path is created if it doesn't already exist, and is scanned for existing persisted entries. Entries /// are removed (oldest first) until the total size of all scanned entries is within the given maximum size. /// + /// To remove stale retry files on startup, call [`remove_stale_files`][Self::remove_stale_files] after construction. + /// /// # Errors /// /// If there is an error creating the root directory, or scanning it for existing entries, or deleting entries to /// shrink the directory to fit the given maximum size, an error is returned. - pub async fn from_root_path( - root_path: PathBuf, max_on_disk_bytes: u64, storage_max_disk_ratio: f64, - disk_usage_retriever: DiskUsageRetrieverWrapper, max_age_days: u32, - ) -> Result { + pub async fn from_root_path(args: PersistedQueueArgs) -> Result { + let PersistedQueueArgs { + root_path, + max_on_disk_bytes, + storage_max_disk_ratio, + disk_usage_retriever, + max_age_days, + } = args; + // Make sure the directory exists first. create_directory_recursive(root_path.clone()) .await .with_error_context(|| format!("Failed to create retry directory '{}'.", root_path.display()))?; - // Remove stale retry files before loading state. This must run after directory creation - // but before refresh_entry_state so the queue doesn't load files that are about to be - // removed. - remove_outdated_retry_files(&root_path, max_age_days).await; - let mut persisted_requests = Self { root_path: root_path.clone(), entries: Vec::new(), total_on_disk_bytes: 0, max_on_disk_bytes, storage_max_disk_ratio, - disk_usage_retriever, + disk_usage_retriever: DiskUsageRetrieverWrapper::new(disk_usage_retriever), + max_age_days, entries_dropped: 0, _entry: PhantomData, }; @@ -160,6 +181,19 @@ where std::mem::take(&mut self.entries_dropped) } + /// Removes retry files older than `max_age_days` (from [`PersistedQueueArgs`]) from the queue directory and + /// reloads entry state. + /// + /// # Errors + /// + /// Returns an error if the queue directory cannot be opened or scanned. Individual file removal failures are + /// logged as warnings and do not stop the cleanup. + pub async fn remove_stale_files(&mut self) -> Result { + let removed = remove_outdated_retry_files(&self.root_path, self.max_age_days).await?; + self.refresh_entry_state().await.map_err(GenericError::from)?; + Ok(removed) + } + /// Enqueues an entry and persists it to disk. /// /// # Errors @@ -461,13 +495,20 @@ async fn create_directory_recursive(path: PathBuf) -> Result<(), GenericError> { /// Deletes files in `queue_path` whose filename-embedded creation timestamp is older than /// `max_age_days`. Does nothing if the directory does not exist. -async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { +/// +/// Setting `max_age_days` to `0` deletes all retry files (cutoff = now), matching the behavior +/// of the core Agent's `FileRemovalPolicy` with `outdatedFileDayCount = 0`. +async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) -> Result { let mut dir = match tokio::fs::read_dir(queue_path).await { Ok(d) => d, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0), Err(e) => { - warn!(path = %queue_path.display(), error = %e, "Failed to open retry queue directory for age-based cleanup."); - return; + return Err(e).with_error_context(|| { + format!( + "Failed to open retry queue directory '{}' for age-based cleanup.", + queue_path.display() + ) + }); } }; let now_ns = std::time::SystemTime::now() @@ -481,8 +522,7 @@ async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { Ok(Some(e)) => e, Ok(None) => break, Err(e) => { - warn!(error = %e, "Error reading retry queue directory during age-based cleanup."); - break; + return Err(e).with_error_context(|| "Error reading retry queue directory during age-based cleanup."); } }; let file_ts = match decode_timestamped_filename(&entry.path()) { @@ -506,9 +546,7 @@ async fn remove_outdated_retry_files(queue_path: &Path, max_age_days: u32) { } } } - if removed > 0 { - info!(count = removed, max_age_days, "Removed outdated retry files from disk."); - } + Ok(removed) } #[cfg(test)] @@ -570,13 +608,13 @@ mod tests { let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -610,13 +648,13 @@ mod tests { let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -641,13 +679,13 @@ mod tests { let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 32, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 32, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -691,13 +729,13 @@ mod tests { let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 80, - 0.35, - DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 80, + storage_max_disk_ratio: 0.35, + disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -750,13 +788,13 @@ mod tests { let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -799,13 +837,13 @@ mod tests { let root_path = temp_dir.path().to_path_buf(); // Use MockDiskUsageRetriever to avoid disk space ratio causing eviction during push. - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -838,13 +876,13 @@ mod tests { let temp_dir = tempfile::tempdir().expect("should not fail to create temporary directory"); let root_path = temp_dir.path().to_path_buf(); - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -868,13 +906,13 @@ mod tests { let root_path = temp_dir.path().to_path_buf(); // Queue sized to hold only one entry. - let mut persisted_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 32, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(MockDiskUsageRetriever {})), - 10, - ) + let mut persisted_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 32, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(MockDiskUsageRetriever {}), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); @@ -925,17 +963,20 @@ mod tests { assert_eq!(1, files_in_dir(&root_path).await); - // Initialize the queue with a 10-day age limit. - // The stale file must be deleted before entries are loaded. - let queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024 * 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 10, - ) + // Initialize the queue and remove stale files with a 10-day age limit. + let mut queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024 * 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); + queue + .remove_stale_files() + .await + .expect("should not fail to remove stale files"); assert_eq!(0, files_in_dir(&root_path).await); assert!(queue.is_empty()); @@ -951,28 +992,32 @@ mod tests { let root_path = temp_dir.path().to_path_buf(); // Seed with a freshly-written entry via a normal queue. - let mut seeding_queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024 * 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 10, - ) + let mut seeding_queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024 * 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 10, + }) .await .expect("should not fail to create persisted queue"); let _ = seeding_queue.push(data).await.expect("should not fail to push data"); assert_eq!(1, files_in_dir(&root_path).await); - // Re-open with max_age_days=0: the just-written file must also be deleted. - let queue = PersistedQueue::::from_root_path( - root_path.clone(), - 1024 * 1024, - 0.8, - DiskUsageRetrieverWrapper::new(Arc::new(DiskUsageRetrieverImpl::new(root_path.clone()))), - 0, - ) + // Re-open and remove stale files with max_age_days=0: the just-written file must also be deleted. + let mut queue = PersistedQueue::::from_root_path(PersistedQueueArgs { + root_path: root_path.clone(), + max_on_disk_bytes: 1024 * 1024, + storage_max_disk_ratio: 0.8, + disk_usage_retriever: Arc::new(DiskUsageRetrieverImpl::new(root_path.clone())), + max_age_days: 0, + }) .await .expect("should not fail to create persisted queue"); + queue + .remove_stale_files() + .await + .expect("should not fail to remove stale files"); assert_eq!(0, files_in_dir(&root_path).await); assert!(queue.is_empty()); From 6392359ed3e83e67ce85146687ef94935e968779 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 1 Jun 2026 10:03:29 -0700 Subject: [PATCH 15/18] docs(dogstatsd): remove log_format_rfc3339 from forwarder PR Accidentally included during rebase conflict resolution; belongs to #1773. Co-Authored-By: Claude Sonnet 4.6 --- docs/agent-data-plane/configuration/dogstatsd.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/agent-data-plane/configuration/dogstatsd.md b/docs/agent-data-plane/configuration/dogstatsd.md index 077c54bfda..c6851c7265 100644 --- a/docs/agent-data-plane/configuration/dogstatsd.md +++ b/docs/agent-data-plane/configuration/dogstatsd.md @@ -32,7 +32,6 @@ tracking. | `dogstatsd_windows_pipe_security_descriptor` | Windows named pipe ACL descriptor | [#1466] | | `forwarder_http_protocol` | HTTP version (auto/http1) | [#1361] | | `forwarder_outdated_file_in_days` | Retry file retention (days) | [#1360] | -| `log_format_rfc3339` | Use RFC3339 timestamp format | [#1373] | | `serializer_experimental_use_v3_api.*` | V3 metrics API migration flags | [#1468] | | `sslkeylogfile` | TLS key log file path | [#1372] | | `tls_handshake_timeout` | HTTP TLS handshake timeout | [#178] | From 7a1fe2c6ea71cda9280c31bb712bcfeefaa0e94a Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 1 Jun 2026 10:10:40 -0700 Subject: [PATCH 16/18] fix(datadog): remove private intra-doc link in PersistedQueueArgs Co-Authored-By: Claude Sonnet 4.6 --- lib/saluki-io/src/net/util/retry/queue/persisted.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/saluki-io/src/net/util/retry/queue/persisted.rs b/lib/saluki-io/src/net/util/retry/queue/persisted.rs index 3c425924a3..59a36ae41c 100644 --- a/lib/saluki-io/src/net/util/retry/queue/persisted.rs +++ b/lib/saluki-io/src/net/util/retry/queue/persisted.rs @@ -85,7 +85,7 @@ impl DiskUsageRetrieverWrapper { } } -/// Arguments for constructing a [`PersistedQueue`]. +/// Arguments for constructing a persisted retry queue. pub struct PersistedQueueArgs { /// Root path under which the queue directory is created. pub root_path: PathBuf, From 05b801ebfb1322d6eceb77bb7128774af3c2fca6 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 1 Jun 2026 11:02:50 -0700 Subject: [PATCH 17/18] chore: sync third-party license file after main merge Co-Authored-By: Claude Sonnet 4.6 --- LICENSE-3rdparty.csv | 70 -------------------------------------------- 1 file changed, 70 deletions(-) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 6490abcd2b..d108c3c2b5 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -10,19 +10,13 @@ anstyle,https://github.com/rust-cli/anstyle,MIT OR Apache-2.0,The anstyle Author anyhow,https://github.com/dtolnay/anyhow,MIT OR Apache-2.0,David Tolnay arc-swap,https://github.com/vorner/arc-swap,MIT OR Apache-2.0,Michal 'vorner' Vaner argh,https://github.com/google/argh,BSD-3-Clause,"Taylor Cramer , Benjamin Brittain , Erick Tryzelaar " -argh_derive,https://github.com/google/argh,BSD-3-Clause,"Taylor Cramer , Benjamin Brittain , Erick Tryzelaar " -argh_shared,https://github.com/google/argh,BSD-3-Clause,"Taylor Cramer , Benjamin Brittain , Erick Tryzelaar " arrayvec,https://github.com/bluss/arrayvec,MIT OR Apache-2.0,bluss asn1-rs,https://github.com/rusticata/asn1-rs,MIT OR Apache-2.0,Pierre Chifflier -asn1-rs-derive,https://github.com/rusticata/asn1-rs,MIT OR Apache-2.0,Pierre Chifflier -asn1-rs-impl,https://github.com/rusticata/asn1-rs,MIT OR Apache-2.0,Pierre Chifflier async-compression,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman , Allen Bui " async-stream,https://github.com/tokio-rs/async-stream,MIT,Carl Lerche -async-stream-impl,https://github.com/tokio-rs/async-stream,MIT,Carl Lerche async-trait,https://github.com/dtolnay/async-trait,MIT OR Apache-2.0,David Tolnay atomic,https://github.com/Amanieu/atomic-rs,Apache-2.0 OR MIT,Amanieu d'Antras atomic-waker,https://github.com/smol-rs/atomic-waker,Apache-2.0 OR MIT,"Stjepan Glavina , Contributors to futures-rs" -aws-lc-fips-sys,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC) AND OpenSSL,AWS-LC aws-lc-rs,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC),AWS-LibCrypto aws-lc-sys,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC) AND Apache-2.0 AND MIT AND BSD-3-Clause AND (Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0),AWS-LC axum,https://github.com/tokio-rs/axum,MIT,The axum Authors @@ -36,7 +30,6 @@ bitflags,https://github.com/bitflags/bitflags,MIT OR Apache-2.0,The Rust Project bitmask-enum,https://github.com/Lukas3674/rust-bitmask-enum,MIT OR Apache-2.0,Lukas3674 block-buffer,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers bollard,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors -bollard-stubs,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors bs58,https://github.com/Nullus157/bs58-rs,MIT OR Apache-2.0,The bs58 Authors bumpalo,https://github.com/fitzgen/bumpalo,MIT OR Apache-2.0,Nick Fitzgerald byte-unit,https://github.com/magiclen/byte-unit,MIT,Magic Len @@ -46,28 +39,21 @@ bytes,https://github.com/tokio-rs/bytes,MIT,"Carl Lerche , Se bytesize,https://github.com/bytesize-rs/bytesize,Apache-2.0,"Hyunsik Choi , MrCroxx , Rob Ede " cast,https://github.com/japaric/cast.rs,MIT OR Apache-2.0,Jorge Aparicio cc,https://github.com/rust-lang/cc-rs,MIT OR Apache-2.0,Alex Crichton -cexpr,https://github.com/jethrogb/rust-cexpr,Apache-2.0 OR MIT,Jethro Beekman cfg-if,https://github.com/rust-lang/cfg-if,MIT OR Apache-2.0,Alex Crichton chacha20,https://github.com/RustCrypto/stream-ciphers,MIT OR Apache-2.0,RustCrypto Developers chrono,https://github.com/chronotope/chrono,MIT OR Apache-2.0,The chrono Authors chrono-tz,https://github.com/chronotope/chrono-tz,MIT OR Apache-2.0,The chrono-tz Authors chumsky,https://codeberg.org/zesterer/chumsky,MIT,"Joshua Barretto , Elijah Hartvigsen " ciborium,https://github.com/enarx/ciborium,Apache-2.0,Nathaniel McCallum -ciborium-io,https://github.com/enarx/ciborium,Apache-2.0,Nathaniel McCallum -ciborium-ll,https://github.com/enarx/ciborium,Apache-2.0,Nathaniel McCallum -clang-sys,https://github.com/KyleMayes/clang-sys,Apache-2.0,Kyle Mayes clap,https://github.com/clap-rs/clap,MIT OR Apache-2.0,The clap Authors clap_builder,https://github.com/clap-rs/clap,MIT OR Apache-2.0,The clap_builder Authors clap_lex,https://github.com/clap-rs/clap,MIT OR Apache-2.0,The clap_lex Authors colored,https://github.com/mackwic/colored,MPL-2.0,Thomas Wickham combine,https://github.com/Marwes/combine,MIT,Markus Westerlind comfy-table,https://github.com/nukesor/comfy-table,MIT,Arne Beer -compression-codecs,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman , Allen Bui " -compression-core,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman , Allen Bui " const-fnv1a-hash,https://github.com/HindrikStegenga/const-fnv1a-hash,MIT,The const-fnv1a-hash Authors const-hex,https://github.com/danipopes/const-hex,MIT OR Apache-2.0,DaniPopes <57450786+DaniPopes@users.noreply.github.com> core-foundation,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers -core-foundation-sys,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers cpufeatures,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers crc32fast,https://github.com/srijs/rust-crc32fast,MIT OR Apache-2.0,"Sam Rijs , Alex Crichton " criterion-plot,https://github.com/criterion-rs/criterion.rs,Apache-2.0 OR MIT,"Jorge Aparicio , Brook Heisler " @@ -81,14 +67,11 @@ crossterm,https://github.com/crossterm-rs/crossterm,MIT,T. Post crunchy,https://github.com/eira-fransham/crunchy,MIT,Eira Fransham crypto-common,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers darling,https://github.com/TedDriggs/darling,MIT,Ted Driggs -darling_core,https://github.com/TedDriggs/darling,MIT,Ted Driggs -darling_macro,https://github.com/TedDriggs/darling,MIT,Ted Driggs data-encoding,https://github.com/ia0/data-encoding,MIT,Julien Cretin der-parser,https://github.com/rusticata/der-parser,MIT OR Apache-2.0,Pierre Chifflier deranged,https://github.com/jhpratt/deranged,MIT OR Apache-2.0,Jacob Pratt derive-ex,https://github.com/frozenlib/derive-ex,MIT OR Apache-2.0,frozenlib derive_more,https://github.com/JelteF/derive_more,MIT,Jelte Fennema -derive_more-impl,https://github.com/JelteF/derive_more,MIT,Jelte Fennema digest,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers displaydoc,https://github.com/yaahc/displaydoc,MIT OR Apache-2.0,Jane Lusby document-features,https://github.com/slint-ui/document-features,MIT OR Apache-2.0,Slint Developers @@ -111,7 +94,6 @@ flate2,https://github.com/rust-lang/flate2-rs,MIT OR Apache-2.0,"Alex Crichton < float-cmp,https://github.com/mikedilger/float-cmp,MIT,Mike Dilger fnv,https://github.com/servo/rust-fnv,Apache-2.0 OR MIT,Alex Crichton foldhash,https://github.com/orlp/foldhash,Zlib,Orson Peters -form_urlencoded,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers fs4,https://github.com/al8n/fs4-rs,MIT OR Apache-2.0,"Dan Burkert , Al Liu " futures,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures Authors futures-channel,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-channel Authors @@ -122,7 +104,6 @@ futures-macro,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futu futures-sink,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-sink Authors futures-task,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-task Authors futures-util,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-util Authors -generator,https://github.com/Xudong-Huang/generator-rs,MIT OR Apache-2.0,Xudong Huang generic-array,https://github.com/fizyk20/generic-array,MIT,"Bartłomiej Kamiński , Aaron Trent " getrandom,https://github.com/rust-random/getrandom,MIT OR Apache-2.0,The Rand Project Developers gimli,https://github.com/gimli-rs/gimli,MIT OR Apache-2.0,The gimli Authors @@ -133,7 +114,6 @@ hash32,https://github.com/japaric/hash32,MIT OR Apache-2.0,Jorge Aparicio hashbrown,https://github.com/rust-lang/hashbrown,MIT OR Apache-2.0,The hashbrown Authors headers,https://github.com/hyperium/headers,MIT,Sean McArthur -headers-core,https://github.com/hyperium/headers,MIT,Sean McArthur heapless,https://github.com/rust-embedded/heapless,MIT OR Apache-2.0,"Jorge Aparicio , Per Lindgren , Emil Fresk " heck,https://github.com/withoutboats/heck,MIT OR Apache-2.0,The heck Authors hex,https://github.com/KokaKiwi/rust-hex,MIT OR Apache-2.0,KokaKiwi @@ -143,7 +123,6 @@ hickory-resolver,https://github.com/hickory-dns/hickory-dns,MIT OR Apache-2.0,Th home,https://github.com/rust-lang/cargo,MIT OR Apache-2.0,Brian Anderson http,https://github.com/hyperium/http,MIT OR Apache-2.0,"Alex Crichton , Carl Lerche , Sean McArthur " http-body,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " -http-body-util,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " http-serde-ext,https://github.com/andrewtoth/http-serde-ext,MIT,Andrew Toth httparse,https://github.com/seanmonstar/httparse,MIT OR Apache-2.0,Sean McArthur httpdate,https://github.com/pyfisch/httpdate,MIT OR Apache-2.0,Pyfisch @@ -167,7 +146,6 @@ icu_provider,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project id-arena,https://github.com/fitzgen/id-arena,MIT OR Apache-2.0,"Nick Fitzgerald , Aleksey Kladov " iddqd,https://github.com/oxidecomputer/iddqd,MIT OR Apache-2.0,The iddqd Authors ident_case,https://github.com/TedDriggs/ident_case,MIT OR Apache-2.0,Ted Driggs -idna,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers idna_adapter,https://github.com/hsivonen/idna_adapter,Apache-2.0 OR MIT,The rust-url developers impls,https://github.com/nvzqz/impls,MIT OR Apache-2.0,"Nikolai Vazquez, Nadrieril Feneanar" indexmap,https://github.com/indexmap-rs/indexmap,Apache-2.0 OR MIT,The indexmap Authors @@ -177,7 +155,6 @@ ipnet,https://github.com/krisprice/ipnet,MIT OR Apache-2.0,Kris Price jiff,https://github.com/BurntSushi/jiff,Unlicense OR MIT,Andrew Gallant -jiff-static,https://github.com/BurntSushi/jiff,Unlicense OR MIT,Andrew Gallant jni,https://github.com/jni-rs/jni-rs,MIT OR Apache-2.0,jni team jni-macros,https://github.com/jni-rs/jni-rs,MIT OR Apache-2.0,The jni-macros Authors jni-sys,https://github.com/jni-rs/jni-sys,MIT OR Apache-2.0,"Steven Fackler , Robert Bragg " @@ -188,23 +165,16 @@ jsonpath-rust,https://github.com/besok/jsonpath-rust,MIT,BorisZhguchev keccak,https://github.com/RustCrypto/sponges,Apache-2.0 OR MIT,RustCrypto Developers kube,https://github.com/kube-rs/kube,Apache-2.0,"clux , Natalie Klestrup Röijezon , kazk " -kube-client,https://github.com/kube-rs/kube,Apache-2.0,"clux , Natalie Klestrup Röijezon , kazk " -kube-core,https://github.com/kube-rs/kube,Apache-2.0,"clux , Natalie Klestrup Röijezon , kazk " lading-payload,https://github.com/datadog/lading,MIT,"Brian L. Troutwine , George Hahn leb128fmt,https://github.com/bluk/leb128fmt,MIT OR Apache-2.0,Bryant Luk libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The Rust Project Developers -libloading,https://github.com/nagisa/rust_libloading,ISC,Simonas Kazlauskas libm,https://github.com/rust-lang/compiler-builtins,MIT,"Alex Crichton , Amanieu d'Antras , Jorge Aparicio , Trevor Gross " linux-raw-sys,https://github.com/sunfishcode/linux-raw-sys,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman litemap,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers litrs,https://github.com/LukasKalbertodt/litrs,MIT OR Apache-2.0,Lukas Kalbertodt -lock_api,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras log,https://github.com/rust-lang/log,MIT OR Apache-2.0,The Rust Project Developers logos,https://github.com/maciejhirsz/logos,MIT OR Apache-2.0,"Maciej Hirsz , Jérome Eertmans (maintainer) " -logos-codegen,https://github.com/maciejhirsz/logos,MIT OR Apache-2.0,"Maciej Hirsz , Jérome Eertmans (maintainer) " -logos-derive,https://github.com/maciejhirsz/logos,MIT OR Apache-2.0,"Maciej Hirsz , Jérome Eertmans (maintainer) " -loom,https://github.com/tokio-rs/loom,MIT,Carl Lerche lru-slab,https://github.com/Ralith/lru-slab,MIT OR Apache-2.0 OR Zlib,Benjamin Saunders mach2,https://github.com/JohnTitor/mach2,BSD-2-Clause OR MIT OR Apache-2.0,The mach2 Authors matchers,https://github.com/hawkw/matchers,MIT,Eliza Weisman @@ -212,7 +182,6 @@ matchit,https://github.com/ibraheemdev/matchit,MIT AND BSD-3-Clause,Ibraheem Ahm matrixmultiply,https://github.com/bluss/matrixmultiply,MIT OR Apache-2.0,"bluss, R. Janis Goldschmidt" memchr,https://github.com/BurntSushi/memchr,Unlicense OR MIT,"Andrew Gallant , bluss" metrics,https://github.com/metrics-rs/metrics,MIT,Toby Lawrence -metrics-util,https://github.com/metrics-rs/metrics,MIT,Toby Lawrence mime,https://github.com/hyperium/mime,MIT OR Apache-2.0,Sean McArthur minimal-lexical,https://github.com/Alexhuszagh/minimal-lexical,MIT OR Apache-2.0,Alex Huszagh miniz_oxide,https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide,MIT OR Zlib OR Apache-2.0,"Frommi , oyvindln , Rich Geldreich richgel99@gmail.com" @@ -246,26 +215,18 @@ ordered-float,https://github.com/reem/rust-ordered-float,MIT,"Jonathan Reem papaya,https://github.com/ibraheemdev/papaya,MIT,Ibraheem Ahmed parking_lot,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras -parking_lot_core,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras paste,https://github.com/dtolnay/paste,MIT OR Apache-2.0,David Tolnay pear,https://github.com/SergioBenitez/Pear,MIT OR Apache-2.0,Sergio Benitez pear_codegen,https://github.com/SergioBenitez/Pear,MIT OR Apache-2.0,Sergio Benitez pem,https://github.com/jcreekmore/pem-rs,MIT,Jonathan Creekmore -percent-encoding,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers pest,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice -pest_derive,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice -pest_generator,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice -pest_meta,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice petgraph,https://github.com/petgraph/petgraph,MIT OR Apache-2.0,"bluss, mitchmindtree" phf,https://github.com/rust-phf/rust-phf,MIT,Steven Fackler -phf_shared,https://github.com/rust-phf/rust-phf,MIT,Steven Fackler piecemeal,https://github.com/tobz/piecemeal,Apache-2.0,Toby Lawrence pin-project,https://github.com/taiki-e/pin-project,Apache-2.0 OR MIT,The pin-project Authors pin-project-internal,https://github.com/taiki-e/pin-project,Apache-2.0 OR MIT,The pin-project-internal Authors pin-project-lite,https://github.com/taiki-e/pin-project-lite,Apache-2.0 OR MIT,The pin-project-lite Authors plotters,https://github.com/plotters-rs/plotters,MIT,Hao Hou -plotters-backend,https://github.com/plotters-rs/plotters,MIT,Hao Hou -plotters-svg,https://github.com/plotters-rs/plotters,MIT,Hao Hou portable-atomic,https://github.com/taiki-e/portable-atomic,Apache-2.0 OR MIT,The portable-atomic Authors portable-atomic-util,https://github.com/taiki-e/portable-atomic-util,Apache-2.0 OR MIT,The portable-atomic-util Authors potential_utf,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -277,12 +238,8 @@ proc-macro2,https://github.com/dtolnay/proc-macro2,MIT OR Apache-2.0,"David Toln proc-macro2-diagnostics,https://github.com/SergioBenitez/proc-macro2-diagnostics,MIT OR Apache-2.0,Sergio Benitez proptest,https://github.com/proptest-rs/proptest,MIT OR Apache-2.0,Jason Lingle prost,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " -prost-build,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " -prost-derive,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " -prost-types,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " protobuf,https://github.com/stepancheg/rust-protobuf,MIT,Stepan Koltsov protobuf-parse,https://github.com/stepancheg/rust-protobuf/tree/master/protobuf-parse,MIT,Stepan Koltsov -protobuf-support,https://github.com/stepancheg/rust-protobuf,MIT,Stepan Koltsov quanta,https://github.com/metrics-rs/quanta,MIT,Toby Lawrence quick_cache,https://github.com/arthurprs/quick-cache,MIT,Arthur Silva quinn,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn Authors @@ -292,7 +249,6 @@ quote,https://github.com/dtolnay/quote,MIT OR Apache-2.0,David Tolnay regex,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " -regex-automata,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " -regex-syntax,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " reqwest,https://github.com/seanmonstar/reqwest,MIT OR Apache-2.0,Sean McArthur resolv-conf,https://github.com/hickory-dns/resolv-conf,MIT OR Apache-2.0,The resolv-conf Authors ring,https://github.com/briansmith/ring,Apache-2.0 AND ISC,The ring Authors @@ -328,25 +282,20 @@ rustversion,https://github.com/dtolnay/rustversion,MIT OR Apache-2.0,David Tolna ryu,https://github.com/dtolnay/ryu,Apache-2.0 OR BSL-1.0,David Tolnay same-file,https://github.com/BurntSushi/same-file,Unlicense OR MIT,Andrew Gallant schannel,https://github.com/steffengy/schannel-rs,MIT,"Steven Fackler , Steffen Butzer " -scoped-tls,https://github.com/alexcrichton/scoped-tls,MIT OR Apache-2.0,Alex Crichton scopeguard,https://github.com/bluss/scopeguard,MIT OR Apache-2.0,bluss secrecy,https://github.com/iqlusioninc/crates/tree/main/secrecy,Apache-2.0 OR MIT,Tony Arcieri security-framework,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " -security-framework-sys,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " seize,https://github.com/ibraheemdev/seize,MIT,Ibraheem Ahmed semver,https://github.com/dtolnay/semver,MIT OR Apache-2.0,David Tolnay serde,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde-value,https://github.com/arcnmx/serde-value,MIT,arcnmx serde_bytes,https://github.com/serde-rs/bytes,MIT OR Apache-2.0,David Tolnay -serde_core,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " -serde_derive,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_html_form,https://github.com/jplatte/serde_html_form,MIT,The serde_html_form Authors serde_json,https://github.com/serde-rs/json,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_path_to_error,https://github.com/dtolnay/path-to-error,MIT OR Apache-2.0,David Tolnay serde_repr,https://github.com/dtolnay/serde-repr,MIT OR Apache-2.0,David Tolnay serde_spanned,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The serde_spanned Authors serde_tuple,https://github.com/kardeiz/serde_tuple,MIT,Jacob Brown -serde_tuple_macros,https://github.com/kardeiz/serde_tuple,MIT,Jacob Brown serde_urlencoded,https://github.com/nox/serde_urlencoded,MIT OR Apache-2.0,Anthony Ramine serde_with,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,"Jonas Bushart, Marcin Kaźmierczak" serde_with_macros,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,Jonas Bushart @@ -366,54 +315,41 @@ sketches-ddsketch,https://github.com/mheffner/rust-sketches-ddsketch,Apache-2.0, slab,https://github.com/tokio-rs/slab,MIT,Carl Lerche smallvec,https://github.com/servo/rust-smallvec,MIT OR Apache-2.0,The Servo Project Developers snafu,https://github.com/shepmaster/snafu,MIT OR Apache-2.0,Jake Goulding -snafu-derive,https://github.com/shepmaster/snafu,MIT OR Apache-2.0,Jake Goulding socket2,https://github.com/rust-lang/socket2,MIT OR Apache-2.0,"Alex Crichton , Thomas de Zeeuw " sponge-cursor,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers stable_deref_trait,https://github.com/storyyeller/stable_deref_trait,MIT OR Apache-2.0,Robert Grosse strsim,https://github.com/rapidfuzz/strsim-rs,MIT,"Danny Guo , maxbachmann " structmeta,https://github.com/frozenlib/structmeta,MIT OR Apache-2.0,frozenlib -structmeta-derive,https://github.com/frozenlib/structmeta,MIT OR Apache-2.0,frozenlib subtle,https://github.com/dalek-cryptography/subtle,BSD-3-Clause,"Isis Lovecruft , Henry de Valence " symlink,https://gitlab.com/chris-morgan/symlink,MIT OR Apache-2.0,Chris Morgan syn,https://github.com/dtolnay/syn,MIT OR Apache-2.0,David Tolnay sync_wrapper,https://github.com/Actyx/sync_wrapper,Apache-2.0,Actyx AG synstructure,https://github.com/mystor/synstructure,MIT,Nika Layzell system-configuration,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN -system-configuration-sys,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN tagptr,https://github.com/oliver-giersch/tagptr,MIT OR Apache-2.0,Oliver Giersch target-triple,https://github.com/dtolnay/target-triple,MIT OR Apache-2.0,David Tolnay tempfile,https://github.com/Stebalien/tempfile,MIT OR Apache-2.0,"Steven Allen , The Rust Project Developers, Ashley Mannix , Jason White " termcolor,https://github.com/BurntSushi/termcolor,Unlicense OR MIT,Andrew Gallant thiserror,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay -thiserror-impl,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay thousands,https://github.com/tov/thousands-rs,MIT OR Apache-2.0,Jesse A. Tov thread_local,https://github.com/Amanieu/thread_local-rs,MIT OR Apache-2.0,Amanieu d'Antras tikv-jemalloc-sys,https://github.com/tikv/jemallocator,MIT OR Apache-2.0,"Alex Crichton , Gonzalo Brito Gadeschi , The TiKV Project Developers" tikv-jemallocator,https://github.com/tikv/jemallocator,MIT OR Apache-2.0,"Alex Crichton , Gonzalo Brito Gadeschi , Simon Sapin , Steven Fackler , The TiKV Project Developers" time,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" -time-core,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" -time-macros,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" tinystr,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers tinytemplate,https://github.com/bheisler/TinyTemplate,Apache-2.0 OR MIT,Brook Heisler tinyvec,https://github.com/Lokathor/tinyvec,Zlib OR Apache-2.0 OR MIT,Lokathor tinyvec_macros,https://github.com/Soveu/tinyvec_macros,MIT OR Apache-2.0 OR Zlib,Soveu tokio,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors -tokio-macros,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-rustls,https://github.com/rustls/tokio-rustls,MIT OR Apache-2.0,The tokio-rustls Authors -tokio-stream,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-tungstenite,https://github.com/snapview/tokio-tungstenite,MIT,"Daniel Abramov , Alexey Galakhov " -tokio-util,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors toml,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml Authors toml_datetime,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml_datetime Authors toml_parser,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml_parser Authors toml_writer,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml_writer Authors tonic,https://github.com/hyperium/tonic,MIT,Lucio Franco -tonic-build,https://github.com/hyperium/tonic,MIT,Lucio Franco -tonic-prost,https://github.com/hyperium/tonic,MIT,Lucio Franco tower,https://github.com/tower-rs/tower,MIT,Tower Maintainers tower-http,https://github.com/tower-rs/tower-http,MIT,Tower Maintainers -tower-layer,https://github.com/tower-rs/tower,MIT,Tower Maintainers -tower-service,https://github.com/tower-rs/tower,MIT,Tower Maintainers tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , Tokio Contributors " tracing-appender,https://github.com/tokio-rs/tracing,MIT,"Zeki Sherif , Tokio Contributors " tracing-attributes,https://github.com/tokio-rs/tracing,MIT,"Tokio Contributors , Eliza Weisman , David Barsky " @@ -463,9 +399,7 @@ webpki-root-certs,https://github.com/rustls/webpki-roots,CDLA-Permissive-2.0,The which,https://github.com/harryfei/which-rs,MIT,Harry Fei widestring,https://github.com/VoidStarKat/widestring-rs,MIT OR Apache-2.0,The widestring Authors winapi,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian -winapi-i686-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian winapi-util,https://github.com/BurntSushi/winapi-util,Unlicense OR MIT,Andrew Gallant -winapi-x86_64-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian windows-core,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-core Authors windows-implement,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-implement Authors windows-interface,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-interface Authors @@ -495,9 +429,6 @@ windows_x86_64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Mi windows_x86_64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows_x86_64_msvc Authors winnow,https://github.com/winnow-rs/winnow,MIT,The winnow Authors wit-bindgen,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton -wit-bindgen-core,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton -wit-bindgen-rust,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton -wit-bindgen-rust-macro,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton wit-component,https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-component,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Peter Huene wit-parser,https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-parser,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton writeable,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -507,7 +438,6 @@ yasna,https://github.com/qnighy/yasna.rs,MIT OR Apache-2.0,Masaki Hara yoke-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zerocopy,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " -zerocopy-derive,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " zerofrom,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerofrom-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zeroize,https://github.com/RustCrypto/utils,Apache-2.0 OR MIT,The RustCrypto Project Developers From b1bb2628aae282f9f33aa32d5b4939fb0554f145 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 1 Jun 2026 15:33:18 -0700 Subject: [PATCH 18/18] chore: resync third-party license file Co-Authored-By: Claude Sonnet 4.6 --- LICENSE-3rdparty.csv | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index d108c3c2b5..6490abcd2b 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -10,13 +10,19 @@ anstyle,https://github.com/rust-cli/anstyle,MIT OR Apache-2.0,The anstyle Author anyhow,https://github.com/dtolnay/anyhow,MIT OR Apache-2.0,David Tolnay arc-swap,https://github.com/vorner/arc-swap,MIT OR Apache-2.0,Michal 'vorner' Vaner argh,https://github.com/google/argh,BSD-3-Clause,"Taylor Cramer , Benjamin Brittain , Erick Tryzelaar " +argh_derive,https://github.com/google/argh,BSD-3-Clause,"Taylor Cramer , Benjamin Brittain , Erick Tryzelaar " +argh_shared,https://github.com/google/argh,BSD-3-Clause,"Taylor Cramer , Benjamin Brittain , Erick Tryzelaar " arrayvec,https://github.com/bluss/arrayvec,MIT OR Apache-2.0,bluss asn1-rs,https://github.com/rusticata/asn1-rs,MIT OR Apache-2.0,Pierre Chifflier +asn1-rs-derive,https://github.com/rusticata/asn1-rs,MIT OR Apache-2.0,Pierre Chifflier +asn1-rs-impl,https://github.com/rusticata/asn1-rs,MIT OR Apache-2.0,Pierre Chifflier async-compression,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman , Allen Bui " async-stream,https://github.com/tokio-rs/async-stream,MIT,Carl Lerche +async-stream-impl,https://github.com/tokio-rs/async-stream,MIT,Carl Lerche async-trait,https://github.com/dtolnay/async-trait,MIT OR Apache-2.0,David Tolnay atomic,https://github.com/Amanieu/atomic-rs,Apache-2.0 OR MIT,Amanieu d'Antras atomic-waker,https://github.com/smol-rs/atomic-waker,Apache-2.0 OR MIT,"Stjepan Glavina , Contributors to futures-rs" +aws-lc-fips-sys,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC) AND OpenSSL,AWS-LC aws-lc-rs,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC),AWS-LibCrypto aws-lc-sys,https://github.com/aws/aws-lc-rs,ISC AND (Apache-2.0 OR ISC) AND Apache-2.0 AND MIT AND BSD-3-Clause AND (Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0),AWS-LC axum,https://github.com/tokio-rs/axum,MIT,The axum Authors @@ -30,6 +36,7 @@ bitflags,https://github.com/bitflags/bitflags,MIT OR Apache-2.0,The Rust Project bitmask-enum,https://github.com/Lukas3674/rust-bitmask-enum,MIT OR Apache-2.0,Lukas3674 block-buffer,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers bollard,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors +bollard-stubs,https://github.com/fussybeaver/bollard,Apache-2.0,Bollard contributors bs58,https://github.com/Nullus157/bs58-rs,MIT OR Apache-2.0,The bs58 Authors bumpalo,https://github.com/fitzgen/bumpalo,MIT OR Apache-2.0,Nick Fitzgerald byte-unit,https://github.com/magiclen/byte-unit,MIT,Magic Len @@ -39,21 +46,28 @@ bytes,https://github.com/tokio-rs/bytes,MIT,"Carl Lerche , Se bytesize,https://github.com/bytesize-rs/bytesize,Apache-2.0,"Hyunsik Choi , MrCroxx , Rob Ede " cast,https://github.com/japaric/cast.rs,MIT OR Apache-2.0,Jorge Aparicio cc,https://github.com/rust-lang/cc-rs,MIT OR Apache-2.0,Alex Crichton +cexpr,https://github.com/jethrogb/rust-cexpr,Apache-2.0 OR MIT,Jethro Beekman cfg-if,https://github.com/rust-lang/cfg-if,MIT OR Apache-2.0,Alex Crichton chacha20,https://github.com/RustCrypto/stream-ciphers,MIT OR Apache-2.0,RustCrypto Developers chrono,https://github.com/chronotope/chrono,MIT OR Apache-2.0,The chrono Authors chrono-tz,https://github.com/chronotope/chrono-tz,MIT OR Apache-2.0,The chrono-tz Authors chumsky,https://codeberg.org/zesterer/chumsky,MIT,"Joshua Barretto , Elijah Hartvigsen " ciborium,https://github.com/enarx/ciborium,Apache-2.0,Nathaniel McCallum +ciborium-io,https://github.com/enarx/ciborium,Apache-2.0,Nathaniel McCallum +ciborium-ll,https://github.com/enarx/ciborium,Apache-2.0,Nathaniel McCallum +clang-sys,https://github.com/KyleMayes/clang-sys,Apache-2.0,Kyle Mayes clap,https://github.com/clap-rs/clap,MIT OR Apache-2.0,The clap Authors clap_builder,https://github.com/clap-rs/clap,MIT OR Apache-2.0,The clap_builder Authors clap_lex,https://github.com/clap-rs/clap,MIT OR Apache-2.0,The clap_lex Authors colored,https://github.com/mackwic/colored,MPL-2.0,Thomas Wickham combine,https://github.com/Marwes/combine,MIT,Markus Westerlind comfy-table,https://github.com/nukesor/comfy-table,MIT,Arne Beer +compression-codecs,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman , Allen Bui " +compression-core,https://github.com/Nullus157/async-compression,MIT OR Apache-2.0,"Wim Looman , Allen Bui " const-fnv1a-hash,https://github.com/HindrikStegenga/const-fnv1a-hash,MIT,The const-fnv1a-hash Authors const-hex,https://github.com/danipopes/const-hex,MIT OR Apache-2.0,DaniPopes <57450786+DaniPopes@users.noreply.github.com> core-foundation,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers +core-foundation-sys,https://github.com/servo/core-foundation-rs,MIT OR Apache-2.0,The Servo Project Developers cpufeatures,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers crc32fast,https://github.com/srijs/rust-crc32fast,MIT OR Apache-2.0,"Sam Rijs , Alex Crichton " criterion-plot,https://github.com/criterion-rs/criterion.rs,Apache-2.0 OR MIT,"Jorge Aparicio , Brook Heisler " @@ -67,11 +81,14 @@ crossterm,https://github.com/crossterm-rs/crossterm,MIT,T. Post crunchy,https://github.com/eira-fransham/crunchy,MIT,Eira Fransham crypto-common,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers darling,https://github.com/TedDriggs/darling,MIT,Ted Driggs +darling_core,https://github.com/TedDriggs/darling,MIT,Ted Driggs +darling_macro,https://github.com/TedDriggs/darling,MIT,Ted Driggs data-encoding,https://github.com/ia0/data-encoding,MIT,Julien Cretin der-parser,https://github.com/rusticata/der-parser,MIT OR Apache-2.0,Pierre Chifflier deranged,https://github.com/jhpratt/deranged,MIT OR Apache-2.0,Jacob Pratt derive-ex,https://github.com/frozenlib/derive-ex,MIT OR Apache-2.0,frozenlib derive_more,https://github.com/JelteF/derive_more,MIT,Jelte Fennema +derive_more-impl,https://github.com/JelteF/derive_more,MIT,Jelte Fennema digest,https://github.com/RustCrypto/traits,MIT OR Apache-2.0,RustCrypto Developers displaydoc,https://github.com/yaahc/displaydoc,MIT OR Apache-2.0,Jane Lusby document-features,https://github.com/slint-ui/document-features,MIT OR Apache-2.0,Slint Developers @@ -94,6 +111,7 @@ flate2,https://github.com/rust-lang/flate2-rs,MIT OR Apache-2.0,"Alex Crichton < float-cmp,https://github.com/mikedilger/float-cmp,MIT,Mike Dilger fnv,https://github.com/servo/rust-fnv,Apache-2.0 OR MIT,Alex Crichton foldhash,https://github.com/orlp/foldhash,Zlib,Orson Peters +form_urlencoded,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers fs4,https://github.com/al8n/fs4-rs,MIT OR Apache-2.0,"Dan Burkert , Al Liu " futures,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures Authors futures-channel,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-channel Authors @@ -104,6 +122,7 @@ futures-macro,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futu futures-sink,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-sink Authors futures-task,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-task Authors futures-util,https://github.com/rust-lang/futures-rs,MIT OR Apache-2.0,The futures-util Authors +generator,https://github.com/Xudong-Huang/generator-rs,MIT OR Apache-2.0,Xudong Huang generic-array,https://github.com/fizyk20/generic-array,MIT,"Bartłomiej Kamiński , Aaron Trent " getrandom,https://github.com/rust-random/getrandom,MIT OR Apache-2.0,The Rand Project Developers gimli,https://github.com/gimli-rs/gimli,MIT OR Apache-2.0,The gimli Authors @@ -114,6 +133,7 @@ hash32,https://github.com/japaric/hash32,MIT OR Apache-2.0,Jorge Aparicio hashbrown,https://github.com/rust-lang/hashbrown,MIT OR Apache-2.0,The hashbrown Authors headers,https://github.com/hyperium/headers,MIT,Sean McArthur +headers-core,https://github.com/hyperium/headers,MIT,Sean McArthur heapless,https://github.com/rust-embedded/heapless,MIT OR Apache-2.0,"Jorge Aparicio , Per Lindgren , Emil Fresk " heck,https://github.com/withoutboats/heck,MIT OR Apache-2.0,The heck Authors hex,https://github.com/KokaKiwi/rust-hex,MIT OR Apache-2.0,KokaKiwi @@ -123,6 +143,7 @@ hickory-resolver,https://github.com/hickory-dns/hickory-dns,MIT OR Apache-2.0,Th home,https://github.com/rust-lang/cargo,MIT OR Apache-2.0,Brian Anderson http,https://github.com/hyperium/http,MIT OR Apache-2.0,"Alex Crichton , Carl Lerche , Sean McArthur " http-body,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " +http-body-util,https://github.com/hyperium/http-body,MIT,"Carl Lerche , Lucio Franco , Sean McArthur " http-serde-ext,https://github.com/andrewtoth/http-serde-ext,MIT,Andrew Toth httparse,https://github.com/seanmonstar/httparse,MIT OR Apache-2.0,Sean McArthur httpdate,https://github.com/pyfisch/httpdate,MIT OR Apache-2.0,Pyfisch @@ -146,6 +167,7 @@ icu_provider,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project id-arena,https://github.com/fitzgen/id-arena,MIT OR Apache-2.0,"Nick Fitzgerald , Aleksey Kladov " iddqd,https://github.com/oxidecomputer/iddqd,MIT OR Apache-2.0,The iddqd Authors ident_case,https://github.com/TedDriggs/ident_case,MIT OR Apache-2.0,Ted Driggs +idna,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers idna_adapter,https://github.com/hsivonen/idna_adapter,Apache-2.0 OR MIT,The rust-url developers impls,https://github.com/nvzqz/impls,MIT OR Apache-2.0,"Nikolai Vazquez, Nadrieril Feneanar" indexmap,https://github.com/indexmap-rs/indexmap,Apache-2.0 OR MIT,The indexmap Authors @@ -155,6 +177,7 @@ ipnet,https://github.com/krisprice/ipnet,MIT OR Apache-2.0,Kris Price jiff,https://github.com/BurntSushi/jiff,Unlicense OR MIT,Andrew Gallant +jiff-static,https://github.com/BurntSushi/jiff,Unlicense OR MIT,Andrew Gallant jni,https://github.com/jni-rs/jni-rs,MIT OR Apache-2.0,jni team jni-macros,https://github.com/jni-rs/jni-rs,MIT OR Apache-2.0,The jni-macros Authors jni-sys,https://github.com/jni-rs/jni-sys,MIT OR Apache-2.0,"Steven Fackler , Robert Bragg " @@ -165,16 +188,23 @@ jsonpath-rust,https://github.com/besok/jsonpath-rust,MIT,BorisZhguchev keccak,https://github.com/RustCrypto/sponges,Apache-2.0 OR MIT,RustCrypto Developers kube,https://github.com/kube-rs/kube,Apache-2.0,"clux , Natalie Klestrup Röijezon , kazk " +kube-client,https://github.com/kube-rs/kube,Apache-2.0,"clux , Natalie Klestrup Röijezon , kazk " +kube-core,https://github.com/kube-rs/kube,Apache-2.0,"clux , Natalie Klestrup Röijezon , kazk " lading-payload,https://github.com/datadog/lading,MIT,"Brian L. Troutwine , George Hahn leb128fmt,https://github.com/bluk/leb128fmt,MIT OR Apache-2.0,Bryant Luk libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The Rust Project Developers +libloading,https://github.com/nagisa/rust_libloading,ISC,Simonas Kazlauskas libm,https://github.com/rust-lang/compiler-builtins,MIT,"Alex Crichton , Amanieu d'Antras , Jorge Aparicio , Trevor Gross " linux-raw-sys,https://github.com/sunfishcode/linux-raw-sys,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Dan Gohman litemap,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers litrs,https://github.com/LukasKalbertodt/litrs,MIT OR Apache-2.0,Lukas Kalbertodt +lock_api,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras log,https://github.com/rust-lang/log,MIT OR Apache-2.0,The Rust Project Developers logos,https://github.com/maciejhirsz/logos,MIT OR Apache-2.0,"Maciej Hirsz , Jérome Eertmans (maintainer) " +logos-codegen,https://github.com/maciejhirsz/logos,MIT OR Apache-2.0,"Maciej Hirsz , Jérome Eertmans (maintainer) " +logos-derive,https://github.com/maciejhirsz/logos,MIT OR Apache-2.0,"Maciej Hirsz , Jérome Eertmans (maintainer) " +loom,https://github.com/tokio-rs/loom,MIT,Carl Lerche lru-slab,https://github.com/Ralith/lru-slab,MIT OR Apache-2.0 OR Zlib,Benjamin Saunders mach2,https://github.com/JohnTitor/mach2,BSD-2-Clause OR MIT OR Apache-2.0,The mach2 Authors matchers,https://github.com/hawkw/matchers,MIT,Eliza Weisman @@ -182,6 +212,7 @@ matchit,https://github.com/ibraheemdev/matchit,MIT AND BSD-3-Clause,Ibraheem Ahm matrixmultiply,https://github.com/bluss/matrixmultiply,MIT OR Apache-2.0,"bluss, R. Janis Goldschmidt" memchr,https://github.com/BurntSushi/memchr,Unlicense OR MIT,"Andrew Gallant , bluss" metrics,https://github.com/metrics-rs/metrics,MIT,Toby Lawrence +metrics-util,https://github.com/metrics-rs/metrics,MIT,Toby Lawrence mime,https://github.com/hyperium/mime,MIT OR Apache-2.0,Sean McArthur minimal-lexical,https://github.com/Alexhuszagh/minimal-lexical,MIT OR Apache-2.0,Alex Huszagh miniz_oxide,https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide,MIT OR Zlib OR Apache-2.0,"Frommi , oyvindln , Rich Geldreich richgel99@gmail.com" @@ -215,18 +246,26 @@ ordered-float,https://github.com/reem/rust-ordered-float,MIT,"Jonathan Reem papaya,https://github.com/ibraheemdev/papaya,MIT,Ibraheem Ahmed parking_lot,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras +parking_lot_core,https://github.com/Amanieu/parking_lot,MIT OR Apache-2.0,Amanieu d'Antras paste,https://github.com/dtolnay/paste,MIT OR Apache-2.0,David Tolnay pear,https://github.com/SergioBenitez/Pear,MIT OR Apache-2.0,Sergio Benitez pear_codegen,https://github.com/SergioBenitez/Pear,MIT OR Apache-2.0,Sergio Benitez pem,https://github.com/jcreekmore/pem-rs,MIT,Jonathan Creekmore +percent-encoding,https://github.com/servo/rust-url,MIT OR Apache-2.0,The rust-url developers pest,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice +pest_derive,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice +pest_generator,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice +pest_meta,https://github.com/pest-parser/pest,MIT OR Apache-2.0,Dragoș Tiselice petgraph,https://github.com/petgraph/petgraph,MIT OR Apache-2.0,"bluss, mitchmindtree" phf,https://github.com/rust-phf/rust-phf,MIT,Steven Fackler +phf_shared,https://github.com/rust-phf/rust-phf,MIT,Steven Fackler piecemeal,https://github.com/tobz/piecemeal,Apache-2.0,Toby Lawrence pin-project,https://github.com/taiki-e/pin-project,Apache-2.0 OR MIT,The pin-project Authors pin-project-internal,https://github.com/taiki-e/pin-project,Apache-2.0 OR MIT,The pin-project-internal Authors pin-project-lite,https://github.com/taiki-e/pin-project-lite,Apache-2.0 OR MIT,The pin-project-lite Authors plotters,https://github.com/plotters-rs/plotters,MIT,Hao Hou +plotters-backend,https://github.com/plotters-rs/plotters,MIT,Hao Hou +plotters-svg,https://github.com/plotters-rs/plotters,MIT,Hao Hou portable-atomic,https://github.com/taiki-e/portable-atomic,Apache-2.0 OR MIT,The portable-atomic Authors portable-atomic-util,https://github.com/taiki-e/portable-atomic-util,Apache-2.0 OR MIT,The portable-atomic-util Authors potential_utf,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -238,8 +277,12 @@ proc-macro2,https://github.com/dtolnay/proc-macro2,MIT OR Apache-2.0,"David Toln proc-macro2-diagnostics,https://github.com/SergioBenitez/proc-macro2-diagnostics,MIT OR Apache-2.0,Sergio Benitez proptest,https://github.com/proptest-rs/proptest,MIT OR Apache-2.0,Jason Lingle prost,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " +prost-build,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " +prost-derive,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " +prost-types,https://github.com/tokio-rs/prost,Apache-2.0,"Dan Burkert , Lucio Franco , Casper Meijn , Tokio Contributors " protobuf,https://github.com/stepancheg/rust-protobuf,MIT,Stepan Koltsov protobuf-parse,https://github.com/stepancheg/rust-protobuf/tree/master/protobuf-parse,MIT,Stepan Koltsov +protobuf-support,https://github.com/stepancheg/rust-protobuf,MIT,Stepan Koltsov quanta,https://github.com/metrics-rs/quanta,MIT,Toby Lawrence quick_cache,https://github.com/arthurprs/quick-cache,MIT,Arthur Silva quinn,https://github.com/quinn-rs/quinn,MIT OR Apache-2.0,The quinn Authors @@ -249,6 +292,7 @@ quote,https://github.com/dtolnay/quote,MIT OR Apache-2.0,David Tolnay regex,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " +regex-automata,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " +regex-syntax,https://github.com/rust-lang/regex,MIT OR Apache-2.0,"The Rust Project Developers, Andrew Gallant " reqwest,https://github.com/seanmonstar/reqwest,MIT OR Apache-2.0,Sean McArthur resolv-conf,https://github.com/hickory-dns/resolv-conf,MIT OR Apache-2.0,The resolv-conf Authors ring,https://github.com/briansmith/ring,Apache-2.0 AND ISC,The ring Authors @@ -282,20 +328,25 @@ rustversion,https://github.com/dtolnay/rustversion,MIT OR Apache-2.0,David Tolna ryu,https://github.com/dtolnay/ryu,Apache-2.0 OR BSL-1.0,David Tolnay same-file,https://github.com/BurntSushi/same-file,Unlicense OR MIT,Andrew Gallant schannel,https://github.com/steffengy/schannel-rs,MIT,"Steven Fackler , Steffen Butzer " +scoped-tls,https://github.com/alexcrichton/scoped-tls,MIT OR Apache-2.0,Alex Crichton scopeguard,https://github.com/bluss/scopeguard,MIT OR Apache-2.0,bluss secrecy,https://github.com/iqlusioninc/crates/tree/main/secrecy,Apache-2.0 OR MIT,Tony Arcieri security-framework,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " +security-framework-sys,https://github.com/kornelski/rust-security-framework,MIT OR Apache-2.0,"Steven Fackler , Kornel " seize,https://github.com/ibraheemdev/seize,MIT,Ibraheem Ahmed semver,https://github.com/dtolnay/semver,MIT OR Apache-2.0,David Tolnay serde,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde-value,https://github.com/arcnmx/serde-value,MIT,arcnmx serde_bytes,https://github.com/serde-rs/bytes,MIT OR Apache-2.0,David Tolnay +serde_core,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " +serde_derive,https://github.com/serde-rs/serde,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_html_form,https://github.com/jplatte/serde_html_form,MIT,The serde_html_form Authors serde_json,https://github.com/serde-rs/json,MIT OR Apache-2.0,"Erick Tryzelaar , David Tolnay " serde_path_to_error,https://github.com/dtolnay/path-to-error,MIT OR Apache-2.0,David Tolnay serde_repr,https://github.com/dtolnay/serde-repr,MIT OR Apache-2.0,David Tolnay serde_spanned,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The serde_spanned Authors serde_tuple,https://github.com/kardeiz/serde_tuple,MIT,Jacob Brown +serde_tuple_macros,https://github.com/kardeiz/serde_tuple,MIT,Jacob Brown serde_urlencoded,https://github.com/nox/serde_urlencoded,MIT OR Apache-2.0,Anthony Ramine serde_with,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,"Jonas Bushart, Marcin Kaźmierczak" serde_with_macros,https://github.com/jonasbb/serde_with,MIT OR Apache-2.0,Jonas Bushart @@ -315,41 +366,54 @@ sketches-ddsketch,https://github.com/mheffner/rust-sketches-ddsketch,Apache-2.0, slab,https://github.com/tokio-rs/slab,MIT,Carl Lerche smallvec,https://github.com/servo/rust-smallvec,MIT OR Apache-2.0,The Servo Project Developers snafu,https://github.com/shepmaster/snafu,MIT OR Apache-2.0,Jake Goulding +snafu-derive,https://github.com/shepmaster/snafu,MIT OR Apache-2.0,Jake Goulding socket2,https://github.com/rust-lang/socket2,MIT OR Apache-2.0,"Alex Crichton , Thomas de Zeeuw " sponge-cursor,https://github.com/RustCrypto/utils,MIT OR Apache-2.0,RustCrypto Developers stable_deref_trait,https://github.com/storyyeller/stable_deref_trait,MIT OR Apache-2.0,Robert Grosse strsim,https://github.com/rapidfuzz/strsim-rs,MIT,"Danny Guo , maxbachmann " structmeta,https://github.com/frozenlib/structmeta,MIT OR Apache-2.0,frozenlib +structmeta-derive,https://github.com/frozenlib/structmeta,MIT OR Apache-2.0,frozenlib subtle,https://github.com/dalek-cryptography/subtle,BSD-3-Clause,"Isis Lovecruft , Henry de Valence " symlink,https://gitlab.com/chris-morgan/symlink,MIT OR Apache-2.0,Chris Morgan syn,https://github.com/dtolnay/syn,MIT OR Apache-2.0,David Tolnay sync_wrapper,https://github.com/Actyx/sync_wrapper,Apache-2.0,Actyx AG synstructure,https://github.com/mystor/synstructure,MIT,Nika Layzell system-configuration,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN +system-configuration-sys,https://github.com/mullvad/system-configuration-rs,MIT OR Apache-2.0,Mullvad VPN tagptr,https://github.com/oliver-giersch/tagptr,MIT OR Apache-2.0,Oliver Giersch target-triple,https://github.com/dtolnay/target-triple,MIT OR Apache-2.0,David Tolnay tempfile,https://github.com/Stebalien/tempfile,MIT OR Apache-2.0,"Steven Allen , The Rust Project Developers, Ashley Mannix , Jason White " termcolor,https://github.com/BurntSushi/termcolor,Unlicense OR MIT,Andrew Gallant thiserror,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay +thiserror-impl,https://github.com/dtolnay/thiserror,MIT OR Apache-2.0,David Tolnay thousands,https://github.com/tov/thousands-rs,MIT OR Apache-2.0,Jesse A. Tov thread_local,https://github.com/Amanieu/thread_local-rs,MIT OR Apache-2.0,Amanieu d'Antras tikv-jemalloc-sys,https://github.com/tikv/jemallocator,MIT OR Apache-2.0,"Alex Crichton , Gonzalo Brito Gadeschi , The TiKV Project Developers" tikv-jemallocator,https://github.com/tikv/jemallocator,MIT OR Apache-2.0,"Alex Crichton , Gonzalo Brito Gadeschi , Simon Sapin , Steven Fackler , The TiKV Project Developers" time,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" +time-core,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" +time-macros,https://github.com/time-rs/time,MIT OR Apache-2.0,"Jacob Pratt , Time contributors" tinystr,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers tinytemplate,https://github.com/bheisler/TinyTemplate,Apache-2.0 OR MIT,Brook Heisler tinyvec,https://github.com/Lokathor/tinyvec,Zlib OR Apache-2.0 OR MIT,Lokathor tinyvec_macros,https://github.com/Soveu/tinyvec_macros,MIT OR Apache-2.0 OR Zlib,Soveu tokio,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors +tokio-macros,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-rustls,https://github.com/rustls/tokio-rustls,MIT OR Apache-2.0,The tokio-rustls Authors +tokio-stream,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors tokio-tungstenite,https://github.com/snapview/tokio-tungstenite,MIT,"Daniel Abramov , Alexey Galakhov " +tokio-util,https://github.com/tokio-rs/tokio,MIT,Tokio Contributors toml,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml Authors toml_datetime,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml_datetime Authors toml_parser,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml_parser Authors toml_writer,https://github.com/toml-rs/toml,MIT OR Apache-2.0,The toml_writer Authors tonic,https://github.com/hyperium/tonic,MIT,Lucio Franco +tonic-build,https://github.com/hyperium/tonic,MIT,Lucio Franco +tonic-prost,https://github.com/hyperium/tonic,MIT,Lucio Franco tower,https://github.com/tower-rs/tower,MIT,Tower Maintainers tower-http,https://github.com/tower-rs/tower-http,MIT,Tower Maintainers +tower-layer,https://github.com/tower-rs/tower,MIT,Tower Maintainers +tower-service,https://github.com/tower-rs/tower,MIT,Tower Maintainers tracing,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , Tokio Contributors " tracing-appender,https://github.com/tokio-rs/tracing,MIT,"Zeki Sherif , Tokio Contributors " tracing-attributes,https://github.com/tokio-rs/tracing,MIT,"Tokio Contributors , Eliza Weisman , David Barsky " @@ -399,7 +463,9 @@ webpki-root-certs,https://github.com/rustls/webpki-roots,CDLA-Permissive-2.0,The which,https://github.com/harryfei/which-rs,MIT,Harry Fei widestring,https://github.com/VoidStarKat/widestring-rs,MIT OR Apache-2.0,The widestring Authors winapi,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian +winapi-i686-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian winapi-util,https://github.com/BurntSushi/winapi-util,Unlicense OR MIT,Andrew Gallant +winapi-x86_64-pc-windows-gnu,https://github.com/retep998/winapi-rs,MIT OR Apache-2.0,Peter Atashian windows-core,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-core Authors windows-implement,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-implement Authors windows-interface,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows-interface Authors @@ -429,6 +495,9 @@ windows_x86_64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,Mi windows_x86_64_msvc,https://github.com/microsoft/windows-rs,MIT OR Apache-2.0,The windows_x86_64_msvc Authors winnow,https://github.com/winnow-rs/winnow,MIT,The winnow Authors wit-bindgen,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton +wit-bindgen-core,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton +wit-bindgen-rust,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton +wit-bindgen-rust-macro,https://github.com/bytecodealliance/wit-bindgen,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton wit-component,https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-component,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Peter Huene wit-parser,https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-parser,Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,Alex Crichton writeable,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers @@ -438,6 +507,7 @@ yasna,https://github.com/qnighy/yasna.rs,MIT OR Apache-2.0,Masaki Hara yoke-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zerocopy,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " +zerocopy-derive,https://github.com/google/zerocopy,BSD-2-Clause OR Apache-2.0 OR MIT,"Joshua Liebow-Feeser , Jack Wrenn " zerofrom,https://github.com/unicode-org/icu4x,Unicode-3.0,The ICU4X Project Developers zerofrom-derive,https://github.com/unicode-org/icu4x,Unicode-3.0,Manish Goregaokar zeroize,https://github.com/RustCrypto/utils,Apache-2.0 OR MIT,The RustCrypto Project Developers