apache/dubbo · critical · AssertionError
This instance has been destroyed
Error message
This instance has been destroyed
What it means
Thrown by retain() on a ReferenceCountedResource when its atomic counter was already <= 0 at the moment of increment. The resource uses reference counting inspired by Netty: counter starts at 1, release() decrements it, and when it reaches 0 the resource is destroyed. Calling retain() (increment) on an already-destroyed instance is an irrecoverable programming error, so it throws AssertionError rather than a checked exception. Dubbo uses this for connection/client lifecycle (e.g., Client shared across proxies).
Source
Thrown at dubbo-common/src/main/java/org/apache/dubbo/common/reference/ReferenceCountedResource.java:44
/**
* inspired by Netty
*/
public abstract class ReferenceCountedResource implements AutoCloseable {
private static final ErrorTypeAwareLogger logger =
LoggerFactory.getErrorTypeAwareLogger(ReferenceCountedResource.class);
private static final AtomicLongFieldUpdater<ReferenceCountedResource> COUNTER_UPDATER =
AtomicLongFieldUpdater.newUpdater(ReferenceCountedResource.class, "counter");
private volatile long counter = 1;
/**
* Increments the reference count by 1.
*/
public final ReferenceCountedResource retain() {
long oldCount = COUNTER_UPDATER.getAndIncrement(this);
if (oldCount <= 0) {
COUNTER_UPDATER.getAndDecrement(this);
throw new AssertionError("This instance has been destroyed");
}
return this;
}
/**
* Decreases the reference count by 1 and calls {@link this#destroy} if the reference count reaches 0.
*/
public final boolean release() {
long remainingCount = COUNTER_UPDATER.decrementAndGet(this);
if (remainingCount == 0) {
destroy();
return true;
} else if (remainingCount <= -1) {
logger.warn(PROTOCOL_ERROR_CLOSE_CLIENT, "", "", "This instance has been destroyed");
return false;
} else {
return false;View on GitHub (pinned to 3a3043227f)
Solutions
- Audit every retain()/release() (or close()) call path for balance — each retain must have exactly one matching release, and retain must never be called after the count reaches 0.
- If the error occurs during shutdown, ensure no new invocations (that trigger retain) are initiated after ReferenceConfig.destroy() or graceful close has started — gate new calls with a lifecycle flag.
- For custom Client/channel wrappers, do not manually call release()/close() unless you own the corresponding retain(); let Dubbo's framework manage the lifecycle.
- Add thread-safety around the retain-then-use pattern: check a volatile 'closed' flag before calling retain, or synchronize with the destroy path.
Example fix
// before — retain called on a possibly-destroyed resource
resource.retain();
use(resource);
resource.release();
// after — guard against destroyed state
if (resource.isDestroyed()) { // add an isDestroyed() check using counter <= 0
throw new IllegalStateException("resource already destroyed");
}
resource.retain();
try {
use(resource);
} finally {
resource.release();
} Defensive patterns
Strategy: validation
Validate before calling
// Before calling retain(), verify the resource is still alive.
// ReferenceCountedResource has no public isDestroyed() — track lifecycle externally:
public boolean isAlive(ReferenceCountedResource r) {
return !closed.get(); // your own volatile flag, set in a destroy hook
}
if (isAlive(resource)) {
resource.retain();
// ... use ...
} else {
throw new IllegalStateException("resource already destroyed");
} Try / catch
// AssertionError is not meant to be caught — it signals a programming bug.
// Correct pattern: prevent it via lifecycle discipline, not catch.
try {
resource.retain();
} catch (AssertionError e) {
// log and fail loudly — this indicates a logic error, not transient failure
logger.error("retain() on destroyed resource", e);
throw e;
} Prevention
- Match every retain() with exactly one release() in a try/finally block.
- Never call retain() after a release() that may have driven the count to 0.
- Use try-with-resources (close() calls release()) for automatic cleanup.
- Gate new retain() calls behind a volatile 'closed' flag synchronized with the destroy path.
When it happens
Trigger: Calling retain() after release() has already driven the counter to 0 and destroy() has run. This happens with unbalanced retain/release pairs — e.g., retaining a connection that a concurrent or prior release() has already torn down, or retaining a shared Dubbo Client object after the reference count hit zero during graceful shutdown.
Common situations: Concurrent close of a Dubbo reference/client while another thread attempts to open a new invocation channel; double-release of a shared resource causing premature destroy followed by a retain; incorrect custom SPI or filter code that calls release() manually and then the framework calls retain(); version upgrades that changed the retain/release protocol for shared clients.
Related errors
- ExtensionDirector is destroyed
- ExtensionLoader is destroyed: ${type}
- INTERNAL_ERROR
- Directory of type {simpleName} already destroyed for service
- Failed to acquire read lock on invokerRefreshLock within tim
AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14).
Data as JSON: /api/errors/22136768e330bf59.
Report an issue: GitHub.