Skip to content

Commit c491c3e

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

6 files changed

Lines changed: 199 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: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,18 @@ 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 futures::stream::FuturesUnordered;
35+
use itertools::Itertools;
3336
use tonic::async_trait;
3437

3538
use crate::{
3639
event::format::get_datatype_suffix,
40+
handlers::http::modal::ingest_server::INGESTOR_META,
3741
metastore::{MetastoreError, metastore_traits::Metastore},
3842
option::Mode,
3943
parseable::{GlobalSchema, PARSEABLE},
44+
storage::object_storage::global_schema_lock_file_path,
4045
};
4146

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

256355
async fn resolve_backend(
@@ -309,7 +408,7 @@ async fn resolve_backend(
309408
}
310409

311410
fn schema_error(message: impl Into<String>) -> MetastoreError {
312-
MetastoreError::JsonSchemaError {
411+
MetastoreError::CASConvergenceFailure {
313412
message: message.into(),
314413
}
315414
}

‎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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,14 @@ pub fn global_schema_path(stream_name: &str, tenant_id: &Option<String>) -> Rela
15271527
])
15281528
}
15291529

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

0 commit comments

Comments
 (0)