mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-04 15:16:58 +02:00
774 lines
26 KiB
Rust
774 lines
26 KiB
Rust
use super::{
|
|
AlbumAvailability, TrackAvailability, album, deezer_id_from_metadata, from_deezer, from_links,
|
|
resolved_links,
|
|
};
|
|
use crate::deezer::MetadataLookup;
|
|
use crate::lyrics::{LyricsError, builtin::TrackResolver};
|
|
use crate::resolver::{Check, PlatformResolverService, Resolver, ResolverError};
|
|
use std::collections::BTreeMap;
|
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Condvar, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
enum Kind {
|
|
Availability,
|
|
Links,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum Value {
|
|
Availability(Box<TrackAvailability>),
|
|
Links(BTreeMap<String, String>),
|
|
}
|
|
type Outcome = Result<Value, ResolverError>;
|
|
|
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
struct Key {
|
|
kind: Kind,
|
|
region: String,
|
|
id: String,
|
|
isrc: bool,
|
|
}
|
|
|
|
struct Entry {
|
|
result: Outcome,
|
|
expires: Instant,
|
|
}
|
|
#[derive(Default)]
|
|
struct Flight {
|
|
waiters: AtomicUsize,
|
|
result: Mutex<Option<Outcome>>,
|
|
ready: Condvar,
|
|
}
|
|
struct Waiter(Arc<Flight>);
|
|
impl Drop for Waiter {
|
|
fn drop(&mut self) {
|
|
self.0.waiters.fetch_sub(1, Ordering::AcqRel);
|
|
}
|
|
}
|
|
|
|
struct State {
|
|
generation: u64,
|
|
region: String,
|
|
cache: BTreeMap<Key, Entry>,
|
|
flights: BTreeMap<(u64, Key), Arc<Flight>>,
|
|
active: usize,
|
|
}
|
|
|
|
struct Inner {
|
|
resolver: PlatformResolverService,
|
|
metadata: Arc<dyn MetadataLookup>,
|
|
state: Mutex<State>,
|
|
closed: AtomicBool,
|
|
idle: Condvar,
|
|
}
|
|
|
|
/// One native owner for raw resolver work and both application caches.
|
|
pub struct AvailabilityService {
|
|
inner: Arc<Inner>,
|
|
}
|
|
|
|
impl AvailabilityService {
|
|
pub fn new(resolver: Arc<dyn Resolver>, metadata: Arc<dyn MetadataLookup>) -> Self {
|
|
Self {
|
|
inner: Arc::new(Inner {
|
|
resolver: PlatformResolverService::new(resolver),
|
|
metadata,
|
|
state: Mutex::new(State {
|
|
generation: 0,
|
|
region: "US".into(),
|
|
cache: BTreeMap::new(),
|
|
flights: BTreeMap::new(),
|
|
active: 0,
|
|
}),
|
|
closed: AtomicBool::new(false),
|
|
idle: Condvar::new(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn set_region(&self, region: &str) -> Result<(), ResolverError> {
|
|
let mut state = self.inner.state.lock().unwrap();
|
|
self.inner.check(&|| Ok(()))?;
|
|
let region = spotiflac_core::matching::uppercase(region.trim());
|
|
state.region = if region.len() == 2 && region.bytes().all(|byte| byte.is_ascii_uppercase())
|
|
{
|
|
region
|
|
} else {
|
|
"US".into()
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
pub fn region(&self) -> Result<String, ResolverError> {
|
|
self.inner.check(&|| Ok(()))?;
|
|
Ok(self.inner.state.lock().unwrap().region.clone())
|
|
}
|
|
|
|
pub fn clear_cache(&self) -> Result<usize, ResolverError> {
|
|
let mut state = self.inner.state.lock().unwrap();
|
|
self.inner.check(&|| Ok(()))?;
|
|
state.generation = state.generation.wrapping_add(1);
|
|
let count = state.cache.len();
|
|
state.cache.clear();
|
|
Ok(count)
|
|
}
|
|
|
|
pub fn check_track(
|
|
&self,
|
|
spotify_id: &str,
|
|
isrc: &str,
|
|
check: &Check<'_>,
|
|
) -> Result<TrackAvailability, ResolverError> {
|
|
match self.lookup(Kind::Availability, spotify_id, isrc, check)? {
|
|
Value::Availability(value) => Ok(*value),
|
|
_ => unreachable!("availability cache type"),
|
|
}
|
|
}
|
|
|
|
pub fn track_platform_links(
|
|
&self,
|
|
spotify_id: &str,
|
|
isrc: &str,
|
|
check: &Check<'_>,
|
|
) -> Result<BTreeMap<String, String>, ResolverError> {
|
|
match self.lookup(Kind::Links, spotify_id, isrc, check)? {
|
|
Value::Links(value) => Ok(value),
|
|
_ => unreachable!("links cache type"),
|
|
}
|
|
}
|
|
|
|
pub fn check_album(
|
|
&self,
|
|
id: &str,
|
|
check: &Check<'_>,
|
|
) -> Result<AlbumAvailability, ResolverError> {
|
|
self.with_raw(check, |resolver, check| album(resolver, id, check))
|
|
}
|
|
|
|
pub fn check_from_deezer(
|
|
&self,
|
|
id: &str,
|
|
check: &Check<'_>,
|
|
) -> Result<TrackAvailability, ResolverError> {
|
|
self.with_raw(check, |resolver, check| from_deezer(resolver, id, check))
|
|
}
|
|
|
|
pub fn deezer_album_id(&self, id: &str, check: &Check<'_>) -> Result<String, ResolverError> {
|
|
let album = self.check_album(id, check)?;
|
|
if album.deezer && !album.deezer_id.is_empty() {
|
|
Ok(album.deezer_id)
|
|
} else {
|
|
Err(ResolverError::Failed("album not found on Deezer".into()))
|
|
}
|
|
}
|
|
|
|
fn with_raw<T>(
|
|
&self,
|
|
check: &Check<'_>,
|
|
work: impl FnOnce(&PlatformResolverService, &Check<'_>) -> Result<T, ResolverError>,
|
|
) -> Result<T, ResolverError> {
|
|
self.inner.check(check)?;
|
|
let result = work(&self.inner.resolver, &|| {
|
|
self.inner.check(check).map_err(|error| error.to_string())
|
|
});
|
|
self.inner.check(check)?;
|
|
result
|
|
}
|
|
|
|
fn lookup(&self, kind: Kind, spotify_id: &str, isrc: &str, check: &Check<'_>) -> Outcome {
|
|
self.inner.check(check)?;
|
|
let spotify_id = spotify_id.trim();
|
|
let isrc = spotiflac_core::matching::uppercase(isrc.trim());
|
|
let (id, isrc) = if !spotify_id.is_empty() {
|
|
(spotify_id.to_owned(), false)
|
|
} else if !isrc.is_empty() {
|
|
(isrc, true)
|
|
} else {
|
|
return Err(ResolverError::Failed(
|
|
"spotify track ID and ISRC are empty".into(),
|
|
));
|
|
};
|
|
if id.len() > 64 << 10 {
|
|
return Err(ResolverError::Failed(
|
|
"availability input exceeds limit".into(),
|
|
));
|
|
}
|
|
let (key, generation, flight, start) = {
|
|
let mut state = self.inner.state.lock().unwrap();
|
|
self.inner.check(check)?;
|
|
let key = Key {
|
|
kind,
|
|
region: state.region.clone(),
|
|
id,
|
|
isrc,
|
|
};
|
|
if let Some(entry) = state.cache.get(&key)
|
|
&& Instant::now() <= entry.expires
|
|
{
|
|
return entry.result.clone().map_err(|_| {
|
|
ResolverError::Failed(
|
|
if kind == Kind::Availability {
|
|
"track availability unavailable (cached)"
|
|
} else {
|
|
"track platform links unavailable (cached)"
|
|
}
|
|
.into(),
|
|
)
|
|
});
|
|
}
|
|
state.cache.remove(&key);
|
|
let generation = state.generation;
|
|
let flight_key = (generation, key.clone());
|
|
let existing = state
|
|
.flights
|
|
.get(&flight_key)
|
|
.filter(|flight| flight.waiters.load(Ordering::Acquire) > 0)
|
|
.cloned();
|
|
let (flight, start) = if let Some(flight) = existing {
|
|
(flight, false)
|
|
} else {
|
|
if state.active >= 64 {
|
|
return Err(ResolverError::Busy);
|
|
}
|
|
let flight = Arc::new(Flight::default());
|
|
state.flights.insert(flight_key, flight.clone());
|
|
state.active += 1;
|
|
(flight, true)
|
|
};
|
|
flight.waiters.fetch_add(1, Ordering::AcqRel);
|
|
(key, generation, flight, start)
|
|
};
|
|
let _waiter = Waiter(flight.clone());
|
|
if start {
|
|
let inner = self.inner.clone();
|
|
let worker_key = key.clone();
|
|
let worker_flight = flight.clone();
|
|
let spawned = std::thread::Builder::new()
|
|
.name("platform-availability".into())
|
|
.spawn(move || {
|
|
let check = || {
|
|
if inner.closed.load(Ordering::Acquire) {
|
|
Err("availability service closed".into())
|
|
} else if worker_flight.waiters.load(Ordering::Acquire) == 0 {
|
|
Err("availability request cancelled".into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
};
|
|
let result =
|
|
catch_unwind(AssertUnwindSafe(|| inner.fetch(&worker_key, &check)))
|
|
.unwrap_or_else(|_| {
|
|
Err(ResolverError::Failed("availability worker panicked".into()))
|
|
});
|
|
let result = check().map_err(ResolverError::Cancelled).and(result);
|
|
inner.finish(generation, &worker_key, &worker_flight, result);
|
|
});
|
|
if let Err(error) = spawned {
|
|
self.inner.finish(
|
|
generation,
|
|
&key,
|
|
&flight,
|
|
Err(ResolverError::Failed(error.to_string())),
|
|
);
|
|
}
|
|
}
|
|
loop {
|
|
self.inner.check(check)?;
|
|
let result = flight.result.lock().unwrap();
|
|
if let Some(value) = result.as_ref() {
|
|
let value = value.clone();
|
|
drop(result);
|
|
self.inner.check(check)?;
|
|
return value;
|
|
}
|
|
drop(
|
|
flight
|
|
.ready
|
|
.wait_timeout(result, Duration::from_millis(25))
|
|
.unwrap(),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn shutdown(&self) {
|
|
self.inner.closed.store(true, Ordering::Release);
|
|
let mut state = self.inner.state.lock().unwrap();
|
|
while state.active != 0 {
|
|
state = self.inner.idle.wait(state).unwrap();
|
|
}
|
|
state.cache.clear();
|
|
drop(state);
|
|
self.inner.resolver.shutdown();
|
|
}
|
|
}
|
|
|
|
impl Drop for AvailabilityService {
|
|
fn drop(&mut self) {
|
|
self.shutdown();
|
|
}
|
|
}
|
|
|
|
impl TrackResolver for AvailabilityService {
|
|
fn deezer_id_from_spotify(&self, id: &str, check: &Check<'_>) -> Result<String, LyricsError> {
|
|
let track = self
|
|
.check_track(id, "", check)
|
|
.map_err(|error| match error {
|
|
ResolverError::Cancelled(message) => LyricsError::Cancelled(message),
|
|
ResolverError::Closed => LyricsError::Cancelled(error.to_string()),
|
|
ResolverError::Transport(message) => LyricsError::Unavailable(message),
|
|
_ => LyricsError::Other(error.to_string()),
|
|
})?;
|
|
if track.deezer && !track.deezer_id.is_empty() {
|
|
Ok(track.deezer_id)
|
|
} else {
|
|
Err(LyricsError::Other("track not found on Deezer".into()))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Inner {
|
|
fn check(&self, check: &Check<'_>) -> Result<(), ResolverError> {
|
|
if self.closed.load(Ordering::Acquire) {
|
|
Err(ResolverError::Closed)
|
|
} else {
|
|
check().map_err(ResolverError::Cancelled)
|
|
}
|
|
}
|
|
|
|
fn fetch(&self, key: &Key, check: &Check<'_>) -> Outcome {
|
|
let id = if key.isrc {
|
|
let started = Instant::now();
|
|
let track = self.metadata.search_by_isrc(&key.id, &|| {
|
|
check()?;
|
|
if started.elapsed() >= Duration::from_secs(30) {
|
|
Err("ISRC lookup deadline exceeded".into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})?;
|
|
let id = deezer_id_from_metadata(&track);
|
|
if id.is_empty() {
|
|
return Err(ResolverError::Failed(format!(
|
|
"failed to resolve Deezer track ID from ISRC {}",
|
|
key.id
|
|
)));
|
|
}
|
|
id
|
|
} else {
|
|
key.id.clone()
|
|
};
|
|
if key.kind == Kind::Availability && key.isrc {
|
|
return from_deezer(&self.resolver, &id, check)
|
|
.map(|value| Value::Availability(Box::new(value)));
|
|
}
|
|
let url = if key.isrc {
|
|
format!("https://www.deezer.com/track/{id}")
|
|
} else {
|
|
format!("https://open.spotify.com/track/{id}")
|
|
};
|
|
let links = resolved_links(&self.resolver, &url, check)?;
|
|
if key.kind == Kind::Availability {
|
|
return Ok(Value::Availability(Box::new(from_links(&id, &links))));
|
|
}
|
|
let links: BTreeMap<_, _> = links
|
|
.into_iter()
|
|
.filter_map(|(key, value)| {
|
|
let value = value.trim();
|
|
(value.starts_with("https://") || value.starts_with("http://"))
|
|
.then(|| (key, value.into()))
|
|
})
|
|
.collect();
|
|
if links.is_empty() {
|
|
Err(ResolverError::Failed("no platform links found".into()))
|
|
} else {
|
|
Ok(Value::Links(links))
|
|
}
|
|
}
|
|
|
|
fn finish(&self, generation: u64, key: &Key, flight: &Arc<Flight>, result: Outcome) {
|
|
let mut state = self.state.lock().unwrap();
|
|
if generation == state.generation
|
|
&& !self.closed.load(Ordering::Acquire)
|
|
&& !matches!(
|
|
result,
|
|
Err(ResolverError::Cancelled(_) | ResolverError::Closed | ResolverError::Busy)
|
|
)
|
|
{
|
|
let limit = if key.kind == Kind::Availability {
|
|
500
|
|
} else {
|
|
200
|
|
};
|
|
if !state.cache.contains_key(key)
|
|
&& state
|
|
.cache
|
|
.keys()
|
|
.filter(|entry| entry.kind == key.kind)
|
|
.count()
|
|
>= limit
|
|
&& let Some(oldest) = state
|
|
.cache
|
|
.iter()
|
|
.filter(|(entry, _)| entry.kind == key.kind)
|
|
.min_by_key(|(_, value)| value.expires)
|
|
.map(|(key, _)| key.clone())
|
|
{
|
|
state.cache.remove(&oldest);
|
|
}
|
|
let ttl = Duration::from_secs(if result.is_ok() { 30 * 60 } else { 5 * 60 });
|
|
state.cache.insert(
|
|
key.clone(),
|
|
Entry {
|
|
result: result.clone(),
|
|
expires: Instant::now() + ttl,
|
|
},
|
|
);
|
|
}
|
|
*flight.result.lock().unwrap() = Some(result);
|
|
flight.ready.notify_all();
|
|
let flight_key = (generation, key.clone());
|
|
if state
|
|
.flights
|
|
.get(&flight_key)
|
|
.is_some_and(|value| Arc::ptr_eq(value, flight))
|
|
{
|
|
state.flights.remove(&flight_key);
|
|
}
|
|
state.active -= 1;
|
|
self.idle.notify_all();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::resolver::{Metadata, Resolution};
|
|
use spotiflac_core::metadata::TrackMetadata;
|
|
|
|
#[derive(Default)]
|
|
struct Probe {
|
|
calls: AtomicUsize,
|
|
metadata_calls: AtomicUsize,
|
|
release: AtomicBool,
|
|
metadata_blocked: AtomicBool,
|
|
failed: AtomicBool,
|
|
}
|
|
|
|
impl Resolver for Probe {
|
|
fn resolve(
|
|
&self,
|
|
_: &str,
|
|
_: &Metadata,
|
|
check: &Check<'_>,
|
|
) -> Result<Resolution, ResolverError> {
|
|
self.calls.fetch_add(1, Ordering::AcqRel);
|
|
while !self.release.load(Ordering::Acquire) {
|
|
check().map_err(ResolverError::Cancelled)?;
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
}
|
|
if self.failed.load(Ordering::Acquire) {
|
|
return Err(ResolverError::Failed("fixture failure".into()));
|
|
}
|
|
Ok(Resolution {
|
|
links: BTreeMap::from([(
|
|
"deezer".into(),
|
|
"https://www.deezer.com/track/101".into(),
|
|
)]),
|
|
..Resolution::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl MetadataLookup for Probe {
|
|
fn search_by_isrc(
|
|
&self,
|
|
_: &str,
|
|
check: &Check<'_>,
|
|
) -> Result<TrackMetadata, ResolverError> {
|
|
self.metadata_calls.fetch_add(1, Ordering::AcqRel);
|
|
while self.metadata_blocked.load(Ordering::Acquire) {
|
|
check().map_err(ResolverError::Cancelled)?;
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
}
|
|
Ok(TrackMetadata {
|
|
spotify_id: "deezer:42".into(),
|
|
..TrackMetadata::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
fn until(predicate: impl Fn() -> bool) {
|
|
let started = Instant::now();
|
|
while !predicate() {
|
|
assert!(
|
|
started.elapsed() < Duration::from_secs(3),
|
|
"availability test timed out"
|
|
);
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn shared_requests_cancel_independently_and_return_owned_cached_responses() {
|
|
let probe = Arc::new(Probe::default());
|
|
let service = Arc::new(AvailabilityService::new(probe.clone(), probe.clone()));
|
|
let cancelled = Arc::new(AtomicBool::new(false));
|
|
let first = {
|
|
let service = service.clone();
|
|
let cancelled = cancelled.clone();
|
|
std::thread::spawn(move || {
|
|
service.check_track("example", "", &|| {
|
|
if cancelled.load(Ordering::Acquire) {
|
|
Err("first cancelled".into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
})
|
|
})
|
|
};
|
|
until(|| probe.calls.load(Ordering::Acquire) == 1);
|
|
let second = {
|
|
let service = service.clone();
|
|
std::thread::spawn(move || service.check_track("example", "", &|| Ok(())))
|
|
};
|
|
until(|| {
|
|
service
|
|
.inner
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.flights
|
|
.values()
|
|
.next()
|
|
.unwrap()
|
|
.waiters
|
|
.load(Ordering::Acquire)
|
|
== 2
|
|
});
|
|
cancelled.store(true, Ordering::Release);
|
|
assert_eq!(
|
|
first.join().unwrap(),
|
|
Err(ResolverError::Cancelled("first cancelled".into()))
|
|
);
|
|
probe.release.store(true, Ordering::Release);
|
|
let mut track = second.join().unwrap().unwrap();
|
|
track.deezer_id = "caller changed".into();
|
|
assert_eq!(
|
|
service
|
|
.check_track("example", "", &|| Ok(()))
|
|
.unwrap()
|
|
.deezer_id,
|
|
"101"
|
|
);
|
|
assert_eq!(probe.calls.load(Ordering::Acquire), 1);
|
|
let mut links = service
|
|
.track_platform_links("example", "", &|| Ok(()))
|
|
.unwrap();
|
|
links.insert("deezer".into(), "changed".into());
|
|
assert_ne!(
|
|
service
|
|
.track_platform_links("example", "", &|| Ok(()))
|
|
.unwrap(),
|
|
links
|
|
);
|
|
assert_eq!(probe.calls.load(Ordering::Acquire), 2);
|
|
assert_eq!(
|
|
service
|
|
.check_track("", "usaa00000001", &|| Ok(()))
|
|
.unwrap()
|
|
.deezer_id,
|
|
"42"
|
|
);
|
|
service
|
|
.track_platform_links("", "USAA00000001", &|| Ok(()))
|
|
.unwrap();
|
|
service
|
|
.check_track("", " USAA00000001 ", &|| Ok(()))
|
|
.unwrap();
|
|
assert_eq!(probe.metadata_calls.load(Ordering::Acquire), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn shutdown_cancels_isrc_and_uncached_calls_and_rejects_retained_apis() {
|
|
let probe = Arc::new(Probe::default());
|
|
probe.metadata_blocked.store(true, Ordering::Release);
|
|
let service = Arc::new(AvailabilityService::new(probe.clone(), probe.clone()));
|
|
let pending = {
|
|
let service = service.clone();
|
|
std::thread::spawn(move || service.check_track("", "USAA00000001", &|| Ok(())))
|
|
};
|
|
until(|| probe.metadata_calls.load(Ordering::Acquire) == 1);
|
|
let raw = {
|
|
let service = service.clone();
|
|
std::thread::spawn(move || service.check_album("example", &|| Ok(())))
|
|
};
|
|
until(|| probe.calls.load(Ordering::Acquire) == 1);
|
|
service.shutdown();
|
|
assert_eq!(pending.join().unwrap(), Err(ResolverError::Closed));
|
|
assert_eq!(raw.join().unwrap(), Err(ResolverError::Closed));
|
|
assert_eq!(
|
|
service.check_track("", "", &|| Ok(())),
|
|
Err(ResolverError::Closed)
|
|
);
|
|
assert_eq!(
|
|
service.track_platform_links("", "", &|| Ok(())),
|
|
Err(ResolverError::Closed)
|
|
);
|
|
assert_eq!(
|
|
service.check_from_deezer("", &|| Ok(())),
|
|
Err(ResolverError::Closed)
|
|
);
|
|
assert_eq!(service.set_region("ID"), Err(ResolverError::Closed));
|
|
assert_eq!(service.region(), Err(ResolverError::Closed));
|
|
assert_eq!(service.clear_cache(), Err(ResolverError::Closed));
|
|
assert_eq!(service.inner.state.lock().unwrap().active, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cache_clear_and_expiry_keep_stale_and_cancelled_results_out() {
|
|
let probe = Arc::new(Probe::default());
|
|
let service = Arc::new(AvailabilityService::new(probe.clone(), probe.clone()));
|
|
let pending = {
|
|
let service = service.clone();
|
|
std::thread::spawn(move || service.check_track("old", "", &|| Ok(())))
|
|
};
|
|
until(|| probe.calls.load(Ordering::Acquire) == 1);
|
|
service.clear_cache().unwrap();
|
|
probe.release.store(true, Ordering::Release);
|
|
pending.join().unwrap().unwrap();
|
|
assert!(service.inner.state.lock().unwrap().cache.is_empty());
|
|
service.check_track("old", "", &|| Ok(())).unwrap();
|
|
probe.failed.store(true, Ordering::Release);
|
|
assert!(
|
|
service
|
|
.track_platform_links("failed", "", &|| Ok(()))
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
service
|
|
.track_platform_links("failed", "", &|| Ok(()))
|
|
.is_err()
|
|
);
|
|
assert_eq!(probe.calls.load(Ordering::Acquire), 3);
|
|
{
|
|
let mut state = service.inner.state.lock().unwrap();
|
|
let positive = state
|
|
.cache
|
|
.values()
|
|
.find(|entry| entry.result.is_ok())
|
|
.unwrap();
|
|
let negative = state
|
|
.cache
|
|
.values()
|
|
.find(|entry| entry.result.is_err())
|
|
.unwrap();
|
|
assert!(
|
|
positive.expires.duration_since(negative.expires) > Duration::from_secs(24 * 60)
|
|
);
|
|
for entry in state.cache.values_mut() {
|
|
entry.expires = Instant::now() - Duration::from_secs(1);
|
|
}
|
|
}
|
|
probe.failed.store(false, Ordering::Release);
|
|
service
|
|
.track_platform_links("failed", "", &|| Ok(()))
|
|
.unwrap();
|
|
assert_eq!(probe.calls.load(Ordering::Acquire), 4);
|
|
probe.release.store(false, Ordering::Release);
|
|
let started = Instant::now();
|
|
assert!(matches!(
|
|
service.check_track(
|
|
"cancel",
|
|
"",
|
|
&|| if started.elapsed() >= Duration::from_millis(40) {
|
|
Err("cancel".into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
),
|
|
Err(ResolverError::Cancelled(_))
|
|
));
|
|
until(|| service.inner.state.lock().unwrap().active == 0);
|
|
assert!(
|
|
!service
|
|
.inner
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.cache
|
|
.keys()
|
|
.any(|key| key.id == "cancel")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn independent_cache_limits_region_keys_and_lookup_limit_are_enforced() {
|
|
let probe = Arc::new(Probe::default());
|
|
let service = Arc::new(AvailabilityService::new(probe.clone(), probe.clone()));
|
|
let mut waiters = Vec::new();
|
|
for index in 0..64 {
|
|
let service = service.clone();
|
|
waiters.push(std::thread::spawn(move || {
|
|
service.check_track(&index.to_string(), "", &|| Ok(()))
|
|
}));
|
|
}
|
|
until(|| service.inner.state.lock().unwrap().active == 64);
|
|
assert_eq!(
|
|
service.check_track("over-limit", "", &|| Ok(())),
|
|
Err(ResolverError::Busy)
|
|
);
|
|
probe.release.store(true, Ordering::Release);
|
|
for waiter in waiters {
|
|
waiter.join().unwrap().unwrap();
|
|
}
|
|
service.clear_cache().unwrap();
|
|
for index in 0..503 {
|
|
service
|
|
.check_track(&index.to_string(), "", &|| Ok(()))
|
|
.unwrap();
|
|
}
|
|
for index in 0..203 {
|
|
service
|
|
.track_platform_links(&index.to_string(), "", &|| Ok(()))
|
|
.unwrap();
|
|
}
|
|
{
|
|
let state = service.inner.state.lock().unwrap();
|
|
assert_eq!(
|
|
state
|
|
.cache
|
|
.keys()
|
|
.filter(|key| key.kind == Kind::Availability)
|
|
.count(),
|
|
500
|
|
);
|
|
assert_eq!(
|
|
state
|
|
.cache
|
|
.keys()
|
|
.filter(|key| key.kind == Kind::Links)
|
|
.count(),
|
|
200
|
|
);
|
|
assert!(!state.cache.keys().any(|key| key.id == "0"));
|
|
}
|
|
service.set_region("id").unwrap();
|
|
service.check_track("502", "", &|| Ok(())).unwrap();
|
|
service.set_region("bad region").unwrap();
|
|
assert_eq!(service.region().unwrap(), "US");
|
|
assert!(
|
|
service
|
|
.inner
|
|
.state
|
|
.lock()
|
|
.unwrap()
|
|
.cache
|
|
.keys()
|
|
.any(|key| key.region == "ID")
|
|
);
|
|
}
|
|
}
|