Skip to content

Commit 1ae0397

Browse files
committed
add atomic flush protocol and tsdb
1 parent 51f9437 commit 1ae0397

6 files changed

Lines changed: 169 additions & 28 deletions

File tree

open-tsdb/src/db.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Tsdb is the coordination layer for OpenTSDB, ensuring the ingestion and query
2+
// layers are synchronized. It maintains a three-level hierarchy:
3+
//
4+
// 1. Mutable Head: this stores incoming data that has not yet been
5+
// flushed to storage. They are updated via deltas, which are applied to
6+
// the ehad chunk for the corresponding time bucket.
7+
//
8+
// 2. Frozen Head: on a trigger (currently time-based), the head
9+
// is frozen and its data is ready to be flushed to storage. Since the time
10+
// it takes to flush the data is variable, the frozen head chunks are maintained
11+
// in memory until the flush is complete. At this point the frozen chunk is
12+
// atomically discarded with the update of the storage snapshot.
13+
//
14+
// 3. Storage Snapshot: a snapshot of the storage layer that does not yet have the
15+
// any of the head chunks' data. This is used for consistency as it is possible
16+
// that data has already been flushed to storage before the frozen chunks are
17+
// discarded (which would result in duplicate reads).
18+
//
19+
// In addition to the three levels, the Tsdb maintains a cache of the entire series
20+
// dictionary for each active time bucket since this is required for ingestion.
21+
22+
use std::sync::{Arc, atomic::AtomicU32};
23+
24+
use dashmap::DashMap;
25+
use opendata_common::{Storage, storage::StorageSnapshot};
26+
use tokio::sync::{Mutex, RwLock};
27+
28+
use crate::delta::TsdbDelta;
29+
use crate::storage::OpenTsdbStorageReadExt;
30+
use crate::util::Result;
31+
use crate::{
32+
head::TsdbHead,
33+
model::{SeriesFingerprint, SeriesId, TimeBucket},
34+
};
35+
36+
pub(crate) struct TsdbState {
37+
head: TsdbHead,
38+
frozen_head: Option<TsdbHead>,
39+
storage_snapshot: Arc<dyn StorageSnapshot>,
40+
}
41+
42+
pub(crate) struct Tsdb {
43+
bucket: TimeBucket,
44+
series_dict: DashMap<SeriesFingerprint, SeriesId>,
45+
next_series_id: AtomicU32,
46+
/// The state of the Tsdb is protected by a read-write
47+
/// lock to allow us to atomically flush and update the
48+
/// storage snapshot. Note that only the read lock is
49+
/// required for queries and ingestion (which modifies the
50+
/// underlying data structures in a thread-safe way)
51+
state: Arc<RwLock<TsdbState>>,
52+
/// Mutex to ensure only one flush operation can run at a time.
53+
/// This prevents concurrent flushes from interfering with each other.
54+
flush_mutex: Arc<Mutex<()>>,
55+
}
56+
57+
impl Tsdb {
58+
pub(crate) async fn load(bucket: TimeBucket, storage: Arc<dyn Storage>) -> Result<Self> {
59+
let series_dict = DashMap::new();
60+
let next_series_id = storage
61+
.load_series_dictionary(&bucket, |fingerprint, series_id| {
62+
series_dict.insert(fingerprint, series_id);
63+
})
64+
.await?;
65+
66+
Ok(Self {
67+
bucket: bucket.clone(),
68+
series_dict,
69+
next_series_id: AtomicU32::new(next_series_id),
70+
state: Arc::new(RwLock::new(TsdbState {
71+
head: TsdbHead::new(bucket.clone()),
72+
frozen_head: None,
73+
storage_snapshot: storage.snapshot().await?,
74+
})),
75+
flush_mutex: Arc::new(Mutex::new(())),
76+
})
77+
}
78+
79+
pub(crate) async fn ingest(&self, delta: TsdbDelta) -> Result<()> {
80+
let state = self.state.read().await;
81+
state.head.merge(&delta)?;
82+
Ok(())
83+
}
84+
85+
pub(crate) async fn flush(&self, storage: Arc<dyn Storage>) -> Result<()> {
86+
let _flush_guard = self.flush_mutex.lock().await;
87+
88+
// blocking section: freeze the head and replaces it
89+
// with a new head block, keeping a reference to the old
90+
// frozen head
91+
{
92+
let mut state = self.state.write().await;
93+
state.head.freeze();
94+
let frozen_head =
95+
std::mem::replace(&mut state.head, TsdbHead::new(self.bucket.clone()));
96+
state.frozen_head = Some(frozen_head);
97+
}
98+
99+
// non-blocking section: flush the frozen head to storage
100+
// this can take time so its important that it only holds
101+
// the read lock
102+
let snapshot = {
103+
let state = self.state.read().await;
104+
let frozen_head = state
105+
.frozen_head
106+
.as_ref()
107+
.expect("frozen_head should be set after write lock above");
108+
// Clone storage to avoid moving it and for clarity
109+
frozen_head.flush(storage.clone()).await?
110+
};
111+
112+
// blocking section: update the storage snapshot and
113+
// discard the frozen head
114+
{
115+
let mut state = self.state.write().await;
116+
state.storage_snapshot = snapshot;
117+
state.frozen_head = None;
118+
}
119+
Ok(())
120+
}
121+
}

