apache/iceberg · warning

Unclosed S3FileIO instance created by:\n\t

Error message

Unclosed S3FileIO instance created by:\n\t{}

What it means

S3FileIO.finalize() detects FileIO instances that were never close()d before garbage collection. It closes the resource, then logs this warning with the stack trace captured at creation time so developers can find the leak. Unclosed S3FileIO instances leak executor threads and credential-refresh schedulers.

Solutions

  1. Close S3FileIO in try-with-resources or a finally block
  2. Reuse a single S3FileIO instance across operations instead of creating per call
  3. Use the logged create-stack trace to locate the leaking construction site
  4. Ensure catalog-managed FileIO instances are closed when the table/catalog is discarded

Example fix

// before
FileIO io = new S3FileIO();
io.deleteFile(path); // never closed
// after
try (FileIO io = new S3FileIO()) {
  io.deleteFile(path);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// assert closure at end of job lifecycle
if (!ioClosed) throw new IllegalStateException("S3FileIO must be closed before shutdown");

Try / catch

try (S3FileIO io = new S3FileIO()) { io.initialize(props); /* use io */ }

Prevention

When it happens

Trigger: Application code creates S3FileIO (directly or via a catalog/loader) and never calls close(), then the instance becomes garbage.

Common situations: Per-file S3FileIO instantiation instead of sharing one; jobs that build Hadoop-aware FileIO without try-with-resources; leaked references in long-running services (Spark/Flink tasks).

Related errors


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

Appendix: source

Thrown at aws/src/main/java/org/apache/iceberg/aws/s3/S3FileIO.java:562

      }
      if (refreshFuture != null) {
        refreshFuture.cancel(true);
        refreshFuture = null;
      }
    }
  }

  @SuppressWarnings({"checkstyle:NoFinalizer", "Finalize", "deprecation"})
  @Override
  protected void finalize() throws Throwable {
    super.finalize();
    if (!isResourceClosed.get()) {
      close();

      if (null != createStack) {
        String trace =
            Joiner.on("\n\t").join(Arrays.copyOfRange(createStack, 1, createStack.length));
        LOG.warn("Unclosed S3FileIO instance created by:\n\t{}", trace);
      }
    }
  }

  @Override
  public boolean recoverFile(String path) {
    PrefixedS3Client client = clientForStoragePath(path);
    S3URI location = new S3URI(path, client.s3FileIOProperties().bucketToAccessPointMapping());
    ListObjectVersionsIterable response =
        client
            .s3()
            .listObjectVersionsPaginator(
                builder -> builder.bucket(location.bucket()).prefix(location.key()));

    // Recover to the last modified version, not isLatest,
    // since isLatest is true for deletion markers.
    Optional<ObjectVersion> recoverVersion =
        response.versions().stream().max(Comparator.comparing(ObjectVersion::lastModified));

View on GitHub (pinned to 86d9c8fc54)