Skip to content

Commit 403f2bf

Browse files
authored
ref(project-cache): Schedule updates instead of spawning tasks (#4233)
We can do better than spawning a large amount of tasks which just wait by using a heap to keep track of when the next task is scheduled and `FuturesUnordered` to wait for the results from the tasks. Each task is dispatched to a `ProjectSource` which is very heavily IO bound which and we don't need parallelism for.
1 parent 726be3f commit 403f2bf

File tree

8 files changed

+433
-30
lines changed

8 files changed

+433
-30
lines changed

relay-server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ chrono = { workspace = true, features = ["clock"] }
4444
data-encoding = { workspace = true }
4545
flate2 = { workspace = true }
4646
fnv = { workspace = true }
47-
futures = { workspace = true }
47+
futures = { workspace = true, features = ["async-await"] }
4848
hashbrown = { workspace = true }
4949
hyper-util = { workspace = true }
5050
itertools = { workspace = true }

relay-server/src/services/projects/cache/service.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
use std::sync::Arc;
22

3+
use futures::future::BoxFuture;
4+
use futures::StreamExt as _;
35
use relay_base_schema::project::ProjectKey;
46
use relay_config::Config;
57
use relay_statsd::metric;
68
use relay_system::Service;
7-
use tokio::sync::{broadcast, mpsc};
9+
use tokio::sync::broadcast;
810

911
use crate::services::projects::cache::handle::ProjectCacheHandle;
1012
use crate::services::projects::cache::state::{CompletedFetch, Fetch, ProjectStore};
1113
use crate::services::projects::project::ProjectState;
1214
use crate::services::projects::source::ProjectSource;
1315
use crate::statsd::{RelayGauges, RelayTimers};
16+
use crate::utils::FuturesScheduled;
1417

1518
/// Size of the broadcast channel for project events.
1619
///
@@ -66,24 +69,21 @@ pub struct ProjectCacheService {
6669
source: ProjectSource,
6770
config: Arc<Config>,
6871

69-
project_update_rx: mpsc::UnboundedReceiver<CompletedFetch>,
70-
project_update_tx: mpsc::UnboundedSender<CompletedFetch>,
72+
scheduled_fetches: FuturesScheduled<BoxFuture<'static, CompletedFetch>>,
7173

7274
project_events_tx: broadcast::Sender<ProjectChange>,
7375
}
7476

7577
impl ProjectCacheService {
7678
/// Creates a new [`ProjectCacheService`].
7779
pub fn new(config: Arc<Config>, source: ProjectSource) -> Self {
78-
let (project_update_tx, project_update_rx) = mpsc::unbounded_channel();
7980
let project_events_tx = broadcast::channel(PROJECT_EVENTS_CHANNEL_SIZE).0;
8081

8182
Self {
8283
store: ProjectStore::default(),
8384
source,
8485
config,
85-
project_update_rx,
86-
project_update_tx,
86+
scheduled_fetches: FuturesScheduled::default(),
8787
project_events_tx,
8888
}
8989
}
@@ -106,14 +106,12 @@ impl ProjectCacheService {
106106
handle
107107
}
108108

109-
/// Schedules a new [`Fetch`] and delivers the result to the [`Self::project_update_tx`] channel.
110-
fn schedule_fetch(&self, fetch: Fetch) {
109+
/// Schedules a new [`Fetch`] in [`Self::scheduled_fetches`].
110+
fn schedule_fetch(&mut self, fetch: Fetch) {
111111
let source = self.source.clone();
112-
let project_updates = self.project_update_tx.clone();
113-
114-
tokio::spawn(async move {
115-
tokio::time::sleep_until(fetch.when()).await;
116112

113+
let when = fetch.when();
114+
let task = async move {
117115
let state = match source
118116
.fetch(fetch.project_key(), false, fetch.revision())
119117
.await
@@ -132,8 +130,13 @@ impl ProjectCacheService {
132130
}
133131
};
134132

135-
let _ = project_updates.send(fetch.complete(state));
136-
});
133+
fetch.complete(state)
134+
};
135+
self.scheduled_fetches.schedule(when, Box::pin(task));
136+
137+
metric!(
138+
gauge(RelayGauges::ProjectCacheScheduledFetches) = self.scheduled_fetches.len() as u64
139+
);
137140
}
138141
}
139142

@@ -207,9 +210,9 @@ impl relay_system::Service for ProjectCacheService {
207210
tokio::select! {
208211
biased;
209212

210-
Some(update) = self.project_update_rx.recv() => timed!(
211-
"project_update",
212-
self.handle_completed_fetch(update)
213+
Some(fetch) = self.scheduled_fetches.next() => timed!(
214+
"completed_fetch",
215+
self.handle_completed_fetch(fetch)
213216
),
214217
Some(message) = rx.recv() => timed!(
215218
message.variant(),

relay-server/src/services/projects/cache/state.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ impl ProjectRef<'_> {
278278
#[derive(Debug)]
279279
pub struct Fetch {
280280
project_key: ProjectKey,
281-
when: Instant,
281+
when: Option<Instant>,
282282
revision: Revision,
283283
}
284284

@@ -290,9 +290,9 @@ impl Fetch {
290290

291291
/// Returns when the fetch for the project should be scheduled.
292292
///
293-
/// This can be now (as soon as possible) or a later point in time, if the project is currently
294-
/// in a backoff.
295-
pub fn when(&self) -> Instant {
293+
/// This can be now (as soon as possible, indicated by `None`) or a later point in time,
294+
/// if the project is currently in a backoff.
295+
pub fn when(&self) -> Option<Instant> {
296296
self.when
297297
}
298298

@@ -462,7 +462,7 @@ impl PrivateProjectState {
462462
}
463463
FetchState::Pending { next_fetch_attempt } => {
464464
// Schedule a new fetch, even if there is a backoff, it will just be sleeping for a while.
465-
next_fetch_attempt.unwrap_or(now)
465+
*next_fetch_attempt
466466
}
467467
FetchState::Complete { last_fetch } => {
468468
if last_fetch.check_expiry(now, config).is_fresh() {
@@ -473,7 +473,7 @@ impl PrivateProjectState {
473473
);
474474
return None;
475475
}
476-
now
476+
None
477477
}
478478
};
479479

@@ -484,7 +484,7 @@ impl PrivateProjectState {
484484
tags.project_key = &self.project_key.as_str(),
485485
attempts = self.backoff.attempt() + 1,
486486
"project state fetch scheduled in {:?}",
487-
when.saturating_duration_since(Instant::now()),
487+
when.unwrap_or(now).saturating_duration_since(now),
488488
);
489489

490490
Some(Fetch {
@@ -501,9 +501,12 @@ impl PrivateProjectState {
501501
);
502502

503503
if fetch.is_pending() {
504-
self.state = FetchState::Pending {
505-
next_fetch_attempt: now.checked_add(self.backoff.next_backoff()),
504+
let next_backoff = self.backoff.next_backoff();
505+
let next_fetch_attempt = match next_backoff.is_zero() {
506+
false => now.checked_add(next_backoff),
507+
true => None,
506508
};
509+
self.state = FetchState::Pending { next_fetch_attempt };
507510
relay_log::trace!(
508511
tags.project_key = &self.project_key.as_str(),
509512
"project state fetch completed but still pending"
@@ -596,7 +599,7 @@ mod tests {
596599

597600
let fetch = store.try_begin_fetch(project_key, &config).unwrap();
598601
assert_eq!(fetch.project_key(), project_key);
599-
assert!(fetch.when() < Instant::now());
602+
assert_eq!(fetch.when(), None);
600603
assert_eq!(fetch.revision().as_str(), None);
601604
assert_state!(store, project_key, ProjectState::Pending);
602605

@@ -608,7 +611,7 @@ mod tests {
608611
let fetch = store.complete_fetch(fetch, &config).unwrap();
609612
assert_eq!(fetch.project_key(), project_key);
610613
// First backoff is still immediately.
611-
assert!(fetch.when() < Instant::now());
614+
assert_eq!(fetch.when(), None);
612615
assert_eq!(fetch.revision().as_str(), None);
613616
assert_state!(store, project_key, ProjectState::Pending);
614617

@@ -617,7 +620,7 @@ mod tests {
617620
let fetch = store.complete_fetch(fetch, &config).unwrap();
618621
assert_eq!(fetch.project_key(), project_key);
619622
// This time it needs to be in the future (backoff).
620-
assert!(fetch.when() > Instant::now());
623+
assert!(fetch.when() > Some(Instant::now()));
621624
assert_eq!(fetch.revision().as_str(), None);
622625
assert_state!(store, project_key, ProjectState::Pending);
623626

relay-server/src/statsd.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub enum RelayGauges {
4646
RedisPoolIdleConnections,
4747
/// The number of notifications in the broadcast channel of the project cache.
4848
ProjectCacheNotificationChannel,
49+
/// The number of scheduled and in progress fetches in the project cache.
50+
ProjectCacheScheduledFetches,
4951
/// Exposes the amount of currently open and handled connections by the server.
5052
ServerActiveConnections,
5153
}
@@ -68,6 +70,7 @@ impl GaugeMetric for RelayGauges {
6870
RelayGauges::ProjectCacheNotificationChannel => {
6971
"project_cache.notification_channel.size"
7072
}
73+
RelayGauges::ProjectCacheScheduledFetches => "project_cache.fetches.size",
7174
RelayGauges::ServerActiveConnections => "server.http.connections",
7275
}
7376
}

relay-server/src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod param_parser;
66
mod pick;
77
mod rate_limits;
88
mod retry;
9+
mod scheduled;
910
mod sizes;
1011
mod sleep_handle;
1112
mod split_off;
@@ -30,6 +31,7 @@ pub use self::param_parser::*;
3031
pub use self::pick::*;
3132
pub use self::rate_limits::*;
3233
pub use self::retry::*;
34+
pub use self::scheduled::*;
3335
pub use self::serde::*;
3436
pub use self::sizes::*;
3537
pub use self::sleep_handle::*;

0 commit comments

Comments
 (0)