[ 
https://issues.apache.org/jira/browse/IMPALA-15461?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Zoltán Borók-Nagy resolved IMPALA-15461.
----------------------------------------
    Fix Version/s: Impala 5.0.0
       Resolution: Fixed

> Iceberg: partition specs are looked up by list position instead of spec id; 
> SELECT crashes impalad after expire_snapshots with clean_expired_metadata 
> removes an old spec
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: IMPALA-15461
>                 URL: https://issues.apache.org/jira/browse/IMPALA-15461
>             Project: IMPALA
>          Issue Type: Bug
>          Components: Backend, Frontend
>            Reporter: Zoltán Borók-Nagy
>            Assignee: Zoltán Borók-Nagy
>            Priority: Major
>              Labels: impala-iceberg
>             Fix For: Impala 5.0.0
>
>
> h3. Problem
> Impala assumes that the partition spec with id N is at position N of the 
> table's spec list. The Iceberg spec does not promise this. 
> {{partition-specs}} is a list of spec objects, and a manifest's 
> {{partition_spec_id}} "must be listed in table metadata {{partition-specs}}", 
> i.e. specs are found by id. Since Iceberg 1.8.0, 
> {{ExpireSnapshots.cleanExpiredMetadata(true)}} removes every spec that is not 
> the default spec and is not used by a manifest of a retained snapshot (unused 
> schemas too, since 1.9.0). Spark exposes it as {{CALL ... 
> expire_snapshots(..., clean_expired_metadata => true)}} since Iceberg 1.10.0. 
> After that a table can have, for example, only spec 1 with 
> {{default-spec-id}} 1. This needs no unusual history: it happens to the first 
> spec of a table that was re-partitioned before its first write (shorter repro 
> below), or to an old spec whose files were all rewritten by compaction (main 
> recipe).
> * Backend: {{HdfsTableDescriptor}} reads 
> {{iceberg_partition_specs_\[default_partition_spec_id\]}}. With specs \[1\] 
> this reads past the end of the vector. This is undefined behavior. In our run 
> (master minicluster, query c34d48ebf0562378:5c585f8600000000) both impalads 
> that ran a fragment crashed with SIGSEGV while building the descriptor table; 
> one was the coordinator. On docker (master 3d4d47dc30, one impalad), 49 of 52 
> statements that reached the backend crashed impalad in the same constructor. 
> The other 3 returned the correct rows; none returned wrong rows. Most crashes 
> are SIGSEGV; some are an abort on {{std::bad_alloc}} or 
> {{std::length_error}}, thrown while copying the garbage fields.
> * Frontend (verified): {{getDefaultPartitionSpec()}} does 
> {{specs.get(defaultSpecId)}} after {{checkState(specs.size() > 
> defaultSpecId)}}. It throws {{IllegalStateException}} for SHOW CREATE TABLE, 
> SHOW PARTITIONS, CREATE TABLE LIKE, INSERT, INSERT OVERWRITE, UPDATE, MERGE 
> and OPTIMIZE. It also throws when the scan of the table has a WHERE predicate 
> on a table column or gets a runtime filter from a join: SELECT ... WHERE, 
> joins, and DELETE (a DELETE without WHERE is rejected with the TRUNCATE 
> hint). Everything else that scans the table reaches the backend and crashes 
> impalad (verified): SELECT, SELECT count(id), queries on its metadata tables 
> (e.g. {{t.snapshots}}), time travel, COMPUTE STATS, INSERT ... SELECT and 
> CTAS from the table, and joins or DELETE ... WHERE id IN (subquery) with 
> RUNTIME_FILTER_MODE=OFF. SELECT count(*) is answered from snapshot metadata 
> and does not crash.
> * If the default id is smaller than the list size, it silently returns 
> another spec: with specs \[1, 2\] and default 1 (spec 1 unpartitioned, spec 2 
> identity(region)), SHOW CREATE TABLE prints {{PARTITIONED BY SPEC (region)}} 
> and INSERT is planned with {{PARTITION-KEYS}}.
> Spark reads these tables fine.
> h3. Steps to reproduce
> Verified on master 3d4d47dc30 (RELEASE, docker, REST catalog, one impalad): 
> specs \[1\] with default 1, Spark returns 1, 2, 3, EXPLAIN of the SELECT 
> plans normally (7846550d1fb08eb5:351d9e0400000000), and the SELECT crashes 
> impalad (f94305d4fa598f69:b94bc09500000000; 19 of 20 runs on two fresh copies 
> of the table crashed, 1 returned the correct rows). The {{rw_b}} variant 
> (below) crashed impalad on both the minicluster and docker.
> Spark 4.1.3 + Iceberg 1.11.0 ({{ath}} is the Spark catalog name of the 
> Iceberg REST catalog):
> {code:sql}
> CREATE NAMESPACE IF NOT EXISTS ath.zz_jira;
> CREATE TABLE ath.zz_jira.nondense (id BIGINT, region STRING) USING iceberg
>   PARTITIONED BY (region) TBLPROPERTIES ('format-version'='2');
> INSERT INTO ath.zz_jira.nondense VALUES (1, 'EU'), (2, 'NA');   -- spec 0 = 
> identity(region)
> ALTER TABLE ath.zz_jira.nondense DROP PARTITION FIELD region;    -- spec 1 
> (no fields), now default
> CALL ath.system.rewrite_data_files(table => 'zz_jira.nondense', options => 
> map('rewrite-all', 'true'));
> INSERT INTO ath.zz_jira.nondense VALUES (3, 'AS');              -- drops the 
> spec-0 manifest that has only DELETED entries
> CALL ath.system.expire_snapshots(table => 'zz_jira.nondense', older_than => 
> current_timestamp(),
>   retain_last => 1, clean_expired_metadata => true);
> -- metadata: "partition-specs" : [ {"spec-id" : 1, "fields" : [ ]} ], 
> "default-spec-id" : 1
> SELECT * FROM ath.zz_jira.nondense ORDER BY id;                 -- Spark: 1, 
> 2, 3
> {code}
> Impala sees the table through the REST catalog (impalad 
> {{--catalog_config_dir}}), or after {{CREATE EXTERNAL TABLE ... STORED AS 
> ICEBERG LOCATION '...' TBLPROPERTIES('iceberg.catalog'='hadoop.tables')}} 
> (the location needs the HadoopTables layout: {{metadata/vN.metadata.json}} 
> and {{version-hint.text}}). Then: {{SELECT * FROM zz_jira.nondense ORDER BY 
> id;}}
> Without the INSERT between rewrite_data_files and expire_snapshots, spec 0 
> survives: the only retained snapshot still has a spec-0 manifest with DELETED 
> entries (verified with this recipe: specs \[0, 1\] after expire; Impala reads 
> it, query 42462edf832efaf4:e40575ad00000000).
> Silent wrong spec: after the recipe, {{ALTER TABLE ... ADD PARTITION FIELD 
> region; ALTER TABLE ... DROP PARTITION FIELD region;}} gives specs \[1, 2\] 
> with default 1 (Iceberg reuses spec id 1).
> Crash variant ({{rw_b}}, 2026-09-28, master minicluster, hadoop.tables): 
> differs by an extra {{INSERT (3, 'AS')}} after DROP PARTITION FIELD, {{DROP 
> COLUMN region}} before the rewrite, {{INSERT VALUES (4)}} as the last insert, 
> and expiration through the Java API ({{cleanExpiredMetadata(true)}}; on 
> docker the Spark procedure, same result). Result: specs \[1\], schemas \[1\], 
> Spark returns 1-4.
> Shorter repro (verified: specs \[1\] with default 1, Spark returns 1, 2, 3, 
> the Impala SELECT crashes impalad, 4b42f3feea4ed23f:d53da1a500000000). The 
> old spec never held data, so no rewrite is needed, and the expiration removes 
> no snapshot:
> {code:sql}
> CREATE TABLE ath.zz_jira.mini (id BIGINT, region STRING) USING iceberg
>   PARTITIONED BY (region) TBLPROPERTIES ('format-version'='2');
> ALTER TABLE ath.zz_jira.mini DROP PARTITION FIELD region;
> INSERT INTO ath.zz_jira.mini VALUES (1, 'EU'), (2, 'NA'), (3, 'AS');
> CALL ath.system.expire_snapshots(table => 'zz_jira.mini', older_than => 
> current_timestamp(),
>   retain_last => 1, clean_expired_metadata => true);
> {code}
> h3. Expected result
> The rows Spark returns (1, 2, 3).
> h3. Actual result (master 3d4d47dc30, RELEASE)
> Minicluster: {{SELECT * FROM rw_b ORDER BY id}} (query 
> c34d48ebf0562378:5c585f8600000000): impala-shell gets {{TSocket read 0 
> bytes}}. The coordinator (pid 2049916) and an executor (pid 2049918) crashed. 
> Both cores have the same stack:
> {noformat}
> #11 __memmove_avx_unaligned_erms
> #18 impala::TIcebergPartitionField::TIcebergPartitionField 
> (CatalogObjects_types.cpp:3310)
> #22 std::vector<impala::TIcebergPartitionField>::push_back
> #23 impala::HdfsTableDescriptor::HdfsTableDescriptor (descriptors.cc:294)
> #24 impala::DescriptorTbl::CreateTblDescriptorInternal (descriptors.cc:637)
> #25 impala::DescriptorTbl::CreateInternal (descriptors.cc:676)
> #26 impala::DescriptorTbl::Create (descriptors.cc:667)
> #27 impala::QueryState::StartFInstances (query-state.h:166)
> #28 impala::QueryExecMgr::ExecuteQueryHelper (query-exec-mgr.cc:173)
> {noformat}
> In frame 23 of core.2049916 the table is {{athl_v5x.rw_b}}, 
> {{iceberg_spec_id_}} = 1, and {{iceberg_partition_specs_}} has one element, 
> with {{spec_id}} 1 and 0 fields.
> Frontend results on docker (EXPLAIN does not start fragments; REST table with 
> specs \[1\], DML on the same table registered in HMS as hadoop.tables):
> ||Statement||Result||Query id||
> |EXPLAIN SELECT * ORDER BY id|plans 
> normally|7846550d1fb08eb5:351d9e0400000000|
> |EXPLAIN SELECT ... WHERE id = 
> 1|IllegalStateException|764ae9ab0790078b:959b730d00000000|
> |EXPLAIN self-join on 
> region|IllegalStateException|5f484b2d1ac66464:2c16331d00000000|
> |SHOW CREATE TABLE / SHOW 
> PARTITIONS|IllegalStateException|d5411b033751c2a6:7bf590fc00000000, 
> e6468807f7404e96:c431a07200000000|
> |EXPLAIN INSERT / INSERT 
> OVERWRITE|IllegalStateException|8e4343456525cd74:d6522cd900000000, 
> e043fe82b2c02021:0ea9696f00000000|
> |EXPLAIN DELETE ... WHERE / 
> UPDATE|IllegalStateException|084e193874245e9e:bd7cbec000000000, 
> 87490281d32e1c20:6d8011e500000000|
> |specs \[1, 2\], default 1: SHOW CREATE TABLE|PARTITIONED BY SPEC 
> (region)|a14502f29d422b4b:4d8451f200000000|
> |specs \[1, 2\], default 1: EXPLAIN 
> INSERT|PARTITION-KEYS=('xx')|36418e2603cf1ca6:cb14990000000000|
> The control table with specs \[0, 1\] runs the REST statements normally (SHOW 
> PARTITIONS answers "Table is not partitioned", as expected for an 
> unpartitioned default spec).
> h3. Analysis (master 3d4d47dc30)
> * The frontend sends the specs as a list, in map order, not indexed by id. 
> {{FeIcebergTable.Utils.loadPartitionSpecByIceberg()}} 
> ({{FeIcebergTable.java:1218-1226}}) iterates {{specs().values()}}. 
> {{getTIcebergTable()}} ({{:974-978}}) adds them to 
> {{TIcebergTable.partition_spec}} and sets {{default_partition_spec_id}} 
> ({{common/thrift/CatalogObjects.thrift:717-718}}; the id of each spec is 
> {{TIcebergPartitionSpec.spec_id}}, {{:654}}).
> * Backend, {{be/src/runtime/descriptors.cc:287-295}}: {{iceberg_spec_id_ = 
> default_partition_spec_id}}, then {{const TIcebergPartitionSpec& spec = 
> iceberg_partition_specs_\[iceberg_spec_id_\]}} (:289). The 
> {{DCHECK_EQ(spec.spec_id, iceberg_spec_id_)}} (:290) exists only in debug 
> builds. The loop at :291-295 then copies the fields of whatever memory 
> follows the vector.
> * Backend, same assumption (verified): 
> {{IcebergDeleteSinkBase::ConstructPartitionInfo()}} uses 
> {{IcebergPartitionSpecs()\[spec_id\]}} 
> ({{be/src/exec/iceberg-delete-sink-base.cc:152-153}}) for position deletes of 
> data files written with a non-default spec (DELETE, UPDATE, MERGE). With 
> specs \[1\] these statements fail before the sink. With specs \[1, 2\], 
> default 1, and a data file written with spec 2 (after the recipe: ADD 
> PARTITION FIELD region, INSERT, DROP PARTITION FIELD region), a DELETE of a 
> row in that file reads index 2 of a 2-element vector and crashed impalad in 
> {{ConstructPartitionInfo}} ({{iceberg-delete-sink-base.cc:157}}, from 
> {{IcebergBufferedDeleteSink::FlushBufferedRecords}}): 
> b64c34605120c14c:bd7667d500000000 (SIGSEGV), 
> d24bdeff14213c0e:5e7a09f300000000 (abort on std::bad_alloc).
> * Backend, metadata tables and time travel (verified): 
> {{IcebergMetadataTable.toThriftDescriptor()}} 
> ({{IcebergMetadataTable.java:123-126}}) sends the base table's specs in an 
> ICEBERG_TABLE descriptor, so {{CreateTblDescriptorInternal}} 
> ({{descriptors.cc:635-637}}) builds an {{HdfsTableDescriptor}} for it too. 
> {{SELECT ... FROM t.snapshots}} and {{t.`files`}} crashed impalad in 3 of 4 
> runs (5348d780119d84f1:f3f8a9b000000000), and so did a time-travel SELECT 
> (9740a78b28f9e22c:96f96ae600000000).
> * Frontend: {{FeIcebergTable.Utils.getDefaultPartitionSpec()}} 
> ({{FeIcebergTable.java:1243-1251}}) throws at {{:1249}}. Verified with the 
> stacks the impalad log has at glog level 1 (the default level logs no stack): 
> SELECT ... WHERE and DELETE ... WHERE via 
> {{IcebergScanPlanner.java:1020/1048/1227}}, joins via 
> {{RuntimeFilterGenerator.java:1505}} -> 
> {{FeIcebergTable.isComputedPartitionColumn()}} (:423), SHOW CREATE TABLE via 
> {{ToSqlUtils.java:564}}, SHOW PARTITIONS via {{ShowStatsStmt.java:142}}, 
> INSERT via {{InsertStmt.java:868}}, INSERT OVERWRITE via 
> {{InsertStmt.java:653}}, UPDATE via {{IcebergUpdateImpl.java:118}}, MERGE via 
> {{IcebergUtil.populatePartitionExprs()}} ({{IcebergUtil.java:1793}}) from 
> {{IcebergMergeImpl.java:176}}. By code reading, OPTIMIZE 
> ({{OptimizeStmt.java:297}}) and UPDATE ({{IcebergUpdateImpl.java:141}}) also 
> call {{populatePartitionExprs()}}, CREATE TABLE LIKE calls 
> {{getDefaultPartitionSpec()}} in catalogd ({{CatalogOpExecutor.java:4693}}), 
> and {{InsertStmt.java:534}} is another caller.
> * A lookup by id already exists: {{FeIcebergTable.getPartitionSpec(int 
> specId)}} ({{:233-238}}), used in 
> {{IcebergCatalogOpExecutor.java:587/631/652}}.
> * History: the frontend lookup and the {{default_partition_spec_id}} field 
> came with IMPALA-10184 (a5019eb12e, 4.0.0). The lookup in 
> {{HdfsTableDescriptor}} came with IMPALA-10380 (296ed74d6f, 4.0.0). 
> IMPALA-11877 part 1 (c579212251, 4.3.0) added the DCHECK_EQ. Part 2 
> (ce4202a70b, 4.3.0) moved the lookup into {{iceberg_partition_specs_}} and 
> added the delete sink lookup, which IMPALA-12313 part 3 (68fe57ff84, 4.4.0) 
> moved to {{iceberg-delete-sink-base.cc}}. So every release since 4.0.0 has 
> the descriptor lookup (code reading).
> * 4.5.2 has the same code ({{descriptors.cc:261}}, 
> {{iceberg-delete-sink-base.cc:151}}, {{FeIcebergTable.java:853-854}}) and the 
> same behavior. Verified on the apache/impala:4.5.2 images (docker, 
> hadoop.tables): the same IllegalStateExceptions (e.g. 
> 614ca16fde04c17c:3737c23d00000000, a64ff5b4fbfdee2e:52076d1900000000) and the 
> wrong spec for specs \[1, 2\] with default 1. SELECT crashed impalad in 7 of 
> 8 runs (05467358c27c9ba6:f592a26700000000) and a metadata-table query in 2 of 
> 4 runs (0b4366fabd375810:3c268cff00000000), in 
> {{HdfsTableDescriptor::HdfsTableDescriptor}}.
> * Impala itself does not create such tables. Its {{ALTER TABLE ... EXECUTE 
> EXPIRE_SNAPSHOTS}} never calls {{cleanExpiredMetadata}} 
> ({{IcebergCatalogOpExecutor.java:258-266}}). Other writers can (API since 
> Iceberg 1.8.0, Spark procedure since 1.10.0; checked in the Iceberg sources 
> and jars); the removal is the {{MetadataUpdate.RemovePartitionSpecs}} table 
> update.
> Suggested fix: look specs up by id everywhere. In the backend, build an id -> 
> spec map in the {{HdfsTableDescriptor}} constructor and return an error if 
> the default id is missing. The constructor cannot return a Status, so check 
> in {{DescriptorTbl::CreateTblDescriptorInternal}} 
> ({{descriptors.cc:635-640}}) or add an Init(). Use the map in 
> {{iceberg-delete-sink-base.cc}} too. In the frontend, implement 
> {{getDefaultPartitionSpec()}} as 
> {{getPartitionSpec(getDefaultPartitionSpecId())}}. Keep {{partition_spec}} a 
> list: {{TIcebergTable}} is shared by catalogd and impalad, and the thrift 
> compatibility check (IMPALA-13305) flags a changed field type. Tests: a table 
> without spec 0 (SELECT, INSERT, DELETE, SHOW CREATE TABLE), and specs \[1, 
> 2\] with default 1 (ADD PARTITION FIELD then DROP PARTITION FIELD after the 
> recipe; verified).
> h3. Workaround
> Do not run {{expire_snapshots}} with {{clean_expired_metadata}} on tables 
> that Impala reads. An affected table has to be copied into a new table (e.g. 
> Spark CTAS), which starts again at spec 0. Verified: the Spark CTAS copy has 
> specs \[0\] and Impala returns 1, 2, 3 (f04b9db466b0b9e6:d4fab6a200000000). 
> Make the copy with another engine: an Impala CTAS or INSERT ... SELECT reads 
> the affected table and crashes impalad (verified: INSERT ... SELECT 
> bc4657ff539c87b7:c4c3888500000000, CTAS 1e4a8ec78e0c9d32:3311ae5b00000000, 
> ff4e09ecfd378505:7e983bf800000000).
> Adding a new spec does not help: its id is 2, and \[1, 2\] with default 2 is 
> still out of range (verified: same IllegalStateExceptions, 
> a440a1878dd1d24c:4fc8b31600000000, 3b4497ad5c3dcf28:f064050d00000000; EXPLAIN 
> SELECT * plans normally, 49477f4e20dca99c:b9398b7d00000000). Going back to 
> the old spec reuses id 1, and Impala silently uses spec 2.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to