mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-17 10:17:55 +00:00
fix(table-catalog): harden commit publication (#5779)
* fix(table-catalog): harden commit publication * fix(table-catalog): make commit replay deterministic * test(table-catalog): cover denied commit object reads * fix(table-catalog): guard ref commits and order publication locks * fix(table-catalog): close commit publication race gaps * fix(table-catalog): close publication review gaps * fix(table-catalog): isolate blocked strong publications * fix(table-catalog): scale and fence commit publication * fix(table-catalog): close publication compatibility gaps * fix(table-catalog): clarify compatibility cleanup marker * fix(table-catalog): repair publication hardening checks * fix(table-catalog): align commit tests with publication fences * fix(table-catalog): bind authorization to request context * refactor(table-catalog): reuse internal error mapping * test(storage): install request context for tag conditions --------- Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
This commit is contained in:
@@ -188,6 +188,25 @@ pub async fn validate_admin_request_with_bucket_object(
|
||||
evaluate_admin_actions(iam_store, &ctx, &actions, resource.bucket, resource.object).await
|
||||
}
|
||||
|
||||
pub(crate) async fn validate_admin_action_with_bucket_object_for_iam<S: Store>(
|
||||
iam_store: Arc<IamSys<S>>,
|
||||
headers: &HeaderMap,
|
||||
cred: &Credentials,
|
||||
is_owner: bool,
|
||||
action: Action,
|
||||
remote_addr: Option<std::net::SocketAddr>,
|
||||
resource: AdminResourceScope<'_>,
|
||||
) -> S3Result<()> {
|
||||
let ctx = AuthContext {
|
||||
headers,
|
||||
cred,
|
||||
is_owner,
|
||||
deny_only: false,
|
||||
remote_addr,
|
||||
};
|
||||
evaluate_admin_actions(iam_store, &ctx, &[action], resource.bucket, resource.object).await
|
||||
}
|
||||
|
||||
/// Admin gate for KMS endpoints that act on one key.
|
||||
///
|
||||
/// `key_id` is the identifier as requested (before any alias resolution), so a
|
||||
|
||||
@@ -33,8 +33,12 @@ impl Operation for EnableTableBucketHandler {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::warehouse(&warehouse);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableBucketAction).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let response = enable_table_bucket_response(&store, &warehouse).await?;
|
||||
let backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let object_store = runtime_sources::object_store_from_req(&req)
|
||||
.ok_or_else(|| table_catalog_internal_error("request object store is not initialized"))?;
|
||||
let store = table_catalog_store_from_backend(backend.clone())?;
|
||||
let publication = TableCommitObjectBackend::preauthorized(backend);
|
||||
let response = enable_table_bucket_response(&store, &publication, object_store.as_ref(), &warehouse).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -47,8 +51,8 @@ impl Operation for GetTableBucketHandler {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::warehouse(&warehouse);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableBucketAction).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let response = table_bucket_response(&store, &warehouse, enabled).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -62,8 +66,8 @@ impl Operation for GetTableCatalogMigrationHandler {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::warehouse(&warehouse);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableCatalogAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_object_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
let started = Instant::now();
|
||||
let result = store
|
||||
.plan_durable_strong_backing_migration(&warehouse)
|
||||
@@ -82,8 +86,8 @@ impl Operation for MaterializeTableCatalogMigrationHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
authorize_table_catalog_request(&req, AdminAction::MigrateTableCatalogAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_object_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
let started = Instant::now();
|
||||
let result = store
|
||||
.materialize_durable_strong_backing_migration(&warehouse)
|
||||
@@ -102,8 +106,8 @@ impl Operation for CancelTableCatalogMigrationHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
authorize_table_catalog_request(&req, AdminAction::MigrateTableCatalogAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_object_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
let started = Instant::now();
|
||||
let result = store
|
||||
.cancel_durable_strong_backing_migration(&warehouse)
|
||||
@@ -125,8 +129,8 @@ impl Operation for ExternalCatalogBridgeHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_object_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
let response = external_catalog_bridge_response(&store, &warehouse, &namespace, &table).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -142,9 +146,9 @@ impl Operation for PutExternalCatalogBridgeHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<ExternalCatalogBridgeRequest>(req.input).await?;
|
||||
let store = table_catalog_object_store()?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
let response = put_external_catalog_bridge_response(&store, &warehouse, &namespace, &table, request).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -154,15 +158,16 @@ pub struct SyncExternalCatalogBridgeHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for SyncExternalCatalogBridgeHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableMetadataLocationAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let store = table_catalog_object_store()?;
|
||||
let principal =
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableMetadataLocationAction).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
if store
|
||||
.load_table(&warehouse, &namespace.public_name(), &table)
|
||||
.await
|
||||
@@ -171,18 +176,21 @@ impl Operation for SyncExternalCatalogBridgeHandler {
|
||||
{
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
|
||||
}
|
||||
let request = read_json_body::<ExternalCatalogBridgeSyncRequest>(req.input).await?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let response = sync_external_catalog_bridge_response(
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
let request = read_json_body::<ExternalCatalogBridgeSyncRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result = sync_external_catalog_bridge_response(
|
||||
&store,
|
||||
&metadata_backend,
|
||||
&commit_backend,
|
||||
&warehouse,
|
||||
&namespace,
|
||||
&table,
|
||||
request,
|
||||
table_bucket_enabled,
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ impl Operation for RestLoadCredentialsHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableCredentialsAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let principal = table_catalog_request_principal(&req).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let issuer = IamTableCredentialIssuer::from_env();
|
||||
let response = load_credentials_response(&store, &warehouse, &namespace, &table, &issuer, Some(&principal)).await?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableCredentialsAction).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let issuer = IamTableCredentialIssuer::from_request(&req)?;
|
||||
let response =
|
||||
load_credentials_response(&store, &warehouse, &namespace, &table, &issuer, Some(&principal.credentials)).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ impl Operation for RestTableMetadataMaintenanceHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<TableMetadataMaintenanceRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let store = table_catalog_object_store()?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_object_store_from_extensions(&req.extensions)?;
|
||||
let response =
|
||||
table_metadata_maintenance_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
@@ -44,8 +44,8 @@ impl Operation for GetTableMaintenanceConfigHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableLifecycleAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.get_table_maintenance_config(&warehouse, &namespace.public_name(), &table)
|
||||
.await
|
||||
@@ -64,9 +64,9 @@ impl Operation for PutTableMaintenanceConfigHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableLifecycleAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<crate::table_catalog::TableMaintenanceConfig>(req.input).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.put_table_maintenance_config(&warehouse, &namespace.public_name(), &table, request)
|
||||
.await
|
||||
@@ -86,8 +86,8 @@ impl Operation for GetTableMaintenanceJobHandler {
|
||||
let job = job_id_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableLifecycleAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let Some(response) = store
|
||||
.get_table_metadata_maintenance_report(&warehouse, &namespace.public_name(), &table, &job)
|
||||
.await
|
||||
@@ -109,8 +109,8 @@ impl Operation for GetTableMaintenanceSchedulerHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableLifecycleAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.get_table_maintenance_scheduler_report(&warehouse, &namespace.public_name(), &table)
|
||||
.await
|
||||
@@ -129,9 +129,9 @@ impl Operation for RunTableMaintenanceSchedulerHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body_or_default::<TableMaintenanceSchedulerRunRequest>(req.input).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.run_table_maintenance_scheduler_once(
|
||||
&warehouse,
|
||||
@@ -155,9 +155,9 @@ impl Operation for RunTableMaintenanceWorkerHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<TableMaintenanceWorkerRunRequest>(req.input).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.run_table_metadata_maintenance_worker_once(
|
||||
&warehouse,
|
||||
@@ -182,9 +182,9 @@ impl Operation for HeartbeatTableMaintenanceJobHandler {
|
||||
let job = job_id_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<TableMaintenanceHeartbeatRequest>(req.input).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.heartbeat_table_metadata_maintenance_job(
|
||||
&warehouse,
|
||||
@@ -211,9 +211,9 @@ impl Operation for TableMaintenanceQuarantineHandler {
|
||||
let job = job_id_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<crate::table_catalog::TableMaintenanceQuarantineOperationRequest>(req.input).await?;
|
||||
let store = table_catalog_store()?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = store
|
||||
.apply_table_maintenance_quarantine_operation(&warehouse, &namespace.public_name(), &table, &job, request)
|
||||
.await
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,8 +26,8 @@ impl Operation for RestListNamespacesHandler {
|
||||
None => TableCatalogResource::warehouse(&warehouse),
|
||||
};
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableNamespaceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = list_namespaces_response(&store, &warehouse, parent.as_ref(), &req.uri).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -49,8 +49,8 @@ impl Operation for RestCreateNamespaceHandler {
|
||||
"namespace creation",
|
||||
)
|
||||
.await?;
|
||||
let store = table_catalog_store()?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let response = create_namespace_response(&store, &warehouse, request, table_bucket_enabled).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -65,8 +65,8 @@ impl Operation for RestGetNamespaceHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableNamespaceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = get_namespace_response(&store, &warehouse, &namespace).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -81,8 +81,8 @@ impl Operation for RestDropNamespaceHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::DeleteTableNamespaceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
drop_namespace_in_store(&store, &warehouse, &namespace.public_name()).await?;
|
||||
Ok(empty_response(StatusCode::NO_CONTENT))
|
||||
}
|
||||
@@ -97,7 +97,7 @@ impl Operation for RestUpdateNamespacePropertiesHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::UpdateTableNamespacePropertiesAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_bounded_json_body::<UpdateNamespacePropertiesRequest>(
|
||||
&req.headers,
|
||||
req.input,
|
||||
@@ -106,7 +106,7 @@ impl Operation for RestUpdateNamespacePropertiesHandler {
|
||||
"namespace properties",
|
||||
)
|
||||
.await?;
|
||||
let store = table_catalog_store()?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = update_namespace_properties_response(&store, &warehouse, &namespace, request).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -121,8 +121,8 @@ impl Operation for RestNamespaceExistsHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableNamespaceAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
Ok(empty_response(namespace_exists_status(&store, &warehouse, &namespace).await?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ impl Operation for ListTableRefsHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response = table_refs_response(&store, &metadata_backend, &warehouse, &namespace, &table).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
@@ -36,19 +36,21 @@ pub struct PutTableRefHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for PutTableRefHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let ref_name = ref_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let request = read_json_body::<PutTableRefRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<PutTableRefRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response =
|
||||
put_table_ref_response(&store, &metadata_backend, &warehouse, &namespace, &table, &ref_name, request).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result = put_table_ref_response(&store, &commit_backend, &warehouse, &namespace, &table, &ref_name, request).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -57,19 +59,21 @@ pub struct DeleteTableRefHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for DeleteTableRefHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let ref_name = ref_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let request = read_json_body_or_default::<DeleteTableRefRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body_or_default::<DeleteTableRefRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response =
|
||||
delete_table_ref_response(&store, &metadata_backend, &warehouse, &namespace, &table, &ref_name, request).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result = delete_table_ref_response(&store, &commit_backend, &warehouse, &namespace, &table, &ref_name, request).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ impl Operation for RestListTablesHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = list_tables_response(&store, &warehouse, &namespace, &req.uri).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -40,11 +40,12 @@ impl Operation for RestCreateTableHandler {
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CreateTableAction).await?;
|
||||
let request = read_json_body::<CreateTableRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let commit_backend = TableCommitObjectBackend::preauthorized(metadata_backend);
|
||||
let response =
|
||||
create_table_response(&store, &metadata_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
|
||||
create_table_response(&store, &commit_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -53,17 +54,20 @@ pub struct RestRegisterTableHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RestRegisterTableHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
|
||||
let request = read_json_body::<RegisterTableRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
let request = read_json_body::<RegisterTableRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let response =
|
||||
register_table_response(&store, &metadata_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result =
|
||||
register_table_response(&store, &commit_backend, &warehouse, &namespace, request, table_bucket_enabled).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -78,8 +82,8 @@ impl Operation for RestLoadTableHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response = load_table_response(&store, &metadata_backend, &warehouse, &namespace, &table).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
@@ -96,8 +100,8 @@ impl Operation for RestTableExistsHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
Ok(empty_response(table_exists_status(&store, &warehouse, &namespace, &table).await?))
|
||||
}
|
||||
}
|
||||
@@ -106,17 +110,20 @@ pub struct RestCommitTableHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RestCommitTableHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let request = read_json_body::<RestCommitTableRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<RestCommitTableRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response = commit_table_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result = commit_table_response(&store, &commit_backend, &warehouse, &namespace, &table, request).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -131,8 +138,8 @@ impl Operation for RestDropTableHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::DeleteTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
drop_table_in_store(&store, &warehouse, &namespace, &table).await?;
|
||||
Ok(empty_response(StatusCode::NO_CONTENT))
|
||||
}
|
||||
@@ -148,8 +155,8 @@ impl Operation for GetTableMetadataLocationHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataLocationAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = get_table_metadata_location_response(&store, &warehouse, &namespace, &table).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -159,18 +166,22 @@ pub struct UpdateTableMetadataLocationHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for UpdateTableMetadataLocationHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableMetadataLocationAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let request = read_json_body::<UpdateTableMetadataLocationRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal =
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableMetadataLocationAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<UpdateTableMetadataLocationRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response =
|
||||
update_table_metadata_location_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result =
|
||||
update_table_metadata_location_response(&store, &commit_backend, &warehouse, &namespace, &table, request).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -185,8 +196,8 @@ impl Operation for ExportTableCatalogHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let started = Instant::now();
|
||||
let result = store
|
||||
.export_table_catalog_entry(&warehouse, &namespace.public_name(), &table)
|
||||
@@ -202,19 +213,21 @@ pub struct ImportTableCatalogHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for ImportTableCatalogHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
|
||||
let request = read_json_body::<CatalogImportRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
let request = read_json_body::<CatalogImportRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let response =
|
||||
catalog_import_response(&store, &metadata_backend, &warehouse, &namespace, &table, request, table_bucket_enabled)
|
||||
.await?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result =
|
||||
catalog_import_response(&store, &commit_backend, &warehouse, &namespace, &table, request, table_bucket_enabled).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
@@ -229,8 +242,8 @@ impl Operation for GetTableCatalogDiagnosticsHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let config = store
|
||||
.get_table_maintenance_config(&warehouse, &namespace.public_name(), &table)
|
||||
.await
|
||||
@@ -263,8 +276,8 @@ impl Operation for RecoverTableCatalogHandler {
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let started = Instant::now();
|
||||
let result = store
|
||||
.recover_table_commits(&warehouse, &namespace.public_name(), &table)
|
||||
@@ -280,17 +293,20 @@ pub struct RollbackTableCatalogHandler {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Operation for RollbackTableCatalogHandler {
|
||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||
let warehouse = warehouse_from_params(¶ms)?;
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let table = table_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let request = read_json_body::<RollbackTableRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<RollbackTableRequest>(std::mem::take(&mut req.input)).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response = rollback_table_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
|
||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||
let result = rollback_table_response(&store, &commit_backend, &warehouse, &namespace, &table, request).await;
|
||||
let response = commit_backend.finish(result).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,8 +23,8 @@ impl Operation for RestListViewsHandler {
|
||||
let namespace = namespace_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
let response = list_views_response(&store, &warehouse, &namespace, &req.uri).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
}
|
||||
@@ -40,9 +40,9 @@ impl Operation for RestCreateViewHandler {
|
||||
let resource = TableCatalogResource::namespace(&warehouse, &namespace);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CreateTableAction).await?;
|
||||
let request = read_json_body::<CreateViewRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
|
||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let response =
|
||||
create_view_response(&store, &metadata_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
@@ -59,8 +59,8 @@ impl Operation for RestLoadViewHandler {
|
||||
let view = view_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableMetadataAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response = load_view_response(&store, &metadata_backend, &warehouse, &namespace, &view).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
@@ -77,8 +77,8 @@ impl Operation for RestViewExistsHandler {
|
||||
let view = view_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
Ok(empty_response(view_exists_status(&store, &warehouse, &namespace, &view).await?))
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,9 @@ impl Operation for RestReplaceViewHandler {
|
||||
let view = view_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let request = read_json_body::<RestCommitViewRequest>(req.input).await?;
|
||||
let metadata_backend = table_catalog_backend()?;
|
||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||
let response = replace_view_response(&store, &metadata_backend, &warehouse, &namespace, &view, request).await?;
|
||||
build_json_response(StatusCode::OK, &response)
|
||||
@@ -112,8 +112,8 @@ impl Operation for RestDropViewHandler {
|
||||
let view = view_name_from_params(¶ms)?;
|
||||
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
|
||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::DeleteTableAction).await?;
|
||||
ensure_table_bucket_enabled(&warehouse).await?;
|
||||
let store = table_catalog_store()?;
|
||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||
drop_view_in_store(&store, &warehouse, &namespace, &view).await?;
|
||||
Ok(empty_response(StatusCode::NO_CONTENT))
|
||||
}
|
||||
|
||||
@@ -793,7 +793,9 @@ pub(crate) mod data_usage {
|
||||
}
|
||||
|
||||
pub(crate) mod access {
|
||||
pub(crate) use crate::storage::storage_api::access_consumer::{ReqInfo, authorize_request};
|
||||
pub(crate) use crate::storage::storage_api::access_consumer::{
|
||||
ReqInfo, authorize_internal_object_request, authorize_request,
|
||||
};
|
||||
pub(crate) use crate::storage::storage_api::request_context_consumer::{RequestContext, spawn_traced};
|
||||
}
|
||||
|
||||
|
||||
+100
-2
@@ -451,7 +451,7 @@ pub async fn check_key_valid_with_context(
|
||||
cred = u.credentials;
|
||||
}
|
||||
|
||||
let claims = check_claims_from_token(session_token, &cred)
|
||||
let claims = check_claims_from_token_with_context(session_token, &cred, ctx)
|
||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("check claims failed {e}")))?;
|
||||
|
||||
cred.claims = if !claims.is_empty() { Some(claims) } else { None };
|
||||
@@ -461,6 +461,14 @@ pub async fn check_key_valid_with_context(
|
||||
}
|
||||
|
||||
pub fn check_claims_from_token(token: &str, cred: &Credentials) -> S3Result<HashMap<String, Value>> {
|
||||
check_claims_from_token_with_context(token, cred, None)
|
||||
}
|
||||
|
||||
fn check_claims_from_token_with_context(
|
||||
token: &str,
|
||||
cred: &Credentials,
|
||||
ctx: Option<&AppContext>,
|
||||
) -> S3Result<HashMap<String, Value>> {
|
||||
if !token.is_empty() && cred.access_key.is_empty() {
|
||||
return Err(s3_error!(InvalidRequest, "no access key"));
|
||||
}
|
||||
@@ -481,7 +489,11 @@ pub fn check_claims_from_token(token: &str, cred: &Credentials) -> S3Result<Hash
|
||||
return Err(s3_error!(InvalidRequest, "invalid access key is temp and expired"));
|
||||
}
|
||||
|
||||
let Some(sys_cred) = current_action_credentials() else {
|
||||
let sys_cred = match ctx {
|
||||
Some(context) => context.action_credentials().get(),
|
||||
None => current_action_credentials(),
|
||||
};
|
||||
let Some(sys_cred) = sys_cred else {
|
||||
return Err(s3_error!(InternalError, "action cred not init"));
|
||||
};
|
||||
|
||||
@@ -1059,14 +1071,47 @@ pub fn get_query_param<'a>(query: &'a str, param_name: &str) -> Option<&'a str>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime_sources::{IamInterface, KmsInterface};
|
||||
use http::{HeaderMap, HeaderValue, Uri};
|
||||
use rustfs_credentials::Credentials;
|
||||
use rustfs_iam::{
|
||||
store::{
|
||||
Store as _,
|
||||
object::{IAM_CONFIG_PREFIX, ObjectStore},
|
||||
},
|
||||
sys::IamSys,
|
||||
};
|
||||
use rustfs_kms::KmsServiceManager;
|
||||
use rustfs_policy::auth::get_new_credentials_with_metadata;
|
||||
use rustfs_trusted_proxies::ValidationMode;
|
||||
use s3s::auth::SecretKey;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
struct ContextIam {
|
||||
handle: Arc<IamSys<ObjectStore>>,
|
||||
}
|
||||
|
||||
impl IamInterface for ContextIam {
|
||||
fn handle(&self) -> Arc<IamSys<ObjectStore>> {
|
||||
self.handle.clone()
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
struct TestKms;
|
||||
|
||||
impl KmsInterface for TestKms {
|
||||
fn handle(&self) -> Arc<KmsServiceManager> {
|
||||
Arc::new(KmsServiceManager::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_credentials() -> Credentials {
|
||||
Credentials {
|
||||
access_key: "test-access-key".to_string(),
|
||||
@@ -1268,6 +1313,59 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_claims_uses_the_explicit_context_signing_secret() {
|
||||
let (_temp_dir, _disk_paths, store) = crate::app::gating_test_env::isolated_multi_pool_ecstore().await;
|
||||
ObjectStore::new(store.clone())
|
||||
.save_iam_config(serde_json::json!({"version": 1}), format!("{}/format.json", *IAM_CONFIG_PREFIX))
|
||||
.await
|
||||
.expect("seed request IAM format");
|
||||
let iam = rustfs_iam::build_iam_sys(store.clone())
|
||||
.await
|
||||
.expect("request IAM should initialize");
|
||||
let matching = AppContext::new(store.clone(), Arc::new(ContextIam { handle: iam.clone() }), Arc::new(TestKms));
|
||||
let mismatching = AppContext::new(store, Arc::new(ContextIam { handle: iam.clone() }), Arc::new(TestKms));
|
||||
assert!(matching.publish_action_credentials(Credentials {
|
||||
access_key: "matching-root".to_string(),
|
||||
secret_key: "matching-signing-secret".to_string(),
|
||||
status: "on".to_string(),
|
||||
..Default::default()
|
||||
}));
|
||||
assert!(mismatching.publish_action_credentials(Credentials {
|
||||
access_key: "mismatching-root".to_string(),
|
||||
secret_key: "mismatching-signing-secret".to_string(),
|
||||
status: "on".to_string(),
|
||||
..Default::default()
|
||||
}));
|
||||
let claims = HashMap::from([
|
||||
(
|
||||
"exp".to_string(),
|
||||
json!((OffsetDateTime::now_utc() + time::Duration::minutes(5)).unix_timestamp()),
|
||||
),
|
||||
("context".to_string(), json!("matching")),
|
||||
]);
|
||||
let mut credential = get_new_credentials_with_metadata(&claims, "matching-signing-secret")
|
||||
.expect("temporary credentials should be generated");
|
||||
credential.parent_user = "matching-root".to_string();
|
||||
iam.set_temp_user(&credential.access_key, &credential, None)
|
||||
.await
|
||||
.expect("temporary credentials should be stored in request IAM");
|
||||
|
||||
let (verified, _) = check_key_valid_with_context(&credential.session_token, &credential.access_key, Some(&matching))
|
||||
.await
|
||||
.expect("the matching request context should verify the token");
|
||||
assert_eq!(
|
||||
verified.claims.as_ref().and_then(|claims| claims.get("context")),
|
||||
Some(&json!("matching"))
|
||||
);
|
||||
assert!(
|
||||
check_key_valid_with_context(&credential.session_token, &credential.access_key, Some(&mismatching))
|
||||
.await
|
||||
.is_err(),
|
||||
"a different server context must not validate the token"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_session_token_from_header() {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
+726
-148
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ use crate::storage::storage_api::ecfs_consumer::contract::{
|
||||
use crate::storage::storage_api::ecfs_consumer::object_lock::{
|
||||
parse_object_lock_legal_hold, parse_object_lock_retention, validate_bucket_object_lock_enabled,
|
||||
};
|
||||
use crate::storage::storage_api::runtime_sources_consumer::runtime_sources;
|
||||
use crate::storage::storage_api::runtime_sources_consumer::{ECStore, runtime_sources};
|
||||
use crate::table_catalog;
|
||||
use http::StatusCode;
|
||||
use metrics::{counter, histogram};
|
||||
@@ -128,6 +128,15 @@ impl FS {
|
||||
let Some(store) = self.server_ctx.object_store() else {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
};
|
||||
Self::get_object_tag_conditions_for_policy_from_store(store.as_ref(), bucket, object, version_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_object_tag_conditions_for_policy_from_store(
|
||||
store: &ECStore,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
version_id: Option<&str>,
|
||||
) -> S3Result<std::collections::HashMap<String, Vec<String>>> {
|
||||
let opts = ObjectOptions {
|
||||
version_id: version_id.map(String::from),
|
||||
..Default::default()
|
||||
|
||||
@@ -67,15 +67,14 @@ pub(crate) use storage_api::{
|
||||
ecstore_layout, ecstore_metrics, ecstore_notification, ecstore_rebalance, ecstore_rio, ecstore_rpc, ecstore_set_disk,
|
||||
ecstore_storage, ecstore_tier, encode_tags, find_local_disk_by_ref, get_bucket_accelerate_config, get_bucket_cors_config,
|
||||
get_bucket_logging_config, get_bucket_metadata, get_bucket_notification_config, get_bucket_object_lock_config,
|
||||
get_bucket_policy_raw, get_bucket_replication_config, get_bucket_request_payment_config, get_bucket_sse_config,
|
||||
get_bucket_website_config, get_local_server_property, get_lock_acquire_timeout, get_public_access_block_config,
|
||||
head_prefix_consumer, helper_consumer, init_background_replication, init_bucket_metadata_sys, init_ecstore_config,
|
||||
init_local_disks_with_instance_ctx, init_lock_clients, is_err_bucket_not_found, is_err_object_not_found,
|
||||
is_err_version_not_found, is_valid_storage_class, options_consumer, prewarm_local_disk_id_map_with_instance_ctx, read_config,
|
||||
record_replication_proxy, rpc_consumer, runtime_sources_consumer, s3_api_consumer, serialize, table_catalog_path_hash,
|
||||
to_s3s_etag, topology_snapshot_from_endpoint_pools_with_capabilities, try_migrate_bucket_metadata, try_migrate_iam_config,
|
||||
try_migrate_server_config, update_bucket_metadata_config, update_bucket_metadata_config_if_incarnation, verify_rpc_signature,
|
||||
wrap_reader,
|
||||
get_bucket_replication_config, get_bucket_request_payment_config, get_bucket_sse_config, get_bucket_website_config,
|
||||
get_local_server_property, get_lock_acquire_timeout, head_prefix_consumer, helper_consumer, init_background_replication,
|
||||
init_bucket_metadata_sys, init_ecstore_config, init_local_disks_with_instance_ctx, init_lock_clients,
|
||||
is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found, is_valid_storage_class, options_consumer,
|
||||
prewarm_local_disk_id_map_with_instance_ctx, read_config, record_replication_proxy, rpc_consumer, runtime_sources_consumer,
|
||||
s3_api_consumer, serialize, table_catalog_path_hash, to_s3s_etag, topology_snapshot_from_endpoint_pools_with_capabilities,
|
||||
try_migrate_bucket_metadata, try_migrate_iam_config, try_migrate_server_config, update_bucket_metadata_config,
|
||||
update_bucket_metadata_config_if_incarnation, verify_rpc_signature, wrap_reader,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -109,9 +109,9 @@ pub(crate) use super::sse::{
|
||||
pub(crate) mod access_consumer {
|
||||
pub(crate) use super::super::access::{
|
||||
PostObjectRequestMarker, ReqInfo, apply_bucket_generation_guard, apply_copy_source_bucket_generation_guard,
|
||||
authorize_request, bucket_config_mutation_incarnation, has_bypass_governance_header, load_bucket_generation_from_store,
|
||||
log_list_buckets_iam_implicit_deny, prepare_list_buckets_iam_authorization, recursive_force_delete_is_authorized,
|
||||
replication_request_authorized, req_info_mut, req_info_ref,
|
||||
authorize_internal_object_request, authorize_request, bucket_config_mutation_incarnation, has_bypass_governance_header,
|
||||
load_bucket_generation_from_store, log_list_buckets_iam_implicit_deny, prepare_list_buckets_iam_authorization,
|
||||
recursive_force_delete_is_authorized, replication_request_authorized, req_info_mut, req_info_ref,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1448,10 +1448,6 @@ pub(crate) async fn get_bucket_accelerate_config(
|
||||
ecstore_bucket::metadata_sys::get_accelerate_config(bucket).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_bucket_policy_raw(bucket: &str) -> Result<(String, time::OffsetDateTime)> {
|
||||
ecstore_bucket::metadata_sys::get_bucket_policy_raw(bucket).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_bucket_cors_config(bucket: &str) -> Result<(s3s::dto::CORSConfiguration, time::OffsetDateTime)> {
|
||||
ecstore_bucket::metadata_sys::get_cors_config(bucket).await
|
||||
}
|
||||
@@ -1466,12 +1462,6 @@ pub(crate) async fn get_bucket_object_lock_config(
|
||||
ecstore_bucket::metadata_sys::get_object_lock_config(bucket).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_public_access_block_config(
|
||||
bucket: &str,
|
||||
) -> Result<(s3s::dto::PublicAccessBlockConfiguration, time::OffsetDateTime)> {
|
||||
ecstore_bucket::metadata_sys::get_public_access_block_config(bucket).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_bucket_replication_config(
|
||||
bucket: &str,
|
||||
) -> Result<(s3s::dto::ReplicationConfiguration, time::OffsetDateTime)> {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::super::*;
|
||||
|
||||
pub(crate) fn commit_log_matches_request(commit_log: &CommitLogEntry, request: &TableCommitRequest, table_id: &str) -> bool {
|
||||
@@ -39,6 +41,49 @@ pub(crate) fn table_matches_staged_base(table: &TableEntry, commit_log: &CommitL
|
||||
&& table.version_token == commit_log.expected_version_token
|
||||
}
|
||||
|
||||
pub(crate) struct TableCommitHistoryIndex<'a> {
|
||||
table_id: &'a str,
|
||||
reachable_states: BTreeSet<(&'a str, &'a str)>,
|
||||
}
|
||||
|
||||
impl<'a> TableCommitHistoryIndex<'a> {
|
||||
pub(crate) fn new(table: &'a TableEntry, commits: impl IntoIterator<Item = &'a CommitLogEntry>) -> Self {
|
||||
let mut by_new_state = BTreeMap::<(&str, &str), Option<(&str, &str)>>::new();
|
||||
for commit in commits
|
||||
.into_iter()
|
||||
.filter(|commit| commit.table_id == table.table_id && !matches!(commit.status, CommitLogStatus::Failed))
|
||||
{
|
||||
let key = (commit.new_metadata_location.as_str(), commit.new_version_token.as_str());
|
||||
let previous = (commit.previous_metadata_location.as_str(), commit.expected_version_token.as_str());
|
||||
by_new_state
|
||||
.entry(key)
|
||||
.and_modify(|candidate| *candidate = None)
|
||||
.or_insert(Some(previous));
|
||||
}
|
||||
|
||||
let mut reachable_states = BTreeSet::new();
|
||||
let mut state = (table.metadata_location.as_str(), table.version_token.as_str());
|
||||
while reachable_states.insert(state) {
|
||||
let Some(Some(previous)) = by_new_state.get(&state) else {
|
||||
break;
|
||||
};
|
||||
state = *previous;
|
||||
}
|
||||
Self {
|
||||
table_id: &table.table_id,
|
||||
reachable_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn proves_committed(&self, target: &CommitLogEntry) -> bool {
|
||||
self.table_id == target.table_id.as_str()
|
||||
&& !matches!(target.status, CommitLogStatus::Failed)
|
||||
&& self
|
||||
.reachable_states
|
||||
.contains(&(target.new_metadata_location.as_str(), target.new_version_token.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn table_catalog_recovery_summary(
|
||||
metadata_status: &TableMetadataPointerStatus,
|
||||
commit_recovery: &TableCommitRecoveryReport,
|
||||
@@ -76,7 +121,7 @@ pub(crate) fn table_catalog_recovery_summary(
|
||||
(metadata_status.unwrap_or(TableCatalogRecoveryStatus::Healthy), actions)
|
||||
}
|
||||
|
||||
fn commit_logs_share_recovery_payload(left: &CommitLogEntry, right: &CommitLogEntry) -> bool {
|
||||
pub(crate) fn commit_logs_share_recovery_payload(left: &CommitLogEntry, right: &CommitLogEntry) -> bool {
|
||||
left.version == right.version
|
||||
&& left.commit_id == right.commit_id
|
||||
&& left.idempotency_key == right.idempotency_key
|
||||
@@ -109,6 +154,7 @@ pub(crate) fn table_commit_recovery_entry(
|
||||
table: &TableEntry,
|
||||
commit_log: &CommitLogEntry,
|
||||
idempotency_commit: Option<&CommitLogEntry>,
|
||||
historically_committed: bool,
|
||||
) -> TableCommitRecoveryEntry {
|
||||
let idempotency_index_status = commit_idempotency_index_status(commit_log, idempotency_commit);
|
||||
let idempotency_index_present = matches!(
|
||||
@@ -127,6 +173,11 @@ pub(crate) fn table_commit_recovery_entry(
|
||||
TableCommitRecoveryState::ManualReview,
|
||||
"idempotency index points at a different commit payload".to_string(),
|
||||
)
|
||||
} else if matches!(commit_log.status, CommitLogStatus::Failed) {
|
||||
(
|
||||
TableCommitRecoveryState::ManualReview,
|
||||
"failed commit log cannot be finalized automatically".to_string(),
|
||||
)
|
||||
} else if table_matches_committed_log(table, commit_log) {
|
||||
if matches!(commit_log.status, CommitLogStatus::Committed) {
|
||||
if idempotency_index_repair_required {
|
||||
@@ -146,6 +197,11 @@ pub(crate) fn table_commit_recovery_entry(
|
||||
"current table pointer already advanced but commit log is not finalized".to_string(),
|
||||
)
|
||||
}
|
||||
} else if matches!(commit_log.status, CommitLogStatus::Staged) && historically_committed {
|
||||
(
|
||||
TableCommitRecoveryState::FinalizationRequired,
|
||||
"a later committed pointer proves this staged commit is part of table history".to_string(),
|
||||
)
|
||||
} else if matches!(commit_log.status, CommitLogStatus::Committed) {
|
||||
if idempotency_index_repair_required {
|
||||
(
|
||||
|
||||
@@ -175,6 +175,46 @@ pub(crate) fn table_metadata_warehouse_location(
|
||||
metadata_warehouse_location(table_bucket, metadata_location, metadata_object, validate_table_warehouse_location)
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_json_sha256(metadata: &serde_json::Value) -> TableCatalogStoreResult<String> {
|
||||
let canonical = serde_json::to_vec(metadata)
|
||||
.map_err(|err| TableCatalogStoreError::Internal(format!("failed to encode metadata digest input: {err}")))?;
|
||||
Ok(hex_simd::encode_to_string(Sha256::digest(canonical), hex_simd::AsciiCase::Lower))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_commit_metadata_digest(
|
||||
request: &TableCommitRequest,
|
||||
metadata_object: &TableCatalogObject,
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
let mut expected_digest = None;
|
||||
for requirement in &request.requirements {
|
||||
if requirement.get("type").and_then(serde_json::Value::as_str) != Some(TABLE_METADATA_DIGEST_REQUIREMENT_TYPE) {
|
||||
continue;
|
||||
}
|
||||
if expected_digest.is_some() {
|
||||
return Err(TableCatalogStoreError::Invalid(
|
||||
"commit contains duplicate metadata digest requirements".to_string(),
|
||||
));
|
||||
}
|
||||
expected_digest = Some(
|
||||
requirement
|
||||
.get("sha256")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|digest| rustfs_utils::crypto::is_sha256_checksum(digest))
|
||||
.ok_or_else(|| TableCatalogStoreError::Invalid("commit metadata digest is invalid".to_string()))?,
|
||||
);
|
||||
}
|
||||
let Some(expected_digest) = expected_digest else {
|
||||
return Ok(());
|
||||
};
|
||||
let metadata = decode_table_metadata_json(&request.new_metadata_location, &metadata_object.data)?;
|
||||
if canonical_json_sha256(&metadata)? != expected_digest {
|
||||
return Err(TableCatalogStoreError::Conflict(
|
||||
"new metadata object changed after commit validation".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn view_metadata_warehouse_location(
|
||||
table_bucket: &str,
|
||||
metadata_location: &str,
|
||||
@@ -197,6 +237,10 @@ pub(crate) fn warehouse_index_candidate_prefixes(object: &str) -> Vec<&str> {
|
||||
prefixes
|
||||
}
|
||||
|
||||
pub(crate) fn warehouse_object_prefixes_overlap(left: &str, right: &str) -> bool {
|
||||
left.starts_with(right) || right.starts_with(left)
|
||||
}
|
||||
|
||||
pub(crate) fn table_data_plane_resource_from_entry(table: TableEntry, warehouse_object_prefix: String) -> TableDataPlaneResource {
|
||||
TableDataPlaneResource {
|
||||
table_bucket: table.table_bucket,
|
||||
@@ -1352,7 +1396,9 @@ where
|
||||
validate_snapshot_graph_data_file_reference(reference, format_version, manifest_sequence_number)?;
|
||||
let object_key = snapshot_graph_object_key(context, &reference.location, reference.object_kind.clone())?;
|
||||
match reference.entry_status {
|
||||
Some(0 | 1) if budget.validated_live_objects.insert(object_key.clone()) => live_object_keys.push(object_key),
|
||||
Some(0 | 1) if budget.validated_live_objects.insert(object_key.clone()) => {
|
||||
live_object_keys.push(object_key);
|
||||
}
|
||||
Some(0 | 1) => {}
|
||||
Some(2) => {}
|
||||
Some(_) => {
|
||||
|
||||
@@ -177,6 +177,14 @@ pub(crate) fn default_table_root_prefix(namespace: &Namespace) -> String {
|
||||
format!("{}{}/{}/", default_namespace_root_prefix(), namespace.storage_id(), TABLE_ROOT)
|
||||
}
|
||||
|
||||
pub(crate) fn default_table_publication_lock_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
|
||||
format!("{}{}/publication.lock", default_table_root_prefix(namespace), table.as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn default_table_bucket_publication_lock_path() -> String {
|
||||
rustfs_common::table_catalog::TABLE_BUCKET_PUBLICATION_LOCK_PATH.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn default_table_marker_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
|
||||
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), TABLE_MARKER_FILE)
|
||||
}
|
||||
|
||||
@@ -72,9 +72,10 @@ pub(crate) use error::{TableCatalogStoreError, TableCatalogStoreResult};
|
||||
pub(crate) use iceberg::*;
|
||||
pub use identifier::{IdentifierSegment, Namespace, is_reserved_table_object_key};
|
||||
pub(crate) use identifier::{
|
||||
default_table_data_dir_path, default_table_delete_dir_path, default_table_metadata_dir_path,
|
||||
default_table_metadata_file_path, default_view_metadata_file_path, is_valid_table_metadata_location,
|
||||
is_valid_view_metadata_location, metadata_location_from_metadata_file_path, validate_bucket_object_mutation,
|
||||
default_table_bucket_publication_lock_path, default_table_data_dir_path, default_table_delete_dir_path,
|
||||
default_table_metadata_dir_path, default_table_metadata_file_path, default_table_publication_lock_path,
|
||||
default_view_metadata_file_path, is_valid_table_metadata_location, is_valid_view_metadata_location,
|
||||
metadata_location_from_metadata_file_path, validate_bucket_object_mutation,
|
||||
};
|
||||
pub(crate) use maintenance::*;
|
||||
pub(crate) use model::*;
|
||||
@@ -89,12 +90,16 @@ pub(crate) const TABLE_NAMESPACE_MARKER_VERSION: u16 = 1;
|
||||
pub(crate) const TABLE_RESOURCE_MARKER_VERSION: u16 = 1;
|
||||
pub(crate) const TABLE_METADATA_POINTER_VERSION: u16 = 1;
|
||||
pub(crate) const TABLE_CATALOG_ENTRY_VERSION: u16 = 1;
|
||||
pub(crate) const TABLE_WAREHOUSE_INDEX_STATE_VERSION: u16 = 2;
|
||||
pub(crate) const TABLE_MAINTENANCE_CONFIG_VERSION: u16 = 1;
|
||||
pub(crate) const TABLE_EXTERNAL_CATALOG_BRIDGE_VERSION: u16 = 1;
|
||||
pub(crate) const TABLE_CATALOG_BACKING_MANIFEST_VERSION: u16 = 1;
|
||||
pub(crate) const ENV_TABLE_CATALOG_BACKING: &str = "RUSTFS_TABLE_CATALOG_BACKING";
|
||||
pub(crate) const ENV_TABLE_CATALOG_PUBLICATION_FENCE_FLEET_CONFIRMED: &str =
|
||||
"RUSTFS_TABLE_CATALOG_PUBLICATION_FENCE_FLEET_CONFIRMED";
|
||||
pub(crate) const TABLE_CATALOG_BACKING_OBJECT: &str = "object";
|
||||
pub(crate) const TABLE_CATALOG_BACKING_DURABLE_STRONG: &str = "durable-strong";
|
||||
pub(crate) const TABLE_METADATA_DIGEST_REQUIREMENT_TYPE: &str = "assert-rustfs-metadata-sha256";
|
||||
pub(crate) const TABLE_METADATA_FILE_NAME_MAX_LEN: usize = 128;
|
||||
pub(crate) const TABLE_METADATA_JSON_MAX_SIZE: usize = 50 * 1024 * 1024;
|
||||
pub(crate) const TABLE_MANIFEST_AVRO_MAX_SIZE: usize = 128 * 1024 * 1024;
|
||||
@@ -104,7 +109,7 @@ const TABLE_MANIFEST_AVRO_MAX_HEADER_ENTRIES: usize = 1_024;
|
||||
const TABLE_COMMIT_MAX_MANIFESTS: usize = 10_000;
|
||||
const TABLE_COMMIT_MAX_AVRO_BYTES: usize = 512 * 1024 * 1024;
|
||||
const TABLE_COMMIT_MAX_FILE_REFERENCES: usize = 1_000_000;
|
||||
const TABLE_COMMIT_OBJECT_VALIDATION_CONCURRENCY: usize = 16;
|
||||
pub(crate) const TABLE_COMMIT_OBJECT_VALIDATION_CONCURRENCY: usize = 16;
|
||||
pub const TABLE_RESERVED_PREFIX: &str = BUCKET_TABLE_RESERVED_PREFIX;
|
||||
const WAREHOUSE_ROOT: &str = "warehouses";
|
||||
const NAMESPACE_ROOT: &str = "namespaces";
|
||||
|
||||
@@ -390,6 +390,23 @@ where
|
||||
"migration snapshot contains duplicate idempotency lookup keys".to_string(),
|
||||
));
|
||||
}
|
||||
let mut commit_logs_by_table = BTreeMap::<&str, Vec<&CommitLogEntry>>::new();
|
||||
for record in &snapshot.commits {
|
||||
commit_logs_by_table
|
||||
.entry(record.table_id.as_str())
|
||||
.or_default()
|
||||
.push(&record.commit);
|
||||
}
|
||||
let history_by_table = tables_by_id
|
||||
.iter()
|
||||
.map(|(table_id, table)| {
|
||||
let commits = commit_logs_by_table
|
||||
.get(table_id)
|
||||
.into_iter()
|
||||
.flat_map(|commits| commits.iter().copied());
|
||||
(*table_id, TableCommitHistoryIndex::new(table, commits))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for record in &snapshot.commits {
|
||||
let table = tables_by_id.get(record.table_id.as_str()).ok_or_else(|| {
|
||||
TableCatalogStoreError::Invalid(format!("commit {} has no table in migration snapshot", record.commit.commit_id))
|
||||
@@ -408,7 +425,14 @@ where
|
||||
.idempotency_key
|
||||
.as_deref()
|
||||
.and_then(|idempotency_key| idempotency_by_key.get(&(record.table_id.as_str(), idempotency_key)).copied());
|
||||
let recovery = table_commit_recovery_entry(table, &record.commit, indexed);
|
||||
let recovery = table_commit_recovery_entry(
|
||||
table,
|
||||
&record.commit,
|
||||
indexed,
|
||||
history_by_table
|
||||
.get(record.table_id.as_str())
|
||||
.is_some_and(|history| history.proves_committed(&record.commit)),
|
||||
);
|
||||
if recovery.recovery_state != TableCommitRecoveryState::Committed {
|
||||
return Err(TableCatalogStoreError::Conflict(format!(
|
||||
"commit {} requires catalog recovery before durable strong migration",
|
||||
|
||||
@@ -166,6 +166,12 @@ pub(crate) trait TableCatalogStore: Send + Sync {
|
||||
|
||||
async fn register_table(&self, entry: TableEntry) -> TableCatalogStoreResult<()>;
|
||||
|
||||
async fn register_table_with_publication(
|
||||
&self,
|
||||
entry: TableEntry,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<()>;
|
||||
|
||||
async fn list_tables(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<TableEntry>>;
|
||||
|
||||
async fn list_all_tables(&self, table_bucket: &str) -> TableCatalogStoreResult<Vec<TableEntry>>;
|
||||
@@ -201,6 +207,12 @@ pub(crate) trait TableCatalogStore: Send + Sync {
|
||||
/// newly introduced or changed snapshots before invoking this persistence boundary.
|
||||
async fn commit_table(&self, request: TableCommitRequest) -> TableCatalogStoreResult<TableCommitResult>;
|
||||
|
||||
async fn commit_table_with_publication(
|
||||
&self,
|
||||
request: TableCommitRequest,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<TableCommitResult>;
|
||||
|
||||
async fn drop_table(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()>;
|
||||
|
||||
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()>;
|
||||
@@ -243,6 +255,131 @@ pub(crate) trait TableCatalogStore: Send + Sync {
|
||||
) -> TableCatalogStoreResult<Option<CommitLogEntry>>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub(crate) trait TableCommitPublication: Send + Sync {
|
||||
async fn begin_table_bucket(&self, table_bucket: &str) -> TableCatalogStoreResult<()>;
|
||||
|
||||
async fn prepare(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()>;
|
||||
|
||||
fn holds_table_bucket(&self, table_bucket: &str) -> bool;
|
||||
|
||||
fn holds_table(&self, table_bucket: &str, namespace: &str, table: &str) -> bool;
|
||||
|
||||
fn complete(&self);
|
||||
}
|
||||
|
||||
pub(crate) struct TableCommitPublicationCompletion<'a> {
|
||||
publication: &'a (dyn TableCommitPublication + Sync),
|
||||
}
|
||||
|
||||
impl<'a> TableCommitPublicationCompletion<'a> {
|
||||
pub(crate) fn new(publication: &'a (dyn TableCommitPublication + Sync)) -> Self {
|
||||
Self { publication }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TableCommitPublicationCompletion<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.publication.complete();
|
||||
}
|
||||
}
|
||||
|
||||
struct TableCommitLockPublication<'a, B> {
|
||||
backend: &'a B,
|
||||
state: parking_lot::Mutex<TableCommitLockPublicationState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TableCommitLockPublicationState {
|
||||
table_bucket: Option<String>,
|
||||
table: Option<(String, String, String)>,
|
||||
guards: Vec<Box<dyn Send>>,
|
||||
}
|
||||
|
||||
impl<'a, B> TableCommitLockPublication<'a, B> {
|
||||
fn new(backend: &'a B) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
state: parking_lot::Mutex::new(TableCommitLockPublicationState::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<'a, B> TableCommitPublication for TableCommitLockPublication<'a, B>
|
||||
where
|
||||
B: TableCatalogObjectBackend,
|
||||
{
|
||||
async fn begin_table_bucket(&self, table_bucket: &str) -> TableCatalogStoreResult<()> {
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
if state.table_bucket.as_deref() == Some(table_bucket) {
|
||||
return Ok(());
|
||||
}
|
||||
if state.table_bucket.is_some() || state.table.is_some() {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table-bucket publication lock is already held for another table bucket".to_string(),
|
||||
));
|
||||
}
|
||||
state.table_bucket = Some(table_bucket.to_string());
|
||||
}
|
||||
let publication_lock = default_table_bucket_publication_lock_path();
|
||||
let guard = match self.backend.acquire_write_lock(table_bucket, &publication_lock).await {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
self.state.lock().table_bucket = None;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
self.state.lock().guards.push(guard);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()> {
|
||||
let namespace = parse_namespace_for_store(namespace)?;
|
||||
let table = parse_table_for_store(table)?;
|
||||
let table_key = (table_bucket.to_string(), namespace.public_name(), table.as_str().to_string());
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
if state.table.as_ref() == Some(&table_key) {
|
||||
return Ok(());
|
||||
}
|
||||
if state.table.is_some() {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table publication lock is already held for another table".to_string(),
|
||||
));
|
||||
}
|
||||
state.table = Some(table_key);
|
||||
}
|
||||
let publication_lock = default_table_publication_lock_path(&namespace, &table);
|
||||
let guard = match self.backend.acquire_write_lock(table_bucket, &publication_lock).await {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
self.state.lock().table = None;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
self.state.lock().guards.push(guard);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn holds_table_bucket(&self, table_bucket: &str) -> bool {
|
||||
self.state.lock().table_bucket.as_deref() == Some(table_bucket)
|
||||
}
|
||||
|
||||
fn holds_table(&self, table_bucket: &str, namespace: &str, table: &str) -> bool {
|
||||
self.state
|
||||
.lock()
|
||||
.table
|
||||
.as_ref()
|
||||
.is_some_and(|held| held.0 == table_bucket && held.1 == namespace && held.2 == table)
|
||||
}
|
||||
|
||||
fn complete(&self) {
|
||||
*self.state.lock() = TableCommitLockPublicationState::default();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct TableCatalogObject {
|
||||
pub data: Vec<u8>,
|
||||
@@ -317,8 +454,26 @@ pub(crate) trait TableCatalogObjectBackend: Clone + Send + Sync + 'static {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn object_metadata_unlocked(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
) -> TableCatalogStoreResult<Option<TableCatalogObjectMetadata>> {
|
||||
Ok(self
|
||||
.read_object_unlocked(bucket, object)
|
||||
.await?
|
||||
.map(|object| TableCatalogObjectMetadata {
|
||||
etag: object.etag,
|
||||
mod_time: object.mod_time,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn object_exists(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<bool>;
|
||||
|
||||
async fn object_exists_unlocked(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<bool> {
|
||||
self.object_exists(bucket, object).await
|
||||
}
|
||||
|
||||
async fn put_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
@@ -370,6 +525,55 @@ pub(crate) trait TableCatalogObjectBackend: Clone + Send + Sync + 'static {
|
||||
}
|
||||
|
||||
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>>;
|
||||
|
||||
async fn begin_table_bucket_commit_publication(&self, _table_bucket: &str) -> TableCatalogStoreResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn table_bucket_commit_publication_is_held(&self, _table_bucket: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn prepare_table_commit_publication(
|
||||
&self,
|
||||
_table_bucket: &str,
|
||||
_namespace: &str,
|
||||
_table: &str,
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn table_commit_publication_is_held(&self, _table_bucket: &str, _namespace: &str, _table: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_table_commit_publication(&self) {}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B> TableCommitPublication for B
|
||||
where
|
||||
B: TableCatalogObjectBackend,
|
||||
{
|
||||
async fn begin_table_bucket(&self, table_bucket: &str) -> TableCatalogStoreResult<()> {
|
||||
self.begin_table_bucket_commit_publication(table_bucket).await
|
||||
}
|
||||
|
||||
async fn prepare(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()> {
|
||||
self.prepare_table_commit_publication(table_bucket, namespace, table).await
|
||||
}
|
||||
|
||||
fn holds_table_bucket(&self, table_bucket: &str) -> bool {
|
||||
self.table_bucket_commit_publication_is_held(table_bucket)
|
||||
}
|
||||
|
||||
fn holds_table(&self, table_bucket: &str, namespace: &str, table: &str) -> bool {
|
||||
self.table_commit_publication_is_held(table_bucket, namespace, table)
|
||||
}
|
||||
|
||||
fn complete(&self) {
|
||||
self.complete_table_commit_publication();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -577,6 +781,10 @@ impl TableCatalogObjectPaths {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn warehouse_index_entries_prefix(&self, table_bucket: &str) -> String {
|
||||
format!("{}{}/", self.table_bucket_root_prefix(table_bucket), WAREHOUSE_INDEX_ROOT)
|
||||
}
|
||||
|
||||
pub fn warehouse_index_entry_path(&self, table_bucket: &str, warehouse_object_prefix: &str) -> String {
|
||||
format!(
|
||||
"{}{}/{}.json",
|
||||
@@ -779,6 +987,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_table_with_publication(
|
||||
&self,
|
||||
entry: TableEntry,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
match self {
|
||||
Self::ObjectBacked(store) => store.register_table_with_publication(entry, publication).await,
|
||||
Self::DurableStrong(store) => store.register_table_with_publication(entry, publication).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_tables(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<TableEntry>> {
|
||||
match self {
|
||||
Self::ObjectBacked(store) => store.list_tables(table_bucket, namespace).await,
|
||||
@@ -831,6 +1050,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn commit_table_with_publication(
|
||||
&self,
|
||||
request: TableCommitRequest,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<TableCommitResult> {
|
||||
match self {
|
||||
Self::ObjectBacked(store) => store.commit_table_with_publication(request, publication).await,
|
||||
Self::DurableStrong(store) => store.commit_table_with_publication(request, publication).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn drop_table(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()> {
|
||||
match self {
|
||||
Self::ObjectBacked(store) => store.drop_table(table_bucket, namespace, table).await,
|
||||
@@ -1205,6 +1435,32 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn object_metadata_unlocked(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
) -> TableCatalogStoreResult<Option<TableCatalogObjectMetadata>> {
|
||||
match self
|
||||
.store
|
||||
.get_object_info(
|
||||
bucket,
|
||||
object,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(info) => Ok(Some(TableCatalogObjectMetadata {
|
||||
etag: info.etag,
|
||||
mod_time: info.mod_time,
|
||||
})),
|
||||
Err(err) if is_missing_storage_error(&err) => Ok(None),
|
||||
Err(err) => Err(storage_error_to_catalog("stat catalog object", err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn object_exists(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<bool> {
|
||||
match self.store.get_object_info(bucket, object, &ObjectOptions::default()).await {
|
||||
Ok(_) => Ok(true),
|
||||
@@ -1213,6 +1469,25 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn object_exists_unlocked(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<bool> {
|
||||
match self
|
||||
.store
|
||||
.get_object_info(
|
||||
bucket,
|
||||
object,
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) if is_missing_storage_error(&err) => Ok(false),
|
||||
Err(err) => Err(storage_error_to_catalog("check catalog object", err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn put_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
|
||||
@@ -519,7 +519,7 @@ where
|
||||
|
||||
async fn write_warehouse_index_state_unlocked(&self, table_bucket: &str) -> TableCatalogStoreResult<()> {
|
||||
let state = TableWarehouseIndexStateEntry {
|
||||
version: TABLE_CATALOG_ENTRY_VERSION,
|
||||
version: TABLE_WAREHOUSE_INDEX_STATE_VERSION,
|
||||
table_bucket: table_bucket.to_string(),
|
||||
state: TableCatalogEntryState::Active,
|
||||
};
|
||||
@@ -542,7 +542,7 @@ where
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(state.version == TABLE_CATALOG_ENTRY_VERSION
|
||||
Ok(state.version == TABLE_WAREHOUSE_INDEX_STATE_VERSION
|
||||
&& state.table_bucket == table_bucket
|
||||
&& state.state == TableCatalogEntryState::Active)
|
||||
}
|
||||
@@ -635,11 +635,51 @@ where
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn ensure_table_warehouse_prefix_available(&self, entry: &TableEntry) -> TableCatalogStoreResult<()> {
|
||||
let candidate = table_warehouse_index_entry(entry)?;
|
||||
let state_object = self.paths.warehouse_index_state_path(&candidate.table_bucket);
|
||||
for object in self
|
||||
.backend
|
||||
.list_objects(self.catalog_bucket(), &self.paths.warehouse_index_entries_prefix(&candidate.table_bucket))
|
||||
.await?
|
||||
{
|
||||
if object == state_object {
|
||||
continue;
|
||||
}
|
||||
let Some((existing, _)) = self
|
||||
.read_entry::<TableWarehouseIndexEntry>(self.catalog_bucket(), &object)
|
||||
.await?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if existing.table_id == candidate.table_id || existing.state != TableCatalogEntryState::Active {
|
||||
continue;
|
||||
}
|
||||
if warehouse_object_prefixes_overlap(&existing.warehouse_object_prefix, &candidate.warehouse_object_prefix)
|
||||
&& self.warehouse_index_entry_has_active_owner(&existing).await?
|
||||
{
|
||||
return Err(TableCatalogStoreError::Conflict(format!(
|
||||
"table warehouse location overlaps an active table: {}",
|
||||
candidate.warehouse_object_prefix
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reserve_table_warehouse_index(&self, entry: &TableEntry) -> TableCatalogStoreResult<WarehouseIndexReservation> {
|
||||
let index = table_warehouse_index_entry(entry)?;
|
||||
let object = self
|
||||
.paths
|
||||
.warehouse_index_entry_path(&index.table_bucket, &index.warehouse_object_prefix);
|
||||
if let Some((existing, _)) = self
|
||||
.read_entry::<TableWarehouseIndexEntry>(self.catalog_bucket(), &object)
|
||||
.await?
|
||||
&& existing == index
|
||||
{
|
||||
return Ok(WarehouseIndexReservation::AlreadyReserved);
|
||||
}
|
||||
self.ensure_table_warehouse_prefix_available(entry).await?;
|
||||
loop {
|
||||
match self
|
||||
.write_entry(self.catalog_bucket(), &object, &index, TableCatalogPutPrecondition::IfAbsent)
|
||||
@@ -745,7 +785,7 @@ where
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(state.version == TABLE_CATALOG_ENTRY_VERSION
|
||||
Ok(state.version == TABLE_WAREHOUSE_INDEX_STATE_VERSION
|
||||
&& state.table_bucket == table_bucket
|
||||
&& state.state == TableCatalogEntryState::Active)
|
||||
}
|
||||
@@ -868,7 +908,27 @@ where
|
||||
if self.read_warehouse_index_state_unlocked(table_bucket).await? {
|
||||
return Ok(());
|
||||
}
|
||||
for table in self.list_all_tables(table_bucket).await? {
|
||||
let tables = self.list_all_tables(table_bucket).await?;
|
||||
let mut active_prefixes = tables
|
||||
.iter()
|
||||
.filter(|table| table.state == TableCatalogEntryState::Active)
|
||||
.filter_map(|table| {
|
||||
table_warehouse_object_prefix(table)
|
||||
.ok()
|
||||
.map(|prefix| (prefix, table.table_id.as_str()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
active_prefixes.sort_unstable_by(|left, right| left.0.cmp(&right.0));
|
||||
if let Some(window) = active_prefixes
|
||||
.windows(2)
|
||||
.find(|window| window[0].1 != window[1].1 && warehouse_object_prefixes_overlap(&window[0].0, &window[1].0))
|
||||
{
|
||||
return Err(TableCatalogStoreError::Conflict(format!(
|
||||
"active table warehouse locations overlap: {} and {}",
|
||||
window[0].0, window[1].0
|
||||
)));
|
||||
}
|
||||
for table in tables {
|
||||
if table.state != TableCatalogEntryState::Active {
|
||||
continue;
|
||||
}
|
||||
@@ -958,12 +1018,30 @@ where
|
||||
&self,
|
||||
entry: TableEntry,
|
||||
precondition: TableCatalogPutPrecondition,
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
let publication = TableCommitLockPublication::new(&self.backend);
|
||||
self.write_table_entry_with_publication(entry, precondition, &publication)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_table_entry_with_publication(
|
||||
&self,
|
||||
entry: TableEntry,
|
||||
precondition: TableCatalogPutPrecondition,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
validate_catalog_entry_version("table", entry.version)?;
|
||||
self.require_table_bucket(&entry.table_bucket).await?;
|
||||
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
||||
let table = parse_table_for_store(&entry.table)?;
|
||||
validate_table_warehouse_location(&entry.table_bucket, &entry.warehouse_location)?;
|
||||
publication.begin_table_bucket(&entry.table_bucket).await?;
|
||||
if !publication.holds_table_bucket(&entry.table_bucket) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table registration requires a table-bucket publication fence".to_string(),
|
||||
));
|
||||
}
|
||||
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||
self.require_table_bucket(&entry.table_bucket).await?;
|
||||
let _migration_guard = self.acquire_object_backed_catalog_write_permit(&entry.table_bucket).await?;
|
||||
let namespace_path = self.paths.namespace_entry_path(&entry.table_bucket, &namespace);
|
||||
let _namespace_guard = self
|
||||
@@ -974,6 +1052,15 @@ where
|
||||
.await?;
|
||||
let table_path = self.paths.table_entry_path(&entry.table_bucket, &namespace, &table);
|
||||
let _table_guard = self.backend.acquire_write_lock(self.catalog_bucket(), &table_path).await?;
|
||||
// Preserve catalog -> publication -> object lock order across rolling upgrades.
|
||||
publication
|
||||
.prepare(&entry.table_bucket, &entry.namespace, &entry.table)
|
||||
.await?;
|
||||
if !publication.holds_table(&entry.table_bucket, &entry.namespace, &entry.table) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table registration requires a table publication fence".to_string(),
|
||||
));
|
||||
}
|
||||
let reservation = self.reserve_table_warehouse_index(&entry).await?;
|
||||
let result = self
|
||||
.write_entry_unlocked(self.catalog_bucket(), &table_path, &entry, precondition)
|
||||
@@ -1056,6 +1143,20 @@ where
|
||||
.map(|entry| entry.map(|(commit, _)| commit))
|
||||
}
|
||||
|
||||
async fn read_table_commit_logs(&self, entry: &TableEntry) -> TableCatalogStoreResult<Vec<(String, CommitLogEntry)>> {
|
||||
let commit_prefix = self.paths.commit_log_entries_prefix(&entry.table_bucket, &entry.table_id);
|
||||
let mut commits = Vec::new();
|
||||
for object in self.backend.list_objects(self.catalog_bucket(), &commit_prefix).await? {
|
||||
if !object.ends_with(".json") {
|
||||
continue;
|
||||
}
|
||||
if let Some(commit_log) = self.read_commit_by_path(&object).await? {
|
||||
commits.push((object, commit_log));
|
||||
}
|
||||
}
|
||||
Ok(commits)
|
||||
}
|
||||
|
||||
async fn finalize_commit_log(
|
||||
&self,
|
||||
commit_path: &str,
|
||||
@@ -1076,15 +1177,10 @@ where
|
||||
entry: &TableEntry,
|
||||
finalized_count: usize,
|
||||
) -> TableCatalogStoreResult<TableCommitRecoveryReport> {
|
||||
let commit_prefix = self.paths.commit_log_entries_prefix(&entry.table_bucket, &entry.table_id);
|
||||
let mut commits = Vec::new();
|
||||
for object in self.backend.list_objects(self.catalog_bucket(), &commit_prefix).await? {
|
||||
if !object.ends_with(".json") {
|
||||
continue;
|
||||
}
|
||||
let Some(commit_log) = self.read_commit_by_path(&object).await? else {
|
||||
continue;
|
||||
};
|
||||
let commit_logs_with_paths = self.read_table_commit_logs(entry).await?;
|
||||
let history = TableCommitHistoryIndex::new(entry, commit_logs_with_paths.iter().map(|(_, commit_log)| commit_log));
|
||||
let mut commits = Vec::with_capacity(commit_logs_with_paths.len());
|
||||
for (_, commit_log) in &commit_logs_with_paths {
|
||||
let idempotency_commit = match commit_log.idempotency_key.as_deref() {
|
||||
Some(idempotency_key) => {
|
||||
let idempotency_path =
|
||||
@@ -1094,7 +1190,12 @@ where
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
commits.push(table_commit_recovery_entry(entry, &commit_log, idempotency_commit.as_ref()));
|
||||
commits.push(table_commit_recovery_entry(
|
||||
entry,
|
||||
commit_log,
|
||||
idempotency_commit.as_ref(),
|
||||
history.proves_committed(commit_log),
|
||||
));
|
||||
}
|
||||
commits.sort_by(|left, right| left.commit_id.cmp(&right.commit_id));
|
||||
|
||||
@@ -1172,15 +1273,10 @@ where
|
||||
)));
|
||||
};
|
||||
|
||||
let commit_prefix = self.paths.commit_log_entries_prefix(table_bucket, &entry.table_id);
|
||||
let commit_logs_with_paths = self.read_table_commit_logs(&entry).await?;
|
||||
let history = TableCommitHistoryIndex::new(&entry, commit_logs_with_paths.iter().map(|(_, commit_log)| commit_log));
|
||||
let mut finalized_count = 0;
|
||||
for commit_path in self.backend.list_objects(self.catalog_bucket(), &commit_prefix).await? {
|
||||
if !commit_path.ends_with(".json") {
|
||||
continue;
|
||||
}
|
||||
let Some(commit_log) = self.read_commit_by_path(&commit_path).await? else {
|
||||
continue;
|
||||
};
|
||||
for (commit_path, commit_log) in &commit_logs_with_paths {
|
||||
let idempotency_path = commit_log.idempotency_key.as_deref().map(|idempotency_key| {
|
||||
self.paths
|
||||
.commit_idempotency_entry_path(table_bucket, &entry.table_id, idempotency_key)
|
||||
@@ -1189,14 +1285,19 @@ where
|
||||
Some(idempotency_path) => self.read_commit_by_path(idempotency_path).await?,
|
||||
None => None,
|
||||
};
|
||||
let recovery_entry = table_commit_recovery_entry(&entry, &commit_log, idempotency_commit.as_ref());
|
||||
let recovery_entry = table_commit_recovery_entry(
|
||||
&entry,
|
||||
commit_log,
|
||||
idempotency_commit.as_ref(),
|
||||
history.proves_committed(commit_log),
|
||||
);
|
||||
if matches!(
|
||||
recovery_entry.recovery_state,
|
||||
TableCommitRecoveryState::FinalizationRequired | TableCommitRecoveryState::IdempotencyIndexRepairRequired
|
||||
) {
|
||||
let mut committed = commit_log;
|
||||
let mut committed = commit_log.clone();
|
||||
committed.status = CommitLogStatus::Committed;
|
||||
self.finalize_commit_log(&commit_path, idempotency_path.as_deref(), &committed)
|
||||
self.finalize_commit_log(commit_path, idempotency_path.as_deref(), &committed)
|
||||
.await?;
|
||||
finalized_count += 1;
|
||||
}
|
||||
@@ -2427,6 +2528,21 @@ where
|
||||
table: &str,
|
||||
config: TableSnapshotExpirationConfig,
|
||||
) -> TableCatalogStoreResult<TableSnapshotExpirationReport> {
|
||||
self.plan_table_snapshot_expiration_with_backend(&self.backend, table_bucket, namespace, table, config)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn plan_table_snapshot_expiration_with_backend<P>(
|
||||
&self,
|
||||
metadata_backend: &P,
|
||||
table_bucket: &str,
|
||||
namespace: &str,
|
||||
table: &str,
|
||||
config: TableSnapshotExpirationConfig,
|
||||
) -> TableCatalogStoreResult<TableSnapshotExpirationReport>
|
||||
where
|
||||
P: TableCatalogObjectBackend,
|
||||
{
|
||||
validate_table_snapshot_expiration_config(&config)?;
|
||||
let namespace = parse_namespace_for_store(namespace)?;
|
||||
let table = parse_table_for_store(table)?;
|
||||
@@ -2445,7 +2561,7 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let Some(current_metadata) = read_table_metadata_value(&self.backend, table_bucket, &entry.metadata_location).await?
|
||||
let Some(current_metadata) = read_table_metadata_value(metadata_backend, table_bucket, &entry.metadata_location).await?
|
||||
else {
|
||||
return Err(TableCatalogStoreError::NotFound(format!(
|
||||
"current metadata object {}",
|
||||
@@ -2507,6 +2623,23 @@ where
|
||||
table: &str,
|
||||
config: TableCompactionPlanningConfig,
|
||||
) -> TableCatalogStoreResult<TableCompactionPlanningReport> {
|
||||
let publication = TableCommitLockPublication::new(&self.backend);
|
||||
self.commit_table_compaction_with_publication(&self.backend, &publication, table_bucket, namespace, table, config)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn commit_table_compaction_with_publication<P>(
|
||||
&self,
|
||||
object_backend: &P,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
table_bucket: &str,
|
||||
namespace: &str,
|
||||
table: &str,
|
||||
config: TableCompactionPlanningConfig,
|
||||
) -> TableCatalogStoreResult<TableCompactionPlanningReport>
|
||||
where
|
||||
P: TableCatalogObjectBackend,
|
||||
{
|
||||
validate_table_compaction_planning_config(&config)?;
|
||||
let namespace = parse_namespace_for_store(namespace)?;
|
||||
let table = parse_table_for_store(table)?;
|
||||
@@ -2525,7 +2658,7 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let Some(current_metadata) = read_table_metadata_value(&self.backend, table_bucket, &entry.metadata_location).await?
|
||||
let Some(current_metadata) = read_table_metadata_value(object_backend, table_bucket, &entry.metadata_location).await?
|
||||
else {
|
||||
return Err(TableCatalogStoreError::NotFound(format!(
|
||||
"current metadata object {}",
|
||||
@@ -2533,13 +2666,13 @@ where
|
||||
)));
|
||||
};
|
||||
let mut report =
|
||||
table_compaction_planning_report(&self.backend, table_bucket, &namespace, &table, &entry, ¤t_metadata, config)
|
||||
table_compaction_planning_report(object_backend, table_bucket, &namespace, &table, &entry, ¤t_metadata, config)
|
||||
.await?;
|
||||
if report.status != TableCompactionPlanningStatus::RewriteCandidates {
|
||||
return Err(TableCatalogStoreError::Invalid("compaction has no safe rewrite candidates".to_string()));
|
||||
}
|
||||
let current_data_files =
|
||||
compaction_current_data_files(&self.backend, table_bucket, &namespace, &table, &entry, ¤t_metadata).await?;
|
||||
compaction_current_data_files(object_backend, table_bucket, &namespace, &table, &entry, ¤t_metadata).await?;
|
||||
let current_data_files_by_key = current_data_files
|
||||
.iter()
|
||||
.map(|file| (file.object_key.as_str(), file))
|
||||
@@ -2574,14 +2707,14 @@ where
|
||||
let sort_order_id = compaction_rewrite_group_sort_order(¤t_data_files_by_key, rewrite_group)?;
|
||||
let mut input_files = Vec::with_capacity(rewrite_group.input_file_locations.len());
|
||||
for input_file in &rewrite_group.input_file_locations {
|
||||
let Some(input_object) = self.backend.read_object(table_bucket, input_file).await? else {
|
||||
let Some(input_object) = object_backend.read_object(table_bucket, input_file).await? else {
|
||||
return Err(TableCatalogStoreError::NotFound(format!("compaction input data file {input_file}")));
|
||||
};
|
||||
input_files.push((input_file.clone(), input_object.data));
|
||||
}
|
||||
let compacted_file = compact_parquet_data_files(&input_files)?;
|
||||
let output_bytes = u64::try_from(compacted_file.data.len()).unwrap_or(u64::MAX);
|
||||
self.backend
|
||||
object_backend
|
||||
.put_object(table_bucket, &output_file, compacted_file.data, TableCatalogPutPrecondition::IfAbsent)
|
||||
.await?;
|
||||
rewrite_group.output_file_location = Some(output_file_path.clone());
|
||||
@@ -2608,7 +2741,7 @@ where
|
||||
default_table_metadata_file_path(&namespace, &table, &format!("compaction-{compaction_id}.metadata.json"));
|
||||
let manifest_data = compacted_manifest_avro_bytes(&manifest_data_files)?;
|
||||
let manifest_length = u64::try_from(manifest_data.len()).unwrap_or(u64::MAX);
|
||||
self.backend
|
||||
object_backend
|
||||
.put_object(table_bucket, &new_manifest, manifest_data, TableCatalogPutPrecondition::IfAbsent)
|
||||
.await?;
|
||||
let added_files_count = compacted_files.len();
|
||||
@@ -2631,7 +2764,7 @@ where
|
||||
added_rows_count,
|
||||
existing_rows_count,
|
||||
})?;
|
||||
self.backend
|
||||
object_backend
|
||||
.put_object(
|
||||
table_bucket,
|
||||
&new_manifest_list,
|
||||
@@ -2648,24 +2781,27 @@ where
|
||||
&entry.metadata_location,
|
||||
now,
|
||||
)?;
|
||||
self.backend
|
||||
object_backend
|
||||
.put_object(table_bucket, &new_metadata, new_metadata_data, TableCatalogPutPrecondition::IfAbsent)
|
||||
.await?;
|
||||
|
||||
let commit_result = self
|
||||
.commit_table(TableCommitRequest {
|
||||
table_bucket: table_bucket.to_string(),
|
||||
namespace: namespace.public_name(),
|
||||
table: table.as_str().to_string(),
|
||||
commit_id: format!("compaction-{compaction_id}"),
|
||||
idempotency_key: Some(format!("compaction-{compaction_id}")),
|
||||
operation: "compaction".to_string(),
|
||||
expected_version_token: entry.version_token,
|
||||
expected_metadata_location: entry.metadata_location,
|
||||
new_metadata_location: new_metadata.clone(),
|
||||
requirements: Vec::new(),
|
||||
writer: Some("rustfs-maintenance".to_string()),
|
||||
})
|
||||
.commit_table_with_publication(
|
||||
TableCommitRequest {
|
||||
table_bucket: table_bucket.to_string(),
|
||||
namespace: namespace.public_name(),
|
||||
table: table.as_str().to_string(),
|
||||
commit_id: format!("compaction-{compaction_id}"),
|
||||
idempotency_key: Some(format!("compaction-{compaction_id}")),
|
||||
operation: "compaction".to_string(),
|
||||
expected_version_token: entry.version_token,
|
||||
expected_metadata_location: entry.metadata_location,
|
||||
new_metadata_location: new_metadata.clone(),
|
||||
requirements: Vec::new(),
|
||||
writer: Some("rustfs-maintenance".to_string()),
|
||||
},
|
||||
publication,
|
||||
)
|
||||
.await?;
|
||||
|
||||
report.status = TableCompactionPlanningStatus::Committed;
|
||||
@@ -3245,6 +3381,8 @@ where
|
||||
|
||||
let table_path = self.paths.table_entry_path(table_bucket, &namespace, &table);
|
||||
let _guard = self.backend.acquire_write_lock(self.catalog_bucket(), &table_path).await?;
|
||||
let publication_lock = default_table_publication_lock_path(&namespace, &table);
|
||||
let _publication_guard = self.backend.acquire_write_lock(table_bucket, &publication_lock).await?;
|
||||
let Some((entry, _)) = self.read_table_with_etag_unlocked(table_bucket, &namespace, &table).await? else {
|
||||
return Err(TableCatalogStoreError::NotFound(format!(
|
||||
"table {}/{}/{}",
|
||||
@@ -3635,6 +3773,15 @@ where
|
||||
self.write_table_entry(entry, TableCatalogPutPrecondition::IfAbsent).await
|
||||
}
|
||||
|
||||
async fn register_table_with_publication(
|
||||
&self,
|
||||
entry: TableEntry,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
self.write_table_entry_with_publication(entry, TableCatalogPutPrecondition::IfAbsent, publication)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_tables(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<TableEntry>> {
|
||||
let namespace = parse_namespace_for_store(namespace)?;
|
||||
let mut entries = Vec::new();
|
||||
@@ -3722,6 +3869,7 @@ where
|
||||
|
||||
match self.backfill_table_warehouse_index(table_bucket).await {
|
||||
Ok(()) => self.resolve_table_data_plane_resource_from_index(table_bucket, object).await,
|
||||
Err(err @ TableCatalogStoreError::Conflict(_)) => Err(err),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
table_bucket = %table_bucket,
|
||||
@@ -3734,6 +3882,16 @@ where
|
||||
}
|
||||
|
||||
async fn commit_table(&self, request: TableCommitRequest) -> TableCatalogStoreResult<TableCommitResult> {
|
||||
let publication = TableCommitLockPublication::new(&self.backend);
|
||||
publication.begin_table_bucket(&request.table_bucket).await?;
|
||||
self.commit_table_with_publication(request, &publication).await
|
||||
}
|
||||
|
||||
async fn commit_table_with_publication(
|
||||
&self,
|
||||
request: TableCommitRequest,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<TableCommitResult> {
|
||||
let commit_started = Instant::now();
|
||||
record_table_commit_attempt(&request.operation);
|
||||
let namespace = parse_namespace_for_store(&request.namespace)?;
|
||||
@@ -3741,6 +3899,16 @@ where
|
||||
let _migration_guard = self.acquire_object_backed_catalog_write_permit(&request.table_bucket).await?;
|
||||
let table_path = self.paths.table_entry_path(&request.table_bucket, &namespace, &table);
|
||||
let _guard = self.backend.acquire_write_lock(self.catalog_bucket(), &table_path).await?;
|
||||
// Preserve catalog -> publication -> object lock order across rolling upgrades.
|
||||
publication
|
||||
.prepare(&request.table_bucket, &request.namespace, &request.table)
|
||||
.await?;
|
||||
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.table) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table commit requires a table publication fence".to_string(),
|
||||
));
|
||||
}
|
||||
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||
|
||||
let Some((current, current_etag)) = self
|
||||
.read_table_with_etag_unlocked(&request.table_bucket, &namespace, &table)
|
||||
@@ -3773,6 +3941,48 @@ where
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let (Some(existing), Some(indexed)) = (&existing_commit, &existing_idempotency_commit)
|
||||
&& !commit_logs_share_recovery_payload(existing, indexed)
|
||||
{
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict(
|
||||
"commit record and idempotency index contain different payloads".to_string(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
if let Some(existing) = existing_idempotency_commit.as_ref()
|
||||
&& !commit_log_matches_request(existing, &request, ¤t.table_id)
|
||||
{
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict("idempotency key already exists".to_string())),
|
||||
);
|
||||
}
|
||||
if existing_commit.is_none() && existing_idempotency_commit.is_some() {
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict(
|
||||
"idempotency key exists without a recoverable commit record".to_string(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(existing) = existing_commit.as_ref() {
|
||||
if !commit_log_matches_request(existing, &request, ¤t.table_id) {
|
||||
return table_commit_result(
|
||||
@@ -3788,7 +3998,48 @@ where
|
||||
))),
|
||||
);
|
||||
}
|
||||
if matches!(existing.status, CommitLogStatus::Committed) || table_matches_committed_log(¤t, existing) {
|
||||
if matches!(existing.status, CommitLogStatus::Failed) {
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict("failed commit record cannot be replayed".to_string())),
|
||||
);
|
||||
}
|
||||
if matches!(existing.status, CommitLogStatus::Committed) && table_matches_staged_base(¤t, existing) {
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict(
|
||||
"committed record still matches the pre-commit table state".to_string(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
let historically_committed = if matches!(existing.status, CommitLogStatus::Staged)
|
||||
&& !table_matches_staged_base(¤t, existing)
|
||||
&& !table_matches_committed_log(¤t, existing)
|
||||
{
|
||||
let commit_logs = self
|
||||
.read_table_commit_logs(¤t)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(_, commit_log)| commit_log)
|
||||
.collect::<Vec<_>>();
|
||||
TableCommitHistoryIndex::new(¤t, commit_logs.iter()).proves_committed(existing)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if matches!(existing.status, CommitLogStatus::Committed)
|
||||
|| (matches!(existing.status, CommitLogStatus::Staged)
|
||||
&& (table_matches_committed_log(¤t, existing) || historically_committed))
|
||||
{
|
||||
let mut committed = existing.clone();
|
||||
committed.status = CommitLogStatus::Committed;
|
||||
let _ = self
|
||||
@@ -3821,32 +4072,6 @@ where
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(existing) = existing_idempotency_commit.as_ref()
|
||||
&& !commit_log_matches_request(existing, &request, ¤t.table_id)
|
||||
{
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict("idempotency key already exists".to_string())),
|
||||
);
|
||||
}
|
||||
if existing_commit.is_none() && existing_idempotency_commit.is_some() {
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Conflict(
|
||||
"idempotency key exists without a recoverable commit record".to_string(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
if current.version_token != request.expected_version_token {
|
||||
return table_commit_result(
|
||||
@@ -3905,6 +4130,7 @@ where
|
||||
))),
|
||||
);
|
||||
};
|
||||
validate_commit_metadata_digest(&request, &new_metadata_object)?;
|
||||
let table_bucket = request.table_bucket.clone();
|
||||
let metadata_location = request.new_metadata_location.clone();
|
||||
let next_warehouse_location = tokio::task::spawn_blocking(move || {
|
||||
@@ -3912,6 +4138,23 @@ where
|
||||
})
|
||||
.await
|
||||
.map_err(|err| TableCatalogStoreError::Internal(format!("table metadata parser task failed: {err}")))??;
|
||||
if next_warehouse_location
|
||||
.as_ref()
|
||||
.is_some_and(|warehouse_location| warehouse_location != ¤t.warehouse_location)
|
||||
&& !publication.holds_table_bucket(&request.table_bucket)
|
||||
{
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Internal(
|
||||
"table warehouse relocation requires a table-bucket publication fence".to_string(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let has_existing_commit = existing_commit.is_some();
|
||||
let mut staged_commit_log = existing_commit.unwrap_or_else(|| CommitLogEntry {
|
||||
@@ -4024,6 +4267,8 @@ where
|
||||
}
|
||||
|
||||
async fn drop_table(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()> {
|
||||
let publication = TableCommitLockPublication::new(&self.backend);
|
||||
publication.begin_table_bucket(table_bucket).await?;
|
||||
let namespace = parse_namespace_for_store(namespace)?;
|
||||
let table = parse_table_for_store(table)?;
|
||||
let _migration_guard = self.acquire_object_backed_catalog_write_permit(table_bucket).await?;
|
||||
@@ -4034,6 +4279,17 @@ where
|
||||
.await?;
|
||||
let object = self.paths.table_entry_path(table_bucket, &namespace, &table);
|
||||
let _guard = self.backend.acquire_write_lock(self.catalog_bucket(), &object).await?;
|
||||
publication
|
||||
.prepare(table_bucket, &namespace.public_name(), table.as_str())
|
||||
.await?;
|
||||
if !publication.holds_table_bucket(table_bucket)
|
||||
|| !publication.holds_table(table_bucket, &namespace.public_name(), table.as_str())
|
||||
{
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table drop requires table-bucket and table publication fences".to_string(),
|
||||
));
|
||||
}
|
||||
let _publication_completion = TableCommitPublicationCompletion::new(&publication);
|
||||
let Some((entry, _)) = self.read_table_with_etag_unlocked(table_bucket, &namespace, &table).await? else {
|
||||
return Err(TableCatalogStoreError::NotFound(format!(
|
||||
"table {}/{}/{}",
|
||||
|
||||
@@ -443,16 +443,20 @@ where
|
||||
continue;
|
||||
};
|
||||
let table_key = (table_bucket.clone(), namespace.clone(), table.clone());
|
||||
if let Some(existing_key) = warehouse_index
|
||||
.entry(table_bucket.clone())
|
||||
.or_default()
|
||||
.insert(warehouse_object_prefix.clone(), table_key.clone())
|
||||
let bucket_index = warehouse_index.entry(table_bucket.clone()).or_default();
|
||||
let predecessor = bucket_index.range(..=warehouse_object_prefix.clone()).next_back();
|
||||
let successor = bucket_index.range(warehouse_object_prefix.clone()..).next();
|
||||
if let Some((existing_prefix, existing_key)) = predecessor
|
||||
.into_iter()
|
||||
.chain(successor)
|
||||
.find(|(existing_prefix, _)| warehouse_object_prefixes_overlap(existing_prefix, &warehouse_object_prefix))
|
||||
{
|
||||
return Err(TableCatalogStoreError::Invalid(format!(
|
||||
"duplicate active table warehouse location in strong catalog snapshot: {warehouse_object_prefix} is owned by {}/{}/{} and {}/{}/{}",
|
||||
"overlapping active table warehouse location in strong catalog snapshot: {warehouse_object_prefix} overlaps {existing_prefix} owned by {}/{}/{} and {}/{}/{}",
|
||||
existing_key.0, existing_key.1, existing_key.2, table_key.0, table_key.1, table_key.2
|
||||
)));
|
||||
}
|
||||
bucket_index.insert(warehouse_object_prefix, table_key);
|
||||
}
|
||||
state.warehouse_index = warehouse_index;
|
||||
Ok(())
|
||||
@@ -696,9 +700,9 @@ where
|
||||
let Ok(existing_prefix) = table_warehouse_object_prefix(existing) else {
|
||||
continue;
|
||||
};
|
||||
if existing_prefix == candidate_prefix {
|
||||
if warehouse_object_prefixes_overlap(&existing_prefix, &candidate_prefix) {
|
||||
return Err(TableCatalogStoreError::Conflict(format!(
|
||||
"table warehouse location is already registered: {candidate_prefix}"
|
||||
"table warehouse location overlaps an active table: {candidate_prefix}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -709,6 +713,14 @@ where
|
||||
state: &StrongTableCatalogState,
|
||||
entry: &TableEntry,
|
||||
) -> TableCommitRecoveryReport {
|
||||
let history = TableCommitHistoryIndex::new(
|
||||
entry,
|
||||
state
|
||||
.commits
|
||||
.iter()
|
||||
.filter(|((table_bucket, table_id, _), _)| table_bucket == &entry.table_bucket && table_id == &entry.table_id)
|
||||
.map(|(_, commit_log)| commit_log),
|
||||
);
|
||||
let mut commits = state
|
||||
.commits
|
||||
.iter()
|
||||
@@ -719,7 +731,7 @@ where
|
||||
.idempotency
|
||||
.get(&Self::idempotency_key(&entry.table_bucket, &entry.table_id, idempotency_key))
|
||||
});
|
||||
table_commit_recovery_entry(entry, commit_log, idempotency_commit)
|
||||
table_commit_recovery_entry(entry, commit_log, idempotency_commit, history.proves_committed(commit_log))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
commits.sort_by(|left, right| left.commit_id.cmp(&right.commit_id));
|
||||
@@ -783,18 +795,11 @@ where
|
||||
.map(|idempotency_key| Self::idempotency_key(&request.table_bucket, ¤t.table_id, idempotency_key));
|
||||
let existing_idempotency_commit = idempotency_key.as_ref().and_then(|key| state.idempotency.get(key));
|
||||
|
||||
if let Some(existing) = existing_commit {
|
||||
if !commit_log_matches_request(existing, request, ¤t.table_id) {
|
||||
return Err(TableCatalogStoreError::Conflict(format!(
|
||||
"commit id already exists: {}",
|
||||
request.commit_id
|
||||
)));
|
||||
}
|
||||
if matches!(existing.status, CommitLogStatus::Committed) || table_matches_committed_log(¤t, existing) {
|
||||
return Ok(current);
|
||||
}
|
||||
if let (Some(existing), Some(indexed)) = (existing_commit, existing_idempotency_commit)
|
||||
&& !commit_logs_share_recovery_payload(existing, indexed)
|
||||
{
|
||||
return Err(TableCatalogStoreError::Conflict(
|
||||
"existing commit record does not match current table state".to_string(),
|
||||
"commit record and idempotency index contain different payloads".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(existing) = existing_idempotency_commit
|
||||
@@ -802,11 +807,55 @@ where
|
||||
{
|
||||
return Err(TableCatalogStoreError::Conflict("idempotency key already exists".to_string()));
|
||||
}
|
||||
if existing_idempotency_commit.is_some() {
|
||||
if existing_commit.is_none() && existing_idempotency_commit.is_some() {
|
||||
return Err(TableCatalogStoreError::Conflict(
|
||||
"idempotency key exists without a recoverable commit record".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(existing) = existing_commit {
|
||||
if !commit_log_matches_request(existing, request, ¤t.table_id) {
|
||||
return Err(TableCatalogStoreError::Conflict(format!(
|
||||
"commit id already exists: {}",
|
||||
request.commit_id
|
||||
)));
|
||||
}
|
||||
if matches!(existing.status, CommitLogStatus::Failed) {
|
||||
return Err(TableCatalogStoreError::Conflict("failed commit record cannot be replayed".to_string()));
|
||||
}
|
||||
if matches!(existing.status, CommitLogStatus::Committed) && table_matches_staged_base(¤t, existing) {
|
||||
return Err(TableCatalogStoreError::Conflict(
|
||||
"committed record still matches the pre-commit table state".to_string(),
|
||||
));
|
||||
}
|
||||
let historically_committed = if matches!(existing.status, CommitLogStatus::Staged)
|
||||
&& !table_matches_staged_base(¤t, existing)
|
||||
&& !table_matches_committed_log(¤t, existing)
|
||||
{
|
||||
TableCommitHistoryIndex::new(
|
||||
¤t,
|
||||
state
|
||||
.commits
|
||||
.iter()
|
||||
.filter(|((table_bucket, table_id, _), _)| {
|
||||
table_bucket == &request.table_bucket && table_id == ¤t.table_id
|
||||
})
|
||||
.map(|(_, commit_log)| commit_log),
|
||||
)
|
||||
.proves_committed(existing)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if matches!(existing.status, CommitLogStatus::Committed)
|
||||
|| (matches!(existing.status, CommitLogStatus::Staged)
|
||||
&& (table_matches_committed_log(¤t, existing) || historically_committed))
|
||||
{
|
||||
return Ok(current);
|
||||
}
|
||||
return Err(TableCatalogStoreError::Conflict(
|
||||
"existing commit record does not match current table state".to_string(),
|
||||
));
|
||||
}
|
||||
if current.version_token != request.expected_version_token {
|
||||
return Err(TableCatalogStoreError::Conflict(
|
||||
"current table version token does not match expected token".to_string(),
|
||||
@@ -831,15 +880,38 @@ where
|
||||
current: TableEntry,
|
||||
) -> Option<TableCommitResult> {
|
||||
let commit_key = Self::commit_key(&request.table_bucket, ¤t.table_id, &request.commit_id);
|
||||
let existing = state.commits.get(&commit_key)?;
|
||||
if !commit_log_matches_request(existing, request, ¤t.table_id) {
|
||||
let existing = state.commits.get(&commit_key)?.clone();
|
||||
if !commit_log_matches_request(&existing, request, ¤t.table_id) {
|
||||
return None;
|
||||
}
|
||||
if !matches!(existing.status, CommitLogStatus::Committed) && !table_matches_committed_log(¤t, existing) {
|
||||
let historically_committed = if matches!(existing.status, CommitLogStatus::Staged)
|
||||
&& !table_matches_staged_base(¤t, &existing)
|
||||
&& !table_matches_committed_log(¤t, &existing)
|
||||
{
|
||||
TableCommitHistoryIndex::new(
|
||||
¤t,
|
||||
state
|
||||
.commits
|
||||
.iter()
|
||||
.filter(|((table_bucket, table_id, _), _)| {
|
||||
table_bucket == &request.table_bucket && table_id == ¤t.table_id
|
||||
})
|
||||
.map(|(_, commit_log)| commit_log),
|
||||
)
|
||||
.proves_committed(&existing)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if matches!(existing.status, CommitLogStatus::Failed)
|
||||
|| (matches!(existing.status, CommitLogStatus::Committed) && table_matches_staged_base(¤t, &existing))
|
||||
|| (!matches!(existing.status, CommitLogStatus::Committed)
|
||||
&& !table_matches_committed_log(¤t, &existing)
|
||||
&& !historically_committed)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut committed = existing.clone();
|
||||
let mut committed = existing;
|
||||
committed.status = CommitLogStatus::Committed;
|
||||
state.commits.insert(commit_key, committed.clone());
|
||||
if let Some(idempotency_key) = committed.idempotency_key.as_deref() {
|
||||
@@ -1202,12 +1274,36 @@ where
|
||||
}
|
||||
|
||||
async fn register_table(&self, entry: TableEntry) -> TableCatalogStoreResult<()> {
|
||||
let _write_guard = self.write_lock.lock().await;
|
||||
self.hydrate_state().await?;
|
||||
let publication = TableCommitLockPublication::new(&self.object_backend);
|
||||
self.register_table_with_publication(entry, &publication).await
|
||||
}
|
||||
|
||||
async fn register_table_with_publication(
|
||||
&self,
|
||||
entry: TableEntry,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<()> {
|
||||
validate_catalog_entry_version("table", entry.version)?;
|
||||
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
||||
let table = parse_table_for_store(&entry.table)?;
|
||||
table_warehouse_object_prefix(&entry)?;
|
||||
publication.begin_table_bucket(&entry.table_bucket).await?;
|
||||
if !publication.holds_table_bucket(&entry.table_bucket) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table registration requires a table-bucket publication fence".to_string(),
|
||||
));
|
||||
}
|
||||
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||
publication
|
||||
.prepare(&entry.table_bucket, &entry.namespace, &entry.table)
|
||||
.await?;
|
||||
if !publication.holds_table(&entry.table_bucket, &entry.namespace, &entry.table) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table registration requires a table publication fence".to_string(),
|
||||
));
|
||||
}
|
||||
let _write_guard = self.write_lock.lock().await;
|
||||
self.hydrate_state().await?;
|
||||
let key = Self::table_key(&entry.table_bucket, &namespace, &table);
|
||||
let (snapshot, precondition) = {
|
||||
let state = self.state.lock().await;
|
||||
@@ -1328,12 +1424,31 @@ where
|
||||
}
|
||||
|
||||
async fn commit_table(&self, request: TableCommitRequest) -> TableCatalogStoreResult<TableCommitResult> {
|
||||
let _write_guard = self.write_lock.lock().await;
|
||||
self.hydrate_state().await?;
|
||||
let publication = TableCommitLockPublication::new(&self.object_backend);
|
||||
publication.begin_table_bucket(&request.table_bucket).await?;
|
||||
self.commit_table_with_publication(request, &publication).await
|
||||
}
|
||||
|
||||
async fn commit_table_with_publication(
|
||||
&self,
|
||||
request: TableCommitRequest,
|
||||
publication: &(dyn TableCommitPublication + Sync),
|
||||
) -> TableCatalogStoreResult<TableCommitResult> {
|
||||
let commit_started = Instant::now();
|
||||
record_table_commit_attempt(&request.operation);
|
||||
let namespace = parse_namespace_for_store(&request.namespace)?;
|
||||
let table = parse_table_for_store(&request.table)?;
|
||||
publication
|
||||
.prepare(&request.table_bucket, &request.namespace, &request.table)
|
||||
.await?;
|
||||
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.table) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table commit requires a table publication fence".to_string(),
|
||||
));
|
||||
}
|
||||
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||
let _write_guard = self.write_lock.lock().await;
|
||||
self.hydrate_state().await?;
|
||||
let key = Self::table_key(&request.table_bucket, &namespace, &table);
|
||||
|
||||
let committed_existing_result = {
|
||||
@@ -1396,6 +1511,7 @@ where
|
||||
))),
|
||||
);
|
||||
};
|
||||
validate_commit_metadata_digest(&request, &new_metadata_object)?;
|
||||
let table_bucket = request.table_bucket.clone();
|
||||
let metadata_location = request.new_metadata_location.clone();
|
||||
let next_warehouse_location = tokio::task::spawn_blocking(move || {
|
||||
@@ -1403,6 +1519,36 @@ where
|
||||
})
|
||||
.await
|
||||
.map_err(|err| TableCatalogStoreError::Internal(format!("table metadata parser task failed: {err}")))??;
|
||||
let current_warehouse_location = {
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.tables
|
||||
.get(&key)
|
||||
.map(|entry| entry.warehouse_location.clone())
|
||||
.ok_or_else(|| {
|
||||
TableCatalogStoreError::NotFound(format!(
|
||||
"table {}/{}/{}",
|
||||
request.table_bucket, request.namespace, request.table
|
||||
))
|
||||
})?
|
||||
};
|
||||
if next_warehouse_location
|
||||
.as_ref()
|
||||
.is_some_and(|warehouse_location| warehouse_location != ¤t_warehouse_location)
|
||||
&& !publication.holds_table_bucket(&request.table_bucket)
|
||||
{
|
||||
return table_commit_result(
|
||||
&request.table_bucket,
|
||||
&request.namespace,
|
||||
&request.table,
|
||||
&request.commit_id,
|
||||
&request.operation,
|
||||
commit_started,
|
||||
Err(TableCatalogStoreError::Internal(
|
||||
"table warehouse relocation requires a table-bucket publication fence".to_string(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let cas_started = Instant::now();
|
||||
let prepared_result = {
|
||||
@@ -1433,6 +1579,15 @@ where
|
||||
}
|
||||
|
||||
async fn drop_table(&self, table_bucket: &str, namespace: &str, table: &str) -> TableCatalogStoreResult<()> {
|
||||
let publication = TableCommitLockPublication::new(&self.object_backend);
|
||||
publication.begin_table_bucket(table_bucket).await?;
|
||||
publication.prepare(table_bucket, namespace, table).await?;
|
||||
if !publication.holds_table_bucket(table_bucket) || !publication.holds_table(table_bucket, namespace, table) {
|
||||
return Err(TableCatalogStoreError::Internal(
|
||||
"table drop requires table-bucket and table publication fences".to_string(),
|
||||
));
|
||||
}
|
||||
let _publication_completion = TableCommitPublicationCompletion::new(&publication);
|
||||
let _write_guard = self.write_lock.lock().await;
|
||||
self.hydrate_state().await?;
|
||||
let namespace = parse_namespace_for_store(namespace)?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user