mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 05:26:50 +00:00
feat(table-catalog): finalize Iceberg REST behavior (#6072)
* feat(table-catalog): finalize Iceberg REST behavior * fix(table-catalog): address REST finalization regressions * test(table-catalog): expect REST commit conflicts * test(table-catalog): avoid serialized view test deadlocks * fix(table-catalog): adapt shared test backend * fix(table-catalog): enforce Iceberg metadata invariants * fix(table-catalog): preserve manifest length in test * test(table-catalog): use valid metadata fixtures * test(table-catalog): seed manifests before manifest lists * fix(table-catalog): restore validation gates --------- Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com> Co-authored-by: overtrue <anzhengchao@gmail.com>
This commit is contained in:
Generated
+4
@@ -278,6 +278,7 @@ checksum = "312c1ea69e5fe9966e0029fb95aca8790100b85aff4f0d3b00a9337c74069a9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bon",
|
"bon",
|
||||||
|
"crc32fast",
|
||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
"log",
|
"log",
|
||||||
"miniz_oxide 0.9.1",
|
"miniz_oxide 0.9.1",
|
||||||
@@ -289,9 +290,11 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_bytes",
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"snap",
|
||||||
"strum",
|
"strum",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9200,6 +9203,7 @@ dependencies = [
|
|||||||
"serial_test",
|
"serial_test",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"shadow-rs",
|
"shadow-rs",
|
||||||
|
"snap",
|
||||||
"socket2",
|
"socket2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
|||||||
+1
-1
@@ -171,7 +171,7 @@ tower = { version = "0.5.3" }
|
|||||||
tower-http = { version = "0.7.0" }
|
tower-http = { version = "0.7.0" }
|
||||||
|
|
||||||
# Serialization and Data Formats
|
# Serialization and Data Formats
|
||||||
apache-avro = "0.22.0"
|
apache-avro = { version = "0.22.0", features = ["snappy", "zstandard"] }
|
||||||
bytes = { version = "1.12.1" }
|
bytes = { version = "1.12.1" }
|
||||||
bytesize = "2.7.0"
|
bytesize = "2.7.0"
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
|
|||||||
@@ -61,17 +61,17 @@ catalog extension.
|
|||||||
|
|
||||||
| Area | Status | Covered behavior |
|
| Area | Status | Covered behavior |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Catalog config | Supported | `GET /v1/config` advertises RustFS catalog defaults and route capabilities. |
|
| Catalog config | Supported | `GET /v1/config` advertises RustFS catalog defaults and only the supported OpenAPI REST paths in `endpoints`. RustFS administration, maintenance, migration, diagnostics, refs, and metadata-location extensions remain available but are not presented as standard Iceberg REST endpoints. |
|
||||||
| Table bucket discovery | Supported | `PUT` and `GET /v1/buckets/{warehouse}` enable and inspect table bucket state. |
|
| Table bucket discovery | Supported | `PUT` and `GET /v1/buckets/{warehouse}` enable and inspect table bucket state. |
|
||||||
| Namespaces | Supported | Create, list, load, existence check, and drop namespace routes are registered on both catalog prefixes. List responses support Iceberg REST `pageSize`/`pageToken` pagination with context-bound tokens and bounded catalog-store reads. Namespace identifiers are limited to 512 ASCII characters so persisted paths and stateless continuation tokens remain bounded. |
|
| Namespaces | Supported | Create, list, load, existence check, and drop namespace routes are registered on both catalog prefixes. List responses support Iceberg REST `pageSize`/`pageToken` pagination with context-bound tokens and bounded catalog-store reads. Namespace identifiers are limited to 512 ASCII characters so persisted paths and stateless continuation tokens remain bounded. |
|
||||||
| Tables | Supported | Create, register, list, load, existence check, commit, metadata-location get/update, and drop table routes are registered on both catalog prefixes. Table and view listings support Iceberg REST `pageSize`/`pageToken` pagination with context-bound tokens and bounded catalog-store reads. |
|
| Tables | Supported | Create, register, list, load, existence check, commit, metadata-location get/update, and drop table routes are registered on both catalog prefixes. Table and view listings support Iceberg REST `pageSize`/`pageToken` pagination with context-bound tokens and bounded catalog-store reads. Commit identifiers must match the URL resource; unknown requirements, updates, and snapshot operations fail as bad requests; staged create, register overwrite, purge-on-drop, and v3-only encryption-key updates return an explicit unsupported-operation response. Standard statistics, partition statistics, and schema/spec cleanup updates are accepted. |
|
||||||
| Commit CAS | Supported | Single-table commits validate base metadata, expected version token, referenced object existence, warehouse scope, and Iceberg commit requirements before advancing the current metadata pointer. Standard commits preserve the normal commit-token file name and use an immutable-table-scoped fallback when rename followed by source-name reuse would otherwise collide at the same generation and commit ID. |
|
| Commit CAS | Supported | Single-table commits validate base metadata, expected version token, referenced object existence, warehouse scope, and Iceberg commit requirements before advancing the current metadata pointer. Externally supplied metadata transitions preserve monotonic column, partition, and sequence assignment watermarks and immutable definitions for retained schemas, partition specs, sort orders, and snapshots. Standard commits preserve the normal commit-token file name and use an immutable-table-scoped fallback when rename followed by source-name reuse would otherwise collide at the same generation and commit ID. The catalog does not advertise `idempotency-key-lifetime`; clients must treat standard mutation-wide `Idempotency-Key` semantics as unsupported. |
|
||||||
| Commit recovery | Supported | Commit log, idempotency lookup, diagnostics, and recovery routes expose staged/finalization gaps and repair safe idempotency gaps without moving the table pointer. |
|
| Commit recovery | Supported | Commit log, idempotency lookup, diagnostics, and recovery routes expose staged/finalization gaps and repair safe idempotency gaps without moving the table pointer. |
|
||||||
| Snapshot refs | Supported | Refs can be listed, created or replaced, and deleted through catalog commits. `main` is protected and refs with explicit retention require forced delete. |
|
| Snapshot refs | Supported | Refs can be listed, created or replaced, and deleted through catalog commits. `main` is protected and refs with explicit retention require forced delete. |
|
||||||
| Iceberg views | Supported | Basic create, list, load, replace, existence check, and drop routes persist view metadata with view-scoped authorization. |
|
| Iceberg views | Supported | Basic create, list, load, replace, existence check, and drop routes persist view metadata with view-scoped authorization. Replace identifiers must match the URL resource, `schema-id: -1` resolves to the last added schema, one commit timestamp is used consistently, and only Iceberg view format version 1 is accepted. |
|
||||||
| Table credentials endpoint | Supported | Returns an empty `storage-credentials` list by default. Returns table-scoped temporary credentials only when credential vending is enabled. |
|
| Table credentials endpoint | Supported | Returns an empty `storage-credentials` list by default. Returns table-scoped temporary credentials only when credential vending is enabled. Credential responses set `Cache-Control: no-store, private`, `Pragma: no-cache`, and `Expires: 0`. |
|
||||||
| Catalog diagnostics and export | Supported | Exposes recovery state, consistency state, backing manifest, recoverable commit-log WAL state, strong backing migration target, single-active-writer policy, and scale validation matrix. |
|
| Catalog diagnostics and export | Supported | Exposes recovery state, consistency state, backing manifest, recoverable commit-log WAL state, strong backing migration target, single-active-writer policy, and scale validation matrix. |
|
||||||
| Catalog import and rollback | Supported | Import/register and rollback use catalog validation and commit paths rather than direct pointer mutation. |
|
| Catalog import and rollback | Supported | Import/register and online rollback use catalog validation and commit paths rather than direct pointer mutation. Online rollback accepts only a forward-safe metadata target that preserves assignment watermarks and retained definitions. Restoring an older target that lowers those watermarks is an offline disaster-recovery operation and requires every writer to be stopped. |
|
||||||
| External catalog bridge | Supported operator path | Operator-supplied metadata pointer sync/import is supported for external catalog identity boundaries. Online vendor SDK polling and policy mirroring are not claimed. |
|
| External catalog bridge | Supported operator path | Operator-supplied metadata pointer sync/import is supported for external catalog identity boundaries. Online vendor SDK polling and policy mirroring are not claimed. |
|
||||||
| Multi-table transactions | Not claimed | RustFS currently claims single-table commit atomicity only. |
|
| Multi-table transactions | Not claimed | RustFS currently claims single-table commit atomicity only. |
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -278,6 +278,8 @@ rustfs-signer.workspace = true
|
|||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true, features = ["raw_value"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
serde_urlencoded = { workspace = true }
|
serde_urlencoded = { workspace = true }
|
||||||
|
snap.workspace = true
|
||||||
|
zstd.workspace = true
|
||||||
|
|
||||||
# Cryptography and Security
|
# Cryptography and Security
|
||||||
rustls = { workspace = true, default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
rustls = { workspace = true, default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
||||||
@@ -355,7 +357,6 @@ rcgen = { workspace = true }
|
|||||||
rustfs-test-utils.workspace = true
|
rustfs-test-utils.workspace = true
|
||||||
# diagnose_e2e fixtures (archives are generated in-test, never checked in)
|
# diagnose_e2e fixtures (archives are generated in-test, never checked in)
|
||||||
zip = { workspace = true }
|
zip = { workspace = true }
|
||||||
zstd = { workspace = true }
|
|
||||||
# Enables the shared MockWarmBackend / xl.meta assertion helpers exposed via
|
# Enables the shared MockWarmBackend / xl.meta assertion helpers exposed via
|
||||||
# the ecstore `api::tier::test_util` facade module (rustfs/backlog#1148 ilm-6).
|
# the ecstore `api::tier::test_util` facade module (rustfs/backlog#1148 ilm-6).
|
||||||
rustfs-ecstore = { workspace = true, features = ["test-util"] }
|
rustfs-ecstore = { workspace = true, features = ["test-util"] }
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ impl Operation for RestLoadCredentialsHandler {
|
|||||||
let issuer = IamTableCredentialIssuer::from_request(&req)?;
|
let issuer = IamTableCredentialIssuer::from_request(&req)?;
|
||||||
let response =
|
let response =
|
||||||
load_credentials_response(&store, &warehouse, &namespace, &table, &issuer, Some(&principal.credentials)).await?;
|
load_credentials_response(&store, &warehouse, &namespace, &table, &issuer, Some(&principal.credentials)).await?;
|
||||||
build_json_response(StatusCode::OK, &response)
|
build_sensitive_json_response(StatusCode::OK, &response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -125,7 +125,9 @@ impl Operation for RestLoadTableHandler {
|
|||||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||||
let response = load_table_response(&store, &metadata_backend, &warehouse, &namespace, &table).await?;
|
let snapshot_selection = rest_table_snapshot_selection_from_query(&req.uri)?;
|
||||||
|
let mut response = load_table_response(&store, &metadata_backend, &warehouse, &namespace, &table).await?;
|
||||||
|
apply_rest_table_snapshot_selection(&mut response.metadata, snapshot_selection);
|
||||||
build_json_response(StatusCode::OK, &response)
|
build_json_response(StatusCode::OK, &response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +160,7 @@ impl Operation for RestCommitTableHandler {
|
|||||||
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||||
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||||
let request = read_json_body::<RestCommitTableRequest>(std::mem::take(&mut req.input)).await?;
|
let request = read_rest_commit_table_request(std::mem::take(&mut req.input)).await?;
|
||||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||||
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||||
@@ -178,6 +180,14 @@ impl Operation for RestDropTableHandler {
|
|||||||
let table = table_name_from_params(¶ms)?;
|
let table = table_name_from_params(¶ms)?;
|
||||||
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
let resource = TableCatalogResource::table(&warehouse, &namespace, &table);
|
||||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::DeleteTableAction).await?;
|
authorize_table_catalog_resource_request(&req, &resource, AdminAction::DeleteTableAction).await?;
|
||||||
|
let purge_requested = rest_purge_requested_from_query(&req.uri)?;
|
||||||
|
if purge_requested {
|
||||||
|
return Err(iceberg_rest_error(
|
||||||
|
ICEBERG_ERROR_UNSUPPORTED_OPERATION,
|
||||||
|
StatusCode::NOT_ACCEPTABLE,
|
||||||
|
"purgeRequested=true is not supported",
|
||||||
|
));
|
||||||
|
}
|
||||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||||
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
let store = table_catalog_store_from_extensions(&req.extensions)?;
|
||||||
drop_table_in_store(&store, &warehouse, &namespace, &table).await?;
|
drop_table_in_store(&store, &warehouse, &namespace, &table).await?;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -43,8 +43,9 @@ impl Operation for RestCreateViewHandler {
|
|||||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||||
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
let table_bucket_enabled = table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||||
|
let publication_backend = TableCommitObjectBackend::preauthorized(metadata_backend);
|
||||||
let response =
|
let response =
|
||||||
create_view_response(&store, &metadata_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
|
create_view_response(&store, &publication_backend, &warehouse, &namespace, request, table_bucket_enabled).await?;
|
||||||
build_json_response(StatusCode::OK, &response)
|
build_json_response(StatusCode::OK, &response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,17 +88,20 @@ pub struct RestReplaceViewHandler {}
|
|||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Operation for RestReplaceViewHandler {
|
impl Operation for RestReplaceViewHandler {
|
||||||
async fn call(&self, req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
async fn call(&self, mut req: S3Request<Body>, params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||||
let warehouse = warehouse_from_params(¶ms)?;
|
let warehouse = warehouse_from_params(¶ms)?;
|
||||||
let namespace = namespace_from_params(¶ms)?;
|
let namespace = namespace_from_params(¶ms)?;
|
||||||
let view = view_name_from_params(¶ms)?;
|
let view = view_name_from_params(¶ms)?;
|
||||||
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
|
let resource = TableCatalogResource::view(&warehouse, &namespace, &view);
|
||||||
authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
let principal = authorize_table_catalog_resource_request(&req, &resource, AdminAction::CommitTableAction).await?;
|
||||||
|
install_table_catalog_s3_request_info(&mut req, &principal)?;
|
||||||
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
ensure_table_bucket_enabled_from_extensions(&req.extensions, &warehouse).await?;
|
||||||
let request = read_json_body::<RestCommitViewRequest>(req.input).await?;
|
let request = read_rest_commit_view_request(std::mem::take(&mut req.input)).await?;
|
||||||
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
let metadata_backend = table_catalog_backend_from_extensions(&req.extensions)?;
|
||||||
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
let store = table_catalog_store_from_backend(metadata_backend.clone())?;
|
||||||
let response = replace_view_response(&store, &metadata_backend, &warehouse, &namespace, &view, request).await?;
|
let commit_backend = TableCommitObjectBackend::for_request(metadata_backend, req);
|
||||||
|
let result = replace_view_response(&store, &commit_backend, &warehouse, &namespace, &view, request).await;
|
||||||
|
let response = commit_backend.finish(result).await?;
|
||||||
build_json_response(StatusCode::OK, &response)
|
build_json_response(StatusCode::OK, &response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1478,7 +1478,7 @@ async fn retain_table_data_plane_publication_guard<T>(
|
|||||||
.map_err(|err| s3_error!(InternalError, "failed to acquire table publication guard: {}", err))?;
|
.map_err(|err| s3_error!(InternalError, "failed to acquire table publication guard: {}", err))?;
|
||||||
let mut state = retained.state.lock();
|
let mut state = retained.state.lock();
|
||||||
state.keys.insert(key);
|
state.keys.insert(key);
|
||||||
state.guards.push(guard);
|
state.guards.push(Box::new(guard));
|
||||||
drop(state);
|
drop(state);
|
||||||
req.extensions.insert(retained);
|
req.extensions.insert(retained);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ use std::io::Read;
|
|||||||
|
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
|
||||||
|
const AVRO_ZSTANDARD_MAX_WINDOW_LOG: u32 = 27;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) struct ManifestDataFileReference {
|
pub(crate) struct ManifestDataFileReference {
|
||||||
pub location: String,
|
pub location: String,
|
||||||
@@ -66,6 +68,7 @@ pub(crate) struct DecodedManifestList {
|
|||||||
pub(crate) struct DecodedManifest {
|
pub(crate) struct DecodedManifest {
|
||||||
pub references: Vec<ManifestDataFileReference>,
|
pub references: Vec<ManifestDataFileReference>,
|
||||||
pub decoded_size: usize,
|
pub decoded_size: usize,
|
||||||
|
pub partition_spec_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn manifest_paths_from_manifest_list_avro(data: &[u8]) -> TableCatalogStoreResult<Vec<String>> {
|
pub(crate) fn manifest_paths_from_manifest_list_avro(data: &[u8]) -> TableCatalogStoreResult<Vec<String>> {
|
||||||
@@ -92,6 +95,25 @@ pub(crate) fn decode_manifest_list_avro(data: &[u8]) -> TableCatalogStoreResult<
|
|||||||
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest list Avro: {err}")))?;
|
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest list Avro: {err}")))?;
|
||||||
let format_version =
|
let format_version =
|
||||||
avro_record_format_version(reader.writer_schema(), &["sequence_number", "min_sequence_number"], "manifest list")?;
|
avro_record_format_version(reader.writer_schema(), &["sequence_number", "min_sequence_number"], "manifest list")?;
|
||||||
|
if format_version == 2 {
|
||||||
|
let apache_avro::Schema::Record(record) = reader.writer_schema() else {
|
||||||
|
return Err(TableCatalogStoreError::Invalid("manifest list Avro schema must be a record".to_string()));
|
||||||
|
};
|
||||||
|
for field in [
|
||||||
|
"added_files_count",
|
||||||
|
"existing_files_count",
|
||||||
|
"deleted_files_count",
|
||||||
|
"added_rows_count",
|
||||||
|
"existing_rows_count",
|
||||||
|
"deleted_rows_count",
|
||||||
|
] {
|
||||||
|
if !record.lookup.contains_key(field) {
|
||||||
|
return Err(TableCatalogStoreError::Invalid(format!(
|
||||||
|
"Iceberg v2 manifest list Avro schema is missing {field}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut manifest_paths = Vec::new();
|
let mut manifest_paths = Vec::new();
|
||||||
for value in reader {
|
for value in reader {
|
||||||
if manifest_paths.len() >= TABLE_MANIFEST_AVRO_MAX_RECORDS {
|
if manifest_paths.len() >= TABLE_MANIFEST_AVRO_MAX_RECORDS {
|
||||||
@@ -115,24 +137,12 @@ pub(crate) fn decode_manifest_list_avro(data: &[u8]) -> TableCatalogStoreResult<
|
|||||||
sequence_number: avro_record_field(&value, "sequence_number").and_then(avro_i64_value),
|
sequence_number: avro_record_field(&value, "sequence_number").and_then(avro_i64_value),
|
||||||
min_sequence_number: avro_record_field(&value, "min_sequence_number").and_then(avro_i64_value),
|
min_sequence_number: avro_record_field(&value, "min_sequence_number").and_then(avro_i64_value),
|
||||||
added_snapshot_id: avro_record_field(&value, "added_snapshot_id").and_then(avro_i64_value),
|
added_snapshot_id: avro_record_field(&value, "added_snapshot_id").and_then(avro_i64_value),
|
||||||
added_files_count: avro_record_field(&value, "added_files_count")
|
added_files_count: avro_nullable_non_negative_i32(&value, "added_files_count", "manifest list")?,
|
||||||
.and_then(avro_i32_value)
|
existing_files_count: avro_nullable_non_negative_i32(&value, "existing_files_count", "manifest list")?,
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
deleted_files_count: avro_nullable_non_negative_i32(&value, "deleted_files_count", "manifest list")?,
|
||||||
existing_files_count: avro_record_field(&value, "existing_files_count")
|
added_rows_count: avro_nullable_non_negative_i64(&value, "added_rows_count", "manifest list")?,
|
||||||
.and_then(avro_i32_value)
|
existing_rows_count: avro_nullable_non_negative_i64(&value, "existing_rows_count", "manifest list")?,
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
deleted_rows_count: avro_nullable_non_negative_i64(&value, "deleted_rows_count", "manifest list")?,
|
||||||
deleted_files_count: avro_record_field(&value, "deleted_files_count")
|
|
||||||
.and_then(avro_i32_value)
|
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
|
||||||
added_rows_count: avro_record_field(&value, "added_rows_count")
|
|
||||||
.and_then(avro_i64_value)
|
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
|
||||||
existing_rows_count: avro_record_field(&value, "existing_rows_count")
|
|
||||||
.and_then(avro_i64_value)
|
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
|
||||||
deleted_rows_count: avro_record_field(&value, "deleted_rows_count")
|
|
||||||
.and_then(avro_i64_value)
|
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(DecodedManifestList {
|
Ok(DecodedManifestList {
|
||||||
@@ -171,6 +181,17 @@ pub(crate) fn decode_manifest_avro(data: &[u8]) -> TableCatalogStoreResult<Decod
|
|||||||
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest Avro: {err}")))?;
|
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to read manifest Avro: {err}")))?;
|
||||||
let format_version =
|
let format_version =
|
||||||
avro_record_format_version(reader.writer_schema(), &["sequence_number", "file_sequence_number"], "manifest")?;
|
avro_record_format_version(reader.writer_schema(), &["sequence_number", "file_sequence_number"], "manifest")?;
|
||||||
|
let partition_spec_id = reader
|
||||||
|
.user_metadata()
|
||||||
|
.get("partition-spec-id")
|
||||||
|
.map(|value| {
|
||||||
|
std::str::from_utf8(value)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse::<i32>().ok())
|
||||||
|
.filter(|value| *value >= 0)
|
||||||
|
.ok_or_else(|| TableCatalogStoreError::Invalid("manifest partition-spec-id metadata is invalid".to_string()))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
for value in reader {
|
for value in reader {
|
||||||
if files.len() >= TABLE_MANIFEST_AVRO_MAX_RECORDS {
|
if files.len() >= TABLE_MANIFEST_AVRO_MAX_RECORDS {
|
||||||
@@ -202,6 +223,9 @@ pub(crate) fn decode_manifest_avro(data: &[u8]) -> TableCatalogStoreResult<Decod
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let partition = avro_record_field(data_file, "partition")
|
||||||
|
.and_then(avro_record_value_fields)
|
||||||
|
.ok_or_else(|| TableCatalogStoreError::Invalid("manifest data file partition must be a record".to_string()))?;
|
||||||
files.push(ManifestDataFileReference {
|
files.push(ManifestDataFileReference {
|
||||||
location: file_path.to_string(),
|
location: file_path.to_string(),
|
||||||
format_version,
|
format_version,
|
||||||
@@ -218,15 +242,14 @@ pub(crate) fn decode_manifest_avro(data: &[u8]) -> TableCatalogStoreResult<Decod
|
|||||||
file_size_bytes: avro_record_field(data_file, "file_size_in_bytes")
|
file_size_bytes: avro_record_field(data_file, "file_size_in_bytes")
|
||||||
.and_then(avro_i64_value)
|
.and_then(avro_i64_value)
|
||||||
.and_then(|value| u64::try_from(value).ok()),
|
.and_then(|value| u64::try_from(value).ok()),
|
||||||
partition: avro_record_field(data_file, "partition")
|
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),
|
sort_order_id: avro_record_field(data_file, "sort_order_id").and_then(avro_i32_value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(DecodedManifest {
|
Ok(DecodedManifest {
|
||||||
references: files,
|
references: files,
|
||||||
decoded_size,
|
decoded_size,
|
||||||
|
partition_spec_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +263,8 @@ pub(crate) async fn decode_manifest_avro_async(data: Vec<u8>) -> TableCatalogSto
|
|||||||
enum AvroContainerCodec {
|
enum AvroContainerCodec {
|
||||||
Null,
|
Null,
|
||||||
Deflate,
|
Deflate,
|
||||||
|
Snappy,
|
||||||
|
Zstandard,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_avro_container(data: &[u8]) -> TableCatalogStoreResult<usize> {
|
fn validate_avro_container(data: &[u8]) -> TableCatalogStoreResult<usize> {
|
||||||
@@ -300,6 +325,8 @@ fn validate_avro_container(data: &[u8]) -> TableCatalogStoreResult<usize> {
|
|||||||
let codec = match codec.unwrap_or(b"null") {
|
let codec = match codec.unwrap_or(b"null") {
|
||||||
b"null" => AvroContainerCodec::Null,
|
b"null" => AvroContainerCodec::Null,
|
||||||
b"deflate" => AvroContainerCodec::Deflate,
|
b"deflate" => AvroContainerCodec::Deflate,
|
||||||
|
b"snappy" => AvroContainerCodec::Snappy,
|
||||||
|
b"zstandard" => AvroContainerCodec::Zstandard,
|
||||||
codec => {
|
codec => {
|
||||||
return Err(TableCatalogStoreError::Unsupported(format!(
|
return Err(TableCatalogStoreError::Unsupported(format!(
|
||||||
"Avro codec {} is not supported for table commit validation",
|
"Avro codec {} is not supported for table commit validation",
|
||||||
@@ -369,6 +396,34 @@ fn avro_block_decoded_size(codec: AvroContainerCodec, block: &[u8], remaining_si
|
|||||||
usize::try_from(decoded_size)
|
usize::try_from(decoded_size)
|
||||||
.map_err(|_| TableCatalogStoreError::Invalid("Avro decoded data size is invalid".to_string()))
|
.map_err(|_| TableCatalogStoreError::Invalid("Avro decoded data size is invalid".to_string()))
|
||||||
}
|
}
|
||||||
|
AvroContainerCodec::Snappy => {
|
||||||
|
let data_end = block
|
||||||
|
.len()
|
||||||
|
.checked_sub(4)
|
||||||
|
.ok_or_else(|| TableCatalogStoreError::Invalid("Avro snappy block is missing its checksum".to_string()))?;
|
||||||
|
let decoded_size = snap::raw::decompress_len(&block[..data_end])
|
||||||
|
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to inspect Avro snappy block: {err}")))?;
|
||||||
|
if decoded_size > remaining_size {
|
||||||
|
return Err(TableCatalogStoreError::Invalid("Avro decoded data exceeds the commit limit".to_string()));
|
||||||
|
}
|
||||||
|
Ok(decoded_size)
|
||||||
|
}
|
||||||
|
AvroContainerCodec::Zstandard => {
|
||||||
|
let limit = remaining_size
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| TableCatalogStoreError::Invalid("Avro decoded data size limit overflowed".to_string()))?;
|
||||||
|
let limit = u64::try_from(limit)
|
||||||
|
.map_err(|_| TableCatalogStoreError::Invalid("Avro decoded data size limit is invalid".to_string()))?;
|
||||||
|
let mut decoder = zstd::stream::read::Decoder::new(block)
|
||||||
|
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to decompress Avro zstandard block: {err}")))?;
|
||||||
|
decoder
|
||||||
|
.window_log_max(AVRO_ZSTANDARD_MAX_WINDOW_LOG)
|
||||||
|
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to bound Avro zstandard window: {err}")))?;
|
||||||
|
let decoded_size = std::io::copy(&mut decoder.take(limit), &mut std::io::sink())
|
||||||
|
.map_err(|err| TableCatalogStoreError::Invalid(format!("failed to decompress Avro zstandard block: {err}")))?;
|
||||||
|
usize::try_from(decoded_size)
|
||||||
|
.map_err(|_| TableCatalogStoreError::Invalid("Avro decoded data size is invalid".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +500,40 @@ fn avro_record_value_fields(value: &apache_avro::types::Value) -> Option<Vec<(St
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn avro_nullable_non_negative_i32(
|
||||||
|
value: &apache_avro::types::Value,
|
||||||
|
field: &str,
|
||||||
|
label: &str,
|
||||||
|
) -> TableCatalogStoreResult<Option<u64>> {
|
||||||
|
let Some(value) = avro_record_field(value, field) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
match avro_non_union_value(value) {
|
||||||
|
apache_avro::types::Value::Null => Ok(None),
|
||||||
|
apache_avro::types::Value::Int(value) => u64::try_from(*value)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| TableCatalogStoreError::Invalid(format!("{label} field {field} must be a non-negative int"))),
|
||||||
|
_ => Err(TableCatalogStoreError::Invalid(format!("{label} field {field} must be a nullable int"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn avro_nullable_non_negative_i64(
|
||||||
|
value: &apache_avro::types::Value,
|
||||||
|
field: &str,
|
||||||
|
label: &str,
|
||||||
|
) -> TableCatalogStoreResult<Option<u64>> {
|
||||||
|
let Some(value) = avro_record_field(value, field) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
match avro_non_union_value(value) {
|
||||||
|
apache_avro::types::Value::Null => Ok(None),
|
||||||
|
apache_avro::types::Value::Long(value) => u64::try_from(*value)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| TableCatalogStoreError::Invalid(format!("{label} field {field} must be a non-negative long"))),
|
||||||
|
_ => Err(TableCatalogStoreError::Invalid(format!("{label} field {field} must be a nullable long"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn avro_non_union_value(value: &apache_avro::types::Value) -> &apache_avro::types::Value {
|
pub(crate) fn avro_non_union_value(value: &apache_avro::types::Value) -> &apache_avro::types::Value {
|
||||||
match value {
|
match value {
|
||||||
apache_avro::types::Value::Union(_, inner) => avro_non_union_value(inner),
|
apache_avro::types::Value::Union(_, inner) => avro_non_union_value(inner),
|
||||||
@@ -472,3 +561,127 @@ fn avro_i64_value(value: &apache_avro::types::Value) -> Option<i64> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_v2_manifest_lists_without_required_count_fields() {
|
||||||
|
let schema = apache_avro::Schema::parse_str(
|
||||||
|
r#"{
|
||||||
|
"type": "record",
|
||||||
|
"name": "manifest_file",
|
||||||
|
"fields": [
|
||||||
|
{"name": "manifest_path", "type": "string"},
|
||||||
|
{"name": "manifest_length", "type": "long"},
|
||||||
|
{"name": "partition_spec_id", "type": "int"},
|
||||||
|
{"name": "content", "type": "int"},
|
||||||
|
{"name": "sequence_number", "type": "long"},
|
||||||
|
{"name": "min_sequence_number", "type": "long"},
|
||||||
|
{"name": "added_snapshot_id", "type": "long"}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("incomplete manifest-list schema should parse");
|
||||||
|
let data = apache_avro::Writer::new(&schema, Vec::new())
|
||||||
|
.expect("manifest-list writer should initialize")
|
||||||
|
.into_inner()
|
||||||
|
.expect("manifest-list bytes should flush");
|
||||||
|
|
||||||
|
let error = match decode_manifest_list_avro(&data) {
|
||||||
|
Ok(_) => panic!("v2 count fields must be declared in the writer schema"),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
TableCatalogStoreError::Invalid("Iceberg v2 manifest list Avro schema is missing added_files_count".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_negative_nullable_manifest_list_counts() {
|
||||||
|
let value = apache_avro::types::Value::Record(vec![
|
||||||
|
("added_files_count".to_string(), apache_avro::types::Value::Int(-1)),
|
||||||
|
("added_rows_count".to_string(), apache_avro::types::Value::Long(-1)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
avro_nullable_non_negative_i32(&value, "added_files_count", "manifest list")
|
||||||
|
.expect_err("negative file counts must be rejected"),
|
||||||
|
TableCatalogStoreError::Invalid("manifest list field added_files_count must be a non-negative int".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
avro_nullable_non_negative_i64(&value, "added_rows_count", "manifest list")
|
||||||
|
.expect_err("negative row counts must be rejected"),
|
||||||
|
TableCatalogStoreError::Invalid("manifest list field added_rows_count must be a non-negative long".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_manifest_partition_with_non_record_schema() {
|
||||||
|
let schema = apache_avro::Schema::parse_str(
|
||||||
|
r#"{
|
||||||
|
"type": "record",
|
||||||
|
"name": "manifest_entry",
|
||||||
|
"fields": [
|
||||||
|
{"name": "status", "type": "int"},
|
||||||
|
{"name": "snapshot_id", "type": "long"},
|
||||||
|
{
|
||||||
|
"name": "data_file",
|
||||||
|
"type": {
|
||||||
|
"type": "record",
|
||||||
|
"name": "data_file",
|
||||||
|
"fields": [
|
||||||
|
{"name": "file_path", "type": "string"},
|
||||||
|
{"name": "record_count", "type": "long"},
|
||||||
|
{"name": "file_size_in_bytes", "type": "long"},
|
||||||
|
{"name": "partition", "type": "string"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("manifest schema should parse");
|
||||||
|
let mut writer = apache_avro::Writer::new(&schema, Vec::new()).expect("manifest writer should initialize");
|
||||||
|
writer
|
||||||
|
.append_value(apache_avro::types::Value::Record(vec![
|
||||||
|
("status".to_string(), apache_avro::types::Value::Int(1)),
|
||||||
|
("snapshot_id".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
|
(
|
||||||
|
"data_file".to_string(),
|
||||||
|
apache_avro::types::Value::Record(vec![
|
||||||
|
(
|
||||||
|
"file_path".to_string(),
|
||||||
|
apache_avro::types::Value::String("s3://warehouse/tables/table-id/data/file.parquet".to_string()),
|
||||||
|
),
|
||||||
|
("record_count".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
|
("file_size_in_bytes".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
|
("partition".to_string(), apache_avro::types::Value::String("not-a-record".to_string())),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
.expect("manifest record should append");
|
||||||
|
let data = writer.into_inner().expect("manifest bytes should flush");
|
||||||
|
|
||||||
|
let error = match decode_manifest_avro(&data) {
|
||||||
|
Ok(_) => panic!("manifest partitions must preserve their record shape"),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
TableCatalogStoreError::Invalid("manifest data file partition must be a record".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_oversized_zstandard_windows() {
|
||||||
|
// Non-single-segment frame with a 2^28-byte window and one empty final block.
|
||||||
|
let compressed = [0x28, 0xb5, 0x2f, 0xfd, 0x00, 0x90, 0x01, 0x00, 0x00];
|
||||||
|
|
||||||
|
let error = avro_block_decoded_size(AvroContainerCodec::Zstandard, &compressed, TABLE_MANIFEST_AVRO_MAX_DECODED_SIZE)
|
||||||
|
.expect_err("zstandard windows larger than the manifest decode budget must be rejected");
|
||||||
|
assert!(matches!(error, TableCatalogStoreError::Invalid(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -111,8 +111,12 @@ const TABLE_MANIFEST_AVRO_MAX_DECODED_SIZE: usize = 128 * 1024 * 1024;
|
|||||||
const TABLE_MANIFEST_AVRO_MAX_RECORDS: usize = 1_000_000;
|
const TABLE_MANIFEST_AVRO_MAX_RECORDS: usize = 1_000_000;
|
||||||
const TABLE_MANIFEST_AVRO_MAX_HEADER_ENTRIES: usize = 1_024;
|
const TABLE_MANIFEST_AVRO_MAX_HEADER_ENTRIES: usize = 1_024;
|
||||||
const TABLE_COMMIT_MAX_MANIFESTS: usize = 10_000;
|
const TABLE_COMMIT_MAX_MANIFESTS: usize = 10_000;
|
||||||
|
const TABLE_COMMIT_MAX_MANIFEST_TRAVERSALS: usize = 20_000;
|
||||||
const TABLE_COMMIT_MAX_AVRO_BYTES: usize = 512 * 1024 * 1024;
|
const TABLE_COMMIT_MAX_AVRO_BYTES: usize = 512 * 1024 * 1024;
|
||||||
const TABLE_COMMIT_MAX_FILE_REFERENCES: usize = 1_000_000;
|
const TABLE_COMMIT_MAX_FILE_REFERENCES: usize = 1_000_000;
|
||||||
|
const TABLE_COMMIT_MAX_STATISTICS_OBJECTS: usize = 1_024;
|
||||||
|
const TABLE_COMMIT_MAX_STATISTICS_BYTES: usize = 512 * 1024 * 1024;
|
||||||
|
const TABLE_STATISTICS_FILE_MAX_SIZE: usize = 128 * 1024 * 1024;
|
||||||
pub(crate) const TABLE_COMMIT_OBJECT_VALIDATION_CONCURRENCY: usize = 16;
|
pub(crate) const TABLE_COMMIT_OBJECT_VALIDATION_CONCURRENCY: usize = 16;
|
||||||
pub const TABLE_RESERVED_PREFIX: &str = BUCKET_TABLE_RESERVED_PREFIX;
|
pub const TABLE_RESERVED_PREFIX: &str = BUCKET_TABLE_RESERVED_PREFIX;
|
||||||
const WAREHOUSE_ROOT: &str = "warehouses";
|
const WAREHOUSE_ROOT: &str = "warehouses";
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ where
|
|||||||
Ok(fence)
|
Ok(fence)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn acquire_table_bucket_registry_write_permit(&self) -> TableCatalogStoreResult<Box<dyn Send>> {
|
pub(super) async fn acquire_table_bucket_registry_write_permit(&self) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
let fence_path = self.paths.backing_migration_global_fence_path();
|
let fence_path = self.paths.backing_migration_global_fence_path();
|
||||||
let lock_path = self.paths.backing_migration_global_fence_lock_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?;
|
let guard = self.backend.acquire_read_lock(self.catalog_bucket(), &lock_path).await?;
|
||||||
@@ -235,7 +235,7 @@ where
|
|||||||
pub(super) async fn acquire_object_backed_catalog_write_permit(
|
pub(super) async fn acquire_object_backed_catalog_write_permit(
|
||||||
&self,
|
&self,
|
||||||
table_bucket: &str,
|
table_bucket: &str,
|
||||||
) -> TableCatalogStoreResult<Box<dyn Send>> {
|
) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
let lock_path = self.paths.backing_migration_fence_lock_path(table_bucket);
|
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?;
|
let guard = self.backend.acquire_read_lock(self.catalog_bucket(), &lock_path).await?;
|
||||||
if self.read_backing_migration_fence(table_bucket).await?.is_some() {
|
if self.read_backing_migration_fence(table_bucket).await?.is_some() {
|
||||||
@@ -290,7 +290,7 @@ where
|
|||||||
async fn collect_bucket_snapshot_with_locks(
|
async fn collect_bucket_snapshot_with_locks(
|
||||||
&self,
|
&self,
|
||||||
table_bucket: &str,
|
table_bucket: &str,
|
||||||
guards: &mut Vec<Box<dyn Send>>,
|
guards: &mut Vec<TableCatalogLockGuard>,
|
||||||
) -> TableCatalogStoreResult<StrongTableCatalogBucketSnapshot> {
|
) -> TableCatalogStoreResult<StrongTableCatalogBucketSnapshot> {
|
||||||
let bucket_path = self.paths.table_bucket_entry_path(table_bucket);
|
let bucket_path = self.paths.table_bucket_entry_path(table_bucket);
|
||||||
guards.push(self.backend.acquire_write_lock(self.catalog_bucket(), &bucket_path).await?);
|
guards.push(self.backend.acquire_write_lock(self.catalog_bucket(), &bucket_path).await?);
|
||||||
|
|||||||
@@ -262,6 +262,29 @@ pub(crate) trait TableCatalogStore: Send + Sync {
|
|||||||
|
|
||||||
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()>;
|
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()>;
|
||||||
|
|
||||||
|
async fn create_view_with_publication(
|
||||||
|
&self,
|
||||||
|
entry: ViewEntry,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<()> {
|
||||||
|
publication.begin_table_bucket(&entry.table_bucket).await?;
|
||||||
|
if !publication.holds_table_bucket(&entry.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
publication
|
||||||
|
.prepare(&entry.table_bucket, &entry.namespace, &entry.view)
|
||||||
|
.await?;
|
||||||
|
if !publication.holds_table(&entry.table_bucket, &entry.namespace, &entry.view) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation requires a view publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||||
|
self.create_view(entry).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>>;
|
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>>;
|
||||||
|
|
||||||
async fn list_views_page(
|
async fn list_views_page(
|
||||||
@@ -283,6 +306,32 @@ pub(crate) trait TableCatalogStore: Send + Sync {
|
|||||||
|
|
||||||
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult>;
|
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult>;
|
||||||
|
|
||||||
|
async fn replace_view_with_publication(
|
||||||
|
&self,
|
||||||
|
request: ViewCommitRequest,
|
||||||
|
table_bucket_fence_required: bool,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
if table_bucket_fence_required {
|
||||||
|
publication.begin_table_bucket(&request.table_bucket).await?;
|
||||||
|
if !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publication
|
||||||
|
.prepare(&request.table_bucket, &request.namespace, &request.view)
|
||||||
|
.await?;
|
||||||
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.view) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement requires a view publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||||
|
self.replace_view(request).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn drop_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<()>;
|
async fn drop_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<()>;
|
||||||
|
|
||||||
async fn get_commit_by_id(
|
async fn get_commit_by_id(
|
||||||
@@ -338,7 +387,7 @@ struct TableCommitLockPublication<'a, B> {
|
|||||||
struct TableCommitLockPublicationState {
|
struct TableCommitLockPublicationState {
|
||||||
table_bucket: Option<String>,
|
table_bucket: Option<String>,
|
||||||
table: Option<(String, String, String)>,
|
table: Option<(String, String, String)>,
|
||||||
guards: Vec<Box<dyn Send>>,
|
guards: Vec<TableCatalogLockGuard>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, B> TableCommitLockPublication<'a, B> {
|
impl<'a, B> TableCommitLockPublication<'a, B> {
|
||||||
@@ -409,15 +458,17 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn holds_table_bucket(&self, table_bucket: &str) -> bool {
|
fn holds_table_bucket(&self, table_bucket: &str) -> bool {
|
||||||
self.state.lock().table_bucket.as_deref() == Some(table_bucket)
|
let state = self.state.lock();
|
||||||
|
state.table_bucket.as_deref() == Some(table_bucket) && state.guards.iter().all(|guard| !guard.is_lock_lost())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn holds_table(&self, table_bucket: &str, namespace: &str, table: &str) -> bool {
|
fn holds_table(&self, table_bucket: &str, namespace: &str, table: &str) -> bool {
|
||||||
self.state
|
let state = self.state.lock();
|
||||||
.lock()
|
state
|
||||||
.table
|
.table
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|held| held.0 == table_bucket && held.1 == namespace && held.2 == table)
|
.is_some_and(|held| held.0 == table_bucket && held.1 == namespace && held.2 == table)
|
||||||
|
&& state.guards.iter().all(|guard| !guard.is_lock_lost())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete(&self) {
|
fn complete(&self) {
|
||||||
@@ -438,6 +489,32 @@ pub(crate) struct TableCatalogObjectMetadata {
|
|||||||
pub mod_time: Option<OffsetDateTime>,
|
pub mod_time: Option<OffsetDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct TableCatalogLockGuard {
|
||||||
|
_guard: Box<dyn Send>,
|
||||||
|
lock_lost: Option<Arc<rustfs_lock::distributed_lock::LockLostSignal>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableCatalogLockGuard {
|
||||||
|
pub(crate) fn stable(guard: impl Send + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
_guard: Box::new(guard),
|
||||||
|
lock_lost: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn namespace(guard: rustfs_lock::NamespaceLockGuard) -> Self {
|
||||||
|
let lock_lost = guard.lock_lost_signal();
|
||||||
|
Self {
|
||||||
|
_guard: Box::new(guard),
|
||||||
|
lock_lost,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_lock_lost(&self) -> bool {
|
||||||
|
self.lock_lost.as_ref().is_some_and(|signal| signal.is_lost())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) struct TableCatalogObjectListPage {
|
pub(crate) struct TableCatalogObjectListPage {
|
||||||
pub objects: Vec<String>,
|
pub objects: Vec<String>,
|
||||||
@@ -588,11 +665,11 @@ pub(crate) trait TableCatalogObjectBackend: Clone + Send + Sync + 'static {
|
|||||||
Ok(TableCatalogObjectListPage { objects, is_truncated })
|
Ok(TableCatalogObjectListPage { objects, is_truncated })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_read_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>> {
|
async fn acquire_read_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
self.acquire_write_lock(bucket, object).await
|
self.acquire_write_lock(bucket, object).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>>;
|
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<TableCatalogLockGuard>;
|
||||||
|
|
||||||
async fn begin_table_bucket_commit_publication(&self, _table_bucket: &str) -> TableCatalogStoreResult<()> {
|
async fn begin_table_bucket_commit_publication(&self, _table_bucket: &str) -> TableCatalogStoreResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1169,6 +1246,17 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_view_with_publication(
|
||||||
|
&self,
|
||||||
|
entry: ViewEntry,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<()> {
|
||||||
|
match self {
|
||||||
|
Self::ObjectBacked(store) => store.create_view_with_publication(entry, publication).await,
|
||||||
|
Self::DurableStrong(store) => store.create_view_with_publication(entry, publication).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>> {
|
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>> {
|
||||||
match self {
|
match self {
|
||||||
Self::ObjectBacked(store) => store.list_views(table_bucket, namespace).await,
|
Self::ObjectBacked(store) => store.list_views(table_bucket, namespace).await,
|
||||||
@@ -1203,6 +1291,26 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn replace_view_with_publication(
|
||||||
|
&self,
|
||||||
|
request: ViewCommitRequest,
|
||||||
|
table_bucket_fence_required: bool,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
match self {
|
||||||
|
Self::ObjectBacked(store) => {
|
||||||
|
store
|
||||||
|
.replace_view_with_publication(request, table_bucket_fence_required, publication)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Self::DurableStrong(store) => {
|
||||||
|
store
|
||||||
|
.replace_view_with_publication(request, table_bucket_fence_required, publication)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn drop_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<()> {
|
async fn drop_view(&self, table_bucket: &str, namespace: &str, view: &str) -> TableCatalogStoreResult<()> {
|
||||||
match self {
|
match self {
|
||||||
Self::ObjectBacked(store) => store.drop_view(table_bucket, namespace, view).await,
|
Self::ObjectBacked(store) => store.drop_view(table_bucket, namespace, view).await,
|
||||||
@@ -1686,7 +1794,7 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>> {
|
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
let lock = self
|
let lock = self
|
||||||
.store
|
.store
|
||||||
.new_ns_lock(bucket, object)
|
.new_ns_lock(bucket, object)
|
||||||
@@ -1696,10 +1804,10 @@ where
|
|||||||
.get_write_lock(get_lock_acquire_timeout())
|
.get_write_lock(get_lock_acquire_timeout())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| TableCatalogStoreError::Internal(format!("failed to acquire catalog table lock: {err}")))?;
|
.map_err(|err| TableCatalogStoreError::Internal(format!("failed to acquire catalog table lock: {err}")))?;
|
||||||
Ok(Box::new(guard))
|
Ok(TableCatalogLockGuard::namespace(guard))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_read_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>> {
|
async fn acquire_read_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
let lock = self
|
let lock = self
|
||||||
.store
|
.store
|
||||||
.new_ns_lock(bucket, object)
|
.new_ns_lock(bucket, object)
|
||||||
@@ -1709,7 +1817,7 @@ where
|
|||||||
.get_read_lock(get_lock_acquire_timeout())
|
.get_read_lock(get_lock_acquire_timeout())
|
||||||
.await
|
.await
|
||||||
.map_err(|err| TableCatalogStoreError::Internal(format!("failed to acquire catalog migration lock: {err}")))?;
|
.map_err(|err| TableCatalogStoreError::Internal(format!("failed to acquire catalog migration lock: {err}")))?;
|
||||||
Ok(Box::new(guard))
|
Ok(TableCatalogLockGuard::namespace(guard))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1036,6 +1036,20 @@ where
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn restore_table_warehouse_index_after_failed_drop(&self, entry: &TableEntry, reason: &'static str) {
|
||||||
|
if let Err(err) = self.reserve_table_warehouse_index(entry).await {
|
||||||
|
tracing::warn!(
|
||||||
|
table_bucket = %entry.table_bucket,
|
||||||
|
namespace = %entry.namespace,
|
||||||
|
table = %entry.table,
|
||||||
|
table_id = %entry.table_id,
|
||||||
|
reason,
|
||||||
|
error = %err,
|
||||||
|
"failed to restore table warehouse index after table drop stopped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_table_warehouse_index_if_changed(&self, current: &TableEntry, next: &TableEntry) {
|
async fn delete_table_warehouse_index_if_changed(&self, current: &TableEntry, next: &TableEntry) {
|
||||||
let Ok(current_index) = table_warehouse_index_entry(current) else {
|
let Ok(current_index) = table_warehouse_index_entry(current) else {
|
||||||
return;
|
return;
|
||||||
@@ -1316,6 +1330,15 @@ where
|
|||||||
}
|
}
|
||||||
self.ensure_table_warehouse_prefix_available(&entry).await?;
|
self.ensure_table_warehouse_prefix_available(&entry).await?;
|
||||||
let reservation = self.reserve_table_warehouse_index(&entry).await?;
|
let reservation = self.reserve_table_warehouse_index(&entry).await?;
|
||||||
|
if !publication.holds_table_bucket(&entry.table_bucket)
|
||||||
|
|| !publication.holds_table(&entry.table_bucket, &entry.namespace, &entry.table)
|
||||||
|
{
|
||||||
|
self.delete_created_table_warehouse_index(&entry, reservation, "table publication fence lost")
|
||||||
|
.await;
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"table registration publication fence was lost before catalog update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let result = self
|
let result = self
|
||||||
.write_entry_unlocked(self.catalog_bucket(), &table_path, &entry, precondition)
|
.write_entry_unlocked(self.catalog_bucket(), &table_path, &entry, precondition)
|
||||||
.await;
|
.await;
|
||||||
@@ -1327,7 +1350,25 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn write_view_entry(&self, entry: ViewEntry, precondition: TableCatalogPutPrecondition) -> TableCatalogStoreResult<()> {
|
async fn write_view_entry(&self, entry: ViewEntry, precondition: TableCatalogPutPrecondition) -> TableCatalogStoreResult<()> {
|
||||||
|
let publication = TableCommitLockPublication::new(&self.backend);
|
||||||
|
self.write_view_entry_with_publication(entry, precondition, &publication)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_view_entry_with_publication(
|
||||||
|
&self,
|
||||||
|
entry: ViewEntry,
|
||||||
|
precondition: TableCatalogPutPrecondition,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<()> {
|
||||||
validate_view_entry_version_and_id(&entry)?;
|
validate_view_entry_version_and_id(&entry)?;
|
||||||
|
publication.begin_table_bucket(&entry.table_bucket).await?;
|
||||||
|
if !publication.holds_table_bucket(&entry.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||||
self.require_table_bucket(&entry.table_bucket).await?;
|
self.require_table_bucket(&entry.table_bucket).await?;
|
||||||
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
||||||
let view = parse_table_for_store(&entry.view)?;
|
let view = parse_table_for_store(&entry.view)?;
|
||||||
@@ -1353,6 +1394,17 @@ where
|
|||||||
entry.table_bucket, entry.namespace, entry.view
|
entry.table_bucket, entry.namespace, entry.view
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
// Preserve catalog -> publication -> object lock order across rolling upgrades.
|
||||||
|
publication
|
||||||
|
.prepare(&entry.table_bucket, &entry.namespace, &entry.view)
|
||||||
|
.await?;
|
||||||
|
if !publication.holds_table_bucket(&entry.table_bucket)
|
||||||
|
|| !publication.holds_table(&entry.table_bucket, &entry.namespace, &entry.view)
|
||||||
|
{
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation publication fence was lost before catalog update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
self.write_entry_unlocked(self.catalog_bucket(), &view_path, &entry, precondition)
|
self.write_entry_unlocked(self.catalog_bucket(), &view_path, &entry, precondition)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -4493,16 +4545,16 @@ where
|
|||||||
validate_commit_metadata_digest(&request, &new_metadata_object)?;
|
validate_commit_metadata_digest(&request, &new_metadata_object)?;
|
||||||
let table_bucket = request.table_bucket.clone();
|
let table_bucket = request.table_bucket.clone();
|
||||||
let metadata_location = request.new_metadata_location.clone();
|
let metadata_location = request.new_metadata_location.clone();
|
||||||
let next_warehouse_location = tokio::task::spawn_blocking(move || {
|
let next_metadata_state = tokio::task::spawn_blocking(move || {
|
||||||
table_metadata_warehouse_location(&table_bucket, &metadata_location, &new_metadata_object)
|
table_metadata_commit_state(&table_bucket, &metadata_location, &new_metadata_object)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| TableCatalogStoreError::Internal(format!("table metadata parser task failed: {err}")))??;
|
.map_err(|err| TableCatalogStoreError::Internal(format!("table metadata parser task failed: {err}")))??;
|
||||||
if next_warehouse_location
|
let warehouse_relocation = next_metadata_state
|
||||||
|
.warehouse_location
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|warehouse_location| warehouse_location != ¤t.warehouse_location)
|
.is_some_and(|warehouse_location| warehouse_location != ¤t.warehouse_location);
|
||||||
&& !publication.holds_table_bucket(&request.table_bucket)
|
if warehouse_relocation && !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
{
|
|
||||||
return table_commit_result(
|
return table_commit_result(
|
||||||
&request.table_bucket,
|
&request.table_bucket,
|
||||||
&request.namespace,
|
&request.namespace,
|
||||||
@@ -4537,9 +4589,12 @@ where
|
|||||||
|
|
||||||
let mut next = current.clone();
|
let mut next = current.clone();
|
||||||
next.metadata_location = staged_commit_log.new_metadata_location.clone();
|
next.metadata_location = staged_commit_log.new_metadata_location.clone();
|
||||||
if let Some(warehouse_location) = next_warehouse_location {
|
if let Some(warehouse_location) = next_metadata_state.warehouse_location {
|
||||||
next.warehouse_location = warehouse_location;
|
next.warehouse_location = warehouse_location;
|
||||||
}
|
}
|
||||||
|
if let Some(format_version) = next_metadata_state.format_version {
|
||||||
|
next.format_version = format_version;
|
||||||
|
}
|
||||||
next.version_token = staged_commit_log.new_version_token.clone();
|
next.version_token = staged_commit_log.new_version_token.clone();
|
||||||
next.generation = current.generation.saturating_add(1);
|
next.generation = current.generation.saturating_add(1);
|
||||||
if next.warehouse_location != current.warehouse_location {
|
if next.warehouse_location != current.warehouse_location {
|
||||||
@@ -4585,6 +4640,24 @@ where
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.table)
|
||||||
|
|| (warehouse_relocation && !publication.holds_table_bucket(&request.table_bucket))
|
||||||
|
{
|
||||||
|
self.delete_created_table_warehouse_index(&next, reservation, "table publication fence lost")
|
||||||
|
.await;
|
||||||
|
return table_commit_result(
|
||||||
|
&request.table_bucket,
|
||||||
|
&request.namespace,
|
||||||
|
&request.table,
|
||||||
|
&request.commit_id,
|
||||||
|
&request.operation,
|
||||||
|
commit_started,
|
||||||
|
Err(TableCatalogStoreError::Internal(
|
||||||
|
"table commit publication fence was lost before pointer update".to_string(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let cas_started = Instant::now();
|
let cas_started = Instant::now();
|
||||||
let cas_result = self
|
let cas_result = self
|
||||||
.write_entry_unlocked(
|
.write_entry_unlocked(
|
||||||
@@ -4662,20 +4735,21 @@ where
|
|||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
self.delete_owned_table_warehouse_index_for_drop(&entry).await?;
|
self.delete_owned_table_warehouse_index_for_drop(&entry).await?;
|
||||||
|
if !publication.holds_table_bucket(table_bucket)
|
||||||
|
|| !publication.holds_table(table_bucket, &namespace.public_name(), table.as_str())
|
||||||
|
{
|
||||||
|
self.restore_table_warehouse_index_after_failed_drop(&entry, "table publication fence lost")
|
||||||
|
.await;
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"table drop publication fence was lost before catalog update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
if let Err(err) = self.backend.delete_object_unlocked(self.catalog_bucket(), &object).await {
|
if let Err(err) = self.backend.delete_object_unlocked(self.catalog_bucket(), &object).await {
|
||||||
match self.read_table_with_etag_unlocked(table_bucket, &namespace, &table).await {
|
match self.read_table_with_etag_unlocked(table_bucket, &namespace, &table).await {
|
||||||
Ok(None) => return Ok(()),
|
Ok(None) => return Ok(()),
|
||||||
Ok(Some((current, _))) if current == entry => {
|
Ok(Some((current, _))) if current == entry => {
|
||||||
if let Err(restore_err) = self.reserve_table_warehouse_index(&entry).await {
|
self.restore_table_warehouse_index_after_failed_drop(&entry, "table entry delete failed")
|
||||||
tracing::warn!(
|
.await;
|
||||||
table_bucket = %entry.table_bucket,
|
|
||||||
namespace = %entry.namespace,
|
|
||||||
table = %entry.table,
|
|
||||||
table_id = %entry.table_id,
|
|
||||||
error = %restore_err,
|
|
||||||
"failed to restore table warehouse index after table entry delete failure"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
return Err(TableCatalogStoreError::Internal(format!(
|
return Err(TableCatalogStoreError::Internal(format!(
|
||||||
@@ -4703,6 +4777,15 @@ where
|
|||||||
self.write_view_entry(entry, TableCatalogPutPrecondition::IfAbsent).await
|
self.write_view_entry(entry, TableCatalogPutPrecondition::IfAbsent).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_view_with_publication(
|
||||||
|
&self,
|
||||||
|
entry: ViewEntry,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<()> {
|
||||||
|
self.write_view_entry_with_publication(entry, TableCatalogPutPrecondition::IfAbsent, publication)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>> {
|
async fn list_views(&self, table_bucket: &str, namespace: &str) -> TableCatalogStoreResult<Vec<ViewEntry>> {
|
||||||
let namespace = parse_namespace_for_store(namespace)?;
|
let namespace = parse_namespace_for_store(namespace)?;
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
@@ -4757,8 +4840,26 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult> {
|
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
let publication = TableCommitLockPublication::new(&self.backend);
|
||||||
|
self.replace_view_with_publication(request, true, &publication).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace_view_with_publication(
|
||||||
|
&self,
|
||||||
|
request: ViewCommitRequest,
|
||||||
|
table_bucket_fence_required: bool,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
let namespace = parse_namespace_for_store(&request.namespace)?;
|
let namespace = parse_namespace_for_store(&request.namespace)?;
|
||||||
let view = parse_table_for_store(&request.view)?;
|
let view = parse_table_for_store(&request.view)?;
|
||||||
|
if table_bucket_fence_required {
|
||||||
|
publication.begin_table_bucket(&request.table_bucket).await?;
|
||||||
|
if !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
let _migration_guard = self.acquire_object_backed_catalog_write_permit(&request.table_bucket).await?;
|
let _migration_guard = self.acquire_object_backed_catalog_write_permit(&request.table_bucket).await?;
|
||||||
let namespace_path = self.paths.namespace_entry_path(&request.table_bucket, &namespace);
|
let namespace_path = self.paths.namespace_entry_path(&request.table_bucket, &namespace);
|
||||||
let _namespace_guard = self
|
let _namespace_guard = self
|
||||||
@@ -4767,6 +4868,16 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
let view_path = self.paths.view_entry_path(&request.table_bucket, &namespace, &view);
|
let view_path = self.paths.view_entry_path(&request.table_bucket, &namespace, &view);
|
||||||
let _guard = self.backend.acquire_write_lock(self.catalog_bucket(), &view_path).await?;
|
let _guard = self.backend.acquire_write_lock(self.catalog_bucket(), &view_path).await?;
|
||||||
|
// Preserve catalog -> publication -> object lock order across rolling upgrades.
|
||||||
|
publication
|
||||||
|
.prepare(&request.table_bucket, &request.namespace, &request.view)
|
||||||
|
.await?;
|
||||||
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.view) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement requires a table publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||||
let Some((current, current_etag)) = self
|
let Some((current, current_etag)) = self
|
||||||
.read_view_with_etag_unlocked(&request.table_bucket, &namespace, &view)
|
.read_view_with_etag_unlocked(&request.table_bucket, &namespace, &view)
|
||||||
.await?
|
.await?
|
||||||
@@ -4814,6 +4925,14 @@ where
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| TableCatalogStoreError::Internal(format!("view metadata parser task failed: {err}")))??;
|
.map_err(|err| TableCatalogStoreError::Internal(format!("view metadata parser task failed: {err}")))??;
|
||||||
|
let warehouse_relocation = next_warehouse_location
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|location| location != current.warehouse_location);
|
||||||
|
if warehouse_relocation && !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view warehouse relocation requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut next = current;
|
let mut next = current;
|
||||||
next.metadata_location = request.new_metadata_location;
|
next.metadata_location = request.new_metadata_location;
|
||||||
@@ -4822,13 +4941,40 @@ where
|
|||||||
}
|
}
|
||||||
next.version_token = format!("token-{}", Uuid::new_v4());
|
next.version_token = format!("token-{}", Uuid::new_v4());
|
||||||
next.generation = next.generation.saturating_add(1);
|
next.generation = next.generation.saturating_add(1);
|
||||||
self.write_entry_unlocked(
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.view)
|
||||||
self.catalog_bucket(),
|
|| ((table_bucket_fence_required || warehouse_relocation) && !publication.holds_table_bucket(&request.table_bucket))
|
||||||
&view_path,
|
{
|
||||||
&next,
|
return Err(TableCatalogStoreError::Internal(
|
||||||
TableCatalogPutPrecondition::IfMatch(current_etag),
|
"view replacement publication fence was lost before catalog update".to_string(),
|
||||||
)
|
));
|
||||||
.await?;
|
}
|
||||||
|
let write_result = self
|
||||||
|
.write_entry_unlocked(
|
||||||
|
self.catalog_bucket(),
|
||||||
|
&view_path,
|
||||||
|
&next,
|
||||||
|
TableCatalogPutPrecondition::IfMatch(current_etag),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(err) = write_result {
|
||||||
|
match self
|
||||||
|
.read_view_with_etag_unlocked(&request.table_bucket, &namespace, &view)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some((persisted, _))) if persisted == next => {}
|
||||||
|
Ok(_) => return Err(err),
|
||||||
|
Err(read_err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
table_bucket = %request.table_bucket,
|
||||||
|
namespace = %request.namespace,
|
||||||
|
view = %request.view,
|
||||||
|
error = %read_err,
|
||||||
|
"failed to verify view state after an ambiguous catalog update"
|
||||||
|
);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(ViewCommitResult { view: next })
|
Ok(ViewCommitResult { view: next })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -554,7 +554,7 @@ where
|
|||||||
|
|
||||||
// Ordinary mutations hold the global migration read lock before the local write lock; migration takes the
|
// Ordinary mutations hold the global migration read lock before the local write lock; migration takes the
|
||||||
// write side before invoking its dedicated snapshot mutation methods.
|
// write side before invoking its dedicated snapshot mutation methods.
|
||||||
async fn acquire_snapshot_write_permit(&self) -> TableCatalogStoreResult<Box<dyn Send>> {
|
async fn acquire_snapshot_write_permit(&self) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
let lock_path = TableCatalogObjectPaths::default().backing_migration_global_fence_lock_path();
|
let lock_path = TableCatalogObjectPaths::default().backing_migration_global_fence_lock_path();
|
||||||
self.object_backend.acquire_read_lock(RUSTFS_META_BUCKET, &lock_path).await
|
self.object_backend.acquire_read_lock(RUSTFS_META_BUCKET, &lock_path).await
|
||||||
}
|
}
|
||||||
@@ -1775,7 +1775,7 @@ where
|
|||||||
request: &TableCommitRequest,
|
request: &TableCommitRequest,
|
||||||
namespace: &Namespace,
|
namespace: &Namespace,
|
||||||
table: &IdentifierSegment,
|
table: &IdentifierSegment,
|
||||||
next_warehouse_location: Option<String>,
|
next_metadata_state: TableMetadataCommitState,
|
||||||
) -> TableCatalogStoreResult<TableCommitResult> {
|
) -> TableCatalogStoreResult<TableCommitResult> {
|
||||||
let key = Self::table_key(&request.table_bucket, namespace, table);
|
let key = Self::table_key(&request.table_bucket, namespace, table);
|
||||||
let current = Self::validate_new_table_commit_locked(state, &key, request)?;
|
let current = Self::validate_new_table_commit_locked(state, &key, request)?;
|
||||||
@@ -1802,9 +1802,12 @@ where
|
|||||||
|
|
||||||
let mut next = current;
|
let mut next = current;
|
||||||
next.metadata_location = commit_log.new_metadata_location.clone();
|
next.metadata_location = commit_log.new_metadata_location.clone();
|
||||||
if let Some(warehouse_location) = next_warehouse_location {
|
if let Some(warehouse_location) = next_metadata_state.warehouse_location {
|
||||||
next.warehouse_location = warehouse_location;
|
next.warehouse_location = warehouse_location;
|
||||||
}
|
}
|
||||||
|
if let Some(format_version) = next_metadata_state.format_version {
|
||||||
|
next.format_version = format_version;
|
||||||
|
}
|
||||||
Self::ensure_table_warehouse_prefix_available_locked(state, &next, &key)?;
|
Self::ensure_table_warehouse_prefix_available_locked(state, &next, &key)?;
|
||||||
next.version_token = commit_log.new_version_token.clone();
|
next.version_token = commit_log.new_version_token.clone();
|
||||||
next.generation = next.generation.saturating_add(1);
|
next.generation = next.generation.saturating_add(1);
|
||||||
@@ -2170,6 +2173,7 @@ where
|
|||||||
let _write_guard = self.write_lock.lock().await;
|
let _write_guard = self.write_lock.lock().await;
|
||||||
self.hydrate_state().await?;
|
self.hydrate_state().await?;
|
||||||
let key = Self::table_key(&entry.table_bucket, &namespace, &table);
|
let key = Self::table_key(&entry.table_bucket, &namespace, &table);
|
||||||
|
let publication_identity = (entry.table_bucket.clone(), entry.namespace.clone(), entry.table.clone());
|
||||||
let (snapshot, precondition, postcondition) = {
|
let (snapshot, precondition, postcondition) = {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
Self::require_table_bucket_in_state(&state, &entry.table_bucket)?;
|
Self::require_table_bucket_in_state(&state, &entry.table_bucket)?;
|
||||||
@@ -2198,6 +2202,13 @@ where
|
|||||||
StrongSnapshotWritePostcondition::TablePresent(entry),
|
StrongSnapshotWritePostcondition::TablePresent(entry),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
if !publication.holds_table_bucket(&publication_identity.0)
|
||||||
|
|| !publication.holds_table(&publication_identity.0, &publication_identity.1, &publication_identity.2)
|
||||||
|
{
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"table registration publication fence was lost before snapshot update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2479,9 +2490,15 @@ where
|
|||||||
let result = match prepared_result {
|
let result = match prepared_result {
|
||||||
Ok((result, Some((snapshot, precondition)))) => {
|
Ok((result, Some((snapshot, precondition)))) => {
|
||||||
let postcondition = Self::commit_write_postcondition(&request.table_bucket, &result.commit_log);
|
let postcondition = Self::commit_write_postcondition(&request.table_bucket, &result.commit_log);
|
||||||
self.finalize_snapshot_write(snapshot, precondition, postcondition)
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.table) {
|
||||||
.await
|
Err(TableCatalogStoreError::Internal(
|
||||||
.map(|_| result)
|
"table commit publication fence was lost before snapshot update".to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
self.finalize_snapshot_write(snapshot, precondition, postcondition)
|
||||||
|
.await
|
||||||
|
.map(|_| result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok((result, None)) => Ok(result),
|
Ok((result, None)) => Ok(result),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
@@ -2519,8 +2536,8 @@ where
|
|||||||
validate_commit_metadata_digest(&request, &new_metadata_object)?;
|
validate_commit_metadata_digest(&request, &new_metadata_object)?;
|
||||||
let table_bucket = request.table_bucket.clone();
|
let table_bucket = request.table_bucket.clone();
|
||||||
let metadata_location = request.new_metadata_location.clone();
|
let metadata_location = request.new_metadata_location.clone();
|
||||||
let next_warehouse_location = tokio::task::spawn_blocking(move || {
|
let next_metadata_state = tokio::task::spawn_blocking(move || {
|
||||||
table_metadata_warehouse_location(&table_bucket, &metadata_location, &new_metadata_object)
|
table_metadata_commit_state(&table_bucket, &metadata_location, &new_metadata_object)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| TableCatalogStoreError::Internal(format!("table metadata parser task failed: {err}")))??;
|
.map_err(|err| TableCatalogStoreError::Internal(format!("table metadata parser task failed: {err}")))??;
|
||||||
@@ -2539,11 +2556,11 @@ where
|
|||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
if next_warehouse_location
|
let warehouse_relocation = next_metadata_state
|
||||||
|
.warehouse_location
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|warehouse_location| warehouse_location != ¤t_warehouse_location)
|
.is_some_and(|warehouse_location| warehouse_location != ¤t_warehouse_location);
|
||||||
&& !publication.holds_table_bucket(&request.table_bucket)
|
if warehouse_relocation && !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
{
|
|
||||||
return table_commit_result(
|
return table_commit_result(
|
||||||
&request.table_bucket,
|
&request.table_bucket,
|
||||||
&request.namespace,
|
&request.namespace,
|
||||||
@@ -2561,7 +2578,7 @@ where
|
|||||||
let prepared_result = {
|
let prepared_result = {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
let (precondition, mut draft_state) = Self::snapshot_draft_context_locked(&state);
|
let (precondition, mut draft_state) = Self::snapshot_draft_context_locked(&state);
|
||||||
match Self::apply_commit_locked(&mut draft_state, &request, &namespace, &table, next_warehouse_location) {
|
match Self::apply_commit_locked(&mut draft_state, &request, &namespace, &table, next_metadata_state) {
|
||||||
Ok(result) => Self::snapshot_from_mutated_state_locked(&mut draft_state, self.snapshot_write_version)
|
Ok(result) => Self::snapshot_from_mutated_state_locked(&mut draft_state, self.snapshot_write_version)
|
||||||
.map(|snapshot| (result, snapshot, precondition)),
|
.map(|snapshot| (result, snapshot, precondition)),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
@@ -2570,7 +2587,16 @@ where
|
|||||||
let result = match prepared_result {
|
let result = match prepared_result {
|
||||||
Ok((result, snapshot, precondition)) => {
|
Ok((result, snapshot, precondition)) => {
|
||||||
let postcondition = Self::commit_write_postcondition(&request.table_bucket, &result.commit_log);
|
let postcondition = Self::commit_write_postcondition(&request.table_bucket, &result.commit_log);
|
||||||
match self.finalize_snapshot_write(snapshot, precondition, postcondition).await {
|
let snapshot_result = if publication.holds_table(&request.table_bucket, &request.namespace, &request.table)
|
||||||
|
&& (!warehouse_relocation || publication.holds_table_bucket(&request.table_bucket))
|
||||||
|
{
|
||||||
|
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
||||||
|
} else {
|
||||||
|
Err(TableCatalogStoreError::Internal(
|
||||||
|
"table commit publication fence was lost before snapshot update".to_string(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
match snapshot_result {
|
||||||
Ok(()) => Ok(result),
|
Ok(()) => Ok(result),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let replay = {
|
let replay = {
|
||||||
@@ -2647,13 +2673,26 @@ where
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
if !publication.holds_table_bucket(table_bucket)
|
||||||
|
|| !publication.holds_table(table_bucket, &namespace.public_name(), table.as_str())
|
||||||
|
{
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"table drop publication fence was lost before snapshot update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()> {
|
async fn create_view(&self, entry: ViewEntry) -> TableCatalogStoreResult<()> {
|
||||||
let _migration_guard = self.acquire_snapshot_write_permit().await?;
|
let publication = TableCommitLockPublication::new(&self.object_backend);
|
||||||
let _write_guard = self.write_lock.lock().await;
|
self.create_view_with_publication(entry, &publication).await
|
||||||
self.hydrate_state().await?;
|
}
|
||||||
|
|
||||||
|
async fn create_view_with_publication(
|
||||||
|
&self,
|
||||||
|
entry: ViewEntry,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<()> {
|
||||||
validate_view_entry_version_and_id(&entry)?;
|
validate_view_entry_version_and_id(&entry)?;
|
||||||
validate_view_warehouse_location(&entry.table_bucket, &entry.warehouse_location)?;
|
validate_view_warehouse_location(&entry.table_bucket, &entry.warehouse_location)?;
|
||||||
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
let namespace = parse_namespace_for_store(&entry.namespace)?;
|
||||||
@@ -2663,7 +2702,26 @@ where
|
|||||||
"view metadata location must be inside the view metadata directory".to_string(),
|
"view metadata location must be inside the view metadata directory".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
publication.begin_table_bucket(&entry.table_bucket).await?;
|
||||||
|
if !publication.holds_table_bucket(&entry.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||||
|
let _migration_guard = self.acquire_snapshot_write_permit().await?;
|
||||||
|
publication
|
||||||
|
.prepare(&entry.table_bucket, &entry.namespace, &entry.view)
|
||||||
|
.await?;
|
||||||
|
if !publication.holds_table(&entry.table_bucket, &entry.namespace, &entry.view) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation requires a table publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _write_guard = self.write_lock.lock().await;
|
||||||
|
self.hydrate_state().await?;
|
||||||
let key = Self::table_key(&entry.table_bucket, &namespace, &view);
|
let key = Self::table_key(&entry.table_bucket, &namespace, &view);
|
||||||
|
let publication_identity = (entry.table_bucket.clone(), entry.namespace.clone(), entry.view.clone());
|
||||||
let (snapshot, precondition, postcondition) = {
|
let (snapshot, precondition, postcondition) = {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
Self::require_table_bucket_in_state(&state, &entry.table_bucket)?;
|
Self::require_table_bucket_in_state(&state, &entry.table_bucket)?;
|
||||||
@@ -2682,6 +2740,13 @@ where
|
|||||||
StrongSnapshotWritePostcondition::ViewPresent(entry),
|
StrongSnapshotWritePostcondition::ViewPresent(entry),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
if !publication.holds_table_bucket(&publication_identity.0)
|
||||||
|
|| !publication.holds_table(&publication_identity.0, &publication_identity.1, &publication_identity.2)
|
||||||
|
{
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view creation publication fence was lost before snapshot update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
self.finalize_snapshot_write(snapshot, precondition, postcondition).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2751,11 +2816,38 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult> {
|
async fn replace_view(&self, request: ViewCommitRequest) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
let publication = TableCommitLockPublication::new(&self.object_backend);
|
||||||
|
self.replace_view_with_publication(request, true, &publication).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace_view_with_publication(
|
||||||
|
&self,
|
||||||
|
request: ViewCommitRequest,
|
||||||
|
table_bucket_fence_required: bool,
|
||||||
|
publication: &(dyn TableCommitPublication + Sync),
|
||||||
|
) -> TableCatalogStoreResult<ViewCommitResult> {
|
||||||
|
if table_bucket_fence_required {
|
||||||
|
publication.begin_table_bucket(&request.table_bucket).await?;
|
||||||
|
if !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _publication_completion = TableCommitPublicationCompletion::new(publication);
|
||||||
let _migration_guard = self.acquire_snapshot_write_permit().await?;
|
let _migration_guard = self.acquire_snapshot_write_permit().await?;
|
||||||
let write_guard = self.write_lock.lock().await;
|
|
||||||
self.hydrate_state().await?;
|
|
||||||
let namespace = parse_namespace_for_store(&request.namespace)?;
|
let namespace = parse_namespace_for_store(&request.namespace)?;
|
||||||
let view = parse_table_for_store(&request.view)?;
|
let view = parse_table_for_store(&request.view)?;
|
||||||
|
publication
|
||||||
|
.prepare(&request.table_bucket, &request.namespace, &request.view)
|
||||||
|
.await?;
|
||||||
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.view) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement requires a table publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let write_guard = self.write_lock.lock().await;
|
||||||
|
self.hydrate_state().await?;
|
||||||
let key = Self::table_key(&request.table_bucket, &namespace, &view);
|
let key = Self::table_key(&request.table_bucket, &namespace, &view);
|
||||||
let expected_view_id = {
|
let expected_view_id = {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
@@ -2798,7 +2890,7 @@ where
|
|||||||
|
|
||||||
let _write_guard = self.write_lock.lock().await;
|
let _write_guard = self.write_lock.lock().await;
|
||||||
self.hydrate_state().await?;
|
self.hydrate_state().await?;
|
||||||
let (snapshot, precondition, next, postcondition) = {
|
let (snapshot, precondition, next, postcondition, warehouse_relocation) = {
|
||||||
let state = self.state.lock().await;
|
let state = self.state.lock().await;
|
||||||
Self::ensure_identifier_is_unambiguous_locked(&state, &key)?;
|
Self::ensure_identifier_is_unambiguous_locked(&state, &key)?;
|
||||||
let Some(current) = state.views.get(&key).cloned() else {
|
let Some(current) = state.views.get(&key).cloned() else {
|
||||||
@@ -2828,6 +2920,14 @@ where
|
|||||||
"current view metadata location does not match expected location".to_string(),
|
"current view metadata location does not match expected location".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let warehouse_relocation = next_warehouse_location
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|location| location != current.warehouse_location);
|
||||||
|
if warehouse_relocation && !publication.holds_table_bucket(&request.table_bucket) {
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view warehouse relocation requires a table-bucket publication fence".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut next = current;
|
let mut next = current;
|
||||||
next.metadata_location = request.new_metadata_location;
|
next.metadata_location = request.new_metadata_location;
|
||||||
@@ -2843,8 +2943,16 @@ where
|
|||||||
precondition,
|
precondition,
|
||||||
next.clone(),
|
next.clone(),
|
||||||
StrongSnapshotWritePostcondition::ViewPresent(next),
|
StrongSnapshotWritePostcondition::ViewPresent(next),
|
||||||
|
warehouse_relocation,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
if !publication.holds_table(&request.table_bucket, &request.namespace, &request.view)
|
||||||
|
|| ((table_bucket_fence_required || warehouse_relocation) && !publication.holds_table_bucket(&request.table_bucket))
|
||||||
|
{
|
||||||
|
return Err(TableCatalogStoreError::Internal(
|
||||||
|
"view replacement publication fence was lost before snapshot update".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
self.finalize_snapshot_write(snapshot, precondition, postcondition).await?;
|
self.finalize_snapshot_write(snapshot, precondition, postcondition).await?;
|
||||||
Ok(ViewCommitResult { view: next })
|
Ok(ViewCommitResult { view: next })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,23 +55,35 @@ pub(crate) fn table_metadata_json(table_uuid: &str, location: &str) -> serde_jso
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn manifest_list_avro_bytes(manifest_paths: &[&str], sequence_number: i64, snapshot_id: i64) -> Vec<u8> {
|
pub(crate) fn manifest_list_avro_bytes(manifests: &[(&str, usize)], sequence_number: i64, snapshot_id: i64) -> Vec<u8> {
|
||||||
let manifests = manifest_paths
|
|
||||||
.iter()
|
|
||||||
.map(|manifest_path| (*manifest_path, 0, sequence_number, snapshot_id))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
manifest_list_avro_entries_with_partition_specs(&manifests)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn manifest_list_avro_entries(manifests: &[(&str, i64, i64)]) -> Vec<u8> {
|
|
||||||
let manifests = manifests
|
let manifests = manifests
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(manifest_path, sequence_number, snapshot_id)| (*manifest_path, 0, *sequence_number, *snapshot_id))
|
.map(|(manifest_path, manifest_length)| (*manifest_path, *manifest_length, 0, sequence_number, snapshot_id))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
manifest_list_avro_entries_with_partition_specs(&manifests)
|
manifest_list_avro_entries_with_partition_specs(&manifests)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn manifest_list_avro_entries_with_partition_specs(manifests: &[(&str, i32, i64, i64)]) -> Vec<u8> {
|
pub(crate) fn manifest_list_avro_entries(manifests: &[(&str, usize, i64, i64)]) -> Vec<u8> {
|
||||||
|
let manifests = manifests
|
||||||
|
.iter()
|
||||||
|
.map(|(manifest_path, manifest_length, sequence_number, snapshot_id)| {
|
||||||
|
(*manifest_path, *manifest_length, 0, *sequence_number, *snapshot_id)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
manifest_list_avro_entries_with_partition_specs(&manifests)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn manifest_list_avro_entries_with_partition_specs(manifests: &[(&str, usize, i32, i64, i64)]) -> Vec<u8> {
|
||||||
|
let manifests = manifests
|
||||||
|
.iter()
|
||||||
|
.map(|(path, length, spec_id, sequence_number, snapshot_id)| {
|
||||||
|
(*path, *length, *spec_id, 0, *sequence_number, *snapshot_id)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
manifest_list_avro_entries_with_content(&manifests)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn manifest_list_avro_entries_with_content(manifests: &[(&str, usize, i32, i32, i64, i64)]) -> Vec<u8> {
|
||||||
let schema = apache_avro::Schema::parse_str(
|
let schema = apache_avro::Schema::parse_str(
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
@@ -97,16 +109,19 @@ pub(crate) fn manifest_list_avro_entries_with_partition_specs(manifests: &[(&str
|
|||||||
)
|
)
|
||||||
.expect("manifest list avro schema should parse");
|
.expect("manifest list avro schema should parse");
|
||||||
let mut writer = apache_avro::Writer::new(&schema, Vec::new()).expect("manifest list writer should initialize");
|
let mut writer = apache_avro::Writer::new(&schema, Vec::new()).expect("manifest list writer should initialize");
|
||||||
for (manifest_path, partition_spec_id, sequence_number, snapshot_id) in manifests {
|
for (manifest_path, manifest_length, partition_spec_id, content, sequence_number, snapshot_id) in manifests {
|
||||||
writer
|
writer
|
||||||
.append_value(apache_avro::types::Value::Record(vec![
|
.append_value(apache_avro::types::Value::Record(vec![
|
||||||
(
|
(
|
||||||
"manifest_path".to_string(),
|
"manifest_path".to_string(),
|
||||||
apache_avro::types::Value::String((*manifest_path).to_string()),
|
apache_avro::types::Value::String((*manifest_path).to_string()),
|
||||||
),
|
),
|
||||||
("manifest_length".to_string(), apache_avro::types::Value::Long(1)),
|
(
|
||||||
|
"manifest_length".to_string(),
|
||||||
|
apache_avro::types::Value::Long(i64::try_from(*manifest_length).expect("test manifest length should fit")),
|
||||||
|
),
|
||||||
("partition_spec_id".to_string(), apache_avro::types::Value::Int(*partition_spec_id)),
|
("partition_spec_id".to_string(), apache_avro::types::Value::Int(*partition_spec_id)),
|
||||||
("content".to_string(), apache_avro::types::Value::Int(0)),
|
("content".to_string(), apache_avro::types::Value::Int(*content)),
|
||||||
("sequence_number".to_string(), apache_avro::types::Value::Long(*sequence_number)),
|
("sequence_number".to_string(), apache_avro::types::Value::Long(*sequence_number)),
|
||||||
("min_sequence_number".to_string(), apache_avro::types::Value::Long(*sequence_number)),
|
("min_sequence_number".to_string(), apache_avro::types::Value::Long(*sequence_number)),
|
||||||
("added_snapshot_id".to_string(), apache_avro::types::Value::Long(*snapshot_id)),
|
("added_snapshot_id".to_string(), apache_avro::types::Value::Long(*snapshot_id)),
|
||||||
@@ -122,7 +137,86 @@ pub(crate) fn manifest_list_avro_entries_with_partition_specs(manifests: &[(&str
|
|||||||
writer.into_inner().expect("manifest list avro bytes should flush")
|
writer.into_inner().expect("manifest list avro bytes should flush")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn manifest_list_avro_entries_with_nullable_counts(manifests: &[(&str, usize, i32, i64, i64)]) -> Vec<u8> {
|
||||||
|
let schema = apache_avro::Schema::parse_str(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"type": "record",
|
||||||
|
"name": "manifest_file",
|
||||||
|
"fields": [
|
||||||
|
{"name": "manifest_path", "type": "string"},
|
||||||
|
{"name": "manifest_length", "type": "long"},
|
||||||
|
{"name": "partition_spec_id", "type": "int"},
|
||||||
|
{"name": "content", "type": "int"},
|
||||||
|
{"name": "sequence_number", "type": "long"},
|
||||||
|
{"name": "min_sequence_number", "type": "long"},
|
||||||
|
{"name": "added_snapshot_id", "type": "long"},
|
||||||
|
{"name": "added_files_count", "type": ["null", "int"], "default": null},
|
||||||
|
{"name": "existing_files_count", "type": ["null", "int"], "default": null},
|
||||||
|
{"name": "deleted_files_count", "type": ["null", "int"], "default": null},
|
||||||
|
{"name": "added_rows_count", "type": ["null", "long"], "default": null},
|
||||||
|
{"name": "existing_rows_count", "type": ["null", "long"], "default": null},
|
||||||
|
{"name": "deleted_rows_count", "type": ["null", "long"], "default": null}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("manifest list avro schema should parse");
|
||||||
|
let mut writer = apache_avro::Writer::new(&schema, Vec::new()).expect("manifest list writer should initialize");
|
||||||
|
for (manifest_path, manifest_length, partition_spec_id, sequence_number, snapshot_id) in manifests {
|
||||||
|
writer
|
||||||
|
.append_value(apache_avro::types::Value::Record(vec![
|
||||||
|
(
|
||||||
|
"manifest_path".to_string(),
|
||||||
|
apache_avro::types::Value::String((*manifest_path).to_string()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"manifest_length".to_string(),
|
||||||
|
apache_avro::types::Value::Long(i64::try_from(*manifest_length).expect("test manifest length should fit")),
|
||||||
|
),
|
||||||
|
("partition_spec_id".to_string(), apache_avro::types::Value::Int(*partition_spec_id)),
|
||||||
|
("content".to_string(), apache_avro::types::Value::Int(0)),
|
||||||
|
("sequence_number".to_string(), apache_avro::types::Value::Long(*sequence_number)),
|
||||||
|
("min_sequence_number".to_string(), apache_avro::types::Value::Long(*sequence_number)),
|
||||||
|
("added_snapshot_id".to_string(), apache_avro::types::Value::Long(*snapshot_id)),
|
||||||
|
(
|
||||||
|
"added_files_count".to_string(),
|
||||||
|
apache_avro::types::Value::Union(0, Box::new(apache_avro::types::Value::Null)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"existing_files_count".to_string(),
|
||||||
|
apache_avro::types::Value::Union(0, Box::new(apache_avro::types::Value::Null)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_files_count".to_string(),
|
||||||
|
apache_avro::types::Value::Union(0, Box::new(apache_avro::types::Value::Null)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"added_rows_count".to_string(),
|
||||||
|
apache_avro::types::Value::Union(0, Box::new(apache_avro::types::Value::Null)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"existing_rows_count".to_string(),
|
||||||
|
apache_avro::types::Value::Union(0, Box::new(apache_avro::types::Value::Null)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"deleted_rows_count".to_string(),
|
||||||
|
apache_avro::types::Value::Union(0, Box::new(apache_avro::types::Value::Null)),
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
.expect("manifest list record should append");
|
||||||
|
}
|
||||||
|
writer.into_inner().expect("manifest list avro bytes should flush")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn manifest_avro_bytes(files: &[(&str, i32, i32, i64, i64)]) -> Vec<u8> {
|
pub(crate) fn manifest_avro_bytes(files: &[(&str, i32, i32, i64, i64)]) -> Vec<u8> {
|
||||||
|
manifest_avro_bytes_with_partition_spec(files, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn manifest_avro_bytes_with_partition_spec(
|
||||||
|
files: &[(&str, i32, i32, i64, i64)],
|
||||||
|
partition_spec_id: Option<i32>,
|
||||||
|
) -> Vec<u8> {
|
||||||
let schema = apache_avro::Schema::parse_str(
|
let schema = apache_avro::Schema::parse_str(
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
@@ -141,6 +235,7 @@ pub(crate) fn manifest_avro_bytes(files: &[(&str, i32, i32, i64, i64)]) -> Vec<u
|
|||||||
"fields": [
|
"fields": [
|
||||||
{"name": "content", "type": "int"},
|
{"name": "content", "type": "int"},
|
||||||
{"name": "file_path", "type": "string"},
|
{"name": "file_path", "type": "string"},
|
||||||
|
{"name": "partition", "type": {"type": "record", "name": "partition", "fields": []}},
|
||||||
{"name": "record_count", "type": "long"},
|
{"name": "record_count", "type": "long"},
|
||||||
{"name": "file_size_in_bytes", "type": "long"}
|
{"name": "file_size_in_bytes", "type": "long"}
|
||||||
]
|
]
|
||||||
@@ -152,6 +247,11 @@ pub(crate) fn manifest_avro_bytes(files: &[(&str, i32, i32, i64, i64)]) -> Vec<u
|
|||||||
)
|
)
|
||||||
.expect("manifest avro schema should parse");
|
.expect("manifest avro schema should parse");
|
||||||
let mut writer = apache_avro::Writer::new(&schema, Vec::new()).expect("manifest writer should initialize");
|
let mut writer = apache_avro::Writer::new(&schema, Vec::new()).expect("manifest writer should initialize");
|
||||||
|
if let Some(partition_spec_id) = partition_spec_id {
|
||||||
|
writer
|
||||||
|
.add_user_metadata("partition-spec-id".to_string(), partition_spec_id.to_string())
|
||||||
|
.expect("manifest partition spec metadata should write");
|
||||||
|
}
|
||||||
for (file_path, content, status, snapshot_id, sequence_number) in files {
|
for (file_path, content, status, snapshot_id, sequence_number) in files {
|
||||||
writer
|
writer
|
||||||
.append_value(apache_avro::types::Value::Record(vec![
|
.append_value(apache_avro::types::Value::Record(vec![
|
||||||
@@ -164,6 +264,7 @@ pub(crate) fn manifest_avro_bytes(files: &[(&str, i32, i32, i64, i64)]) -> Vec<u
|
|||||||
apache_avro::types::Value::Record(vec![
|
apache_avro::types::Value::Record(vec![
|
||||||
("content".to_string(), apache_avro::types::Value::Int(*content)),
|
("content".to_string(), apache_avro::types::Value::Int(*content)),
|
||||||
("file_path".to_string(), apache_avro::types::Value::String((*file_path).to_string())),
|
("file_path".to_string(), apache_avro::types::Value::String((*file_path).to_string())),
|
||||||
|
("partition".to_string(), apache_avro::types::Value::Record(Vec::new())),
|
||||||
("record_count".to_string(), apache_avro::types::Value::Long(1)),
|
("record_count".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
("file_size_in_bytes".to_string(), apache_avro::types::Value::Long(1)),
|
("file_size_in_bytes".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
]),
|
]),
|
||||||
@@ -200,6 +301,7 @@ pub(crate) fn manifest_avro_bytes_with_nullable_sequences(files: &[(&str, i32, i
|
|||||||
"fields": [
|
"fields": [
|
||||||
{"name": "content", "type": "int"},
|
{"name": "content", "type": "int"},
|
||||||
{"name": "file_path", "type": "string"},
|
{"name": "file_path", "type": "string"},
|
||||||
|
{"name": "partition", "type": {"type": "record", "name": "partition", "fields": []}},
|
||||||
{"name": "record_count", "type": "long"},
|
{"name": "record_count", "type": "long"},
|
||||||
{"name": "file_size_in_bytes", "type": "long"}
|
{"name": "file_size_in_bytes", "type": "long"}
|
||||||
]
|
]
|
||||||
@@ -223,6 +325,7 @@ pub(crate) fn manifest_avro_bytes_with_nullable_sequences(files: &[(&str, i32, i
|
|||||||
apache_avro::types::Value::Record(vec![
|
apache_avro::types::Value::Record(vec![
|
||||||
("content".to_string(), apache_avro::types::Value::Int(*content)),
|
("content".to_string(), apache_avro::types::Value::Int(*content)),
|
||||||
("file_path".to_string(), apache_avro::types::Value::String((*file_path).to_string())),
|
("file_path".to_string(), apache_avro::types::Value::String((*file_path).to_string())),
|
||||||
|
("partition".to_string(), apache_avro::types::Value::Record(Vec::new())),
|
||||||
("record_count".to_string(), apache_avro::types::Value::Long(1)),
|
("record_count".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
("file_size_in_bytes".to_string(), apache_avro::types::Value::Long(1)),
|
("file_size_in_bytes".to_string(), apache_avro::types::Value::Long(1)),
|
||||||
]),
|
]),
|
||||||
@@ -284,7 +387,7 @@ pub(crate) struct BlockingObjectPublication {
|
|||||||
backend: TestCatalogObjectBackend,
|
backend: TestCatalogObjectBackend,
|
||||||
object: String,
|
object: String,
|
||||||
started: Arc<tokio::sync::Notify>,
|
started: Arc<tokio::sync::Notify>,
|
||||||
guard: Arc<parking_lot::Mutex<Option<Box<dyn Send>>>>,
|
guard: Arc<parking_lot::Mutex<Option<TableCatalogLockGuard>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockingObjectPublication {
|
impl BlockingObjectPublication {
|
||||||
@@ -812,7 +915,7 @@ impl TableCatalogObjectBackend for TestCatalogObjectBackend {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>> {
|
async fn acquire_write_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
self.lock_attempts.lock().await.push((bucket.to_string(), object.to_string()));
|
self.lock_attempts.lock().await.push((bucket.to_string(), object.to_string()));
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
@@ -828,10 +931,10 @@ impl TableCatalogObjectBackend for TestCatalogObjectBackend {
|
|||||||
.or_insert_with(|| std::sync::Arc::new(tokio::sync::RwLock::new(())))
|
.or_insert_with(|| std::sync::Arc::new(tokio::sync::RwLock::new(())))
|
||||||
.clone()
|
.clone()
|
||||||
};
|
};
|
||||||
Ok(Box::new(lock.write_owned().await))
|
Ok(TableCatalogLockGuard::stable(lock.write_owned().await))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_read_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<Box<dyn Send>> {
|
async fn acquire_read_lock(&self, bucket: &str, object: &str) -> TableCatalogStoreResult<TableCatalogLockGuard> {
|
||||||
// The admin fake implemented only acquire_write_lock, so the trait's
|
// The admin fake implemented only acquire_write_lock, so the trait's
|
||||||
// default read->write delegation made read acquisitions observable in
|
// default read->write delegation made read acquisitions observable in
|
||||||
// lock_attempts as well; keep that (backlog#1837 PR2).
|
// lock_attempts as well; keep that (backlog#1837 PR2).
|
||||||
@@ -850,7 +953,7 @@ impl TableCatalogObjectBackend for TestCatalogObjectBackend {
|
|||||||
.or_insert_with(|| std::sync::Arc::new(tokio::sync::RwLock::new(())))
|
.or_insert_with(|| std::sync::Arc::new(tokio::sync::RwLock::new(())))
|
||||||
.clone()
|
.clone()
|
||||||
};
|
};
|
||||||
Ok(Box::new(lock.read_owned().await))
|
Ok(TableCatalogLockGuard::stable(lock.read_owned().await))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1165,6 +1268,8 @@ pub(crate) struct TestTableCatalogStore {
|
|||||||
pub(crate) fail_put_table_bucket: tokio::sync::Mutex<bool>,
|
pub(crate) fail_put_table_bucket: tokio::sync::Mutex<bool>,
|
||||||
pub(crate) register_table_pause: Option<TestCatalogPublishPause>,
|
pub(crate) register_table_pause: Option<TestCatalogPublishPause>,
|
||||||
pub(crate) commit_table_pause: Option<TestCatalogPublishPause>,
|
pub(crate) commit_table_pause: Option<TestCatalogPublishPause>,
|
||||||
|
pub(crate) create_view_pause: Option<TestCatalogPublishPause>,
|
||||||
|
pub(crate) replace_view_pause: Option<TestCatalogPublishPause>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -1473,6 +1578,10 @@ impl crate::table_catalog::TableCatalogStore for TestTableCatalogStore {
|
|||||||
entry.table_bucket, entry.namespace
|
entry.table_bucket, entry.namespace
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
if let Some(pause) = &self.create_view_pause {
|
||||||
|
pause.started.notify_one();
|
||||||
|
pause.release.notified().await;
|
||||||
|
}
|
||||||
self.views.lock().await.push(entry);
|
self.views.lock().await.push(entry);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1531,6 +1640,10 @@ impl crate::table_catalog::TableCatalogStore for TestTableCatalogStore {
|
|||||||
"current view metadata location does not match expected location".to_string(),
|
"current view metadata location does not match expected location".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(pause) = &self.replace_view_pause {
|
||||||
|
pause.started.notify_one();
|
||||||
|
pause.release.notified().await;
|
||||||
|
}
|
||||||
let mut next = current;
|
let mut next = current;
|
||||||
next.metadata_location = request.new_metadata_location;
|
next.metadata_location = request.new_metadata_location;
|
||||||
next.version_token = "token-view-committed".to_string();
|
next.version_token = "token-view-committed".to_string();
|
||||||
|
|||||||
+1673
-166
File diff suppressed because it is too large
Load Diff
@@ -132,18 +132,8 @@ def failure_probe_plan(warehouse: str, namespace: str, table: str, rest_path: st
|
|||||||
"expected-version-token": "stale-token-from-previous-load",
|
"expected-version-token": "stale-token-from-previous-load",
|
||||||
"expected-metadata-location": "current-metadata-location-from-load-table",
|
"expected-metadata-location": "current-metadata-location-from-load-table",
|
||||||
"new-metadata-location": f"s3://{warehouse}/tables/table-id/metadata/conflict_probe.metadata.json",
|
"new-metadata-location": f"s3://{warehouse}/tables/table-id/metadata/conflict_probe.metadata.json",
|
||||||
"requirements": [
|
"requirements": [],
|
||||||
{
|
"updates": [],
|
||||||
"type": "assert-current-snapshot-id",
|
|
||||||
"snapshot-id": 0,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"updates": [
|
|
||||||
{
|
|
||||||
"action": "set-current-schema",
|
|
||||||
"schema-id": 0,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
probe_step(
|
probe_step(
|
||||||
@@ -157,6 +147,8 @@ def failure_probe_plan(warehouse: str, namespace: str, table: str, rest_path: st
|
|||||||
"expected-version-token": "current-version-token-from-load-table",
|
"expected-version-token": "current-version-token-from-load-table",
|
||||||
"expected-metadata-location": "current-metadata-location-from-load-table",
|
"expected-metadata-location": "current-metadata-location-from-load-table",
|
||||||
"new-metadata-location": f"s3://{warehouse}/tables/table-id/metadata/does_not_exist.metadata.json",
|
"new-metadata-location": f"s3://{warehouse}/tables/table-id/metadata/does_not_exist.metadata.json",
|
||||||
|
"requirements": [],
|
||||||
|
"updates": [],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
probe_step(
|
probe_step(
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class FailureCoverageTest(unittest.TestCase):
|
|||||||
self.assertIn("expected-version-token", by_name["stale-token-commit-conflict"]["body"])
|
self.assertIn("expected-version-token", by_name["stale-token-commit-conflict"]["body"])
|
||||||
self.assertIn("expected-metadata-location", by_name["stale-token-commit-conflict"]["body"])
|
self.assertIn("expected-metadata-location", by_name["stale-token-commit-conflict"]["body"])
|
||||||
self.assertIn("new-metadata-location", by_name["stale-token-commit-conflict"]["body"])
|
self.assertIn("new-metadata-location", by_name["stale-token-commit-conflict"]["body"])
|
||||||
|
self.assertEqual(by_name["stale-token-commit-conflict"]["body"]["requirements"], [])
|
||||||
|
self.assertEqual(by_name["stale-token-commit-conflict"]["body"]["updates"], [])
|
||||||
self.assertNotIn("base", by_name["stale-token-commit-conflict"]["body"])
|
self.assertNotIn("base", by_name["stale-token-commit-conflict"]["body"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
by_name["diagnostics-after-finalization-gap"]["path"],
|
by_name["diagnostics-after-finalization-gap"]["path"],
|
||||||
@@ -56,6 +58,8 @@ class FailureCoverageTest(unittest.TestCase):
|
|||||||
self.assertEqual(by_name["diagnostics-after-finalization-gap"]["method"], "GET")
|
self.assertEqual(by_name["diagnostics-after-finalization-gap"]["method"], "GET")
|
||||||
self.assertEqual(by_name["recovery-repairs-idempotency-index"]["method"], "POST")
|
self.assertEqual(by_name["recovery-repairs-idempotency-index"]["method"], "POST")
|
||||||
self.assertIn("does_not_exist.metadata.json", json.dumps(by_name["missing-metadata-object-rejected"]))
|
self.assertIn("does_not_exist.metadata.json", json.dumps(by_name["missing-metadata-object-rejected"]))
|
||||||
|
self.assertEqual(by_name["missing-metadata-object-rejected"]["body"]["requirements"], [])
|
||||||
|
self.assertEqual(by_name["missing-metadata-object-rejected"]["body"]["updates"], [])
|
||||||
self.assertNotIn("base", by_name["missing-metadata-object-rejected"]["body"])
|
self.assertNotIn("base", by_name["missing-metadata-object-rejected"]["body"])
|
||||||
|
|
||||||
def test_cli_prints_failure_matrix_and_probe_plan(self) -> None:
|
def test_cli_prints_failure_matrix_and_probe_plan(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user