open-tsdb/src/delta.rs

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::HashMap;
22

3+
use dashmap::DashMap;
34
use opentelemetry_proto::tonic::metrics::v1::Metric;
45

56
use crate::{
@@ -15,7 +16,7 @@ pub(crate) struct TsdbDeltaBuilder<'a> {
1516
pub(crate) bucket: TimeBucket,
1617
pub(crate) forward_index: ForwardIndex,
1718
pub(crate) inverted_index: InvertedIndex,
18-
pub(crate) series_dict: &'a HashMap<SeriesFingerprint, SeriesId>,
19+
pub(crate) series_dict: &'a DashMap<SeriesFingerprint, SeriesId>,
1920
pub(crate) series_dict_delta: HashMap<SeriesFingerprint, SeriesId>,
2021
pub(crate) samples: HashMap<SeriesId, Vec<Sample>>,
2122
pub(crate) next_series_id: u32,
@@ -24,7 +25,8 @@ pub(crate) struct TsdbDeltaBuilder<'a> {
2425
impl<'a> TsdbDeltaBuilder<'a> {
2526
pub(crate) fn new(
2627
bucket: TimeBucket,
27-
series_dict: &'a HashMap<SeriesFingerprint, SeriesId>,
28+
series_dict: &'a DashMap<SeriesFingerprint, SeriesId>,
29+
next_series_id: u32,
2830
) -> Self {
2931
Self {
3032
bucket,
@@ -33,7 +35,7 @@ impl<'a> TsdbDeltaBuilder<'a> {
3335
series_dict,
3436
series_dict_delta: HashMap::new(),
3537
samples: HashMap::new(),
36-
next_series_id: 0,
38+
next_series_id,
3739
}
3840
}
3941

@@ -73,8 +75,8 @@ impl<'a> TsdbDeltaBuilder<'a> {
7375
let series_id = self
7476
.series_dict
7577
.get(&fingerprint)
76-
.or_else(|| self.series_dict_delta.get(&fingerprint))
77-
.copied()
78+
.map(|r| *r.value())
79+
.or_else(|| self.series_dict_delta.get(&fingerprint).copied())
7880
.unwrap_or_else(|| {
7981
let series_id = self.next_series_id;
8082
self.next_series_id = series_id + 1;
@@ -128,6 +130,7 @@ pub(crate) struct TsdbDelta {
128130
mod tests {
129131
use super::*;
130132
use crate::model::{Attribute, MetricType, Temporality};
133+
use dashmap::DashMap;
131134

132135
fn create_test_bucket() -> TimeBucket {
133136
TimeBucket::hour(1000)
@@ -157,8 +160,8 @@ mod tests {
157160
fn should_create_new_series_when_ingesting_first_sample() {
158161
// given
159162
let bucket = create_test_bucket();
160-
let series_dict = HashMap::new();
161-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
163+
let series_dict = DashMap::new();
164+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
162165
let attributes = create_test_attributes();
163166
let sample = create_test_sample();
164167
let metric_unit = Some("bytes".to_string());
@@ -205,8 +208,8 @@ mod tests {
205208
fn should_reuse_series_id_for_samples_with_same_attributes() {
206209
// given
207210
let bucket = create_test_bucket();
208-
let series_dict = HashMap::new();
209-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
211+
let series_dict = DashMap::new();
212+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
210213
let attributes = create_test_attributes();
211214
let sample1 = Sample {
212215
timestamp: 1000,
@@ -248,8 +251,8 @@ mod tests {
248251
fn should_create_different_series_id_for_different_attributes() {
249252
// given
250253
let bucket = create_test_bucket();
251-
let series_dict = HashMap::new();
252-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
254+
let series_dict = DashMap::new();
255+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
253256
let attributes1 = vec![Attribute {
254257
key: "service".to_string(),
255258
value: "api".to_string(),
@@ -286,13 +289,13 @@ mod tests {
286289
fn should_reuse_series_id_from_existing_series_dict() {
287290
// given
288291
let bucket = create_test_bucket();
289-
let mut series_dict = HashMap::new();
292+
let series_dict = DashMap::new();
290293
let mut attributes = create_test_attributes();
291294
// Sort attributes to match what ingest_sample does
292295
attributes.sort_by(|a, b| a.key.cmp(&b.key));
293296
let fingerprint = attributes.fingerprint();
294297
series_dict.insert(fingerprint, 42); // Existing series_id
295-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
298+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
296299
let metric_type = MetricType::Gauge;
297300

298301
// when
@@ -314,8 +317,8 @@ mod tests {
314317
fn should_reuse_series_id_from_delta_dict_when_ingesting_again() {
315318
// given
316319
let bucket = create_test_bucket();
317-
let series_dict = HashMap::new();
318-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
320+
let series_dict = DashMap::new();
321+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
319322
let attributes = create_test_attributes();
320323
let metric_type = MetricType::Gauge;
321324

@@ -345,8 +348,8 @@ mod tests {
345348
fn should_sort_attributes_before_fingerprinting() {
346349
// given
347350
let bucket = create_test_bucket();
348-
let series_dict = HashMap::new();
349-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
351+
let series_dict = DashMap::new();
352+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
350353
let attributes1 = vec![
351354
Attribute {
352355
key: "z_key".to_string(),
@@ -393,8 +396,8 @@ mod tests {
393396
fn should_store_metric_unit_and_type_in_forward_index() {
394397
// given
395398
let bucket = create_test_bucket();
396-
let series_dict = HashMap::new();
397-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
399+
let series_dict = DashMap::new();
400+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
398401
let attributes = create_test_attributes();
399402
let metric_unit = Some("requests_per_second".to_string());
400403
let metric_type = MetricType::Sum {
@@ -429,8 +432,8 @@ mod tests {
429432
fn should_index_all_attributes_in_inverted_index() {
430433
// given
431434
let bucket = create_test_bucket();
432-
let series_dict = HashMap::new();
433-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
435+
let series_dict = DashMap::new();
436+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
434437
let attributes = vec![
435438
Attribute {
436439
key: "service".to_string(),
@@ -468,8 +471,8 @@ mod tests {
468471
fn should_handle_empty_attributes_list() {
469472
// given
470473
let bucket = create_test_bucket();
471-
let series_dict = HashMap::new();
472-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
474+
let series_dict = DashMap::new();
475+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
473476
let attributes = Vec::<Attribute>::new();
474477
let metric_type = MetricType::Gauge;
475478

@@ -494,8 +497,8 @@ mod tests {
494497
fn should_handle_none_metric_unit() {
495498
// given
496499
let bucket = create_test_bucket();
497-
let series_dict = HashMap::new();
498-
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict);
500+
let series_dict = DashMap::new();
501+
let mut builder = TsdbDeltaBuilder::new(bucket, &series_dict, 0);
499502
let attributes = create_test_attributes();
500503
let metric_type = MetricType::Gauge;
501504

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ impl TsdbHead {
4242
}
4343
}
4444

45+
/// Returns a reference to the series dictionary (contains only NEW series)
46+
pub fn series_dict(&self) -> &DashMap<SeriesFingerprint, SeriesId> {
47+
&self.series_dict
48+
}
49+
50+
/// Returns a reference to the bucket
51+
pub fn bucket(&self) -> &TimeBucket {
52+
&self.bucket
53+
}
54+
4555
pub fn merge(&self, delta: &TsdbDelta) -> Result<()> {
4656
if self.frozen.load(Ordering::SeqCst) {
4757
return Err(OpenTsdbError::Internal("TsdbHead is frozen".to_string()));
@@ -68,7 +78,7 @@ impl TsdbHead {
6878
self.frozen.fetch_or(true, Ordering::SeqCst)
6979
}
7080

71-
pub async fn flush(&self, storage: &dyn Storage) -> Result<Arc<dyn StorageSnapshot>> {
81+
pub async fn flush(&self, storage: Arc<dyn Storage>) -> Result<Arc<dyn StorageSnapshot>> {
7282
if !self.frozen.load(Ordering::SeqCst) {
7383
return Err(OpenTsdbError::Internal(
7484
"Should only flush frozen TsdbHead".to_string(),

open-tsdb/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#![allow(dead_code)]
2+
mod db;
23
mod delta;
4+
mod head;
35
mod index;
46
mod model;
57
mod otel;
68
mod promql;
79
mod serde;
810
mod storage;
9-
mod tsdb;
1011
mod util;
1112

1213
fn main() {

open-tsdb/src/model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub(crate) enum MetricType {
5050
Summary,
5151
}
5252

53-
#[derive(Clone, Debug)]
53+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5454
pub(crate) struct TimeBucket {
5555
pub(crate) start: BucketStart,
5656
pub(crate) size: BucketSize,

opendata-common/src/clock.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ impl Clock for MockClock {
2727
}
2828
}
2929

30+
impl Default for MockClock {
31+
fn default() -> Self {
32+
Self::new()
33+
}
34+
}
35+
3036
impl MockClock {
3137
pub fn with_time(time: SystemTime) -> Self {
3238
Self {

0 commit comments

Comments
 (0)