apache/pulsar · error · MetadataStoreException.BadVersionException
Version mismatch, actual=%s, expect=%s
Error message
Version mismatch, actual=%s, expect=%s
What it means
storeDelete supports optimistic concurrency: when a caller passes an expected version, the record's current version read inside the transaction must equal it. If it differs, a MetadataStoreException.BadVersionException with 'Version mismatch, actual=%s, expect=%s' is thrown and the delete is aborted.
Source
Thrown at pulsar-metadata/src/main/java/org/apache/pulsar/metadata/impl/RocksdbMetadataStore.java:573
}
@Override
protected CompletableFuture<Void> storeDelete(String path, Optional<Long> expectedVersion, Set<Option> opts) {
log.debug().attr("path", path).attr("instanceId", instanceId).log("storeDelete");
try {
dbStateLock.readLock().lock();
if (isClosed()) {
return alreadyClosedFailedFuture();
}
try (Transaction transaction = db.beginTransaction(writeOptions)) {
byte[] pathBytes = toBytes(path);
byte[] oldValueData = transaction.getForUpdate(optionDontCache, pathBytes, true);
MetaValue metaValue = MetaValue.parse(oldValueData);
if (metaValue == null) {
throw new MetadataStoreException.NotFoundException(String.format("path %s not found.", path));
}
if (expectedVersion.isPresent() && !expectedVersion.get().equals(metaValue.getVersion())) {
throw new MetadataStoreException.BadVersionException(
String.format("Version mismatch, actual=%s, expect=%s", metaValue.getVersion(),
expectedVersion.get()));
}
transaction.delete(pathBytes);
transaction.commit();
receivedNotification(new Notification(NotificationType.Deleted, path));
notifyParentChildrenChanged(path);
return CompletableFuture.completedFuture(null);
}
} catch (Throwable e) {
log.debug().attr("path", path).exception(e).log("error in storeDelete");
return FutureUtil.failedFuture(MetadataStoreException.wrap(e));
} finally {
dbStateLock.readLock().unlock();
}
}
@OverrideView on GitHub (pinned to 820761864e)
Solutions
- Re-read the current version (get) and retry the delete with the fresh version.
- Catch BadVersionException and implement compare-and-swap retry with backoff.
- Serialize deletes for the same path through a single writer/coordinator.
- Adopt the notification mechanism to invalidate cached versions promptly.
Example fix
// before
long ver = getVersion(path);
store.delete(path, Optional.of(ver)).join(); // fails if path changed
// after
retryLoop: for (int i = 0; i < 5; i++) {
try {
store.get(path).thenApply(v -> v.map(Stat::getVersion))
.thenCompose(opt -> store.delete(path, opt)).join();
break;
} catch (CompletionException e) {
if (!(e.getCause() instanceof MetadataStoreException.BadVersionException)) throw e;
}
} Defensive patterns
Strategy: retry
Try / catch
try {
store.delete(path, Optional.of(expectedVersion)).join();
} catch (CompletionException e) {
if (e.getCause() instanceof MetadataStoreException.BadVersionException) {
// re-read version and retry CAS delete
} else throw e;
} Prevention
- Always re-read the version immediately before a versioned delete
- Bound CAS retries with backoff
- Avoid caching versions across multiple operations
When it happens
Trigger: Calling storeDelete/delete(path, expectedVersion) where another writer has modified the record since the caller read its version, so expectedVersion != metaValue.getVersion().
Common situations: Concurrent updates between read and delete in multi-writer setups; stale version cached after another instance's notification; app retry loops reusing an old version instead of re-reading.
Related errors
- Delete request ignored because it is out of date. Please try
- Update request ignored because it is out of date. Please try
- Invalid version type, expected it to be LongVersion
- Invalid MetaValue data, size=${size}
- Invalid MetaValue data, no enough header data. expect %d, ac
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/4b7a03f23f5fc834.
Report an issue: GitHub.