Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/handlers/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ pub async fn fetch_schema(
stream_name: &str,
tenant_id: &Option<String>,
) -> anyhow::Result<arrow_schema::Schema> {
if let Some(global) = PARSEABLE
.metastore
.get_global_schema(stream_name, tenant_id)
.await?
{
return Ok(global.schema);
}
let res: Vec<Schema> = PARSEABLE
.metastore
.get_all_schemas(stream_name, tenant_id)
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod prism;
pub mod query;
pub mod rbac;
mod response;
pub mod schema_registry;
pub mod sse;
mod static_schema;
mod stats;
Expand Down
33 changes: 33 additions & 0 deletions src/metastore/metastore_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use bytes::Bytes;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use erased_serde::Serialize as ErasedSerialize;
use relative_path::RelativePathBuf;
use tonic::async_trait;
use ulid::Ulid;

Expand All @@ -36,6 +37,7 @@ use crate::{
handlers::http::modal::NodeType,
metastore::MetastoreError,
option::Mode,
parseable::GlobalSchema,
users::filters::Filter,
};

Expand Down Expand Up @@ -291,6 +293,37 @@ pub trait Metastore: std::fmt::Debug + Send + Sync {
stream_name: &str,
tenant_id: &Option<String>,
) -> Result<(), MetastoreError>;
async fn get_global_schema_locks(
&self,
stream_name: &str,
tenant_id: &Option<String>,
) -> Result<Vec<String>, MetastoreError>;
async fn put_global_schema_lock(
&self,
lock_file_path: RelativePathBuf,
tenant_id: &Option<String>,
) -> Result<(), MetastoreError>;
async fn delete_global_schema_lock(
&self,
path: RelativePathBuf,
tenant_id: &Option<String>,
) -> Result<(), MetastoreError>;
/// Global schema operations used to detect and resolve schema conflicts.
/// A missing expected hash creates the record; an existing hash is checked
/// before updating. Callers coordinate updates with global-schema lock files;
/// the hash check and write are not atomic. `false` indicates a conflict.
async fn get_global_schema(
&self,
stream_name: &str,
tenant_id: &Option<String>,
) -> Result<Option<GlobalSchema>, MetastoreError>;
async fn put_global_schema(
&self,
obj: GlobalSchema,
stream_name: &str,
tenant_id: &Option<String>,
expected: Option<u64>,
) -> Result<bool, MetastoreError>;

/// parseable metadata
async fn get_parseable_metadata(
Expand Down
143 changes: 135 additions & 8 deletions src/metastore/metastores/object_store_metastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ use crate::{
metastore_traits::{Metastore, MetastoreObject},
},
option::Mode,
parseable::{DEFAULT_TENANT, PARSEABLE},
parseable::{DEFAULT_TENANT, GlobalSchema, PARSEABLE},
storage::{
ALERTS_ROOT_DIRECTORY, ObjectStorage, ObjectStorageError, PARSEABLE_ROOT_DIRECTORY,
SETTINGS_ROOT_DIRECTORY, STREAM_METADATA_FILE_NAME, STREAM_ROOT_DIRECTORY,
TARGETS_ROOT_DIRECTORY, TOMBSTONE_ROOT_DIRECTORY,
ALERTS_ROOT_DIRECTORY, GLOBAL_SCHEMA_FILE_NAME, ObjectStorage, ObjectStorageError,
PARSEABLE_ROOT_DIRECTORY, SETTINGS_ROOT_DIRECTORY, STREAM_METADATA_FILE_NAME,
STREAM_ROOT_DIRECTORY, TARGETS_ROOT_DIRECTORY, TOMBSTONE_ROOT_DIRECTORY,
object_storage::{
alert_json_path, alert_state_json_path, filter_path, manifest_path, mttr_json_path,
outbound_http_policy_json_path, parseable_json_path, schema_path, stream_json_path,
to_bytes,
alert_json_path, alert_state_json_path, filter_path, global_schema_lock_file_path,
global_schema_path, manifest_path, mttr_json_path, outbound_http_policy_json_path,
parseable_json_path, schema_path, stream_json_path, to_bytes,
},
to_object_store_path,
},
users::filters::{Filter, migrate_v1_v2},
};
Expand All @@ -79,6 +80,10 @@ pub fn is_missing_optional_dir(err: &ObjectStorageError) -> bool {
}
}

