Skip to content

Commit

Permalink
Add a geolocator based by the IP-API free tier
Browse files Browse the repository at this point in the history
Because of the limitations of the free tier, this new geolocator is
backed by another geolocator that can provide answers when the free
tier quota is exhausted.

To make this possible, this change adds a new SettableClock fake
clock for testing purposes, which is much nicer to use than the older
MonotonicClock.

Additionally, this change also adds a RequestCounter to keep track of
how many requests have been sent.  I'm keeping this in the geo crate
for now, but this type of request counter should likely be placed in
the core crate; will do so when the need arises.
  • Loading branch information
jmmv committed Sep 27, 2023
1 parent c26eef2 commit 06258f0
Show file tree
Hide file tree
Showing 5 changed files with 432 additions and 1 deletion.
35 changes: 35 additions & 0 deletions core/src/clocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ impl Clock for SystemClock {
pub mod testutils {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use time::{Date, Month, Time};

/// A clock that returns a monotonically increasing instant every time it is queried.
Expand All @@ -68,7 +69,41 @@ pub mod testutils {
}
}

/// A clock that returns a preconfigured instant and that can be modified at will.
///
/// Only supports second-level precision.
pub struct SettableClock {
/// Current fake time.
now: AtomicU64,
}

impl SettableClock {
/// Creates a new clock that returns `now` until reconfigured with `set`.
pub fn new(now: OffsetDateTime) -> Self {
Self { now: AtomicU64::new(now.unix_timestamp() as u64) }
}

/// Sets the new value of `now` that the clock returns.
pub fn set(&self, now: OffsetDateTime) {
self.now.store(now.unix_timestamp() as u64, Ordering::SeqCst);
}

/// Advances the current time by `delta`.
pub fn advance(&self, delta: Duration) {
let seconds = delta.as_secs();
self.now.fetch_add(seconds, Ordering::SeqCst);
}
}

impl Clock for SettableClock {
fn now_utc(&self) -> OffsetDateTime {
let now = self.now.load(Ordering::SeqCst);
OffsetDateTime::from_unix_timestamp(now as i64).unwrap()
}
}

/// Creates an `OffsetDateTime` with the given values, assuming UTC.
// TODO(jmmv): Remove in favor of the datetime!() macro from the time crate.
pub fn utc_datetime(
year: i32,
month: u8,
Expand Down
4 changes: 3 additions & 1 deletion geo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ async-trait = "0.1"
bytes = "1.0"
derivative = "2.2"
futures = "0.3"
iii-iv-core = { path = "../core" }
log = "0.4"
lru_time_cache = "0.11"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
serde = "1"
serde_json = "1"
iii-iv-core = { path = "../core" }
time = "0.3"

[dev-dependencies]
iii-iv-core = { path = "../core", features = ["testutils"] }
serde_test = "1"
temp-env = "0.3.2"
time = { version = "0.3", features = ["macros"] }
tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"] }
122 changes: 122 additions & 0 deletions geo/src/counter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// III-IV
// Copyright 2023 Julio Merino
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

//! Counter of requests for a period of time.

use iii_iv_core::clocks::Clock;
use std::{sync::Arc, time::Duration};

/// Counts the number of requests over the last minute with second resolution.
pub(crate) struct RequestCounter {
/// Clock to obtain the current time from.
clock: Arc<dyn Clock + Send + Sync>,

/// Tracker of per-second counts within a minute.
///
/// Each pair contains the timestamp of the ith second in the array and
/// the counter of requests at that second.
counts: [(i64, u16); 60],
}

impl RequestCounter {
/// Creates a new request counter backed by `clock`.
pub(crate) fn new(clock: Arc<dyn Clock + Send + Sync>) -> Self {
Self { clock, counts: [(0, 0); 60] }
}

/// Adds a request to the counter at the current time.
pub(crate) fn account(&mut self) {
let now = self.clock.now_utc();
let i = usize::from(now.second()) % 60;
let (ts, count) = self.counts[i];
if ts == now.unix_timestamp() {
self.counts[i] = (ts, count + 1);
} else {
self.counts[i] = (now.unix_timestamp(), 1);
}
}

/// Counts the number of requests during the last minute.
pub(crate) fn last_minute(&self) -> usize {
let now = self.clock.now_utc();
let since = (now - Duration::from_secs(60)).unix_timestamp();

let mut total = 0;
for (ts, count) in self.counts {
if ts > since {
total += usize::from(count);
}
}
total
}
}

#[cfg(test)]
mod tests {
use super::*;
use iii_iv_core::clocks::testutils::SettableClock;
use std::time::Duration;
use time::macros::datetime;

#[test]
fn test_continuous() {
let clock = Arc::from(SettableClock::new(datetime!(2023-09-26 18:20:15 UTC)));
let mut counter = RequestCounter::new(clock.clone());

assert_eq!(0, counter.last_minute());
for i in 0..60 {
clock.advance(Duration::from_secs(1));
counter.account();
counter.account();
assert_eq!((i + 1) * 2, counter.last_minute());
}
assert_eq!(120, counter.last_minute());
for i in 0..60 {
clock.advance(Duration::from_secs(1));
counter.account();
assert_eq!(120 - (i + 1), counter.last_minute());
}
assert_eq!(60, counter.last_minute());
for i in 0..60 {
clock.advance(Duration::from_secs(1));
assert_eq!(60 - (i + 1), counter.last_minute());
}
assert_eq!(0, counter.last_minute());
}

#[test]
fn test_gaps() {
let clock = Arc::from(SettableClock::new(datetime!(2023-09-26 17:20:56 UTC)));
let mut counter = RequestCounter::new(clock.clone());

assert_eq!(0, counter.last_minute());
for _ in 0..1000 {
counter.account();
}
assert_eq!(1000, counter.last_minute());

clock.advance(Duration::from_secs(30));
counter.account();
assert_eq!(1001, counter.last_minute());

clock.advance(Duration::from_secs(29));
counter.account();
assert_eq!(1002, counter.last_minute());

clock.advance(Duration::from_secs(1));
counter.account();
assert_eq!(3, counter.last_minute());
}
}
Loading

0 comments on commit 06258f0

Please sign in to comment.