Skip to content

Commit f704fdb

Browse files
committed
rely on lock files instead of etag
1 parent 3c7cb19 commit f704fdb

6 files changed

Lines changed: 186 additions & 12 deletions

File tree

‎src/metastore/metastore_traits.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use bytes::Bytes;
2323
use chrono::{DateTime, Utc};
2424
use dashmap::DashMap;
2525
use erased_serde::Serialize as ErasedSerialize;
26+
use relative_path::RelativePathBuf;
2627
use tonic::async_trait;
2728
use ulid::Ulid;
2829

@@ -292,7 +293,17 @@ pub trait Metastore: std::fmt::Debug + Send + Sync {
292293
stream_name: &str,
293294
tenant_id: &Option<String>,
294295
) -> Result<(), MetastoreError>;
295-
296+
async fn get_global_schema_locks(
297+
&self,
298+
stream_name: &str,
299+
tenant_id: &Option<String>,
300+
) -> Result<Vec<String>, MetastoreError>;
301+
async fn put_global_schema_lock(
302+
&self,
303+
lock_file_path: &RelativePathBuf,
304+
tenant_id: &Option<String>,
305+
) -> Result<(), MetastoreError>;
306+
async fn delete_global_schema_lock(&self, path: &RelativePathBuf, tenant_id: &Option<String>);
296307
/// Global schema operations used to detect and resolve schema conflicts.
297308
/// A missing expected version creates the record; a version updates it with
298309
/// an atomic compare-and-swap. `false` indicates only a version conflict.