fn is_legacy_schema_file(file_name: &str) -> bool {
file_name.contains(".schema") && !file_name.ends_with(GLOBAL_SCHEMA_FILE_NAME)
}

#[async_trait]
impl Metastore for ObjectStoreMetastore {
/// Since Parseable already starts with a connection to an object store, no need to implement this
Expand Down Expand Up @@ -1160,7 +1165,7 @@ impl Metastore for ObjectStoreMetastore {
.storage
.get_objects(
Some(&path_prefix),
Box::new(|file_name: String| file_name.contains(".schema")),
Box::new(|file_name: String| is_legacy_schema_file(&file_name)),
tenant_id,
)
.await?
Expand All @@ -1178,6 +1183,11 @@ impl Metastore for ObjectStoreMetastore {
stream_name: &str,
tenant_id: &Option<String>,
) -> Result<Bytes, MetastoreError> {
if let Some(global_schema) = self.get_global_schema(stream_name, tenant_id).await? {
let schema = to_bytes(&global_schema.schema);
return Ok(schema);
}

Ok(self
.storage
.get_object(&schema_path(stream_name, tenant_id), tenant_id)
Expand All @@ -1196,6 +1206,87 @@ impl Metastore for ObjectStoreMetastore {
.put_object(&path, to_bytes(&obj), tenant_id)
.await?)
}
async fn get_global_schema_locks(
&self,
stream_name: &str,
tenant_id: &Option<String>,
) -> Result<Vec<String>, MetastoreError> {
let path = to_object_store_path(&global_schema_lock_file_path(stream_name, tenant_id));
let mut result_file_list = Vec::new();
let resp = self.storage.list_with_delimiter(Some(path)).await?;
for obj in resp.objects.iter() {
let name = obj.location.filename();
if let Some(name) = name
&& name.ends_with(".lock")
{
result_file_list.push(name.to_owned());
}
}
Ok(result_file_list)
}
async fn put_global_schema_lock(
&self,
lock_file_path: RelativePathBuf,
tenant_id: &Option<String>,
) -> Result<(), MetastoreError> {
let data = to_bytes("");
self.storage
.put_object(&lock_file_path, data, tenant_id)
.await?;
Ok(())
}
Comment thread
parmesant marked this conversation as resolved.
async fn delete_global_schema_lock(
&self,
path: RelativePathBuf,
tenant_id: &Option<String>,
) -> Result<(), MetastoreError> {
match self.storage.delete_object(&path, tenant_id).await {
Ok(()) | Err(ObjectStorageError::NoSuchKey(_)) => Ok(()),
Err(ObjectStorageError::IoError(error))
if error.kind() == std::io::ErrorKind::NotFound =>
{
Ok(())
}
Err(error) => Err(error.into()),
}
}
/// Retrieve the persisted global schema and its stored content hash.
async fn get_global_schema(
&self,
stream_name: &str,
tenant_id: &Option<String>,
) -> Result<Option<GlobalSchema>, MetastoreError> {
// LocalFS does not participate in the shared registry. Only bypass
// explicitly unsupported backends; propagate errors from cloud stores.
let path = global_schema_path(stream_name, tenant_id);
let global_schema = match self.storage.get_object(&path, tenant_id).await {
Ok(obj) => serde_json::from_slice(&obj)?,
Err(ObjectStorageError::NoSuchKey(_) | ObjectStorageError::PathError(_)) => {
return Ok(None);
}
Err(e) => {
tracing::error!("{e}");
return Err(MetastoreError::ObjectStorageError(e));
}
};
Ok(Some(global_schema))
}

/// Create a missing global schema or update one whose stored hash matches.
/// Callers coordinate the non-atomic hash check and update with lock files.
async fn put_global_schema(
&self,
obj: GlobalSchema,
stream_name: &str,
tenant_id: &Option<String>,
expected: Option<u64>,
) -> Result<bool, MetastoreError> {
let path = global_schema_path(stream_name, tenant_id);
Ok(self
.storage
.put_object_if(&path, to_bytes(&obj), expected, tenant_id)
.await?)
}

async fn get_parseable_metadata(
&self,
Expand Down Expand Up @@ -1469,4 +1560,40 @@ mod tests {
Err(ObjectStorageError::NoSuchKey(_))
));
}

#[test]
fn global_schema_paths_normalize_default_tenant_and_isolate_real_tenants() {
let no_tenant = None;
let default_tenant = Some(DEFAULT_TENANT.to_owned());
let tenant_a = Some("tenant-a".to_owned());
let tenant_b = Some("tenant-b".to_owned());

assert_eq!(
global_schema_path("events", &no_tenant).as_str(),
"events/.stream/.global.schema"
);
assert_eq!(
global_schema_path("events", &default_tenant),
global_schema_path("events", &no_tenant)
);
assert_eq!(
global_schema_lock_file_path("events", &default_tenant),
global_schema_lock_file_path("events", &no_tenant)
);
assert_eq!(
global_schema_path("events", &tenant_a).as_str(),
"tenant-a/events/.stream/.global.schema"
);
assert_ne!(
global_schema_path("events", &tenant_a),
global_schema_path("events", &tenant_b)
);
assert_ne!(
global_schema_lock_file_path("events", &tenant_a),
global_schema_lock_file_path("events", &tenant_b)
);
assert!(is_legacy_schema_file(".schema"));
assert!(is_legacy_schema_file(".ingestor.node-1.schema"));
assert!(!is_legacy_schema_file(GLOBAL_SCHEMA_FILE_NAME));
}
}
28 changes: 27 additions & 1 deletion src/metastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ pub enum MetastoreError {
#[error("JSON schema validation error: {message}")]
JsonSchemaError { message: String },

#[error("CAS convergence failure: {message}")]
CASConvergenceFailure { message: String },

#[error("LockFile failure: {message}")]
LockFileFailure { message: String },

#[error("Invalid JSON structure: expected {expected}, found {found}")]
InvalidJsonStructure { expected: String, found: String },

Expand Down Expand Up @@ -100,6 +106,24 @@ impl MetastoreError {
metadata: std::collections::HashMap::new(),
status_code: 400,
},
MetastoreError::CASConvergenceFailure { message } => MetastoreErrorDetail {
operation: "CASConvergenceFailure".to_string(),
message: message.to_string(),
stream_name: None,
file_path: None,
timestamp: Some(chrono::Utc::now()),
metadata: std::collections::HashMap::new(),
status_code: 500,
},
MetastoreError::LockFileFailure { message } => MetastoreErrorDetail {
operation: "LockFileFailure".to_string(),
message: message.to_string(),
stream_name: None,
file_path: None,
timestamp: Some(chrono::Utc::now()),
metadata: std::collections::HashMap::new(),
status_code: 500,
},
MetastoreError::ManifestCodecError(e) => MetastoreErrorDetail {
operation: "ManifestCodecError".to_string(),
message: e.to_string(),
Expand Down Expand Up @@ -164,9 +188,11 @@ impl MetastoreError {
MetastoreError::JsonParseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::ManifestCodecError(..) => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::JsonSchemaError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::InvalidJsonStructure { .. } => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::InvalidJsonStructure { .. } => StatusCode::BAD_REQUEST,
MetastoreError::MissingJsonField { .. } => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::InvalidJsonValue { .. } => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::CASConvergenceFailure { .. }
| MetastoreError::LockFileFailure { .. } => StatusCode::INTERNAL_SERVER_ERROR,
MetastoreError::Error { status_code, .. } => *status_code,
}
}
Expand Down
Loading
Loading