1use crate::{
36 metrics::HopMetrics,
37 pool::HopDataPool,
38 rate_limit::RateLimitConfig,
39 types::{
40 HopError, DEFAULT_BANDWIDTH_BURST_MIB, DEFAULT_BANDWIDTH_PER_MIN_MIB,
41 DEFAULT_CHECK_INTERVAL_SECS, DEFAULT_MAX_POOL_SIZE_MIB, DEFAULT_MAX_USER_SIZE_MIB,
42 DEFAULT_PROMOTION_BUFFER_SECS, DEFAULT_RETENTION_SECS, DEFAULT_SUBMIT_BURST,
43 DEFAULT_SUBMIT_RATE_PER_MIN,
44 },
45};
46use clap::Parser;
47use prometheus_endpoint::Registry;
48use std::{path::PathBuf, sync::Arc};
49
50#[derive(Debug, Clone, Parser)]
52pub struct HopParams {
53 #[arg(id = "enable-hop", long = "enable-hop", default_value_t = false)]
55 pub enabled: bool,
56
57 #[arg(
59 long = "hop-max-pool-size",
60 default_value_t = DEFAULT_MAX_POOL_SIZE_MIB,
61 value_parser = clap::value_parser!(u64).range(1..),
62 )]
63 pub max_pool_size: u64,
64
65 #[arg(
68 long = "hop-max-user-size",
69 default_value_t = DEFAULT_MAX_USER_SIZE_MIB,
70 value_parser = clap::value_parser!(u64).range(1..),
71 )]
72 pub max_user_size: u64,
73
74 #[arg(
76 long = "hop-retention-secs",
77 default_value_t = DEFAULT_RETENTION_SECS,
78 value_parser = clap::value_parser!(u64).range(1..),
79 )]
80 pub retention_secs: u64,
81
82 #[arg(
85 long = "hop-check-interval",
86 default_value_t = DEFAULT_CHECK_INTERVAL_SECS,
87 value_parser = clap::value_parser!(u64).range(1..),
88 )]
89 pub check_interval: u64,
90
91 #[arg(
93 long = "hop-promotion-buffer-secs",
94 default_value_t = DEFAULT_PROMOTION_BUFFER_SECS,
95 value_parser = clap::value_parser!(u64).range(1..),
96 )]
97 pub promotion_buffer_secs: u64,
98
99 #[arg(
102 long = "hop-submit-rate-per-min",
103 default_value_t = DEFAULT_SUBMIT_RATE_PER_MIN,
104 value_parser = clap::value_parser!(u32).range(1..),
105 )]
106 pub submit_rate_per_min: u32,
107
108 #[arg(
110 long = "hop-submit-burst",
111 default_value_t = DEFAULT_SUBMIT_BURST,
112 value_parser = clap::value_parser!(u32).range(1..),
113 )]
114 pub submit_burst: u32,
115
116 #[arg(
119 long = "hop-bandwidth-per-min-mib",
120 default_value_t = DEFAULT_BANDWIDTH_PER_MIN_MIB,
121 value_parser = clap::value_parser!(u64).range(1..),
122 )]
123 pub bandwidth_per_min_mib: u64,
124
125 #[arg(
127 long = "hop-bandwidth-burst-mib",
128 default_value_t = DEFAULT_BANDWIDTH_BURST_MIB,
129 value_parser = clap::value_parser!(u64).range(1..),
130 )]
131 pub bandwidth_burst_mib: u64,
132
133 #[arg(long = "hop-disable-rate-limit")]
135 pub disable_rate_limit: bool,
136
137 #[arg(long = "hop-data-dir")]
141 pub data_dir: Option<std::path::PathBuf>,
142}
143
144impl Default for HopParams {
145 fn default() -> Self {
146 Self {
147 enabled: false,
148 max_pool_size: DEFAULT_MAX_POOL_SIZE_MIB,
149 max_user_size: DEFAULT_MAX_USER_SIZE_MIB,
150 retention_secs: DEFAULT_RETENTION_SECS,
151 check_interval: DEFAULT_CHECK_INTERVAL_SECS,
152 promotion_buffer_secs: DEFAULT_PROMOTION_BUFFER_SECS,
153 submit_rate_per_min: DEFAULT_SUBMIT_RATE_PER_MIN,
154 submit_burst: DEFAULT_SUBMIT_BURST,
155 bandwidth_per_min_mib: DEFAULT_BANDWIDTH_PER_MIN_MIB,
156 bandwidth_burst_mib: DEFAULT_BANDWIDTH_BURST_MIB,
157 disable_rate_limit: false,
158 data_dir: None,
159 }
160 }
161}
162
163impl HopParams {
164 pub fn rate_limit_config(&self) -> RateLimitConfig {
166 if self.disable_rate_limit {
167 return RateLimitConfig::disabled();
168 }
169 RateLimitConfig {
170 enabled: true,
171 submit_rate_per_min: self.submit_rate_per_min,
172 submit_burst: self.submit_burst,
173 bandwidth_per_min: self.bandwidth_per_min_mib.saturating_mul(1024 * 1024),
174 bandwidth_burst: self.bandwidth_burst_mib.saturating_mul(1024 * 1024),
175 }
176 }
177
178 pub fn build_pool(
187 &self,
188 database_path: Option<PathBuf>,
189 registry: Option<&Registry>,
190 ) -> Result<Arc<HopDataPool>, HopError> {
191 let data_dir = match &self.data_dir {
192 Some(dir) => dir.clone(),
193 None => database_path.ok_or(HopError::MissingDataDir)?.join("hop"),
194 };
195
196 tracing::info!(
197 target: "hop",
198 params = ?self,
199 data_dir = %data_dir.display(),
200 "Initializing HOP data pool",
201 );
202
203 let metrics = HopMetrics::new(registry).unwrap_or_else(|e| {
204 tracing::warn!(
205 target: "hop",
206 error = %e,
207 "Failed to register HOP metrics; continuing without metrics"
208 );
209 HopMetrics::disabled()
210 });
211
212 let pool = HopDataPool::new(
213 self.max_pool_size.saturating_mul(1024 * 1024),
214 self.max_user_size.saturating_mul(1024 * 1024),
215 self.retention_secs,
216 data_dir,
217 self.rate_limit_config(),
218 metrics,
219 )?;
220
221 tracing::info!(
222 target: "hop",
223 status = ?pool.status(),
224 "HOP data pool initialized, RPC methods will be registered",
225 );
226
227 Ok(Arc::new(pool))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use clap::Parser;
235
236 #[derive(Parser)]
238 struct TestCli {
239 #[clap(flatten)]
240 hop: HopParams,
241 }
242
243 #[test]
244 fn build_pool_without_any_dir_returns_missing_data_dir() {
245 match HopParams::default().build_pool(None, None) {
246 Err(HopError::MissingDataDir) => (),
247 Err(other) => panic!("expected MissingDataDir, got: {other:?}"),
248 Ok(_) => panic!("expected MissingDataDir, got Ok"),
249 }
250 }
251
252 #[test]
253 fn cli_rejects_zero_for_critical_numeric_parameters() {
254 let zero_flags = [
258 "--hop-max-pool-size",
259 "--hop-max-user-size",
260 "--hop-retention-secs",
261 "--hop-check-interval",
262 "--hop-promotion-buffer-secs",
263 "--hop-submit-rate-per-min",
264 "--hop-submit-burst",
265 "--hop-bandwidth-per-min-mib",
266 "--hop-bandwidth-burst-mib",
267 ];
268 for flag in zero_flags {
269 let argv = ["test-bin", flag, "0"];
270 let result = TestCli::try_parse_from(argv);
271 assert!(
272 result.is_err(),
273 "clap accepted zero for {flag} but it should have been rejected",
274 );
275 }
276 }
277
278 #[test]
279 fn cli_accepts_one_for_critical_numeric_parameters() {
280 let one_flags = ["--hop-max-pool-size", "--hop-retention-secs", "--hop-check-interval"];
281 for flag in one_flags {
282 let argv = ["test-bin", flag, "1"];
283 TestCli::try_parse_from(argv).expect("parse should succeed");
284 }
285 }
286}