Skip to main content

elasticctl_api/rules/
window.rs

1//! The verified corpus read: every rule matching a filter, read in pages
2//! ordered by creation time and checked before it is returned.
3
4use std::collections::HashSet;
5
6use super::{RuleFilter, find};
7use crate::model::Rule;
8use elasticctl_core::{Error, ErrorKind, Result, Transport};
9
10/// Rules per corpus page. A 500-rule page measured at most 4,458,395 bytes,
11/// well under the response body cap.
12pub const WINDOW_PAGE: u32 = 500;
13
14/// The largest corpus `find_window` reads, in at most eight pages. The latency
15/// gate sets it; raising it needs a new measurement.
16pub const WINDOW_MAX_RULES: u64 = 4_000;
17
18/// Creation order keeps earlier pages stable when a rule is created during the
19/// read. Default-order paging returned duplicate rows on Hosted 9.5.6.
20const SORT: &str = "&sort_field=created_at&sort_order=asc";
21
22/// Read passes per call, so a call sends at most 16 requests.
23const PASSES: usize = 2;
24
25/// Every rule matching `filter`, in creation order.
26///
27/// Page 1 gives the total. A total above `WINDOW_MAX_RULES` fails with kind
28/// `unsupported` before page 2. A pass is kept only when every page reported
29/// the same total, the row count equals that total, and every `rule_id` is
30/// distinct. A failed pass is discarded and the read starts again from page 1
31/// once; a second failed pass is kind `conflict`.
32pub async fn find_window(t: &Transport, filter: &RuleFilter) -> Result<Vec<Rule>> {
33    for _ in 0..PASSES {
34        if let Some(rules) = read_pass(t, filter).await? {
35            return Ok(rules);
36        }
37    }
38    Err(Error::new(
39        ErrorKind::Conflict,
40        "the matching rules changed during both corpus read passes; retry the read",
41    ))
42}
43
44/// One pass over every page. `None` means the pass failed verification.
45async fn read_pass(t: &Transport, filter: &RuleFilter) -> Result<Option<Vec<Rule>>> {
46    let (mut rules, total) = find(t, filter, 1, WINDOW_PAGE, SORT).await?;
47    if total > WINDOW_MAX_RULES {
48        return Err(Error::new(
49            ErrorKind::Unsupported,
50            format!(
51                "{total} rules match, more than the {WINDOW_MAX_RULES} a corpus read accepts. \
52                 Narrow the selection with a filter."
53            ),
54        ));
55    }
56    // At most eight pages after the check above.
57    let last_page = total.div_ceil(u64::from(WINDOW_PAGE)) as u32;
58    let mut same_total = true;
59    for page in 2..=last_page {
60        let (more, page_total) = find(t, filter, page, WINDOW_PAGE, SORT).await?;
61        same_total &= page_total == total;
62        rules.extend(more);
63    }
64    let ids = rules
65        .iter()
66        .map(Rule::rule_id)
67        .collect::<Result<HashSet<_>>>()?;
68    let verified = same_total && rules.len() as u64 == total && ids.len() == rules.len();
69    Ok(verified.then_some(rules))
70}