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

  1. 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.
  2. 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.
  3. For custom Client/channel wrappers, do not manually call release()/close() unless you own the corresponding retain(); let Dubbo's framework manage the lifecycle.
  4. 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

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


AI-assisted analysis of apache/dubbo@3a3043227f (2026-08-14). Data as JSON: /api/errors/22136768e330bf59. Report an issue: GitHub.