-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Track registration rate and expose metrics
This PR introduces the functionality of tracking the current and the reference rate of new registrations. In addition, it will also always calculate and expose the threshold rate that would need to be crossed to trigger the captcha being activated. This will be useful to later analyze how often the captcha was actually shown. Note: This is only the tracking / metrics. The captcha itslef is not yet dynamic.
- Loading branch information
1 parent
6a6f616
commit 2707555
Showing
5 changed files
with
215 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
src/internet_identity/src/storage/registration_rates.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
//! Module to track the rate at which new registrations are started over both a longer reference | ||
//! period and a short period (to determine the current rate). | ||
//! These rates are then used to determine whether a captcha needs to be solved or not. | ||
|
||
use crate::state; | ||
use ic_cdk::api::time; | ||
use ic_stable_structures::{Memory, MinHeap}; | ||
use internet_identity_interface::internet_identity::types::{CaptchaTrigger, Timestamp}; | ||
use std::time::Duration; | ||
|
||
pub struct RegistrationRates<M: Memory> { | ||
reference_rate_data: MinHeap<Timestamp, M>, | ||
current_rate_data: MinHeap<Timestamp, M>, | ||
} | ||
|
||
pub struct NormalizedRegistrationRates { | ||
pub reference_rate_per_second: f64, | ||
pub current_rate_per_second: f64, | ||
pub captcha_threshold_rate: f64, | ||
} | ||
|
||
struct DynamicCaptchaConfig { | ||
reference_rate_retention_ns: u64, | ||
current_rate_retention_ns: u64, | ||
threshold_multiplier: f64, | ||
} | ||
|
||
impl<M: Memory> RegistrationRates<M> { | ||
pub fn new( | ||
reference_rate_data: MinHeap<Timestamp, M>, | ||
current_rate_data: MinHeap<Timestamp, M>, | ||
) -> Self { | ||
Self { | ||
reference_rate_data, | ||
current_rate_data, | ||
} | ||
} | ||
pub fn new_registration(&mut self) { | ||
self.prune_expired(); | ||
let Some(data_retention) = dynamic_captcha_config() else { | ||
return; | ||
}; | ||
|
||
let now = time(); | ||
self.reference_rate_data | ||
.push(&(now + data_retention.reference_rate_retention_ns)) | ||
.expect("out of memory"); | ||
self.current_rate_data | ||
.push(&(now + data_retention.current_rate_retention_ns)) | ||
.expect("out of memory"); | ||
} | ||
|
||
pub fn registration_rates(&self) -> Option<NormalizedRegistrationRates> { | ||
let Some(config) = dynamic_captcha_config() else { | ||
return None; | ||
}; | ||
|
||
let now = time(); | ||
let reference_rate_per_second = calculate_rate( | ||
now, | ||
&self.reference_rate_data, | ||
config.reference_rate_retention_ns, | ||
); | ||
let current_rate_per_second = calculate_rate( | ||
now, | ||
&self.current_rate_data, | ||
config.current_rate_retention_ns, | ||
); | ||
let captcha_threshold_rate = reference_rate_per_second * config.threshold_multiplier; | ||
Some(NormalizedRegistrationRates { | ||
reference_rate_per_second, | ||
current_rate_per_second, | ||
captcha_threshold_rate, | ||
}) | ||
} | ||
|
||
fn prune_expired(&mut self) { | ||
prune_data(&mut self.reference_rate_data); | ||
prune_data(&mut self.current_rate_data); | ||
} | ||
} | ||
|
||
/// Calculates the rate per second of registrations taking into account for how long data has | ||
/// already been collected. | ||
/// | ||
/// E.g. if `data_retention_ns` is 3 weeks, the rate cannot just be calculated over a 3-week time | ||
/// window because until 3 weeks of data has been collected the rate would be inaccurate. | ||
/// In particular, it would underestimate the actual rate leading to the captcha threshold being | ||
/// reached more easily thus potentially triggering the captcha prematurely. | ||
fn calculate_rate<M: Memory>( | ||
now: u64, | ||
data: &MinHeap<Timestamp, M>, | ||
data_retention_ns: u64, | ||
) -> f64 { | ||
data | ||
// get the oldest expiration timestamp | ||
.peek() | ||
// calculate the registration timestamp from expiration | ||
.map(|ts| ts - data_retention_ns) | ||
// calculate the time window length with respect to the current time | ||
.map(|ts| now - ts) | ||
// the value _could_ be 0 if the oldest timestamp was added in the same execution round | ||
// -> filter to avoid division by 0 | ||
.filter(|val| *val != 0) | ||
// use the value to calculate the rate per second | ||
.map(|val| rate_per_second(data.len(), val)) | ||
// if we don't have data, the rate is 0 | ||
.unwrap_or(0.0) | ||
} | ||
|
||
fn rate_per_second(count: u64, duration_ns: u64) -> f64 { | ||
count as f64 / Duration::from_nanos(duration_ns).as_secs() as f64 | ||
} | ||
|
||
fn dynamic_captcha_config() -> Option<DynamicCaptchaConfig> { | ||
let trigger = state::persistent_state(|ps| ps.captcha_config.captcha_trigger.clone()); | ||
match trigger { | ||
CaptchaTrigger::Static(_) => None, | ||
CaptchaTrigger::Dynamic { | ||
current_rate_sampling_interval_s, | ||
reference_rate_sampling_interval_s, | ||
threshold_pct, | ||
} => Some(DynamicCaptchaConfig { | ||
reference_rate_retention_ns: Duration::from_secs(reference_rate_sampling_interval_s) | ||
.as_nanos() as u64, | ||
current_rate_retention_ns: Duration::from_secs(current_rate_sampling_interval_s) | ||
.as_nanos() as u64, | ||
threshold_multiplier: 1.0 + (threshold_pct as f64 / 100.0), | ||
}), | ||
} | ||
} | ||
|
||
fn prune_data<M: Memory>(data: &mut MinHeap<Timestamp, M>) { | ||
const MAX_TO_PRUNE: usize = 100; | ||
|
||
let now = time(); | ||
for _ in 0..MAX_TO_PRUNE { | ||
let Some(timestamp) = data.peek() else { | ||
break; | ||
}; | ||
|
||
// The timestamps are sorted because the expiration is constant and time() is monotonic | ||
// -> we can stop pruning once we reach a not expired timestamp | ||
if timestamp > now { | ||
break; | ||
} | ||
data.pop(); | ||
} | ||
} |