apache/iceberg · error · UncheckedIOException

Failed to close changelog scan: + scan

Error message

Failed to close changelog scan: + scan

What it means

SparkChangelogScan.taskGroups plans changelog tasks with try-with-resources; if closing the planned task groups' CloseableIterable throws IOException, it is wrapped in UncheckedIOException('Failed to close changelog scan: <scan>'). The planning succeeded but resource cleanup failed, indicating FileIO-level trouble (file system errors while releasing resources).

Source

Thrown at spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/source/SparkChangelogScan.java:122

  @Override
  public Batch toBatch() {
    return new SparkBatch(
        sparkContext,
        table,
        null != scan ? scan.fileIO() : table::io,
        readConf,
        EMPTY_GROUPING_KEY_TYPE,
        taskGroups(),
        expectedSchema,
        hashCode());
  }

  private List<ScanTaskGroup<ChangelogScanTask>> taskGroups() {
    if (taskGroups == null) {
      try (CloseableIterable<ScanTaskGroup<ChangelogScanTask>> groups = scan.planTasks()) {
        this.taskGroups = Lists.newArrayList(groups);
      } catch (IOException e) {
        throw new UncheckedIOException("Failed to close changelog scan: " + scan, e);
      }
    }

    return taskGroups;
  }

  @Override
  public String description() {
    return String.format(
        Locale.ROOT,
        "%s [fromSnapshotId=%d, toSnapshotId=%d, filters=%s]",
        table,
        startSnapshotId,
        endSnapshotId,
        Spark3Util.describe(filters));
  }

  @Override

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Inspect the chained IOException for the storage-level cause.
  2. Retry the scan/stream micro-batch; close-time errors are usually transient.
  3. Verify storage credentials/refresh (S3 session tokens, HDFS delegation tokens) cover the job duration.
  4. Check network stability and storage endpoint health for executors.

Example fix

// before
try (CloseableIterable<...> groups = scan.planTasks()) { ... }
// after
try {
  try (CloseableIterable<...> groups = scan.planTasks()) { ... }
} catch (UncheckedIOException e) {
  if (isTransient(e.getCause())) retry(); else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// verify storage is reachable before planning
FileSystem fs = FileSystem.get(table.location(), hadoopConf);
fs.exists(new Path(table.location())); // throws early on connectivity issues

Try / catch

try { planTaskGroups(); } catch (UncheckedIOException e) {
  if (isTransientStorageError(e.getCause())) retryWithBackoff(); else throw e;
}

Prevention

When it happens

Trigger: scan.planTasks() succeeds but the CloseableIterable.close() inside the try-with-resources throws IOException — e.g. HDFS/S3 error during manifest reader close, stream aborted by timeout, or disk/network failure on the executor.

Common situations: Object store throttling or token expiry during task-group close; flaky network between Spark executor and storage; HDFS client cache eviction errors.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/79aecb245c674989. Report an issue: GitHub.