Skip to content

Commit 5089e45

Browse files
committed
[ENH] Add a safety cutoff to the rust log service.
If a collection on the rust-based log has more than a configuration-configurable number of records, shut it down hard no matter what. This happens when compaction gets backlogged. By default, no more than 1_000_000 can be on the log uncompacted. This is just a placeholder that we can change.
1 parent b17dfbd commit 5089e45

File tree

1 file changed

+119
-2
lines changed

1 file changed

+119
-2
lines changed

rust/log-service/src/lib.rs

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
use std::cmp::Ordering;
44
use std::collections::HashMap;
5+
use std::collections::HashSet;
56
use std::future::Future;
6-
use std::sync::Arc;
7+
use std::sync::{Arc, Mutex};
78
use std::time::{Duration, Instant, SystemTime};
89

910
use bytes::Bytes;
@@ -264,6 +265,7 @@ pub struct Rollup {
264265
pub advance_to: LogPosition,
265266
pub reinsert: Vec<DirtyMarker>,
266267
pub compactable: Vec<CollectionInfo>,
268+
pub backpressure: Vec<CollectionUuid>,
267269
}
268270

269271
//////////////////////////////////////// RollupPerCollection ///////////////////////////////////////
@@ -361,6 +363,7 @@ impl DirtyMarker {
361363
retrieve_cursor: impl Fn(Arc<Storage>, CollectionUuid) -> F2,
362364
markers: &[(LogPosition, DirtyMarker)],
363365
record_count_threshold: u64,
366+
record_count_backpressure: u64,
364367
reinsert_threshold: u64,
365368
timeout_us: u64,
366369
metrics: &Metrics,
@@ -424,7 +427,6 @@ impl DirtyMarker {
424427
})
425428
.map(
426429
|(collection_id, storage, retrieve_manifest, retrieve_cursor)| async move {
427-
// We play a funny game of Ok(Ok(_)) to force try_join_all to not short-circuit.
428430
let cursor = (*retrieve_cursor)(Arc::clone(&storage), collection_id);
429431
let manifest = (*retrieve_manifest)(Arc::clone(&storage), collection_id);
430432
let (cursor, manifest) = futures::future::join(cursor, manifest).await;
@@ -454,6 +456,7 @@ impl DirtyMarker {
454456
.flat_map(Result::ok)
455457
.collect::<HashMap<_, _>>();
456458
let mut uncompacted = 0u64;
459+
let mut backpressure = vec![];
457460
let compactable = compactable
458461
.into_iter()
459462
.filter_map(|collection_id| {
@@ -494,6 +497,9 @@ impl DirtyMarker {
494497
manifest.maximum_log_position(),
495498
);
496499
uncompacted += maximum_log_position - cursor.position;
500+
if maximum_log_position - cursor.position >= record_count_backpressure {
501+
backpressure.push(*collection_id);
502+
}
497503
if maximum_log_position - cursor.position >= record_count_threshold {
498504
Some(CollectionInfo {
499505
collection_id: collection_id.to_string(),
@@ -542,6 +548,7 @@ impl DirtyMarker {
542548
advance_to,
543549
reinsert,
544550
compactable,
551+
backpressure,
545552
}))
546553
}
547554

@@ -631,10 +638,32 @@ pub struct LogServer {
631638
open_logs: Arc<StateHashTable<LogKey, LogStub>>,
632639
dirty_log: Arc<LogWriter>,
633640
compacting: tokio::sync::Mutex<()>,
641+
backpressure: Mutex<Arc<HashSet<CollectionUuid>>>,
634642
cache: Option<Box<dyn chroma_cache::PersistentCache<String, CachedParquetFragment>>>,
635643
metrics: Metrics,
636644
}
637645

646+
impl LogServer {
647+
fn set_backpressure(&self, to_pressure: &[CollectionUuid]) {
648+
let mut new_backpressure = Arc::new(HashSet::from_iter(to_pressure.iter().cloned()));
649+
// SAFETY(rescrv): Mutex poisoning.
650+
let mut backpressure = self.backpressure.lock().unwrap();
651+
std::mem::swap(&mut *backpressure, &mut new_backpressure);
652+
}
653+
654+
fn check_for_backpressure(&self, collection_id: CollectionUuid) -> Result<(), Status> {
655+
let backpressure = {
656+
// SAFETY(rescrv): Mutex poisoning.
657+
let backpressure = self.backpressure.lock().unwrap();
658+
Arc::clone(&backpressure)
659+
};
660+
if backpressure.contains(&collection_id) {
661+
return Err(Status::resource_exhausted("log needs compaction; too full"));
662+
}
663+
Ok(())
664+
}
665+
}
666+
638667
#[async_trait::async_trait]
639668
impl LogService for LogServer {
640669
async fn push_logs(
@@ -652,6 +681,7 @@ impl LogService for LogServer {
652681
if push_logs.records.is_empty() {
653682
return Err(Status::invalid_argument("Too few records"));
654683
}
684+
self.check_for_backpressure(collection_id)?;
655685
let span = tracing::info_span!("push_logs");
656686

657687
async move {
@@ -832,6 +862,7 @@ impl LogService for LogServer {
832862
let source_collection_id = Uuid::parse_str(&request.source_collection_id)
833863
.map(CollectionUuid)
834864
.map_err(|_| Status::invalid_argument("Failed to parse collection id"))?;
865+
self.check_for_backpressure(source_collection_id)?;
835866
let target_collection_id = Uuid::parse_str(&request.target_collection_id)
836867
.map(CollectionUuid)
837868
.map_err(|_| Status::invalid_argument("Failed to parse collection id"))?;
@@ -1000,6 +1031,7 @@ impl LogService for LogServer {
10001031
self.config.record_count_threshold,
10011032
request.min_compaction_size,
10021033
),
1034+
self.config.num_records_before_backpressure,
10031035
self.config.reinsert_threshold,
10041036
self.config.timeout_us,
10051037
&self.metrics,
@@ -1020,6 +1052,7 @@ impl LogService for LogServer {
10201052
.map_err(|err| Status::unavailable(err.to_string()))
10211053
})
10221054
.collect::<Result<Vec<_>, _>>()?;
1055+
self.set_backpressure(&rollup.backpressure);
10231056
if rollup.advance_to < cursor.position {
10241057
tracing::error!(
10251058
"advance_to went back in time: {:?} -> {:?}",
@@ -1409,6 +1442,8 @@ pub struct LogServerConfig {
14091442
pub cache: Option<CacheConfig>,
14101443
#[serde(default = "LogServerConfig::default_record_count_threshold")]
14111444
pub record_count_threshold: u64,
1445+
#[serde(default = "LogServerConfig::default_num_records_before_backpressure")]
1446+
pub num_records_before_backpressure: u64,
14121447
#[serde(default = "LogServerConfig::default_reinsert_threshold")]
14131448
pub reinsert_threshold: u64,
14141449
#[serde(default = "LogServerConfig::default_timeout_us")]
@@ -1421,6 +1456,11 @@ impl LogServerConfig {
14211456
100
14221457
}
14231458

1459+
/// one million records on the log.
1460+
fn default_num_records_before_backpressure() -> u64 {
1461+
1_000_000
1462+
}
1463+
14241464
/// force compaction if a candidate comes up ten times.
14251465
fn default_reinsert_threshold() -> u64 {
14261466
10
@@ -1442,6 +1482,7 @@ impl Default for LogServerConfig {
14421482
reader: LogReaderOptions::default(),
14431483
cache: None,
14441484
record_count_threshold: Self::default_record_count_threshold(),
1485+
num_records_before_backpressure: Self::default_num_records_before_backpressure(),
14451486
reinsert_threshold: Self::default_reinsert_threshold(),
14461487
timeout_us: Self::default_timeout_us(),
14471488
}
@@ -1483,12 +1524,14 @@ impl Configurable<LogServerConfig> for LogServer {
14831524
let dirty_log = Arc::new(dirty_log);
14841525
let compacting = tokio::sync::Mutex::new(());
14851526
let metrics = Metrics::new(opentelemetry::global::meter("chroma"));
1527+
let backpressure = Mutex::new(Arc::new(HashSet::default()));
14861528
Ok(Self {
14871529
config: config.clone(),
14881530
open_logs: Arc::new(StateHashTable::default()),
14891531
storage,
14901532
dirty_log,
14911533
compacting,
1534+
backpressure,
14921535
cache,
14931536
metrics,
14941537
})
@@ -1592,6 +1635,7 @@ mod tests {
15921635
&markers,
15931636
1,
15941637
1,
1638+
1,
15951639
86_400_000_000,
15961640
&Metrics::new(opentelemetry::global::meter("chroma")),
15971641
)
@@ -1654,6 +1698,7 @@ mod tests {
16541698
&markers,
16551699
3,
16561700
1,
1701+
1,
16571702
86_400_000_000,
16581703
&Metrics::new(opentelemetry::global::meter("chroma")),
16591704
)
@@ -1738,6 +1783,7 @@ mod tests {
17381783
&markers,
17391784
3,
17401785
1,
1786+
1,
17411787
86_400_000_000,
17421788
&Metrics::new(opentelemetry::global::meter("chroma")),
17431789
)
@@ -1852,6 +1898,7 @@ mod tests {
18521898
&markers,
18531899
3,
18541900
1,
1901+
1,
18551902
86_400_000_000,
18561903
&Metrics::new(opentelemetry::global::meter("chroma")),
18571904
)
@@ -1864,6 +1911,76 @@ mod tests {
18641911
assert!(rollup.reinsert[0].collection_id() == collection_id_blocking);
18651912
}
18661913

1914+
#[tokio::test]
1915+
async fn dirty_marker_backpressure() {
1916+
// Test that the dirty marker gives proper backpressure.
1917+
let storage = chroma_storage::test_storage();
1918+
let now = SystemTime::now()
1919+
.duration_since(SystemTime::UNIX_EPOCH)
1920+
.map_err(|_| wal3::Error::Internal)
1921+
.unwrap()
1922+
.as_micros() as u64;
1923+
let collection_id = CollectionUuid::new();
1924+
let markers = vec![(
1925+
LogPosition::from_offset(1),
1926+
DirtyMarker::MarkDirty {
1927+
collection_id,
1928+
log_position: LogPosition::from_offset(1),
1929+
num_records: 1_000_000,
1930+
reinsert_count: 0,
1931+
initial_insertion_epoch_us: now,
1932+
},
1933+
)];
1934+
let rollup = DirtyMarker::rollup(
1935+
Arc::new(storage),
1936+
|_, collection_id| async move {
1937+
if collection_id == collection_id {
1938+
Ok(Some(Manifest {
1939+
writer: "TODO".to_string(),
1940+
acc_bytes: 0,
1941+
setsum: Setsum::default(),
1942+
snapshots: vec![],
1943+
fragments: vec![Fragment {
1944+
seq_no: FragmentSeqNo(1),
1945+
num_bytes: 0,
1946+
path: "TODO".to_string(),
1947+
setsum: Setsum::default(),
1948+
start: LogPosition::from_offset(1),
1949+
limit: LogPosition::from_offset(1_000_001),
1950+
}],
1951+
}))
1952+
} else {
1953+
unreachable!("we aren't testing this case");
1954+
}
1955+
},
1956+
|_, collection_id| async move {
1957+
if collection_id == collection_id {
1958+
Ok(Some(Witness::default_etag_with_cursor(Cursor {
1959+
position: LogPosition::from_offset(1),
1960+
epoch_us: 0,
1961+
writer: "TODO".to_string(),
1962+
})))
1963+
} else {
1964+
unreachable!("we aren't testing this case");
1965+
}
1966+
},
1967+
&markers,
1968+
1,
1969+
1,
1970+
1,
1971+
86_400_000_000,
1972+
&Metrics::new(opentelemetry::global::meter("chroma")),
1973+
)
1974+
.await
1975+
.unwrap()
1976+
.unwrap();
1977+
assert_eq!(LogPosition::from_offset(2), rollup.advance_to);
1978+
assert_eq!(1, rollup.compactable.len());
1979+
assert_eq!(1, rollup.reinsert.len());
1980+
assert_eq!(1, rollup.backpressure.len());
1981+
assert_eq!(collection_id, rollup.backpressure[0]);
1982+
}
1983+
18671984
#[test]
18681985
fn unsafe_constants() {
18691986
assert!(STABLE_PREFIX.is_valid());

0 commit comments

Comments
 (0)