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