‎src/metastore/metastores/object_store_metastore.rs‎

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use bytes::Bytes;
2727
use chrono::{DateTime, Utc};
2828
use dashmap::DashMap;
2929
use futures::{StreamExt, TryStreamExt};
30-
use object_store::UpdateVersion;
3130
use relative_path::RelativePathBuf;
3231
use tonic::async_trait;
3332
use tracing::{info, warn};
@@ -1206,7 +1205,48 @@ impl Metastore for ObjectStoreMetastore {
12061205
.put_object(&path, to_bytes(&obj), tenant_id)
12071206
.await?)
12081207
}
1209-
1208+
async fn get_global_schema_locks(
1209+
&self,
1210+
stream_name: &str,
1211+
tenant_id: &Option<String>,
1212+
) -> Result<Vec<String>, MetastoreError> {
1213+
let path = if let Some(tenant) = tenant_id {
1214+
object_store::path::Path::from(format!(
1215+
"{}/{}/{}",
1216+
tenant, stream_name, STREAM_ROOT_DIRECTORY
1217+
))
1218+
} else {
1219+
object_store::path::Path::from(format!("{}/{}", stream_name, STREAM_ROOT_DIRECTORY))
1220+
};
1221+
let mut result_file_list = Vec::new();
1222+
let resp = self.storage.list_with_delimiter(Some(path)).await?;
1223+
for obj in resp.objects.iter() {
1224+
let name = obj.location.filename();
1225+
if let Some(name) = name
1226+
&& name.ends_with(".lock")
1227+
{
1228+
result_file_list.push(name.to_owned());
1229+
}
1230+
}
1231+
Ok(result_file_list)
1232+
}
1233+
async fn put_global_schema_lock(
1234+
&self,
1235+
lock_file_path: &RelativePathBuf,
1236+
tenant_id: &Option<String>,
1237+
) -> Result<(), MetastoreError> {
1238+
let data = to_bytes(&String::new());
1239+
self.storage
1240+
.put_object(&lock_file_path, data, tenant_id)
1241+
.await?;
1242+
Ok(())
1243+
}
1244+
async fn delete_global_schema_lock(&self, path: &RelativePathBuf, tenant_id: &Option<String>) {
1245+
// lock file deletion is unconditional
1246+
if let Err(e) = self.storage.delete_object(&path, tenant_id).await {
1247+
tracing::error!("Error while deleting lock file- {e}");
1248+
}
1249+
}
12101250
/// Retrieve a global schema and the validator from the same object-store GET.
12111251
async fn get_global_schema(
12121252
&self,
@@ -1216,11 +1256,15 @@ impl Metastore for ObjectStoreMetastore {
12161256
// LocalFS does not participate in the shared registry. Only bypass
12171257
// explicitly unsupported backends; propagate errors from cloud stores.
12181258
let path = global_schema_path(stream_name, tenant_id);
1219-
let global_schema =
1220-
serde_json::from_slice(&self.storage.get_object(&path, tenant_id).await?)?;
1221-
let Some((schema, version)) = self.storage.get_object_versioned(&path, tenant_id).await?
1222-
else {
1223-
return Ok(None);
1259+
let global_schema = match self.storage.get_object(&path, tenant_id).await {
1260+
Ok(obj) => serde_json::from_slice(&obj)?,
1261+
Err(ObjectStorageError::NoSuchKey(_) | ObjectStorageError::PathError(_)) => {
1262+
return Ok(None);
1263+
}
1264+
Err(e) => {
1265+
tracing::error!("{e}");
1266+
return Err(MetastoreError::ObjectStorageError(e));
1267+
}
12241268
};
12251269
Ok(Some(global_schema))
12261270
}

‎src/metastore/mod.rs‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ pub enum MetastoreError {
4949
#[error("JSON schema validation error: {message}")]
5050
JsonSchemaError { message: String },
5151

52+
#[error("CAS convergence failure: {message}")]
53+
CASConvergenceFailure { message: String },
54+
55+
#[error("LockFile failure: {message}")]
56+
LockFileFailure { message: String },
57+
5258
#[error("Invalid JSON structure: expected {expected}, found {found}")]
5359
InvalidJsonStructure { expected: String, found: String },
5460

@@ -100,6 +106,24 @@ impl MetastoreError {
100106
metadata: std::collections::HashMap::new(),
101107
status_code: 400,
102108
},
109+
MetastoreError::CASConvergenceFailure { message } => MetastoreErrorDetail {
110+
operation: "CASConvergenceFailure".to_string(),
111+
message: message.to_string(),
112+
stream_name: None,
113+
file_path: None,
114+
timestamp: Some(chrono::Utc::now()),
115+
metadata: std::collections::HashMap::new(),
116+
status_code: 500,
117+
},
118+
MetastoreError::LockFileFailure { message } => MetastoreErrorDetail {
119+
operation: "LockFileFailure".to_string(),
120+
message: message.to_string(),
121+
stream_name: None,
122+
file_path: None,
123+
timestamp: Some(chrono::Utc::now()),
124+
metadata: std::collections::HashMap::new(),
125+
status_code: 500,
126+
},
103127
MetastoreError::ManifestCodecError(e) => MetastoreErrorDetail {
104128
operation: "ManifestCodecError".to_string(),
105129
message: e.to_string(),
@@ -164,9 +188,11 @@ impl MetastoreError {
164188
MetastoreError::JsonParseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
165189
MetastoreError::ManifestCodecError(..) => StatusCode::INTERNAL_SERVER_ERROR,
166190
MetastoreError::JsonSchemaError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
167-
MetastoreError::InvalidJsonStructure { .. } => StatusCode::INTERNAL_SERVER_ERROR,
191+
MetastoreError::InvalidJsonStructure { .. } => StatusCode::BAD_REQUEST,
168192
MetastoreError::MissingJsonField { .. } => StatusCode::INTERNAL_SERVER_ERROR,
169193
MetastoreError::InvalidJsonValue { .. } => StatusCode::INTERNAL_SERVER_ERROR,
194+
MetastoreError::CASConvergenceFailure { .. }
195+
| MetastoreError::LockFileFailure { .. } => StatusCode::INTERNAL_SERVER_ERROR,
170196
MetastoreError::Error { status_code, .. } => *status_code,
171197
}
172198
}

‎src/schema_registry.rs‎

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ use std::{
3030

3131
use arrow_array::{RecordBatch, RecordBatchOptions};
3232
use arrow_schema::{ArrowError, DataType, Field, FieldRef, Schema};
33+
use chrono::{DateTime, Utc};
34+
use itertools::Itertools;
3335
use tonic::async_trait;
3436

3537
use crate::{
3638
event::format::get_datatype_suffix,
39+
handlers::http::modal::ingest_server::INGESTOR_META,
3740
metastore::{MetastoreError, metastore_traits::Metastore},
3841
option::Mode,
3942
parseable::{GlobalSchema, PARSEABLE},
43+
storage::object_storage::global_schema_lock_file_path,
4044
};
4145

4246
const MAX_CAS_ATTEMPTS: usize = 16;
@@ -250,7 +254,88 @@ pub async fn resolve(
250254
stream_name,
251255
tenant_id,
252256
};
253-
resolve_backend(&backend, stream_name, schemas, pinned).await
257+
// before beginning, acquire lock
258+
// lock file will be- `tenant/stream/.stream/<nodename>-<timestamp>.lock`
259+
let locks = backend
260+
.metastore
261+
.get_global_schema_locks(stream_name, tenant_id)
262+
.await?;
263+
if locks.len() == 0 {
264+
// proceed with writing lock file
265+
} else if locks.len() == 1 {
266+
// only one lock file
267+
// check the time of the existing lock file
268+
// and decide whether to backoff or not
269+
let timestamp = DateTime::from_timestamp_micros(
270+
*&locks[0].rsplit("-").collect_vec()[0]
271+
.parse::<i64>()
272+
.unwrap(),
273+
)
274+
.unwrap();
275+
276+
let mut slept_already = false;
277+
loop {
278+
// if delta > 3 minutes then delete and proceed
279+
if (Utc::now() - timestamp).num_seconds() > 180 {
280+
tracing::warn!(
281+
"delta between lock file- {} and now is greater than 3 minutes, deleting and proceeding",
282+
locks[0]
283+
);
284+
let lock_file =
285+
global_schema_lock_file_path(stream_name, tenant_id).join(&locks[0]);
286+
backend
287+
.metastore
288+
.delete_global_schema_lock(&lock_file, tenant_id)
289+
.await;
290+
} else if !slept_already {
291+
// sleep for 5 seconds and try again
292+
// if lock file still present, skip cycle
293+
tokio::time::sleep(Duration::from_secs(5)).await;
294+
slept_already = true;
295+
} else {
296+
// delta is < 180s and slept already
297+
// check if lock file exists
298+
let locks = backend
299+
.metastore
300+
.get_global_schema_locks(stream_name, tenant_id)
301+
.await?;
302+
if locks.is_empty() {
303+
// proceed normally
304+
} else {
305+
// skip current cycle
306+
return Err(MetastoreError::LockFileFailure {
307+
message: "Unable to obtain a clean lock file state".into(),
308+
});
309+
}
310+
}
311+
}
312+
} else {
313+
// multiple lock files present, self healing required here
314+
// multiple lock files should only be present in case of multiple failures
315+
// safe to assume that conflict resolution can fail now
316+
// delete all of them maybe?
317+
}
318+
let lock_file_path = global_schema_lock_file_path(stream_name, tenant_id).join(format!(
319+
"{}-{}.lock",
320+
INGESTOR_META
321+
.get()
322+
.expect("Ingestor node metadata should exist")
323+
.node_id,
324+
Utc::now().timestamp_micros()
325+
));
326+
backend
327+
.metastore
328+
.put_global_schema_lock(&lock_file_path, tenant_id)
329+
.await?;
330+
331+
// proceed with resolution
332+
let res = resolve_backend(&backend, stream_name, schemas, pinned).await;
333+
334+
backend
335+
.metastore
336+
.delete_global_schema_lock(&lock_file_path, tenant_id)
337+
.await;
338+
res
254339
}
255340

256341
async fn resolve_backend(
@@ -309,7 +394,7 @@ async fn resolve_backend(
309394
}
310395

311396
fn schema_error(message: impl Into<String>) -> MetastoreError {
312-
MetastoreError::JsonSchemaError {
397+
MetastoreError::CASConvergenceFailure {
313398
message: message.into(),
314399
}
315400
}

‎src/storage/mod.rs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ use crate::{
2929
hottier::StreamHotTier,
3030
metadata::SchemaVersion,
3131
metastore::{MetastoreErrorDetail, metastore_traits::MetastoreObject},
32-
migration::to_bytes,
3332
option::StandaloneWithDistributed,
3433
parseable::{GlobalSchema, StreamNotFound},
3534
stats::FullStats,

‎src/storage/object_storage.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,15 @@ pub fn global_schema_path(stream_name: &str, tenant_id: &Option<String>) -> Rela
15271527
])
15281528
}
15291529

1530+
pub fn global_schema_lock_file_path(stream_name: &str, tenant_id: &Option<String>) -> RelativePathBuf {
1531+
let tenant = tenant_id.as_deref().unwrap_or("");
1532+
RelativePathBuf::from_iter([
1533+
tenant,
1534+
stream_name,
1535+
STREAM_ROOT_DIRECTORY,
1536+
])
1537+
}
1538+
15301539
#[inline(always)]
15311540
pub fn stream_json_path(stream_name: &str, tenant_id: &Option<String>) -> RelativePathBuf {
15321541
let tenant = tenant_id.as_deref().unwrap_or("");

0 commit comments

Comments
 (0)