Why SDS's `downgradeFinally` Must Be Called in a Finally Block

SDS's downgradeFinally must execute inside a finally block to guarantee cleanup of concurrency counters, timeout metrics, and ThreadLocal state regardless of whether the protected method returns normally or throws an exception.

The downgradeFinally method in the Didi Service-Degradation-Solution (SDS) serves as the mandatory cleanup entry point for every SDS-protected invocation. Located in didi/sds, this design pattern ensures that critical resource accounting happens exactly once per entry, preventing metric corruption and resource leaks in high-throughput Java applications.

The Critical Responsibilities of downgradeFinally

The downgradeFinally implementation in CommonSdsClient performs three non-negotiable tasks that must survive exceptional exits:

Execution Time Measurement

The method records the actual execution duration via TimeStatisticsUtil.getConsumeTime(). If an exception interrupts the business logic, the normal return path is skipped. Only a finally block guarantees this measurement executes, ensuring the SDS statistics accurately reflect real-world performance regardless of failure states.

Concurrency Counter Release

SdsPowerfulCounterService.getInstance().concurrentRelease(point) decrements the in-flight request counter for the specific degradation point. Without this release executing in a finally block, the system continues to count the request as active, eventually triggering false-positive "concurrency exhausted" states that block legitimate traffic.

Timeout Accounting and Thread-Local Cleanup

When execution exceeds the configured timeout threshold, downgradeFinally invokes timeoutInvokeAddAndGet and clears the thread-local start time via setDowngradeStartTime(null). The start time is stored in a ThreadLocal variable; failing to clear it within a finally block leaks state to subsequent requests on the same thread, corrupting timeout calculations for future invocations.

Consequences of Missing the Finally Block

Omitting downgradeFinally from a finally block—or calling it only on the success path—creates three critical failure modes:

  • Inaccurate statistics – Timeout counts and execution-time histograms become misleading when exception paths bypass metric collection.
  • Resource leakage – The concurrency counter never decrements after exceptions, eventually exhausting the configured limit and triggering unnecessary degradation.
  • Thread-local contamination – Leftover start-time values in ThreadLocal storage affect later calls on the same thread, producing phantom timeout alerts.

Implementation Examples from the SDS Source Code

Manual Integration with SdsEasyUtil

The high-level API explicitly wraps business calls with try/catch/finally to enforce the contract:

try {
    if (sdsClient.shouldDowngrade(point)) {
        return downgradeValue;
    }
    return bizFunction.invokeBizMethod();
} catch (Throwable e) {
    sdsClient.exceptionSign(point, e);
    throw e;
} finally {
    // Guaranteed execution – cleans up counters, records timeout, resets TL state
    sdsClient.downgradeFinally(point);
}

Source: sds-easy/src/main/java/com/didiglobal/sds/easy/SdsEasyUtil.javainvokerMethod

Bytecode Injection in SdsClassFileTransformer

For zero-code instrumentation, the transformer injects downgradeFinally into the finally-part of annotated methods:

// insertAfter(..., true) adds the code to the finally-part of the method
declaredMethod.insertAfter(
    String.format("{ SdsClient ____sdsClient = SdsBootStrap.getClient(); " +
                  "if( ____sdsClient != null ) {____sdsClient.downgradeFinally(\"%s\");} }",
                  sdsDowngradeMethod.point()), true);

Source: sds-bootstrap/src/main/java/com/didiglobal/sds/bootstrap/transformer/SdsClassFileTransformer.javatransform

Core Cleanup Logic in CommonSdsClient

The implementation itself uses a nested finally to ensure ThreadLocal cleanup survives internal errors:

public void downgradeFinally(String point) {
    try {
        long consumeTime = TimeStatisticsUtil.getConsumeTime();
        SdsPowerfulCounterService.getInstance().concurrentRelease(point);
        SdsStrategy strategy = SdsStrategyService.getInstance().getStrategy(point);
        if (strategy != null && strategy.getTimeoutThreshold() != null
                && consumeTime > strategy.getTimeoutThreshold()) {
            Long start = getDowngradeStartTime();
            if (start != null) {
                SdsPowerfulCounterService.getInstance()
                    .timeoutInvokeAddAndGet(point, start);
            }
        }
    } finally {
        // ALWAYS clear the thread-local regardless of any internal error
        setDowngradeStartTime(null);
    }
}

Source: sds-client/src/main/java/com/didiglobal/sds/client/CommonSdsClient.javadowngradeFinally

Key Source Files

The finally contract is enforced across these critical components:

Summary

  • downgradeFinally is mandatory cleanup – It releases concurrency slots, records execution time, and handles timeout accounting for every SDS-protected method.
  • Finally blocks guarantee execution – Only a finally block ensures cleanup runs after both normal returns and exceptions, preventing resource leaks.
  • ThreadLocal safety requires it – The method clears thread-local start times to prevent metric corruption across thread reuse in pooled environments.
  • Both manual and automatic integrations enforce this – Whether using SdsEasyUtil or bytecode injection, the framework ensures downgradeFinally executes in a finally context.

Frequently Asked Questions

What happens if downgradeFinally is called outside a finally block?

Calling downgradeFinally only on the success path causes the concurrency counter to remain incremented when exceptions occur, eventually exhausting the limit. Additionally, ThreadLocal start times leak into subsequent requests, producing false timeout metrics and inaccurate execution statistics.

How does SDS ensure downgradeFinally runs after exceptions?

The framework enforces this through two mechanisms: the SdsEasyUtil wrapper explicitly places the call in a Java finally block, and the bytecode transformer uses Javassist's insertAfter with the true parameter to inject the call into the finally-part of the target method.

Why is ThreadLocal cleanup critical in downgradeFinally?

SDS stores method start times in a ThreadLocal variable to calculate execution duration. If an exception occurs and downgradeFinally is skipped, that start time persists in the thread. When the thread is reused from a pool, the next request reads the stale timestamp, causing getConsumeTime() to return an inflated value and potentially triggering false timeout alerts.

Can I use SDS without manually placing downgradeFinally?

Yes. By using the @SdsDowngradeMethod annotation combined with the SDS Java agent, the SdsClassFileTransformer automatically injects the downgradeFinally call into the finally-part of your method during class loading, eliminating manual try/finally boilerplate while maintaining the cleanup guarantee.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →