refactor(table-catalog): modularize catalog implementation (#5678)

* refactor(table-catalog): split catalog foundations

* refactor(table-catalog): split REST handler modules

* refactor(table-catalog): split domain and store modules

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
This commit is contained in:
Henry Guo
2026-08-04 11:48:00 +08:00
committed by GitHub
parent 71f2e7a209
commit d6e11cf018
31 changed files with 34291 additions and 33836 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,188 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct GetCatalogConfigHandler {}
#[async_trait::async_trait]
impl Operation for GetCatalogConfigHandler {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
authorize_table_catalog_request(&req, AdminAction::GetTableCatalogAction).await?;
let warehouse = warehouse_from_config_query(&req.uri)?;
build_json_response(StatusCode::OK, &catalog_config_response(warehouse.as_deref())?)
}
}
pub struct EnableTableBucketHandler {}
#[async_trait::async_trait]
impl Operation for EnableTableBucketHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
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?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct GetTableBucketHandler {}
#[async_trait::async_trait]
impl Operation for GetTableBucketHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
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 response = table_bucket_response(&store, &warehouse, enabled).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct GetTableCatalogMigrationHandler {}
#[async_trait::async_trait]
impl Operation for GetTableCatalogMigrationHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
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()?;
let started = Instant::now();
let result = store
.plan_durable_strong_backing_migration(&warehouse)
.await
.map_err(catalog_store_error);
record_table_catalog_admin_operation_result("migration", &warehouse, "", "", started, &result);
let response = result?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct MaterializeTableCatalogMigrationHandler {}
#[async_trait::async_trait]
impl Operation for MaterializeTableCatalogMigrationHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
authorize_table_catalog_request(&req, AdminAction::MigrateTableCatalogAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let store = table_catalog_object_store()?;
let started = Instant::now();
let result = store
.materialize_durable_strong_backing_migration(&warehouse)
.await
.map_err(catalog_store_error);
record_table_catalog_admin_operation_result("migration-materialize", &warehouse, "", "", started, &result);
let response = result?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct CancelTableCatalogMigrationHandler {}
#[async_trait::async_trait]
impl Operation for CancelTableCatalogMigrationHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
authorize_table_catalog_request(&req, AdminAction::MigrateTableCatalogAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let store = table_catalog_object_store()?;
let started = Instant::now();
let result = store
.cancel_durable_strong_backing_migration(&warehouse)
.await
.map_err(catalog_store_error);
record_table_catalog_admin_operation_result("migration-cancel", &warehouse, "", "", started, &result);
let response = result?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct ExternalCatalogBridgeHandler {}
#[async_trait::async_trait]
impl Operation for ExternalCatalogBridgeHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let response = external_catalog_bridge_response(&store, &warehouse, &namespace, &table).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct PutExternalCatalogBridgeHandler {}
#[async_trait::async_trait]
impl Operation for PutExternalCatalogBridgeHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RegisterTableAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<ExternalCatalogBridgeRequest>(req.input).await?;
let store = table_catalog_object_store()?;
let response = put_external_catalog_bridge_response(&store, &warehouse, &namespace, &table, request).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct SyncExternalCatalogBridgeHandler {}
#[async_trait::async_trait]
impl Operation for SyncExternalCatalogBridgeHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
if store
.load_table(&warehouse, &namespace.public_name(), &table)
.await
.map_err(catalog_store_error)?
.is_none()
{
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(
&store,
&metadata_backend,
&warehouse,
&namespace,
&table,
request,
table_bucket_enabled,
)
.await?;
build_json_response(StatusCode::OK, &response)
}
}
@@ -0,0 +1,34 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct RestLoadCredentialsHandler {}
#[async_trait::async_trait]
impl Operation for RestLoadCredentialsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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?;
build_json_response(StatusCode::OK, &response)
}
}
@@ -0,0 +1,223 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct RestTableMetadataMaintenanceHandler {}
#[async_trait::async_trait]
impl Operation for RestTableMetadataMaintenanceHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<TableMetadataMaintenanceRequest>(req.input).await?;
let metadata_backend = table_catalog_backend()?;
let store = table_catalog_object_store()?;
let response =
table_metadata_maintenance_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct GetTableMaintenanceConfigHandler {}
#[async_trait::async_trait]
impl Operation for GetTableMaintenanceConfigHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let response = store
.get_table_maintenance_config(&warehouse, &namespace.public_name(), &table)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct PutTableMaintenanceConfigHandler {}
#[async_trait::async_trait]
impl Operation for PutTableMaintenanceConfigHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableLifecycleAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<crate::table_catalog::TableMaintenanceConfig>(req.input).await?;
let store = table_catalog_store()?;
let response = store
.put_table_maintenance_config(&warehouse, &namespace.public_name(), &table, request)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct GetTableMaintenanceJobHandler {}
#[async_trait::async_trait]
impl Operation for GetTableMaintenanceJobHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let job = job_id_from_params(&params)?;
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()?;
let Some(response) = store
.get_table_metadata_maintenance_report(&warehouse, &namespace.public_name(), &table, &job)
.await
.map_err(catalog_store_error)?
else {
return Err(s3_error!(InvalidRequest, "maintenance job not found"));
};
build_json_response(StatusCode::OK, &response)
}
}
pub struct GetTableMaintenanceSchedulerHandler {}
#[async_trait::async_trait]
impl Operation for GetTableMaintenanceSchedulerHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let response = store
.get_table_maintenance_scheduler_report(&warehouse, &namespace.public_name(), &table)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RunTableMaintenanceWorkerHandler {}
#[async_trait::async_trait]
impl Operation for RunTableMaintenanceSchedulerHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body_or_default::<TableMaintenanceSchedulerRunRequest>(req.input).await?;
let store = table_catalog_store()?;
let response = store
.run_table_maintenance_scheduler_once(
&warehouse,
&namespace.public_name(),
&table,
request.scheduler_id().to_string(),
)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RunTableMaintenanceSchedulerHandler {}
#[async_trait::async_trait]
impl Operation for RunTableMaintenanceWorkerHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<TableMaintenanceWorkerRunRequest>(req.input).await?;
let store = table_catalog_store()?;
let response = store
.run_table_metadata_maintenance_worker_once(
&warehouse,
&namespace.public_name(),
&table,
request.worker_id().to_string(),
)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct HeartbeatTableMaintenanceJobHandler {}
#[async_trait::async_trait]
impl Operation for HeartbeatTableMaintenanceJobHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let job = job_id_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<TableMaintenanceHeartbeatRequest>(req.input).await?;
let store = table_catalog_store()?;
let response = store
.heartbeat_table_metadata_maintenance_job(
&warehouse,
&namespace.public_name(),
&table,
&job,
&request.lease_id,
&request.worker_id,
)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct TableMaintenanceQuarantineHandler {}
#[async_trait::async_trait]
impl Operation for TableMaintenanceQuarantineHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let job = job_id_from_params(&params)?;
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::RunTableMaintenanceAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<crate::table_catalog::TableMaintenanceQuarantineOperationRequest>(req.input).await?;
let store = table_catalog_store()?;
let response = store
.apply_table_maintenance_quarantine_operation(&warehouse, &namespace.public_name(), &table, &job, request)
.await
.map_err(catalog_store_error)?;
build_json_response(StatusCode::OK, &response)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct RestListNamespacesHandler {}
#[async_trait::async_trait]
impl Operation for RestListNamespacesHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let resource = TableCatalogResource::warehouse(&warehouse);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::GetTableNamespaceAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let store = table_catalog_store()?;
let response = list_namespaces_response(&store, &warehouse, &req.uri).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestCreateNamespaceHandler {}
#[async_trait::async_trait]
impl Operation for RestCreateNamespaceHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let resource = TableCatalogResource::warehouse(&warehouse);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::SetTableNamespaceAction).await?;
let request = read_json_body::<CreateNamespaceRequest>(req.input).await?;
let store = table_catalog_store()?;
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
let response = create_namespace_response(&store, &warehouse, request, table_bucket_enabled).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestGetNamespaceHandler {}
#[async_trait::async_trait]
impl Operation for RestGetNamespaceHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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()?;
let response = get_namespace_response(&store, &warehouse, &namespace).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestDropNamespaceHandler {}
#[async_trait::async_trait]
impl Operation for RestDropNamespaceHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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()?;
drop_namespace_in_store(&store, &warehouse, &namespace.public_name()).await?;
Ok(empty_response(StatusCode::NO_CONTENT))
}
}
pub struct RestNamespaceExistsHandler {}
#[async_trait::async_trait]
impl Operation for RestNamespaceExistsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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()?;
Ok(empty_response(namespace_exists_status(&store, &warehouse, &namespace).await?))
}
}
@@ -0,0 +1,75 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct ListTableRefsHandler {}
#[async_trait::async_trait]
impl Operation for ListTableRefsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
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)
}
}
pub struct PutTableRefHandler {}
#[async_trait::async_trait]
impl Operation for PutTableRefHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let ref_name = ref_name_from_params(&params)?;
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 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?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct DeleteTableRefHandler {}
#[async_trait::async_trait]
impl Operation for DeleteTableRefHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
let ref_name = ref_name_from_params(&params)?;
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 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?;
build_json_response(StatusCode::OK, &response)
}
}
@@ -0,0 +1,259 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub fn register_table_catalog_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
for prefix in [TABLE_CATALOG_PREFIX, TABLE_CATALOG_COMPAT_PREFIX] {
register_table_catalog_prefix_routes(r, prefix)?;
}
Ok(())
}
fn register_table_catalog_prefix_routes(r: &mut S3Router<AdminOperation>, prefix: &str) -> std::io::Result<()> {
r.insert(Method::GET, format!("{prefix}/config").as_str(), AdminOperation(&GET_CONFIG_HANDLER))?;
r.insert(
Method::PUT,
format!("{prefix}/buckets/{{warehouse}}").as_str(),
AdminOperation(&ENABLE_TABLE_BUCKET_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/buckets/{{warehouse}}").as_str(),
AdminOperation(&GET_TABLE_BUCKET_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/catalog/migration").as_str(),
AdminOperation(&GET_TABLE_CATALOG_MIGRATION_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/catalog/migration").as_str(),
AdminOperation(&MATERIALIZE_TABLE_CATALOG_MIGRATION_HANDLER),
)?;
r.insert(
Method::DELETE,
format!("{prefix}/{{warehouse}}/catalog/migration").as_str(),
AdminOperation(&CANCEL_TABLE_CATALOG_MIGRATION_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces").as_str(),
AdminOperation(&LIST_NAMESPACES_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces").as_str(),
AdminOperation(&CREATE_NAMESPACE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}").as_str(),
AdminOperation(&GET_NAMESPACE_HANDLER),
)?;
r.insert(
Method::HEAD,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}").as_str(),
AdminOperation(&NAMESPACE_EXISTS_HANDLER),
)?;
r.insert(
Method::DELETE,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}").as_str(),
AdminOperation(&DROP_NAMESPACE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables").as_str(),
AdminOperation(&LIST_TABLES_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables").as_str(),
AdminOperation(&CREATE_TABLE_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/register").as_str(),
AdminOperation(&REGISTER_TABLE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/views").as_str(),
AdminOperation(&LIST_VIEWS_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/views").as_str(),
AdminOperation(&CREATE_VIEW_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}").as_str(),
AdminOperation(&LOAD_TABLE_HANDLER),
)?;
r.insert(
Method::HEAD,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}").as_str(),
AdminOperation(&TABLE_EXISTS_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/credentials").as_str(),
AdminOperation(&LOAD_CREDENTIALS_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}").as_str(),
AdminOperation(&COMMIT_TABLE_HANDLER),
)?;
r.insert(
Method::DELETE,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}").as_str(),
AdminOperation(&DROP_TABLE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/views/{{view}}").as_str(),
AdminOperation(&LOAD_VIEW_HANDLER),
)?;
r.insert(
Method::HEAD,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/views/{{view}}").as_str(),
AdminOperation(&VIEW_EXISTS_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/views/{{view}}").as_str(),
AdminOperation(&REPLACE_VIEW_HANDLER),
)?;
r.insert(
Method::DELETE,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/views/{{view}}").as_str(),
AdminOperation(&DROP_VIEW_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/refs").as_str(),
AdminOperation(&LIST_TABLE_REFS_HANDLER),
)?;
r.insert(
Method::PUT,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/refs/{{ref}}").as_str(),
AdminOperation(&PUT_TABLE_REF_HANDLER),
)?;
r.insert(
Method::DELETE,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/refs/{{ref}}").as_str(),
AdminOperation(&DELETE_TABLE_REF_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/metadata-location").as_str(),
AdminOperation(&GET_TABLE_METADATA_LOCATION_HANDLER),
)?;
r.insert(
Method::PUT,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/metadata-location").as_str(),
AdminOperation(&UPDATE_TABLE_METADATA_LOCATION_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/metadata").as_str(),
AdminOperation(&TABLE_METADATA_MAINTENANCE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/config").as_str(),
AdminOperation(&GET_TABLE_MAINTENANCE_CONFIG_HANDLER),
)?;
r.insert(
Method::PUT,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/config").as_str(),
AdminOperation(&PUT_TABLE_MAINTENANCE_CONFIG_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/jobs/{{job}}").as_str(),
AdminOperation(&GET_TABLE_MAINTENANCE_JOB_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/scheduler").as_str(),
AdminOperation(&GET_TABLE_MAINTENANCE_SCHEDULER_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/scheduler/run").as_str(),
AdminOperation(&RUN_TABLE_MAINTENANCE_SCHEDULER_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/worker/run").as_str(),
AdminOperation(&RUN_TABLE_MAINTENANCE_WORKER_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/jobs/{{job}}/heartbeat").as_str(),
AdminOperation(&HEARTBEAT_TABLE_MAINTENANCE_JOB_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/maintenance/jobs/{{job}}/quarantine").as_str(),
AdminOperation(&TABLE_MAINTENANCE_QUARANTINE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/export").as_str(),
AdminOperation(&EXPORT_TABLE_CATALOG_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/import").as_str(),
AdminOperation(&IMPORT_TABLE_CATALOG_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/external").as_str(),
AdminOperation(&EXTERNAL_CATALOG_BRIDGE_HANDLER),
)?;
r.insert(
Method::PUT,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/external").as_str(),
AdminOperation(&PUT_EXTERNAL_CATALOG_BRIDGE_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/external/sync").as_str(),
AdminOperation(&SYNC_EXTERNAL_CATALOG_BRIDGE_HANDLER),
)?;
r.insert(
Method::GET,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/diagnostics").as_str(),
AdminOperation(&GET_TABLE_CATALOG_DIAGNOSTICS_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/recovery").as_str(),
AdminOperation(&RECOVER_TABLE_CATALOG_HANDLER),
)?;
r.insert(
Method::POST,
format!("{prefix}/{{warehouse}}/namespaces/{{namespace}}/tables/{{table}}/catalog/rollback").as_str(),
AdminOperation(&ROLLBACK_TABLE_CATALOG_HANDLER),
)?;
Ok(())
}
@@ -0,0 +1,296 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct RestListTablesHandler {}
#[async_trait::async_trait]
impl Operation for RestListTablesHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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()?;
let response = list_tables_response(&store, &warehouse, &namespace, &req.uri).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestCreateTableHandler {}
#[async_trait::async_trait]
impl Operation for RestCreateTableHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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 store = table_catalog_store_from_backend(metadata_backend.clone())?;
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
let response =
create_table_response(&store, &metadata_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestRegisterTableHandler {}
#[async_trait::async_trait]
impl Operation for RestRegisterTableHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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 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?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestLoadTableHandler {}
#[async_trait::async_trait]
impl Operation for RestLoadTableHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
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)
}
}
pub struct RestTableExistsHandler {}
#[async_trait::async_trait]
impl Operation for RestTableExistsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
Ok(empty_response(table_exists_status(&store, &warehouse, &namespace, &table).await?))
}
}
pub struct RestCommitTableHandler {}
#[async_trait::async_trait]
impl Operation for RestCommitTableHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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 store = table_catalog_store_from_backend(metadata_backend.clone())?;
let response = commit_table_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestDropTableHandler {}
#[async_trait::async_trait]
impl Operation for RestDropTableHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
drop_table_in_store(&store, &warehouse, &namespace, &table).await?;
Ok(empty_response(StatusCode::NO_CONTENT))
}
}
pub struct GetTableMetadataLocationHandler {}
#[async_trait::async_trait]
impl Operation for GetTableMetadataLocationHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let response = get_table_metadata_location_response(&store, &warehouse, &namespace, &table).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct UpdateTableMetadataLocationHandler {}
#[async_trait::async_trait]
impl Operation for UpdateTableMetadataLocationHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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 store = table_catalog_store_from_backend(metadata_backend.clone())?;
let response =
update_table_metadata_location_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct ExportTableCatalogHandler {}
#[async_trait::async_trait]
impl Operation for ExportTableCatalogHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let started = Instant::now();
let result = store
.export_table_catalog_entry(&warehouse, &namespace.public_name(), &table)
.await
.map_err(catalog_store_error);
record_table_catalog_admin_operation_result("export", &warehouse, &namespace.public_name(), &table, started, &result);
let response = result?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct ImportTableCatalogHandler {}
#[async_trait::async_trait]
impl Operation for ImportTableCatalogHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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 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?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct GetTableCatalogDiagnosticsHandler {}
#[async_trait::async_trait]
impl Operation for GetTableCatalogDiagnosticsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let config = store
.get_table_maintenance_config(&warehouse, &namespace.public_name(), &table)
.await
.map_err(catalog_store_error)?;
let started = Instant::now();
let result = store
.diagnose_table_catalog(&warehouse, &namespace.public_name(), &table, config.retain_recent_metadata_files)
.await
.map_err(catalog_store_error);
record_table_catalog_admin_operation_result(
"diagnostics",
&warehouse,
&namespace.public_name(),
&table,
started,
&result,
);
let response = result?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RecoverTableCatalogHandler {}
#[async_trait::async_trait]
impl Operation for RecoverTableCatalogHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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()?;
let started = Instant::now();
let result = store
.recover_table_commits(&warehouse, &namespace.public_name(), &table)
.await
.map_err(catalog_store_error);
record_table_catalog_admin_operation_result("recovery", &warehouse, &namespace.public_name(), &table, started, &result);
let response = result?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RollbackTableCatalogHandler {}
#[async_trait::async_trait]
impl Operation for RollbackTableCatalogHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let table = table_name_from_params(&params)?;
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 store = table_catalog_store_from_backend(metadata_backend.clone())?;
let response = rollback_table_response(&store, &metadata_backend, &warehouse, &namespace, &table, request).await?;
build_json_response(StatusCode::OK, &response)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
pub struct RestListViewsHandler {}
#[async_trait::async_trait]
impl Operation for RestListViewsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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()?;
let response = list_views_response(&store, &warehouse, &namespace, &req.uri).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestCreateViewHandler {}
#[async_trait::async_trait]
impl Operation for RestCreateViewHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
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 store = table_catalog_store_from_backend(metadata_backend.clone())?;
let table_bucket_enabled = table_bucket_enabled_from_metadata(&warehouse).await?;
let response =
create_view_response(&store, &metadata_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
build_json_response(StatusCode::OK, &response)
}
}
pub struct RestLoadViewHandler {}
#[async_trait::async_trait]
impl Operation for RestLoadViewHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let view = view_name_from_params(&params)?;
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()?;
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)
}
}
pub struct RestViewExistsHandler {}
#[async_trait::async_trait]
impl Operation for RestViewExistsHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let view = view_name_from_params(&params)?;
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()?;
Ok(empty_response(view_exists_status(&store, &warehouse, &namespace, &view).await?))
}
}
pub struct RestReplaceViewHandler {}
#[async_trait::async_trait]
impl Operation for RestReplaceViewHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let view = view_name_from_params(&params)?;
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
ensure_table_bucket_enabled(&warehouse).await?;
let request = read_json_body::<RestCommitViewRequest>(req.input).await?;
let metadata_backend = table_catalog_backend()?;
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)
}
}
pub struct RestDropViewHandler {}
#[async_trait::async_trait]
impl Operation for RestDropViewHandler {
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let warehouse = warehouse_from_params(&params)?;
let namespace = namespace_from_params(&params)?;
let view = view_name_from_params(&params)?;
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()?;
drop_view_in_store(&store, &warehouse, &namespace, &view).await?;
Ok(empty_response(StatusCode::NO_CONTENT))
}
}
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogIdentifierError {
Empty,
TooLong { max: usize },
NamespaceTooLong { max: usize },
InvalidCharacter,
InvalidBoundary,
Ambiguous,
}
impl fmt::Display for CatalogIdentifierError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("catalog identifier segment is empty"),
Self::TooLong { max } => write!(f, "catalog identifier segment exceeds {max} characters"),
Self::NamespaceTooLong { max } => write!(f, "catalog namespace exceeds {max} characters"),
Self::InvalidCharacter => f.write_str("catalog identifier segment contains invalid characters"),
Self::InvalidBoundary => {
f.write_str("catalog identifier segment must start and end with a lowercase letter or digit")
}
Self::Ambiguous => f.write_str("catalog identifier segment is ambiguous"),
}
}
}
impl std::error::Error for CatalogIdentifierError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TableObjectMutationError {
ReservedCatalogObject,
}
impl fmt::Display for TableObjectMutationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReservedCatalogObject => f.write_str("object key is reserved for the table catalog"),
}
}
}
impl std::error::Error for TableObjectMutationError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TableCatalogStoreError {
NotFound(String),
Conflict(String),
Invalid(String),
Internal(String),
}
impl fmt::Display for TableCatalogStoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(message) => write!(f, "table catalog entry not found: {message}"),
Self::Conflict(message) => write!(f, "table catalog conflict: {message}"),
Self::Invalid(message) => write!(f, "invalid table catalog entry: {message}"),
Self::Internal(message) => write!(f, "table catalog store error: {message}"),
}
}
}
impl std::error::Error for TableCatalogStoreError {}
pub(crate) type TableCatalogStoreResult<T> = Result<T, TableCatalogStoreError>;
+303
View File
@@ -0,0 +1,303 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
pub(crate) fn commit_log_matches_request(commit_log: &CommitLogEntry, request: &TableCommitRequest, table_id: &str) -> bool {
commit_log.version == TABLE_CATALOG_ENTRY_VERSION
&& commit_log.commit_id == request.commit_id
&& commit_log.idempotency_key == request.idempotency_key
&& commit_log.table_id == table_id
&& commit_log.operation == request.operation
&& commit_log.expected_version_token == request.expected_version_token
&& commit_log.previous_metadata_location == request.expected_metadata_location
&& commit_log.new_metadata_location == request.new_metadata_location
&& commit_log.requirements == request.requirements
&& commit_log.writer == request.writer
}
pub(crate) fn table_matches_committed_log(table: &TableEntry, commit_log: &CommitLogEntry) -> bool {
table.table_id == commit_log.table_id
&& table.metadata_location == commit_log.new_metadata_location
&& table.version_token == commit_log.new_version_token
}
pub(crate) fn table_matches_staged_base(table: &TableEntry, commit_log: &CommitLogEntry) -> bool {
table.table_id == commit_log.table_id
&& table.metadata_location == commit_log.previous_metadata_location
&& table.version_token == commit_log.expected_version_token
}
pub(crate) fn table_catalog_recovery_summary(
metadata_status: &TableMetadataPointerStatus,
commit_recovery: &TableCommitRecoveryReport,
) -> (TableCatalogRecoveryStatus, Vec<TableCatalogRecoveryAction>) {
let mut actions = Vec::new();
let metadata_status = match metadata_status {
TableMetadataPointerStatus::Valid => None,
TableMetadataPointerStatus::MissingObject => {
actions.push(TableCatalogRecoveryAction::RestoreCurrentMetadataObject);
Some(TableCatalogRecoveryStatus::ReadOnlyRecommended)
}
TableMetadataPointerStatus::InvalidJson => {
actions.push(TableCatalogRecoveryAction::FixCurrentMetadataJson);
Some(TableCatalogRecoveryStatus::ReadOnlyRecommended)
}
TableMetadataPointerStatus::InvalidLocation => {
actions.push(TableCatalogRecoveryAction::MoveCurrentMetadataInsideTable);
Some(TableCatalogRecoveryStatus::ReadOnlyRecommended)
}
};
if commit_recovery.manual_review_count > 0 {
actions.push(TableCatalogRecoveryAction::ReviewCommitLog);
return (metadata_status.unwrap_or(TableCatalogRecoveryStatus::ManualReviewRequired), actions);
}
if commit_recovery.finalization_required_count > 0 || commit_recovery.idempotency_repair_required_count > 0 {
actions.push(TableCatalogRecoveryAction::RunCommitRecovery);
return (metadata_status.unwrap_or(TableCatalogRecoveryStatus::Recoverable), actions);
}
if commit_recovery.staged_before_table_update_count > 0 {
actions.push(TableCatalogRecoveryAction::RetryCommit);
return (metadata_status.unwrap_or(TableCatalogRecoveryStatus::Recoverable), actions);
}
(metadata_status.unwrap_or(TableCatalogRecoveryStatus::Healthy), actions)
}
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
&& left.table_id == right.table_id
&& left.operation == right.operation
&& left.expected_version_token == right.expected_version_token
&& left.new_version_token == right.new_version_token
&& left.previous_metadata_location == right.previous_metadata_location
&& left.new_metadata_location == right.new_metadata_location
&& left.requirements == right.requirements
&& left.writer == right.writer
}
fn commit_idempotency_index_status(
commit_log: &CommitLogEntry,
idempotency_commit: Option<&CommitLogEntry>,
) -> TableCommitIdempotencyIndexStatus {
match (commit_log.idempotency_key.as_ref(), idempotency_commit) {
(None, _) => TableCommitIdempotencyIndexStatus::NotRequired,
(Some(_), None) => TableCommitIdempotencyIndexStatus::Missing,
(Some(_), Some(indexed)) if indexed == commit_log => TableCommitIdempotencyIndexStatus::Matches,
(Some(_), Some(indexed)) if commit_logs_share_recovery_payload(indexed, commit_log) => {
TableCommitIdempotencyIndexStatus::Stale
}
(Some(_), Some(_)) => TableCommitIdempotencyIndexStatus::Conflicting,
}
}
pub(crate) fn table_commit_recovery_entry(
table: &TableEntry,
commit_log: &CommitLogEntry,
idempotency_commit: Option<&CommitLogEntry>,
) -> TableCommitRecoveryEntry {
let idempotency_index_status = commit_idempotency_index_status(commit_log, idempotency_commit);
let idempotency_index_present = matches!(
idempotency_index_status,
TableCommitIdempotencyIndexStatus::Matches
| TableCommitIdempotencyIndexStatus::Stale
| TableCommitIdempotencyIndexStatus::Conflicting
);
let idempotency_index_repair_required = matches!(
idempotency_index_status,
TableCommitIdempotencyIndexStatus::Missing | TableCommitIdempotencyIndexStatus::Stale
);
let (recovery_state, reason) = if matches!(idempotency_index_status, TableCommitIdempotencyIndexStatus::Conflicting) {
(
TableCommitRecoveryState::ManualReview,
"idempotency index points at a different commit payload".to_string(),
)
} else if table_matches_committed_log(table, commit_log) {
if matches!(commit_log.status, CommitLogStatus::Committed) {
if idempotency_index_repair_required {
(
TableCommitRecoveryState::IdempotencyIndexRepairRequired,
"committed table pointer is durable but idempotency index needs repair".to_string(),
)
} else {
(
TableCommitRecoveryState::Committed,
"commit log and current table pointer agree".to_string(),
)
}
} else {
(
TableCommitRecoveryState::FinalizationRequired,
"current table pointer already advanced but commit log is not finalized".to_string(),
)
}
} else if matches!(commit_log.status, CommitLogStatus::Committed) {
if idempotency_index_repair_required {
(
TableCommitRecoveryState::IdempotencyIndexRepairRequired,
"historical committed log needs idempotency index repair".to_string(),
)
} else {
(
TableCommitRecoveryState::Committed,
"commit is finalized and may be older than the current table pointer".to_string(),
)
}
} else if table_matches_staged_base(table, commit_log) {
(
TableCommitRecoveryState::StagedBeforeTableUpdate,
"staged commit exists but table pointer has not advanced".to_string(),
)
} else {
(
TableCommitRecoveryState::ManualReview,
"staged commit no longer matches the current table pointer or its expected base".to_string(),
)
};
TableCommitRecoveryEntry {
commit_id: commit_log.commit_id.clone(),
idempotency_key: commit_log.idempotency_key.clone(),
operation: commit_log.operation.clone(),
status: commit_log.status.clone(),
recovery_state,
previous_metadata_location: commit_log.previous_metadata_location.clone(),
new_metadata_location: commit_log.new_metadata_location.clone(),
expected_version_token: commit_log.expected_version_token.clone(),
new_version_token: commit_log.new_version_token.clone(),
idempotency_index_present,
idempotency_index_status,
reason,
}
}
pub(crate) fn record_table_commit_attempt(operation: &str) {
counter!("rustfs_table_catalog_commit_attempts_total", "operation" => operation.to_string()).increment(1);
}
fn table_catalog_store_result_label<T>(result: &TableCatalogStoreResult<T>) -> &'static str {
match result {
Ok(_) => "success",
Err(TableCatalogStoreError::Conflict(_)) => "conflict",
Err(TableCatalogStoreError::Invalid(_)) => "invalid",
Err(TableCatalogStoreError::NotFound(_)) => "not_found",
Err(TableCatalogStoreError::Internal(_)) => "failure",
}
}
fn duration_millis_u64(duration: StdDuration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
pub(crate) fn record_table_commit_cas_result(operation: &str, started: Instant, result: &TableCatalogStoreResult<()>) {
let elapsed = started.elapsed();
let result_label = table_catalog_store_result_label(result);
counter!(
"rustfs_table_catalog_commit_cas_results_total",
"operation" => operation.to_string(),
"result" => result_label.to_string()
)
.increment(1);
histogram!(
"rustfs_table_catalog_commit_cas_duration_seconds",
"operation" => operation.to_string(),
"result" => result_label.to_string()
)
.record(elapsed.as_secs_f64());
}
fn record_table_commit_result(
table_bucket: &str,
namespace: &str,
table: &str,
commit_id: &str,
operation: &str,
started: Instant,
result: &TableCatalogStoreResult<TableCommitResult>,
) {
let elapsed = started.elapsed();
let result_label = table_catalog_store_result_label(result);
counter!(
"rustfs_table_catalog_commit_results_total",
"operation" => operation.to_string(),
"result" => result_label.to_string()
)
.increment(1);
if matches!(result, Err(TableCatalogStoreError::Conflict(_))) {
counter!("rustfs_table_catalog_commit_conflicts_total", "operation" => operation.to_string()).increment(1);
}
histogram!(
"rustfs_table_catalog_commit_duration_seconds",
"operation" => operation.to_string(),
"result" => result_label.to_string()
)
.record(elapsed.as_secs_f64());
match result {
Ok(commit) if elapsed >= TABLE_COMMIT_SLOW_LOG_THRESHOLD => {
tracing::warn!(
table_bucket,
namespace,
table,
commit_id,
operation,
generation = commit.table.generation,
duration_ms = duration_millis_u64(elapsed),
"slow table catalog commit"
);
}
Ok(commit) => {
tracing::debug!(
table_bucket,
namespace,
table,
commit_id,
operation,
generation = commit.table.generation,
duration_ms = duration_millis_u64(elapsed),
"table catalog commit completed"
);
}
Err(error) => {
tracing::warn!(
table_bucket,
namespace,
table,
commit_id,
operation,
result = result_label,
duration_ms = duration_millis_u64(elapsed),
error = %error,
"table catalog commit did not complete"
);
}
}
}
pub(crate) fn table_commit_result(
table_bucket: &str,
namespace: &str,
table: &str,
commit_id: &str,
operation: &str,
started: Instant,
result: TableCatalogStoreResult<TableCommitResult>,
) -> TableCatalogStoreResult<TableCommitResult> {
record_table_commit_result(table_bucket, namespace, table, commit_id, operation, started, &result);
result
}
@@ -0,0 +1,177 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ManifestDataFileReference {
pub location: String,
pub content: ManifestDataFileContent,
pub object_kind: TableMetadataMaintenanceObjectKind,
pub entry_status: Option<i32>,
pub snapshot_id: Option<i64>,
pub sequence_number: Option<i64>,
pub file_sequence_number: Option<i64>,
pub record_count: Option<u64>,
pub file_size_bytes: Option<u64>,
pub partition: Vec<(String, apache_avro::types::Value)>,
pub sort_order_id: Option<i32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ManifestDataFileContent {
Data,
PositionDelete,
EqualityDelete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ManifestListReference {
pub manifest_path: String,
pub partition_spec_id: Option<i32>,
pub sequence_number: Option<i64>,
pub added_snapshot_id: Option<i64>,
}
pub(crate) fn manifest_paths_from_manifest_list_avro(data: &[u8]) -> TableCatalogStoreResult<Vec<String>> {
Ok(manifest_list_references_from_manifest_list_avro(data)?
.into_iter()
.map(|reference| reference.manifest_path)
.collect())
}
pub(crate) fn manifest_list_references_from_manifest_list_avro(
data: &[u8],
) -> TableCatalogStoreResult<Vec<ManifestListReference>> {
let reader = apache_avro::Reader::new(data)
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest list Avro: {err}")))?;
let mut manifest_paths = Vec::new();
for value in reader {
let value =
value.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest list record: {err}")))?;
let manifest_path = avro_record_field(&value, "manifest_path")
.and_then(avro_string_value)
.ok_or_else(|| TableCatalogStoreError::Invalid("manifest list entry missing manifest_path".to_string()))?;
manifest_paths.push(ManifestListReference {
manifest_path: manifest_path.to_string(),
partition_spec_id: avro_record_field(&value, "partition_spec_id").and_then(avro_i32_value),
sequence_number: avro_record_field(&value, "sequence_number").and_then(avro_i64_value),
added_snapshot_id: avro_record_field(&value, "added_snapshot_id").and_then(avro_i64_value),
});
}
Ok(manifest_paths)
}
pub(crate) fn file_references_from_manifest_avro(
data: &[u8],
) -> TableCatalogStoreResult<Vec<(String, TableMetadataMaintenanceObjectKind)>> {
Ok(data_file_references_from_manifest_avro(data)?
.into_iter()
.map(|reference| (reference.location, reference.object_kind))
.collect())
}
pub(crate) fn data_file_references_from_manifest_avro(data: &[u8]) -> TableCatalogStoreResult<Vec<ManifestDataFileReference>> {
let reader = apache_avro::Reader::new(data)
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest Avro: {err}")))?;
let mut files = Vec::new();
for value in reader {
let value = value.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest record: {err}")))?;
let data_file = avro_record_field(&value, "data_file")
.ok_or_else(|| TableCatalogStoreError::Invalid("manifest entry missing data_file".to_string()))?;
let file_path = avro_record_field(data_file, "file_path")
.and_then(avro_string_value)
.ok_or_else(|| TableCatalogStoreError::Invalid("manifest data file missing file_path".to_string()))?;
let content = avro_record_field(data_file, "content")
.and_then(avro_i32_value)
.ok_or_else(|| TableCatalogStoreError::Invalid("manifest data file missing content".to_string()))?;
let (content, object_kind) = match content {
0 => (ManifestDataFileContent::Data, TableMetadataMaintenanceObjectKind::DataFile),
1 => (ManifestDataFileContent::PositionDelete, TableMetadataMaintenanceObjectKind::DeleteFile),
2 => (ManifestDataFileContent::EqualityDelete, TableMetadataMaintenanceObjectKind::DeleteFile),
_ => continue,
};
files.push(ManifestDataFileReference {
location: file_path.to_string(),
content,
object_kind,
entry_status: avro_record_field(&value, "status").and_then(avro_i32_value),
snapshot_id: avro_record_field(&value, "snapshot_id").and_then(avro_i64_value),
sequence_number: avro_record_field(&value, "sequence_number").and_then(avro_i64_value),
file_sequence_number: avro_record_field(&value, "file_sequence_number").and_then(avro_i64_value),
record_count: avro_record_field(data_file, "record_count")
.and_then(avro_i64_value)
.and_then(|value| u64::try_from(value).ok()),
file_size_bytes: avro_record_field(data_file, "file_size_in_bytes")
.and_then(avro_i64_value)
.and_then(|value| u64::try_from(value).ok()),
partition: avro_record_field(data_file, "partition")
.and_then(avro_record_value_fields)
.unwrap_or_default(),
sort_order_id: avro_record_field(data_file, "sort_order_id").and_then(avro_i32_value),
});
}
Ok(files)
}
fn avro_record_field<'a>(value: &'a apache_avro::types::Value, name: &str) -> Option<&'a apache_avro::types::Value> {
let value = avro_non_union_value(value);
let apache_avro::types::Value::Record(fields) = value else {
return None;
};
fields
.iter()
.find_map(|(field_name, field_value)| (field_name == name).then_some(avro_non_union_value(field_value)))
}
fn avro_record_value_fields(value: &apache_avro::types::Value) -> Option<Vec<(String, apache_avro::types::Value)>> {
let value = avro_non_union_value(value);
let apache_avro::types::Value::Record(fields) = value else {
return None;
};
Some(
fields
.iter()
.map(|(field_name, field_value)| (field_name.clone(), avro_non_union_value(field_value).clone()))
.collect(),
)
}
pub(crate) fn avro_non_union_value(value: &apache_avro::types::Value) -> &apache_avro::types::Value {
match value {
apache_avro::types::Value::Union(_, inner) => avro_non_union_value(inner),
value => value,
}
}
fn avro_string_value(value: &apache_avro::types::Value) -> Option<&str> {
match avro_non_union_value(value) {
apache_avro::types::Value::String(value) => Some(value),
_ => None,
}
}
fn avro_i32_value(value: &apache_avro::types::Value) -> Option<i32> {
match avro_non_union_value(value) {
apache_avro::types::Value::Int(value) => Some(*value),
_ => None,
}
}
fn avro_i64_value(value: &apache_avro::types::Value) -> Option<i64> {
match avro_non_union_value(value) {
apache_avro::types::Value::Long(value) => Some(*value),
_ => None,
}
}
@@ -0,0 +1,120 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
pub(crate) fn metadata_log_locations(
current_metadata: &serde_json::Value,
namespace: &Namespace,
table: &IdentifierSegment,
) -> BTreeSet<String> {
let mut locations = BTreeSet::new();
let Some(metadata_log) = current_metadata.get("metadata-log").and_then(serde_json::Value::as_array) else {
return locations;
};
for entry in metadata_log {
let Some(metadata_location) = entry.get("metadata-file").and_then(serde_json::Value::as_str) else {
continue;
};
if is_valid_table_metadata_location(namespace, table, metadata_location) {
locations.insert(metadata_location.to_string());
}
}
locations
}
pub(crate) async fn metadata_locations_for_protected_snapshot_refs<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
current_metadata: &serde_json::Value,
metadata_locations: &[String],
) -> TableCatalogStoreResult<BTreeSet<String>>
where
B: TableCatalogObjectBackend,
{
let protected_snapshot_ids = protected_ref_snapshot_ids(current_metadata);
if protected_snapshot_ids.is_empty() {
return Ok(BTreeSet::new());
}
let mut retained = BTreeSet::new();
for metadata_location in metadata_locations {
if !is_valid_table_metadata_location(namespace, table, metadata_location) {
continue;
}
let Some(metadata_object) = backend.read_object(table_bucket, metadata_location).await? else {
continue;
};
let Ok(metadata) = serde_json::from_slice::<serde_json::Value>(&metadata_object.data) else {
continue;
};
if metadata_contains_protected_snapshot_ref(&metadata, &protected_snapshot_ids) {
retained.insert(metadata_location.clone());
}
}
Ok(retained)
}
pub(crate) fn protected_ref_snapshot_ids(current_metadata: &serde_json::Value) -> BTreeSet<i64> {
let mut snapshot_ids = BTreeSet::new();
let current_snapshot_id = current_metadata
.get("current-snapshot-id")
.and_then(serde_json::Value::as_i64);
let Some(refs) = current_metadata.get("refs").and_then(serde_json::Value::as_object) else {
return snapshot_ids;
};
for reference in refs.values() {
if let Some(snapshot_id) = reference.get("snapshot-id").and_then(serde_json::Value::as_i64)
&& Some(snapshot_id) != current_snapshot_id
{
snapshot_ids.insert(snapshot_id);
}
}
snapshot_ids
}
pub(crate) fn metadata_contains_protected_snapshot_ref(
metadata: &serde_json::Value,
protected_snapshot_ids: &BTreeSet<i64>,
) -> bool {
let current_snapshot_matches = metadata
.get("current-snapshot-id")
.and_then(serde_json::Value::as_i64)
.is_some_and(|snapshot_id| protected_snapshot_ids.contains(&snapshot_id));
if current_snapshot_matches {
return true;
}
let Some(refs) = metadata.get("refs").and_then(serde_json::Value::as_object) else {
return false;
};
refs.values().any(|reference| {
reference
.get("snapshot-id")
.and_then(serde_json::Value::as_i64)
.is_some_and(|snapshot_id| protected_snapshot_ids.contains(&snapshot_id))
})
}
pub(crate) fn metadata_candidate_is_past_safety_window(mod_time: Option<OffsetDateTime>, now: OffsetDateTime) -> bool {
let Some(mod_time) = mod_time else {
return false;
};
mod_time <= now - Duration::seconds(TABLE_METADATA_CLEANUP_SAFETY_WINDOW_SECONDS)
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod commit;
mod manifest;
mod metadata;
mod validation;
pub(crate) use commit::*;
pub(crate) use manifest::*;
pub(crate) use metadata::*;
pub(crate) use validation::*;
@@ -0,0 +1,219 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
fn normalize_warehouse_object_prefix(object_prefix: &str, max_prefix_depth: Option<usize>) -> TableCatalogStoreResult<String> {
let object_prefix = object_prefix.strip_suffix('/').unwrap_or(object_prefix);
if object_prefix.is_empty() {
return Err(TableCatalogStoreError::Invalid(
"table warehouse location must include an object prefix".to_string(),
));
}
if object_prefix.contains('\\') {
return Err(TableCatalogStoreError::Invalid(
"table warehouse location contains an invalid path separator".to_string(),
));
}
let mut segment_count = 0;
for segment in object_prefix.split('/') {
segment_count += 1;
if segment.is_empty() || segment == "." || segment == ".." {
return Err(TableCatalogStoreError::Invalid(
"table warehouse location contains an invalid path segment".to_string(),
));
}
}
if max_prefix_depth.is_some_and(|max_prefix_depth| segment_count > max_prefix_depth) {
return Err(TableCatalogStoreError::Invalid(
"table warehouse location exceeds the maximum prefix depth".to_string(),
));
}
let mut normalized = object_prefix.to_string();
normalized.push('/');
Ok(normalized)
}
fn warehouse_object_prefix_from_location(
table_bucket: &str,
warehouse_location: &str,
max_prefix_depth: Option<usize>,
) -> TableCatalogStoreResult<String> {
let location = warehouse_location
.strip_prefix("s3://")
.ok_or_else(|| TableCatalogStoreError::Invalid("table warehouse location must be an s3 URI".to_string()))?;
let (bucket, object_prefix) = location
.split_once('/')
.ok_or_else(|| TableCatalogStoreError::Invalid("table warehouse location must include an object prefix".to_string()))?;
if bucket != table_bucket {
return Err(TableCatalogStoreError::Invalid(
"table warehouse location must be inside the table bucket".to_string(),
));
}
normalize_warehouse_object_prefix(object_prefix, max_prefix_depth)
}
fn table_warehouse_object_prefix_from_location(table_bucket: &str, warehouse_location: &str) -> TableCatalogStoreResult<String> {
warehouse_object_prefix_from_location(table_bucket, warehouse_location, Some(WAREHOUSE_INDEX_MAX_PREFIX_DEPTH))
}
fn view_warehouse_object_prefix_from_location(table_bucket: &str, warehouse_location: &str) -> TableCatalogStoreResult<String> {
warehouse_object_prefix_from_location(table_bucket, warehouse_location, None)
}
pub(crate) fn validate_table_warehouse_location(table_bucket: &str, warehouse_location: &str) -> TableCatalogStoreResult<()> {
table_warehouse_object_prefix_from_location(table_bucket, warehouse_location).map(|_| ())
}
pub(crate) fn validate_view_warehouse_location(table_bucket: &str, warehouse_location: &str) -> TableCatalogStoreResult<()> {
view_warehouse_object_prefix_from_location(table_bucket, warehouse_location).map(|_| ())
}
pub(crate) fn table_warehouse_object_prefix(entry: &TableEntry) -> TableCatalogStoreResult<String> {
table_warehouse_object_prefix_from_location(&entry.table_bucket, &entry.warehouse_location)
}
pub(crate) fn table_warehouse_index_entry(entry: &TableEntry) -> TableCatalogStoreResult<TableWarehouseIndexEntry> {
Ok(TableWarehouseIndexEntry {
version: TABLE_CATALOG_ENTRY_VERSION,
table_bucket: entry.table_bucket.clone(),
namespace: entry.namespace.clone(),
table: entry.table.clone(),
table_id: entry.table_id.clone(),
warehouse_object_prefix: table_warehouse_object_prefix(entry)?,
state: entry.state.clone(),
})
}
fn table_warehouse_data_dir_path(entry: &TableEntry) -> TableCatalogStoreResult<String> {
Ok(format!("{}{}", table_warehouse_object_prefix(entry)?, DATA_DIR))
}
pub(crate) fn table_object_s3_location(table_bucket: &str, object_key: &str) -> String {
format!("s3://{table_bucket}/{object_key}")
}
fn metadata_warehouse_location(
table_bucket: &str,
metadata_location: &str,
metadata_object: &TableCatalogObject,
validate_location: fn(&str, &str) -> TableCatalogStoreResult<()>,
) -> TableCatalogStoreResult<Option<String>> {
let metadata: serde_json::Value = serde_json::from_slice(&metadata_object.data)
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to parse new metadata {metadata_location}: {err}")))?;
let Some(location) = metadata.get("location").and_then(serde_json::Value::as_str) else {
return Ok(None);
};
validate_location(table_bucket, location)?;
Ok(Some(location.to_string()))
}
pub(crate) fn table_metadata_warehouse_location(
table_bucket: &str,
metadata_location: &str,
metadata_object: &TableCatalogObject,
) -> TableCatalogStoreResult<Option<String>> {
metadata_warehouse_location(table_bucket, metadata_location, metadata_object, validate_table_warehouse_location)
}
pub(crate) fn view_metadata_warehouse_location(
table_bucket: &str,
metadata_location: &str,
metadata_object: &TableCatalogObject,
) -> TableCatalogStoreResult<Option<String>> {
metadata_warehouse_location(table_bucket, metadata_location, metadata_object, validate_view_warehouse_location)
}
pub(crate) fn warehouse_index_candidate_prefixes(object: &str) -> Vec<&str> {
let mut prefixes = Vec::new();
for (index, byte) in object.as_bytes().iter().enumerate() {
if *byte == b'/' {
prefixes.push(&object[..=index]);
if prefixes.len() >= WAREHOUSE_INDEX_MAX_PREFIX_DEPTH {
break;
}
}
}
prefixes.reverse();
prefixes
}
pub(crate) fn table_data_plane_resource_from_entry(table: TableEntry, warehouse_object_prefix: String) -> TableDataPlaneResource {
TableDataPlaneResource {
table_bucket: table.table_bucket,
namespace: table.namespace,
table: table.table,
table_id: table.table_id,
warehouse_object_prefix,
}
}
pub(crate) async fn table_data_plane_resource_for_object<S>(
store: &S,
bucket: &str,
object: &str,
) -> TableCatalogStoreResult<Option<TableDataPlaneResource>>
where
S: TableCatalogStore + ?Sized,
{
store.resolve_table_data_plane_resource(bucket, object).await
}
pub(crate) async fn scan_table_data_plane_resource_for_object<S>(
store: &S,
bucket: &str,
object: &str,
) -> TableCatalogStoreResult<Option<TableDataPlaneResource>>
where
S: TableCatalogStore + ?Sized,
{
if bucket.is_empty() || object.is_empty() {
return Ok(None);
}
let Some(table_bucket) = store.get_table_bucket(bucket).await? else {
return Ok(None);
};
if table_bucket.state != TableCatalogEntryState::Active {
return Ok(None);
}
let mut matched: Option<TableDataPlaneResource> = None;
for namespace in store.list_namespaces(bucket).await? {
if namespace.state != TableCatalogEntryState::Active {
continue;
}
for table in store.list_tables(bucket, &namespace.namespace).await? {
if table.state != TableCatalogEntryState::Active {
continue;
}
let Ok(warehouse_object_prefix) = table_warehouse_object_prefix(&table) else {
continue;
};
if !object.starts_with(&warehouse_object_prefix) {
continue;
}
if matched
.as_ref()
.is_some_and(|current| current.warehouse_object_prefix.len() >= warehouse_object_prefix.len())
{
continue;
}
matched = Some(table_data_plane_resource_from_entry(table, warehouse_object_prefix));
}
}
Ok(matched)
}
+355
View File
@@ -0,0 +1,355 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentifierSegment(String);
impl IdentifierSegment {
pub const MAX_LEN: usize = 64;
pub fn parse(value: impl Into<String>) -> Result<Self, CatalogIdentifierError> {
let value = value.into();
validate_identifier_segment(&value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Namespace {
segments: Vec<IdentifierSegment>,
}
impl Namespace {
pub const MAX_LEN: usize = 512;
pub fn parse(value: &str) -> Result<Self, CatalogIdentifierError> {
if value.is_empty() {
return Err(CatalogIdentifierError::Empty);
}
if value.len() > Self::MAX_LEN {
return Err(CatalogIdentifierError::NamespaceTooLong { max: Self::MAX_LEN });
}
let mut segments = Vec::new();
for segment in value.split('.') {
segments.push(IdentifierSegment::parse(segment.to_string())?);
}
Ok(Self { segments })
}
pub fn segments(&self) -> &[IdentifierSegment] {
&self.segments
}
pub fn storage_id(&self) -> String {
self.segments
.iter()
.map(IdentifierSegment::as_str)
.collect::<Vec<_>>()
.join("/")
}
pub fn public_name(&self) -> String {
self.segments
.iter()
.map(IdentifierSegment::as_str)
.collect::<Vec<_>>()
.join(".")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableIdentifier {
warehouse: IdentifierSegment,
namespace: Namespace,
name: IdentifierSegment,
}
impl TableIdentifier {
pub fn new(warehouse: IdentifierSegment, namespace: Namespace, name: IdentifierSegment) -> Self {
Self {
warehouse,
namespace,
name,
}
}
pub fn warehouse(&self) -> &IdentifierSegment {
&self.warehouse
}
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
pub fn name(&self) -> &IdentifierSegment {
&self.name
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TablePathResolver {
reserved_prefix: &'static str,
}
impl Default for TablePathResolver {
fn default() -> Self {
Self {
reserved_prefix: TABLE_RESERVED_PREFIX,
}
}
}
impl TablePathResolver {
pub fn current_pointer_path(&self, table: &TableIdentifier) -> String {
format!("{}/{}", self.table_root(table), CURRENT_POINTER_FILE)
}
pub fn metadata_dir_path(&self, table: &TableIdentifier) -> String {
format!("{}/{}", self.table_root(table), METADATA_DIR)
}
pub fn metadata_file_path(&self, table: &TableIdentifier, metadata_file_name: &str) -> String {
format!("{}/{}", self.metadata_dir_path(table), metadata_file_name)
}
fn table_root(&self, table: &TableIdentifier) -> String {
format!(
"{}/{}/{}/{}/{}/{}/{}",
self.reserved_prefix,
WAREHOUSE_ROOT,
table.warehouse().as_str(),
NAMESPACE_ROOT,
table.namespace().storage_id(),
TABLE_ROOT,
table.name().as_str()
)
}
}
pub fn is_reserved_table_object_key(object_key: &str) -> bool {
object_key == TABLE_RESERVED_PREFIX
|| object_key
.strip_prefix(TABLE_RESERVED_PREFIX)
.is_some_and(|rest| rest.starts_with('/'))
}
pub(crate) fn default_namespace_root_prefix() -> String {
format!(
"{}/{}/{}/{}/",
TABLE_RESERVED_PREFIX, WAREHOUSE_ROOT, DEFAULT_WAREHOUSE_ID, NAMESPACE_ROOT
)
}
pub(crate) fn default_namespace_marker_path(namespace: &Namespace) -> String {
format!("{}{}/{}", default_namespace_root_prefix(), namespace.storage_id(), NAMESPACE_MARKER_FILE)
}
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_marker_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), TABLE_MARKER_FILE)
}
pub(crate) fn default_table_metadata_dir_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), METADATA_DIR)
}
pub(crate) fn default_table_data_dir_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), DATA_DIR)
}
pub(crate) fn default_table_delete_dir_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), DELETE_DIR)
}
pub(crate) fn default_view_root_prefix(namespace: &Namespace) -> String {
format!("{}{}/{}/", default_namespace_root_prefix(), namespace.storage_id(), VIEW_ROOT)
}
pub(crate) fn default_view_metadata_dir_path(namespace: &Namespace, view: &IdentifierSegment) -> String {
format!("{}{}/{}", default_view_root_prefix(namespace), view.as_str(), METADATA_DIR)
}
pub(crate) fn default_view_metadata_file_path(
namespace: &Namespace,
view: &IdentifierSegment,
metadata_file_name: &str,
) -> String {
format!("{}/{}", default_view_metadata_dir_path(namespace, view), metadata_file_name)
}
pub(crate) fn default_table_metadata_file_path(
namespace: &Namespace,
table: &IdentifierSegment,
metadata_file_name: &str,
) -> String {
format!("{}/{}", default_table_metadata_dir_path(namespace, table), metadata_file_name)
}
pub(crate) fn default_table_current_pointer_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), CURRENT_POINTER_FILE)
}
pub(crate) fn default_table_lifecycle_path(namespace: &Namespace, table: &IdentifierSegment) -> String {
format!("{}{}/{}", default_table_root_prefix(namespace), table.as_str(), LIFECYCLE_FILE)
}
pub(crate) fn namespace_name_from_marker_path(object_key: &str) -> Option<String> {
let prefix = default_namespace_root_prefix();
let suffix = format!("/{NAMESPACE_MARKER_FILE}");
object_key
.strip_prefix(prefix.as_str())
.and_then(|value| value.strip_suffix(suffix.as_str()))
.filter(|value| !value.is_empty())
.map(|value| value.replace('/', "."))
}
pub(crate) fn table_name_from_marker_path(namespace: &Namespace, object_key: &str) -> Option<String> {
let prefix = default_table_root_prefix(namespace);
let suffix = format!("/{TABLE_MARKER_FILE}");
object_key
.strip_prefix(prefix.as_str())
.and_then(|value| value.strip_suffix(suffix.as_str()))
.filter(|value| !value.is_empty() && !value.contains('/'))
.map(ToString::to_string)
}
pub(crate) fn metadata_location_from_metadata_file_path(
namespace: &Namespace,
table: &IdentifierSegment,
object_key: &str,
) -> Option<String> {
let prefix = format!("{}/", default_table_metadata_dir_path(namespace, table));
object_key
.strip_prefix(prefix.as_str())
.filter(|value| is_valid_table_metadata_file_name(value))
.map(|_| object_key.to_string())
}
pub(crate) fn is_valid_table_metadata_location(
namespace: &Namespace,
table: &IdentifierSegment,
metadata_location: &str,
) -> bool {
if metadata_location.is_empty() {
return false;
}
let metadata_prefix = format!("{}/", default_table_metadata_dir_path(namespace, table));
metadata_location
.strip_prefix(&metadata_prefix)
.is_some_and(is_valid_table_metadata_file_name)
}
pub(crate) fn is_valid_view_metadata_location(namespace: &Namespace, view: &IdentifierSegment, metadata_location: &str) -> bool {
if metadata_location.is_empty() {
return false;
}
let metadata_prefix = format!("{}/", default_view_metadata_dir_path(namespace, view));
metadata_location
.strip_prefix(&metadata_prefix)
.is_some_and(is_valid_table_metadata_file_name)
}
pub(crate) fn is_valid_table_metadata_file_name(metadata_file_name: &str) -> bool {
if metadata_file_name.is_empty()
|| metadata_file_name.len() > TABLE_METADATA_FILE_NAME_MAX_LEN
|| !metadata_file_name.ends_with(".json")
|| metadata_file_name.contains("..")
|| metadata_file_name.contains('%')
|| metadata_file_name.contains('/')
|| metadata_file_name.contains('\\')
|| metadata_file_name.bytes().any(|byte| byte.is_ascii_control())
{
return false;
}
let bytes = metadata_file_name.as_bytes();
if !is_lower_ascii_alnum(bytes[0]) || !is_lower_ascii_alnum(bytes[bytes.len() - 1]) {
return false;
}
bytes
.iter()
.all(|byte| is_lower_ascii_alnum(*byte) || matches!(*byte, b'.' | b'_' | b'-'))
}
pub fn validate_object_mutation(table_bucket_enabled: bool, object_key: &str) -> Result<(), TableObjectMutationError> {
if table_bucket_enabled && is_reserved_table_object_key(object_key) {
return Err(TableObjectMutationError::ReservedCatalogObject);
}
Ok(())
}
pub(crate) async fn validate_bucket_object_mutation(bucket: &str, object_key: &str) -> Result<(), TableObjectMutationError> {
if !is_reserved_table_object_key(object_key) {
return Ok(());
}
let table_bucket_enabled = get_bucket_metadata(bucket)
.await
.map(|metadata| metadata.table_bucket_enabled())
.unwrap_or(true);
validate_object_mutation(table_bucket_enabled, object_key)
}
fn validate_identifier_segment(value: &str) -> Result<(), CatalogIdentifierError> {
if value.is_empty() {
return Err(CatalogIdentifierError::Empty);
}
if value.len() > IdentifierSegment::MAX_LEN {
return Err(CatalogIdentifierError::TooLong {
max: IdentifierSegment::MAX_LEN,
});
}
if matches!(value, "." | "..") || value.contains('%') || value.contains('/') || value.contains('\\') {
return Err(CatalogIdentifierError::Ambiguous);
}
let bytes = value.as_bytes();
if !is_lower_ascii_alnum(bytes[0]) || !is_lower_ascii_alnum(bytes[bytes.len() - 1]) {
return Err(CatalogIdentifierError::InvalidBoundary);
}
if bytes
.iter()
.any(|byte| !is_lower_ascii_alnum(*byte) && !matches!(*byte, b'_' | b'-'))
{
return Err(CatalogIdentifierError::InvalidCharacter);
}
Ok(())
}
fn is_lower_ascii_alnum(value: u8) -> bool {
value.is_ascii_lowercase() || value.is_ascii_digit()
}
@@ -0,0 +1,113 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
pub(crate) fn validate_catalog_entry_version(kind: &str, version: u16) -> TableCatalogStoreResult<()> {
if version != TABLE_CATALOG_ENTRY_VERSION {
return Err(TableCatalogStoreError::Invalid(format!("unsupported {kind} entry version")));
}
Ok(())
}
fn validate_table_maintenance_config_version(version: u16) -> TableCatalogStoreResult<()> {
if version != TABLE_MAINTENANCE_CONFIG_VERSION {
return Err(TableCatalogStoreError::Invalid(
"unsupported table maintenance config entry version".to_string(),
));
}
Ok(())
}
pub(crate) fn validate_table_maintenance_config(config: &TableMaintenanceConfig) -> TableCatalogStoreResult<()> {
validate_table_maintenance_config_version(config.version)?;
if config.worker_lease_timeout_seconds == 0 {
return Err(TableCatalogStoreError::Invalid(
"worker-lease-timeout-seconds must be greater than zero".to_string(),
));
}
if config.worker_lease_timeout_seconds > TABLE_MAINTENANCE_WORKER_LEASE_TIMEOUT_MAX_SECONDS {
return Err(TableCatalogStoreError::Invalid(format!(
"worker-lease-timeout-seconds cannot exceed {TABLE_MAINTENANCE_WORKER_LEASE_TIMEOUT_MAX_SECONDS}"
)));
}
if config.max_retry_attempts > 10 {
return Err(TableCatalogStoreError::Invalid("max-retry-attempts cannot exceed 10".to_string()));
}
if config.max_retry_attempts > 0 && config.retry_initial_backoff_seconds == 0 {
return Err(TableCatalogStoreError::Invalid(
"retry-initial-backoff-seconds must be greater than zero when retry is enabled".to_string(),
));
}
if config.max_retry_attempts > 0 && config.retry_initial_backoff_seconds > TABLE_MAINTENANCE_RETRY_BACKOFF_MAX_SECONDS {
return Err(TableCatalogStoreError::Invalid(format!(
"retry-initial-backoff-seconds cannot exceed {TABLE_MAINTENANCE_RETRY_BACKOFF_MAX_SECONDS}"
)));
}
if config.max_retry_attempts > 0 && config.retry_max_backoff_seconds > TABLE_MAINTENANCE_RETRY_BACKOFF_MAX_SECONDS {
return Err(TableCatalogStoreError::Invalid(format!(
"retry-max-backoff-seconds cannot exceed {TABLE_MAINTENANCE_RETRY_BACKOFF_MAX_SECONDS}"
)));
}
if config.max_retry_attempts > 0 && config.retry_max_backoff_seconds < config.retry_initial_backoff_seconds {
return Err(TableCatalogStoreError::Invalid(
"retry-max-backoff-seconds must be greater than or equal to retry-initial-backoff-seconds".to_string(),
));
}
if config.quarantine_enabled && config.quarantine_retention_seconds == 0 {
return Err(TableCatalogStoreError::Invalid(
"quarantine-retention-seconds must be greater than zero when quarantine is enabled".to_string(),
));
}
Ok(())
}
pub(crate) fn validate_table_snapshot_expiration_config(config: &TableSnapshotExpirationConfig) -> TableCatalogStoreResult<()> {
if config.min_snapshots_to_keep == 0 {
return Err(TableCatalogStoreError::Invalid(
"min-snapshots-to-keep must be greater than zero".to_string(),
));
}
if config.max_snapshot_age_ms < 0 {
return Err(TableCatalogStoreError::Invalid("max-snapshot-age-ms cannot be negative".to_string()));
}
Ok(())
}
pub(crate) fn validate_table_compaction_planning_config(config: &TableCompactionPlanningConfig) -> TableCatalogStoreResult<()> {
if config.target_file_size_bytes == 0 {
return Err(TableCatalogStoreError::Invalid(
"target-file-size-bytes must be greater than zero".to_string(),
));
}
if config.small_file_threshold_bytes == 0 {
return Err(TableCatalogStoreError::Invalid(
"small-file-threshold-bytes must be greater than zero".to_string(),
));
}
if config.small_file_threshold_bytes > config.target_file_size_bytes {
return Err(TableCatalogStoreError::Invalid(
"small-file-threshold-bytes cannot exceed target-file-size-bytes".to_string(),
));
}
if config.min_input_files < 2 {
return Err(TableCatalogStoreError::Invalid("min-input-files must be at least two".to_string()));
}
if config.max_rewrite_bytes_per_job < config.target_file_size_bytes {
return Err(TableCatalogStoreError::Invalid(
"max-rewrite-bytes-per-job must be at least target-file-size-bytes".to_string(),
));
}
Ok(())
}
@@ -0,0 +1,23 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod config;
mod planner;
mod recovery;
mod worker;
pub(crate) use config::*;
pub(crate) use planner::*;
pub(crate) use recovery::*;
pub(crate) use worker::*;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,716 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
pub(crate) fn insert_metadata_maintenance_reason(
reasons_by_location: &mut BTreeMap<String, BTreeSet<TableMetadataMaintenanceReason>>,
metadata_location: String,
reason: TableMetadataMaintenanceReason,
) {
reasons_by_location.entry(metadata_location).or_default().insert(reason);
}
pub(crate) fn metadata_maintenance_object_reports(
reasons_by_location: BTreeMap<String, BTreeSet<TableMetadataMaintenanceReason>>,
) -> Vec<TableMetadataMaintenanceObjectReport> {
reasons_by_location
.into_iter()
.map(|(metadata_location, reasons)| {
let reasons = reasons.into_iter().collect::<Vec<_>>();
let state = if reasons.contains(&TableMetadataMaintenanceReason::SafetyWindowSatisfied) {
TableMetadataMaintenanceObjectState::Deletable
} else if reasons.contains(&TableMetadataMaintenanceReason::SafetyWindowPending) {
TableMetadataMaintenanceObjectState::PendingSafetyWindow
} else {
TableMetadataMaintenanceObjectState::Retained
};
TableMetadataMaintenanceObjectReport {
metadata_location,
state,
reasons,
}
})
.collect()
}
#[derive(Debug, Clone)]
struct TableMetadataMaintenanceReferencedObjectAccumulator {
object_kind: TableMetadataMaintenanceObjectKind,
state: TableMetadataMaintenanceObjectState,
reasons: BTreeSet<TableMetadataMaintenanceReason>,
}
fn insert_referenced_object_report(
reports: &mut BTreeMap<String, TableMetadataMaintenanceReferencedObjectAccumulator>,
object_location: String,
object_kind: TableMetadataMaintenanceObjectKind,
state: TableMetadataMaintenanceObjectState,
reason: TableMetadataMaintenanceReason,
) {
let report = reports
.entry(object_location)
.or_insert_with(|| TableMetadataMaintenanceReferencedObjectAccumulator {
object_kind,
state: TableMetadataMaintenanceObjectState::Retained,
reasons: BTreeSet::new(),
});
if state == TableMetadataMaintenanceObjectState::ManualReviewRequired {
report.state = TableMetadataMaintenanceObjectState::ManualReviewRequired;
}
report.reasons.insert(reason);
}
pub(crate) async fn metadata_maintenance_referenced_object_reports<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
current_metadata: &serde_json::Value,
retained_metadata_locations: &[String],
) -> TableCatalogStoreResult<Vec<TableMetadataMaintenanceReferencedObjectReport>>
where
B: TableCatalogObjectBackend,
{
let mut reports = BTreeMap::<String, TableMetadataMaintenanceReferencedObjectAccumulator>::new();
metadata_maintenance_referenced_object_reports_for_metadata(
backend,
table_bucket,
namespace,
table,
warehouse_object_prefix,
current_metadata,
&mut reports,
)
.await?;
for metadata_location in retained_metadata_locations {
let Some(metadata_object) = backend.read_object(table_bucket, metadata_location).await? else {
insert_referenced_object_report(
&mut reports,
metadata_location.clone(),
TableMetadataMaintenanceObjectKind::MetadataFile,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnreadableMetadata,
);
continue;
};
let Ok(metadata) = serde_json::from_slice::<serde_json::Value>(&metadata_object.data) else {
insert_referenced_object_report(
&mut reports,
metadata_location.clone(),
TableMetadataMaintenanceObjectKind::MetadataFile,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnreadableMetadata,
);
continue;
};
if !metadata.is_object() {
insert_referenced_object_report(
&mut reports,
metadata_location.clone(),
TableMetadataMaintenanceObjectKind::MetadataFile,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnreadableMetadata,
);
continue;
}
metadata_maintenance_referenced_object_reports_for_metadata(
backend,
table_bucket,
namespace,
table,
warehouse_object_prefix,
&metadata,
&mut reports,
)
.await?;
}
Ok(reports
.into_iter()
.map(|(object_location, report)| TableMetadataMaintenanceReferencedObjectReport {
object_location,
object_kind: report.object_kind,
state: report.state,
reasons: report.reasons.into_iter().collect(),
})
.collect())
}
async fn metadata_maintenance_referenced_object_reports_for_metadata<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
metadata: &serde_json::Value,
reports: &mut BTreeMap<String, TableMetadataMaintenanceReferencedObjectAccumulator>,
) -> TableCatalogStoreResult<()>
where
B: TableCatalogObjectBackend,
{
let Some(snapshots) = metadata.get("snapshots").and_then(serde_json::Value::as_array) else {
return Ok(());
};
for snapshot in snapshots {
if let Some(manifest_list_location) = snapshot.get("manifest-list").and_then(serde_json::Value::as_str) {
metadata_maintenance_referenced_manifest_list(
backend,
table_bucket,
namespace,
table,
warehouse_object_prefix,
manifest_list_location,
reports,
)
.await?;
continue;
}
let Some(manifests) = snapshot.get("manifests").and_then(serde_json::Value::as_array) else {
continue;
};
for manifest in manifests {
let Some(manifest_location) = manifest.as_str() else {
insert_referenced_object_report(
reports,
"snapshots[].manifests".to_string(),
TableMetadataMaintenanceObjectKind::ManifestFile,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
continue;
};
metadata_maintenance_referenced_manifest_file(
backend,
table_bucket,
namespace,
table,
warehouse_object_prefix,
manifest_location,
reports,
)
.await?;
}
}
Ok(())
}
async fn metadata_maintenance_referenced_manifest_list<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
manifest_list_location: &str,
reports: &mut BTreeMap<String, TableMetadataMaintenanceReferencedObjectAccumulator>,
) -> TableCatalogStoreResult<()>
where
B: TableCatalogObjectBackend,
{
let Some(manifest_list_key) = table_catalog_object_key_from_location(table_bucket, manifest_list_location) else {
insert_referenced_object_report(
reports,
manifest_list_location.to_string(),
TableMetadataMaintenanceObjectKind::ManifestList,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
return Ok(());
};
if table_maintenance_object_kind(namespace, table, warehouse_object_prefix, &manifest_list_key)
!= Some(TableMetadataMaintenanceObjectKind::ManifestList)
{
insert_referenced_object_report(
reports,
manifest_list_key,
TableMetadataMaintenanceObjectKind::ManifestList,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
return Ok(());
}
insert_referenced_object_report(
reports,
manifest_list_key.clone(),
TableMetadataMaintenanceObjectKind::ManifestList,
TableMetadataMaintenanceObjectState::Retained,
TableMetadataMaintenanceReason::ManifestList,
);
let Some(manifest_list_object) = backend.read_object(table_bucket, &manifest_list_key).await? else {
mark_referenced_object_manual_review(
reports,
&manifest_list_key,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
return Ok(());
};
let Ok(manifest_paths) = manifest_paths_from_manifest_list_avro(&manifest_list_object.data) else {
mark_referenced_object_manual_review(
reports,
&manifest_list_key,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
return Ok(());
};
for manifest_location in manifest_paths {
metadata_maintenance_referenced_manifest_file(
backend,
table_bucket,
namespace,
table,
warehouse_object_prefix,
&manifest_location,
reports,
)
.await?;
}
Ok(())
}
async fn metadata_maintenance_referenced_manifest_file<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
manifest_location: &str,
reports: &mut BTreeMap<String, TableMetadataMaintenanceReferencedObjectAccumulator>,
) -> TableCatalogStoreResult<()>
where
B: TableCatalogObjectBackend,
{
let Some(manifest_key) = table_catalog_object_key_from_location(table_bucket, manifest_location) else {
insert_referenced_object_report(
reports,
manifest_location.to_string(),
TableMetadataMaintenanceObjectKind::ManifestFile,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
return Ok(());
};
if table_maintenance_object_kind(namespace, table, warehouse_object_prefix, &manifest_key)
!= Some(TableMetadataMaintenanceObjectKind::ManifestFile)
{
insert_referenced_object_report(
reports,
manifest_key,
TableMetadataMaintenanceObjectKind::ManifestFile,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
return Ok(());
}
insert_referenced_object_report(
reports,
manifest_key.clone(),
TableMetadataMaintenanceObjectKind::ManifestFile,
TableMetadataMaintenanceObjectState::Retained,
TableMetadataMaintenanceReason::ManifestFile,
);
let Some(manifest_object) = backend.read_object(table_bucket, &manifest_key).await? else {
mark_referenced_object_manual_review(reports, &manifest_key, TableMetadataMaintenanceReason::UnsupportedManifestAvro);
return Ok(());
};
let Ok(file_references) = file_references_from_manifest_avro(&manifest_object.data) else {
mark_referenced_object_manual_review(reports, &manifest_key, TableMetadataMaintenanceReason::UnsupportedManifestAvro);
return Ok(());
};
for (file_location, object_kind) in file_references {
let Some(file_key) = table_catalog_object_key_from_location(table_bucket, &file_location) else {
insert_referenced_object_report(
reports,
file_location,
object_kind,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
continue;
};
if table_maintenance_object_kind(namespace, table, warehouse_object_prefix, &file_key) != Some(object_kind.clone()) {
insert_referenced_object_report(
reports,
file_key,
object_kind,
TableMetadataMaintenanceObjectState::ManualReviewRequired,
TableMetadataMaintenanceReason::UnsupportedManifestAvro,
);
continue;
}
insert_referenced_object_report(
reports,
file_key,
object_kind.clone(),
TableMetadataMaintenanceObjectState::Retained,
table_metadata_maintenance_reason_for_object_kind(&object_kind),
);
}
Ok(())
}
fn mark_referenced_object_manual_review(
reports: &mut BTreeMap<String, TableMetadataMaintenanceReferencedObjectAccumulator>,
object_location: &str,
reason: TableMetadataMaintenanceReason,
) {
if let Some(report) = reports.get_mut(object_location) {
report.state = TableMetadataMaintenanceObjectState::ManualReviewRequired;
report.reasons.insert(reason);
}
}
pub(crate) fn table_catalog_object_key_from_location(table_bucket: &str, location: &str) -> Option<String> {
let object = if let Some(location) = location.strip_prefix("s3://") {
let (bucket, object) = location.split_once('/')?;
if bucket != table_bucket {
return None;
}
object
} else {
location
};
if object.is_empty()
|| object.starts_with('/')
|| object.contains("..")
|| object.contains('\\')
|| object.bytes().any(|byte| byte.is_ascii_control())
{
return None;
}
Some(object.to_string())
}
pub(crate) fn table_maintenance_object_kind(
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
object_location: &str,
) -> Option<TableMetadataMaintenanceObjectKind> {
let metadata_prefix = format!("{}/", default_table_metadata_dir_path(namespace, table));
if let Some(kind) = table_maintenance_metadata_object_kind(&metadata_prefix, object_location) {
return Some(kind);
}
let data_prefix = format!("{}/", default_table_data_dir_path(namespace, table));
if object_location
.strip_prefix(&data_prefix)
.is_some_and(is_valid_table_maintenance_nested_object)
{
return Some(TableMetadataMaintenanceObjectKind::DataFile);
}
let delete_prefix = format!("{}/", default_table_delete_dir_path(namespace, table));
if object_location
.strip_prefix(&delete_prefix)
.is_some_and(is_valid_table_maintenance_nested_object)
{
return Some(TableMetadataMaintenanceObjectKind::DeleteFile);
}
if let Some(warehouse_object_prefix) = warehouse_object_prefix {
let metadata_prefix = format!("{warehouse_object_prefix}{METADATA_DIR}/");
if let Some(kind) = table_maintenance_metadata_object_kind(&metadata_prefix, object_location) {
return Some(kind);
}
let data_prefix = format!("{warehouse_object_prefix}{DATA_DIR}/");
if object_location
.strip_prefix(&data_prefix)
.is_some_and(is_valid_table_maintenance_nested_object)
{
return Some(TableMetadataMaintenanceObjectKind::DataFile);
}
let delete_prefix = format!("{warehouse_object_prefix}{DELETE_DIR}/");
if object_location
.strip_prefix(&delete_prefix)
.is_some_and(is_valid_table_maintenance_nested_object)
{
return Some(TableMetadataMaintenanceObjectKind::DeleteFile);
}
}
None
}
fn table_maintenance_metadata_object_kind(
metadata_prefix: &str,
object_location: &str,
) -> Option<TableMetadataMaintenanceObjectKind> {
let file_name = object_location.strip_prefix(metadata_prefix)?;
if file_name.is_empty()
|| file_name.contains('/')
|| file_name.contains('\\')
|| file_name.contains("..")
|| file_name.bytes().any(|byte| byte.is_ascii_control())
|| !file_name.ends_with(".avro")
{
return None;
}
if file_name.starts_with("snap-") {
return Some(TableMetadataMaintenanceObjectKind::ManifestList);
}
Some(TableMetadataMaintenanceObjectKind::ManifestFile)
}
fn is_valid_table_maintenance_nested_object(suffix: &str) -> bool {
!suffix.is_empty()
&& !suffix.starts_with('/')
&& !suffix.contains("..")
&& !suffix.contains('\\')
&& !suffix.bytes().any(|byte| byte.is_ascii_control())
}
fn table_metadata_maintenance_reason_for_object_kind(
object_kind: &TableMetadataMaintenanceObjectKind,
) -> TableMetadataMaintenanceReason {
match object_kind {
TableMetadataMaintenanceObjectKind::MetadataFile => TableMetadataMaintenanceReason::CurrentMetadata,
TableMetadataMaintenanceObjectKind::ManifestList => TableMetadataMaintenanceReason::ManifestList,
TableMetadataMaintenanceObjectKind::ManifestFile => TableMetadataMaintenanceReason::ManifestFile,
TableMetadataMaintenanceObjectKind::DataFile => TableMetadataMaintenanceReason::DataFile,
TableMetadataMaintenanceObjectKind::DeleteFile => TableMetadataMaintenanceReason::DeleteFile,
}
}
pub(crate) fn metadata_maintenance_reachability_graph_report(
metadata_file_count: usize,
referenced_object_reports: &[TableMetadataMaintenanceReferencedObjectReport],
) -> TableMaintenanceReachabilityGraphReport {
let manifest_list_count = referenced_object_reports
.iter()
.filter(|report| report.object_kind == TableMetadataMaintenanceObjectKind::ManifestList)
.count();
let manifest_file_count = referenced_object_reports
.iter()
.filter(|report| report.object_kind == TableMetadataMaintenanceObjectKind::ManifestFile)
.count();
let data_file_count = referenced_object_reports
.iter()
.filter(|report| report.object_kind == TableMetadataMaintenanceObjectKind::DataFile)
.count();
let delete_file_count = referenced_object_reports
.iter()
.filter(|report| report.object_kind == TableMetadataMaintenanceObjectKind::DeleteFile)
.count();
let manual_review_count = referenced_object_reports
.iter()
.filter(|report| report.state == TableMetadataMaintenanceObjectState::ManualReviewRequired)
.count();
let mut reasons = BTreeSet::from([TableMaintenanceReachabilityGraphReason::MetadataJsonParsed]);
if manifest_list_count > 0 {
reasons.insert(TableMaintenanceReachabilityGraphReason::ManifestListAvroReferenced);
}
if referenced_object_reports.iter().any(|report| {
report
.reasons
.contains(&TableMetadataMaintenanceReason::UnsupportedManifestAvro)
}) {
reasons.insert(TableMaintenanceReachabilityGraphReason::ManifestAvroReaderUnavailable);
}
TableMaintenanceReachabilityGraphReport {
status: if manual_review_count == 0 {
TableMaintenanceReachabilityGraphStatus::Complete
} else {
TableMaintenanceReachabilityGraphStatus::ManualReviewRequired
},
metadata_file_count,
manifest_list_count,
manifest_file_count,
data_file_count,
delete_file_count,
manual_review_count,
reasons: reasons.into_iter().collect(),
}
}
pub(crate) async fn metadata_maintenance_object_cleanup_reports<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
referenced_object_reports: &[TableMetadataMaintenanceReferencedObjectReport],
now: OffsetDateTime,
) -> TableCatalogStoreResult<(usize, Vec<String>, Vec<String>, Vec<TableMetadataMaintenanceObjectCleanupReport>)>
where
B: TableCatalogObjectBackend,
{
let scanned_objects =
table_maintenance_cleanup_objects(backend, table_bucket, namespace, table, warehouse_object_prefix).await?;
if referenced_object_reports
.iter()
.any(|report| report.state == TableMetadataMaintenanceObjectState::ManualReviewRequired)
{
return Ok((scanned_objects.len(), Vec::new(), Vec::new(), Vec::new()));
}
let referenced_locations = referenced_object_reports
.iter()
.filter_map(|report| table_catalog_object_key_from_location(table_bucket, &report.object_location))
.collect::<BTreeSet<_>>();
let mut cleanup_candidate_locations = Vec::new();
let mut deletable_object_locations = Vec::new();
let mut cleanup_reports = Vec::new();
for (object_location, object_kind) in scanned_objects {
if referenced_locations.contains(&object_location) {
continue;
}
let mut reasons = BTreeSet::from([
table_metadata_maintenance_reason_for_object_kind(&object_kind),
TableMetadataMaintenanceReason::NoCurrentReachability,
]);
let state = match backend.read_object(table_bucket, &object_location).await? {
Some(object) if metadata_candidate_is_past_safety_window(object.mod_time, now) => {
reasons.insert(TableMetadataMaintenanceReason::SafetyWindowSatisfied);
cleanup_candidate_locations.push(object_location.clone());
deletable_object_locations.push(object_location.clone());
TableMetadataMaintenanceObjectState::Deletable
}
_ => {
reasons.insert(TableMetadataMaintenanceReason::SafetyWindowPending);
cleanup_candidate_locations.push(object_location.clone());
TableMetadataMaintenanceObjectState::PendingSafetyWindow
}
};
cleanup_reports.push(TableMetadataMaintenanceObjectCleanupReport {
object_location,
object_kind,
state,
reasons: reasons.into_iter().collect(),
});
}
Ok((
referenced_locations.len() + cleanup_reports.len(),
cleanup_candidate_locations,
deletable_object_locations,
cleanup_reports,
))
}
async fn table_maintenance_cleanup_objects<B>(
backend: &B,
table_bucket: &str,
namespace: &Namespace,
table: &IdentifierSegment,
warehouse_object_prefix: Option<&str>,
) -> TableCatalogStoreResult<BTreeMap<String, TableMetadataMaintenanceObjectKind>>
where
B: TableCatalogObjectBackend,
{
let mut objects = BTreeMap::new();
let mut metadata_prefixes = vec![format!("{}/", default_table_metadata_dir_path(namespace, table))];
let mut data_prefixes = vec![format!("{}/", default_table_data_dir_path(namespace, table))];
let mut delete_prefixes = vec![format!("{}/", default_table_delete_dir_path(namespace, table))];
if let Some(warehouse_object_prefix) = warehouse_object_prefix {
metadata_prefixes.push(format!("{warehouse_object_prefix}{METADATA_DIR}/"));
data_prefixes.push(format!("{warehouse_object_prefix}{DATA_DIR}/"));
delete_prefixes.push(format!("{warehouse_object_prefix}{DELETE_DIR}/"));
}
metadata_prefixes.sort();
metadata_prefixes.dedup();
data_prefixes.sort();
data_prefixes.dedup();
delete_prefixes.sort();
delete_prefixes.dedup();
for metadata_prefix in metadata_prefixes {
for object in backend.list_objects(table_bucket, &metadata_prefix).await? {
if let Some(kind) = table_maintenance_object_kind(namespace, table, warehouse_object_prefix, &object)
&& matches!(
kind,
TableMetadataMaintenanceObjectKind::ManifestList | TableMetadataMaintenanceObjectKind::ManifestFile
)
{
objects.insert(object, kind);
}
}
}
for data_prefix in data_prefixes {
for object in backend.list_objects(table_bucket, &data_prefix).await? {
if table_maintenance_object_kind(namespace, table, warehouse_object_prefix, &object)
== Some(TableMetadataMaintenanceObjectKind::DataFile)
{
objects.insert(object, TableMetadataMaintenanceObjectKind::DataFile);
}
}
}
for delete_prefix in delete_prefixes {
for object in backend.list_objects(table_bucket, &delete_prefix).await? {
if table_maintenance_object_kind(namespace, table, warehouse_object_prefix, &object)
== Some(TableMetadataMaintenanceObjectKind::DeleteFile)
{
objects.insert(object, TableMetadataMaintenanceObjectKind::DeleteFile);
}
}
}
Ok(objects)
}
pub(crate) fn mark_deleted_metadata_object_reports(
object_reports: &mut [TableMetadataMaintenanceObjectReport],
deleted_locations: &BTreeSet<String>,
) {
for object_report in object_reports {
if !deleted_locations.contains(&object_report.metadata_location) {
continue;
}
object_report.state = TableMetadataMaintenanceObjectState::Deleted;
if !object_report
.reasons
.contains(&TableMetadataMaintenanceReason::DeletedByMaintenance)
{
object_report
.reasons
.push(TableMetadataMaintenanceReason::DeletedByMaintenance);
}
}
}
pub(crate) fn mark_deleted_object_cleanup_reports(
object_reports: &mut [TableMetadataMaintenanceObjectCleanupReport],
deleted_locations: &BTreeSet<String>,
) {
for object_report in object_reports {
if !deleted_locations.contains(&object_report.object_location) {
continue;
}
object_report.state = TableMetadataMaintenanceObjectState::Deleted;
if !object_report
.reasons
.contains(&TableMetadataMaintenanceReason::DeletedByMaintenance)
{
object_report
.reasons
.push(TableMetadataMaintenanceReason::DeletedByMaintenance);
}
}
}
@@ -0,0 +1,225 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::super::*;
pub(crate) fn maintenance_timestamp(now: OffsetDateTime) -> String {
now.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_else(|_| now.unix_timestamp().to_string())
}
pub(crate) fn default_table_maintenance_worker_lease_timeout_seconds() -> u64 {
TABLE_MAINTENANCE_WORKER_LEASE_TIMEOUT_DEFAULT_SECONDS
}
fn parse_maintenance_timestamp(timestamp: &str) -> Option<OffsetDateTime> {
OffsetDateTime::parse(timestamp, &time::format_description::well_known::Rfc3339).ok()
}
pub(crate) fn table_maintenance_quarantine_operator_reason(action: &str, reason: Option<&str>) -> String {
let reason = reason.map(str::trim).filter(|reason| !reason.is_empty());
match reason {
Some(reason) => format!("maintenance quarantine {action} by operator: {reason}"),
None => format!("maintenance quarantine {action} by operator"),
}
}
pub(crate) fn push_table_maintenance_audit_event(
report: &mut TableMetadataMaintenanceReport,
timestamp: OffsetDateTime,
actor: TableMaintenanceAuditActor,
action: TableMaintenanceAuditAction,
reason: Option<String>,
before_status: Option<TableMetadataMaintenanceJobStatus>,
before_quarantined_object_count: Option<usize>,
) {
report.audit_events.push(TableMaintenanceAuditEvent {
timestamp: maintenance_timestamp(timestamp),
actor,
action,
reason,
before_status,
after_status: Some(report.job.status.clone()),
before_quarantined_object_count,
after_quarantined_object_count: Some(report.job.quarantined_object_count),
recommended_actions: report.job.recommended_actions.clone(),
});
}
fn table_maintenance_recommended_actions(job: &TableMetadataMaintenanceJob) -> Vec<TableMaintenanceRecommendedAction> {
let mut actions = Vec::new();
match job.status {
TableMetadataMaintenanceJobStatus::NotYetRun => {}
TableMetadataMaintenanceJobStatus::Queued => {
actions.push(TableMaintenanceRecommendedAction::RunMaintenanceWorker);
}
TableMetadataMaintenanceJobStatus::Running => {
actions.push(TableMaintenanceRecommendedAction::WaitForActiveWorker);
}
TableMetadataMaintenanceJobStatus::Successful => {
if matches!(job.operation, TableMetadataMaintenanceOperation::DryRun)
&& (job.deletable_metadata_file_count > 0 || job.deletable_object_count > 0)
{
actions.push(TableMaintenanceRecommendedAction::ReviewAndRunDelete);
} else {
actions.push(TableMaintenanceRecommendedAction::NoActionRequired);
}
}
TableMetadataMaintenanceJobStatus::Failed => {
if job
.failure_reason
.as_deref()
.is_some_and(|reason| reason == TABLE_MAINTENANCE_DELETE_DISABLED_REASON)
{
actions.push(TableMaintenanceRecommendedAction::EnableDelete);
}
if job.quarantine_enabled && job.quarantined_object_count > 0 {
actions.push(TableMaintenanceRecommendedAction::ReviewQuarantine);
}
if job.next_retry_after.is_some() {
actions.push(TableMaintenanceRecommendedAction::WaitForRetryBackoff);
}
if actions.is_empty() {
actions.push(TableMaintenanceRecommendedAction::InvestigateFailure);
}
}
TableMetadataMaintenanceJobStatus::Disabled => {
actions.push(TableMaintenanceRecommendedAction::EnableBackgroundMaintenance);
}
TableMetadataMaintenanceJobStatus::Paused => {
actions.push(TableMaintenanceRecommendedAction::ResumeMaintenanceWorker);
}
}
actions
}
pub(crate) fn push_unique_maintenance_action(
actions: &mut Vec<TableMaintenanceRecommendedAction>,
action: TableMaintenanceRecommendedAction,
) {
if !actions.contains(&action) {
actions.push(action);
}
}
pub(crate) fn table_maintenance_report_order_timestamp(report: &TableMetadataMaintenanceReport) -> String {
report
.job
.finished_at
.clone()
.or_else(|| report.job.heartbeat_at.clone())
.or_else(|| report.job.started_at.clone())
.or_else(|| report.job.scheduled_at.clone())
.unwrap_or_default()
}
pub(crate) fn table_maintenance_scheduler_job_summary(
report: &TableMetadataMaintenanceReport,
) -> TableMaintenanceSchedulerJobSummary {
TableMaintenanceSchedulerJobSummary {
job_id: report.job.job_id.clone(),
operation: report.job.operation.clone(),
status: report.job.status.clone(),
scheduler_id: report.job.scheduler_id.clone(),
scheduled_at: report.job.scheduled_at.clone(),
worker_id: report.job.worker_id.clone(),
attempt: report.job.attempt,
started_at: report.job.started_at.clone(),
finished_at: report.job.finished_at.clone(),
heartbeat_at: report.job.heartbeat_at.clone(),
next_retry_after: report.job.next_retry_after.clone(),
recommended_actions: report.job.recommended_actions.clone(),
audit_events: report.audit_events.clone(),
}
}
pub(crate) fn table_maintenance_scheduler_quarantine_boundary(
config: &TableMaintenanceConfig,
reports: &[TableMetadataMaintenanceReport],
) -> TableMaintenanceSchedulerQuarantineBoundary {
let source = reports
.iter()
.find(|report| report.job.quarantine_enabled && report.job.quarantined_object_count > 0);
TableMaintenanceSchedulerQuarantineBoundary {
enabled: config.quarantine_enabled,
active: source.is_some(),
retention_seconds: source.map_or(config.quarantine_retention_seconds, |report| report.job.quarantine_retention_seconds),
quarantined_object_count: source.map_or(0, |report| report.job.quarantined_object_count),
source_job_id: source.map(|report| report.job.job_id.clone()),
}
}
pub(crate) fn refresh_table_maintenance_report_recommended_actions(report: &mut TableMetadataMaintenanceReport) {
report.job.recommended_actions = table_maintenance_recommended_actions(&report.job);
}
pub(crate) fn table_maintenance_report_with_recommended_actions(
mut report: TableMetadataMaintenanceReport,
) -> TableMetadataMaintenanceReport {
refresh_table_maintenance_report_recommended_actions(&mut report);
report
}
pub(crate) fn table_maintenance_scheduler_lease_is_active(
job: &TableMetadataMaintenanceJob,
scheduler_lease_timeout_seconds: u64,
now: OffsetDateTime,
) -> bool {
let Some(scheduled_at) = job.scheduled_at.as_deref().and_then(parse_maintenance_timestamp) else {
return false;
};
let timeout_seconds = i64::try_from(scheduler_lease_timeout_seconds).unwrap_or(i64::MAX);
scheduled_at.saturating_add(Duration::seconds(timeout_seconds)) > now
}
pub(crate) fn table_maintenance_job_lease_is_active(
job: &TableMetadataMaintenanceJob,
worker_lease_timeout_seconds: u64,
now: OffsetDateTime,
) -> bool {
let Some(heartbeat_at) = job.heartbeat_at.as_deref().and_then(parse_maintenance_timestamp) else {
return false;
};
let timeout_seconds = i64::try_from(worker_lease_timeout_seconds).unwrap_or(i64::MAX);
heartbeat_at.saturating_add(Duration::seconds(timeout_seconds)) > now
}
pub(crate) fn table_maintenance_job_retry_is_pending(job: &TableMetadataMaintenanceJob, now: OffsetDateTime) -> bool {
if !matches!(job.status, TableMetadataMaintenanceJobStatus::Failed) {
return false;
}
let Some(next_retry_after) = job.next_retry_after.as_deref().and_then(parse_maintenance_timestamp) else {
return false;
};
next_retry_after > now
}
pub(crate) fn apply_maintenance_retry_after(
job: &mut TableMetadataMaintenanceJob,
config: &TableMaintenanceConfig,
now: OffsetDateTime,
) {
if config.max_retry_attempts == 0 || job.attempt >= config.max_retry_attempts {
job.next_retry_after = None;
return;
}
let attempt_index = u32::from(job.attempt.saturating_sub(1));
let multiplier = 1_u64.checked_shl(attempt_index).unwrap_or(u64::MAX);
let delay_seconds = config
.retry_initial_backoff_seconds
.saturating_mul(multiplier)
.min(config.retry_max_backoff_seconds);
let delay_seconds = i64::try_from(delay_seconds).unwrap_or(i64::MAX);
job.next_retry_after = Some(maintenance_timestamp(now.saturating_add(Duration::seconds(delay_seconds))));
}
+345
View File
@@ -0,0 +1,345 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Internal table catalog primitives for the Iceberg REST Catalog framework.
//!
//! This module intentionally does not expose HTTP handlers or mutate existing
//! S3 object behavior. It defines the stable internal boundary that later
//! catalog routes and object guards can share.
#![allow(dead_code)]
use std::{
collections::{BTreeMap, BTreeSet},
num::NonZeroUsize,
ops::Bound,
sync::Arc,
time::{Duration as StdDuration, Instant},
};
use crate::storage_api::table::contract::http::HTTPPreconditions;
use crate::storage_api::table::contract::list::{
ListObjectVersionsInfo as StorageListObjectVersionsInfo, ListObjectsV2Info as StorageListObjectsV2Info,
ListOperations as StorageListOperations, ObjectInfoOrErr as StorageObjectInfoOrErr, WalkOptions as StorageWalkOptions,
};
use crate::storage_api::table::contract::namespace::NamespaceLocking as StorageNamespaceLocking;
use crate::storage_api::table::contract::object::{ObjectIO as StorageObjectIO, ObjectOperations as StorageObjectOperations};
use crate::storage_api::table::contract::range::HTTPRangeSpec;
use crate::storage_api::table::{
BUCKET_TABLE_CATALOG_META_PREFIX, BUCKET_TABLE_CATALOG_TABLE_BUCKETS_PREFIX, BUCKET_TABLE_CONFIG,
BUCKET_TABLE_RESERVED_PREFIX, Error as EcstoreError, RUSTFS_META_BUCKET, StorageError, get_bucket_metadata,
get_lock_acquire_timeout, table_catalog_path_hash,
};
use bytes::Bytes;
use datafusion::{
arrow::datatypes::SchemaRef,
parquet::arrow::{ArrowWriter, arrow_reader::ParquetRecordBatchReaderBuilder},
};
use http::HeaderMap;
use metrics::{counter, histogram};
use rustfs_filemeta::FileInfo;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::{Digest, Sha256};
use time::{Duration, OffsetDateTime};
use tokio::io::AsyncReadExt;
use uuid::Uuid;
use crate::storage_api::table::{
StorageDeletedObject as DeletedObject, StorageGetObjectReader as GetObjectReader, StorageObjectInfo as ObjectInfo,
StorageObjectOptions as ObjectOptions, StorageObjectToDelete as ObjectToDelete, StoragePutObjReader as PutObjReader,
};
mod error;
mod iceberg;
mod identifier;
mod maintenance;
mod model;
mod store;
pub use error::{CatalogIdentifierError, TableObjectMutationError};
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,
};
pub(crate) use maintenance::*;
pub(crate) use model::*;
pub(crate) use store::*;
pub(crate) const TABLE_BUCKET_MARKER_CONFIG: &str = BUCKET_TABLE_CONFIG;
pub(crate) const RESERVED_CATALOG_OBJECT_MESSAGE: &str = "Object key is reserved for the table catalog";
pub(crate) const TABLE_BUCKET_CATALOG_TYPE: &str = "iceberg-rest";
pub(crate) const TABLE_BUCKET_CONFIG_VERSION: u16 = 1;
pub(crate) const DEFAULT_WAREHOUSE_ID: &str = "default";
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_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 TABLE_CATALOG_BACKING_OBJECT: &str = "object";
pub(crate) const TABLE_CATALOG_BACKING_DURABLE_STRONG: &str = "durable-strong";
pub(crate) const TABLE_METADATA_FILE_NAME_MAX_LEN: usize = 128;
pub const TABLE_RESERVED_PREFIX: &str = BUCKET_TABLE_RESERVED_PREFIX;
const WAREHOUSE_ROOT: &str = "warehouses";
const NAMESPACE_ROOT: &str = "namespaces";
const TABLE_ROOT: &str = "tables";
const VIEW_ROOT: &str = "views";
const NAMESPACE_MARKER_FILE: &str = "namespace.json";
const TABLE_MARKER_FILE: &str = "table.json";
const CURRENT_POINTER_FILE: &str = "current.json";
const LIFECYCLE_FILE: &str = "lifecycle.json";
const METADATA_DIR: &str = "metadata";
const DATA_DIR: &str = "data";
const DELETE_DIR: &str = "delete";
const TABLE_BUCKET_ENTRY_FILE: &str = "table-bucket.json";
const NAMESPACE_ENTRY_FILE: &str = "namespace-entry.json";
const TABLE_ENTRY_FILE: &str = "table-entry.json";
const VIEW_ENTRY_FILE: &str = "view-entry.json";
const INTERNAL_CATALOG_ROOT: &str = BUCKET_TABLE_CATALOG_META_PREFIX;
const TABLE_BUCKET_ROOT: &str = BUCKET_TABLE_CATALOG_TABLE_BUCKETS_PREFIX;
const COMMIT_LOG_ROOT: &str = "commits";
const COMMIT_IDEMPOTENCY_ROOT: &str = "commit-idempotency";
const WAREHOUSE_INDEX_ROOT: &str = "warehouse-index";
const WAREHOUSE_INDEX_STATE_FILE: &str = "state.json";
const WAREHOUSE_INDEX_MAX_PREFIX_DEPTH: usize = 64;
const EXTERNAL_CATALOG_ROOT: &str = "external-catalog";
const EXTERNAL_CATALOG_BRIDGE_FILE: &str = "bridge.json";
const MAINTENANCE_ROOT: &str = "maintenance";
const MAINTENANCE_CONFIG_FILE: &str = "config.json";
const MAINTENANCE_JOB_ROOT: &str = "jobs";
const MAINTENANCE_LATEST_JOB_FILE: &str = "latest.json";
const MAINTENANCE_CURRENT_JOB_FILE: &str = "current.json";
const MAINTENANCE_JOB_ALIAS_LATEST: &str = "latest";
const MAINTENANCE_JOB_ALIAS_CURRENT: &str = "current";
const TABLE_CATALOG_LIST_MAX_KEYS: usize = 1000;
const OBJECT_CATALOG_LIST_CURSOR_PREFIX: &str = "object:";
const STRONG_CATALOG_LIST_CURSOR_PREFIX: &str = "strong:";
const TABLE_METADATA_CLEANUP_SAFETY_WINDOW_SECONDS: i64 = 15 * 60;
const TABLE_MAINTENANCE_RETRY_BACKOFF_MAX_SECONDS: u64 = 24 * 60 * 60;
const TABLE_MAINTENANCE_WORKER_LEASE_TIMEOUT_DEFAULT_SECONDS: u64 = 15 * 60;
const TABLE_MAINTENANCE_WORKER_LEASE_TIMEOUT_MAX_SECONDS: u64 = 24 * 60 * 60;
const TABLE_MAINTENANCE_SCHEDULER_AUDIT_LIMIT: usize = 10;
const TABLE_MAINTENANCE_DELETE_DISABLED_REASON: &str = "metadata delete is disabled by maintenance config";
const TABLE_COMMIT_SLOW_LOG_THRESHOLD: StdDuration = StdDuration::from_secs(2);
const ICEBERG_MAIN_REF: &str = "main";
const ICEBERG_MIN_SNAPSHOTS_TO_KEEP_PROPERTY: &str = "history.expire.min-snapshots-to-keep";
const ICEBERG_MAX_SNAPSHOT_AGE_MS_PROPERTY: &str = "history.expire.max-snapshot-age-ms";
const ICEBERG_MAX_REF_AGE_MS_PROPERTY: &str = "history.expire.max-ref-age-ms";
const ICEBERG_REF_MIN_SNAPSHOTS_TO_KEEP_FIELD: &str = "min-snapshots-to-keep";
const ICEBERG_REF_MAX_SNAPSHOT_AGE_MS_FIELD: &str = "max-snapshot-age-ms";
const ICEBERG_REF_MAX_REF_AGE_MS_FIELD: &str = "max-ref-age-ms";
const STRONG_TABLE_CATALOG_SNAPSHOT_VERSION: u16 = 1;
const STRONG_TABLE_CATALOG_BACKING_ROOT: &str = "strong-backing";
const STRONG_TABLE_CATALOG_SNAPSHOT_FILE: &str = "snapshot.json";
const TABLE_CATALOG_MIGRATION_VERSION: u16 = 1;
const TABLE_CATALOG_MIGRATION_ROOT: &str = "backing-migration";
const TABLE_CATALOG_MIGRATION_FENCE_FILE: &str = "durable-strong-fence.json";
const TABLE_CATALOG_MIGRATION_FENCE_LOCK: &str = "durable-strong-fence.lock";
const TABLE_CATALOG_MIGRATION_GLOBAL_FENCE_FILE: &str = "durable-strong-global-fence.json";
const TABLE_CATALOG_MIGRATION_GLOBAL_FENCE_LOCK: &str = "durable-strong-global-fence.lock";
type CatalogListObjectsV2Info = StorageListObjectsV2Info<ObjectInfo>;
type CatalogListObjectVersionsInfo = StorageListObjectVersionsInfo<ObjectInfo>;
type CatalogObjectInfoOrErr = StorageObjectInfoOrErr<ObjectInfo, EcstoreError>;
type CatalogWalkOptions = StorageWalkOptions<fn(&FileInfo) -> bool>;
pub(crate) trait TableCatalogStorage:
StorageObjectIO<
Error = EcstoreError,
RangeSpec = HTTPRangeSpec,
HeaderMap = HeaderMap,
ObjectOptions = ObjectOptions,
ObjectInfo = ObjectInfo,
GetObjectReader = GetObjectReader,
PutObjectReader = PutObjReader,
> + StorageObjectOperations<
Error = EcstoreError,
ObjectInfo = ObjectInfo,
ObjectOptions = ObjectOptions,
FileInfo = FileInfo,
ObjectToDelete = ObjectToDelete,
DeletedObject = DeletedObject,
> + StorageListOperations<
Error = EcstoreError,
ListObjectsV2Info = CatalogListObjectsV2Info,
ListObjectVersionsInfo = CatalogListObjectVersionsInfo,
ObjectInfoOrErr = CatalogObjectInfoOrErr,
WalkOptions = CatalogWalkOptions,
WalkCancellation = tokio_util::sync::CancellationToken,
WalkResultSender = tokio::sync::mpsc::Sender<CatalogObjectInfoOrErr>,
> + StorageNamespaceLocking<Error = EcstoreError, NamespaceLock = rustfs_lock::NamespaceLockWrapper>
{
}
impl<T> TableCatalogStorage for T where
T: StorageObjectIO<
Error = EcstoreError,
RangeSpec = HTTPRangeSpec,
HeaderMap = HeaderMap,
ObjectOptions = ObjectOptions,
ObjectInfo = ObjectInfo,
GetObjectReader = GetObjectReader,
PutObjectReader = PutObjReader,
> + StorageObjectOperations<
Error = EcstoreError,
ObjectInfo = ObjectInfo,
ObjectOptions = ObjectOptions,
FileInfo = FileInfo,
ObjectToDelete = ObjectToDelete,
DeletedObject = DeletedObject,
> + StorageListOperations<
Error = EcstoreError,
ListObjectsV2Info = CatalogListObjectsV2Info,
ListObjectVersionsInfo = CatalogListObjectVersionsInfo,
ObjectInfoOrErr = CatalogObjectInfoOrErr,
WalkOptions = CatalogWalkOptions,
WalkCancellation = tokio_util::sync::CancellationToken,
WalkResultSender = tokio::sync::mpsc::Sender<CatalogObjectInfoOrErr>,
> + StorageNamespaceLocking<Error = EcstoreError, NamespaceLock = rustfs_lock::NamespaceLockWrapper>
{
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TableCatalogBackingMode {
ObjectBacked,
DurableStrong,
}
impl TableCatalogBackingMode {
pub(crate) fn from_env() -> TableCatalogStoreResult<Self> {
match std::env::var(ENV_TABLE_CATALOG_BACKING) {
Ok(value) => Self::parse(&value),
Err(std::env::VarError::NotPresent) => Ok(Self::ObjectBacked),
Err(std::env::VarError::NotUnicode(_)) => Err(TableCatalogStoreError::Invalid(format!(
"{ENV_TABLE_CATALOG_BACKING} must be valid UTF-8"
))),
}
}
fn parse(value: &str) -> TableCatalogStoreResult<Self> {
match value.trim() {
"" | TABLE_CATALOG_BACKING_OBJECT => Ok(Self::ObjectBacked),
TABLE_CATALOG_BACKING_DURABLE_STRONG => Ok(Self::DurableStrong),
value => Err(TableCatalogStoreError::Invalid(format!(
"unsupported table catalog backing {value}; expected {TABLE_CATALOG_BACKING_OBJECT} or {TABLE_CATALOG_BACKING_DURABLE_STRONG}"
))),
}
}
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::ObjectBacked => TABLE_CATALOG_BACKING_OBJECT,
Self::DurableStrong => TABLE_CATALOG_BACKING_DURABLE_STRONG,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TableCatalogListPage<T> {
pub entries: Vec<T>,
pub next_cursor: Option<String>,
}
fn finish_catalog_list_page<T, F>(
mut entries: Vec<T>,
limit: NonZeroUsize,
cursor_prefix: &str,
key: F,
) -> TableCatalogListPage<T>
where
F: Fn(&T) -> &str,
{
let next_cursor = if entries.len() > limit.get() {
entries.truncate(limit.get());
entries.last().map(|entry| format!("{cursor_prefix}{}", key(entry)))
} else {
None
};
TableCatalogListPage { entries, next_cursor }
}
fn catalog_list_page_from_entries<T, F>(
mut entries: Vec<T>,
cursor: Option<&str>,
limit: NonZeroUsize,
key: F,
) -> TableCatalogListPage<T>
where
F: Fn(&T) -> &str,
{
entries.sort_by(|left, right| key(left).cmp(key(right)));
let start = cursor.map_or(0, |cursor| entries.partition_point(|entry| key(entry) <= cursor));
let entries = entries.into_iter().skip(start).take(limit.get().saturating_add(1)).collect();
finish_catalog_list_page(entries, limit, "", key)
}
fn catalog_list_cursor<'a>(cursor: Option<&'a str>, prefix: &str) -> TableCatalogStoreResult<Option<&'a str>> {
cursor
.map(|cursor| {
cursor
.strip_prefix(prefix)
.filter(|cursor| !cursor.is_empty())
.ok_or_else(|| {
TableCatalogStoreError::Invalid("page cursor does not match the active table catalog backing".to_string())
})
})
.transpose()
}
fn parse_namespace_for_store(namespace: &str) -> TableCatalogStoreResult<Namespace> {
Namespace::parse(namespace).map_err(|err| TableCatalogStoreError::Invalid(format!("invalid namespace: {err}")))
}
fn parse_table_for_store(table: &str) -> TableCatalogStoreResult<IdentifierSegment> {
IdentifierSegment::parse(table).map_err(|err| TableCatalogStoreError::Invalid(format!("invalid table name: {err}")))
}
fn http_preconditions_for_catalog_put(precondition: TableCatalogPutPrecondition) -> Option<HTTPPreconditions> {
match precondition {
TableCatalogPutPrecondition::Any => None,
TableCatalogPutPrecondition::IfAbsent => Some(HTTPPreconditions {
if_none_match: Some("*".to_string()),
..Default::default()
}),
TableCatalogPutPrecondition::IfMatch(etag) => Some(HTTPPreconditions {
if_match: Some(etag),
..Default::default()
}),
}
}
fn is_missing_storage_error(err: &StorageError) -> bool {
matches!(
err,
StorageError::ObjectNotFound(_, _) | StorageError::FileNotFound | StorageError::ConfigNotFound
)
}
fn storage_error_to_catalog(action: &str, err: StorageError) -> TableCatalogStoreError {
match err {
StorageError::ObjectNotFound(bucket, object) => TableCatalogStoreError::NotFound(format!("{action}: {bucket}/{object}")),
StorageError::BucketNotFound(bucket) => TableCatalogStoreError::NotFound(format!("{action}: bucket {bucket}")),
StorageError::PreconditionFailed => TableCatalogStoreError::Conflict(format!("{action}: precondition failed")),
other => TableCatalogStoreError::Internal(format!("{action}: {other}")),
}
}
#[cfg(test)]
mod tests;
File diff suppressed because it is too large Load Diff
+881
View File
@@ -0,0 +1,881 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::object::ObjectTableCatalogStore;
use super::strong::{
StrongCommitSnapshotRecord, StrongTableCatalogBucketSnapshot, StrongTableCatalogState, TableCatalogBackingMigrationFence,
TableCatalogBackingMigrationFenceStatus, TableCatalogBackingMigrationGlobalFence, table_catalog_bucket_snapshot_fingerprint,
};
use super::*;
pub(super) fn table_catalog_backing_manifest(
paths: &TableCatalogObjectPaths,
namespace: &Namespace,
table: &IdentifierSegment,
entry: &TableEntry,
commit_recovery: &TableCommitRecoveryReport,
) -> TableCatalogBackingManifest {
let recovery_required = commit_recovery.staged_before_table_update_count > 0
|| commit_recovery.finalization_required_count > 0
|| commit_recovery.idempotency_repair_required_count > 0;
let manual_review_required = commit_recovery.manual_review_count > 0;
let wal_status = if manual_review_required {
TableCatalogWalStatus::ManualReviewRequired
} else if recovery_required {
TableCatalogWalStatus::RecoveryRequired
} else {
TableCatalogWalStatus::Recoverable
};
let migration_status = if manual_review_required {
TableCatalogBackingMigrationStatus::ManualReviewRequired
} else if recovery_required {
TableCatalogBackingMigrationStatus::RecoveryRequired
} else {
TableCatalogBackingMigrationStatus::ReadyToSnapshot
};
let mut blockers = Vec::new();
if recovery_required {
blockers.push(TableCatalogBackingMigrationBlocker::CommitRecoveryRequired);
}
if manual_review_required {
blockers.push(TableCatalogBackingMigrationBlocker::CommitManualReviewRequired);
}
TableCatalogBackingManifest {
version: TABLE_CATALOG_BACKING_MANIFEST_VERSION,
current: TableCatalogBackingProfile {
kind: TableCatalogBackingKind::ObjectBacked,
authority: TableCatalogAuthority::RustfsSysObject,
consistency: TableCatalogConsistencyMode::ConditionalObjectCas,
durability: TableCatalogDurabilityMode::StagedCommitLogBeforePointerUpdate,
current_pointer_path: paths.table_entry_path(&entry.table_bucket, namespace, table),
wal: TableCatalogWalState {
status: wal_status,
commit_log_prefix: paths.commit_log_entries_prefix(&entry.table_bucket, &entry.table_id),
idempotency_index_prefix: paths.commit_idempotency_entries_prefix(&entry.table_bucket, &entry.table_id),
committed_generation: entry.generation,
staged_before_table_update_count: commit_recovery.staged_before_table_update_count,
finalization_required_count: commit_recovery.finalization_required_count,
idempotency_repair_required_count: commit_recovery.idempotency_repair_required_count,
manual_review_count: commit_recovery.manual_review_count,
},
snapshot: TableCatalogSnapshotState {
export_api: "GET /iceberg/v1/{warehouse}/namespaces/{namespace}/tables/{table}/catalog/export".to_string(),
includes_table_bucket: true,
includes_namespace: true,
includes_table_pointer: true,
includes_backing_manifest: true,
},
},
migration: TableCatalogBackingMigrationPlan {
source_kind: TableCatalogBackingKind::ObjectBacked,
target_kind: TableCatalogBackingKind::StrongKvWal,
status: migration_status,
required_steps: vec![
TableCatalogBackingMigrationStep::SnapshotCatalogExport,
TableCatalogBackingMigrationStep::ReplayCommitLog,
TableCatalogBackingMigrationStep::VerifyCurrentPointer,
TableCatalogBackingMigrationStep::EnableSingleWriterFencing,
TableCatalogBackingMigrationStep::CutOverLinearizableReads,
],
blockers,
},
ha: TableCatalogHaPolicy {
writer_region_model: TableCatalogHaWriterModel::SingleActiveWriterRegion,
read_replica_strategy: TableCatalogReadReplicaStrategy::ReadOnlyReplicasForListAndLoad,
commit_read_requirement: TableCatalogCommitReadRequirement::LinearizableLeaderRead,
active_active_supported: false,
failover_requires_operator_promotion: true,
},
scale_validation: TableCatalogScaleValidation {
status: TableCatalogScaleValidationStatus::MatrixPublished,
benchmark_required: true,
required_scenarios: vec![
TableCatalogScaleValidationScenario::ConcurrentCommitCas,
TableCatalogScaleValidationScenario::CommitLogRecoveryReplay,
TableCatalogScaleValidationScenario::MigrationSnapshotReplay,
TableCatalogScaleValidationScenario::ReadReplicaStaleReadGuard,
TableCatalogScaleValidationScenario::ClientConformanceMatrix,
],
},
}
}
impl<B> ObjectTableCatalogStore<B>
where
B: TableCatalogObjectBackend,
{
async fn read_backing_migration_fence(
&self,
table_bucket: &str,
) -> TableCatalogStoreResult<Option<(TableCatalogBackingMigrationFence, Option<String>)>> {
self.read_entry(self.catalog_bucket(), &self.paths.backing_migration_fence_path(table_bucket))
.await
}
pub(super) async fn acquire_table_bucket_registry_write_permit(&self) -> TableCatalogStoreResult<Box<dyn Send>> {
let fence_path = self.paths.backing_migration_global_fence_path();
let lock_path = self.paths.backing_migration_global_fence_lock_path();
let guard = self.backend.acquire_read_lock(self.catalog_bucket(), &lock_path).await?;
if self
.read_entry::<TableCatalogBackingMigrationGlobalFence>(self.catalog_bucket(), &fence_path)
.await?
.is_some()
{
return Err(TableCatalogStoreError::Conflict(
"table bucket registry writes are fenced while durable strong migration is in progress".to_string(),
));
}
Ok(guard)
}
pub(super) async fn acquire_object_backed_catalog_write_permit(
&self,
table_bucket: &str,
) -> TableCatalogStoreResult<Box<dyn Send>> {
let lock_path = self.paths.backing_migration_fence_lock_path(table_bucket);
let guard = self.backend.acquire_read_lock(self.catalog_bucket(), &lock_path).await?;
if self.read_backing_migration_fence(table_bucket).await?.is_some() {
return Err(TableCatalogStoreError::Conflict(format!(
"object-backed catalog writes are fenced while table bucket {table_bucket} is prepared for durable strong cutover"
)));
}
Ok(guard)
}
async fn ensure_global_backing_migration_fence(
&self,
fence_path: &str,
) -> TableCatalogStoreResult<TableCatalogBackingMigrationGlobalFence> {
if let Some((fence, _)) = self
.read_entry::<TableCatalogBackingMigrationGlobalFence>(self.catalog_bucket(), fence_path)
.await?
{
if fence.version != TABLE_CATALOG_MIGRATION_VERSION {
return Err(TableCatalogStoreError::Invalid(
"invalid durable strong global migration fence".to_string(),
));
}
return Ok(fence);
}
let fence = TableCatalogBackingMigrationGlobalFence {
version: TABLE_CATALOG_MIGRATION_VERSION,
migration_id: Uuid::new_v4().to_string(),
};
self.write_entry(self.catalog_bucket(), fence_path, &fence, TableCatalogPutPrecondition::IfAbsent)
.await?;
Ok(fence)
}
async fn clear_global_backing_migration_fence_if_unused(&self, fence_path: &str) -> TableCatalogStoreResult<()> {
let bucket_objects = self
.backend
.list_objects(self.catalog_bucket(), &self.paths.table_bucket_entries_prefix())
.await?;
if bucket_objects
.iter()
.any(|object| object.ends_with(TABLE_CATALOG_MIGRATION_FENCE_FILE))
{
return Ok(());
}
self.backend.delete_object(self.catalog_bucket(), fence_path).await
}
pub(super) async fn ensure_object_backed_writes_allowed(&self, table_bucket: &str) -> TableCatalogStoreResult<()> {
if self
.backend
.object_exists(self.catalog_bucket(), &self.paths.backing_migration_fence_path(table_bucket))
.await?
{
return Err(TableCatalogStoreError::Conflict(format!(
"object-backed catalog writes are fenced while table bucket {table_bucket} is prepared for durable strong cutover"
)));
}
Ok(())
}
async fn collect_bucket_snapshot_with_locks(
&self,
table_bucket: &str,
guards: &mut Vec<Box<dyn Send>>,
) -> TableCatalogStoreResult<StrongTableCatalogBucketSnapshot> {
let bucket_path = self.paths.table_bucket_entry_path(table_bucket);
guards.push(self.backend.acquire_write_lock(self.catalog_bucket(), &bucket_path).await?);
let Some((table_bucket_entry, _)) = self
.read_entry_unlocked::<TableBucketEntry>(self.catalog_bucket(), &bucket_path)
.await?
else {
return Err(TableCatalogStoreError::NotFound(format!("table bucket {table_bucket}")));
};
if table_bucket_entry.table_bucket != table_bucket {
return Err(TableCatalogStoreError::Invalid(format!(
"table bucket entry does not match migration target {table_bucket}"
)));
}
let mut namespaces = Vec::new();
let mut tables = Vec::new();
let mut views = Vec::new();
let mut commits = Vec::new();
let mut idempotency = Vec::new();
let namespace_objects = self
.backend
.list_objects(self.catalog_bucket(), &self.paths.namespace_entries_prefix(table_bucket))
.await?;
let mut unmatched_table_objects = namespace_objects
.iter()
.filter(|object| object.ends_with(TABLE_ENTRY_FILE))
.cloned()
.collect::<BTreeSet<_>>();
let mut unmatched_view_objects = namespace_objects
.iter()
.filter(|object| object.ends_with(VIEW_ENTRY_FILE))
.cloned()
.collect::<BTreeSet<_>>();
for namespace_object in namespace_objects
.iter()
.filter(|object| object.ends_with(NAMESPACE_ENTRY_FILE))
{
guards.push(
self.backend
.acquire_write_lock(self.catalog_bucket(), namespace_object)
.await?,
);
let Some((namespace_entry, _)) = self
.read_entry_unlocked::<NamespaceEntry>(self.catalog_bucket(), namespace_object)
.await?
else {
return Err(TableCatalogStoreError::Conflict(format!(
"namespace changed while preparing durable strong snapshot: {namespace_object}"
)));
};
if namespace_entry.table_bucket != table_bucket {
return Err(TableCatalogStoreError::Invalid(format!(
"namespace {} belongs to a different table bucket",
namespace_entry.namespace
)));
}
let namespace = parse_namespace_for_store(&namespace_entry.namespace)?;
let table_objects = self
.backend
.list_objects(self.catalog_bucket(), &self.paths.table_entries_prefix(table_bucket, &namespace))
.await?;
for table_object in table_objects.iter().filter(|object| object.ends_with(TABLE_ENTRY_FILE)) {
unmatched_table_objects.remove(table_object);
guards.push(self.backend.acquire_write_lock(self.catalog_bucket(), table_object).await?);
let Some((table_entry, _)) = self
.read_entry_unlocked::<TableEntry>(self.catalog_bucket(), table_object)
.await?
else {
return Err(TableCatalogStoreError::Conflict(format!(
"table changed while preparing durable strong snapshot: {table_object}"
)));
};
if table_entry.table_bucket != table_bucket || table_entry.namespace != namespace_entry.namespace {
return Err(TableCatalogStoreError::Invalid(format!(
"table {} does not match its catalog namespace",
table_entry.table
)));
}
for commit_object in self
.backend
.list_objects(
self.catalog_bucket(),
&self.paths.commit_log_entries_prefix(table_bucket, &table_entry.table_id),
)
.await?
.into_iter()
.filter(|object| object.ends_with(".json"))
{
let Some((commit, _)) = self
.read_entry_unlocked::<CommitLogEntry>(self.catalog_bucket(), &commit_object)
.await?
else {
return Err(TableCatalogStoreError::Conflict(format!(
"commit log changed while preparing durable strong snapshot: {commit_object}"
)));
};
commits.push(StrongCommitSnapshotRecord {
table_bucket: table_bucket.to_string(),
table_id: table_entry.table_id.clone(),
lookup_key: commit.commit_id.clone(),
commit,
});
}
for idempotency_object in self
.backend
.list_objects(
self.catalog_bucket(),
&self
.paths
.commit_idempotency_entries_prefix(table_bucket, &table_entry.table_id),
)
.await?
.into_iter()
.filter(|object| object.ends_with(".json"))
{
let Some((commit, _)) = self
.read_entry_unlocked::<CommitLogEntry>(self.catalog_bucket(), &idempotency_object)
.await?
else {
return Err(TableCatalogStoreError::Conflict(format!(
"idempotency index changed while preparing durable strong snapshot: {idempotency_object}"
)));
};
let lookup_key = commit.idempotency_key.clone().ok_or_else(|| {
TableCatalogStoreError::Invalid(format!("idempotency index {idempotency_object} has no idempotency key"))
})?;
idempotency.push(StrongCommitSnapshotRecord {
table_bucket: table_bucket.to_string(),
table_id: table_entry.table_id.clone(),
lookup_key,
commit,
});
}
tables.push(table_entry);
}
let view_objects = self
.backend
.list_objects(self.catalog_bucket(), &self.paths.view_entries_prefix(table_bucket, &namespace))
.await?;
for view_object in view_objects.iter().filter(|object| object.ends_with(VIEW_ENTRY_FILE)) {
unmatched_view_objects.remove(view_object);
guards.push(self.backend.acquire_write_lock(self.catalog_bucket(), view_object).await?);
let Some((view_entry, _)) = self
.read_entry_unlocked::<ViewEntry>(self.catalog_bucket(), view_object)
.await?
else {
return Err(TableCatalogStoreError::Conflict(format!(
"view changed while preparing durable strong snapshot: {view_object}"
)));
};
if view_entry.table_bucket != table_bucket || view_entry.namespace != namespace_entry.namespace {
return Err(TableCatalogStoreError::Invalid(format!(
"view {} does not match its catalog namespace",
view_entry.view
)));
}
views.push(view_entry);
}
namespaces.push(namespace_entry);
}
if let Some(object) = unmatched_table_objects.first() {
return Err(TableCatalogStoreError::Invalid(format!(
"table entry has no namespace entry during durable strong migration: {object}"
)));
}
if let Some(object) = unmatched_view_objects.first() {
return Err(TableCatalogStoreError::Invalid(format!(
"view entry has no namespace entry during durable strong migration: {object}"
)));
}
namespaces.sort_by(|left, right| left.namespace.cmp(&right.namespace));
tables.sort_by(|left, right| (&left.namespace, &left.table).cmp(&(&right.namespace, &right.table)));
views.sort_by(|left, right| (&left.namespace, &left.view).cmp(&(&right.namespace, &right.view)));
commits.sort_by(|left, right| (&left.table_id, &left.lookup_key).cmp(&(&right.table_id, &right.lookup_key)));
idempotency.sort_by(|left, right| (&left.table_id, &left.lookup_key).cmp(&(&right.table_id, &right.lookup_key)));
let snapshot = StrongTableCatalogBucketSnapshot {
table_bucket: table_bucket_entry,
namespaces,
tables,
views,
commits,
idempotency,
};
self.validate_bucket_snapshot_for_migration(&snapshot)?;
Ok(snapshot)
}
fn validate_bucket_snapshot_for_migration(&self, snapshot: &StrongTableCatalogBucketSnapshot) -> TableCatalogStoreResult<()> {
let table_bucket = &snapshot.table_bucket.table_bucket;
let tables_by_id = snapshot
.tables
.iter()
.map(|table| (table.table_id.as_str(), table))
.collect::<BTreeMap<_, _>>();
if tables_by_id.len() != snapshot.tables.len() {
return Err(TableCatalogStoreError::Invalid(
"migration snapshot contains duplicate table ids".to_string(),
));
}
let commits_by_key = snapshot
.commits
.iter()
.map(|record| ((record.table_id.as_str(), record.lookup_key.as_str()), &record.commit))
.collect::<BTreeMap<_, _>>();
if commits_by_key.len() != snapshot.commits.len() {
return Err(TableCatalogStoreError::Invalid(
"migration snapshot contains duplicate commit lookup keys".to_string(),
));
}
let idempotency_by_key = snapshot
.idempotency
.iter()
.map(|record| ((record.table_id.as_str(), record.lookup_key.as_str()), &record.commit))
.collect::<BTreeMap<_, _>>();
if idempotency_by_key.len() != snapshot.idempotency.len() {
return Err(TableCatalogStoreError::Invalid(
"migration snapshot contains duplicate idempotency lookup keys".to_string(),
));
}
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))
})?;
if record.table_bucket != *table_bucket
|| record.commit.table_id != record.table_id
|| record.lookup_key != record.commit.commit_id
{
return Err(TableCatalogStoreError::Invalid(format!(
"commit {} does not match its migration snapshot owner",
record.commit.commit_id
)));
}
let indexed = record
.commit
.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);
if recovery.recovery_state != TableCommitRecoveryState::Committed {
return Err(TableCatalogStoreError::Conflict(format!(
"commit {} requires catalog recovery before durable strong migration",
record.commit.commit_id
)));
}
}
for record in &snapshot.idempotency {
let _table = tables_by_id.get(record.table_id.as_str()).ok_or_else(|| {
TableCatalogStoreError::Invalid(format!(
"idempotency index {} has no table in migration snapshot",
record.lookup_key
))
})?;
if record.table_bucket != *table_bucket || record.commit.table_id != record.table_id {
return Err(TableCatalogStoreError::Invalid(format!(
"idempotency index {} does not match its migration snapshot owner",
record.lookup_key
)));
}
if record.commit.idempotency_key.as_deref() != Some(record.lookup_key.as_str()) {
return Err(TableCatalogStoreError::Invalid(format!(
"idempotency index {} does not match its commit payload",
record.lookup_key
)));
}
let committed = commits_by_key
.get(&(record.table_id.as_str(), record.commit.commit_id.as_str()))
.ok_or_else(|| {
TableCatalogStoreError::Invalid(format!(
"idempotency index {} has no commit record in migration snapshot",
record.lookup_key
))
})?;
if *committed != &record.commit {
return Err(TableCatalogStoreError::Conflict(format!(
"idempotency index {} requires catalog recovery before durable strong migration",
record.lookup_key
)));
}
}
let mut state = StrongTableCatalogState {
hydrated: true,
..StrongTableCatalogState::default()
};
StrongTableCatalogStore::<B>::insert_bucket_snapshot_locked(&mut state, snapshot.clone())?;
if state.namespaces.len() != snapshot.namespaces.len()
|| state.tables.len() != snapshot.tables.len()
|| state.views.len() != snapshot.views.len()
|| state.commits.len() != snapshot.commits.len()
|| state.idempotency.len() != snapshot.idempotency.len()
{
return Err(TableCatalogStoreError::Invalid(
"migration snapshot contains duplicate catalog identities".to_string(),
));
}
Ok(())
}
pub(crate) async fn plan_durable_strong_backing_migration(
&self,
table_bucket: &str,
) -> TableCatalogStoreResult<TableCatalogBackingMigrationDryRunReport> {
if self.get_table_bucket(table_bucket).await?.is_none() {
return Err(TableCatalogStoreError::NotFound(format!("table bucket {table_bucket}")));
}
let namespaces = self.list_namespaces(table_bucket).await?;
let mut table_count: usize = 0;
let mut view_count: usize = 0;
let mut commit_log_count: usize = 0;
let mut idempotency_index_count: usize = 0;
let mut recovery_required_count: usize = 0;
let mut manual_review_count: usize = 0;
let mut warehouse_prefix_owners = BTreeMap::<String, usize>::new();
for namespace in &namespaces {
let tables = self.list_tables(table_bucket, &namespace.namespace).await?;
for table in tables {
table_count += 1;
if table.state == TableCatalogEntryState::Active {
let warehouse_prefix = table_warehouse_object_prefix(&table)?;
warehouse_prefix_owners
.entry(warehouse_prefix)
.and_modify(|count| *count = count.saturating_add(1))
.or_insert(1);
}
let recovery = self.table_commit_recovery_report_for_entry(&table, 0).await?;
commit_log_count = commit_log_count.saturating_add(recovery.commits.len());
idempotency_index_count = idempotency_index_count.saturating_add(
self.backend
.list_objects(
self.catalog_bucket(),
&self.paths.commit_idempotency_entries_prefix(table_bucket, &table.table_id),
)
.await?
.into_iter()
.filter(|object| object.ends_with(".json"))
.count(),
);
recovery_required_count = recovery_required_count
.saturating_add(recovery.staged_before_table_update_count)
.saturating_add(recovery.finalization_required_count)
.saturating_add(recovery.idempotency_repair_required_count);
manual_review_count = manual_review_count.saturating_add(recovery.manual_review_count);
}
view_count = view_count.saturating_add(self.list_views(table_bucket, &namespace.namespace).await?.len());
}
let warehouse_index_ready = self.warehouse_index_ready(table_bucket).await?;
let duplicate_warehouse_prefix_count = warehouse_prefix_owners.values().filter(|count| **count > 1).count();
let mut blockers = Vec::new();
let mut recommended_actions = Vec::new();
if recovery_required_count > 0 {
blockers.push(TableCatalogBackingMigrationBlocker::CommitRecoveryRequired);
}
if manual_review_count > 0 {
blockers.push(TableCatalogBackingMigrationBlocker::CommitManualReviewRequired);
}
if recovery_required_count > 0 || manual_review_count > 0 {
recommended_actions.push(TableCatalogBackingMigrationAction::RunCatalogRecovery);
}
if !warehouse_index_ready {
blockers.push(TableCatalogBackingMigrationBlocker::WarehouseIndexBackfillRequired);
recommended_actions.push(TableCatalogBackingMigrationAction::BackfillWarehouseIndex);
}
if duplicate_warehouse_prefix_count > 0 {
blockers.push(TableCatalogBackingMigrationBlocker::DuplicateWarehousePrefix);
recommended_actions.push(TableCatalogBackingMigrationAction::ReviewDuplicateWarehousePrefixes);
}
let mut status = if manual_review_count > 0 || duplicate_warehouse_prefix_count > 0 {
TableCatalogBackingMigrationStatus::ManualReviewRequired
} else if recovery_required_count > 0 || !warehouse_index_ready {
TableCatalogBackingMigrationStatus::RecoveryRequired
} else {
TableCatalogBackingMigrationStatus::ReadyToSnapshot
};
let strong_store = StrongTableCatalogStore::new(self.backend.clone());
let migration_fence = self.read_backing_migration_fence(table_bucket).await?.map(|(fence, _)| fence);
let object_backed_writes_fenced = migration_fence.is_some();
if status == TableCatalogBackingMigrationStatus::ReadyToSnapshot
&& let Some(fence) = migration_fence.as_ref()
&& fence.status == TableCatalogBackingMigrationFenceStatus::Materialized
&& let Some(source_fingerprint) = fence.source_fingerprint.as_deref()
{
if strong_store.bucket_snapshot_fingerprint(table_bucket).await?.as_deref() == Some(source_fingerprint) {
status = TableCatalogBackingMigrationStatus::SnapshotMaterialized;
} else {
status = TableCatalogBackingMigrationStatus::ManualReviewRequired;
blockers.push(TableCatalogBackingMigrationBlocker::DurableStrongSnapshotChanged);
recommended_actions.push(TableCatalogBackingMigrationAction::ReviewDurableStrongSnapshot);
}
}
let ready_to_enable_durable_strong = status == TableCatalogBackingMigrationStatus::SnapshotMaterialized
&& self.all_table_buckets_materialized(&strong_store).await?;
if status == TableCatalogBackingMigrationStatus::ReadyToSnapshot {
recommended_actions.extend([
TableCatalogBackingMigrationAction::SnapshotObjectBackedCatalog,
TableCatalogBackingMigrationAction::KeepObjectBackedRollbackConfig,
]);
} else if status == TableCatalogBackingMigrationStatus::SnapshotMaterialized {
recommended_actions.push(TableCatalogBackingMigrationAction::VerifyDurableStrongSnapshot);
if ready_to_enable_durable_strong {
recommended_actions.push(TableCatalogBackingMigrationAction::EnableDurableStrongBacking);
} else {
recommended_actions.push(TableCatalogBackingMigrationAction::SnapshotRemainingTableBuckets);
}
recommended_actions.push(TableCatalogBackingMigrationAction::KeepObjectBackedRollbackConfig);
}
Ok(TableCatalogBackingMigrationDryRunReport {
table_bucket: table_bucket.to_string(),
source_kind: TableCatalogBackingKind::ObjectBacked,
target_kind: TableCatalogBackingKind::StrongKvWal,
status,
namespace_count: namespaces.len(),
table_count,
view_count,
commit_log_count,
idempotency_index_count,
warehouse_prefix_count: warehouse_prefix_owners.len(),
warehouse_index_ready,
object_backed_writes_fenced,
ready_to_enable_durable_strong,
blockers,
recommended_actions,
rollback: TableCatalogBackingRollbackPlan {
backing_config_key: ENV_TABLE_CATALOG_BACKING,
current_backing_value: TABLE_CATALOG_BACKING_DURABLE_STRONG,
rollback_backing_value: TABLE_CATALOG_BACKING_OBJECT,
preserves_object_backed_catalog: true,
requires_operator_restart: true,
},
})
}
pub(crate) async fn materialize_durable_strong_backing_migration(
&self,
table_bucket: &str,
) -> TableCatalogStoreResult<TableCatalogBackingMigrationExecutionReport> {
let fence_path = self.paths.backing_migration_fence_path(table_bucket);
let fence_lock_path = self.paths.backing_migration_fence_lock_path(table_bucket);
let global_fence_path = self.paths.backing_migration_global_fence_path();
let global_fence_lock_path = self.paths.backing_migration_global_fence_lock_path();
if self.read_backing_migration_fence(table_bucket).await?.is_none() {
let preflight = self.plan_durable_strong_backing_migration(table_bucket).await?;
if preflight.status != TableCatalogBackingMigrationStatus::ReadyToSnapshot {
return Err(TableCatalogStoreError::Conflict(format!(
"table bucket {table_bucket} is not ready for durable strong snapshot materialization"
)));
}
}
let _global_fence_guard = self
.backend
.acquire_write_lock(self.catalog_bucket(), &global_fence_lock_path)
.await?;
let _fence_guard = self
.backend
.acquire_write_lock(self.catalog_bucket(), &fence_lock_path)
.await?;
let existing_fence = self.read_backing_migration_fence(table_bucket).await?;
let strong_store = StrongTableCatalogStore::new(self.backend.clone());
if let Some((fence, _)) = existing_fence.as_ref()
&& (fence.version != TABLE_CATALOG_MIGRATION_VERSION || fence.table_bucket != table_bucket)
{
return Err(TableCatalogStoreError::Invalid(format!(
"invalid durable strong migration fence for table bucket {table_bucket}"
)));
}
if !self.warehouse_index_ready(table_bucket).await? {
return Err(TableCatalogStoreError::Conflict(format!(
"table bucket {table_bucket} warehouse index must be backfilled before durable strong migration"
)));
}
let mut source_guards = Vec::new();
let source = self
.collect_bucket_snapshot_with_locks(table_bucket, &mut source_guards)
.await?;
let source_fingerprint = table_catalog_bucket_snapshot_fingerprint(&source)?;
if let Some((fence, _)) = existing_fence.as_ref()
&& fence.status == TableCatalogBackingMigrationFenceStatus::Materialized
&& fence.source_fingerprint.as_deref() != Some(source_fingerprint.as_str())
{
return Err(TableCatalogStoreError::Conflict(format!(
"object-backed catalog state no longer matches the materialized snapshot for table bucket {table_bucket}"
)));
}
self.ensure_global_backing_migration_fence(&global_fence_path).await?;
let (migration_id, target_bucket_existed) = if let Some((fence, _)) = existing_fence.as_ref() {
(fence.migration_id.clone(), fence.target_bucket_existed)
} else {
let target_bucket_existed = strong_store.bucket_snapshot_fingerprint(table_bucket).await?.is_some();
let fence = TableCatalogBackingMigrationFence {
version: TABLE_CATALOG_MIGRATION_VERSION,
table_bucket: table_bucket.to_string(),
migration_id: Uuid::new_v4().to_string(),
status: TableCatalogBackingMigrationFenceStatus::Preparing,
target_bucket_existed,
source_fingerprint: None,
target_snapshot_etag: None,
};
self.write_entry(self.catalog_bucket(), &fence_path, &fence, TableCatalogPutPrecondition::IfAbsent)
.await?;
(fence.migration_id, target_bucket_existed)
};
let (target_snapshot_etag, created) = strong_store.materialize_bucket_snapshot(source.clone()).await?;
let completed_fence = TableCatalogBackingMigrationFence {
version: TABLE_CATALOG_MIGRATION_VERSION,
table_bucket: table_bucket.to_string(),
migration_id,
status: TableCatalogBackingMigrationFenceStatus::Materialized,
target_bucket_existed,
source_fingerprint: Some(source_fingerprint.clone()),
target_snapshot_etag: Some(target_snapshot_etag.clone()),
};
self.write_entry(self.catalog_bucket(), &fence_path, &completed_fence, TableCatalogPutPrecondition::Any)
.await?;
drop(source_guards);
drop(_fence_guard);
let ready_to_enable_durable_strong = self.all_table_buckets_materialized(&strong_store).await?;
Ok(TableCatalogBackingMigrationExecutionReport {
table_bucket: table_bucket.to_string(),
source_kind: TableCatalogBackingKind::ObjectBacked,
target_kind: TableCatalogBackingKind::StrongKvWal,
status: if created {
TableCatalogBackingMigrationExecutionStatus::SnapshotMaterialized
} else {
TableCatalogBackingMigrationExecutionStatus::SnapshotAlreadyMaterialized
},
namespace_count: source.namespaces.len(),
table_count: source.tables.len(),
view_count: source.views.len(),
commit_log_count: source.commits.len(),
idempotency_index_count: source.idempotency.len(),
source_fingerprint,
target_snapshot_etag,
object_backed_writes_fenced: true,
ready_to_enable_durable_strong,
})
}
pub(crate) async fn cancel_durable_strong_backing_migration(
&self,
table_bucket: &str,
) -> TableCatalogStoreResult<TableCatalogBackingMigrationCancelReport> {
let fence_path = self.paths.backing_migration_fence_path(table_bucket);
let fence_lock_path = self.paths.backing_migration_fence_lock_path(table_bucket);
let global_fence_path = self.paths.backing_migration_global_fence_path();
let global_fence_lock_path = self.paths.backing_migration_global_fence_lock_path();
let _global_fence_guard = self
.backend
.acquire_write_lock(self.catalog_bucket(), &global_fence_lock_path)
.await?;
let _fence_guard = self
.backend
.acquire_write_lock(self.catalog_bucket(), &fence_lock_path)
.await?;
let Some((fence, _)) = self.read_backing_migration_fence(table_bucket).await? else {
self.clear_global_backing_migration_fence_if_unused(&global_fence_path)
.await?;
return Ok(TableCatalogBackingMigrationCancelReport {
table_bucket: table_bucket.to_string(),
status: TableCatalogBackingMigrationCancelStatus::NoMigrationFence,
object_backed_writes_fenced: false,
});
};
self.ensure_global_backing_migration_fence(&global_fence_path).await?;
if fence.version != TABLE_CATALOG_MIGRATION_VERSION || fence.table_bucket != table_bucket {
return Err(TableCatalogStoreError::Invalid(format!(
"invalid durable strong migration fence for table bucket {table_bucket}"
)));
}
let mut source_guards = Vec::new();
let source = self
.collect_bucket_snapshot_with_locks(table_bucket, &mut source_guards)
.await?;
let source_fingerprint = table_catalog_bucket_snapshot_fingerprint(&source)?;
if fence.status == TableCatalogBackingMigrationFenceStatus::Materialized
&& fence.source_fingerprint.as_deref() != Some(source_fingerprint.as_str())
{
return Err(TableCatalogStoreError::Conflict(format!(
"object-backed catalog state changed after materializing table bucket {table_bucket}"
)));
}
let strong_store = StrongTableCatalogStore::new(self.backend.clone());
if fence.status == TableCatalogBackingMigrationFenceStatus::Materialized
&& strong_store.bucket_snapshot_fingerprint(table_bucket).await?.as_deref() != Some(&source_fingerprint)
{
return Err(TableCatalogStoreError::Conflict(format!(
"durable strong catalog state changed after materializing table bucket {table_bucket}"
)));
}
if !fence.target_bucket_existed {
strong_store
.remove_bucket_snapshot_if_unchanged(table_bucket, &source_fingerprint)
.await?;
}
self.backend.delete_object(self.catalog_bucket(), &fence_path).await?;
self.clear_global_backing_migration_fence_if_unused(&global_fence_path)
.await?;
Ok(TableCatalogBackingMigrationCancelReport {
table_bucket: table_bucket.to_string(),
status: TableCatalogBackingMigrationCancelStatus::FenceReleased,
object_backed_writes_fenced: false,
})
}
async fn all_table_buckets_materialized(&self, strong_store: &StrongTableCatalogStore<B>) -> TableCatalogStoreResult<bool> {
if self
.read_entry::<TableCatalogBackingMigrationGlobalFence>(
self.catalog_bucket(),
&self.paths.backing_migration_global_fence_path(),
)
.await?
.is_none()
{
return Ok(false);
}
let table_bucket_objects = self
.backend
.list_objects(self.catalog_bucket(), &self.paths.table_bucket_entries_prefix())
.await?;
for table_bucket_object in table_bucket_objects
.iter()
.filter(|object| object.ends_with(TABLE_BUCKET_ENTRY_FILE))
{
let Some((entry, _)) = self
.read_entry::<TableBucketEntry>(self.catalog_bucket(), table_bucket_object)
.await?
else {
return Ok(false);
};
let Some((fence, _)) = self.read_backing_migration_fence(&entry.table_bucket).await? else {
return Ok(false);
};
if fence.status != TableCatalogBackingMigrationFenceStatus::Materialized {
return Ok(false);
}
let Some(source_fingerprint) = fence.source_fingerprint.as_deref() else {
return Ok(false);
};
if strong_store
.bucket_snapshot_fingerprint(&entry.table_bucket)
.await?
.as_deref()
!= Some(source_fingerprint)
{
return Ok(false);
}
}
Ok(true)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff