mod support;
use std::error::Error;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::num::NonZeroU64;
use oxide_batch::{
Clock, DropReportWindow, EnqueueResult, ExportQueueBound, InMemoryExplorer,
InMemoryJobRepository, IncidentEventBuffer, JobExecutionId, JobExplorer, JobName, JobOperator,
MAX_DROP_REPORT_WINDOW, MAX_EXPORT_QUEUE_RECORDS, MAX_METRIC_NAME_ALLOWLIST,
MAX_RETAINED_EVENTS_PER_EXECUTION, MAX_SHUTDOWN_DEADLINE, MAX_TELEMETRY_FLUSH_DEADLINE,
METRIC_CARDINALITY_BUDGET, MIN_DROP_REPORT_WINDOW, MIN_EXPORT_QUEUE_RECORDS,
MIN_SHUTDOWN_DEADLINE, MIN_TELEMETRY_FLUSH_DEADLINE, MetricCardinalityGuard, MetricDimensions,
MetricFamily, RetentionService, SequentialIdGenerator, ShutdownDeadline, StepName,
TelemetryEventKind, TelemetryEventSink, TelemetryFlushDeadline, TelemetryQueue,
TelemetryRecord,
};
use oxide_batch_cli::{
Command, ExitCategory, MAX_OUTPUT_BYTES, NoSchema, OutputForm, Response, Services, Writer,
};
use serde_json::{Value, json};
use support::{FixedClock, TestHost, TestServices, run_with_catalog, services, test_catalog};
const REPORT: &str = "bounded-shedding";
const OBSERVATIONS_ENV: &str = "OXIDEBATCH_RESOURCE_OBSERVATIONS";
const SATURATED_QUEUE: usize = MIN_EXPORT_QUEUE_RECORDS;
const OFFERED_RECORDS: usize = SATURATED_QUEUE * 4;
const OFFERED_SERIES: usize = METRIC_CARDINALITY_BUDGET * 2;
const OFFERED_EVENTS: usize = MAX_RETAINED_EVENTS_PER_EXECUTION * 3;
const JOB: &str = "resource-bound-shedding-job";
const BUNDLE_CEILING: usize = 4 * 1024 * 1024;
const CONFIG_CEILING: usize = 256 * 1024;
#[test]
fn bounded_queues_shed_under_overload_without_blocking_batch_work() -> Result<(), Box<dyn Error>> {
let mut violations = Vec::new();
let mut resources = Vec::new();
let queue = saturate_the_exporter_queue();
violations.extend(queue.violations.clone());
resources.push(queue.evidence());
let series = saturate_the_metric_family();
violations.extend(series.violations.clone());
resources.push(series.evidence());
let events = saturate_the_incident_buffer();
violations.extend(events.violations.clone());
resources.push(events.evidence());
let response = overflow_the_operator_response();
violations.extend(response.violations.clone());
resources.push(response.evidence());
let bundle = generate_a_bundle();
violations.extend(bundle.violations.clone());
resources.push(bundle.evidence());
let cells = construction_cells();
violations.extend(cells.iter().filter_map(Cell::violation));
let equivalence = batch_work_finishes_with_the_queue_full();
violations.extend(equivalence.violations.clone());
let document = json!({
"report": REPORT,
"scenario": "bounded_queues_shed_under_overload_without_blocking_batch_work",
"resources": resources,
"construction": cells.iter().map(Cell::evidence).collect::<Vec<_>>(),
"durable_equivalence": equivalence.evidence(),
"execution_manifest": execution_manifest()?,
"violations": violations,
"passed": violations.is_empty(),
});
retain(&document)?;
assert!(
violations.is_empty(),
"the shedding report observed {violations:#?}",
);
Ok(())
}
fn saturate_the_exporter_queue() -> Shed {
let bound =
ExportQueueBound::new(SATURATED_QUEUE).unwrap_or_else(|_| ExportQueueBound::default());
let queue = TelemetryQueue::new(bound, DropReportWindow::default());
let mut violations = Vec::new();
let mut accepted = 0_u64;
let mut dropped = 0_u64;
let mut peak_depth = 0_usize;
let mut reports_due = 0_u64;
for index in 0..OFFERED_RECORDS {
match queue.enqueue(record(), Duration::from_millis(index as u64)) {
EnqueueResult::Accepted => accepted += 1,
EnqueueResult::Dropped { report_due } => {
dropped += 1;
if report_due {
reports_due += 1;
}
}
other => violations.push(format!(
"the exporter queue answered an offer with {other:?}, which this report cannot \
account for",
)),
}
peak_depth = peak_depth.max(queue.len());
}
if peak_depth > SATURATED_QUEUE {
violations.push(format!(
"the exporter queue holds {SATURATED_QUEUE} records and reached a depth of \
{peak_depth}",
));
}
if peak_depth != SATURATED_QUEUE {
violations.push(format!(
"{OFFERED_RECORDS} records were offered to a queue of {SATURATED_QUEUE} and it never \
filled past {peak_depth}, so the drop path was never entered",
));
}
if accepted != SATURATED_QUEUE as u64 {
violations.push(format!(
"the queue accepted {accepted} of {OFFERED_RECORDS} records and holds \
{SATURATED_QUEUE}",
));
}
let excess = (OFFERED_RECORDS - SATURATED_QUEUE) as u64;
if dropped != excess {
violations.push(format!(
"{OFFERED_RECORDS} records were offered to a queue of {SATURATED_QUEUE} and \
{dropped} were dropped rather than the {excess} that did not fit",
));
}
if queue.dropped() != dropped {
violations.push(format!(
"the queue counted {} drops and {dropped} were observed, so the counter an operator \
reads is not the thing that happened",
queue.dropped(),
));
}
if reports_due >= dropped {
violations.push(format!(
"{dropped} drops produced {reports_due} due drop reports, so the report is not \
throttled",
));
}
let drained = queue.len();
Shed {
resource: "telemetry-exporter-queue",
policy: "bounded-shedding",
rule: "drop-newest",
ceiling: MAX_EXPORT_QUEUE_RECORDS as u64,
configured: SATURATED_QUEUE as u64,
offered: OFFERED_RECORDS as u64,
peak: peak_depth as u64,
retained: drained as u64,
discarded: dropped,
violations,
}
}
fn saturate_the_metric_family() -> Shed {
let family = MetricFamily::ExecutionEvents;
let jobs = (0..MAX_METRIC_NAME_ALLOWLIST)
.filter_map(|index| JobName::new(format!("job-{index:04}")).ok())
.collect::<Vec<_>>();
let steps = (0..MAX_METRIC_NAME_ALLOWLIST)
.filter_map(|index| StepName::new(format!("step-{index:04}")).ok())
.collect::<Vec<_>>();
let Ok(mut guard) = MetricCardinalityGuard::new(jobs.clone(), steps.clone()) else {
return Shed::failed(
"metric-series-per-family",
"the report could not build the allowlist the budget is measured against",
);
};
let mut offered = 0_u64;
let mut collapsed = 0_u64;
for job in &jobs {
for step in &steps {
if offered >= OFFERED_SERIES as u64 {
break;
}
let dimensions = MetricDimensions::default()
.with_job_name(job.clone())
.with_step_name(step.clone());
offered += 1;
if guard.observe(family, &dimensions).overflowed() {
collapsed += 1;
}
}
}
let series = guard.series_count(family);
let mut violations = Vec::new();
if series > METRIC_CARDINALITY_BUDGET {
violations.push(format!(
"the family budget is {METRIC_CARDINALITY_BUDGET} series and {series} are retained",
));
}
if collapsed == 0 {
violations.push(format!(
"{offered} label combinations were offered to a budget of \
{METRIC_CARDINALITY_BUDGET} and none was collapsed, so the reserved series was never \
reached",
));
}
if guard.dropped_cardinality(family) != collapsed {
violations.push(format!(
"the guard counted {} collapsed combinations and {collapsed} were observed",
guard.dropped_cardinality(family),
));
}
Shed {
resource: "metric-series-per-family",
policy: "bounded-shedding",
rule: "collapse-to-reserved-series",
ceiling: METRIC_CARDINALITY_BUDGET as u64,
configured: METRIC_CARDINALITY_BUDGET as u64,
offered,
peak: series as u64,
retained: series as u64,
discarded: collapsed,
violations,
}
}
fn saturate_the_incident_buffer() -> Shed {
let buffer = Arc::new(IncidentEventBuffer::default());
let services = services_with_sink(Arc::clone(&buffer) as Arc<dyn TelemetryEventSink>);
let catalog = test_catalog(JOB);
let mut host = TestHost::new();
let launched = run_with_catalog(
&mut host,
&services,
&catalog,
&format!(
"launch --job {JOB} --actor campaign --operation-id shedding-events --output json"
),
);
let mut violations = Vec::new();
if launched != ExitCategory::Success {
violations.push(format!(
"the incident-buffer fixture could not launch: {}",
host.stderr_text(),
));
}
let execution = host.envelope()["data"]["execution"]["execution_id"]
.as_u64()
.unwrap_or(1);
let mut offered = 0_u64;
for _ in 0..OFFERED_EVENTS {
let mut reader = TestHost::new();
let category = run_with_catalog(
&mut reader,
&services,
&catalog,
&format!("execution steps --execution {execution} --output json"),
);
if category != ExitCategory::Success {
violations.push(format!(
"the incident-buffer fixture could not read the execution: {}",
reader.stderr_text(),
));
break;
}
offered += 1;
}
let retained = JobExecutionId::new(execution)
.map(|id| buffer.events_for(id).len())
.unwrap_or_default();
if retained > MAX_RETAINED_EVENTS_PER_EXECUTION {
violations.push(format!(
"the per-execution buffer retains {MAX_RETAINED_EVENTS_PER_EXECUTION} events and \
returned {retained}",
));
}
if offered <= MAX_RETAINED_EVENTS_PER_EXECUTION as u64 {
violations.push(format!(
"{offered} events were emitted for one execution against a buffer of \
{MAX_RETAINED_EVENTS_PER_EXECUTION}, so the eviction rule was never exercised",
));
}
if retained != MAX_RETAINED_EVENTS_PER_EXECUTION {
violations.push(format!(
"{offered} events were emitted for one execution and the buffer returned {retained} \
rather than the {MAX_RETAINED_EVENTS_PER_EXECUTION} it retains",
));
}
Shed {
resource: "retained-incident-events",
policy: "bounded-shedding",
rule: "evict-oldest",
ceiling: MAX_RETAINED_EVENTS_PER_EXECUTION as u64,
configured: MAX_RETAINED_EVENTS_PER_EXECUTION as u64,
offered,
peak: retained as u64,
retained: retained as u64,
discarded: offered.saturating_sub(retained as u64),
violations,
}
}
fn overflow_the_operator_response() -> Shed {
let row = "x".repeat(1024);
let rows = (0..1_024)
.map(|index| json!({ "id": index, "detail": row }))
.collect::<Vec<_>>();
let offered = serde_json::to_vec(&Value::Array(rows.clone()))
.map(|bytes| bytes.len())
.unwrap_or_default();
let mut host = TestHost::new();
let writer = Writer::new(OutputForm::Json, false);
let response = Response::success(Command::InstanceList, Value::Array(rows));
let emitted = writer.emit(&mut host, &response).is_ok();
let written = host.stdout_text();
let mut violations = Vec::new();
if !emitted {
violations.push(
"an over-large response failed to render at all rather than being truncated".to_owned(),
);
}
if written.len() > MAX_OUTPUT_BYTES {
violations.push(format!(
"the operator response bound is {MAX_OUTPUT_BYTES} bytes and {} were written",
written.len(),
));
}
if offered <= MAX_OUTPUT_BYTES {
violations.push(format!(
"the report offered {offered} bytes against a {MAX_OUTPUT_BYTES}-byte bound, so it \
never crossed it",
));
}
if !written.contains("truncated") {
violations.push(
"the response was truncated and does not say so, so an operator cannot tell a short \
page from a complete one"
.to_owned(),
);
}
Shed {
resource: "operator-response",
policy: "bounded-truncation",
rule: "truncate-and-declare",
ceiling: MAX_OUTPUT_BYTES as u64,
configured: MAX_OUTPUT_BYTES as u64,
offered: offered as u64,
peak: written.len() as u64,
retained: written.len() as u64,
discarded: offered.saturating_sub(written.len()) as u64,
violations,
}
}
fn generate_a_bundle() -> Shed {
let (services, _repository) = services();
let catalog = test_catalog(JOB);
let mut host = TestHost::new();
let launched = run_with_catalog(
&mut host,
&services,
&catalog,
&format!(
"launch --job {JOB} --actor campaign --operation-id shedding-bundle --output json"
),
);
let mut violations = Vec::new();
if launched != ExitCategory::Success {
violations.push(format!(
"the bundle fixture could not launch: {}",
host.stderr_text(),
));
}
let execution = host.envelope()["data"]["execution"]["execution_id"]
.as_u64()
.unwrap_or(1);
let mut bundling = TestHost::new();
let generated = run_with_catalog(
&mut bundling,
&services,
&catalog,
&format!("diagnostics bundle --execution {execution} --out shedding-bundle --output json"),
);
if generated != ExitCategory::Success {
violations.push(format!(
"the diagnostics bundle could not be generated: {}",
bundling.stderr_text(),
));
}
let mut total = 0_usize;
let mut files = 0_u64;
for name in bundling.directory_files("shedding-bundle") {
total += bundling.file_text(&format!("shedding-bundle/{name}")).len();
files += 1;
}
if files == 0 {
violations.push("the bundle contains no file, so its size proves nothing".to_owned());
}
if total > BUNDLE_CEILING {
violations.push(format!(
"the bundle bound is {BUNDLE_CEILING} bytes and {total} were written",
));
}
Shed {
resource: "diagnostic-bundle",
policy: "bounded-truncation",
rule: "truncate-and-declare",
ceiling: BUNDLE_CEILING as u64,
configured: BUNDLE_CEILING as u64,
offered: total as u64,
peak: total as u64,
retained: total as u64,
discarded: 0,
violations,
}
}
fn batch_work_finishes_with_the_queue_full() -> Equivalence {
let quiet = launch_with_queue(usize::from(u16::MAX) + 1, false);
let saturated = launch_with_queue(SATURATED_QUEUE, true);
let mut violations = Vec::new();
if saturated.shed == 0 {
violations.push(
"the saturated launch shed no record, so it is not a comparison against a full queue"
.to_owned(),
);
}
if quiet.shed != 0 {
violations.push(format!(
"the baseline launch shed {} records, so it is not a comparison against a queue with \
room",
quiet.shed,
));
}
if saturated.category != ExitCategory::Success {
violations.push(
"batch work did not complete while its exporter queue was saturated, so telemetry \
blocked it"
.to_owned(),
);
}
if saturated.durable != quiet.durable {
violations.push(
"the saturated launch and the baseline launch left different durable records, so a \
shed telemetry record changed an observation"
.to_owned(),
);
}
Equivalence {
baseline_shed: quiet.shed,
saturated_shed: saturated.shed,
baseline_durable: quiet.durable.clone(),
saturated_durable: saturated.durable,
violations,
}
}
fn launch_with_queue(bound: usize, prefill: bool) -> Launch {
let sink = Arc::new(SheddingSink::new(bound, prefill));
let services = services_with_sink(Arc::clone(&sink) as Arc<dyn TelemetryEventSink>);
let catalog = test_catalog(JOB);
let mut host = TestHost::new();
let category = run_with_catalog(
&mut host,
&services,
&catalog,
&format!(
"launch --job {JOB} --actor campaign --operation-id shedding-{bound}-{prefill} \
--output json"
),
);
let envelope = host.envelope();
let execution = &envelope["data"]["execution"];
let durable = json!({
"status": execution["status"],
"exit_status": execution["exit_status"],
"version": execution["version"],
"category": format!("{category:?}"),
});
Launch {
category,
shed: sink.shed(),
durable,
}
}
fn services_with_sink(sink: Arc<dyn TelemetryEventSink>) -> TestServices {
let clock: Arc<dyn Clock> = Arc::new(FixedClock::new());
let repository = InMemoryJobRepository::new(
Arc::clone(&clock),
Arc::new(SequentialIdGenerator::new(NonZeroU64::MIN)),
);
let explorer_repository = InMemoryExplorer::new(&repository);
Services::new(
JobOperator::new(repository.clone(), Arc::clone(&clock)).with_event_sink(Arc::clone(&sink)),
RetentionService::new(repository, Arc::clone(&clock)).with_event_sink(Arc::clone(&sink)),
JobExplorer::new(explorer_repository).with_event_sink(sink),
Box::new(NoSchema),
)
}
fn construction_cells() -> Vec<Cell> {
let mut cells = queue_construction_cells();
cells.extend(deadline_construction_cells());
cells.extend(configuration_construction_cells());
cells
}
fn queue_construction_cells() -> Vec<Cell> {
vec![
Cell::dimensioned(
"telemetry-exporter-queue",
None,
"at the ceiling",
MAX_EXPORT_QUEUE_RECORDS as u64,
MAX_EXPORT_QUEUE_RECORDS as u64,
ExportQueueBound::new(MAX_EXPORT_QUEUE_RECORDS).is_ok(),
true,
"records",
),
Cell::dimensioned(
"telemetry-exporter-queue",
None,
"one past the ceiling",
MAX_EXPORT_QUEUE_RECORDS as u64,
MAX_EXPORT_QUEUE_RECORDS as u64 + 1,
ExportQueueBound::new(MAX_EXPORT_QUEUE_RECORDS + 1).is_ok(),
false,
"records",
),
Cell::dimensioned(
"telemetry-exporter-queue",
None,
"at the floor",
MIN_EXPORT_QUEUE_RECORDS as u64,
MIN_EXPORT_QUEUE_RECORDS as u64,
ExportQueueBound::new(MIN_EXPORT_QUEUE_RECORDS).is_ok(),
true,
"records",
),
Cell::dimensioned(
"telemetry-exporter-queue",
None,
"one below the floor",
MIN_EXPORT_QUEUE_RECORDS as u64,
MIN_EXPORT_QUEUE_RECORDS as u64 - 1,
ExportQueueBound::new(MIN_EXPORT_QUEUE_RECORDS - 1).is_ok(),
false,
"records",
),
Cell::new(
"retained-incident-events",
"at the ceiling",
MAX_RETAINED_EVENTS_PER_EXECUTION as u64,
IncidentEventBuffer::new(MAX_RETAINED_EVENTS_PER_EXECUTION, 4_096).is_ok(),
true,
),
Cell::new(
"retained-incident-events",
"one past the ceiling",
MAX_RETAINED_EVENTS_PER_EXECUTION as u64 + 1,
IncidentEventBuffer::new(MAX_RETAINED_EVENTS_PER_EXECUTION + 1, 4_096).is_ok(),
false,
),
Cell::new(
"metric-name-allowlist",
"at the ceiling",
MAX_METRIC_NAME_ALLOWLIST as u64,
allowlist_of(MAX_METRIC_NAME_ALLOWLIST),
true,
),
Cell::new(
"metric-name-allowlist",
"one past the ceiling",
MAX_METRIC_NAME_ALLOWLIST as u64 + 1,
allowlist_of(MAX_METRIC_NAME_ALLOWLIST + 1),
false,
),
]
}
fn deadline_construction_cells() -> Vec<Cell> {
let mut cells = Vec::new();
cells.extend(range_cells(
"telemetry-drop-report-window",
MIN_DROP_REPORT_WINDOW,
MAX_DROP_REPORT_WINDOW,
DropReportWindow::new,
));
cells.extend(range_cells(
"shutdown-deadline",
MIN_SHUTDOWN_DEADLINE,
MAX_SHUTDOWN_DEADLINE,
ShutdownDeadline::new,
));
cells.extend(range_cells(
"telemetry-flush-deadline",
MIN_TELEMETRY_FLUSH_DEADLINE,
MAX_TELEMETRY_FLUSH_DEADLINE,
TelemetryFlushDeadline::new,
));
cells
}
fn range_cells<T, E>(
resource: &'static str,
minimum: Duration,
maximum: Duration,
construct: impl Fn(Duration) -> Result<T, E>,
) -> Vec<Cell> {
let minimum_ms = u64::try_from(minimum.as_millis()).unwrap_or(u64::MAX);
let maximum_ms = u64::try_from(maximum.as_millis()).unwrap_or(u64::MAX);
vec![
Cell::dimensioned(
resource,
None,
"at the minimum",
minimum_ms,
minimum_ms,
construct(minimum).is_ok(),
true,
"milliseconds",
),
Cell::dimensioned(
resource,
None,
"one millisecond below the minimum",
minimum_ms,
minimum_ms.saturating_sub(1),
minimum
.checked_sub(Duration::from_millis(1))
.is_some_and(|below| construct(below).is_ok()),
false,
"milliseconds",
),
Cell::dimensioned(
resource,
None,
"at the maximum",
maximum_ms,
maximum_ms,
construct(maximum).is_ok(),
true,
"milliseconds",
),
Cell::dimensioned(
resource,
None,
"one millisecond past the maximum",
maximum_ms,
maximum_ms.saturating_add(1),
construct(maximum + Duration::from_millis(1)).is_ok(),
false,
"milliseconds",
),
]
}
fn configuration_construction_cells() -> Vec<Cell> {
const SECRET_CEILING: usize = 64 * 1024;
const DEPTH_CEILING: usize = 4;
vec![
Cell::dimensioned(
"cli-configuration-document",
Some("bytes"),
"at the ceiling",
CONFIG_CEILING as u64,
CONFIG_CEILING as u64,
configuration_accepted(CONFIG_CEILING),
true,
"bytes",
),
Cell::dimensioned(
"cli-configuration-document",
Some("bytes"),
"one byte past the ceiling",
CONFIG_CEILING as u64,
CONFIG_CEILING as u64 + 1,
configuration_accepted(CONFIG_CEILING + 1),
false,
"bytes",
),
Cell::dimensioned(
"cli-configuration-document",
Some("secret-bytes"),
"at the ceiling",
SECRET_CEILING as u64,
SECRET_CEILING as u64,
secret_file_accepted(SECRET_CEILING),
true,
"bytes",
),
Cell::dimensioned(
"cli-configuration-document",
Some("secret-bytes"),
"one byte past the ceiling",
SECRET_CEILING as u64,
SECRET_CEILING as u64 + 1,
secret_file_accepted(SECRET_CEILING + 1),
false,
"bytes",
),
Cell::dimensioned(
"cli-configuration-document",
Some("depth"),
"at the depth ceiling",
DEPTH_CEILING as u64,
DEPTH_CEILING as u64,
nesting_avoids_the_depth_ceiling(DEPTH_CEILING),
true,
"levels",
),
Cell::dimensioned(
"cli-configuration-document",
Some("depth"),
"one level past the depth ceiling",
DEPTH_CEILING as u64,
DEPTH_CEILING as u64 + 1,
nesting_avoids_the_depth_ceiling(DEPTH_CEILING + 1),
false,
"levels",
),
]
}
fn secret_file_accepted(bytes: usize) -> bool {
let contents =
r#"{"config_version":1,"repository":{"ca_certificate__FILE":"ca.pem"}}"#.to_owned();
let secret = "x".repeat(bytes);
let (services, _repository) = services();
let catalog = test_catalog(JOB);
let mut host = TestHost::new()
.with_file("secret-config.json", &contents)
.with_file("ca.pem", &secret);
let category = run_with_catalog(
&mut host,
&services,
&catalog,
"config show --config secret-config.json --output json",
);
category == ExitCategory::Success
}
fn nesting_avoids_the_depth_ceiling(depth: usize) -> bool {
let mut value = "\"leaf\"".to_owned();
for _ in 1..depth {
value = format!(r#"{{"w":{value}}}"#);
}
let contents = format!(r#"{{"config_version":1,"probe":{value}}}"#);
let (services, _repository) = services();
let catalog = test_catalog(JOB);
let mut host = TestHost::new().with_file("depth-config.json", &contents);
let _ = run_with_catalog(
&mut host,
&services,
&catalog,
"config show --config depth-config.json --output json",
);
!host.stderr_text().contains("nests deeper than")
}
fn allowlist_of(names: usize) -> bool {
let steps = (0..names)
.filter_map(|index| StepName::new(format!("allow-{index:04}")).ok())
.collect::<Vec<_>>();
if steps.len() != names {
return false;
}
MetricCardinalityGuard::new(Vec::new(), steps).is_ok()
}
fn configuration_accepted(bytes: usize) -> bool {
let contents = if bytes == 0 {
r#"{"config_version":1,"output":{"page_size":10}}"#.to_owned()
} else {
let prefix = "{\"config_version\":1,\"output\":{\"page_size\":10},\"repository\":{\"url\":\"postgres://user:pass@host/db?note=";
let filler = bytes.saturating_sub(prefix.len() + 3);
format!("{prefix}{}\"}}}}", "f".repeat(filler))
};
let (services, _repository) = services();
let catalog = test_catalog(JOB);
let mut host = TestHost::new().with_file("shedding-config.json", &contents);
let category = run_with_catalog(
&mut host,
&services,
&catalog,
"config show --config shedding-config.json --output json",
);
category == ExitCategory::Success
}
fn record() -> TelemetryRecord {
TelemetryRecord::catalog(TelemetryEventKind::JobStarted)
}
fn retain(document: &Value) -> Result<(), Box<dyn Error>> {
let Ok(directory) = std::env::var(OBSERVATIONS_ENV) else {
return Ok(());
};
if directory.is_empty() {
return Ok(());
}
let directory = std::path::PathBuf::from(directory);
std::fs::create_dir_all(&directory)?;
std::fs::write(
directory.join(format!("{REPORT}.json")),
format!("{}\n", serde_json::to_string_pretty(document)?),
)?;
Ok(())
}
fn workspace_root() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn semantics_paths() -> Result<Vec<String>, Box<dyn Error>> {
let path = workspace_root()
.join("tests")
.join("fixtures")
.join("resource-bounds")
.join("campaign-semantics.json");
let document: Value = serde_json::from_str(&std::fs::read_to_string(&path)?)?;
let categories = document
.get("categories")
.and_then(Value::as_object)
.ok_or_else(|| ReportFailure("the semantics document declares no categories".to_owned()))?;
let mut paths = categories
.values()
.filter_map(|category| category.get("paths").and_then(Value::as_array))
.flatten()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect::<Vec<_>>();
paths.sort();
paths.dedup();
if paths.is_empty() {
return Err(Box::new(ReportFailure(
"the semantics document declares no paths".to_owned(),
)));
}
Ok(paths)
}
fn execution_manifest() -> Result<Value, Box<dyn Error>> {
let root = workspace_root();
let commit = git(&root, &["rev-parse", "HEAD"])
.ok_or_else(|| ReportFailure("the campaign is not running inside a git tree".to_owned()))?;
let mut objects = serde_json::Map::new();
for path in semantics_paths()? {
let object = git(&root, &["rev-parse", &format!("HEAD:{path}")]).ok_or_else(|| {
ReportFailure(format!(
"{path} is declared as campaign semantics and is not present"
))
})?;
objects.insert(path, Value::String(object));
}
Ok(json!({
"execution_commit": commit,
"execution_commit_note": "The tree this run actually executed against, read from the \
checkout the campaign is running in. In CI this is the \
pull-request merge commit rather than the branch head, and it \
is the authority: the objects below are its objects.",
"tree_clean": git(&root, &["status", "--porcelain"]).map(|status| status.is_empty()),
"objects": Value::Object(objects),
}))
}
fn git(root: &std::path::Path, arguments: &[&str]) -> Option<String> {
let output = std::process::Command::new("git")
.current_dir(root)
.args(arguments)
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
#[derive(Debug)]
struct ReportFailure(String);
impl std::fmt::Display for ReportFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for ReportFailure {}
struct SheddingSink {
queue: TelemetryQueue,
offered: AtomicUsize,
}
impl SheddingSink {
fn new(bound: usize, prefill: bool) -> Self {
let queue = TelemetryQueue::new(
ExportQueueBound::new(bound).unwrap_or_default(),
DropReportWindow::default(),
);
if prefill {
for index in 0..bound {
let _ = queue.enqueue(record(), Duration::from_millis(index as u64));
}
}
Self {
queue,
offered: AtomicUsize::new(0),
}
}
fn shed(&self) -> u64 {
self.queue.dropped()
}
}
impl TelemetryEventSink for SheddingSink {
fn emit(&self, event: &TelemetryRecord) {
let offered = self.offered.fetch_add(1, Ordering::SeqCst);
let _ = self
.queue
.enqueue(event.clone(), Duration::from_millis(offered as u64));
}
}
struct Launch {
category: ExitCategory,
shed: u64,
durable: Value,
}
struct Shed {
resource: &'static str,
policy: &'static str,
rule: &'static str,
ceiling: u64,
configured: u64,
offered: u64,
peak: u64,
retained: u64,
discarded: u64,
violations: Vec<String>,
}
impl Shed {
fn failed(resource: &'static str, reason: &str) -> Self {
Self {
resource,
policy: "bounded-shedding",
rule: "unknown",
ceiling: 0,
configured: 0,
offered: 0,
peak: 0,
retained: 0,
discarded: 0,
violations: vec![reason.to_owned()],
}
}
fn evidence(&self) -> Value {
json!({
"resource": self.resource,
"overload_policy": self.policy,
"shedding_rule": self.rule,
"declared_ceiling": self.ceiling,
"configured_ceiling": self.configured,
"offered_load": self.offered,
"observed_peak_occupancy": self.peak,
"retained": self.retained,
"drops": self.discarded,
"rejections": 0,
"waits": 0,
"violations": self.violations,
"passed": self.violations.is_empty(),
})
}
}
struct Equivalence {
baseline_shed: u64,
saturated_shed: u64,
baseline_durable: Value,
saturated_durable: Value,
violations: Vec<String>,
}
impl Equivalence {
fn evidence(&self) -> Value {
let agrees = self.baseline_durable == self.saturated_durable;
json!({
"baseline_dropped_records": self.baseline_shed,
"saturated_dropped_records": self.saturated_shed,
"baseline_durable": self.baseline_durable,
"saturated_durable": self.saturated_durable,
"fields_compared": [{ "field": "durable-record", "agrees": agrees }],
"must_not_observe": [],
"agrees": agrees,
"violations": self.violations,
"passed": self.violations.is_empty(),
})
}
}
struct Cell {
resource: &'static str,
subject: Option<&'static str>,
case: &'static str,
declared: Option<u64>,
unit: Option<&'static str>,
value: u64,
accepted: bool,
expected: bool,
}
impl Cell {
const fn new(
resource: &'static str,
case: &'static str,
value: u64,
accepted: bool,
expected: bool,
) -> Self {
Self {
resource,
subject: None,
case,
declared: None,
unit: None,
value,
accepted,
expected,
}
}
#[allow(clippy::too_many_arguments)]
const fn dimensioned(
resource: &'static str,
subject: Option<&'static str>,
case: &'static str,
declared: u64,
value: u64,
accepted: bool,
expected: bool,
unit: &'static str,
) -> Self {
Self {
resource,
subject,
case,
declared: Some(declared),
unit: Some(unit),
value,
accepted,
expected,
}
}
fn violation(&self) -> Option<String> {
(self.accepted != self.expected).then(|| {
let subject = self.subject.unwrap_or(self.resource);
if self.expected {
format!(
"{subject} refused {} {}, which is inside its declared bound",
self.case, self.value,
)
} else {
format!(
"{subject} accepted {} {}, which is outside its declared bound",
self.case, self.value,
)
}
})
}
fn evidence(&self) -> Value {
json!({
"resource": self.resource,
"subject": self.subject,
"case": self.case,
"declared_ceiling": self.declared.or(match self.resource {
"retained-incident-events" => Some(MAX_RETAINED_EVENTS_PER_EXECUTION as u64),
"metric-name-allowlist" => Some(MAX_METRIC_NAME_ALLOWLIST as u64),
"diagnostic-bundle" => Some(BUNDLE_CEILING as u64),
_ => None,
}),
"unit": self.unit.or(match self.resource {
"retained-incident-events" => Some("events per execution"),
"metric-name-allowlist" => Some("names"),
"diagnostic-bundle" => Some("bytes"),
_ => None,
}),
"value": self.value,
"expected": if self.expected { "accepted" } else { "refused" },
"observed": if self.accepted { "accepted" } else { "refused" },
})
}
}