1use crate::QueryError;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Budget {
16 WorkingSet,
18 Response,
21}
22
23impl Budget {
24 #[must_use]
27 pub fn exceeded(self, limit: u64) -> QueryError {
28 QueryError::ResourceLimit(match self {
29 Self::WorkingSet => format!(
30 "query working set would exceed query-memory-max ({limit} bytes); \
31 raise --query-memory-max or narrow the filter"
32 ),
33 Self::Response => format!(
34 "response would exceed the response size limit ({limit} bytes); \
35 raise --body-limit or request a smaller page"
36 ),
37 })
38 }
39
40 pub(crate) fn check(self, limit: Option<usize>, bytes: usize) -> Result<(), QueryError> {
42 match limit {
43 Some(limit) if bytes > limit => Err(self.exceeded(as_u64(limit))),
44 _ => Ok(()),
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug)]
52pub(crate) struct Cap {
53 kind: Budget,
54 limit: usize,
55 used: usize,
56}
57
58impl Cap {
59 pub(crate) fn new(kind: Budget, limit: usize) -> Self {
60 Self {
61 kind,
62 limit,
63 used: 0,
64 }
65 }
66
67 pub(crate) fn for_response(response: Option<usize>, configured: Option<u64>) -> Option<Self> {
74 match response {
75 Some(bytes) => Some(Self::new(Budget::Response, bytes)),
76 None => configured.filter(|bytes| *bytes != 0).map(|bytes| {
77 Self::new(
78 Budget::WorkingSet,
79 usize::try_from(bytes).unwrap_or(usize::MAX),
80 )
81 }),
82 }
83 }
84
85 pub(crate) fn check(self, bytes: usize) -> Result<(), QueryError> {
87 if self.used.saturating_add(bytes) > self.limit {
88 return Err(self.kind.exceeded(as_u64(self.limit)));
89 }
90 Ok(())
91 }
92
93 pub(crate) fn check_opt(cap: Option<Self>, bytes: usize) -> Result<(), QueryError> {
95 cap.map_or(Ok(()), |cap| cap.check(bytes))
96 }
97
98 pub(crate) fn after(self, bytes: usize) -> Self {
100 Self {
101 used: self.used.saturating_add(bytes),
102 ..self
103 }
104 }
105
106 pub(crate) fn remaining(self) -> usize {
108 self.limit.saturating_sub(self.used)
109 }
110
111 pub(crate) fn limit(self) -> usize {
112 self.limit
113 }
114}
115
116pub(crate) struct WorkingSet {
118 pub(crate) limit: Option<u64>,
119 pub(crate) charged: u64,
120}
121
122impl WorkingSet {
123 pub(crate) fn charge(&mut self, bytes: u64, retained: bool) -> Result<(), QueryError> {
126 if let Some(limit) = self.limit {
127 if self.charged.saturating_add(bytes) > limit {
128 return Err(Budget::WorkingSet.exceeded(limit));
129 }
130 if retained {
131 self.charged += bytes;
132 }
133 }
134 Ok(())
135 }
136}
137
138fn as_u64(bytes: usize) -> u64 {
139 u64::try_from(bytes).unwrap_or(u64::MAX)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::{Budget, Cap, WorkingSet};
145 use crate::QueryError;
146
147 fn message(error: QueryError) -> String {
148 match error {
149 QueryError::ResourceLimit(message) => message,
150 other => panic!("expected a resource limit, got {other:?}"),
151 }
152 }
153
154 #[test]
155 fn each_budget_names_its_own_limit_and_flag() {
156 let working = message(Budget::WorkingSet.check(Some(10), 11).unwrap_err());
157 assert!(working.contains("query-memory-max (10 bytes)"), "{working}");
158 assert!(working.contains("--query-memory-max"), "{working}");
159 assert!(!working.contains("body-limit"), "{working}");
160 let response = message(Cap::new(Budget::Response, 10).check(11).unwrap_err());
161 assert!(
162 response.contains("response size limit (10 bytes)"),
163 "{response}"
164 );
165 assert!(response.contains("--body-limit"), "{response}");
166 assert!(!response.contains("query-memory-max"), "{response}");
167 let mut set = WorkingSet {
168 limit: Some(8),
169 charged: 0,
170 };
171 set.charge(8, true).unwrap();
172 let records = message(set.charge(1, false).unwrap_err());
173 assert_eq!(records, working.replace("(10 bytes)", "(8 bytes)"));
174 }
175
176 #[test]
177 fn a_cap_spends_from_one_allowance_and_reports_the_whole_limit() {
178 let cap = Cap::new(Budget::Response, 100).after(60);
179 assert_eq!(cap.remaining(), 40);
180 cap.check(40).unwrap();
181 let error = message(cap.check(41).unwrap_err());
182 assert!(error.contains("(100 bytes)"), "{error}");
183 assert!(Cap::for_response(None, Some(0)).is_none());
184 assert_eq!(Cap::for_response(Some(0), None).unwrap().remaining(), 0);
185 let fallback = Cap::for_response(None, Some(5)).unwrap();
186 assert!(message(fallback.check(6).unwrap_err()).contains("query-memory-max"));
187 }
188}