apache/iceberg · error · DuplicateWAPCommitException

DuplicateWAPCommitException(wapId)

Error message

DuplicateWAPCommitException(wapId)

What it means

WapUtil.validateWapPublish checks whether a staged write-audit-publish (WAP) snapshot being published carries a wap.id whose value has already been published. Publishing the same staged snapshot twice would duplicate the commit, so it throws DuplicateWAPCommitException with the offending wapId.

Solutions

  1. Generate a unique wap.id per run (e.g. UUID) rather than reusing a fixed value
  2. Check WapUtil.validateWapPublish before calling publishWap and skip if the id is already published
  3. Catch DuplicateWAPCommitException and treat the publish as already complete (idempotent handling)
  4. Ensure only one job owns publishing for a given wap.id (locking/coordination)

Example fix

// before
String wapId = "daily-ingest"; // reused every run
table.publishWap();
// after
String wapId = UUID.randomUUID().toString();
table.properties().put(TableProperties.WRITE_WAP_ID, wapId);
// ... fastAppend ...
WapUtil.validateWapPublish(ops.refresh().current(), stagedId); // throws if already published
table.publishWap();
Defensive patterns

Strategy: try-catch

Validate before calling

String wapId = WapUtil.stagedWapId(table.currentSnapshot().snapshot(table.currentSnapshot().snapshotId()));
// or before publishing:
WapUtil.validateWapPublish(table.ops() != null ? ops.current() : null, stagedId); // throws early if already published

Try / catch

try {
  WapUtil.validateWapPublish(ops.current(), stagedSnapshotId);
  table.publishWap();
} catch (DuplicateWAPCommitException e) {
  log.info("WAP {} already published, skipping", e.wapId());
}

Prevention

When it happens

Trigger: Calling table.fastAppend()/cherryPick flow with WAP enabled (write.wap.id set) and then publishWap for a snapshot whose wap.id matches an already-published ancestor — e.g. publishing the same staged snapshot twice, or re-running a publish job after a partial failure.

Common situations: Idempotent job runners retrying a publish step that already succeeded; two concurrent jobs publishing the same staged WAP snapshot; reusing a wap.id string across runs instead of generating unique ids per run.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at core/src/main/java/org/apache/iceberg/util/WapUtil.java:54

  public static String publishedWapId(Snapshot snapshot) {
    return snapshot.summary() != null
        ? snapshot.summary().get(SnapshotSummary.PUBLISHED_WAP_ID_PROP)
        : null;
  }

  /**
   * Check if a given staged snapshot's associated wap-id was already published. Does not fail for
   * non-WAP workflows.
   *
   * @param current the current {@link TableMetadata metadata} for the target table
   * @param wapSnapshotId a snapshot id which could have been staged and is associated with a wap id
   * @return the WAP ID that will be published, if the snapshot has one
   */
  public static String validateWapPublish(TableMetadata current, long wapSnapshotId) {
    Snapshot cherryPickSnapshot = current.snapshot(wapSnapshotId);
    String wapId = stagedWapId(cherryPickSnapshot);
    if (wapId != null && !wapId.isEmpty() && WapUtil.isWapIdPublished(current, wapId)) {
      throw new DuplicateWAPCommitException(wapId);
    }

    return wapId;
  }

  private static boolean isWapIdPublished(TableMetadata current, String wapId) {
    for (long ancestorId : SnapshotUtil.ancestorIds(current.currentSnapshot(), current::snapshot)) {
      Snapshot snapshot = current.snapshot(ancestorId);
      if (wapId.equals(stagedWapId(snapshot)) || wapId.equals(publishedWapId(snapshot))) {
        return true;
      }
    }
    return false;
  }
}

View on GitHub (pinned to 86d9c8fc54)