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

> Ensure SDS downgradeFinally executes in a finally block for reliable cleanup of counters metrics and state. Protects against exceptions and normal returns.

- Repository: [DiDi/sds](https://github.com/didi/sds)
- Tags: internals
- Published: 2026-02-28

---

**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:

```java
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.java`](https://github.com/didi/sds/blob/main/sds-easy/src/main/java/com/didiglobal/sds/easy/SdsEasyUtil.java) – `invokerMethod`

### Bytecode Injection in `SdsClassFileTransformer`

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

```java
// 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.java`](https://github.com/didi/sds/blob/main/sds-bootstrap/src/main/java/com/didiglobal/sds/bootstrap/transformer/SdsClassFileTransformer.java) – `transform`

### Core Cleanup Logic in `CommonSdsClient`

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

```java
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.java`](https://github.com/didi/sds/blob/main/sds-client/src/main/java/com/didiglobal/sds/client/CommonSdsClient.java) – `downgradeFinally`

## Key Source Files

The `finally` contract is enforced across these critical components:

- **[`sds-client/src/main/java/com/didiglobal/sds/client/CommonSdsClient.java`](https://github.com/didi/sds/blob/main/sds-client/src/main/java/com/didiglobal/sds/client/CommonSdsClient.java)** – Implements the core `downgradeFinally` cleanup logic and ThreadLocal management.
- **[`sds-easy/src/main/java/com/didiglobal/sds/easy/SdsEasyUtil.java`](https://github.com/didi/sds/blob/main/sds-easy/src/main/java/com/didiglobal/sds/easy/SdsEasyUtil.java)** – Provides the high-level `invokerMethod` API that wraps business calls with mandatory `try/finally` semantics.
- **[`sds-bootstrap/src/main/java/com/didiglobal/sds/bootstrap/transformer/SdsClassFileTransformer.java`](https://github.com/didi/sds/blob/main/sds-bootstrap/src/main/java/com/didiglobal/sds/bootstrap/transformer/SdsClassFileTransformer.java)** – Generates bytecode that injects `downgradeFinally` into the finally-part of methods annotated with `@SdsDowngradeMethod`.
- **[`sds-client/src/main/java/com/didiglobal/sds/client/util/TimeStatisticsUtil.java`](https://github.com/didi/sds/blob/main/sds-client/src/main/java/com/didiglobal/sds/client/util/TimeStatisticsUtil.java)** – Supplies the execution-time measurement consumed by `downgradeFinally`.
- **[`sds-client/src/main/java/com/didiglobal/sds/client/service/SdsPowerfulCounterService.java`](https://github.com/didi/sds/blob/main/sds-client/src/main/java/com/didiglobal/sds/client/service/SdsPowerfulCounterService.java)** – Maintains the concurrency and timeout counters updated during cleanup.

## 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.