Repository: etcd-io/etcd

Issue:
`etcd_debugging_mvcc_watcher_total` may become negative value
### Bug report criteria

- [x] This bug report is not security related, security issues should be disclosed privately via [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
- [x] This is not a support request or question, support requests or questions should be raised in the etcd [discussion forums](https://github.com/etcd-io/etcd/discussions).
- [x] You have read the etcd [bug reporting guidelines](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
- [x] Existing open issues along with etcd [frequently asked questions](https://etcd.io/docs/latest/faq) have been checked and this is not a duplicate.

### What happened?

`etcd_debugging_mvcc_watcher_total` may become negative value due to a race condition. The issue was fixed in https://github.com/etcd-io/etcd/pull/19600.

Two problems/followups,
- The PR was merged before it being properly reviewed. The test needs to be enhanced using a failpoint. Refer to https://github.com/etcd-io/etcd/pull/19600#issuecomment-2879359389
- The DCO workflow somehow failed in automatically check-picked PRs, so we need to manually backport the PR to stable releases (including 3.4, 3.5 and 3.6).

cc @kjgorman @ivanvc @jmhbnz @serathius 

### What did you expect to happen?

.

### How can we reproduce it (as minimally and precisely as possible)?

.

### Anything else we need to know?

_No response_

### Etcd version (please run commands below)

<details>

```console
$ etcd --version
# paste output here

$ etcdctl version
# paste output here
```

</details>


### Etcd configuration (command line flags or environment variables)

<details>

# paste your configuration here

</details>


### Etcd debug information (please run commands below, feel free to obfuscate the IP address or FQDN in the output)

<details>

```console
$ etcdctl member list -w table
# paste output here

$ etcdctl --endpoints=<member list> endpoint status -w table
# paste output here
```

</details>


### Relevant log output

```Shell

```

Hints from issue comments:
@ahrtr: > * The PR was merged before it being properly reviewed. The test needs to be enhanced using a failpoint. Refer to [mvcc: avoid double decrement of watcher gauge on close/cancel race #19600 (comment)](https://github.com/etcd-io/etcd/pull/19600#issuecomment-2879359389)

I think we should clearly document our review policy:
- Every PR should have at least 2 approvals before merging.
- For smaller or simple changes (like tests, workflows, etc.), 1 approval is fine.
- But if the author is a maintainer, they still need approval from another maintainer, reviewer, or submodule owner — even for minor changes.

cc @fuweid @ivanvc @jmhbnz @serathius @siyuanfoundation 
@serathius: > The PR was merged before it being properly reviewed. The test needs to be enhanced using a failpoint. Refer to https://github.com/etcd-io/etcd/pull/19600#issuecomment-2879359389

I personally don't agree with it. The change was pretty simple, covered with proper unit tests and reviewed by me and @ivanvc. 

> Every PR should have at least 2 approvals before merging.

While agree that for complicated changes it's good and customary to ask for more reviewers, I don't think it's a good default. I would prefer that we start moving in K8s direction of having a directory owners that are trusted to approve the PR or delegate to the best person to approve.@ahrtr: > I personally don't agree with it. The change was pretty simple, covered with proper unit tests and reviewed by me and [@ivanvc](https://github.com/ivanvc).

@ivanvc did not approve the PR.  Any production change on the core part (i.e. mvcc, applying workflow, etcdserver etc) should have at least two approvals. Rule is rule ( of course, to be reviewed & approved )

Also I see that some PRs were merged without any reviews,

- https://github.com/etcd-io/etcd/pull/19965 
- https://github.com/etcd-io/etcd/pull/19966 
- https://github.com/etcd-io/etcd/pull/19968 
- https://github.com/etcd-io/etcd/pull/19967 
- https://github.com/etcd-io/etcd/pull/19964 
- https://github.com/etcd-io/etcd/pull/19963 

Note we have blameless culture. It's not blaming anyone, it's just for safety & quality -- prevent any potential risk in future.@siyuanfoundation: While I agree in general more reviews would make etcd safer and improve code quality and we should do better. We also need to take into considerations that we only have a small pool of reviewers across different time zones. Having hard requirements could greatly hinder dev speed, especially for the ongoing antithesis project. I think it is acceptable to trust the approver's judgement calls while we all lean more on the more cautious side. 

In addition, if etcd is going to own submodules in kubernetes, it would be more consistent to adopt the review process from kubernetes. @ahrtr: > Having hard requirements could greatly hinder dev speed, especially for the ongoing antithesis project

It isn't just antithesis, it's also robustness test. When robustness test raises any issue, we need to analyse it as well, although not everyone work on it. So quality of robustness test matters.



> In addition, if etcd is going to own submodules in kubernetes

See my https://github.com/etcd-io/etcd/pull/19988, the reviewers include the submodule owners.

> Having hard requirements could greatly hinder dev speed

Quality & stability outweighs dev speed. If you see the PRs listed above, they are all raised & merged in few hours.

> I think it is acceptable to trust the approver's judgement calls

I think we should rely on process/policy to guarantee quality instead of any individual's judgement. Human makes mistakes. @ahrtr: > Having hard requirements

If you read the https://github.com/etcd-io/etcd/pull/19988, they aren't hard requirements. There are some flexiblities.@serathius: > Also I see that some PRs were merged without any reviews,
>
> - https://github.com/etcd-io/etcd/pull/19965 
> - https://github.com/etcd-io/etcd/pull/19966 
> - https://github.com/etcd-io/etcd/pull/19968 
> - https://github.com/etcd-io/etcd/pull/19967 
> - https://github.com/etcd-io/etcd/pull/19964 
> - https://github.com/etcd-io/etcd/pull/19963 
> 
> Note we have blameless culture. It's not blaming anyone, it's just for safety & quality -- prevent any potential risk in future.

That doesn't really seem blameless, a good practice is to ensure diverse examples. However I don't think there is a need scour PR history to make a point. I can take the fault because I merged those PRs. I did that after I broke robustness tests on main, after I missed that tests were not run on PR. It's easy to miss that tests didn't run because they are not showing up until `ok-to-test` is applied and GitHub collapses the list of tests. I should have reverted the faulty PR and just waited for review. Unfortunately instead of that I just repeatable tried to fix the error on multiple attempts. I just didn't want to revert PR from new contributor, and later I desperately tried to prevent on main branch being broken. 

I think the proper solution is not a policy, but just taking back the repo write access from maintainers. It's just to easy to make a mistake, to tempting to try to make a dirty fix.

> I think we should rely on process/policy to guarantee quality instead of any individual's judgement. Human makes mistakes.

I agree with the second part, but I take different conclusion. People make mistakes, but we should trust them to not be malicious and take accountability. Sorry if I broken your trust, but the goal was not to abuse the system. @kjgorman: 👋 Hello team, sorry for any confusion my PR may have caused here. With respect to the failpoint test and cherry-picking to backport, I'm happy to help here, bearing in mind I'm not at all familiar with your processes so might need some pointers.

For the failpoint for example - is this what you had in mind: https://github.com/etcd-io/etcd/pull/19997 ? It does help make the test fail deterministically locally without the patch after `gofail enable` etc., but I notice we don't seem to use failpoints in unit tests at the moment (e.g., couldn't find reference to it in `./scripts/test.sh` for running unit tests), so it seems like this won't run e.g. in CI/on PRs etc. Is that a correct understanding?@ahrtr: > That doesn't really seem blameless, a good practice is to ensure diverse examples. However I don't think there is a need scour PR history to make a point.

This is to clarify why I added the policy: "`If the author is a maintainer, they should still get approval from another maintainer, reviewer, or submodule owner, even for minor changes.`". Sorry if it made you feel uncomfortable — that was not my intention.



> I think the proper solution is not a policy, but just taking back the repo write access from maintainers

Agreed. It's also exactly the reason why I raised the comment in the first place. https://github.com/kubernetes/org/pull/5523#issuecomment-2790512943

cc @ivanvc @jmhbnz 




> but we should trust them to not be malicious and take accountability. Sorry if I broken your trust, but the goal was not to abuse the system.

I think we trust all members, at least trust all maintainers and reviewers.

> With respect to the failpoint test and cherry-picking to backport, I'm happy to help here

thx

> but I notice we don't seem to use failpoints in unit tests at the momen

Yes, failpoint isn't enabled by default for unit test. You can add an e2e test instead. Please refer to the example https://github.com/etcd-io/etcd/pull/19608

@ahrtr: @kjgorman can you pls backport the PR first, so that we can get the fix included in next patche?@kjgorman: Hey @ahrtr sorry for the delay here - busy week for me. I have a couple of hours in the calendar this morning I've set aside for this. I've got an E2E reproducer using failpoints now: https://github.com/etcd-io/etcd/pull/20064

I can also prepare PRs for the fix itself for prior releases (and then maybe we can choose whether or not to also 'backport' the E2E test). @ahrtr: > I can also prepare PRs for the fix itself for prior releases (and then maybe we can choose whether or not to also 'backport' the E2E test).

Yes, please backport the PR https://github.com/etcd-io/etcd/pull/19600 first, thx@ahrtr: @kjgorman Please also update the changelog for 3,4, 3,5 and 3.6, thx.

https://github.com/etcd-io/etcd/tree/main/CHANGELOG@kjgorman: > @kjgorman Please also update the changelog for 3,4, 3,5 and 3.6, thx.

I've raised https://github.com/etcd-io/etcd/pull/20096 for this.@github-actions: This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 21 days if no further activity occurs. Thank you for your contributions.

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 5ba33bc16..a7cfc14dc 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -10,7 +10,7 @@ body:
       label: Bug report criteria
       description: Please confirm this bug report meets the following criteria.
       options:
-        - label: This bug report is not security related, security issues should be disclosed privately via [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
+        - label: This bug report is not security related, security issues should be disclosed privately via security@etcd.io.
         - label: This is not a support request or question, support requests or questions should be raised in the etcd [discussion forums](https://github.com/etcd-io/etcd/discussions).
         - label: You have read the etcd [bug reporting guidelines](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
         - label: Existing open issues along with etcd [frequently asked questions](https://etcd.io/docs/latest/faq) have been checked and this is not a duplicate.
diff --git a/.github/workflows/antithesis-test.yml b/.github/workflows/antithesis-test.yml
index ae378df46..ad0938270 100644
--- a/.github/workflows/antithesis-test.yml
+++ b/.github/workflows/antithesis-test.yml
@@ -14,16 +14,6 @@ on:
         required: false
         default: 'etcd nightly antithesis run'
         type: string
-      config_image:
-        description: 'Config image'
-        required: true
-        default: us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-config:latest
-        type: string
-      images:
-        description: 'System images (separate with ;)'
-        required: true
-        default: us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-client:latest;us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-server:latest;docker.io/library/ubuntu:latest
-        type: string
       duration:
         description: 'Duration (exploration hours)'
         required: true
@@ -34,11 +24,21 @@ on:
         required: true
         type: string
         default: "etcd nightly antithesis run"
+      etcd_ref:
+        description: 'etcd version to build etcd-server from'
+        required: false
+        type: string
+        default: 'release-3.5'
       email:
         description: 'Additional email notification recipient (separate with ;)'
         required: true
         type: string
         default: ""
+      cfg_node_count:
+        description: 'Number of nodes in the etcd cluster'
+        required: false
+        type: int
+        default: 3
 
 # Declare default permissions as read only.
 permissions: read-all
@@ -51,6 +51,8 @@ jobs:
   build-and-push-and-test:
     runs-on: ubuntu-latest
     environment: Antithesis
+    env:
+      CFG_NODE_COUNT: ${{ inputs.cfg_node_count }}
     steps:
       - name: Checkout the code
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -63,35 +65,28 @@ jobs:
           password: ${{ secrets.ANTITHESIS_CONTAINER_REGISTRY_TOKEN }}
 
       - name: Build and push config image
-        uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
-        with:
-          context: ./tests/antithesis
-          file: ./tests/antithesis/Dockerfile.config
-          push: true
-          tags: |
-            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-config:latest,
-            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-config:${{ github.sha }}
+        working-directory: ./tests/antithesis
+        run: |
+          make antithesis-build-config-image IMAGE_TAG=${{ inputs.etcd_ref }}_${{ github.sha }}
+          export IMAGE="${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-config:${{ inputs.etcd_ref }}_${{ github.sha }}"
+          docker tag etcd-config:latest $IMAGE
+          docker push $IMAGE
 
       - name: Build and push client image
-        uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
-        with:
-          context: .
-          file: ./tests/antithesis/test-template/Dockerfile
-          push: true
-          tags: |
-            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:latest,
-            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:${{ github.sha }}
+        working-directory: ./tests/antithesis
+        run: |
+          make antithesis-build-client-docker-image
+          export IMAGE="${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:${{ inputs.etcd_ref }}_${{ github.sha }}"
+          docker tag etcd-client:latest $IMAGE
+          docker push $IMAGE
 
       - name: Build and push etcd image
         working-directory: ./tests/antithesis
         run: |
-          make antithesis-build-etcd-image
-          export TAG="${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-server:latest"
-          docker tag etcd-server:latest $TAG
-          docker push $TAG
-          export TAG="${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-server:${{ github.sha }}"
-          docker tag etcd-server:latest $TAG
-          docker push $TAG
+          make antithesis-build-etcd-image REF=${{ inputs.etcd_ref }}
+          export IMAGE="${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-server:${{ inputs.etcd_ref }}_${{ github.sha }}"
+          docker tag etcd-server:latest $IMAGE
+          docker push $IMAGE
 
       - name: Run Antithesis Tests
         uses: antithesishq/antithesis-trigger-action@b7d0c9d1d9316bd4de73a44144c56636ea3a64ba # main commit on Mar 13, 2025
@@ -101,10 +96,11 @@ jobs:
           username: ${{ secrets.ANTITHESIS_WEBHOOK_USERNAME }}
           password: ${{ secrets.ANTITHESIS_WEBHOOK_PASSWORD }}
           github_token: ${{ secrets.GH_PAT }}
-          config_image: ${{ inputs.config_image }}
-          images: ${{ inputs.images }}
+          config_image: us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-config:${{ inputs.etcd_ref }}_${{ github.sha }}
+          images: us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-client:${{ inputs.etcd_ref }}_${{ github.sha }};us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-server:${{ inputs.etcd_ref }}_${{ github.sha }};docker.io/library/ubuntu:latest;us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-config:${{ inputs.etcd_ref }}_${{ github.sha }}
           description: ${{ inputs.description }}
           email_recipients: ${{ inputs.email }}
           test_name: ${{ inputs.test }}
           additional_parameters: |-
             custom.duration = ${{ inputs.duration }}
+            antithesis.source = ${{ inputs.etcd_ref }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 4dc86ea59..0d28ad53b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -40,7 +40,7 @@ jobs:
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+        uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
         with:
           # If you wish to specify custom queries, you can do so here or in a config file.
           # By default, queries listed here will override any specified in a config file.
@@ -50,6 +50,6 @@ jobs:
       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
       # If this step fails, then you should remove it and run the build manually (see below)
       - name: Autobuild
-        uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+        uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+        uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 03c04d8d7..86be41871 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -28,7 +28,7 @@ jobs:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
+        uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
         with:
           results_file: results.sarif
           results_format: sarif
@@ -50,6 +50,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+        uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
         with:
           sarif_file: results.sarif
diff --git a/.go-version b/.go-version
index ae96cc731..2f4320f67 100644
--- a/.go-version
+++ b/.go-version
@@ -1 +1 @@
-1.24.3
+1.24.4
diff --git a/CHANGELOG/CHANGELOG-3.4.md b/CHANGELOG/CHANGELOG-3.4.md
index aab809de8..0227647bd 100644
--- a/CHANGELOG/CHANGELOG-3.4.md
+++ b/CHANGELOG/CHANGELOG-3.4.md
@@ -6,9 +6,13 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 
 ## v3.4.38 (TBC)
 
+### etcd server
+
+- Fix [mvcc: avoid double decrement of watcher gauge on close/cancel race](https://github.com/etcd-io/etcd/pull/20065)
+
 ### Dependencies
 
-- Compile binaries using [go 1.23.9](https://github.com/etcd-io/etcd/pull/19872).
+- Compile binaries using [go 1.23.10](https://github.com/etcd-io/etcd/pull/20166).
 
 ---
 
diff --git a/CHANGELOG/CHANGELOG-3.5.md b/CHANGELOG/CHANGELOG-3.5.md
index 68d94b64d..c2cd4a6eb 100644
--- a/CHANGELOG/CHANGELOG-3.5.md
+++ b/CHANGELOG/CHANGELOG-3.5.md
@@ -9,6 +9,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 ### etcd server
 
 - Fix [the compaction pause duration metric is not emitted for every compaction batch](https://github.com/etcd-io/etcd/pull/19771)
+- Fix [mvcc: avoid double decrement of watcher gauge on close/cancel race](https://github.com/etcd-io/etcd/pull/20066)
 
 ### Package `clientv3`
 
@@ -17,7 +18,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 
 ### Dependencies
 
-- Compile binaries using [go 1.23.9](https://github.com/etcd-io/etcd/pull/19870)
+- Compile binaries using [go 1.23.10](https://github.com/etcd-io/etcd/pull/20164)
 
 ---
 
diff --git a/CHANGELOG/CHANGELOG-3.6.md b/CHANGELOG/CHANGELOG-3.6.md
index f22670a36..1c8bb7a42 100644
--- a/CHANGELOG/CHANGELOG-3.6.md
+++ b/CHANGELOG/CHANGELOG-3.6.md
@@ -4,6 +4,34 @@ Previous change logs can be found at [CHANGELOG-3.5](https://github.com/etcd-io/
 
 ---
 
+## v3.6.2 (TBD)
+
+### Dependencies
+
+- [Bump bbolt to v1.4.1](https://github.com/etcd-io/etcd/pull/20154)
+
+---
+
+## v3.6.1 (2025-06-06)
+
+### etcd server
+
+- [Replaced the deprecated/removed `UnaryServerInterceptor` and `StreamServerInterceptor` in otelgrpc with `NewServerHandler`](https://github.com/etcd-io/etcd/pull/20043)
+- [Add protection on `PromoteMember` and `UpdateRaftAttributes` to prevent panicking](https://github.com/etcd-io/etcd/pull/20051)
+- [Fix the issue that `--force-new-cluster` can't remove all other members in a corner case](https://github.com/etcd-io/etcd/pull/20071)
+- Fix [mvcc: avoid double decrement of watcher gauge on close/cancel race](https://github.com/etcd-io/etcd/pull/20067)
+- [Add validation to ensure there is no empty v3discovery endpoint](https://github.com/etcd-io/etcd/pull/20113)
+
+### etcdctl
+
+- Fix [command `etcdctl endpoint health` doesn't work when options are set via environment variables](https://github.com/etcd-io/etcd/pull/20121)
+
+### Dependencies
+
+- Compile binaries using [go 1.23.10](https://github.com/etcd-io/etcd/pull/20128).
+
+---
+
 ## v3.6.0 (2025-05-15)
 
 There isn't any production code change since v3.6.0-rc.5.
diff --git a/CHANGELOG/CHANGELOG-3.7.md b/CHANGELOG/CHANGELOG-3.7.md
index e0ff1570e..4662efac8 100644
--- a/CHANGELOG/CHANGELOG-3.7.md
+++ b/CHANGELOG/CHANGELOG-3.7.md
@@ -1,5 +1,4 @@
 
-
 Previous change logs can be found at [CHANGELOG-3.6](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.6.md).
 
 ---
@@ -9,6 +8,13 @@ Previous change logs can be found at [CHANGELOG-3.6](https://github.com/etcd-io/
 ### Breaking Changes
 
 - [Removed all deprecated experimental flags](https://github.com/etcd-io/etcd/pull/19959)
+- [Removed v2discovery](https://github.com/etcd-io/etcd/pull/20109)
+- [Removed client/v2](https://github.com/etcd-io/etcd/pull/20117)
+
+### etcd server
+
+- [Update go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc to v0.61.0 and replaced the deprecated `UnaryServerInterceptor` and `StreamServerInterceptor` with `NewServerHandler`](https://github.com/etcd-io/etcd/pull/20017)
+- [Add Support for Unix Socket endpoints](https://github.com/etcd-io/etcd/pull/19760)
 
 ### Package `pkg`
 
@@ -16,7 +22,7 @@ Previous change logs can be found at [CHANGELOG-3.6](https://github.com/etcd-io/
 
 ### Dependencies
 
-- Compile binaries using [go 1.24.3](https://github.com/etcd-io/etcd/pull/19908)
+- Compile binaries using [go 1.24.4](https://github.com/etcd-io/etcd/pull/20163)
 
 ### Deprecations
 
diff --git a/Dockerfile b/Dockerfile
index 66818aa59..f29109e4a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 ARG ARCH=amd64
-FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:d9f9472a8f4541368192d714a995eb1a99bab1f7071fc8bde261d7eda3b667d8
+FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:b7b9a6953e7bed6baaf37329331051d7bdc1b99c885f6dbeb72d75b1baad54f9
 
 ADD etcd /usr/local/bin/
 ADD etcdctl /usr/local/bin/
diff --git a/Documentation/contributor-guide/release.md b/Documentation/contributor-guide/release.md
index 081c0ac96..35382d634 100644
--- a/Documentation/contributor-guide/release.md
+++ b/Documentation/contributor-guide/release.md
@@ -17,7 +17,6 @@ for ensuring the stability of the release branch.
 - Marek Siarkowicz [@serathius](https://github.com/serathius)
 - Sahdev Zala [@spzala](https://github.com/spzala)
 - Siyuan Zhang [@siyuanfoundation](https://github.com/siyuanfoundation)
-- Wenjia Zhang [@wenjiaswe](https://github.com/wenjiaswe)
 
 All release version numbers follow the format of [semantic versioning 2.0.0](http://semver.org/).
 
@@ -99,6 +98,12 @@ On the day of the release:
 
    It generates all release binaries under the directory `/tmp/etcd-release-${VERSION}/etcd/release/` and images. Binaries are pushed to the Google Cloud bucket
    under project `etcd-development`, and images are pushed to `quay.io` and `gcr.io`.
+   - It is advisable to do a dry run before the actual release. This will create a `/tmp` directory. Do **NOT** forget to remove this directory before the actual release.
+
+      ```bash
+      DRY_RUN=true BRANCH=${BRANCH} ./scripts/release.sh ${VERSION}
+      ```
+
 4. Publish the release page on GitHub
    - Open the **draft** release URL shown by the release script
    - Click the pen button at the top right to edit the release
diff --git a/Documentation/contributor-guide/triage_prs.md b/Documentation/contributor-guide/triage_prs.md
index ffa0f7a1d..f32e45f4c 100644
--- a/Documentation/contributor-guide/triage_prs.md
+++ b/Documentation/contributor-guide/triage_prs.md
@@ -14,7 +14,7 @@ Following are a few example searches on PR for convenience:
 
 ## Scope
 
-These guidelines serve as a primary document for managing PRs in `etcd`. Everyone is welcome to help manage PRs but the work and responsibilities discussed in this document are created with `etcd` maintainers and active contributors in mind.
+These guidelines serve as a primary document for managing PRs and review policy in `etcd`. Everyone is welcome to help manage PRs but the work and responsibilities discussed in this document are created with `etcd` maintainers and active contributors in mind.
 
 ## Ensure tests are run
 
@@ -30,3 +30,32 @@ Reviewers are responsive in a timely fashion, but considering everyone is busy,
 ## Verify important labels are in place
 
 Make sure that appropriate reviewers are added to the PR. Also, make sure that a milestone is identified. If any of these or other important labels are missing, add them. If a correct label cannot be decided, leave a comment for the maintainers to do so as needed.
+
+## Review policy
+
+To ensure code quality and shared ownership, this review policy applies to all pull requests (PRs).
+
+### Default rule
+
+PRs should get at least two approvals (/lgtm or GitHub review approval) before merging.
+
+Notes:
+
+* Approvals should come from a maintainer, reviewer, or submodule owner familiar with the relevant code or area.
+* If there’s disagreement, maintainers should discuss and agree before merging.
+
+### Exceptions for Less Impactful PRs
+
+For low-risk changes — such as:
+
+* CI workflows
+* Documentation
+* Comments
+
+The rule can be relaxed:
+
+* One approval is generally enough.
+
+However:
+
+* If the author is a maintainer, they should still get approval from another maintainer, reviewer, or submodule owner, even for minor changes.
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
index 6573d2f01..5ad999768 100644
--- a/GOVERNANCE.md
+++ b/GOVERNANCE.md
@@ -36,7 +36,12 @@ weeks inactive voting period and as long as two maintainers are on board.
 
 Changes in project governance could be initiated by opening a GitHub PR.
 
+## SIG-etcd Governance
+
+[SIG-etcd Governance] is documented in the Kubernetes/community repository.
+
 [community membership]: /Documentation/contributor-guide/community-membership.md
 [Code of Conduct]: /code-of-conduct.md
 [contributor guide]: /CONTRIBUTING.md
 [maintainers]: /OWNERS
+[SIG-etcd Governance]: https://github.com/kubernetes/community/blob/master/sig-etcd/charter.md#deviations-from-sig-governance
diff --git a/LICENSE b/LICENSE
index d64569567..7a3ae1d40 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2013 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/OWNERS b/OWNERS
index 30037eb01..7783e529a 100644
--- a/OWNERS
+++ b/OWNERS
@@ -6,7 +6,19 @@ approvers:
   - jmhbnz           # James Blair <jablair@redhat.com> <mail@jamesblair.net>
   - serathius        # Marek Siarkowicz <siarkowicz@google.com> <marek.siarkowicz@gmail.com>
   - spzala           # Sahdev Zala <spzala@us.ibm.com>
-  - wenjiaswe        # Wenjia Zhang <wenjiazhang@google.com> <wenjia.swe@gmail.com>
 reviewers:
   - ivanvc           # Ivan Valdes <ivan@vald.es>
   - siyuanfoundation # Siyuan Zhang <sizhang@google.com> <physicsbug@gmail.com>
+emeritus_approvers:
+  - bdarnell         # Ben Darnell <ben@bendarnell.com>
+  - fanminshi        # Fanmin Shi <fanmin.shi@gmail.com>
+  - gyuho            # Gyuho Lee <gyuhox@gmail.com>
+  - hexfusion        # Sam Batschelet <sbatsche@redhat.com>
+  - heyitsanthony    # Anthony Romano <romanoanthony061@gmail.com>
+  - jingyih          # Jingyi Hu <jingyih@google.com>
+  - jpbetz           # Joe Betz <jpbetz@google.com>
+  - mitake           # Hitoshi Mitake <h.mitake@gmail.com>
+  - philips          # Brandon Philips <brandon@ifup.org>
+  - ptabor           # Piotr Tabor <piotr.tabor@gmail.com>
+  - wenjiaswe        # Wenjia Zhang <wenjiazhang@google.com> <wenjia.swe@gmail.com>
+  - xiang90          # Xiang Li <xiangli.cs@gmail.com>
diff --git a/README.md b/README.md
index 3f3bd97da..1b1623708 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 [![Tests](https://github.com/etcd-io/etcd/actions/workflows/tests.yaml/badge.svg)](https://github.com/etcd-io/etcd/actions/workflows/tests.yaml)
 [![codeql-analysis](https://github.com/etcd-io/etcd/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/etcd-io/etcd/actions/workflows/codeql-analysis.yml)
 [![Docs](https://img.shields.io/badge/docs-latest-green.svg)](https://etcd.io/docs)
-[![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/etcd-io/etcd)
+[![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godocs.io/go.etcd.io/etcd/v3)
 [![Releases](https://img.shields.io/github/release/etcd-io/etcd/all.svg?style=flat-square)](https://github.com/etcd-io/etcd/releases)
 [![LICENSE](https://img.shields.io/github/license/etcd-io/etcd.svg?style=flat-square)](https://github.com/etcd-io/etcd/blob/main/LICENSE)
 [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/etcd-io/etcd/badge)](https://scorecard.dev/viewer/?uri=github.com/etcd-io/etcd)
@@ -42,6 +42,18 @@ See [etcdctl][etcdctl] for a simple command line client.
 [vulcand]: https://github.com/vulcand/vulcand
 [etcdctl]: https://github.com/etcd-io/etcd/tree/main/etcdctl
 
+## Documentation
+
+The most common API documentation you'll need can be found here:
+
+* [go.etcd.io/etcd/api/v3](https://godocs.io/go.etcd.io/etcd/api/v3)
+* [go.etcd.io/etcd/client/pkg/v3](https://godocs.io/go.etcd.io/etcd/client/pkg/v3)
+* [go.etcd.io/etcd/client/v3](https://godocs.io/go.etcd.io/etcd/client/v3)
+* [go.etcd.io/etcd/etcdctl/v3](https://godocs.io/go.etcd.io/etcd/etcdctl/v3)
+* [go.etcd.io/etcd/pkg/v3](https://godocs.io/go.etcd.io/etcd/pkg/v3)
+* [go.etcd.io/etcd/raft/v3](https://godocs.io/go.etcd.io/etcd/raft/v3)
+* [go.etcd.io/etcd/server/v3](https://godocs.io/go.etcd.io/etcd/server/v3)
+
 ## Maintainers
 
 [Maintainers](OWNERS) strive to shape an inclusive open source project culture where users are heard and contributors feel respected and empowered. Maintainers aim to build productive relationships across different companies and disciplines. Read more about [Maintainers role and responsibilities](Documentation/contributor-guide/community-membership.md#maintainers).
@@ -186,19 +198,7 @@ See [PR management](https://github.com/etcd-io/etcd/blob/main/Documentation/cont
 
 ## etcd Emeritus Maintainers
 
-These emeritus maintainers dedicated a part of their career to etcd and reviewed code, triaged bugs and pushed the project forward over a substantial period of time. Their contribution is greatly appreciated.
-
-* Fanmin Shi
-* Anthony Romano
-* Brandon Philips
-* Joe Betz
-* Gyuho Lee
-* Jingyi Hu
-* Xiang Li
-* Ben Darnell
-* Sam Batschelet
-* Piotr Tabor
-* Hitoshi Mitake
+etcd [emeritus maintainers](OWNERS) dedicated a part of their career to etcd and reviewed code, triaged bugs and pushed the project forward over a substantial period of time. Their contribution is greatly appreciated.
 
 ### License
 
diff --git a/api/LICENSE b/api/LICENSE
index d64569567..52fb9c29f 100644
--- a/api/LICENSE
+++ b/api/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/api/etcdserverpb/gw/rpc.pb.gw.go b/api/etcdserverpb/gw/rpc.pb.gw.go
index 6ad1e9d9c..8eb910b6e 100644
--- a/api/etcdserverpb/gw/rpc.pb.gw.go
+++ b/api/etcdserverpb/gw/rpc.pb.gw.go
@@ -46,6 +46,9 @@ func request_KV_Range_0(ctx context.Context, marshaler runtime.Marshaler, client
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Range(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -70,6 +73,9 @@ func request_KV_Put_0(ctx context.Context, marshaler runtime.Marshaler, client e
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Put(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -94,6 +100,9 @@ func request_KV_DeleteRange_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.DeleteRange(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -118,6 +127,9 @@ func request_KV_Txn_0(ctx context.Context, marshaler runtime.Marshaler, client e
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Txn(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -142,6 +154,9 @@ func request_KV_Compact_0(ctx context.Context, marshaler runtime.Marshaler, clie
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Compact(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -209,6 +224,9 @@ func request_Lease_LeaseGrant_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseGrant(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -233,6 +251,9 @@ func request_Lease_LeaseRevoke_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseRevoke(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -257,6 +278,9 @@ func request_Lease_LeaseRevoke_1(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseRevoke(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -324,6 +348,9 @@ func request_Lease_LeaseTimeToLive_0(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseTimeToLive(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -348,6 +375,9 @@ func request_Lease_LeaseTimeToLive_1(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseTimeToLive(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -372,6 +402,9 @@ func request_Lease_LeaseLeases_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseLeases(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -396,6 +429,9 @@ func request_Lease_LeaseLeases_1(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseLeases(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -420,6 +456,9 @@ func request_Cluster_MemberAdd_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -444,6 +483,9 @@ func request_Cluster_MemberRemove_0(ctx context.Context, marshaler runtime.Marsh
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberRemove(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -468,6 +510,9 @@ func request_Cluster_MemberUpdate_0(ctx context.Context, marshaler runtime.Marsh
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberUpdate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -492,6 +537,9 @@ func request_Cluster_MemberList_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -516,6 +564,9 @@ func request_Cluster_MemberPromote_0(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberPromote(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -540,6 +591,9 @@ func request_Maintenance_Alarm_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Alarm(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -564,6 +618,9 @@ func request_Maintenance_Status_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Status(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -588,6 +645,9 @@ func request_Maintenance_Defragment_0(ctx context.Context, marshaler runtime.Mar
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Defragment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -612,6 +672,9 @@ func request_Maintenance_Hash_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Hash(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -636,6 +699,9 @@ func request_Maintenance_HashKV_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.HashKV(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -660,6 +726,9 @@ func request_Maintenance_Snapshot_0(ctx context.Context, marshaler runtime.Marsh
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	stream, err := client.Snapshot(ctx, &protoReq)
 	if err != nil {
 		return nil, metadata, err
@@ -680,6 +749,9 @@ func request_Maintenance_MoveLeader_0(ctx context.Context, marshaler runtime.Mar
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MoveLeader(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -704,6 +776,9 @@ func request_Maintenance_Downgrade_0(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Downgrade(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -728,6 +803,9 @@ func request_Auth_AuthEnable_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.AuthEnable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -752,6 +830,9 @@ func request_Auth_AuthDisable_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.AuthDisable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -776,6 +857,9 @@ func request_Auth_AuthStatus_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.AuthStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -800,6 +884,9 @@ func request_Auth_Authenticate_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Authenticate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -824,6 +911,9 @@ func request_Auth_UserAdd_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -848,6 +938,9 @@ func request_Auth_UserGet_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -872,6 +965,9 @@ func request_Auth_UserList_0(ctx context.Context, marshaler runtime.Marshaler, c
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -896,6 +992,9 @@ func request_Auth_UserDelete_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserDelete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -920,6 +1019,9 @@ func request_Auth_UserChangePassword_0(ctx context.Context, marshaler runtime.Ma
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserChangePassword(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -944,6 +1046,9 @@ func request_Auth_UserGrantRole_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserGrantRole(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -968,6 +1073,9 @@ func request_Auth_UserRevokeRole_0(ctx context.Context, marshaler runtime.Marsha
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserRevokeRole(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -992,6 +1100,9 @@ func request_Auth_RoleAdd_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1016,6 +1127,9 @@ func request_Auth_RoleGet_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1040,6 +1154,9 @@ func request_Auth_RoleList_0(ctx context.Context, marshaler runtime.Marshaler, c
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1064,6 +1181,9 @@ func request_Auth_RoleDelete_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleDelete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1088,6 +1208,9 @@ func request_Auth_RoleGrantPermission_0(ctx context.Context, marshaler runtime.M
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleGrantPermission(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1112,6 +1235,9 @@ func request_Auth_RoleRevokePermission_0(ctx context.Context, marshaler runtime.
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleRevokePermission(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
diff --git a/api/go.mod b/api/go.mod
index d3af6bf2d..30b94aa04 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -2,16 +2,16 @@ module go.etcd.io/etcd/api/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang/protobuf v1.5.4
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/stretchr/testify v1.10.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34
-	google.golang.org/grpc v1.72.1
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822
+	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 )
 
@@ -19,12 +19,12 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
-	go.opentelemetry.io/otel v1.35.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/api/go.sum b/api/go.sum
index c3ba24782..f92a0d38b 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -2,8 +2,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -14,8 +14,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -32,16 +32,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -51,8 +51,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -63,8 +63,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -73,12 +73,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/bill-of-materials.json b/bill-of-materials.json
index bd0efd460..0fbec32c2 100644
--- a/bill-of-materials.json
+++ b/bill-of-materials.json
@@ -45,7 +45,7 @@
 		]
 	},
 	{
-		"project": "github.com/cenkalti/backoff/v4",
+		"project": "github.com/cenkalti/backoff/v5",
 		"licenses": [
 			{
 				"type": "MIT License",
@@ -305,6 +305,24 @@
 			}
 		]
 	},
+	{
+		"project": "github.com/olekukonko/errors",
+		"licenses": [
+			{
+				"type": "MIT License",
+				"confidence": 1
+			}
+		]
+	},
+	{
+		"project": "github.com/olekukonko/ll",
+		"licenses": [
+			{
+				"type": "MIT License",
+				"confidence": 1
+			}
+		]
+	},
 	{
 		"project": "github.com/olekukonko/tablewriter",
 		"licenses": [
@@ -454,7 +472,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -463,16 +481,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
-			}
-		]
-	},
-	{
-		"project": "go.etcd.io/etcd/client/v2",
-		"licenses": [
-			{
-				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -481,7 +490,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -490,7 +499,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -499,7 +508,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -508,7 +517,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -517,7 +526,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -526,7 +535,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -535,7 +544,7 @@
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
-				"confidence": 1
+				"confidence": 0.9988925802879292
 			}
 		]
 	},
@@ -764,15 +773,6 @@
 			}
 		]
 	},
-	{
-		"project": "sigs.k8s.io/json",
-		"licenses": [
-			{
-				"type": "Apache License 2.0",
-				"confidence": 0.9617021276595744
-			}
-		]
-	},
 	{
 		"project": "sigs.k8s.io/yaml",
 		"licenses": [
diff --git a/client/internal/v2/LICENSE b/cache/LICENSE
similarity index 99%
rename from client/internal/v2/LICENSE
rename to cache/LICENSE
index d64569567..52fb9c29f 100644
--- a/client/internal/v2/LICENSE
+++ b/cache/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/cache/OWNERS b/cache/OWNERS
new file mode 100644
index 000000000..87a2e98b3
--- /dev/null
+++ b/cache/OWNERS
@@ -0,0 +1,4 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
+labels:
+  - area/cache
diff --git a/cache/README.md b/cache/README.md
new file mode 100644
index 000000000..2c6bf8e59
--- /dev/null
+++ b/cache/README.md
@@ -0,0 +1,3 @@
+# etcd cache
+
+Experimental etcd client cache library.
diff --git a/cache/cache.go b/cache/cache.go
new file mode 100644
index 000000000..012b504d4
--- /dev/null
+++ b/cache/cache.go
@@ -0,0 +1,251 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	rpctypes "go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+// TODO: add gap-free replay for arbitrary startRevs and drop this guard.
+var ErrUnsupportedWatch = errors.New("cache: unsupported watch parameters")
+
+// Cache buffers a single etcd Watch for a given key‐prefix and fan‑outs local watchers.
+type Cache struct {
+	prefix    string // prefix is the key-prefix this shard is responsible for ("" = root).
+	cfg       Config // immutable runtime configuration
+	watcher   clientv3.Watcher
+	demux     *demux // demux fans incoming events out to active watchers and manages resync.
+	ready     chan struct{}
+	stop      context.CancelFunc
+	waitGroup sync.WaitGroup
+}
+
+// watchCtx collects all the knobs that both serveWatchEvents and watchRetryLoop need.
+type watchCtx struct {
+	cache           *Cache
+	backoffStart    time.Duration
+	backoffMax      time.Duration
+	onFirstResponse func() // callback to fire once on first upstream response
+}
+
+// New builds a cache shard that watches only the requested prefix.
+// For the root cache pass "".
+func New(watcher clientv3.Watcher, prefix string, opts ...Option) (*Cache, error) {
+	cfg := defaultConfig()
+	for _, opt := range opts {
+		opt(&cfg)
+	}
+
+	if cfg.HistoryWindowSize <= 0 {
+		return nil, fmt.Errorf("invalid HistoryWindowSize %d (must be > 0)", cfg.HistoryWindowSize)
+	}
+
+	internalCtx, cancel := context.WithCancel(context.Background())
+
+	cache := &Cache{
+		prefix:  prefix,
+		cfg:     cfg,
+		watcher: watcher,
+		ready:   make(chan struct{}),
+		stop:    cancel,
+	}
+
+	cache.demux = newDemux(internalCtx, &cache.waitGroup, cfg.HistoryWindowSize, cfg.ResyncInterval)
+
+	cache.waitGroup.Add(1)
+	go func() {
+		defer cache.waitGroup.Done()
+		readyOnce := sync.Once{}
+
+		watchCtx := &watchCtx{
+			cache:           cache,
+			backoffStart:    cfg.InitialBackoff,
+			backoffMax:      cfg.MaxBackoff,
+			onFirstResponse: func() { readyOnce.Do(func() { close(cache.ready) }) },
+		}
+		serveWatchEvents(internalCtx, watchCtx)
+	}()
+
+	return cache, nil
+}
+
+// Watch registers a cache-backed watcher for a given key or prefix.
+// It returns a WatchChan that streams WatchResponses containing events.
+func (c *Cache) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
+	select {
+	case <-c.ready:
+	case <-ctx.Done():
+		emptyWatchChan := make(chan clientv3.WatchResponse)
+		close(emptyWatchChan)
+		return emptyWatchChan
+	}
+
+	op := clientv3.OpGet(key, opts...)
+	requestedRev := op.Rev()
+
+	// build the internal Watch event stream (for historic reply + live)
+	stream, w, err := c.newWatchEventStream(ctx, key, requestedRev, opts)
+	if err != nil {
+		// terminal error → one-shot reply, needs a single buffer slot
+		responseChan := make(chan clientv3.WatchResponse, 1)
+		if errors.Is(err, rpctypes.ErrCompacted) {
+			var compactRev int64
+			if oldestEvent := c.demux.PeekOldest(); oldestEvent != nil {
+				compactRev = oldestEvent.Kv.ModRevision
+			}
+			responseChan <- clientv3.WatchResponse{CompactRevision: compactRev}
+		} else {
+			responseChan <- clientv3.WatchResponse{Canceled: true}
+		}
+		close(responseChan)
+		return responseChan
+	}
+
+	ctx, cancel := context.WithCancel(ctx)
+	go func() { <-w.Done(); cancel() }()
+
+	responseChan := make(chan clientv3.WatchResponse)
+	go func() {
+		defer cancel()
+		defer close(responseChan)
+
+		for event := range stream {
+			select {
+			case <-ctx.Done():
+				return
+			case responseChan <- clientv3.WatchResponse{Events: []*clientv3.Event{event}}:
+			}
+		}
+	}()
+	return responseChan
+}
+
+// Ready reports whether the cache has finished its initial load.
+func (c *Cache) Ready() bool {
+	select {
+	case <-c.ready:
+		return true
+	default:
+		return false
+	}
+}
+
+// WaitReady blocks until the cache is ready or the ctx is cancelled.
+func (c *Cache) WaitReady(ctx context.Context) error {
+	select {
+	case <-c.ready:
+		return nil
+	case <-ctx.Done():
+		return ctx.Err()
+	}
+}
+
+// Close cancels the private context and blocks until all goroutines return.
+func (c *Cache) Close() {
+	c.stop()
+	c.waitGroup.Wait()
+}
+
+// newWatchEventStream builds a single internal Watch event stream for
+// both historical replay (>= startRev) and live updates filtered by key.
+func (c *Cache) newWatchEventStream(
+	ctx context.Context,
+	key string,
+	startRev int64,
+	opts []clientv3.OpOption,
+) (<-chan *clientv3.Event, *watcher, error) {
+	// TODO: Support watch on subprefix and single key & arbitrary startRev support once we guarantee gap-free replay.
+	if key != c.prefix || !clientv3.IsOptsWithPrefix(opts) || startRev != 0 {
+		return nil, nil, ErrUnsupportedWatch
+	}
+
+	pred := func(k []byte) bool { return strings.HasPrefix(string(k), key) }
+
+	w := newWatcher(c.cfg.PerWatcherBufferSize, pred)
+	c.demux.Register(w, startRev)
+
+	go func() {
+		<-ctx.Done()
+		c.demux.Unregister(w)
+	}()
+
+	return w.eventQueue, w, nil
+}
+
+func serveWatchEvents(ctx context.Context, watchCtx *watchCtx) {
+	backoff := watchCtx.backoffStart
+	for {
+		opts := []clientv3.OpOption{
+			clientv3.WithPrefix(),
+			clientv3.WithProgressNotify(),
+			clientv3.WithCreatedNotify(),
+		}
+		if oldestEvent := watchCtx.cache.demux.PeekOldest(); oldestEvent != nil {
+			opts = append(opts,
+				clientv3.WithRev(oldestEvent.Kv.ModRevision+1))
+		}
+		watchCh := watchCtx.cache.watcher.Watch(ctx, watchCtx.cache.prefix, opts...)
+
+		if err := readWatchChannel(watchCh, watchCtx.cache, watchCtx.cache.demux, watchCtx.onFirstResponse); err == nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-time.After(backoff):
+		}
+		if backoff < watchCtx.backoffMax {
+			backoff *= 2
+		}
+	}
+}
+
+// readWatchChannel reads an etcd Watch stream into History and enqueueCh, returning nil on cancel or first error.
+func readWatchChannel(
+	watchChan clientv3.WatchChan,
+	cache *Cache,
+	demux *demux,
+	onFirstResponse func(),
+) error {
+	for resp := range watchChan {
+		onFirstResponse()
+
+		if err := resp.Err(); err != nil {
+			if errors.Is(err, rpctypes.ErrCompacted) {
+				select {
+				case <-cache.ready:
+					// TODO: Reinitialize cache.ready safely; current direct channel assignment can race with concurrent watchers
+					cache.ready = make(chan struct{})
+				default:
+				}
+				demux.Purge()
+			}
+			return err
+		}
+		for _, event := range resp.Events {
+			demux.Broadcast(event)
+		}
+	}
+	return nil
+}
diff --git a/cache/config.go b/cache/config.go
new file mode 100644
index 000000000..ed0af99f4
--- /dev/null
+++ b/cache/config.go
@@ -0,0 +1,65 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import "time"
+
+type Config struct {
+	// PerWatcherBufferSize caps each watcher’s buffered channel.
+	// Bigger values tolerate brief client slow-downs at the cost of extra memory.
+	PerWatcherBufferSize int
+	// HistoryWindowSize is the max events kept in memory for replay.
+	// It defines how far back the cache can replay events to lagging watchers
+	HistoryWindowSize int
+	// ResyncInterval controls how often the demux attempts to catch a lagging watcher up by replaying events from History.
+	ResyncInterval time.Duration
+	// InitialBackoff is the first delay to wait before retrying an upstream etcd Watch after it ends with an error.
+	InitialBackoff time.Duration
+	// MaxBackoff caps the exponential back-off between successive upstream watch retries.
+	MaxBackoff time.Duration
+}
+
+// TODO: tune via performance/load tests.
+func defaultConfig() Config {
+	return Config{
+		PerWatcherBufferSize: 10,
+		HistoryWindowSize:    2048,
+		ResyncInterval:       50 * time.Millisecond,
+		InitialBackoff:       50 * time.Millisecond,
+		MaxBackoff:           2 * time.Second,
+	}
+}
+
+type Option func(*Config)
+
+func WithPerWatcherBufferSize(n int) Option {
+	return func(c *Config) { c.PerWatcherBufferSize = n }
+}
+
+func WithHistoryWindowSize(n int) Option {
+	return func(c *Config) { c.HistoryWindowSize = n }
+}
+
+func WithResyncInterval(d time.Duration) Option {
+	return func(c *Config) { c.ResyncInterval = d }
+}
+
+func WithInitialBackoff(d time.Duration) Option {
+	return func(c *Config) { c.InitialBackoff = d }
+}
+
+func WithMaxBackoff(d time.Duration) Option {
+	return func(c *Config) { c.MaxBackoff = d }
+}
diff --git a/cache/demux.go b/cache/demux.go
new file mode 100644
index 000000000..b1f6992bd
--- /dev/null
+++ b/cache/demux.go
@@ -0,0 +1,173 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+type demux struct {
+	mu sync.RWMutex
+	// activeWatchers & laggingWatchers hold the first revision the watcher still needs (nextRev).
+	activeWatchers  map[*watcher]int64
+	laggingWatchers map[*watcher]int64
+	history         *ringBuffer
+	resyncInterval  time.Duration
+}
+
+func newDemux(ctx context.Context, wg *sync.WaitGroup, historyWindowSize int, resyncInterval time.Duration) *demux {
+	d := &demux{
+		activeWatchers:  make(map[*watcher]int64),
+		laggingWatchers: make(map[*watcher]int64),
+		history:         newRingBuffer(historyWindowSize),
+		resyncInterval:  resyncInterval,
+	}
+
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		d.resyncLoop(ctx)
+	}()
+	return d
+}
+
+// resyncLoop periodically tries to catch lagging watchers up by replaying events from History.
+func (d *demux) resyncLoop(ctx context.Context) {
+	timer := time.NewTimer(d.resyncInterval)
+	defer timer.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-timer.C:
+			d.resyncLaggingWatchers()
+			timer.Reset(d.resyncInterval)
+		}
+	}
+}
+
+func (d *demux) Register(w *watcher, startingRev int64) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	latestEvent := d.history.PeekLatest()
+
+	// Special case: 0 means “newest”.
+	if startingRev == 0 {
+		if latestEvent == nil {
+			d.activeWatchers[w] = 0
+			return
+		}
+		startingRev = latestEvent.Kv.ModRevision + 1
+	}
+
+	var latestRev int64
+	if latestEvent != nil {
+		latestRev = latestEvent.Kv.ModRevision
+	}
+	if startingRev <= latestRev {
+		d.laggingWatchers[w] = startingRev
+	} else {
+		d.activeWatchers[w] = startingRev
+	}
+}
+
+func (d *demux) Unregister(w *watcher) {
+	func() {
+		d.mu.Lock()
+		defer d.mu.Unlock()
+		delete(d.activeWatchers, w)
+		delete(d.laggingWatchers, w)
+	}()
+	w.Stop()
+}
+
+func (d *demux) Broadcast(event *clientv3.Event) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	d.history.Append(event)
+	for w, nextRev := range d.activeWatchers {
+		if event.Kv.ModRevision < nextRev {
+			continue
+		}
+		if !w.enqueueEvent(event) { // buffer overflow
+			d.laggingWatchers[w] = nextRev
+			delete(d.activeWatchers, w)
+		} else {
+			d.activeWatchers[w] = event.Kv.ModRevision + 1
+		}
+	}
+}
+
+// Purge is called when etcd compaction invalidates our cached history, so clients should resubscribe.
+func (d *demux) Purge() {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.history.RebaseHistory()
+	for w := range d.activeWatchers {
+		w.Stop()
+	}
+	for w := range d.laggingWatchers {
+		w.Stop()
+	}
+	d.activeWatchers = make(map[*watcher]int64)
+	d.laggingWatchers = make(map[*watcher]int64)
+}
+
+func (d *demux) resyncLaggingWatchers() {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	for w, nextRev := range d.laggingWatchers {
+		var oldestRev int64
+		if oldestEvent := d.history.PeekOldest(); oldestEvent != nil {
+			oldestRev = oldestEvent.Kv.ModRevision
+		}
+
+		if oldestRev != 0 && nextRev < oldestRev {
+			w.Stop()
+			delete(d.laggingWatchers, w)
+			continue
+		}
+		// TODO: re-enable key‐predicate in Filter when non‐zero startRev or performance tuning is needed
+		missedEvents := d.history.Filter(AfterRev(nextRev))
+
+		for _, event := range missedEvents {
+			if !w.enqueueEvent(event) { // buffer overflow: watcher still lagging
+				break
+			}
+			nextRev = event.Kv.ModRevision + 1
+		}
+
+		if len(missedEvents) > 0 && nextRev > missedEvents[len(missedEvents)-1].Kv.ModRevision {
+			delete(d.laggingWatchers, w)
+			d.activeWatchers[w] = nextRev
+		} else {
+			d.laggingWatchers[w] = nextRev
+		}
+	}
+}
+
+func (d *demux) PeekOldest() *clientv3.Event {
+	d.mu.RLock()
+	defer d.mu.RUnlock()
+	return d.history.PeekOldest()
+}
diff --git a/cache/go.mod b/cache/go.mod
new file mode 100644
index 000000000..865b6c468
--- /dev/null
+++ b/cache/go.mod
@@ -0,0 +1,76 @@
+module go.etcd.io/etcd/cache
+
+go 1.24
+
+toolchain go1.24.4
+
+require (
+	github.com/google/go-cmp v0.7.0
+	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
+	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
+	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
+	go.etcd.io/etcd/server/v3 v3.6.0-alpha.0
+)
+
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/coreos/go-semver v0.3.1 // indirect
+	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/btree v1.1.3 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
+	github.com/jonboulle/clockwork v0.5.0 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/prometheus/client_golang v1.22.0 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/sirupsen/logrus v1.9.3 // indirect
+	github.com/soheilhy/cmux v0.1.5 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
+	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
+	go.etcd.io/bbolt v1.4.2 // indirect
+	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect
+	go.etcd.io/raft/v3 v3.6.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/crypto v0.39.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	golang.org/x/time v0.12.0 // indirect
+	google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
+	google.golang.org/grpc v1.73.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
+)
+
+replace (
+	go.etcd.io/etcd/api/v3 => ../api
+	go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
+	go.etcd.io/etcd/client/v3 => ../client/v3
+	go.etcd.io/etcd/pkg/v3 => ../pkg
+	go.etcd.io/etcd/server/v3 => ../server
+)
diff --git a/cache/go.sum b/cache/go.sum
new file mode 100644
index 000000000..2e13ddfa4
--- /dev/null
+++ b/cache/go.sum
@@ -0,0 +1,275 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
+github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
+github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
+github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
+github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
+go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
+go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
+go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/client/internal/v2/cluster_error.go b/cache/predicate.go
similarity index 53%
rename from client/internal/v2/cluster_error.go
rename to cache/predicate.go
index 34618cdbd..404dfbb01 100644
--- a/client/internal/v2/cluster_error.go
+++ b/cache/predicate.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The etcd Authors
+// Copyright 2025 The etcd Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,26 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package client
+package cache
 
-import "fmt"
+import clientv3 "go.etcd.io/etcd/client/v3"
 
-type ClusterError struct {
-	Errors []error
-}
+type Prefix string
 
-func (ce *ClusterError) Error() string {
-	s := ErrClusterUnavailable.Error()
-	for i, e := range ce.Errors {
-		s += fmt.Sprintf("; error #%d: %s\n", i, e)
+func (prefix Prefix) Match(key []byte) bool {
+	if prefix == "" {
+		return true
 	}
-	return s
+	prefixLen := len(prefix)
+	return len(key) >= prefixLen && string(key[:prefixLen]) == string(prefix)
 }
 
-func (ce *ClusterError) Detail() string {
-	s := ""
-	for i, e := range ce.Errors {
-		s += fmt.Sprintf("error #%d: %s\n", i, e)
+// AfterRev builds an EntryPredicate that matches events whose ModRevision ≥ rev.
+func AfterRev(rev int64) EntryPredicate {
+	return func(ev *clientv3.Event) bool {
+		return ev.Kv.ModRevision >= rev
 	}
-	return s
 }
diff --git a/cache/ringbuffer.go b/cache/ringbuffer.go
new file mode 100644
index 000000000..06027ed83
--- /dev/null
+++ b/cache/ringbuffer.go
@@ -0,0 +1,92 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"fmt"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+type ringBuffer struct {
+	buffer []*clientv3.Event
+	// head is the index immediately after the last non-empty entry in the buffer (i.e., the next write position).
+	head, tail, size int
+}
+
+// EntryPredicate lets callers decide which entries to keep (e.g. “after revision X”)
+type (
+	EntryPredicate func(*clientv3.Event) bool
+	KeyPredicate   = func([]byte) bool
+)
+
+func newRingBuffer(capacity int) *ringBuffer {
+	// assume capacity > 0 – validated by Cache
+	return &ringBuffer{
+		buffer: make([]*clientv3.Event, capacity),
+	}
+}
+
+func (r *ringBuffer) Append(event *clientv3.Event) {
+	if r.size == len(r.buffer) { // full → overwrite oldest
+		r.tail = (r.tail + 1) % len(r.buffer)
+	} else {
+		r.size++
+	}
+
+	r.buffer[r.head] = event
+	r.head = (r.head + 1) % len(r.buffer)
+}
+
+// Filter returns the events that satisfy every predicate
+// TODO: use binary search on the ring buffer to locate the first entry >= nextRev instead of a full scan
+func (r *ringBuffer) Filter(entryPred EntryPredicate) (events []*clientv3.Event) {
+	events = make([]*clientv3.Event, 0, r.size)
+
+	for n, i := 0, r.tail; n < r.size; n, i = n+1, (i+1)%len(r.buffer) {
+		entry := r.buffer[i]
+		if entry == nil {
+			panic(fmt.Sprintf("ringBuffer.Filter: unexpected nil entry at index %d", i))
+		}
+		if entryPred == nil || entryPred(entry) {
+			events = append(events, entry)
+		}
+	}
+	return events
+}
+
+// PeekLatest returns the most recently-appended event (or nil if empty).
+func (r *ringBuffer) PeekLatest() *clientv3.Event {
+	if r.size == 0 {
+		return nil
+	}
+	idx := (r.head - 1 + len(r.buffer)) % len(r.buffer)
+	return r.buffer[idx]
+}
+
+// PeekOldest returns the oldest event currently stored (or nil if empty).
+func (r *ringBuffer) PeekOldest() *clientv3.Event {
+	if r.size == 0 {
+		return nil
+	}
+	return r.buffer[r.tail]
+}
+
+func (r *ringBuffer) RebaseHistory() {
+	r.head, r.tail, r.size = 0, 0, 0
+	for i := range r.buffer {
+		r.buffer[i] = nil
+	}
+}
diff --git a/cache/watcher.go b/cache/watcher.go
new file mode 100644
index 000000000..219e6008c
--- /dev/null
+++ b/cache/watcher.go
@@ -0,0 +1,61 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"sync/atomic"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+// watcher holds one client’s buffered stream of events.
+type watcher struct {
+	eventQueue chan *clientv3.Event
+	keyPred    KeyPredicate
+	stopped    int32
+	done       chan struct{} // closed together with Stop()
+}
+
+func newWatcher(bufSize int, pred KeyPredicate) *watcher {
+	return &watcher{
+		eventQueue: make(chan *clientv3.Event, bufSize),
+		keyPred:    pred,
+		done:       make(chan struct{}),
+	}
+}
+
+// true  -> event delivered (or filtered/duplicate)
+// false -> buffer full (caller should mark watcher “lagging”)
+func (w *watcher) enqueueEvent(event *clientv3.Event) bool {
+	if w.keyPred != nil && !w.keyPred(event.Kv.Key) {
+		return true
+	}
+	select {
+	case w.eventQueue <- event:
+		return true
+	default:
+		return false
+	}
+}
+
+// Stop closes the event channel atomically.
+func (w *watcher) Stop() {
+	if atomic.CompareAndSwapInt32(&w.stopped, 0, 1) {
+		close(w.eventQueue)
+		close(w.done)
+	}
+}
+
+func (w *watcher) Done() <-chan struct{} { return w.done }
diff --git a/client/internal/v2/README.md b/client/internal/v2/README.md
deleted file mode 100644
index ff070c72d..000000000
--- a/client/internal/v2/README.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# etcd/client
-
-etcd/client is the Go client library for etcd.
-
-[![GoDoc](https://godoc.org/go.etcd.io/etcd/client?status.png)](https://godoc.org/go.etcd.io/etcd/client)
-
-For full compatibility, it is recommended to install released versions of clients using go modules.
-
-## Install
-
-```bash
-go get go.etcd.io/etcd/v3/client
-```
-
-## Usage
-
-```go
-package main
-
-import (
-    "context"
-    "log"
-    "time"
-    
-    "go.etcd.io/etcd/v3/client"
-)
-
-func main() {
-	cfg := client.Config{
-		Endpoints:               []string{"http://127.0.0.1:2379"},
-		Transport:               client.DefaultTransport,
-		// set timeout per request to fail fast when the target endpoint is unavailable
-		HeaderTimeoutPerRequest: time.Second,
-	}
-	c, err := client.New(cfg)
-	if err != nil {
-		log.Fatal(err)
-	}
-	kapi := client.NewKeysAPI(c)
-	// set "/foo" key with "bar" value
-	log.Print("Setting '/foo' key with 'bar' value")
-	resp, err := kapi.Set(context.Background(), "/foo", "bar", nil)
-	if err != nil {
-		log.Fatal(err)
-	} else {
-		// print common key info
-		log.Printf("Set is done. Metadata is %q\n", resp)
-	}
-	// get "/foo" key's value
-	log.Print("Getting '/foo' key value")
-	resp, err = kapi.Get(context.Background(), "/foo", nil)
-	if err != nil {
-		log.Fatal(err)
-	} else {
-		// print common key info
-		log.Printf("Get is done. Metadata is %q\n", resp)
-		// print value
-		log.Printf("%q key has %q value\n", resp.Node.Key, resp.Node.Value)
-	}
-}
-```
-
-## Error Handling
-
-etcd client might return three types of errors.
-
-- context error
-
-Each API call has its first parameter as `context`. A context can be canceled or have an attached deadline. If the context is canceled or reaches its deadline, the responding context error will be returned no matter what internal errors the API call has already encountered.
-
-- cluster error
-
-Each API call tries to send request to the cluster endpoints one by one until it successfully gets a response. If a requests to an endpoint fails, due to exceeding per request timeout or connection issues, the error will be added into a list of errors. If all possible endpoints fail, a cluster error that includes all encountered errors will be returned.
-
-- response error
-
-If the response gets from the cluster is invalid, a plain string error will be returned. For example, it might be a invalid JSON error.
-
-Here is the example code to handle client errors:
-
-```go
-cfg := client.Config{Endpoints: []string{"http://etcd1:2379","http://etcd2:2379","http://etcd3:2379"}}
-c, err := client.New(cfg)
-if err != nil {
-	log.Fatal(err)
-}
-
-kapi := client.NewKeysAPI(c)
-resp, err := kapi.Set(ctx, "test", "bar", nil)
-if err != nil {
-	var cerr *client.ClusterError
-	if errors.Is(err, context.Canceled) {
-		// ctx is canceled by another routine
-	} else if errors.Is(err, context.DeadlineExceeded) {
-		// ctx is attached with a deadline and it exceeded
-	} else if errors.As(err, &cerr) {
-		// process (cerr.Errors)
-	} else {
-		// bad cluster endpoints, which are not etcd servers
-	}
-}
-```
-
-
-## Caveat
-
-1. etcd/client prefers to use the same endpoint as long as the endpoint continues to work well. This saves socket resources, and improves efficiency for both client and server side. This preference doesn't remove consistency from the data consumed by the client because data replicated to each etcd member has already passed through the consensus process.
-
-2. etcd/client does round-robin rotation on other available endpoints if the preferred endpoint isn't functioning properly. For example, if the member that etcd/client connects to is hard killed, etcd/client will fail on the first attempt with the killed member, and succeed on the second attempt with another member. If it fails to talk to all available endpoints, it will return all errors happened.
-
-3. Default etcd/client cannot handle the case that the remote server is SIGSTOPed now. TCP keepalive mechanism doesn't help in this scenario because operating system may still send TCP keep-alive packets. Over time we'd like to improve this functionality, but solving this issue isn't high priority because a real-life case in which a server is stopped, but the connection is kept alive, hasn't been brought to our attention.
-
-4. etcd/client cannot detect whether a member is healthy with watches and non-quorum read requests. If the member is isolated from the cluster, etcd/client may retrieve outdated data. Instead, users can either issue quorum read requests or monitor the /health endpoint for member health information.
diff --git a/client/internal/v2/auth_role.go b/client/internal/v2/auth_role.go
deleted file mode 100644
index b9ef1aae8..000000000
--- a/client/internal/v2/auth_role.go
+++ /dev/null
@@ -1,236 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"net/http"
-	"net/url"
-)
-
-type Role struct {
-	Role        string       `json:"role"`
-	Permissions Permissions  `json:"permissions"`
-	Grant       *Permissions `json:"grant,omitempty"`
-	Revoke      *Permissions `json:"revoke,omitempty"`
-}
-
-type Permissions struct {
-	KV rwPermission `json:"kv"`
-}
-
-type rwPermission struct {
-	Read  []string `json:"read"`
-	Write []string `json:"write"`
-}
-
-type PermissionType int
-
-const (
-	ReadPermission PermissionType = iota
-	WritePermission
-	ReadWritePermission
-)
-
-// NewAuthRoleAPI constructs a new AuthRoleAPI that uses HTTP to
-// interact with etcd's role creation and modification features.
-func NewAuthRoleAPI(c Client) AuthRoleAPI {
-	return &httpAuthRoleAPI{
-		client: c,
-	}
-}
-
-type AuthRoleAPI interface {
-	// AddRole adds a role.
-	AddRole(ctx context.Context, role string) error
-
-	// RemoveRole removes a role.
-	RemoveRole(ctx context.Context, role string) error
-
-	// GetRole retrieves role details.
-	GetRole(ctx context.Context, role string) (*Role, error)
-
-	// GrantRoleKV grants a role some permission prefixes for the KV store.
-	GrantRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
-
-	// RevokeRoleKV revokes some permission prefixes for a role on the KV store.
-	RevokeRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
-
-	// ListRoles lists roles.
-	ListRoles(ctx context.Context) ([]string, error)
-}
-
-type httpAuthRoleAPI struct {
-	client httpClient
-}
-
-type authRoleAPIAction struct {
-	verb string
-	name string
-	role *Role
-}
-
-type authRoleAPIList struct{}
-
-func (list *authRoleAPIList) HTTPRequest(ep url.URL) *http.Request {
-	u := v2AuthURL(ep, "roles", "")
-	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
-	req.Header.Set("Content-Type", "application/json")
-	return req
-}
-
-func (l *authRoleAPIAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2AuthURL(ep, "roles", l.name)
-	if l.role == nil {
-		req, _ := http.NewRequest(l.verb, u.String(), nil)
-		return req
-	}
-	b, err := json.Marshal(l.role)
-	if err != nil {
-		panic(err)
-	}
-	body := bytes.NewReader(b)
-	req, _ := http.NewRequest(l.verb, u.String(), body)
-	req.Header.Set("Content-Type", "application/json")
-	return req
-}
-
-func (r *httpAuthRoleAPI) ListRoles(ctx context.Context) ([]string, error) {
-	resp, body, err := r.client.Do(ctx, &authRoleAPIList{})
-	if err != nil {
-		return nil, err
-	}
-	if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
-		return nil, err
-	}
-	var roleList struct {
-		Roles []Role `json:"roles"`
-	}
-	if err = json.Unmarshal(body, &roleList); err != nil {
-		return nil, err
-	}
-	ret := make([]string, 0, len(roleList.Roles))
-	for _, r := range roleList.Roles {
-		ret = append(ret, r.Role)
-	}
-	return ret, nil
-}
-
-func (r *httpAuthRoleAPI) AddRole(ctx context.Context, rolename string) error {
-	role := &Role{
-		Role: rolename,
-	}
-	return r.addRemoveRole(ctx, &authRoleAPIAction{
-		verb: http.MethodPut,
-		name: rolename,
-		role: role,
-	})
-}
-
-func (r *httpAuthRoleAPI) RemoveRole(ctx context.Context, rolename string) error {
-	return r.addRemoveRole(ctx, &authRoleAPIAction{
-		verb: http.MethodDelete,
-		name: rolename,
-	})
-}
-
-func (r *httpAuthRoleAPI) addRemoveRole(ctx context.Context, req *authRoleAPIAction) error {
-	resp, body, err := r.client.Do(ctx, req)
-	if err != nil {
-		return err
-	}
-	if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
-		var sec authError
-		err := json.Unmarshal(body, &sec)
-		if err != nil {
-			return err
-		}
-		return sec
-	}
-	return nil
-}
-
-func (r *httpAuthRoleAPI) GetRole(ctx context.Context, rolename string) (*Role, error) {
-	return r.modRole(ctx, &authRoleAPIAction{
-		verb: http.MethodGet,
-		name: rolename,
-	})
-}
-
-func buildRWPermission(prefixes []string, permType PermissionType) rwPermission {
-	var out rwPermission
-	switch permType {
-	case ReadPermission:
-		out.Read = prefixes
-	case WritePermission:
-		out.Write = prefixes
-	case ReadWritePermission:
-		out.Read = prefixes
-		out.Write = prefixes
-	}
-	return out
-}
-
-func (r *httpAuthRoleAPI) GrantRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
-	rwp := buildRWPermission(prefixes, permType)
-	role := &Role{
-		Role: rolename,
-		Grant: &Permissions{
-			KV: rwp,
-		},
-	}
-	return r.modRole(ctx, &authRoleAPIAction{
-		verb: http.MethodPut,
-		name: rolename,
-		role: role,
-	})
-}
-
-func (r *httpAuthRoleAPI) RevokeRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
-	rwp := buildRWPermission(prefixes, permType)
-	role := &Role{
-		Role: rolename,
-		Revoke: &Permissions{
-			KV: rwp,
-		},
-	}
-	return r.modRole(ctx, &authRoleAPIAction{
-		verb: http.MethodPut,
-		name: rolename,
-		role: role,
-	})
-}
-
-func (r *httpAuthRoleAPI) modRole(ctx context.Context, req *authRoleAPIAction) (*Role, error) {
-	resp, body, err := r.client.Do(ctx, req)
-	if err != nil {
-		return nil, err
-	}
-	if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
-		var sec authError
-		err = json.Unmarshal(body, &sec)
-		if err != nil {
-			return nil, err
-		}
-		return nil, sec
-	}
-	var role Role
-	if err = json.Unmarshal(body, &role); err != nil {
-		return nil, err
-	}
-	return &role, nil
-}
diff --git a/client/internal/v2/auth_user.go b/client/internal/v2/auth_user.go
deleted file mode 100644
index 75b636c02..000000000
--- a/client/internal/v2/auth_user.go
+++ /dev/null
@@ -1,317 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"net/http"
-	"net/url"
-	"path"
-)
-
-var defaultV2AuthPrefix = "/v2/auth"
-
-type User struct {
-	User     string   `json:"user"`
-	Password string   `json:"password,omitempty"`
-	Roles    []string `json:"roles"`
-	Grant    []string `json:"grant,omitempty"`
-	Revoke   []string `json:"revoke,omitempty"`
-}
-
-// userListEntry is the user representation given by the server for ListUsers
-type userListEntry struct {
-	User  string `json:"user"`
-	Roles []Role `json:"roles"`
-}
-
-type UserRoles struct {
-	User  string `json:"user"`
-	Roles []Role `json:"roles"`
-}
-
-func v2AuthURL(ep url.URL, action string, name string) *url.URL {
-	if name != "" {
-		ep.Path = path.Join(ep.Path, defaultV2AuthPrefix, action, name)
-		return &ep
-	}
-	ep.Path = path.Join(ep.Path, defaultV2AuthPrefix, action)
-	return &ep
-}
-
-// NewAuthAPI constructs a new AuthAPI that uses HTTP to
-// interact with etcd's general auth features.
-func NewAuthAPI(c Client) AuthAPI {
-	return &httpAuthAPI{
-		client: c,
-	}
-}
-
-type AuthAPI interface {
-	// Enable auth.
-	Enable(ctx context.Context) error
-
-	// Disable auth.
-	Disable(ctx context.Context) error
-}
-
-type httpAuthAPI struct {
-	client httpClient
-}
-
-func (s *httpAuthAPI) Enable(ctx context.Context) error {
-	return s.enableDisable(ctx, &authAPIAction{http.MethodPut})
-}
-
-func (s *httpAuthAPI) Disable(ctx context.Context) error {
-	return s.enableDisable(ctx, &authAPIAction{http.MethodDelete})
-}
-
-func (s *httpAuthAPI) enableDisable(ctx context.Context, req httpAction) error {
-	resp, body, err := s.client.Do(ctx, req)
-	if err != nil {
-		return err
-	}
-	if err = assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
-		var sec authError
-		err = json.Unmarshal(body, &sec)
-		if err != nil {
-			return err
-		}
-		return sec
-	}
-	return nil
-}
-
-type authAPIAction struct {
-	verb string
-}
-
-func (l *authAPIAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2AuthURL(ep, "enable", "")
-	req, _ := http.NewRequest(l.verb, u.String(), nil)
-	return req
-}
-
-type authError struct {
-	Message string `json:"message"`
-	Code    int    `json:"-"`
-}
-
-func (e authError) Error() string {
-	return e.Message
-}
-
-// NewAuthUserAPI constructs a new AuthUserAPI that uses HTTP to
-// interact with etcd's user creation and modification features.
-func NewAuthUserAPI(c Client) AuthUserAPI {
-	return &httpAuthUserAPI{
-		client: c,
-	}
-}
-
-type AuthUserAPI interface {
-	// AddUser adds a user.
-	AddUser(ctx context.Context, username string, password string) error
-
-	// RemoveUser removes a user.
-	RemoveUser(ctx context.Context, username string) error
-
-	// GetUser retrieves user details.
-	GetUser(ctx context.Context, username string) (*User, error)
-
-	// GrantUser grants a user some permission roles.
-	GrantUser(ctx context.Context, username string, roles []string) (*User, error)
-
-	// RevokeUser revokes some permission roles from a user.
-	RevokeUser(ctx context.Context, username string, roles []string) (*User, error)
-
-	// ChangePassword changes the user's password.
-	ChangePassword(ctx context.Context, username string, password string) (*User, error)
-
-	// ListUsers lists the users.
-	ListUsers(ctx context.Context) ([]string, error)
-}
-
-type httpAuthUserAPI struct {
-	client httpClient
-}
-
-type authUserAPIAction struct {
-	verb     string
-	username string
-	user     *User
-}
-
-type authUserAPIList struct{}
-
-func (list *authUserAPIList) HTTPRequest(ep url.URL) *http.Request {
-	u := v2AuthURL(ep, "users", "")
-	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
-	req.Header.Set("Content-Type", "application/json")
-	return req
-}
-
-func (l *authUserAPIAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2AuthURL(ep, "users", l.username)
-	if l.user == nil {
-		req, _ := http.NewRequest(l.verb, u.String(), nil)
-		return req
-	}
-	b, err := json.Marshal(l.user)
-	if err != nil {
-		panic(err)
-	}
-	body := bytes.NewReader(b)
-	req, _ := http.NewRequest(l.verb, u.String(), body)
-	req.Header.Set("Content-Type", "application/json")
-	return req
-}
-
-func (u *httpAuthUserAPI) ListUsers(ctx context.Context) ([]string, error) {
-	resp, body, err := u.client.Do(ctx, &authUserAPIList{})
-	if err != nil {
-		return nil, err
-	}
-	if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
-		var sec authError
-		err = json.Unmarshal(body, &sec)
-		if err != nil {
-			return nil, err
-		}
-		return nil, sec
-	}
-
-	var userList struct {
-		Users []userListEntry `json:"users"`
-	}
-
-	if err = json.Unmarshal(body, &userList); err != nil {
-		return nil, err
-	}
-
-	ret := make([]string, 0, len(userList.Users))
-	for _, u := range userList.Users {
-		ret = append(ret, u.User)
-	}
-	return ret, nil
-}
-
-func (u *httpAuthUserAPI) AddUser(ctx context.Context, username string, password string) error {
-	user := &User{
-		User:     username,
-		Password: password,
-	}
-	return u.addRemoveUser(ctx, &authUserAPIAction{
-		verb:     http.MethodPut,
-		username: username,
-		user:     user,
-	})
-}
-
-func (u *httpAuthUserAPI) RemoveUser(ctx context.Context, username string) error {
-	return u.addRemoveUser(ctx, &authUserAPIAction{
-		verb:     http.MethodDelete,
-		username: username,
-	})
-}
-
-func (u *httpAuthUserAPI) addRemoveUser(ctx context.Context, req *authUserAPIAction) error {
-	resp, body, err := u.client.Do(ctx, req)
-	if err != nil {
-		return err
-	}
-	if err = assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
-		var sec authError
-		err = json.Unmarshal(body, &sec)
-		if err != nil {
-			return err
-		}
-		return sec
-	}
-	return nil
-}
-
-func (u *httpAuthUserAPI) GetUser(ctx context.Context, username string) (*User, error) {
-	return u.modUser(ctx, &authUserAPIAction{
-		verb:     http.MethodGet,
-		username: username,
-	})
-}
-
-func (u *httpAuthUserAPI) GrantUser(ctx context.Context, username string, roles []string) (*User, error) {
-	user := &User{
-		User:  username,
-		Grant: roles,
-	}
-	return u.modUser(ctx, &authUserAPIAction{
-		verb:     http.MethodPut,
-		username: username,
-		user:     user,
-	})
-}
-
-func (u *httpAuthUserAPI) RevokeUser(ctx context.Context, username string, roles []string) (*User, error) {
-	user := &User{
-		User:   username,
-		Revoke: roles,
-	}
-	return u.modUser(ctx, &authUserAPIAction{
-		verb:     http.MethodPut,
-		username: username,
-		user:     user,
-	})
-}
-
-func (u *httpAuthUserAPI) ChangePassword(ctx context.Context, username string, password string) (*User, error) {
-	user := &User{
-		User:     username,
-		Password: password,
-	}
-	return u.modUser(ctx, &authUserAPIAction{
-		verb:     http.MethodPut,
-		username: username,
-		user:     user,
-	})
-}
-
-func (u *httpAuthUserAPI) modUser(ctx context.Context, req *authUserAPIAction) (*User, error) {
-	resp, body, err := u.client.Do(ctx, req)
-	if err != nil {
-		return nil, err
-	}
-	if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
-		var sec authError
-		err = json.Unmarshal(body, &sec)
-		if err != nil {
-			return nil, err
-		}
-		return nil, sec
-	}
-	var user User
-	if err = json.Unmarshal(body, &user); err != nil {
-		var userR UserRoles
-		if urerr := json.Unmarshal(body, &userR); urerr != nil {
-			return nil, err
-		}
-		user.User = userR.User
-		for _, r := range userR.Roles {
-			user.Roles = append(user.Roles, r.Role)
-		}
-	}
-	return &user, nil
-}
diff --git a/client/internal/v2/cancelreq.go b/client/internal/v2/cancelreq.go
deleted file mode 100644
index 23e5f89f6..000000000
--- a/client/internal/v2/cancelreq.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2015 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// borrowed from golang/net/context/ctxhttp/cancelreq.go
-
-package client
-
-import "net/http"
-
-func requestCanceler(req *http.Request) func() {
-	ch := make(chan struct{})
-	req.Cancel = ch
-
-	return func() {
-		close(ch)
-	}
-}
diff --git a/client/internal/v2/client.go b/client/internal/v2/client.go
deleted file mode 100644
index 598c2deb7..000000000
--- a/client/internal/v2/client.go
+++ /dev/null
@@ -1,719 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"math/rand"
-	"net"
-	"net/http"
-	"net/url"
-	"sort"
-	"strconv"
-	"sync"
-	"time"
-
-	"go.etcd.io/etcd/api/v3/version"
-)
-
-var (
-	ErrNoEndpoints           = errors.New("client: no endpoints available")
-	ErrTooManyRedirects      = errors.New("client: too many redirects")
-	ErrClusterUnavailable    = errors.New("client: etcd cluster is unavailable or misconfigured")
-	ErrNoLeaderEndpoint      = errors.New("client: no leader endpoint available")
-	errTooManyRedirectChecks = errors.New("client: too many redirect checks")
-
-	// oneShotCtxValue is set on a context using WithValue(&oneShotValue) so
-	// that Do() will not retry a request
-	oneShotCtxValue any
-)
-
-var DefaultRequestTimeout = 5 * time.Second
-
-var DefaultTransport CancelableTransport = &http.Transport{
-	Proxy: http.ProxyFromEnvironment,
-	DialContext: (&net.Dialer{
-		Timeout:   30 * time.Second,
-		KeepAlive: 30 * time.Second,
-	}).DialContext,
-	TLSHandshakeTimeout: 10 * time.Second,
-}
-
-type EndpointSelectionMode int
-
-const (
-	// EndpointSelectionRandom is the default value of the 'SelectionMode'.
-	// As the name implies, the client object will pick a node from the members
-	// of the cluster in a random fashion. If the cluster has three members, A, B,
-	// and C, the client picks any node from its three members as its request
-	// destination.
-	EndpointSelectionRandom EndpointSelectionMode = iota
-
-	// If 'SelectionMode' is set to 'EndpointSelectionPrioritizeLeader',
-	// requests are sent directly to the cluster leader. This reduces
-	// forwarding roundtrips compared to making requests to etcd followers
-	// who then forward them to the cluster leader. In the event of a leader
-	// failure, however, clients configured this way cannot prioritize among
-	// the remaining etcd followers. Therefore, when a client sets 'SelectionMode'
-	// to 'EndpointSelectionPrioritizeLeader', it must use 'client.AutoSync()' to
-	// maintain its knowledge of current cluster state.
-	//
-	// This mode should be used with Client.AutoSync().
-	EndpointSelectionPrioritizeLeader
-)
-
-type Config struct {
-	// Endpoints defines a set of URLs (schemes, hosts and ports only)
-	// that can be used to communicate with a logical etcd cluster. For
-	// example, a three-node cluster could be provided like so:
-	//
-	// 	Endpoints: []string{
-	//		"http://node1.example.com:2379",
-	//		"http://node2.example.com:2379",
-	//		"http://node3.example.com:2379",
-	//	}
-	//
-	// If multiple endpoints are provided, the Client will attempt to
-	// use them all in the event that one or more of them are unusable.
-	//
-	// If Client.Sync is ever called, the Client may cache an alternate
-	// set of endpoints to continue operation.
-	Endpoints []string
-
-	// Transport is used by the Client to drive HTTP requests. If not
-	// provided, DefaultTransport will be used.
-	Transport CancelableTransport
-
-	// CheckRedirect specifies the policy for handling HTTP redirects.
-	// If CheckRedirect is not nil, the Client calls it before
-	// following an HTTP redirect. The sole argument is the number of
-	// requests that have already been made. If CheckRedirect returns
-	// an error, Client.Do will not make any further requests and return
-	// the error back it to the caller.
-	//
-	// If CheckRedirect is nil, the Client uses its default policy,
-	// which is to stop after 10 consecutive requests.
-	CheckRedirect CheckRedirectFunc
-
-	// Username specifies the user credential to add as an authorization header
-	Username string
-
-	// Password is the password for the specified user to add as an authorization header
-	// to the request.
-	Password string
-
-	// HeaderTimeoutPerRequest specifies the time limit to wait for response
-	// header in a single request made by the Client. The timeout includes
-	// connection time, any redirects, and header wait time.
-	//
-	// For non-watch GET request, server returns the response body immediately.
-	// For PUT/POST/DELETE request, server will attempt to commit request
-	// before responding, which is expected to take `100ms + 2 * RTT`.
-	// For watch request, server returns the header immediately to notify Client
-	// watch start. But if server is behind some kind of proxy, the response
-	// header may be cached at proxy, and Client cannot rely on this behavior.
-	//
-	// Especially, wait request will ignore this timeout.
-	//
-	// One API call may send multiple requests to different etcd servers until it
-	// succeeds. Use context of the API to specify the overall timeout.
-	//
-	// A HeaderTimeoutPerRequest of zero means no timeout.
-	HeaderTimeoutPerRequest time.Duration
-
-	// SelectionMode is an EndpointSelectionMode enum that specifies the
-	// policy for choosing the etcd cluster node to which requests are sent.
-	SelectionMode EndpointSelectionMode
-}
-
-func (cfg *Config) transport() CancelableTransport {
-	if cfg.Transport == nil {
-		return DefaultTransport
-	}
-	return cfg.Transport
-}
-
-func (cfg *Config) checkRedirect() CheckRedirectFunc {
-	if cfg.CheckRedirect == nil {
-		return DefaultCheckRedirect
-	}
-	return cfg.CheckRedirect
-}
-
-// CancelableTransport mimics net/http.Transport, but requires that
-// the object also support request cancellation.
-type CancelableTransport interface {
-	http.RoundTripper
-	CancelRequest(req *http.Request)
-}
-
-type CheckRedirectFunc func(via int) error
-
-// DefaultCheckRedirect follows up to 10 redirects, but no more.
-var DefaultCheckRedirect CheckRedirectFunc = func(via int) error {
-	if via > 10 {
-		return ErrTooManyRedirects
-	}
-	return nil
-}
-
-type Client interface {
-	// Sync updates the internal cache of the etcd cluster's membership.
-	Sync(context.Context) error
-
-	// AutoSync periodically calls Sync() every given interval.
-	// The recommended sync interval is 10 seconds to 1 minute, which does
-	// not bring too much overhead to server and makes client catch up the
-	// cluster change in time.
-	//
-	// The example to use it:
-	//
-	//  for {
-	//      err := client.AutoSync(ctx, 10*time.Second)
-	//      if err == context.DeadlineExceeded || err == context.Canceled {
-	//          break
-	//      }
-	//      log.Print(err)
-	//  }
-	AutoSync(context.Context, time.Duration) error
-
-	// Endpoints returns a copy of the current set of API endpoints used
-	// by Client to resolve HTTP requests. If Sync has ever been called,
-	// this may differ from the initial Endpoints provided in the Config.
-	Endpoints() []string
-
-	// SetEndpoints sets the set of API endpoints used by Client to resolve
-	// HTTP requests. If the given endpoints are not valid, an error will be
-	// returned
-	SetEndpoints(eps []string) error
-
-	// GetVersion retrieves the current etcd server and cluster version
-	GetVersion(ctx context.Context) (*version.Versions, error)
-
-	httpClient
-}
-
-func New(cfg Config) (Client, error) {
-	c := &httpClusterClient{
-		clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect(), cfg.HeaderTimeoutPerRequest),
-		rand:          rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
-		selectionMode: cfg.SelectionMode,
-	}
-	if cfg.Username != "" {
-		c.credentials = &credentials{
-			username: cfg.Username,
-			password: cfg.Password,
-		}
-	}
-	if err := c.SetEndpoints(cfg.Endpoints); err != nil {
-		return nil, err
-	}
-	return c, nil
-}
-
-type httpClient interface {
-	Do(context.Context, httpAction) (*http.Response, []byte, error)
-}
-
-func newHTTPClientFactory(tr CancelableTransport, cr CheckRedirectFunc, headerTimeout time.Duration) httpClientFactory {
-	return func(ep url.URL) httpClient {
-		return &redirectFollowingHTTPClient{
-			checkRedirect: cr,
-			client: &simpleHTTPClient{
-				transport:     tr,
-				endpoint:      ep,
-				headerTimeout: headerTimeout,
-			},
-		}
-	}
-}
-
-type credentials struct {
-	username string
-	password string
-}
-
-type httpClientFactory func(url.URL) httpClient
-
-type httpAction interface {
-	HTTPRequest(url.URL) *http.Request
-}
-
-type httpClusterClient struct {
-	clientFactory httpClientFactory
-	endpoints     []url.URL
-	pinned        int
-	credentials   *credentials
-	sync.RWMutex
-	rand          *rand.Rand
-	selectionMode EndpointSelectionMode
-}
-
-func (c *httpClusterClient) getLeaderEndpoint(ctx context.Context, eps []url.URL) (string, error) {
-	ceps := make([]url.URL, len(eps))
-	copy(ceps, eps)
-
-	// To perform a lookup on the new endpoint list without using the current
-	// client, we'll copy it
-	clientCopy := &httpClusterClient{
-		clientFactory: c.clientFactory,
-		credentials:   c.credentials,
-		rand:          c.rand,
-
-		pinned:    0,
-		endpoints: ceps,
-	}
-
-	mAPI := NewMembersAPI(clientCopy)
-	leader, err := mAPI.Leader(ctx)
-	if err != nil {
-		return "", err
-	}
-	if len(leader.ClientURLs) == 0 {
-		return "", ErrNoLeaderEndpoint
-	}
-
-	return leader.ClientURLs[0], nil // TODO: how to handle multiple client URLs?
-}
-
-func (c *httpClusterClient) parseEndpoints(eps []string) ([]url.URL, error) {
-	if len(eps) == 0 {
-		return []url.URL{}, ErrNoEndpoints
-	}
-
-	neps := make([]url.URL, len(eps))
-	for i, ep := range eps {
-		u, err := url.Parse(ep)
-		if err != nil {
-			return []url.URL{}, err
-		}
-		neps[i] = *u
-	}
-	return neps, nil
-}
-
-func (c *httpClusterClient) SetEndpoints(eps []string) error {
-	neps, err := c.parseEndpoints(eps)
-	if err != nil {
-		return err
-	}
-
-	c.Lock()
-	defer c.Unlock()
-
-	c.endpoints = shuffleEndpoints(c.rand, neps)
-	// We're not doing anything for PrioritizeLeader here. This is
-	// due to not having a context meaning we can't call getLeaderEndpoint
-	// However, if you're using PrioritizeLeader, you've already been told
-	// to regularly call sync, where we do have a ctx, and can figure the
-	// leader. PrioritizeLeader is also quite a loose guarantee, so deal
-	// with it
-	c.pinned = 0
-
-	return nil
-}
-
-func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
-	action := act
-	c.RLock()
-	leps := len(c.endpoints)
-	eps := make([]url.URL, leps)
-	n := copy(eps, c.endpoints)
-	pinned := c.pinned
-
-	if c.credentials != nil {
-		action = &authedAction{
-			act:         act,
-			credentials: *c.credentials,
-		}
-	}
-	c.RUnlock()
-
-	if leps == 0 {
-		return nil, nil, ErrNoEndpoints
-	}
-
-	if leps != n {
-		return nil, nil, errors.New("unable to pick endpoint: copy failed")
-	}
-
-	var resp *http.Response
-	var body []byte
-	var err error
-	cerr := &ClusterError{}
-	isOneShot := ctx.Value(&oneShotCtxValue) != nil
-
-	for i := pinned; i < leps+pinned; i++ {
-		k := i % leps
-		hc := c.clientFactory(eps[k])
-		resp, body, err = hc.Do(ctx, action)
-		if err != nil {
-			cerr.Errors = append(cerr.Errors, err)
-			if errors.Is(err, ctx.Err()) {
-				return nil, nil, ctx.Err()
-			}
-			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
-				return nil, nil, err
-			}
-		} else if resp.StatusCode/100 == 5 {
-			switch resp.StatusCode {
-			case http.StatusInternalServerError, http.StatusServiceUnavailable:
-				// TODO: make sure this is a no leader response
-				cerr.Errors = append(cerr.Errors, fmt.Errorf("client: etcd member %s has no leader", eps[k].String()))
-			default:
-				cerr.Errors = append(cerr.Errors, fmt.Errorf("client: etcd member %s returns server error [%s]", eps[k].String(), http.StatusText(resp.StatusCode)))
-			}
-			err = cerr.Errors[0]
-		}
-		if err != nil {
-			if !isOneShot {
-				continue
-			}
-			c.Lock()
-			c.pinned = (k + 1) % leps
-			c.Unlock()
-			return nil, nil, err
-		}
-		if k != pinned {
-			c.Lock()
-			c.pinned = k
-			c.Unlock()
-		}
-		return resp, body, nil
-	}
-
-	return nil, nil, cerr
-}
-
-func (c *httpClusterClient) Endpoints() []string {
-	c.RLock()
-	defer c.RUnlock()
-
-	eps := make([]string, len(c.endpoints))
-	for i, ep := range c.endpoints {
-		eps[i] = ep.String()
-	}
-
-	return eps
-}
-
-func (c *httpClusterClient) Sync(ctx context.Context) error {
-	mAPI := NewMembersAPI(c)
-	ms, err := mAPI.List(ctx)
-	if err != nil {
-		return err
-	}
-
-	var eps []string
-	for _, m := range ms {
-		eps = append(eps, m.ClientURLs...)
-	}
-
-	neps, err := c.parseEndpoints(eps)
-	if err != nil {
-		return err
-	}
-
-	npin := 0
-
-	switch c.selectionMode {
-	case EndpointSelectionRandom:
-		c.RLock()
-		eq := endpointsEqual(c.endpoints, neps)
-		c.RUnlock()
-
-		if eq {
-			return nil
-		}
-		// When items in the endpoint list changes, we choose a new pin
-		neps = shuffleEndpoints(c.rand, neps)
-	case EndpointSelectionPrioritizeLeader:
-		nle, err := c.getLeaderEndpoint(ctx, neps)
-		if err != nil {
-			return ErrNoLeaderEndpoint
-		}
-
-		for i, n := range neps {
-			if n.String() == nle {
-				npin = i
-				break
-			}
-		}
-	default:
-		return fmt.Errorf("invalid endpoint selection mode: %d", c.selectionMode)
-	}
-
-	c.Lock()
-	defer c.Unlock()
-	c.endpoints = neps
-	c.pinned = npin
-
-	return nil
-}
-
-func (c *httpClusterClient) AutoSync(ctx context.Context, interval time.Duration) error {
-	ticker := time.NewTicker(interval)
-	defer ticker.Stop()
-	for {
-		err := c.Sync(ctx)
-		if err != nil {
-			return err
-		}
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-ticker.C:
-		}
-	}
-}
-
-func (c *httpClusterClient) GetVersion(ctx context.Context) (*version.Versions, error) {
-	act := &getAction{Prefix: "/version"}
-
-	resp, body, err := c.Do(ctx, act)
-	if err != nil {
-		return nil, err
-	}
-
-	switch resp.StatusCode {
-	case http.StatusOK:
-		if len(body) == 0 {
-			return nil, ErrEmptyBody
-		}
-		var vresp version.Versions
-		if err := json.Unmarshal(body, &vresp); err != nil {
-			return nil, ErrInvalidJSON
-		}
-		return &vresp, nil
-	default:
-		var etcdErr Error
-		if err := json.Unmarshal(body, &etcdErr); err != nil {
-			return nil, ErrInvalidJSON
-		}
-		return nil, etcdErr
-	}
-}
-
-type roundTripResponse struct {
-	resp *http.Response
-	err  error
-}
-
-type simpleHTTPClient struct {
-	transport     CancelableTransport
-	endpoint      url.URL
-	headerTimeout time.Duration
-}
-
-// ErrNoRequest indicates that the HTTPRequest object could not be found
-// or was nil.  No processing could continue.
-var ErrNoRequest = errors.New("no HTTPRequest was available")
-
-func (c *simpleHTTPClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
-	req := act.HTTPRequest(c.endpoint)
-	if req == nil {
-		return nil, nil, ErrNoRequest
-	}
-
-	if err := printcURL(req); err != nil {
-		return nil, nil, err
-	}
-
-	isWait := false
-	if req.URL != nil {
-		ws := req.URL.Query().Get("wait")
-		if len(ws) != 0 {
-			var err error
-			isWait, err = strconv.ParseBool(ws)
-			if err != nil {
-				return nil, nil, fmt.Errorf("wrong wait value %s (%w for %+v)", ws, err, req)
-			}
-		}
-	}
-
-	var hctx context.Context
-	var hcancel context.CancelFunc
-	if !isWait && c.headerTimeout > 0 {
-		hctx, hcancel = context.WithTimeout(ctx, c.headerTimeout)
-	} else {
-		hctx, hcancel = context.WithCancel(ctx)
-	}
-	defer hcancel()
-
-	reqcancel := requestCanceler(req)
-
-	rtchan := make(chan roundTripResponse, 1)
-	go func() {
-		resp, err := c.transport.RoundTrip(req)
-		rtchan <- roundTripResponse{resp: resp, err: err}
-		close(rtchan)
-	}()
-
-	var resp *http.Response
-	var err error
-
-	select {
-	case rtresp := <-rtchan:
-		resp, err = rtresp.resp, rtresp.err
-	case <-hctx.Done():
-		// cancel and wait for request to actually exit before continuing
-		reqcancel()
-		rtresp := <-rtchan
-		resp = rtresp.resp
-		switch {
-		case ctx.Err() != nil:
-			err = ctx.Err()
-		case hctx.Err() != nil:
-			err = fmt.Errorf("client: endpoint %s exceeded header timeout", c.endpoint.String())
-		default:
-			panic("failed to get error from context")
-		}
-	}
-
-	// always check for resp nil-ness to deal with possible
-	// race conditions between channels above
-	defer func() {
-		if resp != nil {
-			resp.Body.Close()
-		}
-	}()
-
-	if err != nil {
-		return nil, nil, err
-	}
-
-	var body []byte
-	done := make(chan struct{})
-	go func() {
-		body, err = io.ReadAll(resp.Body)
-		done <- struct{}{}
-	}()
-
-	select {
-	case <-ctx.Done():
-		if resp != nil {
-			resp.Body.Close()
-		}
-		<-done
-		return nil, nil, ctx.Err()
-	case <-done:
-	}
-
-	return resp, body, err
-}
-
-type authedAction struct {
-	act         httpAction
-	credentials credentials
-}
-
-func (a *authedAction) HTTPRequest(url url.URL) *http.Request {
-	r := a.act.HTTPRequest(url)
-	r.SetBasicAuth(a.credentials.username, a.credentials.password)
-	return r
-}
-
-type redirectFollowingHTTPClient struct {
-	client        httpClient
-	checkRedirect CheckRedirectFunc
-}
-
-func (r *redirectFollowingHTTPClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
-	next := act
-	for i := 0; i < 100; i++ {
-		if i > 0 {
-			if err := r.checkRedirect(i); err != nil {
-				return nil, nil, err
-			}
-		}
-		resp, body, err := r.client.Do(ctx, next)
-		if err != nil {
-			return nil, nil, err
-		}
-		if resp.StatusCode/100 == 3 {
-			hdr := resp.Header.Get("Location")
-			if hdr == "" {
-				return nil, nil, errors.New("location header not set")
-			}
-			loc, err := url.Parse(hdr)
-			if err != nil {
-				return nil, nil, fmt.Errorf("location header not valid URL: %s", hdr)
-			}
-			next = &redirectedHTTPAction{
-				action:   act,
-				location: *loc,
-			}
-			continue
-		}
-		return resp, body, nil
-	}
-
-	return nil, nil, errTooManyRedirectChecks
-}
-
-type redirectedHTTPAction struct {
-	action   httpAction
-	location url.URL
-}
-
-func (r *redirectedHTTPAction) HTTPRequest(ep url.URL) *http.Request {
-	orig := r.action.HTTPRequest(ep)
-	orig.URL = &r.location
-	return orig
-}
-
-func shuffleEndpoints(r *rand.Rand, eps []url.URL) []url.URL {
-	// copied from Go 1.9<= rand.Rand.Perm
-	n := len(eps)
-	p := make([]int, n)
-	for i := 0; i < n; i++ {
-		j := r.Intn(i + 1)
-		p[i] = p[j]
-		p[j] = i
-	}
-	neps := make([]url.URL, n)
-	for i, k := range p {
-		neps[i] = eps[k]
-	}
-	return neps
-}
-
-func endpointsEqual(left, right []url.URL) bool {
-	if len(left) != len(right) {
-		return false
-	}
-
-	sLeft := make([]string, len(left))
-	sRight := make([]string, len(right))
-	for i, l := range left {
-		sLeft[i] = l.String()
-	}
-	for i, r := range right {
-		sRight[i] = r.String()
-	}
-
-	sort.Strings(sLeft)
-	sort.Strings(sRight)
-	for i := range sLeft {
-		if sLeft[i] != sRight[i] {
-			return false
-		}
-	}
-	return true
-}
diff --git a/client/internal/v2/curl.go b/client/internal/v2/curl.go
deleted file mode 100644
index 5d5dc57ca..000000000
--- a/client/internal/v2/curl.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-)
-
-var cURLDebug = false
-
-func EnablecURLDebug() {
-	cURLDebug = true
-}
-
-func DisablecURLDebug() {
-	cURLDebug = false
-}
-
-// printcURL prints the cURL equivalent request to stderr.
-// It returns an error if the body of the request cannot
-// be read.
-// The caller MUST cancel the request if there is an error.
-func printcURL(req *http.Request) error {
-	if !cURLDebug {
-		return nil
-	}
-	var (
-		command string
-		b       []byte
-		err     error
-	)
-
-	if req.URL != nil {
-		command = fmt.Sprintf("curl -X %s %s", req.Method, req.URL.String())
-	}
-
-	if req.Body != nil {
-		b, err = io.ReadAll(req.Body)
-		if err != nil {
-			return err
-		}
-		command += fmt.Sprintf(" -d %q", string(b))
-	}
-
-	fmt.Fprintf(os.Stderr, "cURL Command: %q\n", command)
-
-	// reset body
-	body := bytes.NewBuffer(b)
-	req.Body = io.NopCloser(body)
-
-	return nil
-}
diff --git a/client/internal/v2/discover.go b/client/internal/v2/discover.go
deleted file mode 100644
index 646ba5dad..000000000
--- a/client/internal/v2/discover.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"go.etcd.io/etcd/client/pkg/v3/srv"
-)
-
-// Discoverer is an interface that wraps the Discover method.
-type Discoverer interface {
-	// Discover looks up the etcd servers for the domain.
-	Discover(domain string, serviceName string) ([]string, error)
-}
-
-type srvDiscover struct{}
-
-// NewSRVDiscover constructs a new Discoverer that uses the stdlib to lookup SRV records.
-func NewSRVDiscover() Discoverer {
-	return &srvDiscover{}
-}
-
-func (d *srvDiscover) Discover(domain string, serviceName string) ([]string, error) {
-	srvs, err := srv.GetClient("etcd-client", domain, serviceName)
-	if err != nil {
-		return nil, err
-	}
-	return srvs.Endpoints, nil
-}
diff --git a/client/internal/v2/doc.go b/client/internal/v2/doc.go
deleted file mode 100644
index 68284c20a..000000000
--- a/client/internal/v2/doc.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/*
-Package client provides bindings for the etcd APIs.
-
-Create a Config and exchange it for a Client:
-
-	import (
-		"net/http"
-		"context"
-
-		"go.etcd.io/etcd/client/v2"
-	)
-
-	cfg := client.Config{
-		Endpoints: []string{"http://127.0.0.1:2379"},
-		Transport: DefaultTransport,
-	}
-
-	c, err := client.New(cfg)
-	if err != nil {
-		// handle error
-	}
-
-Clients are safe for concurrent use by multiple goroutines.
-
-Create a KeysAPI using the Client, then use it to interact with etcd:
-
-	kAPI := client.NewKeysAPI(c)
-
-	// create a new key /foo with the value "bar"
-	_, err = kAPI.Create(context.Background(), "/foo", "bar")
-	if err != nil {
-		// handle error
-	}
-
-	// delete the newly created key only if the value is still "bar"
-	_, err = kAPI.Delete(context.Background(), "/foo", &DeleteOptions{PrevValue: "bar"})
-	if err != nil {
-		// handle error
-	}
-
-Use a custom context to set timeouts on your operations:
-
-	import "time"
-
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	// set a new key, ignoring its previous state
-	_, err := kAPI.Set(ctx, "/ping", "pong", nil)
-	if err != nil {
-		if err == context.DeadlineExceeded {
-			// request took longer than 5s
-		} else {
-			// handle error
-		}
-	}
-*/
-package client
diff --git a/client/internal/v2/go.mod b/client/internal/v2/go.mod
deleted file mode 100644
index c7013cf1f..000000000
--- a/client/internal/v2/go.mod
+++ /dev/null
@@ -1,34 +0,0 @@
-module go.etcd.io/etcd/client/v2
-
-go 1.24
-
-toolchain go1.24.3
-
-require (
-	github.com/stretchr/testify v1.10.0
-	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
-	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
-	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
-)
-
-require (
-	github.com/coreos/go-semver v0.3.1 // indirect
-	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/kr/text v0.2.0 // indirect
-	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
-)
-
-replace (
-	go.etcd.io/etcd/api/v3 => ./../../../api
-	go.etcd.io/etcd/client/pkg/v3 => ./../../pkg
-)
-
-// Bad imports are sometimes causing attempts to pull that code.
-// This makes the error more explicit.
-replace (
-	go.etcd.io/etcd => ./FORBIDDEN_DEPENDENCY
-	go.etcd.io/etcd/pkg/v3 => ./FORBIDDED_DEPENDENCY
-	go.etcd.io/etcd/tests/v3 => ./FORBIDDEN_DEPENDENCY
-	go.etcd.io/etcd/v3 => ./FORBIDDEN_DEPENDENCY
-)
diff --git a/client/internal/v2/go.sum b/client/internal/v2/go.sum
deleted file mode 100644
index 5e88bc955..000000000
--- a/client/internal/v2/go.sum
+++ /dev/null
@@ -1,22 +0,0 @@
-github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
-github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
-github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
diff --git a/client/internal/v2/keys.go b/client/internal/v2/keys.go
deleted file mode 100644
index 87783cd30..000000000
--- a/client/internal/v2/keys.go
+++ /dev/null
@@ -1,678 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-	"net/url"
-	"strconv"
-	"strings"
-	"time"
-
-	kjson "sigs.k8s.io/json"
-
-	"go.etcd.io/etcd/client/pkg/v3/pathutil"
-)
-
-const (
-	ErrorCodeKeyNotFound  = 100
-	ErrorCodeTestFailed   = 101
-	ErrorCodeNotFile      = 102
-	ErrorCodeNotDir       = 104
-	ErrorCodeNodeExist    = 105
-	ErrorCodeRootROnly    = 107
-	ErrorCodeDirNotEmpty  = 108
-	ErrorCodeUnauthorized = 110
-
-	ErrorCodePrevValueRequired = 201
-	ErrorCodeTTLNaN            = 202
-	ErrorCodeIndexNaN          = 203
-	ErrorCodeInvalidField      = 209
-	ErrorCodeInvalidForm       = 210
-
-	ErrorCodeRaftInternal = 300
-	ErrorCodeLeaderElect  = 301
-
-	ErrorCodeWatcherCleared    = 400
-	ErrorCodeEventIndexCleared = 401
-)
-
-type Error struct {
-	Code    int    `json:"errorCode"`
-	Message string `json:"message"`
-	Cause   string `json:"cause"`
-	Index   uint64 `json:"index"`
-}
-
-func (e Error) Error() string {
-	return fmt.Sprintf("%v: %v (%v) [%v]", e.Code, e.Message, e.Cause, e.Index)
-}
-
-var (
-	ErrInvalidJSON = errors.New("client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint")
-	ErrEmptyBody   = errors.New("client: response body is empty")
-)
-
-// PrevExistType is used to define an existence condition when setting
-// or deleting Nodes.
-type PrevExistType string
-
-const (
-	PrevIgnore  = PrevExistType("")
-	PrevExist   = PrevExistType("true")
-	PrevNoExist = PrevExistType("false")
-)
-
-var defaultV2KeysPrefix = "/v2/keys"
-
-// NewKeysAPI builds a KeysAPI that interacts with etcd's key-value
-// API over HTTP.
-func NewKeysAPI(c Client) KeysAPI {
-	return NewKeysAPIWithPrefix(c, defaultV2KeysPrefix)
-}
-
-// NewKeysAPIWithPrefix acts like NewKeysAPI, but allows the caller
-// to provide a custom base URL path. This should only be used in
-// very rare cases.
-func NewKeysAPIWithPrefix(c Client, p string) KeysAPI {
-	return &httpKeysAPI{
-		client: c,
-		prefix: p,
-	}
-}
-
-type KeysAPI interface {
-	// Get retrieves a set of Nodes from etcd
-	Get(ctx context.Context, key string, opts *GetOptions) (*Response, error)
-
-	// Set assigns a new value to a Node identified by a given key. The caller
-	// may define a set of conditions in the SetOptions. If SetOptions.Dir=true
-	// then value is ignored.
-	Set(ctx context.Context, key, value string, opts *SetOptions) (*Response, error)
-
-	// Delete removes a Node identified by the given key, optionally destroying
-	// all of its children as well. The caller may define a set of required
-	// conditions in an DeleteOptions object.
-	Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error)
-
-	// Create is an alias for Set w/ PrevExist=false
-	Create(ctx context.Context, key, value string) (*Response, error)
-
-	// CreateInOrder is used to atomically create in-order keys within the given directory.
-	CreateInOrder(ctx context.Context, dir, value string, opts *CreateInOrderOptions) (*Response, error)
-
-	// Update is an alias for Set w/ PrevExist=true
-	Update(ctx context.Context, key, value string) (*Response, error)
-
-	// Watcher builds a new Watcher targeted at a specific Node identified
-	// by the given key. The Watcher may be configured at creation time
-	// through a WatcherOptions object. The returned Watcher is designed
-	// to emit events that happen to a Node, and optionally to its children.
-	Watcher(key string, opts *WatcherOptions) Watcher
-}
-
-type WatcherOptions struct {
-	// AfterIndex defines the index after-which the Watcher should
-	// start emitting events. For example, if a value of 5 is
-	// provided, the first event will have an index >= 6.
-	//
-	// Setting AfterIndex to 0 (default) means that the Watcher
-	// should start watching for events starting at the current
-	// index, whatever that may be.
-	AfterIndex uint64
-
-	// Recursive specifies whether or not the Watcher should emit
-	// events that occur in children of the given keyspace. If set
-	// to false (default), events will be limited to those that
-	// occur for the exact key.
-	Recursive bool
-}
-
-type CreateInOrderOptions struct {
-	// TTL defines a period of time after-which the Node should
-	// expire and no longer exist. Values <= 0 are ignored. Given
-	// that the zero-value is ignored, TTL cannot be used to set
-	// a TTL of 0.
-	TTL time.Duration
-}
-
-type SetOptions struct {
-	// PrevValue specifies what the current value of the Node must
-	// be in order for the Set operation to succeed.
-	//
-	// Leaving this field empty means that the caller wishes to
-	// ignore the current value of the Node. This cannot be used
-	// to compare the Node's current value to an empty string.
-	//
-	// PrevValue is ignored if Dir=true
-	PrevValue string
-
-	// PrevIndex indicates what the current ModifiedIndex of the
-	// Node must be in order for the Set operation to succeed.
-	//
-	// If PrevIndex is set to 0 (default), no comparison is made.
-	PrevIndex uint64
-
-	// PrevExist specifies whether the Node must currently exist
-	// (PrevExist) or not (PrevNoExist). If the caller does not
-	// care about existence, set PrevExist to PrevIgnore, or simply
-	// leave it unset.
-	PrevExist PrevExistType
-
-	// TTL defines a period of time after-which the Node should
-	// expire and no longer exist. Values <= 0 are ignored. Given
-	// that the zero-value is ignored, TTL cannot be used to set
-	// a TTL of 0.
-	TTL time.Duration
-
-	// Refresh set to true means a TTL value can be updated
-	// without firing a watch or changing the node value. A
-	// value must not be provided when refreshing a key.
-	Refresh bool
-
-	// Dir specifies whether or not this Node should be created as a directory.
-	Dir bool
-
-	// NoValueOnSuccess specifies whether the response contains the current value of the Node.
-	// If set, the response will only contain the current value when the request fails.
-	NoValueOnSuccess bool
-}
-
-type GetOptions struct {
-	// Recursive defines whether or not all children of the Node
-	// should be returned.
-	Recursive bool
-
-	// Sort instructs the server whether or not to sort the Nodes.
-	// If true, the Nodes are sorted alphabetically by key in
-	// ascending order (A to z). If false (default), the Nodes will
-	// not be sorted and the ordering used should not be considered
-	// predictable.
-	Sort bool
-
-	// Quorum specifies whether it gets the latest committed value that
-	// has been applied in quorum of members, which ensures external
-	// consistency (or linearizability).
-	Quorum bool
-}
-
-type DeleteOptions struct {
-	// PrevValue specifies what the current value of the Node must
-	// be in order for the Delete operation to succeed.
-	//
-	// Leaving this field empty means that the caller wishes to
-	// ignore the current value of the Node. This cannot be used
-	// to compare the Node's current value to an empty string.
-	PrevValue string
-
-	// PrevIndex indicates what the current ModifiedIndex of the
-	// Node must be in order for the Delete operation to succeed.
-	//
-	// If PrevIndex is set to 0 (default), no comparison is made.
-	PrevIndex uint64
-
-	// Recursive defines whether or not all children of the Node
-	// should be deleted. If set to true, all children of the Node
-	// identified by the given key will be deleted. If left unset
-	// or explicitly set to false, only a single Node will be
-	// deleted.
-	Recursive bool
-
-	// Dir specifies whether or not this Node should be removed as a directory.
-	Dir bool
-}
-
-type Watcher interface {
-	// Next blocks until an etcd event occurs, then returns a Response
-	// representing that event. The behavior of Next depends on the
-	// WatcherOptions used to construct the Watcher. Next is designed to
-	// be called repeatedly, each time blocking until a subsequent event
-	// is available.
-	//
-	// If the provided context is cancelled, Next will return a non-nil
-	// error. Any other failures encountered while waiting for the next
-	// event (connection issues, deserialization failures, etc) will
-	// also result in a non-nil error.
-	Next(context.Context) (*Response, error)
-}
-
-type Response struct {
-	// Action is the name of the operation that occurred. Possible values
-	// include get, set, delete, update, create, compareAndSwap,
-	// compareAndDelete and expire.
-	Action string `json:"action"`
-
-	// Node represents the state of the relevant etcd Node.
-	Node *Node `json:"node"`
-
-	// PrevNode represents the previous state of the Node. PrevNode is non-nil
-	// only if the Node existed before the action occurred and the action
-	// caused a change to the Node.
-	PrevNode *Node `json:"prevNode"`
-
-	// Index holds the cluster-level index at the time the Response was generated.
-	// This index is not tied to the Node(s) contained in this Response.
-	Index uint64 `json:"-"`
-
-	// ClusterID holds the cluster-level ID reported by the server.  This
-	// should be different for different etcd clusters.
-	ClusterID string `json:"-"`
-}
-
-type Node struct {
-	// Key represents the unique location of this Node (e.g. "/foo/bar").
-	Key string `json:"key"`
-
-	// Dir reports whether node describes a directory.
-	Dir bool `json:"dir,omitempty"`
-
-	// Value is the current data stored on this Node. If this Node
-	// is a directory, Value will be empty.
-	Value string `json:"value"`
-
-	// Nodes holds the children of this Node, only if this Node is a directory.
-	// This slice of will be arbitrarily deep (children, grandchildren, great-
-	// grandchildren, etc.) if a recursive Get or Watch request were made.
-	Nodes Nodes `json:"nodes"`
-
-	// CreatedIndex is the etcd index at-which this Node was created.
-	CreatedIndex uint64 `json:"createdIndex"`
-
-	// ModifiedIndex is the etcd index at-which this Node was last modified.
-	ModifiedIndex uint64 `json:"modifiedIndex"`
-
-	// Expiration is the server side expiration time of the key.
-	Expiration *time.Time `json:"expiration,omitempty"`
-
-	// TTL is the time to live of the key in second.
-	TTL int64 `json:"ttl,omitempty"`
-}
-
-func (n *Node) String() string {
-	return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d, TTL: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex, n.TTL)
-}
-
-// TTLDuration returns the Node's TTL as a time.Duration object
-func (n *Node) TTLDuration() time.Duration {
-	return time.Duration(n.TTL) * time.Second
-}
-
-type Nodes []*Node
-
-// interfaces for sorting
-
-func (ns Nodes) Len() int           { return len(ns) }
-func (ns Nodes) Less(i, j int) bool { return ns[i].Key < ns[j].Key }
-func (ns Nodes) Swap(i, j int)      { ns[i], ns[j] = ns[j], ns[i] }
-
-type httpKeysAPI struct {
-	client httpClient
-	prefix string
-}
-
-func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions) (*Response, error) {
-	act := &setAction{
-		Prefix: k.prefix,
-		Key:    key,
-		Value:  val,
-	}
-
-	if opts != nil {
-		act.PrevValue = opts.PrevValue
-		act.PrevIndex = opts.PrevIndex
-		act.PrevExist = opts.PrevExist
-		act.TTL = opts.TTL
-		act.Refresh = opts.Refresh
-		act.Dir = opts.Dir
-		act.NoValueOnSuccess = opts.NoValueOnSuccess
-	}
-
-	doCtx := ctx
-	if act.PrevExist == PrevNoExist {
-		doCtx = context.WithValue(doCtx, &oneShotCtxValue, &oneShotCtxValue)
-	}
-	resp, body, err := k.client.Do(doCtx, act)
-	if err != nil {
-		return nil, err
-	}
-
-	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
-}
-
-func (k *httpKeysAPI) Create(ctx context.Context, key, val string) (*Response, error) {
-	return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevNoExist})
-}
-
-func (k *httpKeysAPI) CreateInOrder(ctx context.Context, dir, val string, opts *CreateInOrderOptions) (*Response, error) {
-	act := &createInOrderAction{
-		Prefix: k.prefix,
-		Dir:    dir,
-		Value:  val,
-	}
-
-	if opts != nil {
-		act.TTL = opts.TTL
-	}
-
-	resp, body, err := k.client.Do(ctx, act)
-	if err != nil {
-		return nil, err
-	}
-
-	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
-}
-
-func (k *httpKeysAPI) Update(ctx context.Context, key, val string) (*Response, error) {
-	return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevExist})
-}
-
-func (k *httpKeysAPI) Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error) {
-	act := &deleteAction{
-		Prefix: k.prefix,
-		Key:    key,
-	}
-
-	if opts != nil {
-		act.PrevValue = opts.PrevValue
-		act.PrevIndex = opts.PrevIndex
-		act.Dir = opts.Dir
-		act.Recursive = opts.Recursive
-	}
-
-	doCtx := context.WithValue(ctx, &oneShotCtxValue, &oneShotCtxValue)
-	resp, body, err := k.client.Do(doCtx, act)
-	if err != nil {
-		return nil, err
-	}
-
-	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
-}
-
-func (k *httpKeysAPI) Get(ctx context.Context, key string, opts *GetOptions) (*Response, error) {
-	act := &getAction{
-		Prefix: k.prefix,
-		Key:    key,
-	}
-
-	if opts != nil {
-		act.Recursive = opts.Recursive
-		act.Sorted = opts.Sort
-		act.Quorum = opts.Quorum
-	}
-
-	resp, body, err := k.client.Do(ctx, act)
-	if err != nil {
-		return nil, err
-	}
-
-	return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
-}
-
-func (k *httpKeysAPI) Watcher(key string, opts *WatcherOptions) Watcher {
-	act := waitAction{
-		Prefix: k.prefix,
-		Key:    key,
-	}
-
-	if opts != nil {
-		act.Recursive = opts.Recursive
-		if opts.AfterIndex > 0 {
-			act.WaitIndex = opts.AfterIndex + 1
-		}
-	}
-
-	return &httpWatcher{
-		client:   k.client,
-		nextWait: act,
-	}
-}
-
-type httpWatcher struct {
-	client   httpClient
-	nextWait waitAction
-}
-
-func (hw *httpWatcher) Next(ctx context.Context) (*Response, error) {
-	for {
-		httpresp, body, err := hw.client.Do(ctx, &hw.nextWait)
-		if err != nil {
-			return nil, err
-		}
-
-		resp, err := unmarshalHTTPResponse(httpresp.StatusCode, httpresp.Header, body)
-		if err != nil {
-			if errors.Is(err, ErrEmptyBody) {
-				continue
-			}
-			return nil, err
-		}
-
-		hw.nextWait.WaitIndex = resp.Node.ModifiedIndex + 1
-		return resp, nil
-	}
-}
-
-// v2KeysURL forms a URL representing the location of a key.
-// The endpoint argument represents the base URL of an etcd
-// server. The prefix is the path needed to route from the
-// provided endpoint's path to the root of the keys API
-// (typically "/v2/keys").
-func v2KeysURL(ep url.URL, prefix, key string) *url.URL {
-	// We concatenate all parts together manually. We cannot use
-	// path.Join because it does not reserve trailing slash.
-	// We call CanonicalURLPath to further cleanup the path.
-	if prefix != "" && prefix[0] != '/' {
-		prefix = "/" + prefix
-	}
-	if key != "" && key[0] != '/' {
-		key = "/" + key
-	}
-	ep.Path = pathutil.CanonicalURLPath(ep.Path + prefix + key)
-	return &ep
-}
-
-type getAction struct {
-	Prefix    string
-	Key       string
-	Recursive bool
-	Sorted    bool
-	Quorum    bool
-}
-
-func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2KeysURL(ep, g.Prefix, g.Key)
-
-	params := u.Query()
-	params.Set("recursive", strconv.FormatBool(g.Recursive))
-	params.Set("sorted", strconv.FormatBool(g.Sorted))
-	params.Set("quorum", strconv.FormatBool(g.Quorum))
-	u.RawQuery = params.Encode()
-
-	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
-	return req
-}
-
-type waitAction struct {
-	Prefix    string
-	Key       string
-	WaitIndex uint64
-	Recursive bool
-}
-
-func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2KeysURL(ep, w.Prefix, w.Key)
-
-	params := u.Query()
-	params.Set("wait", "true")
-	params.Set("waitIndex", strconv.FormatUint(w.WaitIndex, 10))
-	params.Set("recursive", strconv.FormatBool(w.Recursive))
-	u.RawQuery = params.Encode()
-
-	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
-	return req
-}
-
-type setAction struct {
-	Prefix           string
-	Key              string
-	Value            string
-	PrevValue        string
-	PrevIndex        uint64
-	PrevExist        PrevExistType
-	TTL              time.Duration
-	Refresh          bool
-	Dir              bool
-	NoValueOnSuccess bool
-}
-
-func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2KeysURL(ep, a.Prefix, a.Key)
-
-	params := u.Query()
-	form := url.Values{}
-
-	// we're either creating a directory or setting a key
-	if a.Dir {
-		params.Set("dir", strconv.FormatBool(a.Dir))
-	} else {
-		// These options are only valid for setting a key
-		if a.PrevValue != "" {
-			params.Set("prevValue", a.PrevValue)
-		}
-		form.Add("value", a.Value)
-	}
-
-	// Options which apply to both setting a key and creating a dir
-	if a.PrevIndex != 0 {
-		params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
-	}
-	if a.PrevExist != PrevIgnore {
-		params.Set("prevExist", string(a.PrevExist))
-	}
-	if a.TTL > 0 {
-		form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
-	}
-
-	if a.Refresh {
-		form.Add("refresh", "true")
-	}
-	if a.NoValueOnSuccess {
-		params.Set("noValueOnSuccess", strconv.FormatBool(a.NoValueOnSuccess))
-	}
-
-	u.RawQuery = params.Encode()
-	body := strings.NewReader(form.Encode())
-
-	req, _ := http.NewRequest(http.MethodPut, u.String(), body)
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
-	return req
-}
-
-type deleteAction struct {
-	Prefix    string
-	Key       string
-	PrevValue string
-	PrevIndex uint64
-	Dir       bool
-	Recursive bool
-}
-
-func (a *deleteAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2KeysURL(ep, a.Prefix, a.Key)
-
-	params := u.Query()
-	if a.PrevValue != "" {
-		params.Set("prevValue", a.PrevValue)
-	}
-	if a.PrevIndex != 0 {
-		params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
-	}
-	if a.Dir {
-		params.Set("dir", "true")
-	}
-	if a.Recursive {
-		params.Set("recursive", "true")
-	}
-	u.RawQuery = params.Encode()
-
-	req, _ := http.NewRequest(http.MethodDelete, u.String(), nil)
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
-	return req
-}
-
-type createInOrderAction struct {
-	Prefix string
-	Dir    string
-	Value  string
-	TTL    time.Duration
-}
-
-func (a *createInOrderAction) HTTPRequest(ep url.URL) *http.Request {
-	u := v2KeysURL(ep, a.Prefix, a.Dir)
-
-	form := url.Values{}
-	form.Add("value", a.Value)
-	if a.TTL > 0 {
-		form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
-	}
-	body := strings.NewReader(form.Encode())
-
-	req, _ := http.NewRequest(http.MethodPost, u.String(), body)
-	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-	return req
-}
-
-func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
-	switch code {
-	case http.StatusOK, http.StatusCreated:
-		if len(body) == 0 {
-			return nil, ErrEmptyBody
-		}
-		res, err = unmarshalSuccessfulKeysResponse(header, body)
-	default:
-		err = unmarshalFailedKeysResponse(body)
-	}
-	return res, err
-}
-
-func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response, error) {
-	var res Response
-	err := kjson.UnmarshalCaseSensitivePreserveInts(body, &res)
-	if err != nil {
-		return nil, ErrInvalidJSON
-	}
-	if header.Get("X-Etcd-Index") != "" {
-		res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
-		if err != nil {
-			return nil, err
-		}
-	}
-	res.ClusterID = header.Get("X-Etcd-Cluster-ID")
-	return &res, nil
-}
-
-func unmarshalFailedKeysResponse(body []byte) error {
-	var etcdErr Error
-	if err := json.Unmarshal(body, &etcdErr); err != nil {
-		return ErrInvalidJSON
-	}
-	return etcdErr
-}
diff --git a/client/internal/v2/members.go b/client/internal/v2/members.go
deleted file mode 100644
index f53c2dbb2..000000000
--- a/client/internal/v2/members.go
+++ /dev/null
@@ -1,303 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"net/url"
-	"path"
-
-	"go.etcd.io/etcd/client/pkg/v3/types"
-)
-
-var (
-	defaultV2MembersPrefix = "/v2/members"
-	defaultLeaderSuffix    = "/leader"
-)
-
-type Member struct {
-	// ID is the unique identifier of this Member.
-	ID string `json:"id"`
-
-	// Name is a human-readable, non-unique identifier of this Member.
-	Name string `json:"name"`
-
-	// PeerURLs represents the HTTP(S) endpoints this Member uses to
-	// participate in etcd's consensus protocol.
-	PeerURLs []string `json:"peerURLs"`
-
-	// ClientURLs represents the HTTP(S) endpoints on which this Member
-	// serves its client-facing APIs.
-	ClientURLs []string `json:"clientURLs"`
-}
-
-type memberCollection []Member
-
-func (c *memberCollection) UnmarshalJSON(data []byte) error {
-	d := struct {
-		Members []Member
-	}{}
-
-	if err := json.Unmarshal(data, &d); err != nil {
-		return err
-	}
-
-	if d.Members == nil {
-		*c = make([]Member, 0)
-		return nil
-	}
-
-	*c = d.Members
-	return nil
-}
-
-type memberCreateOrUpdateRequest struct {
-	PeerURLs types.URLs
-}
-
-func (m *memberCreateOrUpdateRequest) MarshalJSON() ([]byte, error) {
-	s := struct {
-		PeerURLs []string `json:"peerURLs"`
-	}{
-		PeerURLs: make([]string, len(m.PeerURLs)),
-	}
-
-	for i, u := range m.PeerURLs {
-		s.PeerURLs[i] = u.String()
-	}
-
-	return json.Marshal(&s)
-}
-
-// NewMembersAPI constructs a new MembersAPI that uses HTTP to
-// interact with etcd's membership API.
-func NewMembersAPI(c Client) MembersAPI {
-	return &httpMembersAPI{
-		client: c,
-	}
-}
-
-type MembersAPI interface {
-	// List enumerates the current cluster membership.
-	List(ctx context.Context) ([]Member, error)
-
-	// Add instructs etcd to accept a new Member into the cluster.
-	Add(ctx context.Context, peerURL string) (*Member, error)
-
-	// Remove demotes an existing Member out of the cluster.
-	Remove(ctx context.Context, mID string) error
-
-	// Update instructs etcd to update an existing Member in the cluster.
-	Update(ctx context.Context, mID string, peerURLs []string) error
-
-	// Leader gets current leader of the cluster
-	Leader(ctx context.Context) (*Member, error)
-}
-
-type httpMembersAPI struct {
-	client httpClient
-}
-
-func (m *httpMembersAPI) List(ctx context.Context) ([]Member, error) {
-	req := &membersAPIActionList{}
-	resp, body, err := m.client.Do(ctx, req)
-	if err != nil {
-		return nil, err
-	}
-
-	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
-		return nil, err
-	}
-
-	var mCollection memberCollection
-	if err := json.Unmarshal(body, &mCollection); err != nil {
-		return nil, err
-	}
-
-	return mCollection, nil
-}
-
-func (m *httpMembersAPI) Add(ctx context.Context, peerURL string) (*Member, error) {
-	urls, err := types.NewURLs([]string{peerURL})
-	if err != nil {
-		return nil, err
-	}
-
-	req := &membersAPIActionAdd{peerURLs: urls}
-	resp, body, err := m.client.Do(ctx, req)
-	if err != nil {
-		return nil, err
-	}
-
-	if err := assertStatusCode(resp.StatusCode, http.StatusCreated, http.StatusConflict); err != nil {
-		return nil, err
-	}
-
-	if resp.StatusCode != http.StatusCreated {
-		var merr membersError
-		if err := json.Unmarshal(body, &merr); err != nil {
-			return nil, err
-		}
-		return nil, merr
-	}
-
-	var memb Member
-	if err := json.Unmarshal(body, &memb); err != nil {
-		return nil, err
-	}
-
-	return &memb, nil
-}
-
-func (m *httpMembersAPI) Update(ctx context.Context, memberID string, peerURLs []string) error {
-	urls, err := types.NewURLs(peerURLs)
-	if err != nil {
-		return err
-	}
-
-	req := &membersAPIActionUpdate{peerURLs: urls, memberID: memberID}
-	resp, body, err := m.client.Do(ctx, req)
-	if err != nil {
-		return err
-	}
-
-	if err := assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusNotFound, http.StatusConflict); err != nil {
-		return err
-	}
-
-	if resp.StatusCode != http.StatusNoContent {
-		var merr membersError
-		if err := json.Unmarshal(body, &merr); err != nil {
-			return err
-		}
-		return merr
-	}
-
-	return nil
-}
-
-func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
-	req := &membersAPIActionRemove{memberID: memberID}
-	resp, _, err := m.client.Do(ctx, req)
-	if err != nil {
-		return err
-	}
-
-	return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone)
-}
-
-func (m *httpMembersAPI) Leader(ctx context.Context) (*Member, error) {
-	req := &membersAPIActionLeader{}
-	resp, body, err := m.client.Do(ctx, req)
-	if err != nil {
-		return nil, err
-	}
-
-	if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
-		return nil, err
-	}
-
-	var leader Member
-	if err := json.Unmarshal(body, &leader); err != nil {
-		return nil, err
-	}
-
-	return &leader, nil
-}
-
-type membersAPIActionList struct{}
-
-func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
-	u := v2MembersURL(ep)
-	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
-	return req
-}
-
-type membersAPIActionRemove struct {
-	memberID string
-}
-
-func (d *membersAPIActionRemove) HTTPRequest(ep url.URL) *http.Request {
-	u := v2MembersURL(ep)
-	u.Path = path.Join(u.Path, d.memberID)
-	req, _ := http.NewRequest(http.MethodDelete, u.String(), nil)
-	return req
-}
-
-type membersAPIActionAdd struct {
-	peerURLs types.URLs
-}
-
-func (a *membersAPIActionAdd) HTTPRequest(ep url.URL) *http.Request {
-	u := v2MembersURL(ep)
-	m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
-	b, _ := json.Marshal(&m)
-	req, _ := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(b))
-	req.Header.Set("Content-Type", "application/json")
-	return req
-}
-
-type membersAPIActionUpdate struct {
-	memberID string
-	peerURLs types.URLs
-}
-
-func (a *membersAPIActionUpdate) HTTPRequest(ep url.URL) *http.Request {
-	u := v2MembersURL(ep)
-	m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
-	u.Path = path.Join(u.Path, a.memberID)
-	b, _ := json.Marshal(&m)
-	req, _ := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(b))
-	req.Header.Set("Content-Type", "application/json")
-	return req
-}
-
-func assertStatusCode(got int, want ...int) (err error) {
-	for _, w := range want {
-		if w == got {
-			return nil
-		}
-	}
-	return fmt.Errorf("unexpected status code %d", got)
-}
-
-type membersAPIActionLeader struct{}
-
-func (l *membersAPIActionLeader) HTTPRequest(ep url.URL) *http.Request {
-	u := v2MembersURL(ep)
-	u.Path = path.Join(u.Path, defaultLeaderSuffix)
-	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
-	return req
-}
-
-// v2MembersURL add the necessary path to the provided endpoint
-// to route requests to the default v2 members API.
-func v2MembersURL(ep url.URL) *url.URL {
-	ep.Path = path.Join(ep.Path, defaultV2MembersPrefix)
-	return &ep
-}
-
-type membersError struct {
-	Message string `json:"message"`
-	Code    int    `json:"-"`
-}
-
-func (e membersError) Error() string {
-	return e.Message
-}
diff --git a/client/internal/v2/util.go b/client/internal/v2/util.go
deleted file mode 100644
index 76e4132c0..000000000
--- a/client/internal/v2/util.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2016 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"errors"
-	"regexp"
-)
-
-var (
-	roleNotFoundRegExp *regexp.Regexp
-	userNotFoundRegExp *regexp.Regexp
-)
-
-func init() {
-	roleNotFoundRegExp = regexp.MustCompile("auth: Role .* does not exist.")
-	userNotFoundRegExp = regexp.MustCompile("auth: User .* does not exist.")
-}
-
-// IsKeyNotFound returns true if the error code is ErrorCodeKeyNotFound.
-func IsKeyNotFound(err error) bool {
-	var cErr Error
-	return errors.As(err, &cErr) && cErr.Code == ErrorCodeKeyNotFound
-}
-
-// IsRoleNotFound returns true if the error means role not found of v2 API.
-func IsRoleNotFound(err error) bool {
-	var ae authError
-	return errors.As(err, &ae) && roleNotFoundRegExp.MatchString(ae.Message)
-}
-
-// IsUserNotFound returns true if the error means user not found of v2 API.
-func IsUserNotFound(err error) bool {
-	var ae authError
-	return errors.As(err, &ae) && userNotFoundRegExp.MatchString(ae.Message)
-}
diff --git a/client/pkg/LICENSE b/client/pkg/LICENSE
index d64569567..52fb9c29f 100644
--- a/client/pkg/LICENSE
+++ b/client/pkg/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/client/pkg/go.mod b/client/pkg/go.mod
index 634655bda..b0e41bbf6 100644
--- a/client/pkg/go.mod
+++ b/client/pkg/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/client/pkg/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/coreos/go-systemd/v22 v22.5.0
diff --git a/client/v3/LICENSE b/client/v3/LICENSE
index d64569567..52fb9c29f 100644
--- a/client/v3/LICENSE
+++ b/client/v3/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/client/v3/go.mod b/client/v3/go.mod
index 5a7d68c98..1e9908ae4 100644
--- a/client/v3/go.mod
+++ b/client/v3/go.mod
@@ -2,18 +2,18 @@ module go.etcd.io/etcd/client/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/dustin/go-humanize v1.0.1
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
 	github.com/prometheus/client_golang v1.22.0
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/grpc v1.72.1
+	google.golang.org/grpc v1.73.0
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -25,20 +25,21 @@ require (
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
-	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/client/v3/go.sum b/client/v3/go.sum
index dee69b948..52d7e6d4f 100644
--- a/client/v3/go.sum
+++ b/client/v3/go.sum
@@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -24,12 +24,12 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -48,8 +48,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -60,16 +60,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -85,8 +85,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -97,8 +97,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -107,12 +107,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/client/v3/watch.go b/client/v3/watch.go
index a46f98b8e..335aa3a35 100644
--- a/client/v3/watch.go
+++ b/client/v3/watch.go
@@ -580,7 +580,7 @@ func (w *watchGRPCStream) run() {
 		case pbresp := <-w.respc:
 			if cur == nil || pbresp.Created || pbresp.Canceled {
 				cur = pbresp
-			} else if cur != nil && cur.WatchId == pbresp.WatchId {
+			} else if cur.WatchId == pbresp.WatchId {
 				// merge new events
 				cur.Events = append(cur.Events, pbresp.Events...)
 				// update "Fragment" field; last response with "Fragment" == false
diff --git a/codecov.yml b/codecov.yml
index 26d584162..7d655ae41 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -9,7 +9,6 @@ codecov:
 fixes:
   - go.etcd.io/etcd/api/v3/::api/
   - go.etcd.io/etcd/client/v3/::client/v3/
-  - go.etcd.io/etcd/client/v2/::client/v2/
   - go.etcd.io/etcd/etcdctl/v3/::etcdctl/
   - go.etcd.io/etcd/etcdutl/v3/::etcdutl/
   - go.etcd.io/etcd/pkg/v3/::pkg/
diff --git a/contrib/raftexample/raft.go b/contrib/raftexample/raft.go
index 5e5dc010b..615e8c13e 100644
--- a/contrib/raftexample/raft.go
+++ b/contrib/raftexample/raft.go
@@ -25,6 +25,8 @@ import (
 	"strconv"
 	"time"
 
+	"go.uber.org/zap"
+
 	"go.etcd.io/etcd/client/pkg/v3/fileutil"
 	"go.etcd.io/etcd/client/pkg/v3/types"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp"
@@ -34,8 +36,6 @@ import (
 	"go.etcd.io/etcd/server/v3/storage/wal/walpb"
 	"go.etcd.io/raft/v3"
 	"go.etcd.io/raft/v3/raftpb"
-
-	"go.uber.org/zap"
 )
 
 type commit struct {
diff --git a/dummy.go b/dummy.go
index 48db9af93..62bfaa90f 100644
--- a/dummy.go
+++ b/dummy.go
@@ -18,7 +18,6 @@ package main_test
 // module (e.g. for sake of 'bom' generation).
 // Thanks to this 'go mod tidy' is not removing that dependencies from go.mod.
 import (
-	_ "go.etcd.io/etcd/client/v2"                // keep
 	_ "go.etcd.io/etcd/etcdctl/v3/ctlv3/command" // keep
 	_ "go.etcd.io/etcd/etcdutl/v3/etcdutl"       // keep
 	_ "go.etcd.io/etcd/tests/v3/integration"     // keep
diff --git a/etcdctl/LICENSE b/etcdctl/LICENSE
index d64569567..52fb9c29f 100644
--- a/etcdctl/LICENSE
+++ b/etcdctl/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/etcdctl/ctlv3/command/check.go b/etcdctl/ctlv3/command/check.go
index 4626d5222..c88015400 100644
--- a/etcdctl/ctlv3/command/check.go
+++ b/etcdctl/ctlv3/command/check.go
@@ -182,7 +182,7 @@ func newCheckPerfCommand(cmd *cobra.Command, args []string) {
 	bar := pb.New(cfg.duration)
 	bar.Start()
 
-	r := report.NewReport("%4.4f")
+	r := report.NewReport("%4.4f", "", false)
 	var wg sync.WaitGroup
 
 	wg.Add(len(clients))
@@ -353,7 +353,7 @@ func newCheckDatascaleCommand(cmd *cobra.Command, args []string) {
 	ksize, vsize := 512, 512
 	k, v := make([]byte, ksize), string(make([]byte, vsize))
 
-	r := report.NewReport("%4.4f")
+	r := report.NewReport("%4.4f", "", false)
 	var wg sync.WaitGroup
 	wg.Add(len(clients))
 
diff --git a/etcdctl/ctlv3/command/ep_command.go b/etcdctl/ctlv3/command/ep_command.go
index 65df3dff3..d82194d32 100644
--- a/etcdctl/ctlv3/command/ep_command.go
+++ b/etcdctl/ctlv3/command/ep_command.go
@@ -29,7 +29,6 @@ import (
 	"go.etcd.io/etcd/client/pkg/v3/logutil"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/pkg/v3/cobrautl"
-	"go.etcd.io/etcd/pkg/v3/flags"
 )
 
 var (
@@ -96,8 +95,6 @@ func epHealthCommandFunc(cmd *cobra.Command, args []string) {
 	if err != nil {
 		cobrautl.ExitWithError(cobrautl.ExitError, err)
 	}
-	flags.SetPflagsFromEnv(lg, "ETCDCTL", cmd.InheritedFlags())
-	initDisplayFromCmd(cmd)
 
 	cfgSpec := clientConfigFromCmd(cmd)
 
@@ -253,10 +250,24 @@ func endpointsFromCluster(cmd *cobra.Command) []string {
 		return endpoints
 	}
 
-	cfgSpec := clientConfigFromCmd(cmd)
+	sec := secureCfgFromCmd(cmd)
+	dt := dialTimeoutFromCmd(cmd)
+	ka := keepAliveTimeFromCmd(cmd)
+	kat := keepAliveTimeoutFromCmd(cmd)
+	eps, err := endpointsFromCmd(cmd)
+	if err != nil {
+		cobrautl.ExitWithError(cobrautl.ExitError, err)
+	}
 	// exclude auth for not asking needless password (MemberList() doesn't need authentication)
 	lg, _ := logutil.CreateDefaultZapLogger(zap.InfoLevel)
-	cfg, err := clientv3.NewClientConfig(cfgSpec, lg)
+
+	cfg, err := clientv3.NewClientConfig(&clientv3.ConfigSpec{
+		Endpoints:        eps,
+		DialTimeout:      dt,
+		KeepAliveTime:    ka,
+		KeepAliveTimeout: kat,
+		Secure:           sec,
+	}, lg)
 	if err != nil {
 		cobrautl.ExitWithError(cobrautl.ExitError, err)
 	}
diff --git a/etcdctl/ctlv3/command/printer_json.go b/etcdctl/ctlv3/command/printer_json.go
index 49abb35ff..6b2d16b83 100644
--- a/etcdctl/ctlv3/command/printer_json.go
+++ b/etcdctl/ctlv3/command/printer_json.go
@@ -17,20 +17,55 @@ package command
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 	"os"
 	"strconv"
 	"strings"
 
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	clientv3 "go.etcd.io/etcd/client/v3"
 )
 
 type jsonPrinter struct {
-	isHex bool
+	writer io.Writer
+	isHex  bool
 	printer
 }
 
+type (
+	HexResponseHeader pb.ResponseHeader
+	HexMember         pb.Member
+)
+
+func (h *HexResponseHeader) MarshalJSON() ([]byte, error) {
+	type Alias pb.ResponseHeader
+
+	return json.Marshal(&struct {
+		ClusterID string `json:"cluster_id"`
+		MemberID  string `json:"member_id"`
+		Alias
+	}{
+		ClusterID: fmt.Sprintf("%x", h.ClusterId),
+		MemberID:  fmt.Sprintf("%x", h.MemberId),
+		Alias:     (Alias)(*h),
+	})
+}
+
+func (m *HexMember) MarshalJSON() ([]byte, error) {
+	type Alias pb.Member
+
+	return json.Marshal(&struct {
+		ID string `json:"ID"`
+		Alias
+	}{
+		ID:    fmt.Sprintf("%x", m.ID),
+		Alias: (Alias)(*m),
+	})
+}
+
 func newJSONPrinter(isHex bool) printer {
 	return &jsonPrinter{
+		writer:  os.Stdout,
 		isHex:   isHex,
 		printer: &printerRPC{newPrinterUnsupported("json"), printJSON},
 	}
@@ -42,22 +77,27 @@ func (p *jsonPrinter) EndpointHashKV(r []epHashKV) { printJSON(r) }
 
 func (p *jsonPrinter) MemberList(r clientv3.MemberListResponse) {
 	if p.isHex {
-		printMemberListWithHexJSON(r)
+		printMemberListWithHexJSON(p.writer, r)
 	} else {
 		printJSON(r)
 	}
 }
+func (p *jsonPrinter) MemberAdd(r clientv3.MemberAddResponse) { p.printJSON(r) }
 
-func printJSON(v any) {
+func printJSONTo(w io.Writer, v any) {
 	b, err := json.Marshal(v)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "%v\n", err)
 		return
 	}
-	fmt.Println(string(b))
+	fmt.Fprintln(w, string(b))
+}
+
+func printJSON(v any) {
+	printJSONTo(os.Stdout, v)
 }
 
-func printMemberListWithHexJSON(r clientv3.MemberListResponse) {
+func printMemberListWithHexJSON(w io.Writer, r clientv3.MemberListResponse) {
 	var buffer strings.Builder
 	var b []byte
 	buffer.WriteString("{\"header\":{\"cluster_id\":\"")
@@ -96,5 +136,42 @@ func printMemberListWithHexJSON(r clientv3.MemberListResponse) {
 		}
 	}
 	buffer.WriteString("}")
-	fmt.Println(buffer.String())
+	fmt.Fprintln(w, buffer.String())
+}
+
+func (p *jsonPrinter) printJSON(v any) {
+	var data any
+	if !p.isHex {
+		printJSONTo(p.writer, v)
+		return
+	}
+
+	switch r := v.(type) {
+	case clientv3.MemberAddResponse:
+		type Alias clientv3.MemberAddResponse
+
+		data = &struct {
+			Header  *HexResponseHeader `json:"header"`
+			Member  *HexMember         `json:"member"`
+			Members []*HexMember       `json:"members"`
+			*Alias
+		}{
+			Header:  (*HexResponseHeader)(r.Header),
+			Member:  (*HexMember)(r.Member),
+			Members: toHexMembers(r.Members),
+			Alias:   (*Alias)(&r),
+		}
+	default:
+		data = v
+	}
+
+	printJSONTo(p.writer, data)
+}
+
+func toHexMembers(members []*pb.Member) []*HexMember {
+	hexMembers := make([]*HexMember, len(members))
+	for i, member := range members {
+		hexMembers[i] = (*HexMember)(member)
+	}
+	return hexMembers
 }
diff --git a/etcdctl/ctlv3/command/printer_table.go b/etcdctl/ctlv3/command/printer_table.go
index c576231ed..fcab69fb0 100644
--- a/etcdctl/ctlv3/command/printer_table.go
+++ b/etcdctl/ctlv3/command/printer_table.go
@@ -18,6 +18,7 @@ import (
 	"os"
 
 	"github.com/olekukonko/tablewriter"
+	"github.com/olekukonko/tablewriter/tw"
 
 	v3 "go.etcd.io/etcd/client/v3"
 )
@@ -26,44 +27,44 @@ type tablePrinter struct{ printer }
 
 func (tp *tablePrinter) MemberList(r v3.MemberListResponse) {
 	hdr, rows := makeMemberListTable(r)
-	table := tablewriter.NewWriter(os.Stdout)
-	table.SetHeader(hdr)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header(hdr)
 	for _, row := range rows {
 		table.Append(row)
 	}
-	table.SetAlignment(tablewriter.ALIGN_RIGHT)
 	table.Render()
 }
 
 func (tp *tablePrinter) EndpointHealth(r []epHealth) {
 	hdr, rows := makeEndpointHealthTable(r)
-	table := tablewriter.NewWriter(os.Stdout)
-	table.SetHeader(hdr)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header(hdr)
 	for _, row := range rows {
 		table.Append(row)
 	}
-	table.SetAlignment(tablewriter.ALIGN_RIGHT)
 	table.Render()
 }
 
 func (tp *tablePrinter) EndpointStatus(r []epStatus) {
 	hdr, rows := makeEndpointStatusTable(r)
-	table := tablewriter.NewWriter(os.Stdout)
-	table.SetHeader(hdr)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header(hdr)
 	for _, row := range rows {
 		table.Append(row)
 	}
-	table.SetAlignment(tablewriter.ALIGN_RIGHT)
 	table.Render()
 }
 
 func (tp *tablePrinter) EndpointHashKV(r []epHashKV) {
 	hdr, rows := makeEndpointHashKVTable(r)
-	table := tablewriter.NewWriter(os.Stdout)
-	table.SetHeader(hdr)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header(hdr)
 	for _, row := range rows {
 		table.Append(row)
 	}
-	table.SetAlignment(tablewriter.ALIGN_RIGHT)
 	table.Render()
 }
diff --git a/etcdctl/go.mod b/etcdctl/go.mod
index e64be6ff0..66c6c4821 100644
--- a/etcdctl/go.mod
+++ b/etcdctl/go.mod
@@ -2,13 +2,13 @@ module go.etcd.io/etcd/etcdctl/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/bgentry/speakeasy v0.2.0
 	github.com/cheggaaa/pb/v3 v3.1.7
 	github.com/dustin/go-humanize v1.0.1
-	github.com/olekukonko/tablewriter v0.0.5
+	github.com/olekukonko/tablewriter v1.0.7
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
@@ -17,8 +17,8 @@ require (
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/time v0.11.0
-	google.golang.org/grpc v1.72.1
+	golang.org/x/time v0.12.0
+	google.golang.org/grpc v1.73.0
 )
 
 require (
@@ -27,21 +27,24 @@ require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
+	github.com/olekukonko/ll v0.0.8 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/etcdctl/go.sum b/etcdctl/go.sum
index 6444f2fc2..2a9cb4dcd 100644
--- a/etcdctl/go.sum
+++ b/etcdctl/go.sum
@@ -19,8 +19,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -32,12 +32,12 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -50,21 +50,24 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
+github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
+github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -83,16 +86,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -108,8 +111,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -121,10 +124,10 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
-golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -133,12 +136,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/etcdutl/LICENSE b/etcdutl/LICENSE
index d64569567..52fb9c29f 100644
--- a/etcdutl/LICENSE
+++ b/etcdutl/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/etcdutl/README.md b/etcdutl/README.md
index 06cf85d7e..ead1b41cc 100644
--- a/etcdutl/README.md
+++ b/etcdutl/README.md
@@ -1,5 +1,4 @@
-etcdutl
-========
+# etcdutl
 
 `etcdutl` is a command line administration utility for [etcd][etcd].
 
@@ -8,7 +7,7 @@ For operations over a network, please use `etcdctl`.
 
 ### DEFRAG [options]
 
-DEFRAG directly defragments an etcd data directory while etcd is not running. 
+DEFRAG directly defragments an etcd data directory while etcd is not running.
 When an etcd member reclaims storage space from deleted and compacted keys, the space is kept in a free list and the database file remains the same size. By defragmenting the database, the etcd member releases this free space back to the file system.
 
 In order to defrag a live etcd instances over the network, please use `etcdctl defrag` instead.
@@ -36,7 +35,6 @@ To defragment a data directory directly, use the `--data-dir` flag:
 
 DEFRAG returns a zero exit code only if it succeeded in defragmenting all given endpoints.
 
-
 ### SNAPSHOT RESTORE [options] \<filename\>
 
 SNAPSHOT RESTORE creates an etcd data directory for an etcd cluster member from a backend database snapshot and a new cluster configuration. Restoring the snapshot into each member for a new cluster configuration will initialize a new etcd cluster preloaded by the snapshot data.
@@ -174,6 +172,74 @@ Prints etcd version and API version.
 # API version: 3.1
 ```
 
+### LIST-BUCKET [options] \<data dir or db file path\>
+
+`list-bucket` prints all bucket names.
+
+#### Flags
+
+- timeout -- Time to wait to obtain a file lock on db file, 0 to block indefinitely.
+
+##### Examples for LIST-BUCKET
+
+```bash
+
+$ ./etcdutl list-bucket ~/tmp/etcd/default.etcd/member/snap/db
+alarm
+auth
+authRoles
+authUsers
+cluster
+key
+lease
+members
+members_removed
+meta
+```
+
+### ITERATE-BUCKET [options] \<data dir or db file path\> \<bucket name\>
+
+`iterate-bucket` lists key-value pairs in a given bucket in reverse order.
+
+#### Flags for ITERATE-BUCKET
+
+- timeout -- Time to wait to obtain a file lock on db file, 0 to block indefinitely.
+- limit -- Max number of key-value pairs to iterate (0 to iterate all).
+- decode -- true to decode Protocol Buffer encoded data.
+
+##### Examples for ITERATE-BUCKET
+
+```bash
+
+# with `--decode` option
+$ ./etcdutl iterate-bucket ~/tmp/etcd/default.etcd/member/snap/db key --decode
+rev={Revision:{Main:4 Sub:0} tombstone:false}, value=[key "k1" | val "v3" | created 2 | mod 4 | ver 3]
+rev={Revision:{Main:3 Sub:0} tombstone:false}, value=[key "k1" | val "v2" | created 2 | mod 3 | ver 2]
+rev={Revision:{Main:2 Sub:0} tombstone:false}, value=[key "k1" | val "v1" | created 2 | mod 2 | ver 1]
+
+# without `--decode` option
+$ ./etcdutl iterate-bucket ~/tmp/etcd/default.etcd/member/snap/db key
+key="\x00\x00\x00\x00\x00\x00\x00\x04_\x00\x00\x00\x00\x00\x00\x00\x00", value="\n\x02k1\x10\x02\x18\x04 \x03*\x02v3"
+key="\x00\x00\x00\x00\x00\x00\x00\x03_\x00\x00\x00\x00\x00\x00\x00\x00", value="\n\x02k1\x10\x02\x18\x03 \x02*\x02v2"
+key="\x00\x00\x00\x00\x00\x00\x00\x02_\x00\x00\x00\x00\x00\x00\x00\x00", value="\n\x02k1\x10\x02\x18\x02 \x01*\x02v1"
+```
+
+### HASH [options] \<data dir or db file path\>
+
+`hash` prints the hash of the db file.
+
+#### Flags for HASH
+
+- timeout -- Time to wait to obtain a file lock on db file, 0 to block indefinitely.
+
+##### Examples for HASH
+
+```bash
+
+$ ./etcdutl hash ~/tmp/etcd/default.etcd/member/snap/db
+db path: /Users/wachao/tmp/etcd/default.etcd/member/snap/db
+Hash: 4031086527
+```
 
 ## Exit codes
 
diff --git a/etcdutl/ctl.go b/etcdutl/ctl.go
index 8418729bf..5e91c14cf 100644
--- a/etcdutl/ctl.go
+++ b/etcdutl/ctl.go
@@ -45,6 +45,9 @@ func init() {
 		etcdutl.NewVersionCommand(),
 		etcdutl.NewCompletionCommand(),
 		etcdutl.NewMigrateCommand(),
+		etcdutl.NewListBucketCommand(),
+		etcdutl.NewIterateBucketCommand(),
+		etcdutl.NewHashCommand(),
 	)
 }
 
diff --git a/etcdutl/etcdutl/bucket_command.go b/etcdutl/etcdutl/bucket_command.go
new file mode 100644
index 000000000..ba2461e6d
--- /dev/null
+++ b/etcdutl/etcdutl/bucket_command.go
@@ -0,0 +1,276 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package etcdutl
+
+import (
+	"encoding/binary"
+	"fmt"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+	"go.uber.org/zap"
+
+	bolt "go.etcd.io/bbolt"
+	"go.etcd.io/etcd/api/v3/authpb"
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/client/pkg/v3/fileutil"
+	"go.etcd.io/etcd/server/v3/lease/leasepb"
+	"go.etcd.io/etcd/server/v3/storage/backend"
+	"go.etcd.io/etcd/server/v3/storage/datadir"
+	"go.etcd.io/etcd/server/v3/storage/mvcc"
+	"go.etcd.io/etcd/server/v3/storage/schema"
+)
+
+var (
+	// TODO: add this configure to top level etcdutl command
+	flockTimeout        time.Duration
+	iterateBucketLimit  uint64
+	iterateBucketDecode bool
+)
+
+func NewListBucketCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list-bucket [data dir or db file path]",
+		Short: "bucket lists all buckets.",
+		Args:  cobra.ExactArgs(1),
+		Run:   listBucketCommandFunc,
+	}
+
+	// TODO: add this flag to top level etctutl command
+	cmd.PersistentFlags().DurationVar(&flockTimeout, "timeout", 10*time.Second, "time to wait to obtain a file lock on db file, 0 to block indefinitely")
+
+	return cmd
+}
+
+func NewIterateBucketCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "iterate-bucket [data dir or db file path] [bucket name]",
+		Short: "iterate-bucket lists key-value pairs in reverse order.",
+		Args:  cobra.ExactArgs(2),
+		Run:   iterateBucketCommandFunc,
+	}
+
+	// TODO: add this flag to top level etctutl command
+	cmd.PersistentFlags().DurationVar(&flockTimeout, "timeout", 10*time.Second, "time to wait to obtain a file lock on db file, 0 to block indefinitely")
+	cmd.PersistentFlags().Uint64Var(&iterateBucketLimit, "limit", 0, "max number of key-value pairs to iterate (0 to iterate all)")
+	cmd.PersistentFlags().BoolVar(&iterateBucketDecode, "decode", false, "true to decode Protocol Buffer encoded data")
+
+	return cmd
+}
+
+func NewHashCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "hash [data dir or db file path]",
+		Short: "hash computes the hash of db file.",
+		Args:  cobra.ExactArgs(1),
+		Run:   getHashCommandFunc,
+	}
+
+	// TODO: add this flag to top level etctutl command
+	cmd.PersistentFlags().DurationVar(&flockTimeout, "timeout", 10*time.Second, "time to wait to obtain a file lock on db file, 0 to block indefinitely")
+
+	return cmd
+}
+
+func listBucketCommandFunc(_ *cobra.Command, args []string) {
+	lg := GetLogger()
+	dp := args[0]
+	if !strings.HasSuffix(dp, "db") {
+		dp = filepath.Join(datadir.ToSnapDir(dp), "db")
+	}
+
+	if !fileutil.Exist(dp) {
+		lg.Fatal("db file not exist", zap.String("path", dp))
+	}
+
+	bts, err := getBuckets(dp)
+	if err != nil {
+		lg.Fatal("Failed to get buckets", zap.Error(err))
+	}
+	for _, b := range bts {
+		fmt.Println(b)
+	}
+}
+
+func getBuckets(dbPath string) (buckets []string, err error) {
+	db, derr := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: flockTimeout})
+	if derr != nil {
+		return nil, fmt.Errorf("failed to open bolt DB %w", derr)
+	}
+	defer db.Close()
+
+	err = db.View(func(tx *bolt.Tx) error {
+		return tx.ForEach(func(b []byte, _ *bolt.Bucket) error {
+			buckets = append(buckets, string(b))
+			return nil
+		})
+	})
+	return buckets, err
+}
+
+func iterateBucketCommandFunc(_ *cobra.Command, args []string) {
+	lg := GetLogger()
+	dp := args[0]
+	if !strings.HasSuffix(dp, "db") {
+		dp = filepath.Join(datadir.ToSnapDir(dp), "db")
+	}
+	if !fileutil.Exist(dp) {
+		lg.Fatal("db file not exist", zap.String("path", dp))
+	}
+	bucket := args[1]
+	err := iterateBucket(dp, bucket, iterateBucketLimit, iterateBucketDecode)
+	if err != nil {
+		lg.Fatal("Failed to iterate bucket", zap.Error(err))
+	}
+}
+
+type decoder func(k, v []byte)
+
+// key is the bucket name, and value is the function to decode K/V in the bucket.
+var decoders = map[string]decoder{
+	"key":       keyDecoder,
+	"lease":     leaseDecoder,
+	"auth":      authDecoder,
+	"authRoles": authRolesDecoder,
+	"authUsers": authUsersDecoder,
+	"meta":      metaDecoder,
+}
+
+func defaultDecoder(k, v []byte) {
+	fmt.Printf("key=%q, value=%q\n", k, v)
+}
+
+func keyDecoder(k, v []byte) {
+	rev := mvcc.BytesToBucketKey(k)
+	var kv mvccpb.KeyValue
+	if err := kv.Unmarshal(v); err != nil {
+		panic(err)
+	}
+	fmt.Printf("rev=%+v, value=[key %q | val %q | created %d | mod %d | ver %d]\n", rev, string(kv.Key), string(kv.Value), kv.CreateRevision, kv.ModRevision, kv.Version)
+}
+
+func bytesToLeaseID(bytes []byte) int64 {
+	if len(bytes) != 8 {
+		panic(fmt.Errorf("lease ID must be 8-byte"))
+	}
+	return int64(binary.BigEndian.Uint64(bytes))
+}
+
+func leaseDecoder(k, v []byte) {
+	leaseID := bytesToLeaseID(k)
+	var lpb leasepb.Lease
+	if err := lpb.Unmarshal(v); err != nil {
+		panic(err)
+	}
+	fmt.Printf("lease ID=%016x, TTL=%ds, remaining TTL=%ds\n", leaseID, lpb.TTL, lpb.RemainingTTL)
+}
+
+func authDecoder(k, v []byte) {
+	if string(k) == "authRevision" {
+		rev := binary.BigEndian.Uint64(v)
+		fmt.Printf("key=%q, value=%v\n", k, rev)
+	} else {
+		fmt.Printf("key=%q, value=%v\n", k, v)
+	}
+}
+
+func authRolesDecoder(_, v []byte) {
+	role := &authpb.Role{}
+	err := role.Unmarshal(v)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("role=%q, keyPermission=%v\n", string(role.Name), role.KeyPermission)
+}
+
+func authUsersDecoder(_, v []byte) {
+	user := &authpb.User{}
+	err := user.Unmarshal(v)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("user=%q, roles=%q, option=%v\n", user.Name, user.Roles, user.Options)
+}
+
+func metaDecoder(k, v []byte) {
+	if string(k) == string(schema.MetaConsistentIndexKeyName) || string(k) == string(schema.MetaTermKeyName) {
+		fmt.Printf("key=%q, value=%v\n", k, binary.BigEndian.Uint64(v))
+	} else if string(k) == string(schema.ScheduledCompactKeyName) || string(k) == string(schema.FinishedCompactKeyName) {
+		rev := mvcc.BytesToRev(v)
+		fmt.Printf("key=%q, value=%v\n", k, rev)
+	} else {
+		defaultDecoder(k, v)
+	}
+}
+
+func iterateBucket(dbPath, bucket string, limit uint64, decode bool) (err error) {
+	db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: flockTimeout})
+	if err != nil {
+		return fmt.Errorf("failed to open bolt DB %w", err)
+	}
+	defer db.Close()
+
+	err = db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(bucket))
+		if b == nil {
+			return fmt.Errorf("got nil bucket for %s", bucket)
+		}
+
+		c := b.Cursor()
+
+		// iterate in reverse order (use First() and Next() for ascending order)
+		for k, v := c.Last(); k != nil; k, v = c.Prev() {
+			// TODO: remove sensitive information
+			// (https://github.com/etcd-io/etcd/issues/7620)
+			if dec, ok := decoders[bucket]; decode && ok {
+				dec(k, v)
+			} else {
+				defaultDecoder(k, v)
+			}
+
+			limit--
+			if limit == 0 {
+				break
+			}
+		}
+
+		return nil
+	})
+	return err
+}
+
+func getHashCommandFunc(_ *cobra.Command, args []string) {
+	lg := GetLogger()
+	dp := args[0]
+	if !strings.HasSuffix(dp, "db") {
+		dp = filepath.Join(datadir.ToSnapDir(dp), "db")
+	}
+	if !fileutil.Exist(dp) {
+		lg.Fatal("db file not exist", zap.String("path", dp))
+	}
+
+	hash, err := getHash(dp)
+	if err != nil {
+		lg.Fatal("failed to get hash", zap.Error(err))
+	}
+	fmt.Printf("db path: %s\nHash: %d\n", dp, hash)
+}
+
+func getHash(dbPath string) (hash uint32, err error) {
+	b := backend.NewDefaultBackend(zap.NewNop(), dbPath)
+	return b.Hash(schema.DefaultIgnores)
+}
diff --git a/etcdutl/etcdutl/printer_table.go b/etcdutl/etcdutl/printer_table.go
index ec66ea38f..8558d5a0b 100644
--- a/etcdutl/etcdutl/printer_table.go
+++ b/etcdutl/etcdutl/printer_table.go
@@ -18,6 +18,7 @@ import (
 	"os"
 
 	"github.com/olekukonko/tablewriter"
+	"github.com/olekukonko/tablewriter/tw"
 
 	"go.etcd.io/etcd/etcdutl/v3/snapshot"
 )
@@ -26,22 +27,22 @@ type tablePrinter struct{ printer }
 
 func (tp *tablePrinter) DBStatus(r snapshot.Status) {
 	hdr, rows := makeDBStatusTable(r)
-	table := tablewriter.NewWriter(os.Stdout)
-	table.SetHeader(hdr)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header(hdr)
 	for _, row := range rows {
 		table.Append(row)
 	}
-	table.SetAlignment(tablewriter.ALIGN_RIGHT)
 	table.Render()
 }
 
 func (tp *tablePrinter) DBHashKV(r HashKV) {
 	hdr, rows := makeDBHashKVTable(r)
-	table := tablewriter.NewWriter(os.Stdout)
-	table.SetHeader(hdr)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header(hdr)
 	for _, row := range rows {
 		table.Append(row)
 	}
-	table.SetAlignment(tablewriter.ALIGN_RIGHT)
 	table.Render()
 }
diff --git a/etcdutl/go.mod b/etcdutl/go.mod
index 4d0bfa804..43d07c22e 100644
--- a/etcdutl/go.mod
+++ b/etcdutl/go.mod
@@ -2,12 +2,11 @@ module go.etcd.io/etcd/etcdutl/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
 	go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
-	go.etcd.io/etcd/client/v2 => ./../client/internal/v2
 	go.etcd.io/etcd/client/v3 => ../client/v3
 	go.etcd.io/etcd/pkg/v3 => ../pkg
 	go.etcd.io/etcd/server/v3 => ../server
@@ -24,10 +23,10 @@ replace (
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/dustin/go-humanize v1.0.1
-	github.com/olekukonko/tablewriter v0.0.5
+	github.com/olekukonko/tablewriter v1.0.7
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
-	go.etcd.io/bbolt v1.4.0
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
@@ -39,11 +38,12 @@ require (
 
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/fatih/color v1.18.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@@ -52,17 +52,21 @@ require (
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
+	github.com/olekukonko/ll v0.0.8 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_golang v1.22.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -70,28 +74,26 @@ require (
 	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
-	go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
-	go.opentelemetry.io/otel v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
-	go.opentelemetry.io/otel/trace v1.35.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/crypto v0.38.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/crypto v0.39.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	golang.org/x/time v0.11.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/grpc v1.72.1 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	golang.org/x/time v0.12.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/grpc v1.73.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/etcdutl/go.sum b/etcdutl/go.sum
index 30884b30c..245e953b8 100644
--- a/etcdutl/go.sum
+++ b/etcdutl/go.sum
@@ -1,7 +1,7 @@
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
@@ -17,9 +17,11 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -38,12 +40,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -58,13 +60,20 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
+github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
+github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -72,8 +81,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -100,30 +109,30 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
-go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
-go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
+go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -133,8 +142,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -142,25 +151,26 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
-golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -169,12 +179,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -185,7 +195,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/go.mod b/go.mod
index 8dbe4877a..3001e26e8 100644
--- a/go.mod
+++ b/go.mod
@@ -2,12 +2,11 @@ module go.etcd.io/etcd/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 replace (
 	go.etcd.io/etcd/api/v3 => ./api
 	go.etcd.io/etcd/client/pkg/v3 => ./client/pkg
-	go.etcd.io/etcd/client/v2 => ./client/internal/v2
 	go.etcd.io/etcd/client/v3 => ./client/v3
 	go.etcd.io/etcd/etcdctl/v3 => ./etcdctl
 	go.etcd.io/etcd/etcdutl/v3 => ./etcdutl
@@ -23,10 +22,9 @@ require (
 	github.com/dustin/go-humanize v1.0.1
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
-	go.etcd.io/bbolt v1.4.0
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
-	go.etcd.io/etcd/client/v2 v2.306.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0
@@ -35,20 +33,20 @@ require (
 	go.etcd.io/etcd/tests/v3 v3.0.0-00010101000000-000000000000
 	go.etcd.io/raft/v3 v3.6.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/time v0.11.0
-	google.golang.org/grpc v1.72.1
+	golang.org/x/time v0.12.0
+	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 )
 
 require (
 	github.com/VividCortex/ewma v1.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@@ -59,20 +57,22 @@ require (
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/olekukonko/tablewriter v0.0.5 // indirect
+	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
+	github.com/olekukonko/ll v0.0.8 // indirect
+	github.com/olekukonko/tablewriter v1.0.7 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_golang v1.22.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -82,23 +82,22 @@ require (
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.etcd.io/gofail v0.2.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
-	go.opentelemetry.io/otel v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
-	go.opentelemetry.io/otel/trace v1.35.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/crypto v0.38.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/crypto v0.39.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 4eca952f3..eca17b15f 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
 github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -37,8 +37,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -68,12 +68,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -94,13 +94,16 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
+github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
+github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -111,8 +114,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -143,32 +146,32 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
-go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA=
 go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
-go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
+go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -181,8 +184,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -198,16 +201,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -219,10 +222,10 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
-golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -240,17 +243,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -264,7 +267,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/pkg/LICENSE b/pkg/LICENSE
index d64569567..52fb9c29f 100644
--- a/pkg/LICENSE
+++ b/pkg/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/pkg/adt/interval_tree.go b/pkg/adt/interval_tree.go
index 4de7ca480..85542020f 100644
--- a/pkg/adt/interval_tree.go
+++ b/pkg/adt/interval_tree.go
@@ -434,38 +434,11 @@ func (ivt *intervalTree) createIntervalNode(ivl Interval, val any) *intervalNode
 	}
 }
 
-// TODO: make this consistent with textbook implementation
-//
-// "Introduction to Algorithms" (Cormen et al, 3rd ed.), chapter 13.3, p315
-//
-//	 RB-INSERT(T, z)
-//
-//	 y = T.nil
-//	 x = T.root
-//
-//	 while x ≠ T.nil
-//	 	y = x
-//	 	if z.key < x.key
-//	 		x = x.left
-//	 	else
-//	 		x = x.right
-//
-//	 z.p = y
-//
-//	 if y == T.nil
-//	 	T.root = z
-//	 else if z.key < y.key
-//	 	y.left = z
-//	 else
-//	 	y.right = z
-//
-//	 z.left = T.nil
-//	 z.right = T.nil
-//	 z.color = RED
-//
-//	 RB-INSERT-FIXUP(T, z)
-
 // Insert adds a node with the given interval into the tree.
+//
+// Cormen "Introduction to Algorithms", Chapter 14 Exercise 14.3.5.
+// The algorithm follows Cormen "Introduction to Algorithms", Chapter 14 Exercise 14.3.5.
+// for modifying an interval tree structure to support exact interval matching.
 func (ivt *intervalTree) Insert(ivl Interval, val any) {
 	y := ivt.sentinel
 	z := ivt.createIntervalNode(ivl, val)
@@ -714,7 +687,10 @@ func (ivt *intervalTree) Visit(ivl Interval, ivv IntervalVisitor) {
 	ivt.root.visit(&ivl, ivt.sentinel, func(n *intervalNode) bool { return ivv(&n.iv) })
 }
 
-// find the exact node for a given interval
+// find the exact node for a given interval. The implementation follows
+// Cormen "Introduction to Algorithms", Chapter 14 Exercise 14.3.5. for
+// exact interval matching. The search runs in O(log n) time on an n-node
+// interval tree.
 func (ivt *intervalTree) find(ivl Interval) *intervalNode {
 	x := ivt.root
 	// Search until hit sentinel or exact match.
diff --git a/pkg/go.mod b/pkg/go.mod
index 2b7a25604..2081725f8 100644
--- a/pkg/go.mod
+++ b/pkg/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/pkg/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/creack/pty v1.1.18
@@ -12,22 +12,21 @@ require (
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/grpc v1.72.1
+	google.golang.org/grpc v1.73.0
 )
 
 require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	go.opentelemetry.io/otel v1.35.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/pkg/go.sum b/pkg/go.sum
index 326a21867..f44d0ce78 100644
--- a/pkg/go.sum
+++ b/pkg/go.sum
@@ -7,8 +7,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -37,32 +37,32 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/osutil/interrupt_unix.go b/pkg/osutil/interrupt_unix.go
index ec9dc7b67..64b270e3c 100644
--- a/pkg/osutil/interrupt_unix.go
+++ b/pkg/osutil/interrupt_unix.go
@@ -22,9 +22,9 @@ import (
 	"sync"
 	"syscall"
 
-	"go.etcd.io/etcd/client/pkg/v3/verify"
-
 	"go.uber.org/zap"
+
+	"go.etcd.io/etcd/client/pkg/v3/verify"
 )
 
 // InterruptHandler is a function that is called on receiving a
diff --git a/pkg/report/perfdash.go b/pkg/report/perfdash.go
new file mode 100644
index 000000000..2fe9f8dad
--- /dev/null
+++ b/pkg/report/perfdash.go
@@ -0,0 +1,87 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package report
+
+import (
+	"encoding/json"
+	"fmt"
+	"math"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+type Metrics struct {
+	Perc50 float64 `json:"Perc50"`
+	Perc90 float64 `json:"Perc90"`
+	Perc99 float64 `json:"Perc99"`
+}
+
+type Labels struct {
+	Metric string `json:"Metric"`
+}
+
+type DataItem struct {
+	Data   Metrics `json:"data"`
+	Labels Labels  `json:"labels"`
+	Unit   string  `json:"unit"`
+}
+
+type perfdashFormattedReport struct {
+	Version   string     `json:"version"`
+	DataItems []DataItem `json:"dataItems"`
+}
+
+func (r *report) writePerfDashReport(reportName string) {
+	pcls, data := Percentiles(r.stats.Lats)
+	pclsData := make(map[float64]float64)
+	for i := 0; i < len(pcls); i++ {
+		pclsData[pcls[i]] = data[i] * 1000 // Since the reported data is in seconds, convert to ms.
+	}
+	report := perfdashFormattedReport{
+		Version: "v1",
+		DataItems: []DataItem{
+			{
+				Data: Metrics{
+					Perc50: math.Round(pclsData[50]*10000) / 10000,
+					Perc90: math.Round(pclsData[90]*10000) / 10000,
+					Perc99: math.Round(pclsData[99]*10000) / 10000,
+				},
+				Unit: "ms",
+				Labels: Labels{
+					Metric: "APIResponsiveness",
+				},
+			},
+		},
+	}
+	reportB, _ := json.MarshalIndent(report, "", "  ")
+
+	artifactsDir := os.Getenv("ARTIFACTS")
+	if artifactsDir == "" {
+		artifactsDir = "./_artifacts"
+	}
+
+	fileName := fmt.Sprintf("etcd_perf_%s_%s.json", reportName, time.Now().UTC().Format(time.RFC3339))
+	err := os.MkdirAll(artifactsDir, 0o755)
+	if err != nil {
+		fmt.Println("Error creating artifacts directory:", err)
+	}
+	destPath := filepath.Join(artifactsDir, fileName)
+	err = os.WriteFile(destPath, reportB, 0o644)
+	if err != nil {
+		fmt.Println("Error writing to file:", err)
+	}
+	fmt.Println("Successfully created a JSON perf report at", destPath)
+}
diff --git a/pkg/report/report.go b/pkg/report/report.go
index a33f97cfa..3f578e9f1 100644
--- a/pkg/report/report.go
+++ b/pkg/report/report.go
@@ -41,8 +41,10 @@ type Result struct {
 func (res *Result) Duration() time.Duration { return res.End.Sub(res.Start) }
 
 type report struct {
-	results   chan Result
-	precision string
+	generatePerfReport bool
+	reportName         string
+	precision          string
+	results            chan Result
 
 	stats Stats
 	sps   *secondPoints
@@ -81,19 +83,23 @@ type Report interface {
 	Stats() <-chan Stats
 }
 
-func NewReport(precision string) Report { return newReport(precision) }
+func NewReport(precision, reportName string, generatePerfReport bool) Report {
+	return newReport(precision, reportName, generatePerfReport)
+}
 
-func newReport(precision string) *report {
+func newReport(precision, reportName string, generatePerfReport bool) *report {
 	r := &report{
-		results:   make(chan Result, 16),
-		precision: precision,
+		results:            make(chan Result, 16),
+		precision:          precision,
+		generatePerfReport: generatePerfReport,
+		reportName:         reportName,
 	}
 	r.stats.ErrorDist = make(map[string]int)
 	return r
 }
 
-func NewReportSample(precision string) Report {
-	r := NewReport(precision).(*report)
+func NewReportSample(precision, reportName string, generatePerfReport bool) Report {
+	r := NewReport(precision, reportName, generatePerfReport).(*report)
 	r.sps = newSecondPoints()
 	return r
 }
@@ -105,6 +111,9 @@ func (r *report) Run() <-chan string {
 	go func() {
 		defer close(donec)
 		r.processResults()
+		if r.generatePerfReport {
+			r.writePerfDashReport(r.reportName)
+		}
 		donec <- r.String()
 	}()
 	return donec
@@ -155,8 +164,8 @@ func (r *report) sec2str(sec float64) string { return fmt.Sprintf(r.precision+"
 
 type reportRate struct{ *report }
 
-func NewReportRate(precision string) Report {
-	return &reportRate{NewReport(precision).(*report)}
+func NewReportRate(precision, reportName string, generatePerfReport bool) Report {
+	return &reportRate{NewReport(precision, reportName, generatePerfReport).(*report)}
 }
 
 func (r *reportRate) String() string {
diff --git a/pkg/report/weighted.go b/pkg/report/weighted.go
index 411214f6d..a06b59374 100644
--- a/pkg/report/weighted.go
+++ b/pkg/report/weighted.go
@@ -30,10 +30,10 @@ type weightedReport struct {
 
 // NewWeightedReport returns a report that includes
 // both weighted and unweighted statistics.
-func NewWeightedReport(r Report, precision string) Report {
+func NewWeightedReport(r Report, precision, reportName string, generatePerfReport bool) Report {
 	return &weightedReport{
 		baseReport: r,
-		report:     newReport(precision),
+		report:     newReport(precision, reportName, generatePerfReport),
 		results:    make(chan Result, 16),
 	}
 }
diff --git a/scripts/test.sh b/scripts/test.sh
index 7a1376548..6f65888a2 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -533,7 +533,12 @@ function dump_deps_of_module() {
   if ! module=$(run go mod edit -json | jq -r .Module.Path); then
     return 255
   fi
-  run go mod edit -json | jq -r '.Require[] | .Path+","+.Version+","+if .Indirect then " (indirect)" else "" end+",'"${module}"'"'
+  local require
+  require=$(run go mod edit -json | jq -r '.Require')
+  if [ "$require" == "null" ]; then
+    return 0
+  fi
+  echo "$require" | jq -r '.[] | .Path+","+.Version+","+if .Indirect then " (indirect)" else "" end+",'"${module}"'"'
 }
 
 # Checks whether dependencies are consistent across modules
@@ -587,7 +592,7 @@ function release_pass {
   UPGRADE_VER=$(git ls-remote --tags https://github.com/etcd-io/etcd.git \
     | grep --only-matching --perl-regexp "(?<=v)${binary_major}.${previous_minor}.[\d]+?(?=[\^])" \
     | sort --numeric-sort --key 1.5 | tail -1 | sed 's/^/v/')
-  log_callout "Found latest release: ${UPGRADE_VER}."
+  log_callout "Found previous minor version (v${binary_major}.${previous_minor}) latest release: ${UPGRADE_VER}."
 
   if [ -n "${MANUAL_VER:-}" ]; then
     # in case, we need to test against different version
diff --git a/scripts/test_lib.sh b/scripts/test_lib.sh
index 31ca532b0..0c5c2d28c 100644
--- a/scripts/test_lib.sh
+++ b/scripts/test_lib.sh
@@ -78,7 +78,7 @@ function run_for_module {
 }
 
 function module_dirs() {
-  echo "api pkg client/pkg client/internal/v2 client/v3 server etcdutl etcdctl tests tools/mod tools/rw-heatmaps tools/testgrid-analysis ."
+  echo "api pkg client/pkg client/v3 server etcdutl etcdctl tests tools/mod tools/rw-heatmaps tools/testgrid-analysis cache ."
 }
 
 # maybe_run [cmd...] runs given command depending on the DRY_RUN flag.
@@ -98,7 +98,6 @@ function modules() {
     "${ROOT_MODULE}/api/v3"
     "${ROOT_MODULE}/pkg/v3"
     "${ROOT_MODULE}/client/pkg/v3"
-    "${ROOT_MODULE}/client/v2"
     "${ROOT_MODULE}/client/v3"
     "${ROOT_MODULE}/server/v3"
     "${ROOT_MODULE}/etcdutl/v3"
diff --git a/security/README.md b/security/README.md
index 3129778ca..99193d1dd 100644
--- a/security/README.md
+++ b/security/README.md
@@ -6,7 +6,7 @@ Join the [etcd-dev](https://groups.google.com/g/etcd-dev) group for emails about
 
 We’re extremely grateful for security researchers and users that report vulnerabilities to the etcd Open Source Community. All reports are thoroughly investigated by a dedicated committee of community volunteers called [Product Security Committee](security-release-process.md#product-security-committee).
 
-To make a report, please email the private [etcd maintainers](mailto:etcd-maintainers@googlegroups.com) list with the security details and the details expected for [all etcd bug reports](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
+To make a report, please email the private [security@etcd.io](mailto:security@etcd.io) list with the security details and the details expected for [all etcd bug reports](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
 
 ### When Should I Report a Vulnerability?
 
@@ -43,6 +43,6 @@ This list provides actionable information regarding etcd security to multiple di
 
 ### Request to Join
 
-New membership requests are sent to [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
+New membership requests are sent to [security@etcd.io](mailto:security@etcd.io).
 
 File an issue [here](https://github.com/etcd-io/etcd/issues/new?template=distributors-application.md), filling in the criteria template.
diff --git a/security/email-templates.md b/security/email-templates.md
index 8d93f76d5..766613716 100644
--- a/security/email-templates.md
+++ b/security/email-templates.md
@@ -7,6 +7,7 @@ This is a collection of email templates to handle various situations the securit
 ```
 Subject: Upcoming security release of etcd $VERSION
 To: etcd-dev@googlegroups.com
+Cc: security@etcd-io
 Cc: etcd-maintainers@googlegroups.com
 
 Hello etcd Community,
@@ -34,6 +35,7 @@ $PERSON on behalf of the etcd Product Security Committee and maintainers
 ```
 Subject: Security release of etcd $VERSION is now available
 To: etcd-dev@googlegroups.com
+Cc: security@etcd-io
 Cc: etcd-maintainers@googlegroups.com
 
 Hello etcd Community,
diff --git a/security/security-release-process.md b/security/security-release-process.md
index 78fb55dd9..3dcb606a4 100644
--- a/security/security-release-process.md
+++ b/security/security-release-process.md
@@ -22,7 +22,7 @@ The PSC members will share various tasks as listed below:
 
 ### Contacting the Product Security Committee
 
-Contact the team by sending email to [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
+Contact the team by sending email to [security@etcd.io](mailto:security@etcd.io).
 
 ### Product Security Committee Membership
 
@@ -55,7 +55,7 @@ The etcd Community asks that all suspected vulnerabilities be privately and resp
 
 ### Public Disclosure Processes
 
-If anyone knows of a publicly disclosed security vulnerability please IMMEDIATELY email [etcd maintainers](mailto:etcd-maintainers@googlegroups.com) to inform the PSC about the vulnerability so they may start the patch, release, and communication process.
+If anyone knows of a publicly disclosed security vulnerability please IMMEDIATELY email [security@etcd.io](mailto:security@etcd.io) to inform the PSC about the vulnerability so they may start the patch, release, and communication process.
 
 If possible the PSC will ask the person making the public report if the issue can be handled via a private disclosure process. If the reporter denies the PSC will move swiftly with the fix and release process. In extreme cases GitHub can be asked to delete the issue but this generally isn't necessary and is unlikely to make a public disclosure less damaging.
 
@@ -90,7 +90,7 @@ If the CVSS score is under ~4.0
 
 Note: CVSS is convenient but imperfect. Ultimately, the PSC has discretion on classifying the severity of a vulnerability.
 
-The severity of the bug and related handling decisions must be discussed on the [etcd maintainers](mailto:etcd-maintainers@googlegroups.com) mailing list.
+The severity of the bug and related handling decisions must be discussed on the [security@etcd.io](mailto:security@etcd.io) mailing list.
 
 ### Fix Disclosure Process
 
diff --git a/server/LICENSE b/server/LICENSE
index d64569567..52fb9c29f 100644
--- a/server/LICENSE
+++ b/server/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/server/config/config.go b/server/config/config.go
index 8f57112b3..2d5513395 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -42,9 +42,7 @@ const (
 type ServerConfig struct {
 	Name string
 
-	DiscoveryURL   string
-	DiscoveryProxy string
-	DiscoveryCfg   v3discovery.DiscoveryConfig
+	DiscoveryCfg v3discovery.DiscoveryConfig
 
 	ClientURLs types.URLs
 	PeerURLs   types.URLs
@@ -220,8 +218,8 @@ func (c *ServerConfig) VerifyBootstrap() error {
 	if CheckDuplicateURL(c.InitialPeerURLsMap) {
 		return fmt.Errorf("initial cluster %s has duplicate url", c.InitialPeerURLsMap)
 	}
-	if c.InitialPeerURLsMap.String() == "" && c.DiscoveryURL == "" {
-		return fmt.Errorf("initial cluster unset and no discovery URL found")
+	if c.InitialPeerURLsMap.String() == "" && !c.ShouldDiscover() {
+		return fmt.Errorf("initial cluster unset and no discovery endpoints found")
 	}
 	return nil
 }
@@ -237,7 +235,7 @@ func (c *ServerConfig) VerifyJoinExisting() error {
 	if CheckDuplicateURL(c.InitialPeerURLsMap) {
 		return fmt.Errorf("initial cluster %s has duplicate url", c.InitialPeerURLsMap)
 	}
-	if c.DiscoveryURL != "" {
+	if c.ShouldDiscover() {
 		return fmt.Errorf("discovery URL should not be set when joining existing initial cluster")
 	}
 	return nil
@@ -315,7 +313,7 @@ func (c *ServerConfig) WALDir() string {
 func (c *ServerConfig) SnapDir() string { return filepath.Join(c.MemberDir(), "snap") }
 
 func (c *ServerConfig) ShouldDiscover() bool {
-	return c.DiscoveryURL != "" || len(c.DiscoveryCfg.Endpoints) > 0
+	return len(c.DiscoveryCfg.Endpoints) > 0
 }
 
 // ReqTimeout returns timeout for request to finish.
diff --git a/server/embed/config.go b/server/embed/config.go
index 5b4aceeb1..7b9c73234 100644
--- a/server/embed/config.go
+++ b/server/embed/config.go
@@ -121,7 +121,7 @@ const (
 
 var (
 	ErrConflictBootstrapFlags = fmt.Errorf("multiple discovery or bootstrap flags are set. " +
-		"Choose one of \"initial-cluster\", \"discovery\", \"discovery-endpoints\" or \"discovery-srv\"")
+		"Choose one of \"initial-cluster\", \"discovery-endpoints\" or \"discovery-srv\"")
 	ErrUnsetAdvertiseClientURLsFlag = fmt.Errorf("--advertise-client-urls is required when --listen-client-urls is set explicitly")
 	ErrLogRotationInvalidLogOutput  = fmt.Errorf("--log-outputs requires a single file path when --log-rotate-config-json is defined")
 
@@ -262,9 +262,7 @@ type Config struct {
 	ClusterState          string `json:"initial-cluster-state"`
 	DNSCluster            string `json:"discovery-srv"`
 	DNSClusterServiceName string `json:"discovery-srv-name"`
-	Dproxy                string `json:"discovery-proxy"`
 
-	Durl         string                      `json:"discovery"`
 	DiscoveryCfg v3discovery.DiscoveryConfig `json:"discovery-config"`
 
 	InitialCluster      string `json:"initial-cluster"`
@@ -656,8 +654,6 @@ func (cfg *Config) AddFlags(fs *flag.FlagSet) {
 		"List of this member's client URLs to advertise to the public.",
 	)
 
-	fs.StringVar(&cfg.Durl, "discovery", cfg.Durl, "Discovery URL used to bootstrap the cluster for v2 discovery. Will be deprecated in v3.7, and be decommissioned in v3.8.")
-
 	fs.Var(
 		flags.NewUniqueStringsValue(""),
 		"discovery-endpoints",
@@ -676,7 +672,6 @@ func (cfg *Config) AddFlags(fs *flag.FlagSet) {
 	fs.StringVar(&cfg.DiscoveryCfg.Auth.Username, "discovery-user", "", "V3 discovery: username[:password] for authentication (prompt if password is not supplied).")
 	fs.StringVar(&cfg.DiscoveryCfg.Auth.Password, "discovery-password", "", "V3 discovery: password for authentication (if this option is used, --user option shouldn't include password).")
 
-	fs.StringVar(&cfg.Dproxy, "discovery-proxy", cfg.Dproxy, "HTTP proxy to use for traffic to discovery service. Will be deprecated in v3.7, and be decommissioned in v3.8.")
 	fs.StringVar(&cfg.DNSCluster, "discovery-srv", cfg.DNSCluster, "DNS domain used to bootstrap initial cluster.")
 	fs.StringVar(&cfg.DNSClusterServiceName, "discovery-srv-name", cfg.DNSClusterServiceName, "Service name to query when using DNS discovery.")
 	fs.StringVar(&cfg.InitialCluster, "initial-cluster", cfg.InitialCluster, "Initial cluster configuration for bootstrapping.")
@@ -884,7 +879,7 @@ func (cfg *configYAML) configFromFile(path string) error {
 	}
 
 	// If a discovery or discovery-endpoints flag is set, clear default initial cluster set by InitialClusterFromName
-	if (cfg.Durl != "" || cfg.DNSCluster != "" || len(cfg.DiscoveryCfg.Endpoints) > 0) && cfg.InitialCluster == defaultInitialCluster {
+	if (cfg.DNSCluster != "" || len(cfg.DiscoveryCfg.Endpoints) > 0) && cfg.InitialCluster == defaultInitialCluster {
 		cfg.InitialCluster = ""
 	}
 	if cfg.ClusterState == "" {
@@ -963,11 +958,11 @@ func (cfg *Config) Validate() error {
 	}
 	if err := checkHostURLs(cfg.AdvertiseClientUrls); err != nil {
 		addrs := cfg.getAdvertiseClientURLs()
-		return fmt.Errorf(`--advertise-client-urls %q must be "host:port" (%w)`, strings.Join(addrs, ","), err)
+		return fmt.Errorf(`--advertise-client-urls %q must be in the format "host:port", "unix:/path/to/socket" or "unixs:/path/to/socket" (%w)`, strings.Join(addrs, ","), err)
 	}
 	// Check if conflicting flags are passed.
 	nSet := 0
-	for _, v := range []bool{cfg.Durl != "", cfg.InitialCluster != "", cfg.DNSCluster != "", len(cfg.DiscoveryCfg.Endpoints) > 0} {
+	for _, v := range []bool{cfg.InitialCluster != "", cfg.DNSCluster != "", len(cfg.DiscoveryCfg.Endpoints) > 0} {
 		if v {
 			nSet++
 		}
@@ -981,28 +976,18 @@ func (cfg *Config) Validate() error {
 		return ErrConflictBootstrapFlags
 	}
 
-	// Check if both v2 discovery and v3 discovery flags are passed.
-	v2discoveryFlagsExist := cfg.Dproxy != ""
-	v3discoveryFlagsExist := len(cfg.DiscoveryCfg.Endpoints) > 0 ||
-		cfg.DiscoveryCfg.Token != "" ||
-		cfg.DiscoveryCfg.Secure.Cert != "" ||
-		cfg.DiscoveryCfg.Secure.Key != "" ||
-		cfg.DiscoveryCfg.Secure.Cacert != "" ||
-		cfg.DiscoveryCfg.Auth.Username != "" ||
-		cfg.DiscoveryCfg.Auth.Password != ""
-
-	if v2discoveryFlagsExist && v3discoveryFlagsExist {
-		return errors.New("both v2 discovery settings (discovery, discovery-proxy) " +
-			"and v3 discovery settings (discovery-token, discovery-endpoints, discovery-cert, " +
-			"discovery-key, discovery-cacert, discovery-user, discovery-password) are set")
-	}
-
 	// If one of `discovery-token` and `discovery-endpoints` is provided,
 	// then the other one must be provided as well.
 	if (cfg.DiscoveryCfg.Token != "") != (len(cfg.DiscoveryCfg.Endpoints) > 0) {
 		return errors.New("both --discovery-token and --discovery-endpoints must be set")
 	}
 
+	for _, ep := range cfg.DiscoveryCfg.Endpoints {
+		if strings.TrimSpace(ep) == "" {
+			return errors.New("--discovery-endpoints must not contain empty endpoints")
+		}
+	}
+
 	if cfg.TickMs == 0 {
 		return fmt.Errorf("--heartbeat-interval must be >0 (set to %dms)", cfg.TickMs)
 	}
@@ -1086,13 +1071,6 @@ func (cfg *Config) Validate() error {
 func (cfg *Config) PeerURLsMapAndToken(which string) (urlsmap types.URLsMap, token string, err error) {
 	token = cfg.InitialClusterToken
 	switch {
-	case cfg.Durl != "":
-		urlsmap = types.URLsMap{}
-		// If using v2 discovery, generate a temporary cluster based on
-		// self's advertised peer URLs
-		urlsmap[cfg.Name] = cfg.AdvertisePeerUrls
-		token = cfg.Durl
-
 	case len(cfg.DiscoveryCfg.Endpoints) > 0:
 		urlsmap = types.URLsMap{}
 		// If using v3 discovery, generate a temporary cluster based on
@@ -1355,6 +1333,9 @@ func checkBindURLs(urls []url.URL) error {
 
 func checkHostURLs(urls []url.URL) error {
 	for _, url := range urls {
+		if url.Scheme == "unix" || url.Scheme == "unixs" {
+			continue
+		}
 		host, _, err := net.SplitHostPort(url.Host)
 		if err != nil {
 			return err
diff --git a/server/embed/etcd.go b/server/embed/etcd.go
index 6a5b2f1c3..67330d406 100644
--- a/server/embed/etcd.go
+++ b/server/embed/etcd.go
@@ -191,8 +191,6 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
 		MaxWALFiles:                       cfg.MaxWalFiles,
 		InitialPeerURLsMap:                urlsmap,
 		InitialClusterToken:               token,
-		DiscoveryURL:                      cfg.Durl,
-		DiscoveryProxy:                    cfg.Dproxy,
 		DiscoveryCfg:                      cfg.DiscoveryCfg,
 		NewCluster:                        cfg.IsNewCluster(),
 		PeerTLSInfo:                       cfg.PeerTLSInfo,
@@ -366,8 +364,6 @@ func print(lg *zap.Logger, ec Config, sc config.ServerConfig, memberInitialized
 		zap.String("auto-compaction-mode", sc.AutoCompactionMode),
 		zap.Duration("auto-compaction-retention", sc.AutoCompactionRetention),
 		zap.String("auto-compaction-interval", sc.AutoCompactionRetention.String()),
-		zap.String("discovery-url", sc.DiscoveryURL),
-		zap.String("discovery-proxy", sc.DiscoveryProxy),
 
 		zap.String("discovery-token", sc.DiscoveryCfg.Token),
 		zap.String("discovery-endpoints", strings.Join(sc.DiscoveryCfg.Endpoints, ",")),
diff --git a/server/etcdmain/config.go b/server/etcdmain/config.go
index 9968cae34..4c857987a 100644
--- a/server/etcdmain/config.go
+++ b/server/etcdmain/config.go
@@ -255,7 +255,7 @@ func (cfg *config) configFromCmdLine() error {
 	}
 
 	// disable default initial-cluster if discovery is set
-	if (cfg.ec.Durl != "" || cfg.ec.DNSCluster != "" || cfg.ec.DNSClusterServiceName != "" || len(cfg.ec.DiscoveryCfg.Endpoints) > 0) && !flags.IsSet(cfg.cf.flagSet, "initial-cluster") {
+	if (cfg.ec.DNSCluster != "" || cfg.ec.DNSClusterServiceName != "" || len(cfg.ec.DiscoveryCfg.Endpoints) > 0) && !flags.IsSet(cfg.cf.flagSet, "initial-cluster") {
 		cfg.ec.InitialCluster = ""
 	}
 
diff --git a/server/etcdmain/etcd.go b/server/etcdmain/etcd.go
index cb633e7e5..518ce8ff3 100644
--- a/server/etcdmain/etcd.go
+++ b/server/etcdmain/etcd.go
@@ -29,7 +29,6 @@ import (
 	"go.etcd.io/etcd/client/pkg/v3/types"
 	"go.etcd.io/etcd/pkg/v3/osutil"
 	"go.etcd.io/etcd/server/v3/embed"
-	"go.etcd.io/etcd/server/v3/etcdserver/api/v2discovery"
 	"go.etcd.io/etcd/server/v3/etcdserver/errors"
 )
 
@@ -131,38 +130,14 @@ func startEtcdOrProxyV2(args []string) {
 	if err != nil {
 		var derr *errors.DiscoveryError
 		if errorspkg.As(err, &derr) {
-			switch {
-			case errorspkg.Is(derr.Err, v2discovery.ErrDuplicateID):
-				lg.Warn(
-					"member has been registered with discovery service",
-					zap.String("name", cfg.ec.Name),
-					zap.String("discovery-token", cfg.ec.Durl),
-					zap.Error(derr.Err),
-				)
-				lg.Warn(
-					"but could not find valid cluster configuration",
-					zap.String("data-dir", cfg.ec.Dir),
-				)
-				lg.Warn("check data dir if previous bootstrap succeeded")
-				lg.Warn("or use a new discovery token if previous bootstrap failed")
-
-			case errorspkg.Is(derr.Err, v2discovery.ErrDuplicateName):
-				lg.Warn(
-					"member with duplicated name has already been registered",
-					zap.String("discovery-token", cfg.ec.Durl),
-					zap.Error(derr.Err),
-				)
-				lg.Warn("cURL the discovery token URL for details")
-				lg.Warn("do not reuse discovery token; generate a new one to bootstrap a cluster")
+			lg.Warn(
+				"failed to bootstrap; discovery token was already used",
+				zap.String("discovery-token", cfg.ec.DiscoveryCfg.Token),
+				zap.Strings("discovery-endpoints", cfg.ec.DiscoveryCfg.Endpoints),
+				zap.Error(err),
+			)
+			lg.Warn("do not reuse discovery token; generate a new one to bootstrap a cluster")
 
-			default:
-				lg.Warn(
-					"failed to bootstrap; discovery token was already used",
-					zap.String("discovery-token", cfg.ec.Durl),
-					zap.Error(err),
-				)
-				lg.Warn("do not reuse discovery token; generate a new one to bootstrap a cluster")
-			}
 			os.Exit(1)
 		}
 
@@ -174,8 +149,8 @@ func startEtcdOrProxyV2(args []string) {
 			if types.URLs(cfg.ec.AdvertisePeerUrls).String() == embed.DefaultInitialAdvertisePeerURLs {
 				lg.Warn("forgot to set --initial-advertise-peer-urls?")
 			}
-			if cfg.ec.InitialCluster == cfg.ec.InitialClusterFromName(cfg.ec.Name) && len(cfg.ec.Durl) == 0 && len(cfg.ec.DiscoveryCfg.Endpoints) == 0 {
-				lg.Warn("V2 discovery settings (i.e., --discovery) or v3 discovery settings (i.e., --discovery-token, --discovery-endpoints) are not set")
+			if cfg.ec.InitialCluster == cfg.ec.InitialClusterFromName(cfg.ec.Name) && len(cfg.ec.DiscoveryCfg.Endpoints) == 0 {
+				lg.Warn("V3 discovery settings (i.e., --discovery-token, --discovery-endpoints) are not set")
 			}
 			os.Exit(1)
 		}
diff --git a/server/etcdserver/api/cluster.go b/server/etcdserver/api/cluster.go
index f05997da5..cf483089d 100644
--- a/server/etcdserver/api/cluster.go
+++ b/server/etcdserver/api/cluster.go
@@ -15,10 +15,10 @@
 package api
 
 import (
+	"github.com/coreos/go-semver/semver"
+
 	"go.etcd.io/etcd/client/pkg/v3/types"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
-
-	"github.com/coreos/go-semver/semver"
 )
 
 // Cluster is an interface representing a collection of members in one etcd cluster.
diff --git a/server/etcdserver/api/membership/cluster.go b/server/etcdserver/api/membership/cluster.go
index 15cb40672..68369f60c 100644
--- a/server/etcdserver/api/membership/cluster.go
+++ b/server/etcdserver/api/membership/cluster.go
@@ -116,6 +116,7 @@ func NewCluster(lg *zap.Logger, opts ...ClusterOption) *RaftCluster {
 		removed:       make(map[types.ID]bool),
 		downgradeInfo: &serverversion.DowngradeInfo{Enabled: false},
 		maxLearners:   clOpts.maxLearners,
+		v2store:       v2store.New(),
 	}
 }
 
@@ -399,9 +400,7 @@ func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange, shouldAp
 func (c *RaftCluster) AddMember(m *Member, shouldApplyV3 ShouldApplyV3) {
 	c.Lock()
 	defer c.Unlock()
-	if c.v2store != nil {
-		mustSaveMemberToStore(c.lg, c.v2store, m)
-	}
+	mustSaveMemberToStore(c.lg, c.v2store, m)
 
 	if m.ID == c.localID {
 		setIsLearnerMetric(m)
@@ -437,9 +436,7 @@ func (c *RaftCluster) AddMember(m *Member, shouldApplyV3 ShouldApplyV3) {
 func (c *RaftCluster) RemoveMember(id types.ID, shouldApplyV3 ShouldApplyV3) {
 	c.Lock()
 	defer c.Unlock()
-	if c.v2store != nil {
-		mustDeleteMemberFromStore(c.lg, c.v2store, id)
-	}
+	mustDeleteMemberFromStore(c.lg, c.v2store, id)
 	if shouldApplyV3 {
 		c.be.MustDeleteMemberFromBackend(id)
 
@@ -481,9 +478,7 @@ func (c *RaftCluster) UpdateAttributes(id types.ID, attr Attributes, shouldApply
 
 	if m, ok := c.members[id]; ok {
 		m.Attributes = attr
-		if c.v2store != nil {
-			mustUpdateMemberAttrInStore(c.lg, c.v2store, m)
-		}
+		mustUpdateMemberAttrInStore(c.lg, c.v2store, m)
 		if shouldApplyV3 {
 			c.be.MustSaveMemberToBackend(m)
 		}
@@ -513,10 +508,16 @@ func (c *RaftCluster) PromoteMember(id types.ID, shouldApplyV3 ShouldApplyV3) {
 	c.Lock()
 	defer c.Unlock()
 
-	if c.v2store != nil {
+	if _, ok := c.members[id]; ok {
 		m := *(c.members[id])
 		m.RaftAttributes.IsLearner = false
 		mustUpdateMemberInStore(c.lg, c.v2store, &m)
+	} else {
+		c.lg.Info("Skipped promoting non-existent member in v2store",
+			zap.String("cluster-id", c.cid.String()),
+			zap.String("local-member-id", c.localID.String()),
+			zap.String("promoted-member-id", id.String()),
+		)
 	}
 
 	if id == c.localID {
@@ -547,11 +548,20 @@ func (c *RaftCluster) UpdateRaftAttributes(id types.ID, raftAttr RaftAttributes,
 	c.Lock()
 	defer c.Unlock()
 
-	if c.v2store != nil {
+	if _, ok := c.members[id]; ok {
 		m := *(c.members[id])
 		m.RaftAttributes = raftAttr
 		mustUpdateMemberInStore(c.lg, c.v2store, &m)
+	} else {
+		c.lg.Info("Skipped updating non-existent member in v2store",
+			zap.String("cluster-id", c.cid.String()),
+			zap.String("local-member-id", c.localID.String()),
+			zap.String("updated-remote-peer-id", id.String()),
+			zap.Strings("updated-remote-peer-urls", raftAttr.PeerURLs),
+			zap.Bool("updated-remote-peer-is-learner", raftAttr.IsLearner),
+		)
 	}
+
 	if shouldApplyV3 {
 		c.members[id].RaftAttributes = raftAttr
 		c.be.MustSaveMemberToBackend(c.members[id])
@@ -608,9 +618,8 @@ func (c *RaftCluster) SetVersion(ver *semver.Version, onSet func(*zap.Logger, *s
 	c.version = ver
 	sv := semver.Must(semver.NewVersion(version.Version))
 	serverversion.MustDetectDowngrade(c.lg, sv, c.version)
-	if c.v2store != nil {
-		mustSaveClusterVersionToStore(c.lg, c.v2store, ver)
-	}
+	mustSaveClusterVersionToStore(c.lg, c.v2store, ver)
+
 	if shouldApplyV3 {
 		c.be.MustSaveClusterVersionToBackend(ver)
 	}
diff --git a/server/etcdserver/api/v2discovery/discovery.go b/server/etcdserver/api/v2discovery/discovery.go
deleted file mode 100644
index 610ae5494..000000000
--- a/server/etcdserver/api/v2discovery/discovery.go
+++ /dev/null
@@ -1,417 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Package v2discovery provides an implementation of the cluster discovery that
-// is used by etcd with v2 client.
-package v2discovery
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"math"
-	"net/http"
-	"net/url"
-	"path"
-	"sort"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/jonboulle/clockwork"
-	"go.uber.org/zap"
-
-	"go.etcd.io/etcd/client/pkg/v3/transport"
-	"go.etcd.io/etcd/client/pkg/v3/types"
-	"go.etcd.io/etcd/client/v2"
-)
-
-var (
-	ErrInvalidURL           = errors.New("discovery: invalid URL")
-	ErrBadSizeKey           = errors.New("discovery: size key is bad")
-	ErrSizeNotFound         = errors.New("discovery: size key not found")
-	ErrTokenNotFound        = errors.New("discovery: token not found")
-	ErrDuplicateID          = errors.New("discovery: found duplicate id")
-	ErrDuplicateName        = errors.New("discovery: found duplicate name")
-	ErrFullCluster          = errors.New("discovery: cluster is full")
-	ErrTooManyRetries       = errors.New("discovery: too many retries")
-	ErrBadDiscoveryEndpoint = errors.New("discovery: bad discovery endpoint")
-)
-
-var (
-	// Number of retries discovery will attempt before giving up and erroring out.
-	nRetries             = uint(math.MaxUint32)
-	maxExpoentialRetries = uint(8)
-)
-
-// JoinCluster will connect to the discovery service at the given url, and
-// register the server represented by the given id and config to the cluster
-func JoinCluster(lg *zap.Logger, durl, dproxyurl string, id types.ID, config string) (string, error) {
-	d, err := newDiscovery(lg, durl, dproxyurl, id)
-	if err != nil {
-		return "", err
-	}
-	return d.joinCluster(config)
-}
-
-// GetCluster will connect to the discovery service at the given url and
-// retrieve a string describing the cluster
-func GetCluster(lg *zap.Logger, durl, dproxyurl string) (string, error) {
-	d, err := newDiscovery(lg, durl, dproxyurl, 0)
-	if err != nil {
-		return "", err
-	}
-	return d.getCluster()
-}
-
-type discovery struct {
-	lg      *zap.Logger
-	cluster string
-	id      types.ID
-	c       client.KeysAPI
-	retries uint
-	url     *url.URL
-
-	clock clockwork.Clock
-}
-
-// newProxyFunc builds a proxy function from the given string, which should
-// represent a URL that can be used as a proxy. It performs basic
-// sanitization of the URL and returns any error encountered.
-func newProxyFunc(lg *zap.Logger, proxy string) (func(*http.Request) (*url.URL, error), error) {
-	if lg == nil {
-		lg = zap.NewNop()
-	}
-	if proxy == "" {
-		return nil, nil
-	}
-	// Do a small amount of URL sanitization to help the user
-	// Derived from net/http.ProxyFromEnvironment
-	proxyURL, err := url.Parse(proxy)
-	if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
-		// proxy was bogus. Try prepending "http://" to it and
-		// see if that parses correctly. If not, we ignore the
-		// error and complain about the original one
-		var err2 error
-		proxyURL, err2 = url.Parse("http://" + proxy)
-		if err2 == nil {
-			err = nil
-		}
-	}
-	if err != nil {
-		return nil, fmt.Errorf("invalid proxy address %q: %w", proxy, err)
-	}
-
-	lg.Info("running proxy with discovery", zap.String("proxy-url", proxyURL.String()))
-	return http.ProxyURL(proxyURL), nil
-}
-
-func newDiscovery(lg *zap.Logger, durl, dproxyurl string, id types.ID) (*discovery, error) {
-	if lg == nil {
-		lg = zap.NewNop()
-	}
-	u, err := url.Parse(durl)
-	if err != nil {
-		return nil, err
-	}
-	token := u.Path
-	u.Path = ""
-	pf, err := newProxyFunc(lg, dproxyurl)
-	if err != nil {
-		return nil, err
-	}
-
-	// TODO: add ResponseHeaderTimeout back when watch on discovery service writes header early
-	tr, err := transport.NewTransport(transport.TLSInfo{}, 30*time.Second)
-	if err != nil {
-		return nil, err
-	}
-	tr.Proxy = pf
-	cfg := client.Config{
-		Transport: tr,
-		Endpoints: []string{u.String()},
-	}
-	c, err := client.New(cfg)
-	if err != nil {
-		return nil, err
-	}
-	dc := client.NewKeysAPIWithPrefix(c, "")
-	return &discovery{
-		lg:      lg,
-		cluster: token,
-		c:       dc,
-		id:      id,
-		url:     u,
-		clock:   clockwork.NewRealClock(),
-	}, nil
-}
-
-func (d *discovery) joinCluster(config string) (string, error) {
-	// fast path: if the cluster is full, return the error
-	// do not need to register to the cluster in this case.
-	if _, _, _, err := d.checkCluster(); err != nil {
-		return "", err
-	}
-
-	if err := d.createSelf(config); err != nil {
-		// Fails, even on a timeout, if createSelf times out.
-		// TODO(barakmich): Retrying the same node might want to succeed here
-		// (ie, createSelf should be idempotent for discovery).
-		return "", err
-	}
-
-	nodes, size, index, err := d.checkCluster()
-	if err != nil {
-		return "", err
-	}
-
-	all, err := d.waitNodes(nodes, size, index)
-	if err != nil {
-		return "", err
-	}
-
-	return nodesToCluster(all, size)
-}
-
-func (d *discovery) getCluster() (string, error) {
-	nodes, size, index, err := d.checkCluster()
-	if err != nil {
-		if errors.Is(err, ErrFullCluster) {
-			return nodesToCluster(nodes, size)
-		}
-		return "", err
-	}
-
-	all, err := d.waitNodes(nodes, size, index)
-	if err != nil {
-		return "", err
-	}
-	return nodesToCluster(all, size)
-}
-
-func (d *discovery) createSelf(contents string) error {
-	ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
-	resp, err := d.c.Create(ctx, d.selfKey(), contents)
-	cancel()
-	if err != nil {
-		var eerr client.Error
-		if errors.As(err, &eerr) && eerr.Code == client.ErrorCodeNodeExist {
-			return ErrDuplicateID
-		}
-		return err
-	}
-
-	// ensure self appears on the server we connected to
-	w := d.c.Watcher(d.selfKey(), &client.WatcherOptions{AfterIndex: resp.Node.CreatedIndex - 1})
-	_, err = w.Next(context.Background())
-	return err
-}
-
-func (d *discovery) checkCluster() ([]*client.Node, uint64, uint64, error) {
-	configKey := path.Join("/", d.cluster, "_config")
-	ctx, cancel := context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
-	// find cluster size
-	resp, err := d.c.Get(ctx, path.Join(configKey, "size"), nil)
-	cancel()
-	if err != nil {
-		var eerr *client.Error
-		if errors.As(err, &eerr) && eerr.Code == client.ErrorCodeKeyNotFound {
-			return nil, 0, 0, ErrSizeNotFound
-		}
-		if errors.Is(err, client.ErrInvalidJSON) {
-			return nil, 0, 0, ErrBadDiscoveryEndpoint
-		}
-		var ce *client.ClusterError
-		if errors.As(err, &ce) {
-			d.lg.Warn(
-				"failed to get from discovery server",
-				zap.String("discovery-url", d.url.String()),
-				zap.String("path", path.Join(configKey, "size")),
-				zap.Error(err),
-				zap.String("err-detail", ce.Detail()),
-			)
-			return d.checkClusterRetry()
-		}
-		return nil, 0, 0, err
-	}
-	size, err := strconv.ParseUint(resp.Node.Value, 10, 0)
-	if err != nil {
-		return nil, 0, 0, ErrBadSizeKey
-	}
-
-	ctx, cancel = context.WithTimeout(context.Background(), client.DefaultRequestTimeout)
-	resp, err = d.c.Get(ctx, d.cluster, nil)
-	cancel()
-	if err != nil {
-		var ce *client.ClusterError
-		if errors.As(err, &ce) {
-			d.lg.Warn(
-				"failed to get from discovery server",
-				zap.String("discovery-url", d.url.String()),
-				zap.String("path", d.cluster),
-				zap.Error(err),
-				zap.String("err-detail", ce.Detail()),
-			)
-			return d.checkClusterRetry()
-		}
-		return nil, 0, 0, err
-	}
-	var nodes []*client.Node
-	// append non-config keys to nodes
-	for _, n := range resp.Node.Nodes {
-		if path.Base(n.Key) != path.Base(configKey) {
-			nodes = append(nodes, n)
-		}
-	}
-
-	snodes := sortableNodes{nodes}
-	sort.Sort(snodes)
-
-	// find self position
-	for i := range nodes {
-		if path.Base(nodes[i].Key) == path.Base(d.selfKey()) {
-			break
-		}
-		if uint64(i) >= size-1 {
-			return nodes[:size], size, resp.Index, ErrFullCluster
-		}
-	}
-	return nodes, size, resp.Index, nil
-}
-
-func (d *discovery) logAndBackoffForRetry(step string) {
-	d.retries++
-	// logAndBackoffForRetry stops exponential backoff when the retries are more than maxExpoentialRetries and is set to a constant backoff afterward.
-	retries := d.retries
-	if retries > maxExpoentialRetries {
-		retries = maxExpoentialRetries
-	}
-	retryTimeInSecond := time.Duration(0x1<<retries) * time.Second
-	d.lg.Info(
-		"retry connecting to discovery service",
-		zap.String("url", d.url.String()),
-		zap.String("reason", step),
-		zap.Duration("backoff", retryTimeInSecond),
-	)
-	d.clock.Sleep(retryTimeInSecond)
-}
-
-func (d *discovery) checkClusterRetry() ([]*client.Node, uint64, uint64, error) {
-	if d.retries < nRetries {
-		d.logAndBackoffForRetry("cluster status check")
-		return d.checkCluster()
-	}
-	return nil, 0, 0, ErrTooManyRetries
-}
-
-func (d *discovery) waitNodesRetry() ([]*client.Node, error) {
-	if d.retries < nRetries {
-		d.logAndBackoffForRetry("waiting for other nodes")
-		nodes, n, index, err := d.checkCluster()
-		if err != nil {
-			return nil, err
-		}
-		return d.waitNodes(nodes, n, index)
-	}
-	return nil, ErrTooManyRetries
-}
-
-func (d *discovery) waitNodes(nodes []*client.Node, size uint64, index uint64) ([]*client.Node, error) {
-	if uint64(len(nodes)) > size {
-		nodes = nodes[:size]
-	}
-	// watch from the next index
-	w := d.c.Watcher(d.cluster, &client.WatcherOptions{AfterIndex: index, Recursive: true})
-	all := make([]*client.Node, len(nodes))
-	copy(all, nodes)
-	for _, n := range all {
-		if path.Base(n.Key) == path.Base(d.selfKey()) {
-			d.lg.Info(
-				"found self from discovery server",
-				zap.String("discovery-url", d.url.String()),
-				zap.String("self", path.Base(d.selfKey())),
-			)
-		} else {
-			d.lg.Info(
-				"found peer from discovery server",
-				zap.String("discovery-url", d.url.String()),
-				zap.String("peer", path.Base(n.Key)),
-			)
-		}
-	}
-
-	// wait for others
-	for uint64(len(all)) < size {
-		d.lg.Info(
-			"found peers from discovery server; waiting for more",
-			zap.String("discovery-url", d.url.String()),
-			zap.Int("found-peers", len(all)),
-			zap.Int("needed-peers", int(size-uint64(len(all)))),
-		)
-		resp, err := w.Next(context.Background())
-		if err != nil {
-			var ce *client.ClusterError
-			if errors.As(err, &ce) {
-				d.lg.Warn(
-					"error while waiting for peers",
-					zap.String("discovery-url", d.url.String()),
-					zap.Error(err),
-					zap.String("err-detail", ce.Detail()),
-				)
-				return d.waitNodesRetry()
-			}
-			return nil, err
-		}
-		d.lg.Info(
-			"found peer from discovery server",
-			zap.String("discovery-url", d.url.String()),
-			zap.String("peer", path.Base(resp.Node.Key)),
-		)
-		all = append(all, resp.Node)
-	}
-	d.lg.Info(
-		"found all needed peers from discovery server",
-		zap.String("discovery-url", d.url.String()),
-		zap.Int("found-peers", len(all)),
-	)
-	return all, nil
-}
-
-func (d *discovery) selfKey() string {
-	return path.Join("/", d.cluster, d.id.String())
-}
-
-func nodesToCluster(ns []*client.Node, size uint64) (string, error) {
-	s := make([]string, len(ns))
-	for i, n := range ns {
-		s[i] = n.Value
-	}
-	us := strings.Join(s, ",")
-	m, err := types.NewURLsMap(us)
-	if err != nil {
-		return us, ErrInvalidURL
-	}
-	if uint64(m.Len()) != size {
-		return us, ErrDuplicateName
-	}
-	return us, nil
-}
-
-type sortableNodes struct{ Nodes []*client.Node }
-
-func (ns sortableNodes) Len() int { return len(ns.Nodes) }
-func (ns sortableNodes) Less(i, j int) bool {
-	return ns.Nodes[i].CreatedIndex < ns.Nodes[j].CreatedIndex
-}
-func (ns sortableNodes) Swap(i, j int) { ns.Nodes[i], ns.Nodes[j] = ns.Nodes[j], ns.Nodes[i] }
diff --git a/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go b/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go
index 912149f5a..0f4f23d67 100644
--- a/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go
+++ b/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go
@@ -46,6 +46,9 @@ func request_Election_Campaign_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Campaign(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -70,6 +73,9 @@ func request_Election_Proclaim_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Proclaim(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -94,6 +100,9 @@ func request_Election_Leader_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Leader(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -118,6 +127,9 @@ func request_Election_Observe_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	stream, err := client.Observe(ctx, &protoReq)
 	if err != nil {
 		return nil, metadata, err
@@ -138,6 +150,9 @@ func request_Election_Resign_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Resign(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
diff --git a/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go b/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go
index 5efb75939..836214a0f 100644
--- a/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go
+++ b/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go
@@ -46,6 +46,9 @@ func request_Lock_Lock_0(ctx context.Context, marshaler runtime.Marshaler, clien
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Lock(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -70,6 +73,9 @@ func request_Lock_Unlock_0(ctx context.Context, marshaler runtime.Marshaler, cli
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Unlock(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
diff --git a/server/etcdserver/api/v3rpc/grpc.go b/server/etcdserver/api/v3rpc/grpc.go
index ed55e0357..efa151437 100644
--- a/server/etcdserver/api/v3rpc/grpc.go
+++ b/server/etcdserver/api/v3rpc/grpc.go
@@ -67,8 +67,7 @@ func Server(s *etcdserver.EtcdServer, tls *tls.Config, interceptor grpc.UnarySer
 	}
 
 	if s.Cfg.EnableDistributedTracing {
-		chainUnaryInterceptors = append(chainUnaryInterceptors, otelgrpc.UnaryServerInterceptor(s.Cfg.TracerOptions...))
-		chainStreamInterceptors = append(chainStreamInterceptors, otelgrpc.StreamServerInterceptor(s.Cfg.TracerOptions...))
+		opts = append(opts, grpc.StatsHandler(otelgrpc.NewServerHandler(s.Cfg.TracerOptions...)))
 	}
 
 	opts = append(opts, grpc.ChainUnaryInterceptor(chainUnaryInterceptors...))
diff --git a/server/etcdserver/api/v3rpc/watch.go b/server/etcdserver/api/v3rpc/watch.go
index b0a7e4a19..d4a5bc351 100644
--- a/server/etcdserver/api/v3rpc/watch.go
+++ b/server/etcdserver/api/v3rpc/watch.go
@@ -305,12 +305,7 @@ func (sws *serverWatchStream) recvLoop() error {
 
 			filters := FiltersFromRequest(creq)
 
-			wsrev := sws.watchStream.Rev()
-			rev := creq.StartRevision
-			if rev == 0 {
-				rev = wsrev + 1
-			}
-			id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, rev, filters...)
+			id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, creq.StartRevision, filters...)
 			if err == nil {
 				sws.mu.Lock()
 				if creq.ProgressNotify {
@@ -328,7 +323,7 @@ func (sws *serverWatchStream) recvLoop() error {
 			}
 
 			wr := &pb.WatchResponse{
-				Header:   sws.newResponseHeader(wsrev),
+				Header:   sws.newResponseHeader(sws.watchStream.Rev()),
 				WatchId:  int64(id),
 				Created:  true,
 				Canceled: err != nil,
diff --git a/server/etcdserver/bootstrap.go b/server/etcdserver/bootstrap.go
index 0d4e6ff96..c9354017d 100644
--- a/server/etcdserver/bootstrap.go
+++ b/server/etcdserver/bootstrap.go
@@ -37,7 +37,6 @@ import (
 	"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/snap"
-	"go.etcd.io/etcd/server/v3/etcdserver/api/v2discovery"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/v2store"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/v3discovery"
 	"go.etcd.io/etcd/server/v3/etcdserver/cindex"
@@ -87,7 +86,8 @@ func bootstrap(cfg config.ServerConfig) (b *bootstrappedServer, err error) {
 		if err = fileutil.IsDirWriteable(cfg.WALDir()); err != nil {
 			return nil, fmt.Errorf("cannot write to WAL directory: %w", err)
 		}
-		bwal = bootstrapWALFromSnapshot(cfg, backend.snapshot)
+		cfg.Logger.Info("Bootstrapping WAL from snapshot")
+		bwal = bootstrapWALFromSnapshot(cfg, backend.snapshot, backend.ci)
 	}
 
 	cfg.Logger.Info("bootstrapping cluster")
@@ -331,14 +331,8 @@ func bootstrapNewClusterNoWAL(cfg config.ServerConfig, prt http.RoundTripper) (*
 		return nil, fmt.Errorf("member %s has already been bootstrapped", m.ID)
 	}
 	if cfg.ShouldDiscover() {
-		var str string
-		if cfg.DiscoveryURL != "" {
-			cfg.Logger.Warn("V2 discovery is deprecated!")
-			str, err = v2discovery.JoinCluster(cfg.Logger, cfg.DiscoveryURL, cfg.DiscoveryProxy, m.ID, cfg.InitialPeerURLsMap.String())
-		} else {
-			cfg.Logger.Info("Bootstrapping cluster using v3 discovery.")
-			str, err = v3discovery.JoinCluster(cfg.Logger, &cfg.DiscoveryCfg, m.ID, cfg.InitialPeerURLsMap.String())
-		}
+		cfg.Logger.Info("Bootstrapping cluster using v3 discovery.")
+		str, err := v3discovery.JoinCluster(cfg.Logger, &cfg.DiscoveryCfg, m.ID, cfg.InitialPeerURLsMap.String())
 		if err != nil {
 			return nil, &servererrors.DiscoveryError{Op: "join", Err: err}
 		}
@@ -551,7 +545,7 @@ func (b *bootstrappedRaft) newRaftNode(ss *snap.Snapshotter, wal *wal.WAL, cl *m
 	)
 }
 
-func bootstrapWALFromSnapshot(cfg config.ServerConfig, snapshot *raftpb.Snapshot) *bootstrappedWAL {
+func bootstrapWALFromSnapshot(cfg config.ServerConfig, snapshot *raftpb.Snapshot, ci cindex.ConsistentIndexer) *bootstrappedWAL {
 	wal, st, ents, snap, meta := openWALFromSnapshot(cfg, snapshot)
 	bwal := &bootstrappedWAL{
 		lg:       cfg.Logger,
@@ -564,6 +558,19 @@ func bootstrapWALFromSnapshot(cfg config.ServerConfig, snapshot *raftpb.Snapshot
 	}
 
 	if cfg.ForceNewCluster {
+		consistentIndex := ci.ConsistentIndex()
+		oldCommitIndex := bwal.st.Commit
+		// If only `HardState.Commit` increases, HardState won't be persisted
+		// to disk, even though the committed entries might have already been
+		// applied. This can result in consistent_index > CommitIndex.
+		//
+		// When restarting etcd with `--force-new-cluster`, all uncommitted
+		// entries are dropped. To avoid losing entries that were actually
+		// committed, we reset Commit to max(HardState.Commit, consistent_index).
+		//
+		// See: https://github.com/etcd-io/raft/pull/300 for more details.
+		bwal.st.Commit = max(oldCommitIndex, consistentIndex)
+
 		// discard the previously uncommitted entries
 		bwal.ents = bwal.CommitedEntries()
 		entries := bwal.NewConfigChangeEntries()
@@ -573,6 +580,7 @@ func bootstrapWALFromSnapshot(cfg config.ServerConfig, snapshot *raftpb.Snapshot
 			"forcing restart member",
 			zap.String("cluster-id", meta.clusterID.String()),
 			zap.String("local-member-id", meta.nodeID.String()),
+			zap.Uint64("wal-commit-index", oldCommitIndex),
 			zap.Uint64("commit-index", bwal.st.Commit),
 		)
 	} else {
diff --git a/server/etcdserver/txn/range.go b/server/etcdserver/txn/range.go
index 8e9fef34a..5988691aa 100644
--- a/server/etcdserver/txn/range.go
+++ b/server/etcdserver/txn/range.go
@@ -43,9 +43,29 @@ func Range(ctx context.Context, lg *zap.Logger, kv mvcc.KV, r *pb.RangeRequest)
 func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *pb.RangeRequest) (*pb.RangeResponse, error) {
 	trace := traceutil.Get(ctx)
 
-	resp := &pb.RangeResponse{}
-	resp.Header = &pb.ResponseHeader{}
+	limit := rangeLimit(r)
+	ro := mvcc.RangeOptions{
+		Limit: limit,
+		Rev:   r.Revision,
+		Count: r.CountOnly,
+	}
+
+	rr, err := txnRead.Range(ctx, r.Key, mkGteRange(r.RangeEnd), ro)
+	if err != nil {
+		return nil, err
+	}
+
+	filterRangeResults(rr, r)
+	sortRangeResults(rr, r, lg)
+	trace.Step("filter and sort the key-value pairs")
+
+	resp := asembleRangeResponse(rr, r)
+	trace.Step("assemble the response")
+
+	return resp, nil
+}
 
+func rangeLimit(r *pb.RangeRequest) int64 {
 	limit := r.Limit
 	if r.SortOrder != pb.RangeRequest_NONE ||
 		r.MinModRevision != 0 || r.MaxModRevision != 0 ||
@@ -57,18 +77,10 @@ func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *
 		// fetch one extra for 'more' flag
 		limit = limit + 1
 	}
+	return limit
+}
 
-	ro := mvcc.RangeOptions{
-		Limit: limit,
-		Rev:   r.Revision,
-		Count: r.CountOnly,
-	}
-
-	rr, err := txnRead.Range(ctx, r.Key, mkGteRange(r.RangeEnd), ro)
-	if err != nil {
-		return nil, err
-	}
-
+func filterRangeResults(rr *mvcc.RangeResult, r *pb.RangeRequest) {
 	if r.MaxModRevision != 0 {
 		f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision > r.MaxModRevision }
 		pruneKVs(rr, f)
@@ -85,7 +97,9 @@ func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *
 		f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision < r.MinCreateRevision }
 		pruneKVs(rr, f)
 	}
+}
 
+func sortRangeResults(rr *mvcc.RangeResult, r *pb.RangeRequest, lg *zap.Logger) {
 	sortOrder := r.SortOrder
 	if r.SortTarget != pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_NONE {
 		// Since current mvcc.Range implementation returns results
@@ -121,12 +135,14 @@ func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *
 			sort.Sort(sort.Reverse(sorter))
 		}
 	}
+}
 
+func asembleRangeResponse(rr *mvcc.RangeResult, r *pb.RangeRequest) *pb.RangeResponse {
+	resp := &pb.RangeResponse{Header: &pb.ResponseHeader{}}
 	if r.Limit > 0 && len(rr.KVs) > int(r.Limit) {
 		rr.KVs = rr.KVs[:r.Limit]
 		resp.More = true
 	}
-	trace.Step("filter and sort the key-value pairs")
 	resp.Header.Revision = rr.Rev
 	resp.Count = int64(rr.Count)
 	resp.Kvs = make([]*mvccpb.KeyValue, len(rr.KVs))
@@ -136,8 +152,7 @@ func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *
 		}
 		resp.Kvs[i] = &rr.KVs[i]
 	}
-	trace.Step("assemble the response")
-	return resp, nil
+	return resp
 }
 
 func checkRange(rv mvcc.ReadView, req *pb.RangeRequest) error {
diff --git a/server/go.mod b/server/go.mod
index 8f161e957..49eb7ed2a 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/server/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/coreos/go-semver v0.3.1
@@ -15,8 +15,8 @@ require (
 	github.com/google/btree v1.1.3
 	github.com/google/go-cmp v0.7.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/jonboulle/clockwork v0.5.0
 	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.2
@@ -25,23 +25,22 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2
-	go.etcd.io/bbolt v1.4.0
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
-	go.etcd.io/etcd/client/v2 v2.306.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/raft/v3 v3.6.0
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
-	go.opentelemetry.io/otel v1.35.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
-	go.opentelemetry.io/otel/sdk v1.35.0
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0
+	go.opentelemetry.io/otel v1.37.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
+	go.opentelemetry.io/otel/sdk v1.37.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.38.0
-	golang.org/x/net v0.40.0
-	golang.org/x/time v0.11.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34
-	google.golang.org/grpc v1.72.1
+	golang.org/x/crypto v0.39.0
+	golang.org/x/net v0.41.0
+	golang.org/x/time v0.12.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822
+	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	sigs.k8s.io/yaml v1.4.0
@@ -49,10 +48,10 @@ require (
 
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
@@ -61,27 +60,25 @@ require (
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
-	go.opentelemetry.io/otel/trace v1.35.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 )
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
 	go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
-	go.etcd.io/etcd/client/v2 => ./../client/internal/v2
 	go.etcd.io/etcd/client/v3 => ../client/v3
 	go.etcd.io/etcd/pkg/v3 => ../pkg
 )
diff --git a/server/go.sum b/server/go.sum
index e8f31bb0c..ff6173ef9 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -29,8 +29,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -60,12 +60,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -94,8 +94,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -123,30 +123,30 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
-go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
-go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
+go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -159,8 +159,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -176,16 +176,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -196,10 +196,10 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
-golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -217,17 +217,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -241,7 +241,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/server/storage/mvcc/watchable_store.go b/server/storage/mvcc/watchable_store.go
index e309b5cfa..1996ee618 100644
--- a/server/storage/mvcc/watchable_store.go
+++ b/server/storage/mvcc/watchable_store.go
@@ -15,12 +15,14 @@
 package mvcc
 
 import (
+	"fmt"
 	"sync"
 	"time"
 
 	"go.uber.org/zap"
 
 	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/client/pkg/v3/verify"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/pkg/v3/traceutil"
 	"go.etcd.io/etcd/server/v3/lease"
@@ -122,12 +124,13 @@ func (s *watchableStore) NewWatchStream() WatchStream {
 
 func (s *watchableStore) watch(key, end []byte, startRev int64, id WatchID, ch chan<- WatchResponse, fcs ...FilterFunc) (*watcher, cancelFunc) {
 	wa := &watcher{
-		key:    key,
-		end:    end,
-		minRev: startRev,
-		id:     id,
-		ch:     ch,
-		fcs:    fcs,
+		key:      key,
+		end:      end,
+		startRev: startRev,
+		minRev:   startRev,
+		id:       id,
+		ch:       ch,
+		fcs:      fcs,
 	}
 
 	s.mu.Lock()
@@ -532,11 +535,15 @@ func (s *watchableStore) progressIfSync(watchers map[WatchID]*watcher, responseW
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
+	rev := s.rev()
 	// Any watcher unsynced?
 	for _, w := range watchers {
 		if _, ok := s.synced.watchers[w]; !ok {
 			return false
 		}
+		if rev < w.startRev {
+			return false
+		}
 	}
 
 	// If all watchers are synchronised, send out progress
@@ -545,7 +552,7 @@ func (s *watchableStore) progressIfSync(watchers map[WatchID]*watcher, responseW
 	// notification will be broadcasted client-side if required
 	// (see dispatchEvent in client/v3/watch.go)
 	for _, w := range watchers {
-		w.send(WatchResponse{WatchID: responseWatchID, Revision: s.rev()})
+		w.send(WatchResponse{WatchID: responseWatchID, Revision: rev})
 		return true
 	}
 	return true
@@ -572,6 +579,7 @@ type watcher struct {
 	// except when the watcher were to be moved from "synced" watcher group
 	restore bool
 
+	startRev int64
 	// minRev is the minimum revision update the watcher will accept
 	minRev int64
 	id     WatchID
@@ -602,6 +610,16 @@ func (w *watcher) send(wr WatchResponse) bool {
 		wr.Events = ne
 	}
 
+	verify.Verify(func() {
+		if w.startRev > 0 {
+			for _, ev := range wr.Events {
+				if ev.Kv.ModRevision < w.startRev {
+					panic(fmt.Sprintf("Event.ModRevision(%d) is less than the w.startRev(%d) for watchID: %d", ev.Kv.ModRevision, w.startRev, w.id))
+				}
+			}
+		}
+	})
+
 	// if all events are filtered out, we should send nothing.
 	if !progressEvent && len(wr.Events) == 0 {
 		return true
diff --git a/tools/.golangci.yaml b/tools/.golangci.yaml
index d5a87964e..41e7f9d45 100644
--- a/tools/.golangci.yaml
+++ b/tools/.golangci.yaml
@@ -1,6 +1,15 @@
 ---
 run:
   timeout: 30m
+formatters:
+  enable:
+    - gci
+  settings: # please keep this alphabetized
+    gci:
+      sections:
+        - standard
+        - default
+        - prefix(go.etcd.io)
 issues:
   max-same-issues: 0
   # Excluding configuration per-path, per-linter, per-text and per-source
@@ -36,8 +45,7 @@ linters:
     - unparam
     - unused
     - usestdlibvars
-    # Disabled in the meanwhile, it raises several issues with new Go 1.24 functions
-    # - usetesting
+    - usetesting
     - whitespace
 linters-settings: # please keep this alphabetized
   goimports:
diff --git a/tools/benchmark/cmd/lease.go b/tools/benchmark/cmd/lease.go
index 5ed3dbe4b..941c98828 100644
--- a/tools/benchmark/cmd/lease.go
+++ b/tools/benchmark/cmd/lease.go
@@ -40,14 +40,14 @@ func init() {
 	leaseKeepaliveCmd.Flags().IntVar(&leaseKeepaliveTotal, "total", 10000, "Total number of lease keepalive requests")
 }
 
-func leaseKeepaliveFunc(_ *cobra.Command, _ []string) {
+func leaseKeepaliveFunc(cmd *cobra.Command, _ []string) {
 	requests := make(chan struct{})
 	clients := mustCreateClients(totalClients, totalConns)
 
 	bar = pb.New(leaseKeepaliveTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c v3.Lease) {
diff --git a/tools/benchmark/cmd/mvcc-put.go b/tools/benchmark/cmd/mvcc-put.go
index 25cdb61d5..b6a6379c2 100644
--- a/tools/benchmark/cmd/mvcc-put.go
+++ b/tools/benchmark/cmd/mvcc-put.go
@@ -21,11 +21,11 @@ import (
 	"runtime/pprof"
 	"time"
 
+	"github.com/spf13/cobra"
+
 	"go.etcd.io/etcd/pkg/v3/report"
 	"go.etcd.io/etcd/pkg/v3/traceutil"
 	"go.etcd.io/etcd/server/v3/lease"
-
-	"github.com/spf13/cobra"
 )
 
 // mvccPutCmd represents a storage put performance benchmarking tool
@@ -69,7 +69,7 @@ func createBytesSlice(bytesN, sliceN int) [][]byte {
 	return rs
 }
 
-func mvccPutFunc(_ *cobra.Command, _ []string) {
+func mvccPutFunc(cmd *cobra.Command, _ []string) {
 	if cpuProfPath != "" {
 		f, err := os.Create(cpuProfPath)
 		if err != nil {
@@ -105,7 +105,7 @@ func mvccPutFunc(_ *cobra.Command, _ []string) {
 	vals := createBytesSlice(valueSize, mvccTotalRequests*nrTxnOps)
 
 	weight := float64(nrTxnOps)
-	r := newWeightedReport()
+	r := newWeightedReport(cmd.Name())
 	rrc := r.Results()
 
 	rc := r.Run()
diff --git a/tools/benchmark/cmd/mvcc.go b/tools/benchmark/cmd/mvcc.go
index baf2bf1a1..67e3f7400 100644
--- a/tools/benchmark/cmd/mvcc.go
+++ b/tools/benchmark/cmd/mvcc.go
@@ -18,13 +18,12 @@ import (
 	"os"
 	"time"
 
+	"github.com/spf13/cobra"
 	"go.uber.org/zap"
 
 	"go.etcd.io/etcd/server/v3/lease"
 	"go.etcd.io/etcd/server/v3/storage/backend"
 	"go.etcd.io/etcd/server/v3/storage/mvcc"
-
-	"github.com/spf13/cobra"
 )
 
 var (
diff --git a/tools/benchmark/cmd/put.go b/tools/benchmark/cmd/put.go
index 62b9a3dc8..7f66046f2 100644
--- a/tools/benchmark/cmd/put.go
+++ b/tools/benchmark/cmd/put.go
@@ -88,7 +88,7 @@ func putFunc(cmd *cobra.Command, _ []string) {
 	bar = pb.New(putTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/range.go b/tools/benchmark/cmd/range.go
index 111d69bd0..6bbed8ac3 100644
--- a/tools/benchmark/cmd/range.go
+++ b/tools/benchmark/cmd/range.go
@@ -86,7 +86,7 @@ func rangeFunc(cmd *cobra.Command, args []string) {
 	bar = pb.New(rangeTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/root.go b/tools/benchmark/cmd/root.go
index c85beb9db..95aadde05 100644
--- a/tools/benchmark/cmd/root.go
+++ b/tools/benchmark/cmd/root.go
@@ -55,6 +55,8 @@ var (
 
 	targetLeader     bool
 	autoSyncInterval time.Duration
+
+	generatePerfReport bool
 )
 
 func init() {
@@ -74,4 +76,6 @@ func init() {
 
 	RootCmd.PersistentFlags().BoolVar(&targetLeader, "target-leader", false, "connect only to the leader node")
 	RootCmd.PersistentFlags().DurationVar(&autoSyncInterval, "auto-sync-interval", time.Duration(0), "AutoSyncInterval is the interval to update endpoints with its latest members")
+
+	RootCmd.PersistentFlags().BoolVar(&generatePerfReport, "report-perfdash", false, "Generate benchmark report in perfdash format")
 }
diff --git a/tools/benchmark/cmd/stm.go b/tools/benchmark/cmd/stm.go
index d6dfba0d3..22dc88657 100644
--- a/tools/benchmark/cmd/stm.go
+++ b/tools/benchmark/cmd/stm.go
@@ -110,7 +110,7 @@ func stmFunc(cmd *cobra.Command, _ []string) {
 	bar = pb.New(stmTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go doSTM(clients[i], requests, r.Results())
diff --git a/tools/benchmark/cmd/txn_mixed.go b/tools/benchmark/cmd/txn_mixed.go
index ffc004ecf..bbd88d599 100644
--- a/tools/benchmark/cmd/txn_mixed.go
+++ b/tools/benchmark/cmd/txn_mixed.go
@@ -95,8 +95,8 @@ func mixedTxnFunc(cmd *cobra.Command, _ []string) {
 	bar = pb.New(mixedTxnTotal)
 	bar.Start()
 
-	reportRead := newReport()
-	reportWrite := newReport()
+	reportRead := newReport(cmd.Name() + "-read")
+	reportWrite := newReport(cmd.Name() + "-write")
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/txn_put.go b/tools/benchmark/cmd/txn_put.go
index d69293b83..5e2d9d7a4 100644
--- a/tools/benchmark/cmd/txn_put.go
+++ b/tools/benchmark/cmd/txn_put.go
@@ -55,7 +55,7 @@ func init() {
 	txnPutCmd.Flags().IntVar(&keySpaceSize, "key-space-size", 1, "Maximum possible keys")
 }
 
-func txnPutFunc(_ *cobra.Command, _ []string) {
+func txnPutFunc(cmd *cobra.Command, _ []string) {
 	if keySpaceSize <= 0 {
 		fmt.Fprintf(os.Stderr, "expected positive --key-space-size, got (%v)", keySpaceSize)
 		os.Exit(1)
@@ -78,7 +78,7 @@ func txnPutFunc(_ *cobra.Command, _ []string) {
 	bar = pb.New(txnPutTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/util.go b/tools/benchmark/cmd/util.go
index a6b999390..4233b8477 100644
--- a/tools/benchmark/cmd/util.go
+++ b/tools/benchmark/cmd/util.go
@@ -157,24 +157,24 @@ func mustRandBytes(n int) []byte {
 	return rb
 }
 
-func newReport() report.Report {
+func newReport(reportName string) report.Report {
 	p := "%4.4f"
 	if precise {
 		p = "%g"
 	}
 	if sample {
-		return report.NewReportSample(p)
+		return report.NewReportSample(p, reportName, generatePerfReport)
 	}
-	return report.NewReport(p)
+	return report.NewReport(p, reportName, generatePerfReport)
 }
 
-func newWeightedReport() report.Report {
+func newWeightedReport(reportName string) report.Report {
 	p := "%4.4f"
 	if precise {
 		p = "%g"
 	}
 	if sample {
-		return report.NewReportSample(p)
+		return report.NewReportSample(p, reportName, generatePerfReport)
 	}
-	return report.NewWeightedReport(report.NewReport(p), p)
+	return report.NewWeightedReport(report.NewReport(p, reportName, generatePerfReport), p, reportName, generatePerfReport)
 }
diff --git a/tools/benchmark/cmd/watch.go b/tools/benchmark/cmd/watch.go
index a52a34360..7b707bf96 100644
--- a/tools/benchmark/cmd/watch.go
+++ b/tools/benchmark/cmd/watch.go
@@ -117,7 +117,7 @@ func benchMakeWatches(clients []*clientv3.Client, wk *watchedKeys) {
 	bar = pb.New(watchStreams * watchWatchesPerStream)
 	bar.Start()
 
-	r := newReport()
+	r := newReport("watch-make")
 	rch := r.Results()
 
 	wg.Add(len(streams) + 1)
@@ -189,7 +189,7 @@ func benchPutWatches(clients []*clientv3.Client, wk *watchedKeys) {
 	bar = pb.New(eventsTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport("watch-put")
 
 	wg.Add(len(wk.watches))
 	nrRxed := int32(eventsTotal)
diff --git a/tools/benchmark/cmd/watch_get.go b/tools/benchmark/cmd/watch_get.go
index b9a8bc336..96abad665 100644
--- a/tools/benchmark/cmd/watch_get.go
+++ b/tools/benchmark/cmd/watch_get.go
@@ -49,7 +49,7 @@ func init() {
 	watchGetCmd.Flags().IntVar(&watchEvents, "events", 8, "Number of events per watcher")
 }
 
-func watchGetFunc(_ *cobra.Command, _ []string) {
+func watchGetFunc(cmd *cobra.Command, _ []string) {
 	clients := mustCreateClients(totalClients, totalConns)
 	getClient := mustCreateClients(1, 1)
 
@@ -75,7 +75,7 @@ func watchGetFunc(_ *cobra.Command, _ []string) {
 	bar.Start()
 
 	// report from trying to do serialized gets with concurrent watchers
-	r := newReport()
+	r := newReport(cmd.Name())
 	ctx, cancel := context.WithCancel(context.TODO())
 	f := func() {
 		defer close(r.Results())
diff --git a/tools/benchmark/cmd/watch_latency.go b/tools/benchmark/cmd/watch_latency.go
index 2e60860b4..4afe636d8 100644
--- a/tools/benchmark/cmd/watch_latency.go
+++ b/tools/benchmark/cmd/watch_latency.go
@@ -60,7 +60,7 @@ func init() {
 	watchLatencyCmd.Flags().IntVar(&watchLValueSize, "val-size", 32, "Value size of watch response")
 }
 
-func watchLatencyFunc(_ *cobra.Command, _ []string) {
+func watchLatencyFunc(cmd *cobra.Command, _ []string) {
 	key := string(mustRandBytes(watchLKeySize))
 	value := string(mustRandBytes(watchLValueSize))
 	wchs := setupWatchChannels(key)
@@ -93,9 +93,9 @@ func watchLatencyFunc(_ *cobra.Command, _ []string) {
 		}()
 	}
 
-	putReport := newReport()
+	putReport := newReport(cmd.Name() + "-put")
 	putReportResults := putReport.Run()
-	watchReport := newReport()
+	watchReport := newReport(cmd.Name() + "-watch")
 	watchReportResults := watchReport.Run()
 	for i := 0; i < watchLPutTotal; i++ {
 		// limit key put as per reqRate
diff --git a/tools/etcd-dump-db/README.md b/tools/etcd-dump-db/README.md
index b176096f0..e2375da05 100644
--- a/tools/etcd-dump-db/README.md
+++ b/tools/etcd-dump-db/README.md
@@ -2,6 +2,9 @@
 
 `etcd-dump-db` inspects etcd db files.
 
+> ⚠️ **Deprecated**: This tool has been deprecated.
+> Please use [etcdutl](https://github.com/etcd-io/etcd/tree/main/etcdutl) instead.
+
 ## Installation
 
 Install the tool by running the following command from the etcd source directory.
diff --git a/tools/etcd-dump-db/main.go b/tools/etcd-dump-db/main.go
index 6be6d83ac..530e64092 100644
--- a/tools/etcd-dump-db/main.go
+++ b/tools/etcd-dump-db/main.go
@@ -24,6 +24,8 @@ import (
 	"time"
 
 	"github.com/spf13/cobra"
+
+	"go.etcd.io/etcd/client/pkg/v3/fileutil"
 )
 
 var (
@@ -85,7 +87,7 @@ func listBucketCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 
@@ -106,7 +108,7 @@ func iterateBucketCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 	bucket := args[1]
@@ -124,7 +126,7 @@ func scanKeysCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 	startRev, err := strconv.ParseInt(args[1], 10, 64)
@@ -145,7 +147,7 @@ func getHashCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 
diff --git a/tools/etcd-dump-db/utils.go b/tools/etcd-dump-db/utils.go
index 184cb5181..35ba3d5c1 100644
--- a/tools/etcd-dump-db/utils.go
+++ b/tools/etcd-dump-db/utils.go
@@ -15,15 +15,9 @@
 package main
 
 import (
-	"os"
 	"unsafe"
 )
 
-func existFileOrDir(name string) bool {
-	_, err := os.Stat(name)
-	return err == nil
-}
-
 func unsafeAdd(base unsafe.Pointer, offset uintptr) unsafe.Pointer {
 	return unsafe.Pointer(uintptr(base) + offset)
 }
diff --git a/tools/etcd-dump-metrics/metrics.go b/tools/etcd-dump-metrics/metrics.go
index a1a40b185..f3332093b 100644
--- a/tools/etcd-dump-metrics/metrics.go
+++ b/tools/etcd-dump-metrics/metrics.go
@@ -22,9 +22,9 @@ import (
 	"strings"
 	"time"
 
-	"go.etcd.io/etcd/client/pkg/v3/transport"
-
 	"go.uber.org/zap"
+
+	"go.etcd.io/etcd/client/pkg/v3/transport"
 )
 
 func fetchMetrics(ep string) (lines []string, err error) {
diff --git a/tools/mod/go.mod b/tools/mod/go.mod
index 2a43fce42..ff3335842 100644
--- a/tools/mod/go.mod
+++ b/tools/mod/go.mod
@@ -2,22 +2,22 @@ module go.etcd.io/etcd/tools/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
-	github.com/alexfalkowski/gocovmerge v1.8.0
+	github.com/alexfalkowski/gocovmerge v1.11.0
 	github.com/appscodelabs/license-bill-of-materials v0.0.0-20220707232035-6018e0c5287c
 	github.com/chzchzchz/goword v0.0.0-20170907005317-a9744cb52b03
 	github.com/cloudflare/cfssl v1.6.5
 	github.com/gogo/protobuf v1.3.2
 	github.com/golangci/golangci-lint v1.64.8
 	github.com/google/addlicense v1.1.1
-	github.com/google/yamlfmt v0.16.0
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/google/yamlfmt v0.17.2
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	go.etcd.io/gofail v0.2.0
 	go.etcd.io/protodoc v0.0.0-20180829002748-484ab544e116
 	go.etcd.io/raft/v3 v3.6.0
-	gotest.tools/gotestsum v1.12.2
+	gotest.tools/gotestsum v1.12.3
 	gotest.tools/v3 v3.5.2
 	honnef.co/go/tools v0.6.1
 )
@@ -51,7 +51,6 @@ require (
 	github.com/blizzy78/varnamelen v0.8.0 // indirect
 	github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
 	github.com/bombsimon/wsl/v4 v4.5.0 // indirect
-	github.com/braydonk/yaml v0.9.0 // indirect
 	github.com/breml/bidichk v0.3.2 // indirect
 	github.com/breml/errchkjson v0.4.0 // indirect
 	github.com/butuzov/ireturn v0.3.1 // indirect
@@ -75,7 +74,7 @@ require (
 	github.com/fzipp/gocyclo v0.6.0 // indirect
 	github.com/ghostiam/protogetter v0.3.9 // indirect
 	github.com/go-critic/go-critic v0.12.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/go-toolsmith/astcast v1.1.0 // indirect
 	github.com/go-toolsmith/astcopy v1.1.0 // indirect
@@ -146,14 +145,16 @@ require (
 	github.com/nishanths/exhaustive v0.12.0 // indirect
 	github.com/nishanths/predeclared v0.2.2 // indirect
 	github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
-	github.com/olekukonko/tablewriter v0.0.5 // indirect
+	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
+	github.com/olekukonko/ll v0.0.8 // indirect
+	github.com/olekukonko/tablewriter v1.0.7 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/polyfloyd/go-errorlint v1.7.1 // indirect
 	github.com/prometheus/client_golang v1.22.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
 	github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
@@ -212,19 +213,19 @@ require (
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.38.0 // indirect
+	golang.org/x/crypto v0.39.0 // indirect
 	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
-	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
-	golang.org/x/sync v0.14.0 // indirect
+	golang.org/x/mod v0.25.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
+	golang.org/x/sync v0.15.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/term v0.32.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	golang.org/x/tools v0.33.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/grpc v1.72.1 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	golang.org/x/tools v0.34.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/grpc v1.73.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/tools/mod/go.sum b/tools/mod/go.sum
index a7ca568d5..4970480c7 100644
--- a/tools/mod/go.sum
+++ b/tools/mod/go.sum
@@ -36,8 +36,8 @@ github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsr
 github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
-github.com/alexfalkowski/gocovmerge v1.8.0 h1:N5e33J3oMAweMBsJTOv0GwOuAoD3CdR5NhNOt7aJSVs=
-github.com/alexfalkowski/gocovmerge v1.8.0/go.mod h1:RaTPWgDVJlySp1sgyUz5w+ZLh17fIptLDZD5100epAI=
+github.com/alexfalkowski/gocovmerge v1.11.0 h1:mHYRBKEBHxjTWveV6RCAnCAhF6l1evO9JYboZRvZmuU=
+github.com/alexfalkowski/gocovmerge v1.11.0/go.mod h1:gkfERgPiozeXq7FlJBQDmTeXo8FnrNxZwabB4uSGZ3M=
 github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=
 github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
 github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
@@ -65,8 +65,6 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR
 github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A=
 github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc=
-github.com/braydonk/yaml v0.9.0 h1:ewGMrVmEVpsm3VwXQDR388sLg5+aQ8Yihp6/hc4m+h4=
-github.com/braydonk/yaml v0.9.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4=
 github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs=
 github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos=
 github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk=
@@ -128,8 +126,8 @@ github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYF
 github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
 github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
 github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@@ -205,8 +203,8 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z
 github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
-github.com/google/yamlfmt v0.16.0 h1:5auoxqdx2CxOb022XGBElFFVH8uE/lAJDCWKRMq4mT8=
-github.com/google/yamlfmt v0.16.0/go.mod h1:/fF8jQmFopG3InQoWYG3gTORPXqLwNkcBqAT4UA4ab0=
+github.com/google/yamlfmt v0.17.2 h1:TkXxhmj7dnpmOnlWGOXog92Gs6MWcTZqnf3kuyp8yFQ=
+github.com/google/yamlfmt v0.17.2/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
 github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
 github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
 github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
@@ -222,8 +220,8 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW
 github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
 github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
 github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
 github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
 github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -301,7 +299,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
@@ -327,8 +324,12 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm
 github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
 github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=
 github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
+github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
+github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
 github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
 github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -356,8 +357,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=
@@ -539,8 +540,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
@@ -558,8 +559,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
+golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -581,8 +582,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -593,8 +594,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -655,8 +656,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
@@ -677,19 +678,19 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
-golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
@@ -707,8 +708,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/gotestsum v1.12.2 h1:eli4tu9Q2D/ogDsEGSr8XfQfl7mT0JsGOG6DFtUiZ/Q=
-gotest.tools/gotestsum v1.12.2/go.mod h1:kjRtCglPZVsSU0hFHX3M5VWBM6Y63emHuB14ER1/sow=
+gotest.tools/gotestsum v1.12.3 h1:jFwenGJ0RnPkuKh2VzAYl1mDOJgbhobBDeL2W1iEycs=
+gotest.tools/gotestsum v1.12.3/go.mod h1:Y1+e0Iig4xIRtdmYbEV7K7H6spnjc1fX4BOuUhWw2Wk=
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
diff --git a/tools/mod/tools.go b/tools/mod/tools.go
index ce9af7751..8a133579d 100644
--- a/tools/mod/tools.go
+++ b/tools/mod/tools.go
@@ -31,10 +31,11 @@ import (
 	_ "github.com/google/yamlfmt/cmd/yamlfmt"
 	_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
 	_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
-	_ "go.etcd.io/gofail"
-	_ "go.etcd.io/protodoc"
-	_ "go.etcd.io/raft/v3"
 	_ "gotest.tools/gotestsum"
 	_ "gotest.tools/v3"
 	_ "honnef.co/go/tools/cmd/staticcheck"
+
+	_ "go.etcd.io/gofail"
+	_ "go.etcd.io/protodoc"
+	_ "go.etcd.io/raft/v3"
 )
diff --git a/tools/proto-annotations/cmd/etcd_version.go b/tools/proto-annotations/cmd/etcd_version.go
index 86766466a..893e72659 100644
--- a/tools/proto-annotations/cmd/etcd_version.go
+++ b/tools/proto-annotations/cmd/etcd_version.go
@@ -17,6 +17,7 @@ package cmd
 import (
 	"fmt"
 	"io"
+	"slices"
 	"sort"
 	"strings"
 
@@ -72,10 +73,8 @@ func allEtcdVersionAnnotations() (annotations []etcdVersionAnnotation, err error
 	var fileAnnotations []etcdVersionAnnotation
 	protoregistry.GlobalFiles.RangeFiles(func(file protoreflect.FileDescriptor) bool {
 		pkg := string(file.Package())
-		for _, externalPkg := range externalPackages {
-			if pkg == externalPkg {
-				return true
-			}
+		if slices.Contains(externalPackages, pkg) {
+			return true
 		}
 		fileAnnotations, err = fileEtcdVersionAnnotations(file)
 		if err != nil {
diff --git a/tools/rw-heatmaps/go.mod b/tools/rw-heatmaps/go.mod
index 70d03fdc3..06d52f630 100644
--- a/tools/rw-heatmaps/go.mod
+++ b/tools/rw-heatmaps/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/rw-heatmaps/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/spf13/cobra v1.9.1
@@ -22,5 +22,5 @@ require (
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/image v0.18.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
+	golang.org/x/text v0.26.0 // indirect
 )
diff --git a/tools/rw-heatmaps/go.sum b/tools/rw-heatmaps/go.sum
index 3256affc3..a94e9aba2 100644
--- a/tools/rw-heatmaps/go.sum
+++ b/tools/rw-heatmaps/go.sum
@@ -53,8 +53,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
diff --git a/tools/testgrid-analysis/go.mod b/tools/testgrid-analysis/go.mod
index 4ac797991..fc3c407a3 100644
--- a/tools/testgrid-analysis/go.mod
+++ b/tools/testgrid-analysis/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/testgrid-analysis/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 require (
 	github.com/GoogleCloudPlatform/testgrid v0.0.173
@@ -12,15 +12,14 @@ require (
 )
 
 require (
-	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	go.opentelemetry.io/otel v1.35.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/grpc v1.72.1 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/grpc v1.73.0 // indirect
 )
diff --git a/tools/testgrid-analysis/go.sum b/tools/testgrid-analysis/go.sum
index 0aeacd1fb..233708159 100644
--- a/tools/testgrid-analysis/go.sum
+++ b/tools/testgrid-analysis/go.sum
@@ -863,8 +863,8 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@@ -1180,16 +1180,16 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
@@ -1336,8 +1336,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1516,8 +1516,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1841,8 +1841,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1888,8 +1888,8 @@ google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGO
 google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=



Test patch:
diff --git a/cache/cache_test.go b/cache/cache_test.go
new file mode 100644
index 000000000..3ecc1bc79
--- /dev/null
+++ b/cache/cache_test.go
@@ -0,0 +1,308 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/client/pkg/v3/types"
+	clientv3 "go.etcd.io/etcd/client/v3"
+	embed "go.etcd.io/etcd/server/v3/embed"
+)
+
+func TestWatchWithCache(t *testing.T) {
+	client := startEtcd(t)
+	cache, err := New(client, "/", WithHistoryWindowSize(32))
+	if err != nil {
+		t.Fatalf("New(...): %v", err)
+	}
+	t.Cleanup(cache.Close)
+	if err := cache.WaitReady(t.Context()); err != nil {
+		t.Fatalf("cache not ready: %v", err)
+	}
+	testWatch(t, client.KV, cache)
+}
+
+func TestWatchWithoutCache(t *testing.T) {
+	client := startEtcd(t)
+	testWatch(t, client.KV, client.Watcher)
+}
+
+func testWatch(t *testing.T, kv clientv3.KV, watcher Watcher) {
+	ctx := t.Context()
+	event1Put := &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/a"),
+			Value:          []byte("1"),
+			CreateRevision: 2,
+			ModRevision:    2,
+			Version:        1,
+		},
+	}
+	event2Put := &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/b"),
+			Value:          []byte("2"),
+			CreateRevision: 3,
+			ModRevision:    3,
+			Version:        1,
+		},
+	}
+	event3Delete := &clientv3.Event{
+		Type: clientv3.EventTypeDelete,
+		Kv: &mvccpb.KeyValue{
+			Key:         []byte("/a"),
+			ModRevision: 4,
+		},
+	}
+	tcs := []struct {
+		name       string
+		key        string
+		opts       []clientv3.OpOption
+		wantEvents []*clientv3.Event
+	}{
+		{
+			name:       "Watch all events",
+			key:        "/",
+			opts:       []clientv3.OpOption{clientv3.WithPrefix()},
+			wantEvents: []*clientv3.Event{event1Put, event2Put, event3Delete},
+		},
+	}
+	t.Log("Open test watchers")
+	watches := make([]clientv3.WatchChan, len(tcs))
+	for i, tc := range tcs {
+		watches[i] = watcher.Watch(ctx, tc.key, tc.opts...)
+	}
+	t.Log("Setup data")
+	if _, err := kv.Put(ctx, string(event1Put.Kv.Key), string(event1Put.Kv.Value)); err != nil {
+		t.Fatalf("Put: %v", err)
+	}
+	if _, err := kv.Put(ctx, string(event2Put.Kv.Key), string(event2Put.Kv.Value)); err != nil {
+		t.Fatalf("Put: %v", err)
+	}
+	if _, err := kv.Delete(ctx, string(event3Delete.Kv.Key)); err != nil {
+		t.Fatalf("Delete: %v", err)
+	}
+	t.Log("Validate")
+	for i, tc := range tcs {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			events, _ := readEvents(watches[i])
+			if diff := cmp.Diff(tc.wantEvents, events); diff != "" {
+				t.Errorf("unexpected events (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestLaggingWatcher(t *testing.T) {
+	const prefix = "/test/"
+	cli := startEtcd(t)
+
+	tests := []struct {
+		name       string
+		window     int
+		eventCount int
+		wantEvents int
+		wantClosed bool
+	}{
+		{
+			name:       "all event fit",
+			window:     10,
+			eventCount: 9,
+			wantEvents: 9,
+			wantClosed: false,
+		},
+		{
+			name:       "events fill window",
+			window:     10,
+			eventCount: 10,
+			wantEvents: 10,
+			wantClosed: false,
+		},
+		{
+			name:       "event fill pipeline",
+			window:     10,
+			eventCount: 11,
+			wantEvents: 11,
+			wantClosed: false,
+		},
+		{
+			name:       "pipeline overflow",
+			window:     10,
+			eventCount: 12,
+			wantEvents: 1,
+			wantClosed: true,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			cache, err := New(
+				cli, prefix,
+				WithHistoryWindowSize(tt.window),
+				WithPerWatcherBufferSize(0),
+				WithResyncInterval(10*time.Millisecond),
+			)
+			if err != nil {
+				t.Fatalf("New(...): %v", err)
+			}
+			defer cache.Close()
+
+			if err := cache.WaitReady(t.Context()); err != nil {
+				t.Fatalf("cache not ready: %v", err)
+			}
+			ch := cache.Watch(t.Context(), prefix, clientv3.WithPrefix())
+
+			generateEvents(t, cli, prefix, tt.eventCount)
+			gotEvents, ok := readEvents(ch)
+			closed := !ok
+
+			if tt.wantEvents != len(gotEvents) {
+				t.Errorf("gotEvents=%v, wantEvents=%v", len(gotEvents), tt.wantEvents)
+			}
+			if closed != tt.wantClosed {
+				t.Errorf("closed=%v, wantClosed=%v", closed, tt.wantClosed)
+			}
+		})
+	}
+}
+
+func TestRejectsUnsupportedWatch(t *testing.T) {
+	client := startEtcd(t)
+	ctx := t.Context()
+
+	cache, err := New(client, "")
+	if err != nil {
+		t.Fatalf("New(...): %v", err)
+	}
+	t.Cleanup(cache.Close)
+	if err := cache.WaitReady(ctx); err != nil {
+		t.Fatal(err)
+	}
+
+	tests := []struct {
+		name string
+		key  string
+		opts []clientv3.OpOption
+	}{
+		{
+			name: "non_zero_start_revision",
+			key:  "",
+			opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(123)},
+		},
+		{
+			name: "subprefix_watch",
+			key:  "foo/",
+			opts: []clientv3.OpOption{clientv3.WithPrefix()},
+		},
+		{
+			name: "single_key_watch",
+			key:  "foo/bar",
+			opts: nil, // exact-key watch (no WithPrefix)
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			watchCh := cache.Watch(ctx, tt.key, tt.opts...)
+			resp, ok := <-watchCh
+
+			if !ok || !resp.Canceled {
+				t.Errorf("expected canceled response, got %#v (closed=%v)", resp, !ok)
+			}
+		})
+	}
+}
+
+func startEtcd(t *testing.T) *clientv3.Client {
+	t.Helper()
+
+	cfg := embed.NewConfig()
+	cfg.Dir = t.TempDir()
+	cfg.LogLevel = "error"
+
+	newClient, _ := types.NewURLs([]string{"http://127.0.0.1:0"})
+	newPeer, _ := types.NewURLs([]string{"http://127.0.0.1:0"})
+	cfg.ListenClientUrls, cfg.AdvertiseClientUrls = newClient, newClient
+	cfg.ListenPeerUrls, cfg.AdvertisePeerUrls = newPeer, newPeer
+
+	cfg.InitialCluster = fmt.Sprintf("%s=%s", cfg.Name, cfg.AdvertisePeerUrls[0].String())
+
+	srv, err := embed.StartEtcd(cfg)
+	if err != nil {
+		t.Fatalf("start etcd: %v", err)
+	}
+	t.Cleanup(func() { srv.Close() })
+
+	select {
+	case <-srv.Server.ReadyNotify():
+	case <-time.After(10 * time.Second):
+		t.Fatalf("etcd ready timeout")
+	}
+	client, err := clientv3.New(clientv3.Config{
+		Endpoints:   []string{srv.Clients[0].Addr().String()},
+		DialTimeout: 5 * time.Second,
+	})
+	if err != nil {
+		t.Fatalf("new client: %v", err)
+	}
+	t.Cleanup(func() {
+		if err := client.Close(); err != nil {
+			t.Errorf("client.Close() error: %v", err)
+		}
+	})
+	return client
+}
+
+func generateEvents(t *testing.T, client *clientv3.Client, prefix string, n int) {
+	t.Helper()
+	for i := 0; i < n; i++ {
+		key := fmt.Sprintf("%s%d", prefix, i)
+		if _, err := client.Put(t.Context(), key, fmt.Sprintf("%d", i)); err != nil {
+			t.Fatalf("Put(%q): %v", key, err)
+		}
+	}
+}
+
+type Watcher interface {
+	Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan
+}
+
+func readEvents(watch clientv3.WatchChan) (events []*clientv3.Event, ok bool) {
+	deadline := time.After(200 * time.Millisecond)
+	for {
+		select {
+		case resp, ok := <-watch:
+			if !ok {
+				return events, false
+			}
+			events = append(events, resp.Events...)
+		case <-deadline:
+			return events, true
+		}
+	}
+}
diff --git a/cache/ringbuffer_test.go b/cache/ringbuffer_test.go
new file mode 100644
index 000000000..39f2ac916
--- /dev/null
+++ b/cache/ringbuffer_test.go
@@ -0,0 +1,233 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func TestPeekLatestAndOldest(t *testing.T) {
+	tests := []struct {
+		name          string
+		capacity      int
+		revs          []int64
+		wantLatestRev int64
+		wantOldestRev int64
+	}{
+		{
+			name:          "empty_buffer",
+			capacity:      4,
+			revs:          nil,
+			wantLatestRev: 0,
+			wantOldestRev: 0,
+		},
+		{
+			name:          "single_element",
+			capacity:      8,
+			revs:          []int64{1},
+			wantLatestRev: 1,
+			wantOldestRev: 1,
+		},
+		{
+			name:          "ascending_fill",
+			capacity:      4,
+			revs:          []int64{1, 2, 3, 4},
+			wantLatestRev: 4,
+			wantOldestRev: 1,
+		},
+		{
+			name:          "overwrite_when_full",
+			capacity:      3,
+			revs:          []int64{5, 6, 7, 8},
+			wantLatestRev: 8,
+			wantOldestRev: 6,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			rb := newRingBuffer(tt.capacity)
+			for _, r := range tt.revs {
+				rb.Append(event(r, "k"))
+			}
+
+			latestEvent := rb.PeekLatest()
+			oldestEvent := rb.PeekOldest()
+
+			gotLatestRev := int64(0)
+			if latestEvent != nil {
+				gotLatestRev = latestEvent.Kv.ModRevision
+			}
+			gotOldestRev := int64(0)
+			if oldestEvent != nil {
+				gotOldestRev = oldestEvent.Kv.ModRevision
+			}
+
+			if tt.wantLatestRev != gotLatestRev {
+				t.Fatalf("PeekLatest()=%d, want=%d", gotLatestRev, tt.wantLatestRev)
+			}
+			if tt.wantOldestRev != gotOldestRev {
+				t.Fatalf("PeekOldest()=%d, want=%d", gotOldestRev, tt.wantOldestRev)
+			}
+		})
+	}
+}
+
+func TestFilter(t *testing.T) {
+	tests := []struct {
+		name             string
+		capacity         int
+		revs             []int64
+		predicate        EntryPredicate
+		wantFilteredRevs []int64
+		wantLatestRev    int64
+	}{
+		{
+			name:             "no_filter",
+			capacity:         5,
+			revs:             []int64{1, 2, 3},
+			predicate:        AfterRev(0),
+			wantFilteredRevs: []int64{1, 2, 3},
+			wantLatestRev:    3,
+		},
+		{
+			name:             "partial_match",
+			capacity:         5,
+			revs:             []int64{10, 11, 12, 13},
+			predicate:        AfterRev(12),
+			wantFilteredRevs: []int64{12, 13},
+			wantLatestRev:    13,
+		},
+		{
+			name:             "filter_when_full",
+			capacity:         3,
+			revs:             []int64{20, 21, 22, 23, 24},
+			predicate:        AfterRev(23),
+			wantFilteredRevs: []int64{23, 24},
+			wantLatestRev:    24,
+		},
+		{
+			name:             "none_match",
+			capacity:         4,
+			revs:             []int64{30, 31},
+			predicate:        AfterRev(100),
+			wantFilteredRevs: []int64{},
+			wantLatestRev:    31,
+		},
+		{
+			name:             "nil_predicate",
+			capacity:         4,
+			revs:             []int64{1, 2, 3},
+			predicate:        nil,
+			wantFilteredRevs: []int64{1, 2, 3},
+			wantLatestRev:    3,
+		},
+		{
+			name:             "empty_buffer_nil_predicate",
+			capacity:         3,
+			revs:             nil,
+			predicate:        nil,
+			wantFilteredRevs: []int64{},
+			wantLatestRev:    0,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			rb := newRingBuffer(tt.capacity)
+			for _, r := range tt.revs {
+				rb.Append(event(r, "k"))
+			}
+
+			gotEvents := rb.Filter(tt.predicate)
+			gotRevs := make([]int64, len(gotEvents))
+			for i, ev := range gotEvents {
+				gotRevs[i] = ev.Kv.ModRevision
+			}
+
+			if diff := cmp.Diff(tt.wantFilteredRevs, gotRevs); diff != "" {
+				t.Fatalf("Filter() revisions mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestRebaseHistory(t *testing.T) {
+	tests := []struct {
+		name string
+		revs []int64
+	}{
+		{
+			name: "rebase_empty_buffer",
+			revs: nil,
+		},
+		{
+			name: "rebase_after_data",
+			revs: []int64{7, 8, 9},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			rb := newRingBuffer(4)
+			for _, r := range tt.revs {
+				rb.Append(event(r, "k"))
+			}
+
+			rb.RebaseHistory()
+
+			oldestRev := int64(0)
+			if ev := rb.PeekOldest(); ev != nil {
+				oldestRev = ev.Kv.ModRevision
+			}
+			latestRev := int64(0)
+			if ev := rb.PeekLatest(); ev != nil {
+				latestRev = ev.Kv.ModRevision
+			}
+
+			if oldestRev != 0 {
+				t.Fatalf("PeekOldest()=%d, want=%d", oldestRev, 0)
+			}
+			if latestRev != 0 {
+				t.Fatalf("PeekLatest()=%d, want=%d", latestRev, 0)
+			}
+
+			events := rb.Filter(nil)
+			if len(events) != 0 {
+				t.Fatalf("Filter() len(events)=%d, want=%d", len(events), 0)
+			}
+		})
+	}
+}
+
+func event(rev int64, key string) *clientv3.Event {
+	return &clientv3.Event{
+		Kv: &mvccpb.KeyValue{
+			Key:         []byte(key),
+			ModRevision: rev,
+		},
+	}
+}
diff --git a/client/internal/v2/client_test.go b/client/internal/v2/client_test.go
deleted file mode 100644
index 66e5671a3..000000000
--- a/client/internal/v2/client_test.go
+++ /dev/null
@@ -1,1036 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"context"
-	"errors"
-	"io"
-	"math/rand"
-	"net/http"
-	"net/url"
-	"reflect"
-	"sort"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"go.etcd.io/etcd/api/v3/version"
-	"go.etcd.io/etcd/client/pkg/v3/testutil"
-)
-
-type actionAssertingHTTPClient struct {
-	t   *testing.T
-	num int
-	act httpAction
-
-	resp http.Response
-	body []byte
-	err  error
-}
-
-func (a *actionAssertingHTTPClient) Do(_ context.Context, act httpAction) (*http.Response, []byte, error) {
-	if !reflect.DeepEqual(a.act, act) {
-		a.t.Errorf("#%d: unexpected httpAction: want=%#v got=%#v", a.num, a.act, act)
-	}
-
-	return &a.resp, a.body, a.err
-}
-
-type staticHTTPClient struct {
-	resp http.Response
-	body []byte
-	err  error
-}
-
-func (s *staticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
-	return &s.resp, s.body, s.err
-}
-
-type staticHTTPAction struct {
-	request http.Request
-}
-
-func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
-	return &s.request
-}
-
-type staticHTTPResponse struct {
-	resp http.Response
-	body []byte
-	err  error
-}
-
-type multiStaticHTTPClient struct {
-	responses []staticHTTPResponse
-	cur       int
-}
-
-func (s *multiStaticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
-	r := s.responses[s.cur]
-	s.cur++
-	return &r.resp, r.body, r.err
-}
-
-func newStaticHTTPClientFactory(responses []staticHTTPResponse) httpClientFactory {
-	var cur int
-	return func(url.URL) httpClient {
-		r := responses[cur]
-		cur++
-		return &staticHTTPClient{resp: r.resp, body: r.body, err: r.err}
-	}
-}
-
-type fakeTransport struct {
-	respchan     chan *http.Response
-	errchan      chan error
-	startCancel  chan struct{}
-	finishCancel chan struct{}
-}
-
-func newFakeTransport() *fakeTransport {
-	return &fakeTransport{
-		respchan:     make(chan *http.Response, 1),
-		errchan:      make(chan error, 1),
-		startCancel:  make(chan struct{}, 1),
-		finishCancel: make(chan struct{}, 1),
-	}
-}
-
-func (t *fakeTransport) CancelRequest(*http.Request) {
-	t.startCancel <- struct{}{}
-}
-
-type fakeAction struct{}
-
-func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
-	return &http.Request{}
-}
-
-func TestSimpleHTTPClientDoSuccess(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	tr.respchan <- &http.Response{
-		StatusCode: http.StatusTeapot,
-		Body:       io.NopCloser(strings.NewReader("foo")),
-	}
-
-	resp, body, err := c.Do(t.Context(), &fakeAction{})
-	require.NoErrorf(t, err, "incorrect error value")
-	wantCode := http.StatusTeapot
-	require.Equalf(t, wantCode, resp.StatusCode, "invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
-
-	wantBody := []byte("foo")
-	require.Truef(t, reflect.DeepEqual(wantBody, body), "invalid response body: want=%q got=%q", wantBody, body)
-}
-
-func TestSimpleHTTPClientDoError(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	tr.errchan <- errors.New("fixture")
-
-	_, _, err := c.Do(t.Context(), &fakeAction{})
-	assert.Errorf(t, err, "expected non-nil error, got nil")
-}
-
-type nilAction struct{}
-
-func (a *nilAction) HTTPRequest(url.URL) *http.Request {
-	return nil
-}
-
-func TestSimpleHTTPClientDoNilRequest(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	tr.errchan <- errors.New("fixture")
-
-	_, _, err := c.Do(t.Context(), &nilAction{})
-	require.ErrorIsf(t, err, ErrNoRequest, "expected non-nil error, got nil")
-}
-
-func TestSimpleHTTPClientDoCancelContext(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	tr.startCancel <- struct{}{}
-	tr.finishCancel <- struct{}{}
-
-	_, _, err := c.Do(t.Context(), &fakeAction{})
-	assert.Errorf(t, err, "expected non-nil error, got nil")
-}
-
-type checkableReadCloser struct {
-	io.ReadCloser
-	closed bool
-}
-
-func (c *checkableReadCloser) Close() error {
-	if !c.closed {
-		c.closed = true
-		return c.ReadCloser.Close()
-	}
-	return nil
-}
-
-func TestSimpleHTTPClientDoCancelContextResponseBodyClosed(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	// create an already-cancelled context
-	ctx, cancel := context.WithCancel(t.Context())
-	cancel()
-
-	body := &checkableReadCloser{ReadCloser: io.NopCloser(strings.NewReader("foo"))}
-	go func() {
-		// wait that simpleHTTPClient knows the context is already timed out,
-		// and calls CancelRequest
-		testutil.WaitSchedule()
-
-		// response is returned before cancel effects
-		tr.respchan <- &http.Response{Body: body}
-	}()
-
-	_, _, err := c.Do(ctx, &fakeAction{})
-	require.Errorf(t, err, "expected non-nil error, got nil")
-
-	require.Truef(t, body.closed, "expected closed body")
-}
-
-type blockingBody struct {
-	c chan struct{}
-}
-
-func (bb *blockingBody) Read(p []byte) (n int, err error) {
-	<-bb.c
-	return 0, errors.New("closed")
-}
-
-func (bb *blockingBody) Close() error {
-	close(bb.c)
-	return nil
-}
-
-func TestSimpleHTTPClientDoCancelContextResponseBodyClosedWithBlockingBody(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	ctx, cancel := context.WithCancel(t.Context())
-	body := &checkableReadCloser{ReadCloser: &blockingBody{c: make(chan struct{})}}
-	go func() {
-		tr.respchan <- &http.Response{Body: body}
-		time.Sleep(2 * time.Millisecond)
-		// cancel after the body is received
-		cancel()
-	}()
-
-	_, _, err := c.Do(ctx, &fakeAction{})
-	require.ErrorIsf(t, err, context.Canceled, "expected %+v, got %+v", context.Canceled, err)
-
-	require.Truef(t, body.closed, "expected closed body")
-}
-
-func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
-	tr := newFakeTransport()
-	c := &simpleHTTPClient{transport: tr}
-
-	donechan := make(chan struct{})
-	ctx, cancel := context.WithCancel(t.Context())
-	go func() {
-		c.Do(ctx, &fakeAction{})
-		close(donechan)
-	}()
-
-	// This should call CancelRequest and begin the cancellation process
-	cancel()
-
-	select {
-	case <-donechan:
-		t.Fatalf("simpleHTTPClient.Do should not have exited yet")
-	default:
-	}
-
-	tr.finishCancel <- struct{}{}
-
-	select {
-	case <-donechan:
-		// expected behavior
-		return
-	case <-time.After(time.Second):
-		t.Fatalf("simpleHTTPClient.Do did not exit within 1s")
-	}
-}
-
-func TestSimpleHTTPClientDoHeaderTimeout(t *testing.T) {
-	tr := newFakeTransport()
-	tr.finishCancel <- struct{}{}
-	c := &simpleHTTPClient{transport: tr, headerTimeout: time.Millisecond}
-
-	errc := make(chan error, 1)
-	go func() {
-		_, _, err := c.Do(t.Context(), &fakeAction{})
-		errc <- err
-	}()
-
-	select {
-	case err := <-errc:
-		require.Errorf(t, err, "expected non-nil error, got nil")
-	case <-time.After(time.Second):
-		t.Fatalf("unexpected timeout when waiting for the test to finish")
-	}
-}
-
-func TestHTTPClusterClientDo(t *testing.T) {
-	fakeErr := errors.New("fake")
-	fakeURL := url.URL{}
-	tests := []struct {
-		client *httpClusterClient
-		ctx    context.Context
-
-		wantCode   int
-		wantErr    error
-		wantPinned int
-	}{
-		// first good response short-circuits Do
-		{
-			client: &httpClusterClient{
-				endpoints: []url.URL{fakeURL, fakeURL},
-				clientFactory: newStaticHTTPClientFactory(
-					[]staticHTTPResponse{
-						{resp: http.Response{StatusCode: http.StatusTeapot}},
-						{err: fakeErr},
-					},
-				),
-				rand: rand.New(rand.NewSource(0)),
-			},
-			wantCode: http.StatusTeapot,
-		},
-
-		// fall through to good endpoint if err is arbitrary
-		{
-			client: &httpClusterClient{
-				endpoints: []url.URL{fakeURL, fakeURL},
-				clientFactory: newStaticHTTPClientFactory(
-					[]staticHTTPResponse{
-						{err: fakeErr},
-						{resp: http.Response{StatusCode: http.StatusTeapot}},
-					},
-				),
-				rand: rand.New(rand.NewSource(0)),
-			},
-			wantCode:   http.StatusTeapot,
-			wantPinned: 1,
-		},
-
-		// context.Canceled short-circuits Do
-		{
-			client: &httpClusterClient{
-				endpoints: []url.URL{fakeURL, fakeURL},
-				clientFactory: newStaticHTTPClientFactory(
-					[]staticHTTPResponse{
-						{err: context.Canceled},
-						{resp: http.Response{StatusCode: http.StatusTeapot}},
-					},
-				),
-				rand: rand.New(rand.NewSource(0)),
-			},
-			wantErr: context.Canceled,
-		},
-
-		// return err if there are no endpoints
-		{
-			client: &httpClusterClient{
-				endpoints:     []url.URL{},
-				clientFactory: newHTTPClientFactory(nil, nil, 0),
-				rand:          rand.New(rand.NewSource(0)),
-			},
-			wantErr: ErrNoEndpoints,
-		},
-
-		// return err if all endpoints return arbitrary errors
-		{
-			client: &httpClusterClient{
-				endpoints: []url.URL{fakeURL, fakeURL},
-				clientFactory: newStaticHTTPClientFactory(
-					[]staticHTTPResponse{
-						{err: fakeErr},
-						{err: fakeErr},
-					},
-				),
-				rand: rand.New(rand.NewSource(0)),
-			},
-			wantErr: &ClusterError{Errors: []error{fakeErr, fakeErr}},
-		},
-
-		// 500-level errors cause Do to fallthrough to next endpoint
-		{
-			client: &httpClusterClient{
-				endpoints: []url.URL{fakeURL, fakeURL},
-				clientFactory: newStaticHTTPClientFactory(
-					[]staticHTTPResponse{
-						{resp: http.Response{StatusCode: http.StatusBadGateway}},
-						{resp: http.Response{StatusCode: http.StatusTeapot}},
-					},
-				),
-				rand: rand.New(rand.NewSource(0)),
-			},
-			wantCode:   http.StatusTeapot,
-			wantPinned: 1,
-		},
-
-		// 500-level errors cause one shot Do to fallthrough to next endpoint
-		{
-			client: &httpClusterClient{
-				endpoints: []url.URL{fakeURL, fakeURL},
-				clientFactory: newStaticHTTPClientFactory(
-					[]staticHTTPResponse{
-						{resp: http.Response{StatusCode: http.StatusBadGateway}},
-						{resp: http.Response{StatusCode: http.StatusTeapot}},
-					},
-				),
-				rand: rand.New(rand.NewSource(0)),
-			},
-			ctx:        context.WithValue(t.Context(), &oneShotCtxValue, &oneShotCtxValue),
-			wantErr:    errors.New("client: etcd member  returns server error [Bad Gateway]"),
-			wantPinned: 1,
-		},
-	}
-
-	for i, tt := range tests {
-		if tt.ctx == nil {
-			tt.ctx = t.Context()
-		}
-		resp, _, err := tt.client.Do(tt.ctx, nil)
-		if (tt.wantErr == nil && !errors.Is(err, tt.wantErr)) || (tt.wantErr != nil && tt.wantErr.Error() != err.Error()) {
-			t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
-			continue
-		}
-
-		if resp == nil {
-			if tt.wantCode != 0 {
-				t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
-				continue
-			}
-		} else if resp.StatusCode != tt.wantCode {
-			t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
-			continue
-		}
-
-		if tt.client.pinned != tt.wantPinned {
-			t.Errorf("#%d: pinned=%d, want=%d", i, tt.client.pinned, tt.wantPinned)
-		}
-	}
-}
-
-func TestHTTPClusterClientDoDeadlineExceedContext(t *testing.T) {
-	fakeURL := url.URL{}
-	tr := newFakeTransport()
-	tr.finishCancel <- struct{}{}
-	c := &httpClusterClient{
-		clientFactory: newHTTPClientFactory(tr, DefaultCheckRedirect, 0),
-		endpoints:     []url.URL{fakeURL},
-	}
-
-	errc := make(chan error, 1)
-	go func() {
-		ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
-		defer cancel()
-		_, _, err := c.Do(ctx, &fakeAction{})
-		errc <- err
-	}()
-
-	select {
-	case err := <-errc:
-		if !errors.Is(err, context.DeadlineExceeded) {
-			t.Errorf("err = %+v, want %+v", err, context.DeadlineExceeded)
-		}
-	case <-time.After(time.Second):
-		t.Fatalf("unexpected timeout when waiting for request to deadline exceed")
-	}
-}
-
-type fakeCancelContext struct{}
-
-var errFakeCancelContext = errors.New("fake context canceled")
-
-func (f fakeCancelContext) Deadline() (time.Time, bool) { return time.Time{}, false }
-func (f fakeCancelContext) Done() <-chan struct{} {
-	d := make(chan struct{}, 1)
-	d <- struct{}{}
-	return d
-}
-func (f fakeCancelContext) Err() error        { return errFakeCancelContext }
-func (f fakeCancelContext) Value(key any) any { return 1 }
-
-func withTimeout(parent context.Context, _timeout time.Duration) (
-	ctx context.Context,
-	cancel context.CancelFunc,
-) {
-	ctx = parent
-	cancel = func() {
-		ctx = nil
-	}
-	return ctx, cancel
-}
-
-func TestHTTPClusterClientDoCanceledContext(t *testing.T) {
-	fakeURL := url.URL{}
-	tr := newFakeTransport()
-	tr.finishCancel <- struct{}{}
-	c := &httpClusterClient{
-		clientFactory: newHTTPClientFactory(tr, DefaultCheckRedirect, 0),
-		endpoints:     []url.URL{fakeURL},
-	}
-
-	errc := make(chan error, 1)
-	go func() {
-		ctx, cancel := withTimeout(fakeCancelContext{}, time.Millisecond)
-		cancel()
-		_, _, err := c.Do(ctx, &fakeAction{})
-		errc <- err
-	}()
-
-	select {
-	case err := <-errc:
-		if !errors.Is(err, errFakeCancelContext) {
-			t.Errorf("err = %+v, want %+v", err, errFakeCancelContext)
-		}
-	case <-time.After(time.Second):
-		t.Fatalf("unexpected timeout when waiting for request to fake context canceled")
-	}
-}
-
-func TestRedirectedHTTPAction(t *testing.T) {
-	act := &redirectedHTTPAction{
-		action: &staticHTTPAction{
-			request: http.Request{
-				Method: http.MethodDelete,
-				URL: &url.URL{
-					Scheme: "https",
-					Host:   "foo.example.com",
-					Path:   "/ping",
-				},
-			},
-		},
-		location: url.URL{
-			Scheme: "https",
-			Host:   "bar.example.com",
-			Path:   "/pong",
-		},
-	}
-
-	want := &http.Request{
-		Method: http.MethodDelete,
-		URL: &url.URL{
-			Scheme: "https",
-			Host:   "bar.example.com",
-			Path:   "/pong",
-		},
-	}
-	got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
-
-	require.Truef(t, reflect.DeepEqual(want, got), "HTTPRequest is %#v, want %#v", want, got)
-}
-
-func TestRedirectFollowingHTTPClient(t *testing.T) {
-	tests := []struct {
-		checkRedirect CheckRedirectFunc
-		client        httpClient
-		wantCode      int
-		wantErr       error
-	}{
-		// errors bubbled up
-		{
-			checkRedirect: func(int) error { return ErrTooManyRedirects },
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						err: errors.New("fail"),
-					},
-				},
-			},
-			wantErr: errors.New("fail"),
-		},
-
-		// no need to follow redirect if none given
-		{
-			checkRedirect: func(int) error { return ErrTooManyRedirects },
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTeapot,
-						},
-					},
-				},
-			},
-			wantCode: http.StatusTeapot,
-		},
-
-		// redirects if less than max
-		{
-			checkRedirect: func(via int) error {
-				if via >= 2 {
-					return ErrTooManyRedirects
-				}
-				return nil
-			},
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-							Header:     http.Header{"Location": []string{"http://example.com"}},
-						},
-					},
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTeapot,
-						},
-					},
-				},
-			},
-			wantCode: http.StatusTeapot,
-		},
-
-		// succeed after reaching max redirects
-		{
-			checkRedirect: func(via int) error {
-				if via >= 3 {
-					return ErrTooManyRedirects
-				}
-				return nil
-			},
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-							Header:     http.Header{"Location": []string{"http://example.com"}},
-						},
-					},
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-							Header:     http.Header{"Location": []string{"http://example.com"}},
-						},
-					},
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTeapot,
-						},
-					},
-				},
-			},
-			wantCode: http.StatusTeapot,
-		},
-
-		// fail if too many redirects
-		{
-			checkRedirect: func(via int) error {
-				if via >= 2 {
-					return ErrTooManyRedirects
-				}
-				return nil
-			},
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-							Header:     http.Header{"Location": []string{"http://example.com"}},
-						},
-					},
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-							Header:     http.Header{"Location": []string{"http://example.com"}},
-						},
-					},
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTeapot,
-						},
-					},
-				},
-			},
-			wantErr: ErrTooManyRedirects,
-		},
-
-		// fail if Location header not set
-		{
-			checkRedirect: func(int) error { return ErrTooManyRedirects },
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-						},
-					},
-				},
-			},
-			wantErr: errors.New("location header not set"),
-		},
-
-		// fail if Location header is invalid
-		{
-			checkRedirect: func(int) error { return ErrTooManyRedirects },
-			client: &multiStaticHTTPClient{
-				responses: []staticHTTPResponse{
-					{
-						resp: http.Response{
-							StatusCode: http.StatusTemporaryRedirect,
-							Header:     http.Header{"Location": []string{":"}},
-						},
-					},
-				},
-			},
-			//revive:disable:error-strings
-			wantErr: errors.New("location header not valid URL: :"),
-			//revive:enable:error-strings
-		},
-
-		// fail if redirects checked way too many times
-		{
-			checkRedirect: func(int) error { return nil },
-			client: &staticHTTPClient{
-				resp: http.Response{
-					StatusCode: http.StatusTemporaryRedirect,
-					Header:     http.Header{"Location": []string{"http://example.com"}},
-				},
-			},
-			wantErr: errTooManyRedirectChecks,
-		},
-	}
-
-	for i, tt := range tests {
-		client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect}
-		resp, _, err := client.Do(t.Context(), nil)
-		if (tt.wantErr == nil && !errors.Is(err, tt.wantErr)) || (tt.wantErr != nil && tt.wantErr.Error() != err.Error()) {
-			t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
-			continue
-		}
-
-		if resp == nil {
-			if tt.wantCode != 0 {
-				t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
-			}
-			continue
-		}
-
-		if resp.StatusCode != tt.wantCode {
-			t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
-			continue
-		}
-	}
-}
-
-func TestDefaultCheckRedirect(t *testing.T) {
-	tests := []struct {
-		num int
-		err error
-	}{
-		{0, nil},
-		{5, nil},
-		{10, nil},
-		{11, ErrTooManyRedirects},
-		{29, ErrTooManyRedirects},
-	}
-
-	for i, tt := range tests {
-		err := DefaultCheckRedirect(tt.num)
-		if !reflect.DeepEqual(tt.err, err) {
-			t.Errorf("#%d: want=%#v got=%#v", i, tt.err, err)
-		}
-	}
-}
-
-func TestHTTPClusterClientSync(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-
-	want := []string{"http://127.0.0.1:2379"}
-	got := hc.Endpoints()
-	require.Truef(t, reflect.DeepEqual(want, got), "incorrect endpoints: want=%#v got=%#v", want, got)
-
-	err = hc.Sync(t.Context())
-	require.NoErrorf(t, err, "unexpected error during Sync: %#v", err)
-
-	want = []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"}
-	got = hc.Endpoints()
-	sort.Strings(got)
-	require.Truef(t, reflect.DeepEqual(want, got), "incorrect endpoints post-Sync: want=%#v got=%#v", want, got)
-
-	err = hc.SetEndpoints([]string{"http://127.0.0.1:4009"})
-	require.NoErrorf(t, err, "unexpected error during reset: %#v", err)
-
-	want = []string{"http://127.0.0.1:4009"}
-	got = hc.Endpoints()
-	require.Truef(t, reflect.DeepEqual(want, got), "incorrect endpoints post-reset: want=%#v got=%#v", want, got)
-}
-
-func TestHTTPClusterClientSyncFail(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{err: errors.New("fail")},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-
-	want := []string{"http://127.0.0.1:2379"}
-	got := hc.Endpoints()
-	require.Truef(t, reflect.DeepEqual(want, got), "incorrect endpoints: want=%#v got=%#v", want, got)
-
-	err = hc.Sync(t.Context())
-	require.Errorf(t, err, "got nil error during Sync")
-
-	got = hc.Endpoints()
-	require.Truef(t, reflect.DeepEqual(want, got), "incorrect endpoints after failed Sync: want=%#v got=%#v", want, got)
-}
-
-func TestHTTPClusterClientAutoSyncCancelContext(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-
-	ctx, cancel := context.WithCancel(t.Context())
-	cancel()
-
-	err = hc.AutoSync(ctx, time.Hour)
-	require.ErrorIsf(t, err, context.Canceled, "incorrect error value: want=%v got=%v", context.Canceled, err)
-}
-
-func TestHTTPClusterClientAutoSyncFail(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{err: errors.New("fail")},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-
-	err = hc.AutoSync(t.Context(), time.Hour)
-	require.Truef(t, strings.HasPrefix(err.Error(), ErrClusterUnavailable.Error()), "incorrect error value: want=%v got=%v", ErrClusterUnavailable, err)
-}
-
-func TestHTTPClusterClientGetVersion(t *testing.T) {
-	body := []byte(`{"etcdserver":"2.3.2","etcdcluster":"2.3.0"}`)
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Length": []string{"44"}}},
-			body: body,
-		},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-
-	actual, err := hc.GetVersion(t.Context())
-	if err != nil {
-		t.Errorf("non-nil error: %#v", err)
-	}
-	expected := version.Versions{Server: "2.3.2", Cluster: "2.3.0"}
-	if !reflect.DeepEqual(&expected, actual) {
-		t.Errorf("incorrect Response: want=%#v got=%#v", expected, actual)
-	}
-}
-
-// TestHTTPClusterClientSyncPinEndpoint tests that Sync() pins the endpoint when
-// it gets the exactly same member list as before.
-func TestHTTPClusterClientSyncPinEndpoint(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-	pinnedEndpoint := hc.endpoints[hc.pinned]
-
-	for i := 0; i < 3; i++ {
-		err = hc.Sync(t.Context())
-		require.NoErrorf(t, err, "#%d: unexpected error during Sync", i)
-
-		if g := hc.endpoints[hc.pinned]; g != pinnedEndpoint {
-			t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, pinnedEndpoint)
-		}
-	}
-}
-
-// TestHTTPClusterClientSyncUnpinEndpoint tests that Sync() unpins the endpoint when
-// it gets a different member list than before.
-func TestHTTPClusterClientSyncUnpinEndpoint(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-	}
-	err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
-	require.NoErrorf(t, err, "unexpected error during setup")
-	wants := []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}
-
-	for i := 0; i < 3; i++ {
-		err = hc.Sync(t.Context())
-		require.NoErrorf(t, err, "#%d: unexpected error during Sync", i)
-
-		if g := hc.endpoints[hc.pinned]; g.String() != wants[i] {
-			t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, wants[i])
-		}
-	}
-}
-
-// TestHTTPClusterClientSyncPinLeaderEndpoint tests that Sync() pins the leader
-// when the selection mode is EndpointSelectionPrioritizeLeader
-func TestHTTPClusterClientSyncPinLeaderEndpoint(t *testing.T) {
-	cf := newStaticHTTPClientFactory([]staticHTTPResponse{
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-		{
-			resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
-			body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}`),
-		},
-	})
-
-	hc := &httpClusterClient{
-		clientFactory: cf,
-		rand:          rand.New(rand.NewSource(0)),
-		selectionMode: EndpointSelectionPrioritizeLeader,
-		endpoints:     []url.URL{{}}, // Need somewhere to pretend to send to initially
-	}
-
-	wants := []string{"http://127.0.0.1:4003", "http://127.0.0.1:4002"}
-
-	for i, want := range wants {
-		err := hc.Sync(t.Context())
-		require.NoErrorf(t, err, "#%d: unexpected error during Sync", i)
-
-		pinned := hc.endpoints[hc.pinned].String()
-		if pinned != want {
-			t.Errorf("#%d: pinned endpoint = %v, want %v", i, pinned, want)
-		}
-	}
-}
-
-func TestHTTPClusterClientResetFail(t *testing.T) {
-	tests := [][]string{
-		// need at least one endpoint
-		{},
-
-		// urls must be valid
-		{":"},
-	}
-
-	for i, tt := range tests {
-		hc := &httpClusterClient{rand: rand.New(rand.NewSource(0))}
-		err := hc.SetEndpoints(tt)
-		if err == nil {
-			t.Errorf("#%d: expected non-nil error", i)
-		}
-	}
-}
-
-func TestHTTPClusterClientResetPinRandom(t *testing.T) {
-	round := 2000
-	pinNum := 0
-	for i := 0; i < round; i++ {
-		hc := &httpClusterClient{rand: rand.New(rand.NewSource(int64(i)))}
-		err := hc.SetEndpoints([]string{"http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"})
-		require.NoErrorf(t, err, "#%d: reset error", i)
-		if hc.endpoints[hc.pinned].String() == "http://127.0.0.1:4001" {
-			pinNum++
-		}
-	}
-
-	min := 1.0/3.0 - 0.05
-	max := 1.0/3.0 + 0.05
-	if ratio := float64(pinNum) / float64(round); ratio > max || ratio < min {
-		t.Errorf("pinned ratio = %v, want [%v, %v]", ratio, min, max)
-	}
-}
diff --git a/client/internal/v2/fake_transport_test.go b/client/internal/v2/fake_transport_test.go
deleted file mode 100644
index 7a725c17c..000000000
--- a/client/internal/v2/fake_transport_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"errors"
-	"net/http"
-)
-
-func (t *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
-	select {
-	case resp := <-t.respchan:
-		return resp, nil
-	case err := <-t.errchan:
-		return nil, err
-	case <-t.startCancel:
-	case <-req.Cancel:
-	}
-	select {
-	// this simulates that the request is finished before cancel effects
-	case resp := <-t.respchan:
-		return resp, nil
-	// wait on finishCancel to simulate taking some amount of
-	// time while calling CancelRequest
-	case <-t.finishCancel:
-		return nil, errors.New("cancelled")
-	}
-}
diff --git a/client/internal/v2/keys_bench_test.go b/client/internal/v2/keys_bench_test.go
deleted file mode 100644
index e408e6ce9..000000000
--- a/client/internal/v2/keys_bench_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"encoding/json"
-	"net/http"
-	"reflect"
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func createTestNode(size int) *Node {
-	return &Node{
-		Key:           strings.Repeat("a", 30),
-		Value:         strings.Repeat("a", size),
-		CreatedIndex:  123456,
-		ModifiedIndex: 123456,
-		TTL:           123456789,
-	}
-}
-
-func createTestNodeWithChildren(children, size int) *Node {
-	node := createTestNode(size)
-	for i := 0; i < children; i++ {
-		node.Nodes = append(node.Nodes, createTestNode(size))
-	}
-	return node
-}
-
-func createTestResponse(children, size int) *Response {
-	return &Response{
-		Action:   "aaaaa",
-		Node:     createTestNodeWithChildren(children, size),
-		PrevNode: nil,
-	}
-}
-
-func benchmarkResponseUnmarshalling(b *testing.B, children, size int) {
-	b.Helper()
-	header := http.Header{}
-	header.Add("X-Etcd-Index", "123456")
-	response := createTestResponse(children, size)
-	body, err := json.Marshal(response)
-	require.NoError(b, err)
-
-	b.ResetTimer()
-	newResponse := new(Response)
-	for i := 0; i < b.N; i++ {
-		if newResponse, err = unmarshalSuccessfulKeysResponse(header, body); err != nil {
-			b.Errorf("error unmarshalling response (%v)", err)
-		}
-	}
-	if !reflect.DeepEqual(response.Node, newResponse.Node) {
-		b.Errorf("Unexpected difference in a parsed response: \n%+v\n%+v", response, newResponse)
-	}
-}
-
-func BenchmarkSmallResponseUnmarshal(b *testing.B) {
-	benchmarkResponseUnmarshalling(b, 30, 20)
-}
-
-func BenchmarkManySmallResponseUnmarshal(b *testing.B) {
-	benchmarkResponseUnmarshalling(b, 3000, 20)
-}
-
-func BenchmarkMediumResponseUnmarshal(b *testing.B) {
-	benchmarkResponseUnmarshalling(b, 300, 200)
-}
-
-func BenchmarkLargeResponseUnmarshal(b *testing.B) {
-	benchmarkResponseUnmarshalling(b, 3000, 2000)
-}
diff --git a/client/internal/v2/keys_test.go b/client/internal/v2/keys_test.go
deleted file mode 100644
index 7bc264a7b..000000000
--- a/client/internal/v2/keys_test.go
+++ /dev/null
@@ -1,1429 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"errors"
-	"fmt"
-	"io"
-	"net/http"
-	"net/url"
-	"reflect"
-	"testing"
-	"time"
-)
-
-func TestV2KeysURLHelper(t *testing.T) {
-	tests := []struct {
-		endpoint url.URL
-		prefix   string
-		key      string
-		want     url.URL
-	}{
-		// key is empty, no problem
-		{
-			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
-			prefix:   "",
-			key:      "",
-			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
-		},
-
-		// key is joined to path
-		{
-			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
-			prefix:   "",
-			key:      "/foo/bar",
-			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar"},
-		},
-
-		// key is joined to path when path is empty
-		{
-			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: ""},
-			prefix:   "",
-			key:      "/foo/bar",
-			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/foo/bar"},
-		},
-
-		// Host field carries through with port
-		{
-			endpoint: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
-			prefix:   "",
-			key:      "",
-			want:     url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
-		},
-
-		// Scheme carries through
-		{
-			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
-			prefix:   "",
-			key:      "",
-			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
-		},
-		// Prefix is applied
-		{
-			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
-			prefix:   "/bar",
-			key:      "/baz",
-			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz"},
-		},
-		// Prefix is joined to path
-		{
-			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
-			prefix:   "/bar",
-			key:      "",
-			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar"},
-		},
-		// Keep trailing slash
-		{
-			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
-			prefix:   "/bar",
-			key:      "/baz/",
-			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz/"},
-		},
-	}
-
-	for i, tt := range tests {
-		got := v2KeysURL(tt.endpoint, tt.prefix, tt.key)
-		if tt.want != *got {
-			t.Errorf("#%d: want=%#v, got=%#v", i, tt.want, *got)
-		}
-	}
-}
-
-func TestGetAction(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}
-	baseWantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/keys/foo/bar",
-	}
-	wantHeader := http.Header{}
-
-	tests := []struct {
-		recursive bool
-		sorted    bool
-		quorum    bool
-		wantQuery string
-	}{
-		{
-			recursive: false,
-			sorted:    false,
-			quorum:    false,
-			wantQuery: "quorum=false&recursive=false&sorted=false",
-		},
-		{
-			recursive: true,
-			sorted:    false,
-			quorum:    false,
-			wantQuery: "quorum=false&recursive=true&sorted=false",
-		},
-		{
-			recursive: false,
-			sorted:    true,
-			quorum:    false,
-			wantQuery: "quorum=false&recursive=false&sorted=true",
-		},
-		{
-			recursive: true,
-			sorted:    true,
-			quorum:    false,
-			wantQuery: "quorum=false&recursive=true&sorted=true",
-		},
-		{
-			recursive: false,
-			sorted:    false,
-			quorum:    true,
-			wantQuery: "quorum=true&recursive=false&sorted=false",
-		},
-	}
-
-	for i, tt := range tests {
-		f := getAction{
-			Key:       "/foo/bar",
-			Recursive: tt.recursive,
-			Sorted:    tt.sorted,
-			Quorum:    tt.quorum,
-		}
-		got := *f.HTTPRequest(ep)
-
-		wantURL := baseWantURL
-		wantURL.RawQuery = tt.wantQuery
-
-		err := assertRequest(got, http.MethodGet, wantURL, wantHeader, nil)
-		if err != nil {
-			t.Errorf("#%d: %v", i, err)
-		}
-	}
-}
-
-func TestWaitAction(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}
-	baseWantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/keys/foo/bar",
-	}
-	wantHeader := http.Header{}
-
-	tests := []struct {
-		waitIndex uint64
-		recursive bool
-		wantQuery string
-	}{
-		{
-			recursive: false,
-			waitIndex: uint64(0),
-			wantQuery: "recursive=false&wait=true&waitIndex=0",
-		},
-		{
-			recursive: false,
-			waitIndex: uint64(12),
-			wantQuery: "recursive=false&wait=true&waitIndex=12",
-		},
-		{
-			recursive: true,
-			waitIndex: uint64(12),
-			wantQuery: "recursive=true&wait=true&waitIndex=12",
-		},
-	}
-
-	for i, tt := range tests {
-		f := waitAction{
-			Key:       "/foo/bar",
-			WaitIndex: tt.waitIndex,
-			Recursive: tt.recursive,
-		}
-		got := *f.HTTPRequest(ep)
-
-		wantURL := baseWantURL
-		wantURL.RawQuery = tt.wantQuery
-
-		err := assertRequest(got, http.MethodGet, wantURL, wantHeader, nil)
-		if err != nil {
-			t.Errorf("#%d: unexpected error: %#v", i, err)
-		}
-	}
-}
-
-func TestSetAction(t *testing.T) {
-	wantHeader := http.Header(map[string][]string{
-		"Content-Type": {"application/x-www-form-urlencoded"},
-	})
-
-	tests := []struct {
-		act      setAction
-		wantURL  string
-		wantBody string
-	}{
-		// default prefix
-		{
-			act: setAction{
-				Prefix: defaultV2KeysPrefix,
-				Key:    "foo",
-			},
-			wantURL:  "http://example.com/v2/keys/foo",
-			wantBody: "value=",
-		},
-
-		// non-default prefix
-		{
-			act: setAction{
-				Prefix: "/pfx",
-				Key:    "foo",
-			},
-			wantURL:  "http://example.com/pfx/foo",
-			wantBody: "value=",
-		},
-
-		// no prefix
-		{
-			act: setAction{
-				Key: "foo",
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "value=",
-		},
-
-		// Key with path separators
-		{
-			act: setAction{
-				Prefix: defaultV2KeysPrefix,
-				Key:    "foo/bar/baz",
-			},
-			wantURL:  "http://example.com/v2/keys/foo/bar/baz",
-			wantBody: "value=",
-		},
-
-		// Key with leading slash, Prefix with trailing slash
-		{
-			act: setAction{
-				Prefix: "/foo/",
-				Key:    "/bar",
-			},
-			wantURL:  "http://example.com/foo/bar",
-			wantBody: "value=",
-		},
-
-		// Key with trailing slash
-		{
-			act: setAction{
-				Key: "/foo/",
-			},
-			wantURL:  "http://example.com/foo/",
-			wantBody: "value=",
-		},
-
-		// Value is set
-		{
-			act: setAction{
-				Key:   "foo",
-				Value: "baz",
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "value=baz",
-		},
-
-		// PrevExist set, but still ignored
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevExist: PrevIgnore,
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "value=",
-		},
-
-		// PrevExist set to true
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevExist: PrevExist,
-			},
-			wantURL:  "http://example.com/foo?prevExist=true",
-			wantBody: "value=",
-		},
-
-		// PrevExist set to false
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevExist: PrevNoExist,
-			},
-			wantURL:  "http://example.com/foo?prevExist=false",
-			wantBody: "value=",
-		},
-
-		// PrevValue is urlencoded
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevValue: "bar baz",
-			},
-			wantURL:  "http://example.com/foo?prevValue=bar+baz",
-			wantBody: "value=",
-		},
-
-		// PrevIndex is set
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevIndex: uint64(12),
-			},
-			wantURL:  "http://example.com/foo?prevIndex=12",
-			wantBody: "value=",
-		},
-
-		// TTL is set
-		{
-			act: setAction{
-				Key: "foo",
-				TTL: 3 * time.Minute,
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "ttl=180&value=",
-		},
-
-		// Refresh is set
-		{
-			act: setAction{
-				Key:     "foo",
-				TTL:     3 * time.Minute,
-				Refresh: true,
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "refresh=true&ttl=180&value=",
-		},
-
-		// Dir is set
-		{
-			act: setAction{
-				Key: "foo",
-				Dir: true,
-			},
-			wantURL:  "http://example.com/foo?dir=true",
-			wantBody: "",
-		},
-		// Dir is set with a value
-		{
-			act: setAction{
-				Key:   "foo",
-				Value: "bar",
-				Dir:   true,
-			},
-			wantURL:  "http://example.com/foo?dir=true",
-			wantBody: "",
-		},
-		// Dir is set with PrevExist set to true
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevExist: PrevExist,
-				Dir:       true,
-			},
-			wantURL:  "http://example.com/foo?dir=true&prevExist=true",
-			wantBody: "",
-		},
-		// Dir is set with PrevValue
-		{
-			act: setAction{
-				Key:       "foo",
-				PrevValue: "bar",
-				Dir:       true,
-			},
-			wantURL:  "http://example.com/foo?dir=true",
-			wantBody: "",
-		},
-		// NoValueOnSuccess is set
-		{
-			act: setAction{
-				Key:              "foo",
-				NoValueOnSuccess: true,
-			},
-			wantURL:  "http://example.com/foo?noValueOnSuccess=true",
-			wantBody: "value=",
-		},
-	}
-
-	for i, tt := range tests {
-		u, err := url.Parse(tt.wantURL)
-		if err != nil {
-			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
-		}
-
-		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
-		if err := assertRequest(*got, http.MethodPut, u, wantHeader, []byte(tt.wantBody)); err != nil {
-			t.Errorf("#%d: %v", i, err)
-		}
-	}
-}
-
-func TestCreateInOrderAction(t *testing.T) {
-	wantHeader := http.Header(map[string][]string{
-		"Content-Type": {"application/x-www-form-urlencoded"},
-	})
-
-	tests := []struct {
-		act      createInOrderAction
-		wantURL  string
-		wantBody string
-	}{
-		// default prefix
-		{
-			act: createInOrderAction{
-				Prefix: defaultV2KeysPrefix,
-				Dir:    "foo",
-			},
-			wantURL:  "http://example.com/v2/keys/foo",
-			wantBody: "value=",
-		},
-
-		// non-default prefix
-		{
-			act: createInOrderAction{
-				Prefix: "/pfx",
-				Dir:    "foo",
-			},
-			wantURL:  "http://example.com/pfx/foo",
-			wantBody: "value=",
-		},
-
-		// no prefix
-		{
-			act: createInOrderAction{
-				Dir: "foo",
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "value=",
-		},
-
-		// Key with path separators
-		{
-			act: createInOrderAction{
-				Prefix: defaultV2KeysPrefix,
-				Dir:    "foo/bar/baz",
-			},
-			wantURL:  "http://example.com/v2/keys/foo/bar/baz",
-			wantBody: "value=",
-		},
-
-		// Key with leading slash, Prefix with trailing slash
-		{
-			act: createInOrderAction{
-				Prefix: "/foo/",
-				Dir:    "/bar",
-			},
-			wantURL:  "http://example.com/foo/bar",
-			wantBody: "value=",
-		},
-
-		// Key with trailing slash
-		{
-			act: createInOrderAction{
-				Dir: "/foo/",
-			},
-			wantURL:  "http://example.com/foo/",
-			wantBody: "value=",
-		},
-
-		// Value is set
-		{
-			act: createInOrderAction{
-				Dir:   "foo",
-				Value: "baz",
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "value=baz",
-		},
-		// TTL is set
-		{
-			act: createInOrderAction{
-				Dir: "foo",
-				TTL: 3 * time.Minute,
-			},
-			wantURL:  "http://example.com/foo",
-			wantBody: "ttl=180&value=",
-		},
-	}
-
-	for i, tt := range tests {
-		u, err := url.Parse(tt.wantURL)
-		if err != nil {
-			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
-		}
-
-		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
-		if err := assertRequest(*got, http.MethodPost, u, wantHeader, []byte(tt.wantBody)); err != nil {
-			t.Errorf("#%d: %v", i, err)
-		}
-	}
-}
-
-func TestDeleteAction(t *testing.T) {
-	wantHeader := http.Header(map[string][]string{
-		"Content-Type": {"application/x-www-form-urlencoded"},
-	})
-
-	tests := []struct {
-		act     deleteAction
-		wantURL string
-	}{
-		// default prefix
-		{
-			act: deleteAction{
-				Prefix: defaultV2KeysPrefix,
-				Key:    "foo",
-			},
-			wantURL: "http://example.com/v2/keys/foo",
-		},
-
-		// non-default prefix
-		{
-			act: deleteAction{
-				Prefix: "/pfx",
-				Key:    "foo",
-			},
-			wantURL: "http://example.com/pfx/foo",
-		},
-
-		// no prefix
-		{
-			act: deleteAction{
-				Key: "foo",
-			},
-			wantURL: "http://example.com/foo",
-		},
-
-		// Key with path separators
-		{
-			act: deleteAction{
-				Prefix: defaultV2KeysPrefix,
-				Key:    "foo/bar/baz",
-			},
-			wantURL: "http://example.com/v2/keys/foo/bar/baz",
-		},
-
-		// Key with leading slash, Prefix with trailing slash
-		{
-			act: deleteAction{
-				Prefix: "/foo/",
-				Key:    "/bar",
-			},
-			wantURL: "http://example.com/foo/bar",
-		},
-
-		// Key with trailing slash
-		{
-			act: deleteAction{
-				Key: "/foo/",
-			},
-			wantURL: "http://example.com/foo/",
-		},
-
-		// Recursive set to true
-		{
-			act: deleteAction{
-				Key:       "foo",
-				Recursive: true,
-			},
-			wantURL: "http://example.com/foo?recursive=true",
-		},
-
-		// PrevValue is urlencoded
-		{
-			act: deleteAction{
-				Key:       "foo",
-				PrevValue: "bar baz",
-			},
-			wantURL: "http://example.com/foo?prevValue=bar+baz",
-		},
-
-		// PrevIndex is set
-		{
-			act: deleteAction{
-				Key:       "foo",
-				PrevIndex: uint64(12),
-			},
-			wantURL: "http://example.com/foo?prevIndex=12",
-		},
-	}
-
-	for i, tt := range tests {
-		u, err := url.Parse(tt.wantURL)
-		if err != nil {
-			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
-		}
-
-		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
-		if err := assertRequest(*got, http.MethodDelete, u, wantHeader, nil); err != nil {
-			t.Errorf("#%d: %v", i, err)
-		}
-	}
-}
-
-func assertRequest(got http.Request, wantMethod string, wantURL *url.URL, wantHeader http.Header, wantBody []byte) error {
-	if wantMethod != got.Method {
-		return fmt.Errorf("want.Method=%#v got.Method=%#v", wantMethod, got.Method)
-	}
-
-	if !reflect.DeepEqual(wantURL, got.URL) {
-		return fmt.Errorf("want.URL=%#v got.URL=%#v", wantURL, got.URL)
-	}
-
-	if !reflect.DeepEqual(wantHeader, got.Header) {
-		return fmt.Errorf("want.Header=%#v got.Header=%#v", wantHeader, got.Header)
-	}
-
-	if got.Body == nil {
-		if wantBody != nil {
-			return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, got.Body)
-		}
-	} else {
-		if wantBody == nil {
-			return fmt.Errorf("want.Body=%v got.Body=%s", wantBody, got.Body)
-		}
-		gotBytes, err := io.ReadAll(got.Body)
-		if err != nil {
-			return err
-		}
-
-		if !reflect.DeepEqual(wantBody, gotBytes) {
-			return fmt.Errorf("want.Body=%s got.Body=%s", wantBody, gotBytes)
-		}
-	}
-
-	return nil
-}
-
-func TestUnmarshalSuccessfulResponse(t *testing.T) {
-	var expiration time.Time
-	expiration.UnmarshalText([]byte("2015-04-07T04:40:23.044979686Z"))
-
-	tests := []struct {
-		indexHdr     string
-		clusterIDHdr string
-		body         string
-		wantRes      *Response
-		wantErr      bool
-	}{
-		// Neither PrevNode or Node
-		{
-			indexHdr: "1",
-			body:     `{"action":"delete"}`,
-			wantRes:  &Response{Action: "delete", Index: 1},
-			wantErr:  false,
-		},
-
-		// PrevNode
-		{
-			indexHdr: "15",
-			body:     `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
-			wantRes: &Response{
-				Action: "delete",
-				Index:  15,
-				Node:   nil,
-				PrevNode: &Node{
-					Key:           "/foo",
-					Value:         "bar",
-					ModifiedIndex: 12,
-					CreatedIndex:  10,
-				},
-			},
-			wantErr: false,
-		},
-
-		// Node
-		{
-			indexHdr: "15",
-			body:     `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`,
-			wantRes: &Response{
-				Action: "get",
-				Index:  15,
-				Node: &Node{
-					Key:           "/foo",
-					Value:         "bar",
-					ModifiedIndex: 12,
-					CreatedIndex:  10,
-					TTL:           10,
-					Expiration:    &expiration,
-				},
-				PrevNode: nil,
-			},
-			wantErr: false,
-		},
-
-		// Node Dir
-		{
-			indexHdr:     "15",
-			clusterIDHdr: "abcdef",
-			body:         `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`,
-			wantRes: &Response{
-				Action: "get",
-				Index:  15,
-				Node: &Node{
-					Key:           "/foo",
-					Dir:           true,
-					ModifiedIndex: 12,
-					CreatedIndex:  10,
-				},
-				PrevNode:  nil,
-				ClusterID: "abcdef",
-			},
-			wantErr: false,
-		},
-
-		// PrevNode and Node
-		{
-			indexHdr: "15",
-			body:     `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
-			wantRes: &Response{
-				Action: "update",
-				Index:  15,
-				PrevNode: &Node{
-					Key:           "/foo",
-					Value:         "baz",
-					ModifiedIndex: 10,
-					CreatedIndex:  10,
-				},
-				Node: &Node{
-					Key:           "/foo",
-					Value:         "bar",
-					ModifiedIndex: 12,
-					CreatedIndex:  10,
-				},
-			},
-			wantErr: false,
-		},
-
-		// Garbage in body
-		{
-			indexHdr: "",
-			body:     `garbage`,
-			wantRes:  nil,
-			wantErr:  true,
-		},
-
-		// non-integer index
-		{
-			indexHdr: "poo",
-			body:     `{}`,
-			wantRes:  nil,
-			wantErr:  true,
-		},
-	}
-
-	for i, tt := range tests {
-		h := make(http.Header)
-		h.Add("X-Etcd-Index", tt.indexHdr)
-		res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body))
-		if tt.wantErr != (err != nil) {
-			t.Errorf("#%d: wantErr=%t, err=%v", i, tt.wantErr, err)
-		}
-
-		if (res == nil) != (tt.wantRes == nil) {
-			t.Errorf("#%d: received res=%#v, but expected res=%#v", i, res, tt.wantRes)
-			continue
-		} else if tt.wantRes == nil {
-			// expected and successfully got nil response
-			continue
-		}
-
-		if res.Action != tt.wantRes.Action {
-			t.Errorf("#%d: Action=%s, expected %s", i, res.Action, tt.wantRes.Action)
-		}
-		if res.Index != tt.wantRes.Index {
-			t.Errorf("#%d: Index=%d, expected %d", i, res.Index, tt.wantRes.Index)
-		}
-		if !reflect.DeepEqual(res.Node, tt.wantRes.Node) {
-			t.Errorf("#%d: Node=%v, expected %v", i, res.Node, tt.wantRes.Node)
-		}
-	}
-}
-
-func TestUnmarshalFailedKeysResponse(t *testing.T) {
-	body := []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`)
-
-	wantErr := Error{
-		Code:    100,
-		Message: "Key not found",
-		Cause:   "/foo",
-		Index:   uint64(18),
-	}
-
-	gotErr := unmarshalFailedKeysResponse(body)
-	if !reflect.DeepEqual(wantErr, gotErr) {
-		t.Errorf("unexpected error: want=%#v got=%#v", wantErr, gotErr)
-	}
-}
-
-func TestUnmarshalFailedKeysResponseBadJSON(t *testing.T) {
-	err := unmarshalFailedKeysResponse([]byte(`{"er`))
-	var cErr Error
-	if err == nil {
-		t.Errorf("got nil error")
-	} else if errors.As(err, &cErr) {
-		t.Errorf("error is of incorrect type *Error: %#v", err)
-	}
-}
-
-func TestHTTPWatcherNextWaitAction(t *testing.T) {
-	initAction := waitAction{
-		Prefix:    "/pants",
-		Key:       "/foo/bar",
-		Recursive: true,
-		WaitIndex: 19,
-	}
-
-	client := &actionAssertingHTTPClient{
-		t:   t,
-		act: &initAction,
-		resp: http.Response{
-			StatusCode: http.StatusOK,
-			Header:     http.Header{"X-Etcd-Index": []string{"42"}},
-		},
-		body: []byte(`{"action":"update","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`),
-	}
-
-	wantResponse := &Response{
-		Action:   "update",
-		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(21)},
-		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
-		Index:    uint64(42),
-	}
-
-	wantNextWait := waitAction{
-		Prefix:    "/pants",
-		Key:       "/foo/bar",
-		Recursive: true,
-		WaitIndex: 22,
-	}
-
-	watcher := &httpWatcher{
-		client:   client,
-		nextWait: initAction,
-	}
-
-	resp, err := watcher.Next(t.Context())
-	if err != nil {
-		t.Errorf("non-nil error: %#v", err)
-	}
-
-	if !reflect.DeepEqual(wantResponse, resp) {
-		t.Errorf("received incorrect Response: want=%#v got=%#v", wantResponse, resp)
-	}
-
-	if !reflect.DeepEqual(wantNextWait, watcher.nextWait) {
-		t.Errorf("nextWait incorrect: want=%#v got=%#v", wantNextWait, watcher.nextWait)
-	}
-}
-
-func TestHTTPWatcherNextFail(t *testing.T) {
-	tests := []httpClient{
-		// generic HTTP client failure
-		&staticHTTPClient{
-			err: errors.New("fail"),
-		},
-
-		// unusable status code
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusTeapot,
-			},
-		},
-
-		// etcd Error response
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusNotFound,
-			},
-			body: []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`),
-		},
-	}
-
-	for i, tt := range tests {
-		act := waitAction{
-			Prefix:    "/pants",
-			Key:       "/foo/bar",
-			Recursive: true,
-			WaitIndex: 19,
-		}
-
-		watcher := &httpWatcher{
-			client:   tt,
-			nextWait: act,
-		}
-
-		resp, err := watcher.Next(t.Context())
-		if err == nil {
-			t.Errorf("#%d: expected non-nil error", i)
-		}
-		if resp != nil {
-			t.Errorf("#%d: expected nil Response, got %#v", i, resp)
-		}
-		if !reflect.DeepEqual(act, watcher.nextWait) {
-			t.Errorf("#%d: nextWait changed: want=%#v got=%#v", i, act, watcher.nextWait)
-		}
-	}
-}
-
-func TestHTTPKeysAPIWatcherAction(t *testing.T) {
-	tests := []struct {
-		key  string
-		opts *WatcherOptions
-		want waitAction
-	}{
-		{
-			key:  "/foo",
-			opts: nil,
-			want: waitAction{
-				Key:       "/foo",
-				Recursive: false,
-				WaitIndex: 0,
-			},
-		},
-
-		{
-			key: "/foo",
-			opts: &WatcherOptions{
-				Recursive:  false,
-				AfterIndex: 0,
-			},
-			want: waitAction{
-				Key:       "/foo",
-				Recursive: false,
-				WaitIndex: 0,
-			},
-		},
-
-		{
-			key: "/foo",
-			opts: &WatcherOptions{
-				Recursive:  true,
-				AfterIndex: 0,
-			},
-			want: waitAction{
-				Key:       "/foo",
-				Recursive: true,
-				WaitIndex: 0,
-			},
-		},
-
-		{
-			key: "/foo",
-			opts: &WatcherOptions{
-				Recursive:  false,
-				AfterIndex: 19,
-			},
-			want: waitAction{
-				Key:       "/foo",
-				Recursive: false,
-				WaitIndex: 20,
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		testError := errors.New("fail")
-		kAPI := &httpKeysAPI{
-			client: &staticHTTPClient{err: testError},
-		}
-
-		want := &httpWatcher{
-			client:   &staticHTTPClient{err: testError},
-			nextWait: tt.want,
-		}
-
-		got := kAPI.Watcher(tt.key, tt.opts)
-		if !reflect.DeepEqual(want, got) {
-			t.Errorf("#%d: incorrect watcher: want=%#v got=%#v", i, want, got)
-		}
-	}
-}
-
-func TestHTTPKeysAPISetAction(t *testing.T) {
-	tests := []struct {
-		key        string
-		value      string
-		opts       *SetOptions
-		wantAction httpAction
-	}{
-		// nil SetOptions
-		{
-			key:   "/foo",
-			value: "bar",
-			opts:  nil,
-			wantAction: &setAction{
-				Key:       "/foo",
-				Value:     "bar",
-				PrevValue: "",
-				PrevIndex: 0,
-				PrevExist: PrevIgnore,
-				TTL:       0,
-			},
-		},
-		// empty SetOptions
-		{
-			key:   "/foo",
-			value: "bar",
-			opts:  &SetOptions{},
-			wantAction: &setAction{
-				Key:       "/foo",
-				Value:     "bar",
-				PrevValue: "",
-				PrevIndex: 0,
-				PrevExist: PrevIgnore,
-				TTL:       0,
-			},
-		},
-		// populated SetOptions
-		{
-			key:   "/foo",
-			value: "bar",
-			opts: &SetOptions{
-				PrevValue: "baz",
-				PrevIndex: 13,
-				PrevExist: PrevExist,
-				TTL:       time.Minute,
-				Dir:       true,
-			},
-			wantAction: &setAction{
-				Key:       "/foo",
-				Value:     "bar",
-				PrevValue: "baz",
-				PrevIndex: 13,
-				PrevExist: PrevExist,
-				TTL:       time.Minute,
-				Dir:       true,
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
-		kAPI := httpKeysAPI{client: client}
-		kAPI.Set(t.Context(), tt.key, tt.value, tt.opts)
-	}
-}
-
-func TestHTTPKeysAPISetError(t *testing.T) {
-	tests := []httpClient{
-		// generic HTTP client failure
-		&staticHTTPClient{
-			err: errors.New("fail"),
-		},
-
-		// unusable status code
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusTeapot,
-			},
-		},
-
-		// etcd Error response
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusInternalServerError,
-			},
-			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
-		},
-	}
-
-	for i, tt := range tests {
-		kAPI := httpKeysAPI{client: tt}
-		resp, err := kAPI.Set(t.Context(), "/foo", "bar", nil)
-		if err == nil {
-			t.Errorf("#%d: received nil error", i)
-		}
-		if resp != nil {
-			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
-		}
-	}
-}
-
-func TestHTTPKeysAPISetResponse(t *testing.T) {
-	client := &staticHTTPClient{
-		resp: http.Response{
-			StatusCode: http.StatusOK,
-			Header:     http.Header{"X-Etcd-Index": []string{"21"}},
-		},
-		body: []byte(`{"action":"set","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":21},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`),
-	}
-
-	wantResponse := &Response{
-		Action:   "set",
-		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(21), ModifiedIndex: uint64(21)},
-		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
-		Index:    uint64(21),
-	}
-
-	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
-	resp, err := kAPI.Set(t.Context(), "/foo/bar/baz", "snarf", nil)
-	if err != nil {
-		t.Errorf("non-nil error: %#v", err)
-	}
-	if !reflect.DeepEqual(wantResponse, resp) {
-		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
-	}
-}
-
-func TestHTTPKeysAPIGetAction(t *testing.T) {
-	tests := []struct {
-		key        string
-		opts       *GetOptions
-		wantAction httpAction
-	}{
-		// nil GetOptions
-		{
-			key:  "/foo",
-			opts: nil,
-			wantAction: &getAction{
-				Key:       "/foo",
-				Sorted:    false,
-				Recursive: false,
-			},
-		},
-		// empty GetOptions
-		{
-			key:  "/foo",
-			opts: &GetOptions{},
-			wantAction: &getAction{
-				Key:       "/foo",
-				Sorted:    false,
-				Recursive: false,
-			},
-		},
-		// populated GetOptions
-		{
-			key: "/foo",
-			opts: &GetOptions{
-				Sort:      true,
-				Recursive: true,
-				Quorum:    true,
-			},
-			wantAction: &getAction{
-				Key:       "/foo",
-				Sorted:    true,
-				Recursive: true,
-				Quorum:    true,
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
-		kAPI := httpKeysAPI{client: client}
-		kAPI.Get(t.Context(), tt.key, tt.opts)
-	}
-}
-
-func TestHTTPKeysAPIGetError(t *testing.T) {
-	tests := []httpClient{
-		// generic HTTP client failure
-		&staticHTTPClient{
-			err: errors.New("fail"),
-		},
-
-		// unusable status code
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusTeapot,
-			},
-		},
-
-		// etcd Error response
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusInternalServerError,
-			},
-			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
-		},
-	}
-
-	for i, tt := range tests {
-		kAPI := httpKeysAPI{client: tt}
-		resp, err := kAPI.Get(t.Context(), "/foo", nil)
-		if err == nil {
-			t.Errorf("#%d: received nil error", i)
-		}
-		if resp != nil {
-			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
-		}
-	}
-}
-
-func TestHTTPKeysAPIGetResponse(t *testing.T) {
-	client := &staticHTTPClient{
-		resp: http.Response{
-			StatusCode: http.StatusOK,
-			Header:     http.Header{"X-Etcd-Index": []string{"42"}},
-		},
-		body: []byte(`{"action":"get","node":{"key":"/pants/foo/bar","modifiedIndex":25,"createdIndex":19,"nodes":[{"key":"/pants/foo/bar/baz","value":"snarf","createdIndex":21,"modifiedIndex":25}]}}`),
-	}
-
-	wantResponse := &Response{
-		Action: "get",
-		Node: &Node{
-			Key: "/pants/foo/bar",
-			Nodes: []*Node{
-				{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: 21, ModifiedIndex: 25},
-			},
-			CreatedIndex:  uint64(19),
-			ModifiedIndex: uint64(25),
-		},
-		Index: uint64(42),
-	}
-
-	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
-	resp, err := kAPI.Get(t.Context(), "/foo/bar", &GetOptions{Recursive: true})
-	if err != nil {
-		t.Errorf("non-nil error: %#v", err)
-	}
-	if !reflect.DeepEqual(wantResponse, resp) {
-		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
-	}
-}
-
-func TestHTTPKeysAPIDeleteAction(t *testing.T) {
-	tests := []struct {
-		key        string
-		opts       *DeleteOptions
-		wantAction httpAction
-	}{
-		// nil DeleteOptions
-		{
-			key:  "/foo",
-			opts: nil,
-			wantAction: &deleteAction{
-				Key:       "/foo",
-				PrevValue: "",
-				PrevIndex: 0,
-				Recursive: false,
-			},
-		},
-		// empty DeleteOptions
-		{
-			key:  "/foo",
-			opts: &DeleteOptions{},
-			wantAction: &deleteAction{
-				Key:       "/foo",
-				PrevValue: "",
-				PrevIndex: 0,
-				Recursive: false,
-			},
-		},
-		// populated DeleteOptions
-		{
-			key: "/foo",
-			opts: &DeleteOptions{
-				PrevValue: "baz",
-				PrevIndex: 13,
-				Recursive: true,
-			},
-			wantAction: &deleteAction{
-				Key:       "/foo",
-				PrevValue: "baz",
-				PrevIndex: 13,
-				Recursive: true,
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
-		kAPI := httpKeysAPI{client: client}
-		kAPI.Delete(t.Context(), tt.key, tt.opts)
-	}
-}
-
-func TestHTTPKeysAPIDeleteError(t *testing.T) {
-	tests := []httpClient{
-		// generic HTTP client failure
-		&staticHTTPClient{
-			err: errors.New("fail"),
-		},
-
-		// unusable status code
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusTeapot,
-			},
-		},
-
-		// etcd Error response
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusInternalServerError,
-			},
-			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
-		},
-	}
-
-	for i, tt := range tests {
-		kAPI := httpKeysAPI{client: tt}
-		resp, err := kAPI.Delete(t.Context(), "/foo", nil)
-		if err == nil {
-			t.Errorf("#%d: received nil error", i)
-		}
-		if resp != nil {
-			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
-		}
-	}
-}
-
-func TestHTTPKeysAPIDeleteResponse(t *testing.T) {
-	client := &staticHTTPClient{
-		resp: http.Response{
-			StatusCode: http.StatusOK,
-			Header:     http.Header{"X-Etcd-Index": []string{"22"}},
-		},
-		body: []byte(`{"action":"delete","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":22,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`),
-	}
-
-	wantResponse := &Response{
-		Action:   "delete",
-		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(22)},
-		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
-		Index:    uint64(22),
-	}
-
-	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
-	resp, err := kAPI.Delete(t.Context(), "/foo/bar/baz", nil)
-	if err != nil {
-		t.Errorf("non-nil error: %#v", err)
-	}
-	if !reflect.DeepEqual(wantResponse, resp) {
-		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
-	}
-}
-
-func TestHTTPKeysAPICreateAction(t *testing.T) {
-	act := &setAction{
-		Key:       "/foo",
-		Value:     "bar",
-		PrevExist: PrevNoExist,
-		PrevIndex: 0,
-		PrevValue: "",
-		TTL:       0,
-	}
-
-	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
-	kAPI.Create(t.Context(), "/foo", "bar")
-}
-
-func TestHTTPKeysAPICreateInOrderAction(t *testing.T) {
-	act := &createInOrderAction{
-		Dir:   "/foo",
-		Value: "bar",
-		TTL:   0,
-	}
-	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
-	kAPI.CreateInOrder(t.Context(), "/foo", "bar", nil)
-}
-
-func TestHTTPKeysAPIUpdateAction(t *testing.T) {
-	act := &setAction{
-		Key:       "/foo",
-		Value:     "bar",
-		PrevExist: PrevExist,
-		PrevIndex: 0,
-		PrevValue: "",
-		TTL:       0,
-	}
-
-	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
-	kAPI.Update(t.Context(), "/foo", "bar")
-}
-
-func TestNodeTTLDuration(t *testing.T) {
-	tests := []struct {
-		node *Node
-		want time.Duration
-	}{
-		{
-			node: &Node{TTL: 0},
-			want: 0,
-		},
-		{
-			node: &Node{TTL: 97},
-			want: 97 * time.Second,
-		},
-	}
-
-	for i, tt := range tests {
-		got := tt.node.TTLDuration()
-		if tt.want != got {
-			t.Errorf("#%d: incorrect duration: want=%v got=%v", i, tt.want, got)
-		}
-	}
-}
diff --git a/client/internal/v2/members_test.go b/client/internal/v2/members_test.go
deleted file mode 100644
index e93b55c47..000000000
--- a/client/internal/v2/members_test.go
+++ /dev/null
@@ -1,593 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package client
-
-import (
-	"encoding/json"
-	"errors"
-	"net/http"
-	"net/url"
-	"reflect"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"go.etcd.io/etcd/client/pkg/v3/types"
-)
-
-func TestMembersAPIActionList(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com"}
-	act := &membersAPIActionList{}
-
-	wantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/members",
-	}
-
-	got := *act.HTTPRequest(ep)
-	err := assertRequest(got, http.MethodGet, wantURL, http.Header{}, nil)
-	if err != nil {
-		t.Error(err.Error())
-	}
-}
-
-func TestMembersAPIActionAdd(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com"}
-	act := &membersAPIActionAdd{
-		peerURLs: types.URLs([]url.URL{
-			{Scheme: "https", Host: "127.0.0.1:8081"},
-			{Scheme: "http", Host: "127.0.0.1:8080"},
-		}),
-	}
-
-	wantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/members",
-	}
-	wantHeader := http.Header{
-		"Content-Type": []string{"application/json"},
-	}
-	wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`)
-
-	got := *act.HTTPRequest(ep)
-	err := assertRequest(got, http.MethodPost, wantURL, wantHeader, wantBody)
-	if err != nil {
-		t.Error(err.Error())
-	}
-}
-
-func TestMembersAPIActionUpdate(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com"}
-	act := &membersAPIActionUpdate{
-		memberID: "0xabcd",
-		peerURLs: types.URLs([]url.URL{
-			{Scheme: "https", Host: "127.0.0.1:8081"},
-			{Scheme: "http", Host: "127.0.0.1:8080"},
-		}),
-	}
-
-	wantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/members/0xabcd",
-	}
-	wantHeader := http.Header{
-		"Content-Type": []string{"application/json"},
-	}
-	wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`)
-
-	got := *act.HTTPRequest(ep)
-	err := assertRequest(got, http.MethodPut, wantURL, wantHeader, wantBody)
-	if err != nil {
-		t.Error(err.Error())
-	}
-}
-
-func TestMembersAPIActionRemove(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com"}
-	act := &membersAPIActionRemove{memberID: "XXX"}
-
-	wantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/members/XXX",
-	}
-
-	got := *act.HTTPRequest(ep)
-	err := assertRequest(got, http.MethodDelete, wantURL, http.Header{}, nil)
-	if err != nil {
-		t.Error(err.Error())
-	}
-}
-
-func TestMembersAPIActionLeader(t *testing.T) {
-	ep := url.URL{Scheme: "http", Host: "example.com"}
-	act := &membersAPIActionLeader{}
-
-	wantURL := &url.URL{
-		Scheme: "http",
-		Host:   "example.com",
-		Path:   "/v2/members/leader",
-	}
-
-	got := *act.HTTPRequest(ep)
-	err := assertRequest(got, http.MethodGet, wantURL, http.Header{}, nil)
-	if err != nil {
-		t.Error(err.Error())
-	}
-}
-
-func TestAssertStatusCode(t *testing.T) {
-	if err := assertStatusCode(404, 400); err == nil {
-		t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404")
-	}
-
-	if err := assertStatusCode(404, 400, 404); err != nil {
-		t.Errorf("assertStatusCode found conflict in (404,400) vs 400: %v", err)
-	}
-}
-
-func TestV2MembersURL(t *testing.T) {
-	got := v2MembersURL(url.URL{
-		Scheme: "http",
-		Host:   "foo.example.com:4002",
-		Path:   "/pants",
-	})
-	want := &url.URL{
-		Scheme: "http",
-		Host:   "foo.example.com:4002",
-		Path:   "/pants/v2/members",
-	}
-
-	require.Truef(t, reflect.DeepEqual(want, got), "v2MembersURL got %#v, want %#v", got, want)
-}
-
-func TestMemberUnmarshal(t *testing.T) {
-	tests := []struct {
-		body       []byte
-		wantMember Member
-		wantError  bool
-	}{
-		// no URLs, just check ID & Name
-		{
-			body:       []byte(`{"id": "c", "name": "dungarees"}`),
-			wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil},
-		},
-
-		// both client and peer URLs
-		{
-			body: []byte(`{"peerURLs": ["http://127.0.0.1:2379"], "clientURLs": ["http://127.0.0.1:2379"]}`),
-			wantMember: Member{
-				PeerURLs: []string{
-					"http://127.0.0.1:2379",
-				},
-				ClientURLs: []string{
-					"http://127.0.0.1:2379",
-				},
-			},
-		},
-
-		// multiple peer URLs
-		{
-			body: []byte(`{"peerURLs": ["http://127.0.0.1:2379", "https://example.com"]}`),
-			wantMember: Member{
-				PeerURLs: []string{
-					"http://127.0.0.1:2379",
-					"https://example.com",
-				},
-				ClientURLs: nil,
-			},
-		},
-
-		// multiple client URLs
-		{
-			body: []byte(`{"clientURLs": ["http://127.0.0.1:2379", "https://example.com"]}`),
-			wantMember: Member{
-				PeerURLs: nil,
-				ClientURLs: []string{
-					"http://127.0.0.1:2379",
-					"https://example.com",
-				},
-			},
-		},
-
-		// invalid JSON
-		{
-			body:      []byte(`{"peerU`),
-			wantError: true,
-		},
-	}
-
-	for i, tt := range tests {
-		got := Member{}
-		err := json.Unmarshal(tt.body, &got)
-		if tt.wantError != (err != nil) {
-			t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err)
-			continue
-		}
-
-		if !reflect.DeepEqual(tt.wantMember, got) {
-			t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got)
-		}
-	}
-}
-
-func TestMemberCollectionUnmarshalFail(t *testing.T) {
-	mc := &memberCollection{}
-	if err := mc.UnmarshalJSON([]byte(`{`)); err == nil {
-		t.Errorf("got nil error")
-	}
-}
-
-func TestMemberCollectionUnmarshal(t *testing.T) {
-	tests := []struct {
-		body []byte
-		want memberCollection
-	}{
-		{
-			body: []byte(`{}`),
-			want: memberCollection([]Member{}),
-		},
-		{
-			body: []byte(`{"members":[]}`),
-			want: memberCollection([]Member{}),
-		},
-		{
-			body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
-			want: memberCollection(
-				[]Member{
-					{
-						ID:   "2745e2525fce8fe",
-						Name: "node3",
-						PeerURLs: []string{
-							"http://127.0.0.1:7003",
-						},
-						ClientURLs: []string{
-							"http://127.0.0.1:4003",
-						},
-					},
-					{
-						ID:   "42134f434382925",
-						Name: "node1",
-						PeerURLs: []string{
-							"http://127.0.0.1:2380",
-							"http://127.0.0.1:7001",
-						},
-						ClientURLs: []string{
-							"http://127.0.0.1:2379",
-							"http://127.0.0.1:4001",
-						},
-					},
-					{
-						ID:   "94088180e21eb87b",
-						Name: "node2",
-						PeerURLs: []string{
-							"http://127.0.0.1:7002",
-						},
-						ClientURLs: []string{
-							"http://127.0.0.1:4002",
-						},
-					},
-				},
-			),
-		},
-	}
-
-	for i, tt := range tests {
-		var got memberCollection
-		err := json.Unmarshal(tt.body, &got)
-		if err != nil {
-			t.Errorf("#%d: unexpected error: %v", i, err)
-			continue
-		}
-
-		if !reflect.DeepEqual(tt.want, got) {
-			t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.want, got)
-		}
-	}
-}
-
-func TestMemberCreateRequestMarshal(t *testing.T) {
-	req := memberCreateOrUpdateRequest{
-		PeerURLs: types.URLs([]url.URL{
-			{Scheme: "http", Host: "127.0.0.1:8081"},
-			{Scheme: "https", Host: "127.0.0.1:8080"},
-		}),
-	}
-	want := []byte(`{"peerURLs":["http://127.0.0.1:8081","https://127.0.0.1:8080"]}`)
-
-	got, err := json.Marshal(&req)
-	require.NoErrorf(t, err, "Marshal returned unexpected err")
-
-	require.Truef(t, reflect.DeepEqual(want, got), "Failed to marshal memberCreateRequest: want=%s, got=%s", want, got)
-}
-
-func TestHTTPMembersAPIAddSuccess(t *testing.T) {
-	wantAction := &membersAPIActionAdd{
-		peerURLs: types.URLs([]url.URL{
-			{Scheme: "http", Host: "127.0.0.1:7002"},
-		}),
-	}
-
-	mAPI := &httpMembersAPI{
-		client: &actionAssertingHTTPClient{
-			t:   t,
-			act: wantAction,
-			resp: http.Response{
-				StatusCode: http.StatusCreated,
-			},
-			body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"]}`),
-		},
-	}
-
-	wantResponseMember := &Member{
-		ID:       "94088180e21eb87b",
-		PeerURLs: []string{"http://127.0.0.1:7002"},
-	}
-
-	m, err := mAPI.Add(t.Context(), "http://127.0.0.1:7002")
-	if err != nil {
-		t.Errorf("got non-nil err: %#v", err)
-	}
-	if !reflect.DeepEqual(wantResponseMember, m) {
-		t.Errorf("incorrect Member: want=%#v got=%#v", wantResponseMember, m)
-	}
-}
-
-func TestHTTPMembersAPIAddError(t *testing.T) {
-	okPeer := "http://example.com:2379"
-	tests := []struct {
-		peerURL string
-		client  httpClient
-
-		// if wantErr == nil, assert that the returned error is non-nil
-		// if wantErr != nil, assert that the returned error matches
-		wantErr error
-	}{
-		// malformed peer URL
-		{
-			peerURL: ":",
-		},
-
-		// generic httpClient failure
-		{
-			peerURL: okPeer,
-			client:  &staticHTTPClient{err: errors.New("fail")},
-		},
-
-		// unrecognized HTTP status code
-		{
-			peerURL: okPeer,
-			client: &staticHTTPClient{
-				resp: http.Response{StatusCode: http.StatusTeapot},
-			},
-		},
-
-		// unmarshal body into membersError on StatusConflict
-		{
-			peerURL: okPeer,
-			client: &staticHTTPClient{
-				resp: http.Response{
-					StatusCode: http.StatusConflict,
-				},
-				body: []byte(`{"message":"fail!"}`),
-			},
-			wantErr: membersError{Message: "fail!"},
-		},
-
-		// fail to unmarshal body on StatusConflict
-		{
-			peerURL: okPeer,
-			client: &staticHTTPClient{
-				resp: http.Response{
-					StatusCode: http.StatusConflict,
-				},
-				body: []byte(`{"`),
-			},
-		},
-
-		// fail to unmarshal body on StatusCreated
-		{
-			peerURL: okPeer,
-			client: &staticHTTPClient{
-				resp: http.Response{
-					StatusCode: http.StatusCreated,
-				},
-				body: []byte(`{"id":"XX`),
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		mAPI := &httpMembersAPI{client: tt.client}
-		m, err := mAPI.Add(t.Context(), tt.peerURL)
-		if err == nil {
-			t.Errorf("#%d: got nil err", i)
-		}
-		if tt.wantErr != nil && !reflect.DeepEqual(tt.wantErr, err) {
-			t.Errorf("#%d: incorrect error: want=%#v got=%#v", i, tt.wantErr, err)
-		}
-		if m != nil {
-			t.Errorf("#%d: got non-nil Member", i)
-		}
-	}
-}
-
-func TestHTTPMembersAPIRemoveSuccess(t *testing.T) {
-	wantAction := &membersAPIActionRemove{
-		memberID: "94088180e21eb87b",
-	}
-
-	mAPI := &httpMembersAPI{
-		client: &actionAssertingHTTPClient{
-			t:   t,
-			act: wantAction,
-			resp: http.Response{
-				StatusCode: http.StatusNoContent,
-			},
-		},
-	}
-
-	if err := mAPI.Remove(t.Context(), "94088180e21eb87b"); err != nil {
-		t.Errorf("got non-nil err: %#v", err)
-	}
-}
-
-func TestHTTPMembersAPIRemoveFail(t *testing.T) {
-	tests := []httpClient{
-		// generic error
-		&staticHTTPClient{
-			err: errors.New("fail"),
-		},
-
-		// unexpected HTTP status code
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusInternalServerError,
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		mAPI := &httpMembersAPI{client: tt}
-		if err := mAPI.Remove(t.Context(), "94088180e21eb87b"); err == nil {
-			t.Errorf("#%d: got nil err", i)
-		}
-	}
-}
-
-func TestHTTPMembersAPIListSuccess(t *testing.T) {
-	wantAction := &membersAPIActionList{}
-	mAPI := &httpMembersAPI{
-		client: &actionAssertingHTTPClient{
-			t:   t,
-			act: wantAction,
-			resp: http.Response{
-				StatusCode: http.StatusOK,
-			},
-			body: []byte(`{"members":[{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}]}`),
-		},
-	}
-
-	wantResponseMembers := []Member{
-		{
-			ID:         "94088180e21eb87b",
-			Name:       "node2",
-			PeerURLs:   []string{"http://127.0.0.1:7002"},
-			ClientURLs: []string{"http://127.0.0.1:4002"},
-		},
-	}
-
-	m, err := mAPI.List(t.Context())
-	if err != nil {
-		t.Errorf("got non-nil err: %#v", err)
-	}
-	if !reflect.DeepEqual(wantResponseMembers, m) {
-		t.Errorf("incorrect Members: want=%#v got=%#v", wantResponseMembers, m)
-	}
-}
-
-func TestHTTPMembersAPIListError(t *testing.T) {
-	tests := []httpClient{
-		// generic httpClient failure
-		&staticHTTPClient{err: errors.New("fail")},
-
-		// unrecognized HTTP status code
-		&staticHTTPClient{
-			resp: http.Response{StatusCode: http.StatusTeapot},
-		},
-
-		// fail to unmarshal body on StatusOK
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusOK,
-			},
-			body: []byte(`[{"id":"XX`),
-		},
-	}
-
-	for i, tt := range tests {
-		mAPI := &httpMembersAPI{client: tt}
-		ms, err := mAPI.List(t.Context())
-		if err == nil {
-			t.Errorf("#%d: got nil err", i)
-		}
-		if ms != nil {
-			t.Errorf("#%d: got non-nil Member slice", i)
-		}
-	}
-}
-
-func TestHTTPMembersAPILeaderSuccess(t *testing.T) {
-	wantAction := &membersAPIActionLeader{}
-	mAPI := &httpMembersAPI{
-		client: &actionAssertingHTTPClient{
-			t:   t,
-			act: wantAction,
-			resp: http.Response{
-				StatusCode: http.StatusOK,
-			},
-			body: []byte(`{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}`),
-		},
-	}
-
-	wantResponseMember := &Member{
-		ID:         "94088180e21eb87b",
-		Name:       "node2",
-		PeerURLs:   []string{"http://127.0.0.1:7002"},
-		ClientURLs: []string{"http://127.0.0.1:4002"},
-	}
-
-	m, err := mAPI.Leader(t.Context())
-	if err != nil {
-		t.Errorf("err = %v, want %v", err, nil)
-	}
-	if !reflect.DeepEqual(wantResponseMember, m) {
-		t.Errorf("incorrect member: member = %v, want %v", wantResponseMember, m)
-	}
-}
-
-func TestHTTPMembersAPILeaderError(t *testing.T) {
-	tests := []httpClient{
-		// generic httpClient failure
-		&staticHTTPClient{err: errors.New("fail")},
-
-		// unrecognized HTTP status code
-		&staticHTTPClient{
-			resp: http.Response{StatusCode: http.StatusTeapot},
-		},
-
-		// fail to unmarshal body on StatusOK
-		&staticHTTPClient{
-			resp: http.Response{
-				StatusCode: http.StatusOK,
-			},
-			body: []byte(`[{"id":"XX`),
-		},
-	}
-
-	for i, tt := range tests {
-		mAPI := &httpMembersAPI{client: tt}
-		m, err := mAPI.Leader(t.Context())
-		if err == nil {
-			t.Errorf("#%d: err = nil, want not nil", i)
-		}
-		if m != nil {
-			t.Errorf("member slice = %v, want nil", m)
-		}
-	}
-}
diff --git a/client/v3/client_test.go b/client/v3/client_test.go
index 474a48203..14b4af2df 100644
--- a/client/v3/client_test.go
+++ b/client/v3/client_test.go
@@ -347,7 +347,7 @@ func TestNewWithOnlyJWT(t *testing.T) {
 	}
 	defer c.Close()
 
-	meta, err := c.authTokenBundle.PerRPCCredentials().GetRequestMetadata(context.Background(), "")
+	meta, err := c.authTokenBundle.PerRPCCredentials().GetRequestMetadata(t.Context(), "")
 	if err != nil {
 		t.Errorf("Error building request metadata: %s", err)
 	}
diff --git a/etcdctl/ctlv3/command/printer_json_test.go b/etcdctl/ctlv3/command/printer_json_test.go
new file mode 100644
index 000000000..98296e4f6
--- /dev/null
+++ b/etcdctl/ctlv3/command/printer_json_test.go
@@ -0,0 +1,161 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+const (
+	keyHeader  = "header"
+	keyMember  = "member"
+	keyMembers = "members"
+
+	keyClusterID = "cluster_id"
+	keyMemberID  = "member_id"
+	keyRaftTerm  = "raft_term"
+	keyRevision  = "revision"
+	keyID        = "ID"
+)
+
+func assertNumericFieldEqual(t *testing.T, obj map[string]any, key string, want int64) {
+	raw, ok := obj[key]
+	require.Truef(t, ok, "missing key %q in map %v", key, obj)
+
+	n, ok := raw.(json.Number)
+	require.Truef(t, ok, "field %q is not json.Number: %v", key, raw)
+
+	val, err := n.Int64()
+	require.NoErrorf(t, err, "failed to convert field %q to int64: %v", key, n)
+
+	assert.Equalf(t, want, val, "unexpected value for field %q", key)
+}
+
+func assertHexFieldEqual(t *testing.T, obj map[string]any, key string, want string) {
+	raw, ok := obj[key]
+	require.Truef(t, ok, "missing key %q in map %v", key, obj)
+
+	str, ok := raw.(string)
+	require.Truef(t, ok, "field %q is not a string: %v", key, str)
+
+	assert.Equalf(t, want, str, "unexpected value for hex field %q", key)
+}
+
+type testCase struct {
+	number            uint64
+	wantHexString     string
+	wantDecimalNumber int64
+}
+
+var testCases = []testCase{
+	{1, "1", 1},
+	{100, "64", 100},
+	{1234567890, "499602d2", 1234567890},
+	{math.MaxInt64, "7fffffffffffffff", math.MaxInt64},
+}
+
+func TestMemberAdd(t *testing.T) {
+	tests := []struct {
+		name  string
+		isHex bool
+		cases []testCase
+	}{
+		{name: "decimal", isHex: false, cases: testCases},
+		{name: "hex", isHex: true, cases: testCases},
+	}
+
+	for _, testGroup := range tests {
+		t.Run(testGroup.name, func(t *testing.T) {
+			var buffer bytes.Buffer
+			p := &jsonPrinter{writer: &buffer, isHex: testGroup.isHex}
+
+			for _, tt := range testGroup.cases {
+				t.Run(fmt.Sprintf("number=%d", tt.number), func(t *testing.T) {
+					buffer.Reset()
+					decoder := json.NewDecoder(&buffer)
+					decoder.UseNumber()
+
+					response := clientv3.MemberAddResponse{
+						Header: &pb.ResponseHeader{
+							ClusterId: tt.number,
+							MemberId:  tt.number,
+							Revision:  int64(tt.number),
+							RaftTerm:  tt.number,
+						},
+						Member:  &pb.Member{ID: tt.number},
+						Members: []*pb.Member{{ID: tt.number}},
+					}
+					p.MemberAdd(response)
+
+					var got map[string]any
+					err := decoder.Decode(&got)
+					require.NoErrorf(t, err, "failed to decode JSON")
+
+					rawHeader, ok := got[keyHeader]
+					require.Truef(t, ok, "output does not contain %s field: %v", keyHeader, got)
+					header, ok := rawHeader.(map[string]any)
+					require.Truef(t, ok, "field %s is not map[string]any: %v", keyHeader, rawHeader)
+
+					if testGroup.isHex {
+						assertHexFieldEqual(t, header, keyClusterID, tt.wantHexString)
+						assertHexFieldEqual(t, header, keyMemberID, tt.wantHexString)
+					} else {
+						assertNumericFieldEqual(t, header, keyClusterID, tt.wantDecimalNumber)
+						assertNumericFieldEqual(t, header, keyMemberID, tt.wantDecimalNumber)
+					}
+					assertNumericFieldEqual(t, header, keyRaftTerm, tt.wantDecimalNumber)
+					assertNumericFieldEqual(t, header, keyRevision, tt.wantDecimalNumber)
+
+					rawMember, ok := got[keyMember]
+					require.Truef(t, ok, "output does not contain %s field: %v", keyMember, got)
+					member, ok := rawMember.(map[string]any)
+					require.Truef(t, ok, "field %s is not map[string]any: %v", keyMember, rawMember)
+
+					if testGroup.isHex {
+						assertHexFieldEqual(t, member, keyID, tt.wantHexString)
+					} else {
+						assertNumericFieldEqual(t, member, keyID, tt.wantDecimalNumber)
+					}
+
+					rawMembers, ok := got[keyMembers]
+					require.Truef(t, ok, "output does not contain %s field: %v", keyMembers, got)
+					members, ok := rawMembers.([]any)
+					require.Truef(t, ok, "field %s is not []any: %v", keyMembers, rawMembers)
+
+					for _, rawMemberItem := range members {
+						memberItem, ok := rawMemberItem.(map[string]any)
+						require.Truef(t, ok, "%s item is not map[string]any: %v", keyMembers, rawMemberItem)
+
+						if testGroup.isHex {
+							assertHexFieldEqual(t, memberItem, keyID, tt.wantHexString)
+						} else {
+							assertNumericFieldEqual(t, memberItem, keyID, tt.wantDecimalNumber)
+						}
+					}
+				})
+			}
+		})
+	}
+}
diff --git a/pkg/report/report_test.go b/pkg/report/report_test.go
index 0c9e37028..1d5a49e07 100644
--- a/pkg/report/report_test.go
+++ b/pkg/report/report_test.go
@@ -37,7 +37,7 @@ func TestPercentiles(t *testing.T) {
 }
 
 func TestReport(t *testing.T) {
-	r := NewReportSample("%f")
+	r := NewReportSample("%f", "", false)
 	go func() {
 		start := time.Now()
 		for i := 0; i < 5; i++ {
@@ -77,7 +77,7 @@ func TestReport(t *testing.T) {
 }
 
 func TestWeightedReport(t *testing.T) {
-	r := NewWeightedReport(NewReport("%f"), "%f")
+	r := NewWeightedReport(NewReport("%f", "", false), "%f", "", false)
 	go func() {
 		start := time.Now()
 		for i := 0; i < 5; i++ {
diff --git a/server/config/config_test.go b/server/config/config_test.go
index b1ae74b78..8fa22b289 100644
--- a/server/config/config_test.go
+++ b/server/config/config_test.go
@@ -22,6 +22,8 @@ import (
 	"go.uber.org/zap/zaptest"
 
 	"go.etcd.io/etcd/client/pkg/v3/types"
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/server/v3/etcdserver/api/v3discovery"
 )
 
 func mustNewURLs(t *testing.T, urls []string) []url.URL {
@@ -33,10 +35,14 @@ func mustNewURLs(t *testing.T, urls []string) []url.URL {
 	return u
 }
 
-func TestConfigVerifyBootstrapWithoutClusterAndDiscoveryURLFail(t *testing.T) {
+func TestConfigVerifyBootstrapWithoutClusterFail(t *testing.T) {
 	c := &ServerConfig{
-		Name:               "node1",
-		DiscoveryURL:       "",
+		Name: "node1",
+		DiscoveryCfg: v3discovery.DiscoveryConfig{
+			ConfigSpec: clientv3.ConfigSpec{
+				Endpoints: []string{},
+			},
+		},
 		InitialPeerURLsMap: types.URLsMap{},
 		Logger:             zaptest.NewLogger(t),
 	}
@@ -49,8 +55,12 @@ func TestConfigVerifyExistingWithDiscoveryURLFail(t *testing.T) {
 	cluster, err := types.NewURLsMap("node1=http://127.0.0.1:2380")
 	require.NoErrorf(t, err, "NewCluster error: %v", err)
 	c := &ServerConfig{
-		Name:               "node1",
-		DiscoveryURL:       "http://127.0.0.1:2379/abcdefg",
+		Name: "node1",
+		DiscoveryCfg: v3discovery.DiscoveryConfig{
+			ConfigSpec: clientv3.ConfigSpec{
+				Endpoints: []string{"http://192.168.0.100:2379"},
+			},
+		},
 		PeerURLs:           mustNewURLs(t, []string{"http://127.0.0.1:2380"}),
 		InitialPeerURLsMap: cluster,
 		NewCluster:         false,
@@ -196,9 +206,17 @@ func TestShouldDiscover(t *testing.T) {
 		"http://discovery.etcd.io/asdf": true,
 	}
 	for durl, w := range tests {
+		var eps []string
+		if durl != "" {
+			eps = append(eps, durl)
+		}
 		cfg := ServerConfig{
-			DiscoveryURL: durl,
-			Logger:       zaptest.NewLogger(t),
+			DiscoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{
+					Endpoints: eps,
+				},
+			},
+			Logger: zaptest.NewLogger(t),
 		}
 		if g := cfg.ShouldDiscover(); g != w {
 			t.Errorf("durl=%q: ShouldDiscover()=%t, want=%t", durl, g, w)
diff --git a/server/embed/config_test.go b/server/embed/config_test.go
index 729595aab..d9850f6e9 100644
--- a/server/embed/config_test.go
+++ b/server/embed/config_test.go
@@ -34,7 +34,9 @@ import (
 	"go.etcd.io/etcd/client/pkg/v3/srv"
 	"go.etcd.io/etcd/client/pkg/v3/transport"
 	"go.etcd.io/etcd/client/pkg/v3/types"
+	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/pkg/v3/featuregate"
+	"go.etcd.io/etcd/server/v3/etcdserver/api/v3discovery"
 	"go.etcd.io/etcd/server/v3/features"
 )
 
@@ -784,3 +786,114 @@ func TestMatchNewConfigAddFlags(t *testing.T) {
 		t.Errorf("Diff: %s", diff)
 	}
 }
+
+func TestCheckHostURLs(t *testing.T) {
+	tests := []struct {
+		name    string
+		urls    []url.URL
+		wantErr bool
+	}{
+		{
+			name: "valid HTTP URLs",
+			urls: []url.URL{
+				{Scheme: "http", Host: "127.0.0.1:2379"},
+				{Scheme: "http", Host: "localhost:2379"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid HTTPS URLs",
+			urls: []url.URL{
+				{Scheme: "https", Host: "127.0.0.1:2379"},
+				{Scheme: "https", Host: "localhost:2379"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "valid Unix socket URLs",
+			urls: []url.URL{
+				{Scheme: "unix", Host: "", Path: "/tmp/etcd.sock"},
+				{Scheme: "unixs", Host: "", Path: "/tmp/etcd-secure.sock"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "empty host in URL",
+			urls: []url.URL{
+				{Scheme: "http", Host: ""},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid host format",
+			urls: []url.URL{
+				{Scheme: "http", Host: "invalid_host"},
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing port in host",
+			urls: []url.URL{
+				{Scheme: "http", Host: "127.0.0.1"},
+			},
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := checkHostURLs(tt.urls)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("checkHostURLs() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestDiscoveryCfg(t *testing.T) {
+	testCases := []struct {
+		name         string
+		discoveryCfg v3discovery.DiscoveryConfig
+		wantErr      bool
+	}{
+		{
+			name: "Valid discovery config",
+			discoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{
+					Endpoints: []string{"http://10.0.0.100:2379", "http://10.0.0.101:2379"},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Partial empty discovery endpoints",
+			discoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{
+					Endpoints: []string{"http://10.0.0.100:2379", ""},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Empty discovery endpoint",
+			discoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{
+					Endpoints: []string{"", ""},
+				},
+			},
+			wantErr: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			cfg := NewConfig()
+			cfg.InitialCluster = ""
+			cfg.DiscoveryCfg = tc.discoveryCfg
+			cfg.DiscoveryCfg.Token = "foo"
+			err := cfg.Validate()
+
+			require.Equal(t, tc.wantErr, err != nil)
+		})
+	}
+}
diff --git a/server/etcdmain/config_test.go b/server/etcdmain/config_test.go
index 643e89715..b6e06c6a3 100644
--- a/server/etcdmain/config_test.go
+++ b/server/etcdmain/config_test.go
@@ -28,9 +28,11 @@ import (
 	"github.com/stretchr/testify/require"
 	"sigs.k8s.io/yaml"
 
+	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/pkg/v3/featuregate"
 	"go.etcd.io/etcd/pkg/v3/flags"
 	"go.etcd.io/etcd/server/v3/embed"
+	"go.etcd.io/etcd/server/v3/etcdserver/api/v3discovery"
 	"go.etcd.io/etcd/server/v3/features"
 )
 
@@ -207,21 +209,21 @@ func TestConfigFileClusteringFlags(t *testing.T) {
 func TestConfigParsingConflictClusteringFlags(t *testing.T) {
 	conflictArgs := [][]string{
 		{
-			"-initial-cluster=0=localhost:8000",
-			"-discovery=http://example.com/abc",
+			"--initial-cluster=0=localhost:8000",
+			"--discovery-endpoints=http://example.com/abc",
 		},
 		{
-			"-discovery-srv=example.com",
-			"-discovery=http://example.com/abc",
+			"--discovery-srv=example.com",
+			"--discovery-endpoints=http://example.com/abc",
 		},
 		{
-			"-initial-cluster=0=localhost:8000",
-			"-discovery-srv=example.com",
+			"--initial-cluster=0=localhost:8000",
+			"--discovery-srv=example.com",
 		},
 		{
-			"-initial-cluster=0=localhost:8000",
-			"-discovery=http://example.com/abc",
-			"-discovery-srv=example.com",
+			"--initial-cluster=0=localhost:8000",
+			"--discovery-endpoints=http://example.com/abc",
+			"--discovery-srv=example.com",
 		},
 	}
 
@@ -235,17 +237,21 @@ func TestConfigParsingConflictClusteringFlags(t *testing.T) {
 
 func TestConfigFileConflictClusteringFlags(t *testing.T) {
 	tests := []struct {
-		InitialCluster string `json:"initial-cluster"`
-		DNSCluster     string `json:"discovery-srv"`
-		Durl           string `json:"discovery"`
+		InitialCluster string                      `json:"initial-cluster"`
+		DNSCluster     string                      `json:"discovery-srv"`
+		DiscoveryCfg   v3discovery.DiscoveryConfig `json:"discovery-config"`
 	}{
 		{
 			InitialCluster: "0=localhost:8000",
-			Durl:           "http://example.com/abc",
+			DiscoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{Endpoints: []string{"http://example.com/abc"}},
+			},
 		},
 		{
 			DNSCluster: "example.com",
-			Durl:       "http://example.com/abc",
+			DiscoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{Endpoints: []string{"http://example.com/abc"}},
+			},
 		},
 		{
 			InitialCluster: "0=localhost:8000",
@@ -253,8 +259,10 @@ func TestConfigFileConflictClusteringFlags(t *testing.T) {
 		},
 		{
 			InitialCluster: "0=localhost:8000",
-			Durl:           "http://example.com/abc",
-			DNSCluster:     "example.com",
+			DiscoveryCfg: v3discovery.DiscoveryConfig{
+				ConfigSpec: clientv3.ConfigSpec{Endpoints: []string{"http://example.com/abc"}},
+			},
+			DNSCluster: "example.com",
 		},
 	}
 
@@ -283,29 +291,28 @@ func TestConfigParsingMissedAdvertiseClientURLsFlag(t *testing.T) {
 	}{
 		{
 			[]string{
-				"-initial-cluster=infra1=http://127.0.0.1:2380",
-				"-listen-client-urls=http://127.0.0.1:2379",
+				"--initial-cluster=infra1=http://127.0.0.1:2380",
+				"--listen-client-urls=http://127.0.0.1:2379",
 			},
 			embed.ErrUnsetAdvertiseClientURLsFlag,
 		},
 		{
 			[]string{
-				"-discovery-srv=example.com",
-				"-listen-client-urls=http://127.0.0.1:2379",
+				"--discovery-srv=example.com",
+				"--listen-client-urls=http://127.0.0.1:2379",
 			},
 			embed.ErrUnsetAdvertiseClientURLsFlag,
 		},
 		{
 			[]string{
-				"-discovery=http://example.com/abc",
-				"-discovery-fallback=exit",
-				"-listen-client-urls=http://127.0.0.1:2379",
+				"--discovery-fallback=exit",
+				"--listen-client-urls=http://127.0.0.1:2379",
 			},
 			embed.ErrUnsetAdvertiseClientURLsFlag,
 		},
 		{
 			[]string{
-				"-listen-client-urls=http://127.0.0.1:2379",
+				"--listen-client-urls=http://127.0.0.1:2379",
 			},
 			embed.ErrUnsetAdvertiseClientURLsFlag,
 		},
diff --git a/server/etcdserver/api/etcdhttp/health_test.go b/server/etcdserver/api/etcdhttp/health_test.go
index 06226e136..69822bc93 100644
--- a/server/etcdserver/api/etcdhttp/health_test.go
+++ b/server/etcdserver/api/etcdhttp/health_test.go
@@ -26,8 +26,6 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"go.uber.org/zap/zaptest"
 
-	"go.etcd.io/raft/v3"
-
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	"go.etcd.io/etcd/client/pkg/v3/testutil"
 	"go.etcd.io/etcd/client/pkg/v3/types"
@@ -35,6 +33,7 @@ import (
 	"go.etcd.io/etcd/server/v3/config"
 	betesting "go.etcd.io/etcd/server/v3/storage/backend/testing"
 	"go.etcd.io/etcd/server/v3/storage/schema"
+	"go.etcd.io/raft/v3"
 )
 
 type fakeHealthServer struct {
diff --git a/server/etcdserver/api/membership/cluster_test.go b/server/etcdserver/api/membership/cluster_test.go
index df6c0f2f2..6da995855 100644
--- a/server/etcdserver/api/membership/cluster_test.go
+++ b/server/etcdserver/api/membership/cluster_test.go
@@ -23,6 +23,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/zap/zaptest"
 
 	"go.etcd.io/etcd/client/pkg/v3/testutil"
@@ -666,6 +667,7 @@ func newTestCluster(tb testing.TB, membs []*Member) *RaftCluster {
 		members: make(map[types.ID]*Member),
 		removed: make(map[types.ID]bool),
 		be:      newMembershipBackend(),
+		v2store: v2store.New(),
 	}
 	for _, m := range membs {
 		c.members[m.ID] = m
@@ -996,6 +998,118 @@ func TestIsReadyToPromoteMember(t *testing.T) {
 	}
 }
 
+func TestPromoteMember(t *testing.T) {
+	clientURLs := []string{"http://127.0.0.1:2379"}
+	testCases := []struct {
+		name        string
+		members     []*Member
+		promoteID   types.ID
+		wantMembers map[types.ID]*Member
+	}{
+		{
+			name: "promote a voting member",
+			members: []*Member{
+				newTestMember(1, nil, "1", clientURLs),
+				newTestMemberAsLearner(2, nil, "2", clientURLs),
+			},
+			promoteID: 1,
+			wantMembers: map[types.ID]*Member{
+				1: newTestMember(1, nil, "1", clientURLs),
+				2: newTestMemberAsLearner(2, nil, "2", clientURLs),
+			},
+		},
+		{
+			name: "promote a learner",
+			members: []*Member{
+				newTestMember(1, nil, "1", clientURLs),
+				newTestMemberAsLearner(2, nil, "2", clientURLs),
+			},
+			promoteID: 2,
+			wantMembers: map[types.ID]*Member{
+				1: newTestMember(1, nil, "1", clientURLs),
+				2: newTestMember(2, nil, "2", clientURLs),
+			},
+		},
+		{
+			name: "promote a non-exist member",
+			members: []*Member{
+				newTestMember(1, nil, "1", clientURLs),
+				newTestMemberAsLearner(2, nil, "2", clientURLs),
+			},
+			promoteID: 3,
+			wantMembers: map[types.ID]*Member{
+				1: newTestMember(1, nil, "1", clientURLs),
+				2: newTestMemberAsLearner(2, nil, "2", clientURLs),
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := newTestCluster(t, tc.members)
+			st := v2store.New("/0", "/1")
+			c.Store(st)
+			c.SetStore(st)
+
+			c.PromoteMember(tc.promoteID, false)
+
+			mst, _ := membersFromStore(c.lg, st)
+			require.Equal(t, tc.wantMembers, mst)
+		})
+	}
+}
+
+func TestUpdateRaftAttributes(t *testing.T) {
+	clientURLs := []string{"http://127.0.0.1:2379"}
+	oldPeerURLs := []string{"http://127.0.0.1:2380"}
+	newPeerURLs := []string{"http://127.0.0.1:2382"}
+	testCases := []struct {
+		name           string
+		members        []*Member
+		updateMemberID types.ID
+		wantMembers    map[types.ID]*Member
+	}{
+		{
+			name: "update an existing member",
+			members: []*Member{
+				newTestMember(1, oldPeerURLs, "1", clientURLs),
+				newTestMember(2, oldPeerURLs, "2", clientURLs),
+			},
+			updateMemberID: 2,
+			wantMembers: map[types.ID]*Member{
+				1: newTestMember(1, oldPeerURLs, "1", clientURLs),
+				2: newTestMember(2, newPeerURLs, "2", clientURLs),
+			},
+		},
+		{
+			name: "update a non-exist member",
+			members: []*Member{
+				newTestMember(1, oldPeerURLs, "1", clientURLs),
+				newTestMember(2, oldPeerURLs, "2", clientURLs),
+			},
+			updateMemberID: 3,
+			wantMembers: map[types.ID]*Member{
+				1: newTestMember(1, oldPeerURLs, "1", clientURLs),
+				2: newTestMember(2, oldPeerURLs, "2", clientURLs),
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := newTestCluster(t, tc.members)
+			st := v2store.New("/0", "/1")
+			c.Store(st)
+			c.SetStore(st)
+
+			c.UpdateRaftAttributes(tc.updateMemberID, RaftAttributes{PeerURLs: newPeerURLs}, false)
+
+			mst, _ := membersFromStore(c.lg, st)
+			require.Equal(t, tc.wantMembers, mst)
+		})
+	}
+}
+
 func TestClusterStore(t *testing.T) {
 	name := "etcd"
 	clientURLs := []string{"http://127.0.0.1:4001"}
diff --git a/server/etcdserver/api/membership/membership_test.go b/server/etcdserver/api/membership/membership_test.go
index 77be3bba2..03bf384ad 100644
--- a/server/etcdserver/api/membership/membership_test.go
+++ b/server/etcdserver/api/membership/membership_test.go
@@ -34,10 +34,6 @@ func TestAddRemoveMember(t *testing.T) {
 	c.AddMember(newTestMember(18, nil, "node18", nil), true)
 	c.RemoveMember(18, true)
 
-	// Skipping removal of already removed member
-	c.RemoveMember(17, true)
-	c.RemoveMember(18, true)
-
 	c.AddMember(newTestMember(19, nil, "node19", nil), true)
 
 	// Recover from backend
diff --git a/server/etcdserver/api/v2discovery/discovery_test.go b/server/etcdserver/api/v2discovery/discovery_test.go
deleted file mode 100644
index 28e4e9790..000000000
--- a/server/etcdserver/api/v2discovery/discovery_test.go
+++ /dev/null
@@ -1,572 +0,0 @@
-// Copyright 2015 The etcd Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package v2discovery
-
-import (
-	"context"
-	"errors"
-	"math"
-	"math/rand"
-	"net/http"
-	"net/url"
-	"reflect"
-	"sort"
-	"strconv"
-	"testing"
-	"time"
-
-	"github.com/jonboulle/clockwork"
-	"go.uber.org/zap/zaptest"
-
-	"go.etcd.io/etcd/client/pkg/v3/types"
-	"go.etcd.io/etcd/client/v2"
-)
-
-const (
-	maxRetryInTest = 3
-)
-
-func TestNewProxyFuncUnset(t *testing.T) {
-	pf, err := newProxyFunc(zaptest.NewLogger(t), "")
-	if pf != nil {
-		t.Fatal("unexpected non-nil proxyFunc")
-	}
-	if err != nil {
-		t.Fatalf("unexpected non-nil err: %v", err)
-	}
-}
-
-func TestNewProxyFuncBad(t *testing.T) {
-	tests := []string{
-		"%%",
-		"http://foo.com/%1",
-	}
-	for i, in := range tests {
-		pf, err := newProxyFunc(zaptest.NewLogger(t), in)
-		if pf != nil {
-			t.Errorf("#%d: unexpected non-nil proxyFunc", i)
-		}
-		if err == nil {
-			t.Errorf("#%d: unexpected nil err", i)
-		}
-	}
-}
-
-func TestNewProxyFunc(t *testing.T) {
-	tests := map[string]string{
-		"bar.com":              "http://bar.com",
-		"http://disco.foo.bar": "http://disco.foo.bar",
-	}
-	for in, w := range tests {
-		pf, err := newProxyFunc(zaptest.NewLogger(t), in)
-		if pf == nil {
-			t.Errorf("%s: unexpected nil proxyFunc", in)
-			continue
-		}
-		if err != nil {
-			t.Errorf("%s: unexpected non-nil err: %v", in, err)
-			continue
-		}
-		g, err := pf(&http.Request{})
-		if err != nil {
-			t.Errorf("%s: unexpected non-nil err: %v", in, err)
-		}
-		if g.String() != w {
-			t.Errorf("%s: proxyURL=%q, want %q", in, g, w)
-		}
-	}
-}
-
-func TestCheckCluster(t *testing.T) {
-	cluster := "/prefix/1000"
-	self := "/1000/1"
-
-	tests := []struct {
-		nodes []*client.Node
-		index uint64
-		werr  error
-		wsize int
-	}{
-		{
-			// self is in the size range
-			[]*client.Node{
-				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
-				{Key: "/1000/_config/"},
-				{Key: self, CreatedIndex: 2},
-				{Key: "/1000/2", CreatedIndex: 3},
-				{Key: "/1000/3", CreatedIndex: 4},
-				{Key: "/1000/4", CreatedIndex: 5},
-			},
-			5,
-			nil,
-			3,
-		},
-		{
-			// self is in the size range
-			[]*client.Node{
-				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
-				{Key: "/1000/_config/"},
-				{Key: "/1000/2", CreatedIndex: 2},
-				{Key: "/1000/3", CreatedIndex: 3},
-				{Key: self, CreatedIndex: 4},
-				{Key: "/1000/4", CreatedIndex: 5},
-			},
-			5,
-			nil,
-			3,
-		},
-		{
-			// self is out of the size range
-			[]*client.Node{
-				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
-				{Key: "/1000/_config/"},
-				{Key: "/1000/2", CreatedIndex: 2},
-				{Key: "/1000/3", CreatedIndex: 3},
-				{Key: "/1000/4", CreatedIndex: 4},
-				{Key: self, CreatedIndex: 5},
-			},
-			5,
-			ErrFullCluster,
-			3,
-		},
-		{
-			// self is not in the cluster
-			[]*client.Node{
-				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
-				{Key: "/1000/_config/"},
-				{Key: "/1000/2", CreatedIndex: 2},
-				{Key: "/1000/3", CreatedIndex: 3},
-			},
-			3,
-			nil,
-			3,
-		},
-		{
-			[]*client.Node{
-				{Key: "/1000/_config/size", Value: "3", CreatedIndex: 1},
-				{Key: "/1000/_config/"},
-				{Key: "/1000/2", CreatedIndex: 2},
-				{Key: "/1000/3", CreatedIndex: 3},
-				{Key: "/1000/4", CreatedIndex: 4},
-			},
-			3,
-			ErrFullCluster,
-			3,
-		},
-		{
-			// bad size key
-			[]*client.Node{
-				{Key: "/1000/_config/size", Value: "bad", CreatedIndex: 1},
-			},
-			0,
-			ErrBadSizeKey,
-			0,
-		},
-		{
-			// no size key
-			[]*client.Node{},
-			0,
-			ErrSizeNotFound,
-			0,
-		},
-	}
-
-	for i, tt := range tests {
-		var rs []*client.Response
-		if len(tt.nodes) > 0 {
-			rs = append(rs, &client.Response{Node: tt.nodes[0], Index: tt.index})
-			rs = append(rs, &client.Response{
-				Node: &client.Node{
-					Key:   cluster,
-					Nodes: tt.nodes[1:],
-				},
-				Index: tt.index,
-			})
-		}
-		c := &clientWithResp{rs: rs}
-		dBase := newTestDiscovery(t, cluster, 1, c)
-
-		cRetry := &clientWithRetry{failTimes: 3}
-		cRetry.rs = rs
-		fc := clockwork.NewFakeClock()
-		dRetry := newTestDiscoveryWithClock(t, cluster, 1, cRetry, fc)
-
-		for _, d := range []*discovery{dBase, dRetry} {
-			go func() {
-				for i := uint(1); i <= maxRetryInTest; i++ {
-					fc.BlockUntil(1)
-					fc.Advance(time.Second * (0x1 << i))
-				}
-			}()
-			ns, size, index, err := d.checkCluster()
-			if !errors.Is(err, tt.werr) {
-				t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
-			}
-			if reflect.DeepEqual(ns, tt.nodes) {
-				t.Errorf("#%d: nodes = %v, want %v", i, ns, tt.nodes)
-			}
-			if size != uint64(tt.wsize) {
-				t.Errorf("#%d: size = %v, want %d", i, size, tt.wsize)
-			}
-			if index != tt.index {
-				t.Errorf("#%d: index = %v, want %d", i, index, tt.index)
-			}
-		}
-	}
-}
-
-func TestWaitNodes(t *testing.T) {
-	all := []*client.Node{
-		0: {Key: "/1000/1", CreatedIndex: 2},
-		1: {Key: "/1000/2", CreatedIndex: 3},
-		2: {Key: "/1000/3", CreatedIndex: 4},
-	}
-
-	tests := []struct {
-		nodes []*client.Node
-		rs    []*client.Response
-	}{
-		{
-			all,
-			[]*client.Response{},
-		},
-		{
-			all[:1],
-			[]*client.Response{
-				{Node: &client.Node{Key: "/1000/2", CreatedIndex: 3}},
-				{Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}},
-			},
-		},
-		{
-			all[:2],
-			[]*client.Response{
-				{Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}},
-			},
-		},
-		{
-			append(all, &client.Node{Key: "/1000/4", CreatedIndex: 5}),
-			[]*client.Response{
-				{Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}},
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		// Basic case
-		c := &clientWithResp{rs: nil, w: &watcherWithResp{rs: tt.rs}}
-		dBase := newTestDiscovery(t, "1000", 1, c)
-
-		// Retry case
-		var retryScanResp []*client.Response
-		if len(tt.nodes) > 0 {
-			retryScanResp = append(retryScanResp, &client.Response{
-				Node: &client.Node{
-					Key:   "1000",
-					Value: strconv.Itoa(3),
-				},
-			})
-			retryScanResp = append(retryScanResp, &client.Response{
-				Node: &client.Node{
-					Nodes: tt.nodes,
-				},
-			})
-		}
-		cRetry := &clientWithResp{
-			rs: retryScanResp,
-			w:  &watcherWithRetry{rs: tt.rs, failTimes: 2},
-		}
-		fc := clockwork.NewFakeClock()
-		dRetry := newTestDiscoveryWithClock(t, "1000", 1, cRetry, fc)
-
-		for _, d := range []*discovery{dBase, dRetry} {
-			go func() {
-				for i := uint(1); i <= maxRetryInTest; i++ {
-					fc.BlockUntil(1)
-					fc.Advance(time.Second * (0x1 << i))
-				}
-			}()
-			g, err := d.waitNodes(tt.nodes, uint64(3), 0) // we do not care about index in this test
-			if err != nil {
-				t.Errorf("#%d: err = %v, want %v", i, err, nil)
-			}
-			if !reflect.DeepEqual(g, all) {
-				t.Errorf("#%d: all = %v, want %v", i, g, all)
-			}
-		}
-	}
-}
-
-func TestCreateSelf(t *testing.T) {
-	rs := []*client.Response{{Node: &client.Node{Key: "1000/1", CreatedIndex: 2}}}
-
-	w := &watcherWithResp{rs: rs}
-	errw := &watcherWithErr{err: errors.New("watch err")}
-
-	c := &clientWithResp{rs: rs, w: w}
-	errc := &clientWithErr{err: errors.New("create err"), w: w}
-	errdupc := &clientWithErr{err: client.Error{Code: client.ErrorCodeNodeExist}}
-	errwc := &clientWithResp{rs: rs, w: errw}
-
-	tests := []struct {
-		c    client.KeysAPI
-		werr error
-	}{
-		// no error
-		{c, nil},
-		// client.create returns an error
-		{errc, errc.err},
-		// watcher.next returns an error
-		{errwc, errw.err},
-		// parse key exist error to duplicate ID error
-		{errdupc, ErrDuplicateID},
-	}
-
-	for i, tt := range tests {
-		d := newTestDiscovery(t, "1000", 1, tt.c)
-		if err := d.createSelf(""); !errors.Is(err, tt.werr) {
-			t.Errorf("#%d: err = %v, want %v", i, err, nil)
-		}
-	}
-}
-
-func TestNodesToCluster(t *testing.T) {
-	tests := []struct {
-		nodes    []*client.Node
-		size     uint64
-		wcluster string
-		werr     error
-	}{
-		{
-			[]*client.Node{
-				0: {Key: "/1000/1", Value: "1=http://1.1.1.1:2380", CreatedIndex: 1},
-				1: {Key: "/1000/2", Value: "2=http://2.2.2.2:2380", CreatedIndex: 2},
-				2: {Key: "/1000/3", Value: "3=http://3.3.3.3:2380", CreatedIndex: 3},
-			},
-			3,
-			"1=http://1.1.1.1:2380,2=http://2.2.2.2:2380,3=http://3.3.3.3:2380",
-			nil,
-		},
-		{
-			[]*client.Node{
-				0: {Key: "/1000/1", Value: "1=http://1.1.1.1:2380", CreatedIndex: 1},
-				1: {Key: "/1000/2", Value: "2=http://2.2.2.2:2380", CreatedIndex: 2},
-				2: {Key: "/1000/3", Value: "2=http://3.3.3.3:2380", CreatedIndex: 3},
-			},
-			3,
-			"1=http://1.1.1.1:2380,2=http://2.2.2.2:2380,2=http://3.3.3.3:2380",
-			ErrDuplicateName,
-		},
-		{
-			[]*client.Node{
-				0: {Key: "/1000/1", Value: "1=1.1.1.1:2380", CreatedIndex: 1},
-				1: {Key: "/1000/2", Value: "2=http://2.2.2.2:2380", CreatedIndex: 2},
-				2: {Key: "/1000/3", Value: "2=http://3.3.3.3:2380", CreatedIndex: 3},
-			},
-			3,
-			"1=1.1.1.1:2380,2=http://2.2.2.2:2380,2=http://3.3.3.3:2380",
-			ErrInvalidURL,
-		},
-	}
-
-	for i, tt := range tests {
-		cluster, err := nodesToCluster(tt.nodes, tt.size)
-		if !errors.Is(err, tt.werr) {
-			t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
-		}
-		if !reflect.DeepEqual(cluster, tt.wcluster) {
-			t.Errorf("#%d: cluster = %v, want %v", i, cluster, tt.wcluster)
-		}
-	}
-}
-
-func TestSortableNodes(t *testing.T) {
-	ns := []*client.Node{
-		0: {CreatedIndex: 5},
-		1: {CreatedIndex: 1},
-		2: {CreatedIndex: 3},
-		3: {CreatedIndex: 4},
-	}
-	// add some randomness
-	for i := 0; i < 10000; i++ {
-		ns = append(ns, &client.Node{CreatedIndex: uint64(rand.Int31())})
-	}
-	sns := sortableNodes{ns}
-	sort.Sort(sns)
-	var cis []int
-	for _, n := range sns.Nodes {
-		cis = append(cis, int(n.CreatedIndex))
-	}
-	if !sort.IntsAreSorted(cis) {
-		t.Errorf("isSorted = %v, want %v", sort.IntsAreSorted(cis), true)
-	}
-	cis = make([]int, 0)
-	for _, n := range ns {
-		cis = append(cis, int(n.CreatedIndex))
-	}
-	if !sort.IntsAreSorted(cis) {
-		t.Errorf("isSorted = %v, want %v", sort.IntsAreSorted(cis), true)
-	}
-}
-
-func TestRetryFailure(t *testing.T) {
-	nRetries = maxRetryInTest
-	defer func() { nRetries = math.MaxUint32 }()
-
-	cluster := "1000"
-	c := &clientWithRetry{failTimes: 4}
-	fc := clockwork.NewFakeClock()
-	d := newTestDiscoveryWithClock(t, cluster, 1, c, fc)
-	go func() {
-		for i := uint(1); i <= maxRetryInTest; i++ {
-			fc.BlockUntil(1)
-			fc.Advance(time.Second * (0x1 << i))
-		}
-	}()
-	if _, _, _, err := d.checkCluster(); !errors.Is(err, ErrTooManyRetries) {
-		t.Errorf("err = %v, want %v", err, ErrTooManyRetries)
-	}
-}
-
-type clientWithResp struct {
-	rs []*client.Response
-	w  client.Watcher
-	client.KeysAPI
-}
-
-func (c *clientWithResp) Create(ctx context.Context, key string, value string) (*client.Response, error) {
-	if len(c.rs) == 0 {
-		return &client.Response{}, nil
-	}
-	r := c.rs[0]
-	c.rs = c.rs[1:]
-	return r, nil
-}
-
-func (c *clientWithResp) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
-	if len(c.rs) == 0 {
-		return &client.Response{}, &client.Error{Code: client.ErrorCodeKeyNotFound}
-	}
-	r := c.rs[0]
-	c.rs = append(c.rs[1:], r)
-	return r, nil
-}
-
-func (c *clientWithResp) Watcher(key string, opts *client.WatcherOptions) client.Watcher {
-	return c.w
-}
-
-type clientWithErr struct {
-	err error
-	w   client.Watcher
-	client.KeysAPI
-}
-
-func (c *clientWithErr) Create(ctx context.Context, key string, value string) (*client.Response, error) {
-	return &client.Response{}, c.err
-}
-
-func (c *clientWithErr) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
-	return &client.Response{}, c.err
-}
-
-func (c *clientWithErr) Watcher(key string, opts *client.WatcherOptions) client.Watcher {
-	return c.w
-}
-
-type watcherWithResp struct {
-	client.KeysAPI
-	rs []*client.Response
-}
-
-func (w *watcherWithResp) Next(context.Context) (*client.Response, error) {
-	if len(w.rs) == 0 {
-		return &client.Response{}, nil
-	}
-	r := w.rs[0]
-	w.rs = w.rs[1:]
-	return r, nil
-}
-
-type watcherWithErr struct {
-	err error
-}
-
-func (w *watcherWithErr) Next(context.Context) (*client.Response, error) {
-	return &client.Response{}, w.err
-}
-
-// clientWithRetry will timeout all requests up to failTimes
-type clientWithRetry struct {
-	clientWithResp
-	failCount int
-	failTimes int
-}
-
-func (c *clientWithRetry) Create(ctx context.Context, key string, value string) (*client.Response, error) {
-	if c.failCount < c.failTimes {
-		c.failCount++
-		return nil, &client.ClusterError{Errors: []error{context.DeadlineExceeded}}
-	}
-	return c.clientWithResp.Create(ctx, key, value)
-}
-
-func (c *clientWithRetry) Get(ctx context.Context, key string, opts *client.GetOptions) (*client.Response, error) {
-	if c.failCount < c.failTimes {
-		c.failCount++
-		return nil, &client.ClusterError{Errors: []error{context.DeadlineExceeded}}
-	}
-	return c.clientWithResp.Get(ctx, key, opts)
-}
-
-// watcherWithRetry will timeout all requests up to failTimes
-type watcherWithRetry struct {
-	rs        []*client.Response
-	failCount int
-	failTimes int
-}
-
-func (w *watcherWithRetry) Next(context.Context) (*client.Response, error) {
-	if w.failCount < w.failTimes {
-		w.failCount++
-		return nil, &client.ClusterError{Errors: []error{context.DeadlineExceeded}}
-	}
-	if len(w.rs) == 0 {
-		return &client.Response{}, nil
-	}
-	r := w.rs[0]
-	w.rs = w.rs[1:]
-	return r, nil
-}
-
-func newTestDiscovery(t *testing.T, cluster string, id types.ID, c client.KeysAPI) *discovery {
-	return &discovery{
-		lg:      zaptest.NewLogger(t),
-		cluster: cluster,
-		id:      id,
-		c:       c,
-		url:     &url.URL{Scheme: "http", Host: "test.com"},
-	}
-}
-
-func newTestDiscoveryWithClock(t *testing.T, cluster string, id types.ID, c client.KeysAPI, clock clockwork.Clock) *discovery {
-	return &discovery{
-		lg:      zaptest.NewLogger(t),
-		cluster: cluster,
-		id:      id,
-		c:       c,
-		url:     &url.URL{Scheme: "http", Host: "test.com"},
-		clock:   clock,
-	}
-}
diff --git a/server/etcdserver/cluster_util_test.go b/server/etcdserver/cluster_util_test.go
index 1ca9ec134..d3f653644 100644
--- a/server/etcdserver/cluster_util_test.go
+++ b/server/etcdserver/cluster_util_test.go
@@ -17,11 +17,11 @@ package etcdserver
 import (
 	"testing"
 
-	"go.etcd.io/etcd/api/v3/version"
-	"go.etcd.io/etcd/client/pkg/v3/types"
-
 	"github.com/coreos/go-semver/semver"
 	"go.uber.org/zap/zaptest"
+
+	"go.etcd.io/etcd/api/v3/version"
+	"go.etcd.io/etcd/client/pkg/v3/types"
 )
 
 func TestIsCompatibleWithVers(t *testing.T) {
diff --git a/server/etcdserver/server_test.go b/server/etcdserver/server_test.go
index 5d4335d71..3fcf51627 100644
--- a/server/etcdserver/server_test.go
+++ b/server/etcdserver/server_test.go
@@ -1510,6 +1510,7 @@ func newTestCluster(tb testing.TB) *membership.RaftCluster {
 func newTestClusterWithBackend(tb testing.TB, membs []*membership.Member, be backend.Backend) *membership.RaftCluster {
 	lg := zaptest.NewLogger(tb)
 	c := membership.NewCluster(lg)
+	c.SetStore(v2store.New())
 	c.SetBackend(schema.NewMembershipBackend(lg, be))
 	for _, m := range membs {
 		c.AddMember(m, true)
diff --git a/server/storage/mvcc/watcher_test.go b/server/storage/mvcc/watcher_test.go
index e774c70cf..9a9f4f13a 100644
--- a/server/storage/mvcc/watcher_test.go
+++ b/server/storage/mvcc/watcher_test.go
@@ -289,21 +289,12 @@ func TestWatchStreamCancelWatcherByID(t *testing.T) {
 	}
 }
 
-// TestWatcherRequestProgress ensures synced watcher can correctly
-// report its correct progress.
-func TestWatcherRequestProgress(t *testing.T) {
+func TestWatcherRequestProgressBadId(t *testing.T) {
 	b, _ := betesting.NewDefaultTmpBackend(t)
 	s := newWatchableStore(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
 
 	defer cleanup(s, b)
-
-	testKey := []byte("foo")
-	notTestKey := []byte("bad")
-	testValue := []byte("bar")
-	s.Put(testKey, testValue, lease.NoLease)
-
 	w := s.NewWatchStream()
-
 	badID := WatchID(1000)
 	w.RequestProgress(badID)
 	select {
@@ -311,26 +302,79 @@ func TestWatcherRequestProgress(t *testing.T) {
 		t.Fatalf("unexpected %+v", resp)
 	default:
 	}
+}
 
-	id, _ := w.Watch(0, notTestKey, nil, 1)
-	w.RequestProgress(id)
-	select {
-	case resp := <-w.Chan():
-		t.Fatalf("unexpected %+v", resp)
-	default:
+func TestWatcherRequestProgress(t *testing.T) {
+	testKey := []byte("foo")
+	notTestKey := []byte("bad")
+	testValue := []byte("bar")
+	tcs := []struct {
+		name                     string
+		startRev                 int64
+		expectProgressBeforeSync bool
+		expectProgressAfterSync  bool
+	}{
+		{
+			name:                     "Zero revision",
+			startRev:                 0,
+			expectProgressBeforeSync: true,
+			expectProgressAfterSync:  true,
+		},
+		{
+			name:                    "Old revision",
+			startRev:                1,
+			expectProgressAfterSync: true,
+		},
+		{
+			name:                    "Current revision",
+			startRev:                2,
+			expectProgressAfterSync: true,
+		},
+		{
+			name:     "Current revision plus one",
+			startRev: 3,
+		},
+		{
+			name:     "Current revision plus two",
+			startRev: 4,
+		},
 	}
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			b, _ := betesting.NewDefaultTmpBackend(t)
+			s := newWatchableStore(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
 
-	s.syncWatchers([]mvccpb.Event{})
+			defer cleanup(s, b)
+
+			s.Put(testKey, testValue, lease.NoLease)
+
+			w := s.NewWatchStream()
+
+			id, _ := w.Watch(0, notTestKey, nil, tc.startRev)
+			w.RequestProgress(id)
+			asssertProgressSent(t, w, id, tc.expectProgressBeforeSync)
+			s.syncWatchers([]mvccpb.Event{})
+			w.RequestProgress(id)
+			asssertProgressSent(t, w, id, tc.expectProgressAfterSync)
+		})
+	}
+}
 
-	w.RequestProgress(id)
-	wrs := WatchResponse{WatchID: id, Revision: 2}
+func asssertProgressSent(t *testing.T, stream WatchStream, id WatchID, expectProgress bool) {
 	select {
-	case resp := <-w.Chan():
-		if !reflect.DeepEqual(resp, wrs) {
-			t.Fatalf("got %+v, expect %+v", resp, wrs)
+	case resp := <-stream.Chan():
+		if expectProgress {
+			wrs := WatchResponse{WatchID: id, Revision: 2}
+			if !reflect.DeepEqual(resp, wrs) {
+				t.Fatalf("got %+v, expect %+v", resp, wrs)
+			}
+		} else {
+			t.Fatalf("unexpected response %+v", resp)
+		}
+	default:
+		if expectProgress {
+			t.Fatalf("failed to receive progress")
 		}
-	case <-time.After(time.Second):
-		t.Fatal("failed to receive progress")
 	}
 }
 
diff --git a/tests/LICENSE b/tests/LICENSE
index d64569567..52fb9c29f 100644
--- a/tests/LICENSE
+++ b/tests/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright [yyyy] [name of copyright owner]
+   Copyright 2020 The etcd Authors
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/tests/antithesis/Dockerfile.config b/tests/antithesis/Dockerfile.config
deleted file mode 100644
index e34c18321..000000000
--- a/tests/antithesis/Dockerfile.config
+++ /dev/null
@@ -1,3 +0,0 @@
-from scratch
-
-COPY docker-compose.yml /docker-compose.yml
diff --git a/tests/antithesis/Makefile b/tests/antithesis/Makefile
index 590fe5eb6..835f5a5be 100644
--- a/tests/antithesis/Makefile
+++ b/tests/antithesis/Makefile
@@ -2,44 +2,76 @@ REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
 USER_ID := $(shell id -u)
 GROUP_ID := $(shell id -g)
 ARCH ?= $(shell go env GOARCH)
+REF = main
+IMAGE_TAG = latest
+
+CFG_NODE_COUNT ?= 3
 
 .PHONY: antithesis-build-client-docker-image
-antithesis-build-client-docker-image:
-	docker build --build-arg GO_VERSION=$(shell cat $(REPOSITORY_ROOT)/.go-version) -f $(REPOSITORY_ROOT)/tests/antithesis/test-template/Dockerfile $(REPOSITORY_ROOT) -t etcd-client:latest
+antithesis-build-client-docker-image: validate-node-count
+	docker build \
+		--build-arg GO_VERSION=$(shell cat $(REPOSITORY_ROOT)/.go-version) \
+		--build-arg CFG_NODE_COUNT=$(CFG_NODE_COUNT) \
+		-f $(REPOSITORY_ROOT)/tests/antithesis/test-template/Dockerfile $(REPOSITORY_ROOT) -t etcd-client:latest
 
 .PHONY: antithesis-build-etcd-image
 antithesis-build-etcd-image:
-	git fetch origin release-3.5
-	git worktree remove -f /tmp/etcd3.5 || true
-	git worktree add /tmp/etcd3.5 origin/release-3.5
-	cd /tmp/etcd3.5 && make build
-	cd /tmp/etcd3.5 && cp ./Dockerfile-release.$(ARCH) ./bin/Dockerfile
-	cd /tmp/etcd3.5 && docker build \
-		--tag etcd-server:latest \
-		./bin
-	git worktree remove -f /tmp/etcd3.5
+	docker build --build-arg GO_VERSION=$(shell cat $(REPOSITORY_ROOT)/.go-version) --build-arg REF=$(REF) $(REPOSITORY_ROOT)/tests/antithesis/server/ -t etcd-server:latest
+
+.PHONY: antithesis-build-etcd-image-release-3.4
+antithesis-build-etcd-image-release-3.4: REF=release-3.4
+antithesis-build-etcd-image-release-3.4: antithesis-build-etcd-image
+
+.PHONY: antithesis-build-etcd-image-release-3.5
+antithesis-build-etcd-image-release-3.5: REF=release-3.5
+antithesis-build-etcd-image-release-3.5: antithesis-build-etcd-image
+
+.PHONY: antithesis-build-etcd-image-release-3.6
+antithesis-build-etcd-image-release-3.6: REF=release-3.6
+antithesis-build-etcd-image-release-3.6: antithesis-build-etcd-image
+
+.PHONY: antithesis-build-etcd-image-main
+antithesis-build-etcd-image-main: REF=main
+antithesis-build-etcd-image-main: antithesis-build-etcd-image
+
+.PHONY: antithesis-build-config-image
+antithesis-build-config-image: validate-node-count
+	docker build -f config/Dockerfile config -t etcd-config:latest \
+		--build-arg IMAGE_TAG=$(IMAGE_TAG) \
+		--build-arg NODE_COUNT=$(CFG_NODE_COUNT)
 
 .PHONY: antithesis-docker-compose-up
-antithesis-docker-compose-up:
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose up
+antithesis-docker-compose-up: validate-node-count
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml up
 
 .PHONY: antithesis-run-container-traffic
-antithesis-run-container-traffic:
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose exec client /opt/antithesis/test/v1/robustness/singleton_driver_traffic
+antithesis-run-container-traffic: validate-node-count
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml exec client /opt/antithesis/test/v1/robustness/singleton_driver_traffic
 
 .PHONY: antithesis-run-container-validation
-antithesis-run-container-validation:
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose exec client /opt/antithesis/test/v1/robustness/finally_validation
+antithesis-run-container-validation: validate-node-count
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml exec client /opt/antithesis/test/v1/robustness/finally_validation
 
 .PHONY: antithesis-run-local-traffic
 antithesis-run-local-traffic:
-	go run --race ./test-template/robustness/traffic/main.go --local
+	go run -ldflags "-X main.NodeCount=$(CFG_NODE_COUNT)" --race ./test-template/robustness/traffic/main.go --local
 
 .PHONY: antithesis-run-local-validation
 antithesis-run-local-validation:
-	go run --race ./test-template/robustness/finally/main.go --local
+	go run -ldflags "-X main.NodeCount=$(CFG_NODE_COUNT)" --race ./test-template/robustness/finally/main.go --local
 
 .PHONY: antithesis-clean
-antithesis-clean:
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose down
+antithesis-clean: validate-node-count
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml down --remove-orphans
 	rm -rf /tmp/etcddata0 /tmp/etcddata1 /tmp/etcddata2 /tmp/etcdreport
+
+.PHONY: validate-node-count
+validate-node-count:
+	@if [ "$(CFG_NODE_COUNT)" != "1" ] && [ "$(CFG_NODE_COUNT)" != "3" ]; then \
+		echo "CFG_NODE_COUNT must be either 1 or 3 (got $(CFG_NODE_COUNT))"; \
+		exit 1; \
+	fi
diff --git a/tests/antithesis/README.md b/tests/antithesis/README.md
index 3927ff9a0..fb69b576a 100644
--- a/tests/antithesis/README.md
+++ b/tests/antithesis/README.md
@@ -11,6 +11,12 @@ make antithesis-build-client-docker-image
 make antithesis-build-etcd-image
 ```
 
+Both commands build etcd-server and etcd-client from the current branch. To build a different version of etcd you can use:
+
+```bash
+make antithesis-build-etcd-image REF=${GIT_REF} 
+```
+
 ### 2. (Optional) Check the Image Locally
 
 You can verify your new image is built:
@@ -33,6 +39,8 @@ Run the following command from the root directory for Antithesis tests (`tests/a
 make antithesis-docker-compose-up
 ```
 
+The command uses the etcd client and server images built from step 1.
+
 The client will continuously check the health of the etcd nodes and print logs similar to:
 
 ```
diff --git a/tests/antithesis/config/Dockerfile b/tests/antithesis/config/Dockerfile
new file mode 100644
index 000000000..9c796f6f3
--- /dev/null
+++ b/tests/antithesis/config/Dockerfile
@@ -0,0 +1,12 @@
+ARG GO_VERSION=1.24.4
+
+FROM golang:$GO_VERSION AS build
+RUN go install github.com/a8m/envsubst/cmd/envsubst@v1.4.3
+
+ARG IMAGE_TAG
+ARG NODE_COUNT
+COPY docker-compose-${NODE_COUNT}-node.yml /docker-compose.yml.template
+RUN IMAGE_TAG=${IMAGE_TAG} cat /docker-compose.yml.template | envsubst > /docker-compose.yml
+
+FROM scratch
+COPY --from=build /docker-compose.yml /docker-compose.yml
diff --git a/tests/antithesis/config/docker-compose-1-node.yml b/tests/antithesis/config/docker-compose-1-node.yml
new file mode 100644
index 000000000..37f8fe9ce
--- /dev/null
+++ b/tests/antithesis/config/docker-compose-1-node.yml
@@ -0,0 +1,54 @@
+---
+services:
+  # This is needed for creating non-root data folders on host.
+  # By default, if the folders don't exist when mounting, compose creates them with root as owner.
+  # With root owner, accessing the WAL files from local tests will fail due to an unauthorized access error.
+  init:
+    image: 'docker.io/library/ubuntu:latest'
+    user: root
+    group_add:
+      - '${GROUP_ID:-root}'
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcddata0
+      - ${ETCD_ROBUSTNESS_REPORT_PATH:-/tmp/etcdreport}:/var/report
+    command:
+      - /bin/sh
+      - -c
+      - |
+        rm -rf /var/etcddata0/* /var/report/*
+        chown -R ${USER_ID:-root}:${GROUP_ID:-root} /var/etcddata0 /var/report
+
+  etcd0:
+    image: 'etcd-server:${IMAGE_TAG:-latest}'
+    container_name: etcd0
+    hostname: etcd0
+    environment:
+      ETCD_NAME: "etcd0"
+      ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd0:2380"
+      ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
+      ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
+      ETCD_ADVERTISE_CLIENT_URLS: "http://etcd0.etcd:2379"
+      ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
+      ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380"
+      ETCD_INITIAL_CLUSTER_STATE: "new"
+      ETCD_DATA_DIR: "/var/etcd/data"
+    user: "${USER_ID:-root}:${GROUP_ID:-root}"
+    depends_on:
+      init:
+        condition: service_completed_successfully
+    ports:
+      - 12379:2379
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcd/data
+
+  client:
+    image: 'etcd-client:${IMAGE_TAG:-latest}'
+    container_name: client
+    entrypoint: ["/opt/antithesis/entrypoint/entrypoint"]
+    user: "${USER_ID:-root}:${GROUP_ID:-root}"
+    depends_on:
+      etcd0:
+        condition: service_started
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcddata0
+      - ${ETCD_ROBUSTNESS_REPORT_PATH:-/tmp/etcdreport}:/var/report
diff --git a/tests/antithesis/docker-compose.yml b/tests/antithesis/config/docker-compose-3-node.yml
similarity index 95%
rename from tests/antithesis/docker-compose.yml
rename to tests/antithesis/config/docker-compose-3-node.yml
index ec03689e7..e3ea9df8f 100644
--- a/tests/antithesis/docker-compose.yml
+++ b/tests/antithesis/config/docker-compose-3-node.yml
@@ -21,7 +21,7 @@ services:
         chown -R ${USER_ID:-root}:${GROUP_ID:-root} /var/etcddata0 /var/etcddata1 /var/etcddata2 /var/report
 
   etcd0:
-    image: 'etcd-server:latest'
+    image: 'etcd-server:${IMAGE_TAG:-latest}'
     container_name: etcd0
     hostname: etcd0
     environment:
@@ -44,7 +44,7 @@ services:
       - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcd/data
 
   etcd1:
-    image: 'etcd-server:latest'
+    image: 'etcd-server:${IMAGE_TAG:-latest}'
     container_name: etcd1
     hostname: etcd1
     environment:
@@ -67,7 +67,7 @@ services:
       - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}1:/var/etcd/data
 
   etcd2:
-    image: 'etcd-server:latest'
+    image: 'etcd-server:${IMAGE_TAG:-latest}'
     container_name: etcd2
     hostname: etcd2
     environment:
@@ -90,7 +90,7 @@ services:
       - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}2:/var/etcd/data
 
   client:
-    image: 'etcd-client:latest'
+    image: 'etcd-client:${IMAGE_TAG:-latest}'
     container_name: client
     entrypoint: ["/opt/antithesis/entrypoint/entrypoint"]
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
diff --git a/tests/antithesis/server/Dockerfile b/tests/antithesis/server/Dockerfile
new file mode 100644
index 000000000..415811a64
--- /dev/null
+++ b/tests/antithesis/server/Dockerfile
@@ -0,0 +1,43 @@
+ARG GO_VERSION=1.24.4
+ARG ARCH=amd64
+
+FROM golang:$GO_VERSION
+
+# cloning etcd
+ARG REF=main
+RUN git clone --depth=1 https://github.com/etcd-io/etcd.git --branch=${REF} /etcd
+WORKDIR /etcd
+
+# setup go mod
+RUN go mod download
+
+# install instrumentor
+RUN go get github.com/antithesishq/antithesis-sdk-go@a802e8810442e01d16b3e9df77d7ce3875e36e55 # v0.4.3
+RUN go install github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor@a802e8810442e01d16b3e9df77d7ce3875e36e55 # v0.4.3
+RUN go mod tidy
+
+# compile etcd server with instrumentor
+RUN mkdir /etcd_instrumented
+RUN `go env GOPATH`/bin/antithesis-go-instrumentor /etcd /etcd_instrumented
+RUN rm -rf /etcd
+RUN mkdir -p /symbols
+RUN cp -r /etcd_instrumented/symbols/* /symbols
+
+EXPOSE 2379 2380
+
+# start etcd server
+WORKDIR /etcd_instrumented/customer
+
+# Some previous versions hardcode CGO_ENABLED=0
+RUN find . -type f -exec sed -i 's/CGO_ENABLED=0/CGO_ENABLED=${CGO_ENABLED}/' {} +
+# Some previous versions explicitly need gobin, which could no longer be installed with go get
+RUN go install github.com/myitcv/gobin@v0.0.14
+# 3.4.0 has vendoring. need to do this after instrumentation or else build fails
+RUN if [ -d "vendor" ]; then go mod vendor; fi
+
+# The instrumentation adds code and packages. Need go mod tidy for all modules before building
+RUN for d in server etcdutl etcdctl; do (cd ${d} && go mod tidy || true); done
+# The instrumentation also adds a new main file which clashes with dummy.go found in non release-3.4 branches
+RUN if [ -f "dummy.go" ]; then sed -i 's/package main_test/package main/' dummy.go; fi
+RUN CGO_ENABLED=1 make build
+CMD ["./bin/etcd"]
diff --git a/tests/antithesis/test-template/Dockerfile b/tests/antithesis/test-template/Dockerfile
index 850857a79..04f9ee66e 100644
--- a/tests/antithesis/test-template/Dockerfile
+++ b/tests/antithesis/test-template/Dockerfile
@@ -1,11 +1,12 @@
-ARG GO_VERSION=1.24.3
+ARG GO_VERSION=1.24.4
 ARG ARCH=amd64
 
 FROM golang:$GO_VERSION
+ARG CFG_NODE_COUNT=3
 WORKDIR /build
 COPY . .
 
 WORKDIR /build/tests
-RUN go build -o /opt/antithesis/entrypoint/entrypoint -race ./antithesis/test-template/entrypoint/main.go
-RUN go build -o /opt/antithesis/test/v1/robustness/singleton_driver_traffic -race ./antithesis/test-template/robustness/traffic/main.go
-RUN go build -o /opt/antithesis/test/v1/robustness/finally_validation -race ./antithesis/test-template/robustness/finally/main.go
+RUN go build -ldflags "-X main.NodeCount=$CFG_NODE_COUNT" -o /opt/antithesis/entrypoint/entrypoint -race ./antithesis/test-template/entrypoint/main.go
+RUN go build -ldflags "-X main.NodeCount=$CFG_NODE_COUNT" -o /opt/antithesis/test/v1/robustness/singleton_driver_traffic -race ./antithesis/test-template/robustness/traffic/main.go
+RUN go build -ldflags "-X main.NodeCount=$CFG_NODE_COUNT" -o /opt/antithesis/test/v1/robustness/finally_validation -race ./antithesis/test-template/robustness/finally/main.go
diff --git a/tests/antithesis/test-template/entrypoint/main.go b/tests/antithesis/test-template/entrypoint/main.go
index cf2f46f75..ab9e365b3 100644
--- a/tests/antithesis/test-template/entrypoint/main.go
+++ b/tests/antithesis/test-template/entrypoint/main.go
@@ -24,14 +24,19 @@ import (
 	"github.com/antithesishq/antithesis-sdk-go/lifecycle"
 
 	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/tests/v3/antithesis/test-template/robustness/common"
 )
 
 // Sleep duration
 const SLEEP = 10
 
+var NodeCount = "3"
+
 // CheckHealth checks health of all etcd nodes
 func CheckHealth() bool {
-	nodeOptions := []string{"etcd0", "etcd1", "etcd2"}
+	cfg := common.MakeConfig(NodeCount)
+
+	nodeOptions := []string{"etcd0", "etcd1", "etcd2"}[:cfg.NodeCount]
 
 	// iterate over each node and check health
 	for _, node := range nodeOptions {
diff --git a/client/internal/v2/main_test.go b/tests/antithesis/test-template/robustness/common/config.go
similarity index 71%
rename from client/internal/v2/main_test.go
rename to tests/antithesis/test-template/robustness/common/config.go
index d40d87357..97a8ef396 100644
--- a/client/internal/v2/main_test.go
+++ b/tests/antithesis/test-template/robustness/common/config.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The etcd Authors
+// Copyright 2025 The etcd Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package client_test
+package common
 
-import (
-	"testing"
+import "strconv"
 
-	"go.etcd.io/etcd/client/pkg/v3/testutil"
-)
+type Config struct {
+	NodeCount int
+}
+
+func MakeConfig(nodeCount string) *Config {
+	cfg := &Config{}
+
+	cfg.NodeCount, _ = strconv.Atoi(nodeCount)
 
-func TestMain(m *testing.M) {
-	testutil.MustTestMainWithLeakDetection(m)
+	return cfg
 }
diff --git a/tests/antithesis/test-template/robustness/common/path.go b/tests/antithesis/test-template/robustness/common/path.go
index 7c0e9e02a..492da9ea2 100644
--- a/tests/antithesis/test-template/robustness/common/path.go
+++ b/tests/antithesis/test-template/robustness/common/path.go
@@ -38,23 +38,23 @@ const (
 	localReportPath          = "report"
 )
 
-func DefaultPaths() (string, []string, map[string]string) {
-	hosts := []string{defaultetcd0, defaultetcd1, defaultetcd2}
-	reportPath := defaultReportPath
-	dataPaths := etcdDataPaths(defaultetcdDataPath, len(hosts))
-	return reportPath, hosts, dataPaths
+func DefaultPaths(cfg *Config) (hosts []string, reportPath string, dataPaths map[string]string) {
+	hosts = []string{defaultetcd0, defaultetcd1, defaultetcd2}[:cfg.NodeCount]
+	reportPath = defaultReportPath
+	dataPaths = etcdDataPaths(defaultetcdDataPath, cfg.NodeCount)
+	return hosts, reportPath, dataPaths
 }
 
-func LocalPaths() (string, []string, map[string]string) {
-	hosts := []string{localetcd0, localetcd1, localetcd2}
-	reportPath := localReportPath
+func LocalPaths(cfg *Config) (hosts []string, reportPath string, dataPaths map[string]string) {
+	hosts = []string{localetcd0, localetcd1, localetcd2}[:cfg.NodeCount]
+	reportPath = localReportPath
 	etcdDataPath := defaultetcdLocalDataPath
 	envPath := os.Getenv(localetcdDataPathEnv)
 	if envPath != "" {
 		etcdDataPath = envPath + "%d"
 	}
-	dataPaths := etcdDataPaths(etcdDataPath, len(hosts))
-	return reportPath, hosts, dataPaths
+	dataPaths = etcdDataPaths(etcdDataPath, cfg.NodeCount)
+	return hosts, reportPath, dataPaths
 }
 
 func etcdDataPaths(dir string, amount int) map[string]string {
diff --git a/tests/antithesis/test-template/robustness/finally/main.go b/tests/antithesis/test-template/robustness/finally/main.go
index 852820bfb..da9b87e59 100644
--- a/tests/antithesis/test-template/robustness/finally/main.go
+++ b/tests/antithesis/test-template/robustness/finally/main.go
@@ -19,17 +19,16 @@ package main
 import (
 	"flag"
 	"maps"
+	"os"
 	"path/filepath"
 	"slices"
 	"time"
 
-	"github.com/anishathalye/porcupine"
 	"github.com/antithesishq/antithesis-sdk-go/assert"
 	"go.uber.org/zap"
 
 	"go.etcd.io/etcd/tests/v3/antithesis/test-template/robustness/common"
 	"go.etcd.io/etcd/tests/v3/robustness/report"
-	"go.etcd.io/etcd/tests/v3/robustness/traffic"
 	"go.etcd.io/etcd/tests/v3/robustness/validate"
 )
 
@@ -37,13 +36,17 @@ const (
 	reportFileName = "history.html"
 )
 
+var NodeCount = "3"
+
 func main() {
 	local := flag.Bool("local", false, "run finally locally and connect to etcd instances via localhost")
 	flag.Parse()
 
-	reportPath, _, dirs := common.DefaultPaths()
+	cfg := common.MakeConfig(NodeCount)
+
+	_, reportPath, dirs := common.DefaultPaths(cfg)
 	if *local {
-		reportPath, _, dirs = common.LocalPaths()
+		_, reportPath, dirs = common.LocalPaths(cfg)
 	}
 
 	lg, err := zap.NewProduction()
@@ -52,26 +55,41 @@ func main() {
 	}
 	reports, err := report.LoadClientReports(reportPath)
 	assert.Always(err == nil, "Loaded client reports", map[string]any{"error": err})
-	result := validateReports(lg, dirs, reports)
+	tf, err := report.LoadTrafficDetail(reportPath)
+	if err != nil && !os.IsNotExist(err) {
+		panic(err)
+	}
+
+	result := validateReports(lg, dirs, reports, tf)
 	if err := result.Linearization.Visualize(lg, filepath.Join(reportPath, reportFileName)); err != nil {
 		panic(err)
 	}
 }
 
-func validateReports(lg *zap.Logger, serversDataPath map[string]string, reports []report.ClientReport) validate.Result {
+func validateReports(lg *zap.Logger, serversDataPath map[string]string, reports []report.ClientReport, tf report.TrafficDetail) validate.RobustnessResult {
 	persistedRequests, err := report.PersistedRequests(lg, slices.Collect(maps.Values(serversDataPath)))
-	assert.Always(err == nil, "Loaded persisted requests", map[string]any{"error": err})
+	assertResult(validate.ResultFromError(err), "Loaded persisted requests")
 
-	validateConfig := validate.Config{ExpectRevisionUnique: traffic.EtcdAntithesis.ExpectUniqueRevision()}
+	validateConfig := validate.Config{ExpectRevisionUnique: tf.ExpectUniqueRevision}
 	result := validate.ValidateAndReturnVisualize(lg, validateConfig, reports, persistedRequests, 5*time.Minute)
-	assert.Always(result.Assumptions == nil, "Validation assumptions fulfilled", map[string]any{"error": result.Assumptions})
-	if result.Linearization.Linearizable == porcupine.Unknown {
+	assertResult(result.Assumptions, "Validation assumptions fulfilled")
+	if result.Linearization.Timeout {
 		assert.Unreachable("Linearization timeout", nil)
 	} else {
-		assert.Always(result.Linearization.Linearizable == porcupine.Ok, "Linearization validation passes", nil)
+		assertResult(result.Linearization.Result, "Linearization validation passes")
 	}
-	assert.Always(result.WatchError == nil, "Watch validation passes", map[string]any{"error": result.WatchError})
-	assert.Always(result.SerializableError == nil, "Serializable validation passes", map[string]any{"error": result.SerializableError})
+	assertResult(result.Watch, "Watch validation passes")
+	assertResult(result.Serializable, "Serializable validation passes")
 	lg.Info("Completed robustness validation")
 	return result
 }
+
+func assertResult(result validate.Result, name string) {
+	switch result.Status {
+	case validate.Success, validate.Failure:
+		assert.Always(result.Status == validate.Success, name, map[string]any{"msg": result.Message})
+	case validate.Unknown:
+	default:
+		assert.Unreachable(name, map[string]any{"msg": result.Message})
+	}
+}
diff --git a/tests/antithesis/test-template/robustness/traffic/main.go b/tests/antithesis/test-template/robustness/traffic/main.go
index 8a9f5bb3d..75424b92b 100644
--- a/tests/antithesis/test-template/robustness/traffic/main.go
+++ b/tests/antithesis/test-template/robustness/traffic/main.go
@@ -19,6 +19,7 @@ package main
 import (
 	"context"
 	"flag"
+	"math/rand/v2"
 	"os"
 	"slices"
 	"sync"
@@ -37,21 +38,35 @@ import (
 	"go.etcd.io/etcd/tests/v3/robustness/traffic"
 )
 
-var profile = traffic.Profile{
-	MinimalQPS:                     100,
-	MaximalQPS:                     1000,
-	BurstableQPS:                   1000,
-	ClientCount:                    3,
-	MaxNonUniqueRequestConcurrency: 3,
-}
+var (
+	profile = traffic.Profile{
+		MinimalQPS:                     100,
+		MaximalQPS:                     1000,
+		BurstableQPS:                   1000,
+		MemberClientCount:              3,
+		ClusterClientCount:             1,
+		MaxNonUniqueRequestConcurrency: 3,
+	}
+	trafficNames = []string{
+		"etcd",
+		"kubernetes",
+	}
+	traffics = []traffic.Traffic{
+		traffic.EtcdPutDeleteLease,
+		traffic.Kubernetes,
+	}
+	NodeCount = "3"
+)
 
 func main() {
 	local := flag.Bool("local", false, "run tests locally and connect to etcd instances via localhost")
 	flag.Parse()
 
-	reportPath, hosts, etcdetcdDataPaths := common.DefaultPaths()
+	cfg := common.MakeConfig(NodeCount)
+
+	hosts, reportPath, etcdetcdDataPaths := common.DefaultPaths(cfg)
 	if *local {
-		reportPath, hosts, etcdetcdDataPaths = common.LocalPaths()
+		hosts, reportPath, etcdetcdDataPaths = common.LocalPaths(cfg)
 	}
 
 	ctx := context.Background()
@@ -62,7 +77,10 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	r := report.TestReport{Logger: lg, ServersDataPath: etcdetcdDataPaths}
+	choice := rand.IntN(len(traffics))
+	tf := traffics[choice]
+	lg.Info("Traffic", zap.String("Type", trafficNames[choice]))
+	r := report.TestReport{Logger: lg, ServersDataPath: etcdetcdDataPaths, Traffic: &report.TrafficDetail{ExpectUniqueRevision: tf.ExpectUniqueRevision()}}
 	defer func() {
 		if err = r.Report(reportPath); err != nil {
 			lg.Error("Failed to save traffic generation report", zap.Error(err))
@@ -70,21 +88,21 @@ func main() {
 	}()
 
 	lg.Info("Start traffic generation", zap.Duration("duration", duration))
-	r.Client, err = runTraffic(ctx, lg, hosts, baseTime, duration)
+	r.Client, err = runTraffic(ctx, lg, tf, hosts, baseTime, duration)
 	if err != nil {
 		lg.Error("Failed to generate traffic")
 		panic(err)
 	}
 }
 
-func runTraffic(ctx context.Context, lg *zap.Logger, hosts []string, baseTime time.Time, duration time.Duration) ([]report.ClientReport, error) {
+func runTraffic(ctx context.Context, lg *zap.Logger, tf traffic.Traffic, hosts []string, baseTime time.Time, duration time.Duration) ([]report.ClientReport, error) {
 	ids := identity.NewIDProvider()
-	r, err := traffic.CheckEmptyDatabaseAtStart(ctx, lg, hosts, ids, baseTime)
+	trafficSet := client.NewSet(ids, baseTime)
+	defer trafficSet.Close()
+	err := traffic.CheckEmptyDatabaseAtStart(ctx, lg, hosts, trafficSet)
 	if err != nil {
 		lg.Fatal("Failed empty database at start check", zap.Error(err))
 	}
-	trafficReports := []report.ClientReport{r}
-	watchReport := []report.ClientReport{}
 	maxRevisionChan := make(chan int64, 1)
 	watchConfig := client.WatchConfig{
 		RequestProgress: true,
@@ -93,22 +111,23 @@ func runTraffic(ctx context.Context, lg *zap.Logger, hosts []string, baseTime ti
 	startTime := time.Since(baseTime)
 	g.Go(func() error {
 		defer close(maxRevisionChan)
-		trafficReports = slices.Concat(trafficReports, simulateTraffic(ctx, hosts, ids, baseTime, duration))
-		maxRevision := report.OperationsMaxRevision(trafficReports)
+		simulateTraffic(ctx, tf, hosts, trafficSet, duration)
+		maxRevision := report.OperationsMaxRevision(trafficSet.Reports())
 		maxRevisionChan <- maxRevision
 		lg.Info("Finished simulating Traffic", zap.Int64("max-revision", maxRevision))
 		return nil
 	})
+	watchSet := client.NewSet(ids, baseTime)
+	defer watchSet.Close()
 	g.Go(func() error {
-		var err error
-		watchReport, err = client.CollectClusterWatchEvents(ctx, lg, hosts, maxRevisionChan, watchConfig, baseTime, ids)
+		err := client.CollectClusterWatchEvents(ctx, lg, hosts, maxRevisionChan, watchConfig, watchSet)
 		return err
 	})
 	if err := g.Wait(); err != nil {
 		return nil, err
 	}
 	endTime := time.Since(baseTime)
-	reports := slices.Concat(trafficReports, watchReport)
+	reports := slices.Concat(trafficSet.Reports(), watchSet.Reports())
 	totalStats := traffic.CalculateStats(reports, startTime, endTime)
 	lg.Info("Completed traffic generation",
 		zap.Int("successes", totalStats.Successes),
@@ -120,39 +139,86 @@ func runTraffic(ctx context.Context, lg *zap.Logger, hosts []string, baseTime ti
 	return reports, nil
 }
 
-func simulateTraffic(ctx context.Context, hosts []string, ids identity.Provider, baseTime time.Time, duration time.Duration) []report.ClientReport {
-	var mux sync.Mutex
+func simulateTraffic(ctx context.Context, tf traffic.Traffic, hosts []string, clientSet *client.ClientSet, duration time.Duration) {
 	var wg sync.WaitGroup
 	storage := identity.NewLeaseIDStorage()
 	limiter := rate.NewLimiter(rate.Limit(profile.MaximalQPS), profile.BurstableQPS)
 	concurrencyLimiter := traffic.NewConcurrencyLimiter(profile.MaxNonUniqueRequestConcurrency)
 	finish := closeAfter(ctx, duration)
-	reports := []report.ClientReport{}
-	for i := 0; i < profile.ClientCount; i++ {
-		c := connect([]string{hosts[i%len(hosts)]}, ids, baseTime)
-		defer c.Close()
+	keyStore := traffic.NewKeyStore(10, "key")
+	for i := range profile.MemberClientCount {
+		c := connect(clientSet, []string{hosts[i%len(hosts)]})
 		wg.Add(1)
 		go func(c *client.RecordingClient) {
 			defer wg.Done()
 			defer c.Close()
-
-			traffic.EtcdAntithesis.RunTrafficLoop(ctx, c, limiter,
-				ids,
+			tf.RunTrafficLoop(ctx, c, limiter,
+				clientSet.IdentityProvider(),
+				storage,
+				concurrencyLimiter,
+				keyStore,
+				finish,
+			)
+		}(c)
+	}
+	for range profile.ClusterClientCount {
+		c := connect(clientSet, hosts)
+		wg.Add(1)
+		go func(c *client.RecordingClient) {
+			defer wg.Done()
+			defer c.Close()
+			tf.RunTrafficLoop(ctx, c, limiter,
+				clientSet.IdentityProvider(),
 				storage,
 				concurrencyLimiter,
+				keyStore,
 				finish,
 			)
-			mux.Lock()
-			reports = append(reports, c.Report())
-			mux.Unlock()
+		}(c)
+	}
+	wg.Add(1)
+	compactClient := connect(clientSet, hosts)
+	go func(c *client.RecordingClient) {
+		defer wg.Done()
+		defer c.Close()
+		tf.RunCompactLoop(ctx, c, traffic.DefaultCompactionPeriod, finish)
+	}(compactClient)
+	defragPeriod := traffic.DefaultCompactionPeriod * time.Duration(len(hosts))
+	for _, h := range hosts {
+		c := connect(clientSet, []string{h})
+		wg.Add(1)
+		go func(c *client.RecordingClient) {
+			defer wg.Done()
+			defer c.Close()
+			runDefragLoop(ctx, c, defragPeriod, finish)
 		}(c)
 	}
 	wg.Wait()
-	return reports
 }
 
-func connect(endpoints []string, ids identity.Provider, baseTime time.Time) *client.RecordingClient {
-	cli, err := client.NewRecordingClient(endpoints, ids, baseTime)
+func runDefragLoop(ctx context.Context, c *client.RecordingClient, period time.Duration, finish <-chan struct{}) {
+	jittered := time.Duration(robustnessrand.RandRange(int64(period-period/2), int64(period+period/2)))
+	ticker := time.NewTicker(jittered)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-finish:
+			return
+		case <-ticker.C:
+		}
+		dctx, cancel := context.WithTimeout(ctx, traffic.RequestTimeout)
+		_, err := c.Defragment(dctx)
+		cancel()
+		if err != nil {
+			continue
+		}
+	}
+}
+
+func connect(cs *client.ClientSet, endpoints []string) *client.RecordingClient {
+	cli, err := cs.NewClient(endpoints)
 	if err != nil {
 		// Antithesis Assertion: client should always be able to connect to an etcd host
 		assert.Unreachable("Client failed to connect to an etcd host", map[string]any{"endpoints": endpoints, "error": err})
diff --git a/tests/common/auth_test.go b/tests/common/auth_test.go
index fb995fa3c..f4a73c4ee 100644
--- a/tests/common/auth_test.go
+++ b/tests/common/auth_test.go
@@ -767,7 +767,7 @@ func TestAuthJWTExpire(t *testing.T) {
 
 func TestAuthJWTOnly(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: verifyJWTOnlyAuth}))
 	defer clus.Close()
diff --git a/tests/e2e/etcd_config_test.go b/tests/e2e/etcd_config_test.go
index 50e1aa50a..35264a45e 100644
--- a/tests/e2e/etcd_config_test.go
+++ b/tests/e2e/etcd_config_test.go
@@ -735,3 +735,55 @@ func TestV2DeprecationEnforceDefaultValue(t *testing.T) {
 		})
 	}
 }
+
+func TestEtcdAdvertiseClientUnix(t *testing.T) {
+	e2e.SkipInShortMode(t)
+
+	// Create a temporary directory for the data directory
+	dataDir := t.TempDir()
+	socketDir := t.TempDir()
+	unixSocket := fmt.Sprintf("unix://%s/etcd-client.sock", socketDir)
+
+	// Start etcd with AdvertiseClientUrls set to a unix socket
+	proc, err := e2e.SpawnCmd(
+		[]string{
+			e2e.BinPath.Etcd,
+			"--data-dir", dataDir,
+			"--name", "etcd1",
+			"--listen-client-urls", unixSocket,
+			"--advertise-client-urls", unixSocket,
+		}, nil,
+	)
+	require.NoError(t, err)
+	defer func() {
+		_ = proc.Stop()
+		_ = proc.Close()
+	}()
+
+	// Wait for the process to be ready
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, e2e.EtcdServerReadyLines))
+
+	// Write a key/value pair using etcdctl with unix socket
+	putArgs := []string{
+		e2e.BinPath.Etcdctl,
+		"--endpoints", unixSocket,
+		"put", "foo", "bar",
+	}
+	putProc, err := e2e.SpawnCmd(putArgs, nil)
+	require.NoError(t, err)
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), putProc, []string{"OK"}))
+	require.NoError(t, putProc.Stop())
+	_ = putProc.Close()
+
+	// Read the key back using etcdctl with unix socket
+	getArgs := []string{
+		e2e.BinPath.Etcdctl,
+		"--endpoints", unixSocket,
+		"get", "foo",
+	}
+	getProc, err := e2e.SpawnCmd(getArgs, nil)
+	require.NoError(t, err)
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), getProc, []string{"foo", "bar"}))
+	require.NoError(t, getProc.Stop())
+	_ = getProc.Close()
+}
diff --git a/tests/e2e/etcd_release_upgrade_test.go b/tests/e2e/etcd_release_upgrade_test.go
index 18e08f7d3..3a057b68d 100644
--- a/tests/e2e/etcd_release_upgrade_test.go
+++ b/tests/e2e/etcd_release_upgrade_test.go
@@ -199,7 +199,7 @@ func TestClusterUpgradeAfterPromotingMembers(t *testing.T) {
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx := context.Background()
+			ctx := t.Context()
 
 			epc, _ := mustCreateNewClusterByPromotingMembers(t, e2e.LastVersion, clusterSize,
 				e2e.WithSnapshotCount(uint64(tc.snapshot)))
@@ -220,7 +220,7 @@ func TestClusterUpgradeAfterPromotingMembers(t *testing.T) {
 
 			t.Logf("Checking all members are ready to serve client requests")
 			for i := 0; i < clusterSize; i++ {
-				err = epc.Procs[i].Etcdctl().Put(context.Background(), "foo", "bar", config.PutOptions{})
+				err = epc.Procs[i].Etcdctl().Put(t.Context(), "foo", "bar", config.PutOptions{})
 				require.NoError(t, err)
 			}
 		})
@@ -230,7 +230,7 @@ func TestClusterUpgradeAfterPromotingMembers(t *testing.T) {
 func mustCreateNewClusterByPromotingMembers(t *testing.T, clusterVersion e2e.ClusterVersion, clusterSize int, opts ...e2e.EPClusterOption) (*e2e.EtcdProcessCluster, []*etcdserverpb.Member) {
 	require.GreaterOrEqualf(t, clusterSize, 1, "clusterSize must be at least 1")
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	t.Logf("Creating new etcd cluster - version: %s, clusterSize: %v", clusterVersion, clusterSize)
 	opts = append(opts, e2e.WithVersion(clusterVersion), e2e.WithClusterSize(1))
@@ -284,7 +284,7 @@ func mustCreateNewClusterByPromotingMembers(t *testing.T, clusterVersion e2e.Clu
 }
 
 func ensureAllMembersAreVotingMembers(t *testing.T, epc *e2e.EtcdProcessCluster) {
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 
 	resp, err := epc.Etcdctl().MemberList(ctx, false)
diff --git a/tests/e2e/force_new_cluster_test.go b/tests/e2e/force_new_cluster_test.go
new file mode 100644
index 000000000..b62d26510
--- /dev/null
+++ b/tests/e2e/force_new_cluster_test.go
@@ -0,0 +1,136 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !cluster_proxy
+
+package e2e
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"go.etcd.io/bbolt"
+	"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
+	"go.etcd.io/etcd/server/v3/storage/datadir"
+	"go.etcd.io/etcd/server/v3/storage/schema"
+	"go.etcd.io/etcd/tests/v3/framework/config"
+	"go.etcd.io/etcd/tests/v3/framework/e2e"
+)
+
+// TestForceNewCluster verified that etcd works as expected when --force-new-cluster.
+// Refer to discussion in https://github.com/etcd-io/etcd/issues/20009.
+func TestForceNewCluster(t *testing.T) {
+	e2e.BeforeTest(t)
+
+	testCases := []struct {
+		name      string
+		snapcount int
+	}{
+		{
+			name:      "create a snapshot after promotion",
+			snapcount: 10,
+		},
+		{
+			name:      "no snapshot after promotion",
+			snapcount: 0,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			epc, promotedMembers := mustCreateNewClusterByPromotingMembers(t, e2e.CurrentVersion, 5,
+				e2e.WithKeepDataDir(true), e2e.WithSnapshotCount(uint64(tc.snapcount)))
+			require.Len(t, promotedMembers, 4)
+
+			for i := 0; i < tc.snapcount; i++ {
+				err := epc.Etcdctl().Put(t.Context(), "foo", "bar", config.PutOptions{})
+				require.NoError(t, err)
+			}
+
+			require.NoError(t, epc.Close())
+
+			m := epc.Procs[0]
+			t.Logf("Forcibly create a one-member cluster with member: %s", m.Config().Name)
+			m.Config().Args = append(m.Config().Args, "--force-new-cluster")
+			require.NoError(t, m.Start(t.Context()))
+
+			t.Log("Restarting the member")
+			require.NoError(t, m.Restart(t.Context()))
+
+			t.Log("Closing the member")
+			require.NoError(t, m.Close())
+		})
+	}
+}
+
+func TestForceNewCluster_MemberCount(t *testing.T) {
+	e2e.BeforeTest(t)
+
+	epc, promotedMembers := mustCreateNewClusterByPromotingMembers(t, e2e.CurrentVersion, 3, e2e.WithKeepDataDir(true))
+	require.Len(t, promotedMembers, 2)
+
+	// Wait for the backend TXN to sync/commit the data to disk, to ensure
+	// the consistent-index is persisted. Another way is to issue a snapshot
+	// command to forcibly commit the backend TXN.
+	time.Sleep(time.Second)
+
+	t.Log("Killing all the members")
+	require.NoError(t, epc.Kill())
+	require.NoError(t, epc.Wait(t.Context()))
+
+	m := epc.Procs[0]
+	t.Logf("Forcibly create a one-member cluster with member: %s", m.Config().Name)
+	m.Config().Args = append(m.Config().Args, "--force-new-cluster")
+	require.NoError(t, m.Start(t.Context()))
+
+	t.Log("Online checking the member count")
+	mresp, merr := m.Etcdctl().MemberList(t.Context(), false)
+	require.NoError(t, merr)
+	require.Len(t, mresp.Members, 1)
+
+	t.Log("Closing the member")
+	require.NoError(t, m.Close())
+	require.NoError(t, m.Wait(t.Context()))
+
+	t.Log("Offline checking the member count")
+	members := mustReadMembersFromBoltDB(t, m.Config().DataDirPath)
+	require.Len(t, members, 1)
+}
+
+func mustReadMembersFromBoltDB(t *testing.T, dataDir string) []*membership.Member {
+	dbPath := datadir.ToBackendFileName(dataDir)
+	db, err := bbolt.Open(dbPath, 0o400, &bbolt.Options{ReadOnly: true})
+	require.NoError(t, err)
+	defer func() {
+		require.NoError(t, db.Close())
+	}()
+
+	var members []*membership.Member
+	_ = db.View(func(tx *bbolt.Tx) error {
+		b := tx.Bucket(schema.Members.Name())
+		_ = b.ForEach(func(k, v []byte) error {
+			m := membership.Member{}
+			err := json.Unmarshal(v, &m)
+			require.NoError(t, err)
+			members = append(members, &m)
+			return nil
+		})
+		return nil
+	})
+
+	return members
+}
diff --git a/tests/e2e/reproduce_19406_test.go b/tests/e2e/reproduce_19406_test.go
index be942aaba..2fce10e56 100644
--- a/tests/e2e/reproduce_19406_test.go
+++ b/tests/e2e/reproduce_19406_test.go
@@ -15,17 +15,16 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"net/url"
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/pkg/v3/stringutil"
 	"go.etcd.io/etcd/tests/v3/framework/e2e"
-
-	"github.com/stretchr/testify/require"
 )
 
 // TestReproduce19406 reproduces the issue: https://github.com/etcd-io/etcd/issues/19406
@@ -33,7 +32,7 @@ func TestReproduce19406(t *testing.T) {
 	e2e.BeforeTest(t)
 
 	compactionSleepInterval := 100 * time.Millisecond
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	clus, cerr := e2e.NewEtcdProcessCluster(ctx, t,
 		e2e.WithClusterSize(1),
diff --git a/tests/framework/e2e/cluster.go b/tests/framework/e2e/cluster.go
index 09444dd51..f942a17fb 100644
--- a/tests/framework/e2e/cluster.go
+++ b/tests/framework/e2e/cluster.go
@@ -177,10 +177,6 @@ type EtcdProcessClusterConfig struct {
 	IsPeerTLS          bool
 	IsPeerAutoTLS      bool
 	CN                 bool
-
-	// Discovery is for v2discovery
-	// Note: remove this field when we remove the v2discovery
-	Discovery string // v2 discovery
 }
 
 func DefaultConfig() *EtcdProcessClusterConfig {
@@ -294,10 +290,6 @@ func WithRollingStart(rolling bool) EPClusterOption {
 	return func(c *EtcdProcessClusterConfig) { c.RollingStart = rolling }
 }
 
-func WithDiscovery(discovery string) EPClusterOption {
-	return func(c *EtcdProcessClusterConfig) { c.Discovery = discovery }
-}
-
 func WithDiscoveryEndpoints(endpoints []string) EPClusterOption {
 	return func(c *EtcdProcessClusterConfig) { c.ServerConfig.DiscoveryCfg.Endpoints = endpoints }
 }
@@ -507,7 +499,7 @@ func (cfg *EtcdProcessClusterConfig) EtcdAllServerProcessConfigs(tb testing.TB)
 }
 
 func (cfg *EtcdProcessClusterConfig) SetInitialOrDiscovery(serverCfg *EtcdServerProcessConfig, initialCluster []string, initialClusterState string) {
-	if cfg.Discovery == "" && len(cfg.ServerConfig.DiscoveryCfg.Endpoints) == 0 {
+	if len(cfg.ServerConfig.DiscoveryCfg.Endpoints) == 0 {
 		serverCfg.InitialCluster = strings.Join(initialCluster, ",")
 		serverCfg.Args = append(serverCfg.Args, "--initial-cluster="+serverCfg.InitialCluster)
 		serverCfg.Args = append(serverCfg.Args, "--initial-cluster-state="+initialClusterState)
@@ -605,10 +597,6 @@ func (cfg *EtcdProcessClusterConfig) EtcdServerProcessConfig(tb testing.TB, i in
 
 	args = append(args, cfg.TLSArgs()...)
 
-	if cfg.Discovery != "" {
-		args = append(args, "--discovery="+cfg.Discovery)
-	}
-
 	execPath := cfg.binaryPath(i)
 
 	if cfg.ServerConfig.SnapshotCatchUpEntries != etcdserver.DefaultSnapshotCatchUpEntries {
@@ -617,6 +605,16 @@ func (cfg *EtcdProcessClusterConfig) EtcdServerProcessConfig(tb testing.TB, i in
 		}
 	}
 
+	var (
+		binaryVersion *semver.Version
+		err           error
+	)
+	if execPath != "" {
+		binaryVersion, err = GetVersionFromBinary(execPath)
+		if err != nil {
+			tb.Logf("Failed to get binary version from %s: %v", execPath, err)
+		}
+	}
 	defaultValues := values(*embed.NewConfig())
 	overrideValues := values(cfg.ServerConfig)
 	for flag, value := range overrideValues {
@@ -626,7 +624,7 @@ func (cfg *EtcdProcessClusterConfig) EtcdServerProcessConfig(tb testing.TB, i in
 		if strings.HasSuffix(flag, "snapshot-catchup-entries") && !CouldSetSnapshotCatchupEntries(execPath) {
 			continue
 		}
-		args = append(args, fmt.Sprintf("--%s=%s", flag, value))
+		args = append(args, convertFlag(flag, value, binaryVersion))
 	}
 	envVars := map[string]string{}
 	maps.Copy(envVars, cfg.EnvVars)
@@ -984,6 +982,38 @@ func (epc *EtcdProcessCluster) rollingStart(f func(ep EtcdProcess) error) error
 	return nil
 }
 
+func (epc *EtcdProcessCluster) Kill() (err error) {
+	for _, p := range epc.Procs {
+		if p == nil {
+			continue
+		}
+		if curErr := p.Kill(); curErr != nil {
+			if err != nil {
+				err = fmt.Errorf("%w; %w", err, curErr)
+			} else {
+				err = curErr
+			}
+		}
+	}
+	return err
+}
+
+func (epc *EtcdProcessCluster) Wait(ctx context.Context) error {
+	closedC := make(chan error, len(epc.Procs))
+	for i := range epc.Procs {
+		go func(n int) {
+			epc.Procs[n].Wait(ctx)
+			closedC <- epc.Procs[n].Wait(ctx)
+		}(i)
+	}
+	for range epc.Procs {
+		if err := <-closedC; err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (epc *EtcdProcessCluster) Stop() (err error) {
 	for _, p := range epc.Procs {
 		if p == nil {
diff --git a/tests/framework/e2e/cluster_test.go b/tests/framework/e2e/cluster_test.go
index 1b45c0b06..1691a3458 100644
--- a/tests/framework/e2e/cluster_test.go
+++ b/tests/framework/e2e/cluster_test.go
@@ -120,13 +120,6 @@ func TestEtcdServerProcessConfig(t *testing.T) {
 				"--listen-metrics-urls=http://localhost:2",
 			},
 		},
-		{
-			name:   "Discovery",
-			config: NewConfig(WithDiscovery("123")),
-			expectArgsContain: []string{
-				"--discovery=123",
-			},
-		},
 		{
 			name:   "ClientTLS",
 			config: NewConfig(WithClientConnType(ClientTLS)),
diff --git a/tests/framework/e2e/config.go b/tests/framework/e2e/config.go
index 646a868e9..3cdc3f692 100644
--- a/tests/framework/e2e/config.go
+++ b/tests/framework/e2e/config.go
@@ -14,6 +14,15 @@
 
 package e2e
 
+import (
+	"fmt"
+	"strings"
+
+	"github.com/coreos/go-semver/semver"
+
+	"go.etcd.io/etcd/api/v3/version"
+)
+
 type ClusterVersion string
 
 const (
@@ -33,3 +42,60 @@ func (cv ClusterVersion) String() string {
 type ClusterContext struct {
 	Version ClusterVersion
 }
+
+var experimentalFlags = map[string]struct{}{
+	"compact-hash-check-time":              {},
+	"corrupt-check-time":                   {},
+	"compaction-batch-limit":               {},
+	"watch-progress-notify-interval":       {},
+	"warning-apply-duration":               {},
+	"bootstrap-defrag-threshold-megabytes": {},
+	"memory-mlock":                         {},
+	"snapshot-catchup-entries":             {},
+	"compaction-sleep-interval":            {},
+	"downgrade-check-time":                 {},
+	"peer-skip-client-san-verification":    {},
+	"enable-distributed-tracing":           {},
+	"distributed-tracing-address":          {},
+	"distributed-tracing-service-name":     {},
+	"distributed-tracing-instance-id":      {},
+	"distributed-tracing-sampling-rate":    {},
+}
+
+// convertFlag converts between experimental and non-experimental flags.
+// All experimental flags have been removed in version 3.7, but versions prior
+// to 3.6 only support experimental flags. The robustness tests use the same
+// code from the main branch to test all previously supported releases, so we
+// need to convert flags accordingly based on the binary version.
+func convertFlag(name, value string, ver *semver.Version) string {
+	// For versions >= 3.6, use the normal (non-experimental) flag.
+	if ver == nil || !ver.LessThan(version.V3_6) {
+		return fmt.Sprintf("--%s=%s", name, value)
+	}
+
+	// For versions < 3.6, use the experimental flag if it exists in `experimentalFlags`
+	if _, ok := experimentalFlags[name]; ok {
+		return fmt.Sprintf("--experimental-%s=%s", name, value)
+	}
+
+	return fmt.Sprintf("--%s=%s", name, value)
+}
+
+func convertFlags(args []string, ver *semver.Version) []string {
+	var retArgs []string
+
+	for _, arg := range args {
+		kv := strings.Split(arg, "=")
+		if len(kv) != 2 {
+			retArgs = append(retArgs, arg)
+			continue
+		}
+
+		name := strings.TrimPrefix(kv[0], "--")
+		name = strings.TrimPrefix(name, "experimental-")
+
+		retArgs = append(retArgs, convertFlag(name, kv[1], ver))
+	}
+
+	return retArgs
+}
diff --git a/tests/framework/e2e/downgrade.go b/tests/framework/e2e/downgrade.go
index 432a422f0..ecc22d591 100644
--- a/tests/framework/e2e/downgrade.go
+++ b/tests/framework/e2e/downgrade.go
@@ -147,6 +147,11 @@ func DowngradeUpgradeMembersByID(t *testing.T, lg *zap.Logger, clus *EtcdProcess
 		newExecPath = BinPath.EtcdLastRelease
 	}
 
+	binaryVersion, err := GetVersionFromBinary(newExecPath)
+	if err != nil {
+		return fmt.Errorf("failed to get binary version from %s: %w", newExecPath, err)
+	}
+
 	g := new(errgroup.Group)
 	for _, memberID := range membersToChange {
 		member := clus.Procs[memberID]
@@ -157,6 +162,11 @@ func DowngradeUpgradeMembersByID(t *testing.T, lg *zap.Logger, clus *EtcdProcess
 		if err := member.Stop(); err != nil {
 			return err
 		}
+
+		// When we downgrade or upgrade a member, we need to re-generate the flags, to convert some non-experimental
+		// flags to experimental flags, or vice verse.
+		member.Config().Args = convertFlags(member.Config().Args, binaryVersion)
+
 		member.Config().ExecPath = newExecPath
 		lg.Info("Restarting member", zap.String("member", member.Config().Name))
 		// We shouldn't block on waiting for the member to be ready,
diff --git a/tests/framework/integration/cluster.go b/tests/framework/integration/cluster.go
index e4d7620ac..2fcbba7d7 100644
--- a/tests/framework/integration/cluster.go
+++ b/tests/framework/integration/cluster.go
@@ -139,8 +139,6 @@ type ClusterConfig struct {
 	PeerTLS   *transport.TLSInfo
 	ClientTLS *transport.TLSInfo
 
-	DiscoveryURL string
-
 	AuthToken string
 
 	QuotaBackendBytes    int64
@@ -196,11 +194,6 @@ func SchemeFromTLSInfo(tls *transport.TLSInfo) string {
 
 // fillClusterForMembers fills up Member.InitialPeerURLsMap from each member's [name, scheme and PeerListeners address]
 func (c *Cluster) fillClusterForMembers() error {
-	if c.Cfg.DiscoveryURL != "" {
-		// Cluster will be discovered
-		return nil
-	}
-
 	addrs := make([]string, 0)
 	for _, m := range c.Members {
 		scheme := SchemeFromTLSInfo(m.PeerTLSInfo)
@@ -296,7 +289,6 @@ func (c *Cluster) MustNewMember(t testutil.TB) *Member {
 			CorruptCheckTime:            c.Cfg.CorruptCheckTime,
 			Metrics:                     c.Cfg.Metrics,
 		})
-	m.DiscoveryURL = c.Cfg.DiscoveryURL
 	return m
 }
 
diff --git a/tests/go.mod b/tests/go.mod
index 91dd6690e..963bd9bc8 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -2,12 +2,11 @@ module go.etcd.io/etcd/tests/v3
 
 go 1.24
 
-toolchain go1.24.3
+toolchain go1.24.4
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
 	go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
-	go.etcd.io/etcd/client/v2 => ./../client/internal/v2
 	go.etcd.io/etcd/client/v3 => ../client/v3
 	go.etcd.io/etcd/etcdctl/v3 => ../etcdctl
 	go.etcd.io/etcd/etcdutl/v3 => ../etcdutl
@@ -23,13 +22,14 @@ require (
 	github.com/golang/protobuf v1.5.4
 	github.com/google/go-cmp v0.7.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.2
-	github.com/prometheus/common v0.64.0
+	github.com/prometheus/common v0.65.0
 	github.com/soheilhy/cmux v0.1.5
 	github.com/stretchr/testify v1.10.0
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
@@ -39,16 +39,16 @@ require (
 	go.etcd.io/etcd/server/v3 v3.6.0-alpha.0
 	go.etcd.io/gofail v0.2.0
 	go.etcd.io/raft/v3 v3.6.0
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
-	go.opentelemetry.io/otel v1.35.0
-	go.opentelemetry.io/otel/sdk v1.35.0
-	go.opentelemetry.io/otel/trace v1.35.0
-	go.opentelemetry.io/proto/otlp v1.6.0
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0
+	go.opentelemetry.io/otel v1.37.0
+	go.opentelemetry.io/otel/sdk v1.37.0
+	go.opentelemetry.io/otel/trace v1.37.0
+	go.opentelemetry.io/proto/otlp v1.7.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.38.0
-	golang.org/x/sync v0.14.0
-	golang.org/x/time v0.11.0
-	google.golang.org/grpc v1.72.1
+	golang.org/x/crypto v0.39.0
+	golang.org/x/sync v0.15.0
+	golang.org/x/time v0.12.0
+	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 )
 
@@ -56,7 +56,7 @@ require (
 	github.com/VividCortex/ewma v1.2.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bgentry/speakeasy v0.2.0 // indirect
-	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cheggaaa/pb/v3 v3.1.7 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
@@ -64,7 +64,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fatih/color v1.18.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
@@ -78,7 +78,9 @@ require (
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/olekukonko/tablewriter v0.0.5 // indirect
+	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
+	github.com/olekukonko/ll v0.0.8 // indirect
+	github.com/olekukonko/tablewriter v1.0.7 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
@@ -87,20 +89,17 @@ require (
 	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
-	go.etcd.io/bbolt v1.4.0 // indirect
-	go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	golang.org/x/text v0.25.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/tests/go.sum b/tests/go.sum
index 3f6f5d4a4..5b36f88d8 100644
--- a/tests/go.sum
+++ b/tests/go.sum
@@ -10,8 +10,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
 github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -43,8 +43,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -74,12 +74,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -100,13 +100,16 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
+github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
+github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
+github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -117,8 +120,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -149,32 +152,32 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
-go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA=
 go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
-go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
-go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
+go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -187,8 +190,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -204,16 +207,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
+golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -225,10 +228,10 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
-golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -246,17 +249,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
-google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
-google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -270,7 +273,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
-sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/tests/integration/clientv3/cluster_test.go b/tests/integration/clientv3/cluster_test.go
index 055da70f3..5abc46eca 100644
--- a/tests/integration/clientv3/cluster_test.go
+++ b/tests/integration/clientv3/cluster_test.go
@@ -37,7 +37,7 @@ func TestMemberList(t *testing.T) {
 
 	capi := clus.RandClient()
 
-	resp, err := capi.MemberList(context.Background())
+	resp, err := capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
@@ -56,7 +56,7 @@ func TestMemberAdd(t *testing.T) {
 	capi := clus.RandClient()
 
 	urls := []string{"http://127.0.0.1:1234"}
-	resp, err := capi.MemberAdd(context.Background(), urls)
+	resp, err := capi.MemberAdd(t.Context(), urls)
 	if err != nil {
 		t.Fatalf("failed to add member %v", err)
 	}
@@ -74,13 +74,13 @@ func TestMemberAddWithExistingURLs(t *testing.T) {
 
 	capi := clus.RandClient()
 
-	resp, err := capi.MemberList(context.Background())
+	resp, err := capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
 
 	existingURL := resp.Members[0].PeerURLs[0]
-	_, err = capi.MemberAdd(context.Background(), []string{existingURL})
+	_, err = capi.MemberAdd(t.Context(), []string{existingURL})
 	expectedErrKeywords := "Peer URLs already exists"
 	if err == nil {
 		t.Fatalf("expecting add member to fail, got no error")
@@ -97,7 +97,7 @@ func TestMemberRemove(t *testing.T) {
 	defer clus.Terminate(t)
 
 	capi := clus.Client(1)
-	resp, err := capi.MemberList(context.Background())
+	resp, err := capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
@@ -113,12 +113,12 @@ func TestMemberRemove(t *testing.T) {
 		}
 	}
 
-	_, err = capi.MemberRemove(context.Background(), rmvID)
+	_, err = capi.MemberRemove(t.Context(), rmvID)
 	if err != nil {
 		t.Fatalf("failed to remove member %v", err)
 	}
 
-	resp, err = capi.MemberList(context.Background())
+	resp, err = capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
@@ -135,18 +135,18 @@ func TestMemberUpdate(t *testing.T) {
 	defer clus.Terminate(t)
 
 	capi := clus.RandClient()
-	resp, err := capi.MemberList(context.Background())
+	resp, err := capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
 
 	urls := []string{"http://127.0.0.1:1234"}
-	_, err = capi.MemberUpdate(context.Background(), resp.Members[0].ID, urls)
+	_, err = capi.MemberUpdate(t.Context(), resp.Members[0].ID, urls)
 	if err != nil {
 		t.Fatalf("failed to update member %v", err)
 	}
 
-	resp, err = capi.MemberList(context.Background())
+	resp, err = capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
@@ -178,11 +178,11 @@ func TestMemberAddUpdateWrongURLs(t *testing.T) {
 		{"localhost:1234"},
 	}
 	for i := range tt {
-		_, err := capi.MemberAdd(context.Background(), tt[i])
+		_, err := capi.MemberAdd(t.Context(), tt[i])
 		if err == nil {
 			t.Errorf("#%d: MemberAdd err = nil, but error", i)
 		}
-		_, err = capi.MemberUpdate(context.Background(), 0, tt[i])
+		_, err = capi.MemberUpdate(t.Context(), 0, tt[i])
 		if err == nil {
 			t.Errorf("#%d: MemberUpdate err = nil, but error", i)
 		}
@@ -198,7 +198,7 @@ func TestMemberAddForLearner(t *testing.T) {
 	capi := clus.RandClient()
 
 	urls := []string{"http://127.0.0.1:1234"}
-	resp, err := capi.MemberAddAsLearner(context.Background(), urls)
+	resp, err := capi.MemberAddAsLearner(t.Context(), urls)
 	if err != nil {
 		t.Fatalf("failed to add member %v", err)
 	}
@@ -234,7 +234,7 @@ func TestMemberPromote(t *testing.T) {
 
 	learnerMember := clus.MustNewMember(t)
 	urls := learnerMember.PeerURLs.StringSlice()
-	memberAddResp, err := capi.MemberAddAsLearner(context.Background(), urls)
+	memberAddResp, err := capi.MemberAddAsLearner(t.Context(), urls)
 	if err != nil {
 		t.Fatalf("failed to add member %v", err)
 	}
@@ -256,7 +256,7 @@ func TestMemberPromote(t *testing.T) {
 
 	// learner is not started yet. Expect learner progress check to fail.
 	// As the result, member promote request will fail.
-	_, err = capi.MemberPromote(context.Background(), learnerID)
+	_, err = capi.MemberPromote(t.Context(), learnerID)
 	expectedErrKeywords := "can only promote a learner member which is in sync with leader"
 	if err == nil {
 		t.Fatalf("expecting promote not ready learner to fail, got no error")
@@ -280,7 +280,7 @@ func TestMemberPromote(t *testing.T) {
 			t.Fatalf("failed all attempts to promote learner member, last error: %v", err)
 		}
 
-		_, err = capi.MemberPromote(context.Background(), learnerID)
+		_, err = capi.MemberPromote(t.Context(), learnerID)
 		// successfully promoted learner
 		if err == nil {
 			break
@@ -308,7 +308,7 @@ func TestMemberPromoteMemberNotLearner(t *testing.T) {
 	followerIdx := (leaderIdx + 1) % 3
 	cli := clus.Client(followerIdx)
 
-	resp, err := cli.MemberList(context.Background())
+	resp, err := cli.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
@@ -319,7 +319,7 @@ func TestMemberPromoteMemberNotLearner(t *testing.T) {
 	// promoting any of the voting members in cluster should fail
 	expectedErrKeywords := "can only promote a learner member"
 	for _, m := range resp.Members {
-		_, err = cli.MemberPromote(context.Background(), m.ID)
+		_, err = cli.MemberPromote(t.Context(), m.ID)
 		if err == nil {
 			t.Fatalf("expect promoting voting member to fail, got no error")
 		}
@@ -344,7 +344,7 @@ func TestMemberPromoteMemberNotExist(t *testing.T) {
 	followerIdx := (leaderIdx + 1) % 3
 	cli := clus.Client(followerIdx)
 
-	resp, err := cli.MemberList(context.Background())
+	resp, err := cli.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to list member %v", err)
 	}
@@ -369,7 +369,7 @@ func TestMemberPromoteMemberNotExist(t *testing.T) {
 	}
 
 	expectedErrKeywords := "member not found"
-	_, err = cli.MemberPromote(context.Background(), randID)
+	_, err = cli.MemberPromote(t.Context(), randID)
 	if err == nil {
 		t.Fatalf("expect promoting voting member to fail, got no error")
 	}
@@ -388,7 +388,7 @@ func TestMaxLearnerInCluster(t *testing.T) {
 
 	// 2. adding 2 learner members should succeed
 	for i := 0; i < 2; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 		_, err := clus.Client(0).MemberAddAsLearner(ctx, []string{fmt.Sprintf("http://127.0.0.1:123%d", i)})
 		cancel()
 		if err != nil {
@@ -399,7 +399,7 @@ func TestMaxLearnerInCluster(t *testing.T) {
 	// ensure client endpoint is voting member
 	leaderIdx := clus.WaitLeader(t)
 	capi := clus.Client(leaderIdx)
-	resp1, err := capi.MemberList(context.Background())
+	resp1, err := capi.MemberList(t.Context())
 	if err != nil {
 		t.Fatalf("failed to get member list")
 	}
@@ -414,7 +414,7 @@ func TestMaxLearnerInCluster(t *testing.T) {
 	}
 
 	// 3. cluster has 3 voting member and 2 learner, adding another learner should fail
-	_, err = clus.Client(0).MemberAddAsLearner(context.Background(), []string{"http://127.0.0.1:2342"})
+	_, err = clus.Client(0).MemberAddAsLearner(t.Context(), []string{"http://127.0.0.1:2342"})
 	if err == nil {
 		t.Fatalf("expect member add to fail, got no error")
 	}
@@ -424,7 +424,7 @@ func TestMaxLearnerInCluster(t *testing.T) {
 	}
 
 	// 4. cluster has 3 voting member and 1 learner, adding a voting member should succeed
-	_, err = clus.Client(0).MemberAdd(context.Background(), []string{"http://127.0.0.1:3453"})
+	_, err = clus.Client(0).MemberAdd(t.Context(), []string{"http://127.0.0.1:3453"})
 	if err != nil {
 		t.Errorf("failed to add member %v", err)
 	}
diff --git a/tests/integration/clientv3/concurrency/election_test.go b/tests/integration/clientv3/concurrency/election_test.go
index b597e9130..1dc00e6b8 100644
--- a/tests/integration/clientv3/concurrency/election_test.go
+++ b/tests/integration/clientv3/concurrency/election_test.go
@@ -47,7 +47,7 @@ func TestResumeElection(t *testing.T) {
 	e := concurrency.NewElection(s, prefix)
 
 	// entire test should never take more than 10 seconds
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second*10)
 	defer cancel()
 
 	// become leader
diff --git a/tests/integration/clientv3/concurrency/mutex_test.go b/tests/integration/clientv3/concurrency/mutex_test.go
index c9a3b7aae..3d8ea858c 100644
--- a/tests/integration/clientv3/concurrency/mutex_test.go
+++ b/tests/integration/clientv3/concurrency/mutex_test.go
@@ -15,7 +15,6 @@
 package concurrency_test
 
 import (
-	"context"
 	"errors"
 	"testing"
 
@@ -42,21 +41,21 @@ func TestMutexLockSessionExpired(t *testing.T) {
 	m2 := concurrency.NewMutex(s2, "/my-lock/")
 
 	// acquire lock for s1
-	require.NoError(t, m1.Lock(context.TODO()))
+	require.NoError(t, m1.Lock(t.Context()))
 
 	m2Locked := make(chan struct{})
 	var err2 error
 	go func() {
 		defer close(m2Locked)
 		// m2 blocks since m1 already acquired lock /my-lock/
-		if err2 = m2.Lock(context.TODO()); err2 == nil {
+		if err2 = m2.Lock(t.Context()); err2 == nil {
 			t.Error("expect session expired error")
 		}
 	}()
 
 	// revoke the session of m2 before unlock m1
 	require.NoError(t, s2.Close())
-	require.NoError(t, m1.Unlock(context.TODO()))
+	require.NoError(t, m1.Unlock(t.Context()))
 
 	<-m2Locked
 }
@@ -71,17 +70,17 @@ func TestMutexUnlock(t *testing.T) {
 	defer s1.Close()
 
 	m1 := concurrency.NewMutex(s1, "/my-lock/")
-	err = m1.Unlock(context.TODO())
+	err = m1.Unlock(t.Context())
 	require.Errorf(t, err, "expect lock released error")
 	if !errors.Is(err, concurrency.ErrLockReleased) {
 		t.Fatal(err)
 	}
 
-	require.NoError(t, m1.Lock(context.TODO()))
+	require.NoError(t, m1.Lock(t.Context()))
 
-	require.NoError(t, m1.Unlock(context.TODO()))
+	require.NoError(t, m1.Unlock(t.Context()))
 
-	err = m1.Unlock(context.TODO())
+	err = m1.Unlock(t.Context())
 	if err == nil {
 		t.Fatal("expect lock released error")
 	}
diff --git a/tests/integration/clientv3/concurrency/session_test.go b/tests/integration/clientv3/concurrency/session_test.go
index e7bcc5638..21691bdf1 100644
--- a/tests/integration/clientv3/concurrency/session_test.go
+++ b/tests/integration/clientv3/concurrency/session_test.go
@@ -31,7 +31,7 @@ func TestSessionOptions(t *testing.T) {
 	cli, err := integration2.NewClient(t, clientv3.Config{Endpoints: exampleEndpoints()})
 	require.NoError(t, err)
 	defer cli.Close()
-	lease, err := cli.Grant(context.Background(), 100)
+	lease, err := cli.Grant(t.Context(), 100)
 	require.NoError(t, err)
 	s, err := concurrency.NewSession(cli, concurrency.WithLease(lease.ID))
 	require.NoError(t, err)
@@ -58,7 +58,7 @@ func TestSessionTTLOptions(t *testing.T) {
 
 	leaseID := s.Lease()
 	// TTL retrieved should be less than the set TTL, but not equal to default:60 or exprired:-1
-	resp, err := cli.Lease.TimeToLive(context.Background(), leaseID)
+	resp, err := cli.Lease.TimeToLive(t.Context(), leaseID)
 	if err != nil {
 		t.Log(err)
 	}
@@ -77,7 +77,7 @@ func TestSessionCtx(t *testing.T) {
 	cli, err := integration2.NewClient(t, clientv3.Config{Endpoints: exampleEndpoints()})
 	require.NoError(t, err)
 	defer cli.Close()
-	lease, err := cli.Grant(context.Background(), 100)
+	lease, err := cli.Grant(t.Context(), 100)
 	require.NoError(t, err)
 	s, err := concurrency.NewSession(cli, concurrency.WithLease(lease.ID))
 	require.NoError(t, err)
diff --git a/tests/integration/clientv3/connectivity/black_hole_test.go b/tests/integration/clientv3/connectivity/black_hole_test.go
index a48444714..38c463126 100644
--- a/tests/integration/clientv3/connectivity/black_hole_test.go
+++ b/tests/integration/clientv3/connectivity/black_hole_test.go
@@ -66,7 +66,7 @@ func TestBalancerUnderBlackholeKeepAliveWatch(t *testing.T) {
 	require.NoError(t, err)
 	defer cli.Close()
 
-	wch := cli.Watch(context.Background(), "foo", clientv3.WithCreatedNotify())
+	wch := cli.Watch(t.Context(), "foo", clientv3.WithCreatedNotify())
 	_, ok := <-wch
 	require.Truef(t, ok, "watch failed on creation")
 
@@ -78,7 +78,7 @@ func TestBalancerUnderBlackholeKeepAliveWatch(t *testing.T) {
 
 	clus.Members[0].Bridge().Blackhole()
 
-	_, err = clus.Client(1).Put(context.TODO(), "foo", "bar")
+	_, err = clus.Client(1).Put(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 	select {
 	case <-wch:
@@ -94,9 +94,9 @@ func TestBalancerUnderBlackholeKeepAliveWatch(t *testing.T) {
 	clus.Members[1].Bridge().Blackhole()
 
 	// make sure client[0] can connect to eps[0] after remove the blackhole.
-	_, err = clus.Client(0).Get(context.TODO(), "foo")
+	_, err = clus.Client(0).Get(t.Context(), "foo")
 	require.NoError(t, err)
-	_, err = clus.Client(0).Put(context.TODO(), "foo", "bar1")
+	_, err = clus.Client(0).Put(t.Context(), "foo", "bar1")
 	require.NoError(t, err)
 
 	select {
@@ -196,7 +196,7 @@ func testBalancerUnderBlackholeNoKeepAlive(t *testing.T, op func(*clientv3.Clien
 	// TODO: first operation can succeed
 	// when gRPC supports better retry on non-delivered request
 	for i := 0; i < 5; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+		ctx, cancel := context.WithTimeout(t.Context(), time.Second*5)
 		err = op(cli, ctx)
 		cancel()
 		if err == nil {
diff --git a/tests/integration/clientv3/connectivity/dial_test.go b/tests/integration/clientv3/connectivity/dial_test.go
index 80a3cb90f..527d50096 100644
--- a/tests/integration/clientv3/connectivity/dial_test.go
+++ b/tests/integration/clientv3/connectivity/dial_test.go
@@ -129,7 +129,7 @@ func testDialSetEndpoints(t *testing.T, setBefore bool) {
 		cli.SetEndpoints(eps[toKill%3], eps[(toKill+1)%3])
 	}
 	time.Sleep(time.Second * 2)
-	ctx, cancel := context.WithTimeout(context.Background(), integration2.RequestWaitTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), integration2.RequestWaitTimeout)
 	_, err = cli.Get(ctx, "foo", clientv3.WithSerializable())
 	require.NoError(t, err)
 	cancel()
@@ -150,7 +150,7 @@ func TestSwitchSetEndpoints(t *testing.T) {
 
 	cli.SetEndpoints(eps...)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	_, err := cli.Get(ctx, "foo")
 	require.NoError(t, err)
@@ -187,7 +187,7 @@ func TestDialForeignEndpoint(t *testing.T) {
 	// grpc can return a lazy connection that's not connected yet; confirm
 	// that it can communicate with the cluster.
 	kvc := clientv3.NewKVFromKVClient(pb.NewKVClient(conn), clus.Client(0))
-	ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer cancel()
 	_, gerr := kvc.Get(ctx, "abc")
 	require.NoError(t, gerr)
@@ -201,7 +201,7 @@ func TestSetEndpointAndPut(t *testing.T) {
 	defer clus.Terminate(t)
 
 	clus.Client(1).SetEndpoints(clus.Members[0].GRPCURL)
-	_, err := clus.Client(1).Put(context.TODO(), "foo", "bar")
+	_, err := clus.Client(1).Put(t.Context(), "foo", "bar")
 	if err != nil && !strings.Contains(err.Error(), "closing") {
 		t.Fatal(err)
 	}
diff --git a/tests/integration/clientv3/connectivity/network_partition_test.go b/tests/integration/clientv3/connectivity/network_partition_test.go
index eb14534e9..c5f034ecb 100644
--- a/tests/integration/clientv3/connectivity/network_partition_test.go
+++ b/tests/integration/clientv3/connectivity/network_partition_test.go
@@ -136,7 +136,7 @@ func testBalancerUnderNetworkPartition(t *testing.T, op func(*clientv3.Client, c
 	clus.Members[0].InjectPartition(t, clus.Members[1:]...)
 
 	for i := 0; i < 5; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), timeout)
+		ctx, cancel := context.WithTimeout(t.Context(), timeout)
 		err = op(cli, ctx)
 		t.Logf("Op returned error: %v", err)
 		t.Log("Cancelling...")
@@ -191,7 +191,7 @@ func TestBalancerUnderNetworkPartitionLinearizableGetLeaderElection(t *testing.T
 
 	// expects balancer to round robin to leader within two attempts
 	for i := 0; i < 2; i++ {
-		ctx, cancel := context.WithTimeout(context.TODO(), timeout)
+		ctx, cancel := context.WithTimeout(t.Context(), timeout)
 		_, err = cli.Get(ctx, "a")
 		cancel()
 		if err == nil {
@@ -240,7 +240,7 @@ func testBalancerUnderNetworkPartitionWatch(t *testing.T, isolateLeader bool) {
 	// under the cover to other available eps, but expose the failure to the
 	// caller (test assertion).
 
-	wch := watchCli.Watch(clientv3.WithRequireLeader(context.Background()), "foo", clientv3.WithCreatedNotify())
+	wch := watchCli.Watch(clientv3.WithRequireLeader(t.Context()), "foo", clientv3.WithCreatedNotify())
 	select {
 	case <-wch:
 	case <-time.After(integration2.RequestWaitTimeout):
@@ -297,13 +297,13 @@ func TestDropReadUnderNetworkPartition(t *testing.T) {
 
 	clus.Members[leaderIndex].InjectPartition(t, clus.Members[(leaderIndex+1)%3], clus.Members[(leaderIndex+2)%3])
 	kvc := clientv3.NewKVFromKVClient(pb.NewKVClient(conn), nil)
-	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	_, err = kvc.Get(ctx, "a")
 	cancel()
 	require.ErrorIsf(t, err, rpctypes.ErrLeaderChanged, "expected %v, got %v", rpctypes.ErrLeaderChanged, err)
 
 	for i := 0; i < 5; i++ {
-		ctx, cancel = context.WithTimeout(context.TODO(), 10*time.Second)
+		ctx, cancel = context.WithTimeout(t.Context(), 10*time.Second)
 		_, err = kvc.Get(ctx, "a")
 		cancel()
 		if err != nil {
diff --git a/tests/integration/clientv3/connectivity/server_shutdown_test.go b/tests/integration/clientv3/connectivity/server_shutdown_test.go
index 9e44ea9f9..bb053ac10 100644
--- a/tests/integration/clientv3/connectivity/server_shutdown_test.go
+++ b/tests/integration/clientv3/connectivity/server_shutdown_test.go
@@ -58,7 +58,7 @@ func TestBalancerUnderServerShutdownWatch(t *testing.T) {
 	watchCli.SetEndpoints(eps...)
 
 	key, val := "foo", "bar"
-	wch := watchCli.Watch(context.Background(), key, clientv3.WithCreatedNotify())
+	wch := watchCli.Watch(t.Context(), key, clientv3.WithCreatedNotify())
 	select {
 	case <-wch:
 	case <-time.After(integration2.RequestWaitTimeout):
@@ -94,7 +94,7 @@ func TestBalancerUnderServerShutdownWatch(t *testing.T) {
 	require.NoError(t, err)
 	defer putCli.Close()
 	for {
-		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
 		_, err = putCli.Put(ctx, key, val)
 		cancel()
 		if err == nil {
@@ -170,7 +170,7 @@ func testBalancerUnderServerShutdownMutable(t *testing.T, op func(*clientv3.Clie
 	// TODO: remove this (expose client connection state?)
 	time.Sleep(time.Second)
 
-	cctx, ccancel := context.WithTimeout(context.Background(), time.Second)
+	cctx, ccancel := context.WithTimeout(t.Context(), time.Second)
 	err = op(cli, cctx)
 	ccancel()
 	require.NoError(t, err)
@@ -222,7 +222,7 @@ func testBalancerUnderServerShutdownImmutable(t *testing.T, op func(*clientv3.Cl
 
 	// switched to others when eps[0] was explicitly shut down
 	// and following request should succeed
-	cctx, ccancel := context.WithTimeout(context.Background(), timeout)
+	cctx, ccancel := context.WithTimeout(t.Context(), timeout)
 	err = op(cli, cctx)
 	ccancel()
 	if err != nil {
@@ -329,7 +329,7 @@ func testBalancerUnderServerStopInflightRangeOnRestart(t *testing.T, linearizabl
 	donec, readyc := make(chan struct{}), make(chan struct{}, 1)
 	go func() {
 		defer close(donec)
-		ctx, cancel := context.WithTimeout(context.TODO(), clientTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), clientTimeout)
 		readyc <- struct{}{}
 
 		// TODO: The new grpc load balancer will not pin to an endpoint
diff --git a/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go b/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go
index 1fefd906d..70f30ea3d 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go
@@ -15,7 +15,6 @@
 package recipes_test
 
 import (
-	"context"
 	"testing"
 	"time"
 
@@ -46,7 +45,7 @@ func testBarrier(t *testing.T, waiters int, chooseClient func() *clientv3.Client
 	require.Errorf(t, b.Hold(), "able to double-hold barrier")
 
 	// put a random key to move the revision forward
-	if _, err := chooseClient().Put(context.Background(), "x", ""); err != nil {
+	if _, err := chooseClient().Put(t.Context(), "x", ""); err != nil {
 		t.Errorf("could not put x (%v)", err)
 	}
 
diff --git a/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go b/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go
index 9dcc5530c..85bf219c0 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go
@@ -15,7 +15,6 @@
 package recipes_test
 
 import (
-	"context"
 	"errors"
 	"sync"
 	"testing"
@@ -153,7 +152,7 @@ func TestDoubleBarrierTooManyClients(t *testing.T) {
 		t.Errorf("Unexcepted error, expected: ErrTooManyClients, got: %v", err)
 	}
 
-	resp, err := clus.RandClient().Get(context.TODO(), "test-barrier/waiters", clientv3.WithPrefix())
+	resp, err := clus.RandClient().Get(t.Context(), "test-barrier/waiters", clientv3.WithPrefix())
 	if err != nil {
 		t.Errorf("Unexpected error: %v", err)
 	}
diff --git a/tests/integration/clientv3/experimental/recipes/v3_lock_test.go b/tests/integration/clientv3/experimental/recipes/v3_lock_test.go
index 50b571b15..4bf2d8249 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_lock_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_lock_test.go
@@ -71,7 +71,7 @@ func testMutexLock(t *testing.T, waiters int, chooseClient func() *clientv3.Clie
 				return
 			}
 			m := concurrency.NewMutex(session, "test-mutex")
-			if err := m.Lock(context.TODO()); err != nil {
+			if err := m.Lock(t.Context()); err != nil {
 				errC <- fmt.Errorf("#%d: failed to wait on lock: %w", i, err)
 				return
 			}
@@ -93,7 +93,7 @@ func testMutexLock(t *testing.T, waiters int, chooseClient func() *clientv3.Clie
 				t.Fatalf("lock %d followers did not wait", i)
 			default:
 			}
-			require.NoErrorf(t, m.Unlock(context.TODO()), "could not release lock")
+			require.NoErrorf(t, m.Unlock(t.Context()), "could not release lock")
 		}
 	}
 	wg.Wait()
@@ -120,7 +120,7 @@ func TestMutexTryLockMultiNode(t *testing.T) {
 }
 
 func testMutexTryLock(t *testing.T, lockers int, chooseClient func() *clientv3.Client) {
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	lockedC := make(chan *concurrency.Mutex)
@@ -182,10 +182,10 @@ func TestMutexSessionRelock(t *testing.T) {
 	}
 
 	m := concurrency.NewMutex(session, "test-mutex")
-	require.NoError(t, m.Lock(context.TODO()))
+	require.NoError(t, m.Lock(t.Context()))
 
 	m2 := concurrency.NewMutex(session, "test-mutex")
-	require.NoError(t, m2.Lock(context.TODO()))
+	require.NoError(t, m2.Lock(t.Context()))
 }
 
 // TestMutexWaitsOnCurrentHolder ensures a mutex is only acquired once all
@@ -197,7 +197,7 @@ func TestMutexWaitsOnCurrentHolder(t *testing.T) {
 	clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	cctx := context.Background()
+	cctx := t.Context()
 
 	cli := clus.Client(0)
 
diff --git a/tests/integration/clientv3/kv_test.go b/tests/integration/clientv3/kv_test.go
index c09f1345c..5b996516e 100644
--- a/tests/integration/clientv3/kv_test.go
+++ b/tests/integration/clientv3/kv_test.go
@@ -49,7 +49,7 @@ func TestKVPutError(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err := kv.Put(ctx, "", "bar")
 	if !errors.Is(err, rpctypes.ErrEmptyKey) {
@@ -81,9 +81,9 @@ func TestKVPutWithLease(t *testing.T) {
 	lapi := clus.RandClient()
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
-	lease, err := lapi.Grant(context.Background(), 10)
+	lease, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Fatalf("failed to create lease %v", err)
 	}
@@ -117,17 +117,17 @@ func TestKVPutWithIgnoreValue(t *testing.T) {
 
 	kv := clus.RandClient()
 
-	_, err := kv.Put(context.TODO(), "foo", "", clientv3.WithIgnoreValue())
+	_, err := kv.Put(t.Context(), "foo", "", clientv3.WithIgnoreValue())
 	if !errors.Is(err, rpctypes.ErrKeyNotFound) {
 		t.Fatalf("err expected %v, got %v", rpctypes.ErrKeyNotFound, err)
 	}
 
-	_, err = kv.Put(context.TODO(), "foo", "bar")
+	_, err = kv.Put(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 
-	_, err = kv.Put(context.TODO(), "foo", "", clientv3.WithIgnoreValue())
+	_, err = kv.Put(t.Context(), "foo", "", clientv3.WithIgnoreValue())
 	require.NoError(t, err)
-	rr, rerr := kv.Get(context.TODO(), "foo")
+	rr, rerr := kv.Get(t.Context(), "foo")
 	require.NoError(t, rerr)
 	if len(rr.Kvs) != 1 {
 		t.Fatalf("len(rr.Kvs) expected 1, got %d", len(rr.Kvs))
@@ -148,22 +148,22 @@ func TestKVPutWithIgnoreLease(t *testing.T) {
 
 	lapi := clus.RandClient()
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
 
-	if _, err = kv.Put(context.TODO(), "zoo", "bar", clientv3.WithIgnoreLease()); !errors.Is(err, rpctypes.ErrKeyNotFound) {
+	if _, err = kv.Put(t.Context(), "zoo", "bar", clientv3.WithIgnoreLease()); !errors.Is(err, rpctypes.ErrKeyNotFound) {
 		t.Fatalf("err expected %v, got %v", rpctypes.ErrKeyNotFound, err)
 	}
 
-	_, err = kv.Put(context.TODO(), "zoo", "bar", clientv3.WithLease(resp.ID))
+	_, err = kv.Put(t.Context(), "zoo", "bar", clientv3.WithLease(resp.ID))
 	require.NoError(t, err)
 
-	_, err = kv.Put(context.TODO(), "zoo", "bar1", clientv3.WithIgnoreLease())
+	_, err = kv.Put(t.Context(), "zoo", "bar1", clientv3.WithIgnoreLease())
 	require.NoError(t, err)
 
-	rr, rerr := kv.Get(context.TODO(), "zoo")
+	rr, rerr := kv.Get(t.Context(), "zoo")
 	require.NoError(t, rerr)
 	if len(rr.Kvs) != 1 {
 		t.Fatalf("len(rr.Kvs) expected 1, got %d", len(rr.Kvs))
@@ -190,7 +190,7 @@ func TestKVPutWithRequireLeader(t *testing.T) {
 	time.Sleep(time.Duration(3*electionTicks) * tickDuration)
 
 	kv := clus.Client(0)
-	_, err := kv.Put(clientv3.WithRequireLeader(context.Background()), "foo", "bar")
+	_, err := kv.Put(clientv3.WithRequireLeader(t.Context()), "foo", "bar")
 	if !errors.Is(err, rpctypes.ErrNoLeader) {
 		t.Fatal(err)
 	}
@@ -222,7 +222,7 @@ func TestKVRange(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	keySet := []string{"a", "b", "c", "c", "c", "foo", "foo/abc", "fop"}
 	for i, key := range keySet {
@@ -290,7 +290,7 @@ func TestKVGetErrConnClosed(t *testing.T) {
 
 	go func() {
 		defer close(donec)
-		_, err := cli.Get(context.TODO(), "foo")
+		_, err := cli.Get(t.Context(), "foo")
 		if !clientv3.IsConnCanceled(err) {
 			t.Errorf("expected %v, got %v", context.Canceled, err)
 		}
@@ -315,7 +315,7 @@ func TestKVNewAfterClose(t *testing.T) {
 
 	donec := make(chan struct{})
 	go func() {
-		_, err := cli.Get(context.TODO(), "foo")
+		_, err := cli.Get(t.Context(), "foo")
 		if !clientv3.IsConnCanceled(err) {
 			t.Errorf("expected %v, got %v", context.Canceled, err)
 		}
@@ -335,7 +335,7 @@ func TestKVDeleteRange(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	tests := []struct {
 		key   string
@@ -384,7 +384,7 @@ func TestKVCompactError(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	for i := 0; i < 5; i++ {
 		if _, err := kv.Put(ctx, "foo", "bar"); err != nil {
@@ -414,7 +414,7 @@ func TestKVCompact(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	for i := 0; i < 10; i++ {
 		if _, err := kv.Put(ctx, "foo", "bar"); err != nil {
@@ -474,7 +474,7 @@ func TestKVGetRetry(t *testing.T) {
 	fIdx := (clus.WaitLeader(t) + 1) % clusterSize
 
 	kv := clus.Client(fIdx)
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err := kv.Put(ctx, "foo", "bar")
 	require.NoError(t, err)
@@ -524,7 +524,7 @@ func TestKVPutFailGetRetry(t *testing.T) {
 	kv := clus.Client(0)
 	clus.Members[0].Stop(t)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 	defer cancel()
 	_, err := kv.Put(ctx, "foo", "bar")
 	if err == nil {
@@ -534,7 +534,7 @@ func TestKVPutFailGetRetry(t *testing.T) {
 	donec := make(chan struct{}, 1)
 	go func() {
 		// Get will fail, but reconnect will trigger
-		gresp, gerr := kv.Get(context.TODO(), "foo")
+		gresp, gerr := kv.Get(t.Context(), "foo")
 		if gerr != nil {
 			t.Error(gerr)
 		}
@@ -564,7 +564,7 @@ func TestKVGetCancel(t *testing.T) {
 	oldconn := clus.Client(0).ActiveConnection()
 	kv := clus.Client(0)
 
-	ctx, cancel := context.WithCancel(context.TODO())
+	ctx, cancel := context.WithCancel(t.Context())
 	cancel()
 
 	resp, err := kv.Get(ctx, "abc")
@@ -586,7 +586,7 @@ func TestKVGetStoppedServerAndClose(t *testing.T) {
 
 	cli := clus.Client(0)
 	clus.Members[0].Stop(t)
-	ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 	// this Get fails and triggers an asynchronous connection retry
 	_, err := cli.Get(ctx, "abc")
 	cancel()
@@ -605,7 +605,7 @@ func TestKVPutStoppedServerAndClose(t *testing.T) {
 	cli := clus.Client(0)
 	clus.Members[0].Stop(t)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 	// get retries on all errors.
 	// so here we use it to eat the potential broken pipe error for the next put.
 	// grpc client might see a broken pipe error when we issue the get request before
@@ -616,7 +616,7 @@ func TestKVPutStoppedServerAndClose(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	ctx, cancel = context.WithTimeout(context.TODO(), time.Second)
+	ctx, cancel = context.WithTimeout(t.Context(), time.Second)
 	// this Put fails and triggers an asynchronous connection retry
 	_, err = cli.Put(ctx, "abc", "123")
 	cancel()
@@ -632,7 +632,7 @@ func TestKVPutAtMostOnce(t *testing.T) {
 	clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1, UseBridge: true})
 	defer clus.Terminate(t)
 
-	_, err := clus.Client(0).Put(context.TODO(), "k", "1")
+	_, err := clus.Client(0).Put(t.Context(), "k", "1")
 	require.NoError(t, err)
 
 	for i := 0; i < 10; i++ {
@@ -645,14 +645,14 @@ func TestKVPutAtMostOnce(t *testing.T) {
 				time.Sleep(5 * time.Millisecond)
 			}
 		}()
-		_, err = clus.Client(0).Put(context.TODO(), "k", "v")
+		_, err = clus.Client(0).Put(t.Context(), "k", "v")
 		<-donec
 		if err != nil {
 			break
 		}
 	}
 
-	resp, err := clus.Client(0).Get(context.TODO(), "k")
+	resp, err := clus.Client(0).Get(t.Context(), "k")
 	require.NoError(t, err)
 	if resp.Kvs[0].Version > 11 {
 		t.Fatalf("expected version <= 10, got %+v", resp.Kvs[0])
@@ -727,7 +727,7 @@ func TestKVLargeRequests(t *testing.T) {
 			},
 		)
 		cli := clus.Client(0)
-		_, err := cli.Put(context.TODO(), "foo", strings.Repeat("a", test.valueSize))
+		_, err := cli.Put(t.Context(), "foo", strings.Repeat("a", test.valueSize))
 
 		var etcdErr rpctypes.EtcdError
 		if errors.As(err, &etcdErr) {
@@ -740,7 +740,7 @@ func TestKVLargeRequests(t *testing.T) {
 
 		// put request went through, now expects large response back
 		if err == nil {
-			_, err = cli.Get(context.TODO(), "foo")
+			_, err = cli.Get(t.Context(), "foo")
 			if err != nil {
 				t.Errorf("#%d: get expected no error, got %v", i, err)
 			}
@@ -819,7 +819,7 @@ func TestKVForLearner(t *testing.T) {
 	}
 
 	for idx, test := range tests {
-		_, err := cli.Do(context.TODO(), test.op)
+		_, err := cli.Do(t.Context(), test.op)
 		if err != nil && !test.wErr {
 			t.Errorf("%d: expect no error, got %v", idx, err)
 		}
@@ -864,14 +864,14 @@ func TestBalancerSupportLearner(t *testing.T) {
 	// wait until learner member is ready
 	<-clus.Members[3].ReadyNotify()
 
-	if _, err = cli.Get(context.Background(), "foo"); err == nil {
+	if _, err = cli.Get(t.Context(), "foo"); err == nil {
 		t.Fatalf("expect Get request to learner to fail, got no error")
 	}
 	t.Logf("Expected: Read from learner error: %v", err)
 
 	eps := []string{learnerEp, clus.Members[0].GRPCURL}
 	cli.SetEndpoints(eps...)
-	if _, err := cli.Get(context.Background(), "foo"); err != nil {
+	if _, err := cli.Get(t.Context(), "foo"); err != nil {
 		t.Errorf("expect no error (balancer should retry when request to learner fails), got error: %v", err)
 	}
 }
diff --git a/tests/integration/clientv3/lease/lease_test.go b/tests/integration/clientv3/lease/lease_test.go
index 083d45a3c..0cdd96ab7 100644
--- a/tests/integration/clientv3/lease/lease_test.go
+++ b/tests/integration/clientv3/lease/lease_test.go
@@ -40,7 +40,7 @@ func TestLeaseNotFoundError(t *testing.T) {
 
 	kv := clus.RandClient()
 
-	_, err := kv.Put(context.TODO(), "foo", "bar", clientv3.WithLease(clientv3.LeaseID(500)))
+	_, err := kv.Put(t.Context(), "foo", "bar", clientv3.WithLease(clientv3.LeaseID(500)))
 	require.ErrorIsf(t, err, rpctypes.ErrLeaseNotFound, "expected %v, got %v", rpctypes.ErrLeaseNotFound, err)
 }
 
@@ -54,15 +54,15 @@ func TestLeaseGrant(t *testing.T) {
 
 	kv := clus.RandClient()
 
-	_, merr := lapi.Grant(context.Background(), clientv3.MaxLeaseTTL+1)
+	_, merr := lapi.Grant(t.Context(), clientv3.MaxLeaseTTL+1)
 	require.ErrorIsf(t, merr, rpctypes.ErrLeaseTTLTooLarge, "err = %v, want %v", merr, rpctypes.ErrLeaseTTLTooLarge)
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
 
-	_, err = kv.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
+	_, err = kv.Put(t.Context(), "foo", "bar", clientv3.WithLease(resp.ID))
 	require.NoErrorf(t, err, "failed to create key with lease %v", err)
 }
 
@@ -76,17 +76,17 @@ func TestLeaseRevoke(t *testing.T) {
 
 	kv := clus.RandClient()
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
 
-	_, err = lapi.Revoke(context.Background(), resp.ID)
+	_, err = lapi.Revoke(t.Context(), resp.ID)
 	if err != nil {
 		t.Errorf("failed to revoke lease %v", err)
 	}
 
-	_, err = kv.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
+	_, err = kv.Put(t.Context(), "foo", "bar", clientv3.WithLease(resp.ID))
 	require.ErrorIsf(t, err, rpctypes.ErrLeaseNotFound, "err = %v, want %v", err, rpctypes.ErrLeaseNotFound)
 }
 
@@ -98,17 +98,17 @@ func TestLeaseKeepAliveOnce(t *testing.T) {
 
 	lapi := clus.RandClient()
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
 
-	_, err = lapi.KeepAliveOnce(context.Background(), resp.ID)
+	_, err = lapi.KeepAliveOnce(t.Context(), resp.ID)
 	if err != nil {
 		t.Errorf("failed to keepalive lease %v", err)
 	}
 
-	_, err = lapi.KeepAliveOnce(context.Background(), clientv3.LeaseID(0))
+	_, err = lapi.KeepAliveOnce(t.Context(), clientv3.LeaseID(0))
 	if !errors.Is(err, rpctypes.ErrLeaseNotFound) {
 		t.Errorf("expected %v, got %v", rpctypes.ErrLeaseNotFound, err)
 	}
@@ -123,7 +123,7 @@ func TestLeaseKeepAlive(t *testing.T) {
 	lapi := clus.Client(0)
 	clus.TakeClient(0)
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
@@ -133,7 +133,7 @@ func TestLeaseKeepAlive(t *testing.T) {
 		_ func()
 	}
 
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 	rc, kerr := lapi.KeepAlive(uncomparableCtx{Context: ctx}, resp.ID)
 	if kerr != nil {
@@ -151,7 +151,7 @@ func TestLeaseKeepAlive(t *testing.T) {
 		t.Errorf("ID = %x, want %x", kresp.ID, resp.ID)
 	}
 
-	ctx2, cancel2 := context.WithCancel(context.Background())
+	ctx2, cancel2 := context.WithCancel(t.Context())
 	rc2, kerr2 := lapi.KeepAlive(uncomparableCtx{Context: ctx2}, resp.ID)
 	if kerr2 != nil {
 		t.Errorf("failed to keepalive lease %v", kerr2)
@@ -187,11 +187,11 @@ func TestLeaseKeepAliveSeconds(t *testing.T) {
 
 	cli := clus.Client(0)
 
-	resp, err := cli.Grant(context.Background(), 3)
+	resp, err := cli.Grant(t.Context(), 3)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
-	rc, kerr := cli.KeepAlive(context.Background(), resp.ID)
+	rc, kerr := cli.KeepAlive(t.Context(), resp.ID)
 	if kerr != nil {
 		t.Errorf("failed to keepalive lease %v", kerr)
 	}
@@ -217,12 +217,12 @@ func TestLeaseKeepAliveHandleFailure(t *testing.T) {
 	// TODO: change this line to get a cluster client
 	lapi := clus.RandClient()
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
 
-	rc, kerr := lapi.KeepAlive(context.Background(), resp.ID)
+	rc, kerr := lapi.KeepAlive(t.Context(), resp.ID)
 	if kerr != nil {
 		t.Errorf("failed to keepalive lease %v", kerr)
 	}
@@ -272,14 +272,14 @@ func TestLeaseKeepAliveNotFound(t *testing.T) {
 	cli := clus.RandClient()
 	var lchs []leaseCh
 	for i := 0; i < 3; i++ {
-		resp, rerr := cli.Grant(context.TODO(), 5)
+		resp, rerr := cli.Grant(t.Context(), 5)
 		require.NoError(t, rerr)
-		kach, kaerr := cli.KeepAlive(context.Background(), resp.ID)
+		kach, kaerr := cli.KeepAlive(t.Context(), resp.ID)
 		require.NoError(t, kaerr)
 		lchs = append(lchs, leaseCh{resp.ID, kach})
 	}
 
-	_, err := cli.Revoke(context.TODO(), lchs[1].lid)
+	_, err := cli.Revoke(t.Context(), lchs[1].lid)
 	require.NoError(t, err)
 
 	<-lchs[0].ch
@@ -304,7 +304,7 @@ func TestLeaseGrantErrConnClosed(t *testing.T) {
 	donec := make(chan struct{})
 	go func() {
 		defer close(donec)
-		_, err := cli.Grant(context.TODO(), 5)
+		_, err := cli.Grant(t.Context(), 5)
 		if !clientv3.IsConnCanceled(err) {
 			// context.Canceled if grpc-go balancer calls 'Get' with an inflight client.Close.
 			t.Errorf("expected %v, or server unavailable, got %v", context.Canceled, err)
@@ -330,7 +330,7 @@ func TestLeaseKeepAliveFullResponseQueue(t *testing.T) {
 	lapi := clus.Client(0)
 
 	// expect lease keepalive every 10-second
-	lresp, err := lapi.Grant(context.Background(), 30)
+	lresp, err := lapi.Grant(t.Context(), 30)
 	require.NoErrorf(t, err, "failed to create lease %v", err)
 	id := lresp.ID
 
@@ -341,14 +341,14 @@ func TestLeaseKeepAliveFullResponseQueue(t *testing.T) {
 	clientv3.LeaseResponseChSize = 0
 
 	// never fetch from response queue, and let it become full
-	_, err = lapi.KeepAlive(context.Background(), id)
+	_, err = lapi.KeepAlive(t.Context(), id)
 	require.NoErrorf(t, err, "failed to keepalive lease %v", err)
 
 	// TTL should not be refreshed after 3 seconds
 	// expect keepalive to be triggered after TTL/3
 	time.Sleep(3 * time.Second)
 
-	tr, terr := lapi.TimeToLive(context.Background(), id)
+	tr, terr := lapi.TimeToLive(t.Context(), id)
 	require.NoErrorf(t, terr, "failed to get lease information %v", terr)
 	if tr.TTL >= 29 {
 		t.Errorf("unexpected kept-alive lease TTL %d", tr.TTL)
@@ -367,7 +367,7 @@ func TestLeaseGrantNewAfterClose(t *testing.T) {
 
 	donec := make(chan struct{})
 	go func() {
-		_, err := cli.Grant(context.TODO(), 5)
+		_, err := cli.Grant(t.Context(), 5)
 		if !clientv3.IsConnCanceled(err) {
 			t.Errorf("expected %v or server unavailable, got %v", context.Canceled, err)
 		}
@@ -387,7 +387,7 @@ func TestLeaseRevokeNewAfterClose(t *testing.T) {
 	defer clus.Terminate(t)
 
 	cli := clus.Client(0)
-	resp, err := cli.Grant(context.TODO(), 5)
+	resp, err := cli.Grant(t.Context(), 5)
 	require.NoError(t, err)
 	leaseID := resp.ID
 
@@ -396,7 +396,7 @@ func TestLeaseRevokeNewAfterClose(t *testing.T) {
 
 	errMsgCh := make(chan string, 1)
 	go func() {
-		_, err := cli.Revoke(context.TODO(), leaseID)
+		_, err := cli.Revoke(t.Context(), leaseID)
 		if !clientv3.IsConnCanceled(err) {
 			errMsgCh <- fmt.Sprintf("expected %v or server unavailable, got %v", context.Canceled, err)
 		} else {
@@ -422,9 +422,9 @@ func TestLeaseKeepAliveCloseAfterDisconnectRevoke(t *testing.T) {
 	cli := clus.Client(0)
 
 	// setup lease and do a keepalive
-	resp, err := cli.Grant(context.Background(), 10)
+	resp, err := cli.Grant(t.Context(), 10)
 	require.NoError(t, err)
-	rc, kerr := cli.KeepAlive(context.Background(), resp.ID)
+	rc, kerr := cli.KeepAlive(t.Context(), resp.ID)
 	require.NoError(t, kerr)
 	kresp := <-rc
 	require.Equalf(t, kresp.ID, resp.ID, "ID = %x, want %x", kresp.ID, resp.ID)
@@ -434,7 +434,7 @@ func TestLeaseKeepAliveCloseAfterDisconnectRevoke(t *testing.T) {
 	time.Sleep(time.Second)
 	clus.WaitLeader(t)
 
-	_, err = clus.Client(1).Revoke(context.TODO(), resp.ID)
+	_, err = clus.Client(1).Revoke(t.Context(), resp.ID)
 	require.NoError(t, err)
 
 	clus.Members[0].Restart(t)
@@ -461,11 +461,11 @@ func TestLeaseKeepAliveInitTimeout(t *testing.T) {
 	cli := clus.Client(0)
 
 	// setup lease and do a keepalive
-	resp, err := cli.Grant(context.Background(), 5)
+	resp, err := cli.Grant(t.Context(), 5)
 	require.NoError(t, err)
 	// keep client disconnected
 	clus.Members[0].Stop(t)
-	rc, kerr := cli.KeepAlive(context.Background(), resp.ID)
+	rc, kerr := cli.KeepAlive(t.Context(), resp.ID)
 	require.NoError(t, kerr)
 	select {
 	case ka, ok := <-rc:
@@ -488,9 +488,9 @@ func TestLeaseKeepAliveTTLTimeout(t *testing.T) {
 	cli := clus.Client(0)
 
 	// setup lease and do a keepalive
-	resp, err := cli.Grant(context.Background(), 5)
+	resp, err := cli.Grant(t.Context(), 5)
 	require.NoError(t, err)
-	rc, kerr := cli.KeepAlive(context.Background(), resp.ID)
+	rc, kerr := cli.KeepAlive(t.Context(), resp.ID)
 	require.NoError(t, kerr)
 	kresp := <-rc
 	require.Equalf(t, kresp.ID, resp.ID, "ID = %x, want %x", kresp.ID, resp.ID)
@@ -516,7 +516,7 @@ func TestLeaseTimeToLive(t *testing.T) {
 	c := clus.RandClient()
 	lapi := c
 
-	resp, err := lapi.Grant(context.Background(), 10)
+	resp, err := lapi.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
@@ -524,15 +524,15 @@ func TestLeaseTimeToLive(t *testing.T) {
 	kv := clus.RandClient()
 	keys := []string{"foo1", "foo2"}
 	for i := range keys {
-		_, err = kv.Put(context.TODO(), keys[i], "bar", clientv3.WithLease(resp.ID))
+		_, err = kv.Put(t.Context(), keys[i], "bar", clientv3.WithLease(resp.ID))
 		require.NoError(t, err)
 	}
 
 	// linearized read to ensure Puts propagated to server backing lapi
-	_, err = c.Get(context.TODO(), "abc")
+	_, err = c.Get(t.Context(), "abc")
 	require.NoError(t, err)
 
-	lresp, lerr := lapi.TimeToLive(context.Background(), resp.ID, clientv3.WithAttachedKeys())
+	lresp, lerr := lapi.TimeToLive(t.Context(), resp.ID, clientv3.WithAttachedKeys())
 	require.NoError(t, lerr)
 	require.Equalf(t, lresp.ID, resp.ID, "leaseID expected %d, got %d", resp.ID, lresp.ID)
 	require.Equalf(t, int64(10), lresp.GrantedTTL, "GrantedTTL expected %d, got %d", 10, lresp.GrantedTTL)
@@ -546,7 +546,7 @@ func TestLeaseTimeToLive(t *testing.T) {
 	sort.Strings(ks)
 	require.Truef(t, reflect.DeepEqual(ks, keys), "keys expected %v, got %v", keys, ks)
 
-	lresp, lerr = lapi.TimeToLive(context.Background(), resp.ID)
+	lresp, lerr = lapi.TimeToLive(t.Context(), resp.ID)
 	require.NoError(t, lerr)
 	require.Emptyf(t, lresp.Keys, "unexpected keys %+v", lresp.Keys)
 }
@@ -558,16 +558,16 @@ func TestLeaseTimeToLiveLeaseNotFound(t *testing.T) {
 	defer clus.Terminate(t)
 
 	cli := clus.RandClient()
-	resp, err := cli.Grant(context.Background(), 10)
+	resp, err := cli.Grant(t.Context(), 10)
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
-	_, err = cli.Revoke(context.Background(), resp.ID)
+	_, err = cli.Revoke(t.Context(), resp.ID)
 	if err != nil {
 		t.Errorf("failed to Revoke lease %v", err)
 	}
 
-	lresp, err := cli.TimeToLive(context.Background(), resp.ID)
+	lresp, err := cli.TimeToLive(t.Context(), resp.ID)
 	// TimeToLive() should return a response with TTL=-1.
 	require.NoErrorf(t, err, "expected err to be nil")
 	require.NotNilf(t, lresp, "expected lresp not to be nil")
@@ -586,14 +586,14 @@ func TestLeaseLeases(t *testing.T) {
 
 	var ids []clientv3.LeaseID
 	for i := 0; i < 5; i++ {
-		resp, err := cli.Grant(context.Background(), 10)
+		resp, err := cli.Grant(t.Context(), 10)
 		if err != nil {
 			t.Errorf("failed to create lease %v", err)
 		}
 		ids = append(ids, resp.ID)
 	}
 
-	resp, err := cli.Leases(context.Background())
+	resp, err := cli.Leases(t.Context())
 	require.NoError(t, err)
 	require.Lenf(t, resp.Leases, 5, "len(resp.Leases) expected 5, got %d", len(resp.Leases))
 	for i := range resp.Leases {
@@ -610,10 +610,10 @@ func TestLeaseRenewLostQuorum(t *testing.T) {
 	defer clus.Terminate(t)
 
 	cli := clus.Client(0)
-	r, err := cli.Grant(context.TODO(), 4)
+	r, err := cli.Grant(t.Context(), 4)
 	require.NoError(t, err)
 
-	kctx, kcancel := context.WithCancel(context.Background())
+	kctx, kcancel := context.WithCancel(t.Context())
 	defer kcancel()
 	ka, err := cli.KeepAlive(kctx, r.ID)
 	require.NoError(t, err)
@@ -651,7 +651,7 @@ func TestLeaseKeepAliveLoopExit(t *testing.T) {
 	clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx := context.Background()
+	ctx := t.Context()
 	cli := clus.Client(0)
 	clus.TakeClient(0)
 
@@ -727,15 +727,15 @@ func TestLeaseWithRequireLeader(t *testing.T) {
 	defer clus.Terminate(t)
 
 	c := clus.Client(0)
-	lid1, err1 := c.Grant(context.TODO(), 60)
+	lid1, err1 := c.Grant(t.Context(), 60)
 	require.NoError(t, err1)
-	lid2, err2 := c.Grant(context.TODO(), 60)
+	lid2, err2 := c.Grant(t.Context(), 60)
 	require.NoError(t, err2)
 	// kaReqLeader close if the leader is lost
-	kaReqLeader, kerr1 := c.KeepAlive(clientv3.WithRequireLeader(context.TODO()), lid1.ID)
+	kaReqLeader, kerr1 := c.KeepAlive(clientv3.WithRequireLeader(t.Context()), lid1.ID)
 	require.NoError(t, kerr1)
 	// kaWait will wait even if the leader is lost
-	kaWait, kerr2 := c.KeepAlive(context.TODO(), lid2.ID)
+	kaWait, kerr2 := c.KeepAlive(t.Context(), lid2.ID)
 	require.NoError(t, kerr2)
 
 	select {
diff --git a/tests/integration/clientv3/lease/leasing_test.go b/tests/integration/clientv3/lease/leasing_test.go
index 1751fdeb7..09dd1cf44 100644
--- a/tests/integration/clientv3/lease/leasing_test.go
+++ b/tests/integration/clientv3/lease/leasing_test.go
@@ -51,15 +51,15 @@ func TestLeasingPutGet(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV3()
 
-	resp, err := lKV1.Get(context.TODO(), "abc")
+	resp, err := lKV1.Get(t.Context(), "abc")
 	require.NoError(t, err)
 	if len(resp.Kvs) != 0 {
 		t.Errorf("expected nil, got %q", resp.Kvs[0].Key)
 	}
 
-	_, err = lKV1.Put(context.TODO(), "abc", "def")
+	_, err = lKV1.Put(t.Context(), "abc", "def")
 	require.NoError(t, err)
-	resp, err = lKV2.Get(context.TODO(), "abc")
+	resp, err = lKV2.Get(t.Context(), "abc")
 	require.NoError(t, err)
 	if string(resp.Kvs[0].Key) != "abc" {
 		t.Errorf("expected key=%q, got key=%q", "abc", resp.Kvs[0].Key)
@@ -68,12 +68,12 @@ func TestLeasingPutGet(t *testing.T) {
 		t.Errorf("expected value=%q, got value=%q", "bar", resp.Kvs[0].Value)
 	}
 
-	_, err = lKV3.Get(context.TODO(), "abc")
+	_, err = lKV3.Get(t.Context(), "abc")
 	require.NoError(t, err)
-	_, err = lKV2.Put(context.TODO(), "abc", "ghi")
+	_, err = lKV2.Put(t.Context(), "abc", "ghi")
 	require.NoError(t, err)
 
-	resp, err = lKV3.Get(context.TODO(), "abc")
+	resp, err = lKV3.Get(t.Context(), "abc")
 	require.NoError(t, err)
 	if string(resp.Kvs[0].Key) != "abc" {
 		t.Errorf("expected key=%q, got key=%q", "abc", resp.Kvs[0].Key)
@@ -96,20 +96,20 @@ func TestLeasingInterval(t *testing.T) {
 
 	keys := []string{"abc/a", "abc/b", "abc/a/a"}
 	for _, k := range keys {
-		_, err = clus.Client(0).Put(context.TODO(), k, "v")
+		_, err = clus.Client(0).Put(t.Context(), k, "v")
 		require.NoError(t, err)
 	}
 
-	resp, err := lkv.Get(context.TODO(), "abc/", clientv3.WithPrefix())
+	resp, err := lkv.Get(t.Context(), "abc/", clientv3.WithPrefix())
 	require.NoError(t, err)
 	require.Lenf(t, resp.Kvs, 3, "expected keys %+v, got response keys %+v", keys, resp.Kvs)
 
 	// load into cache
-	_, err = lkv.Get(context.TODO(), "abc/a")
+	_, err = lkv.Get(t.Context(), "abc/a")
 	require.NoError(t, err)
 
 	// get when prefix is also a cached key
-	resp, err = lkv.Get(context.TODO(), "abc/a", clientv3.WithPrefix())
+	resp, err = lkv.Get(t.Context(), "abc/a", clientv3.WithPrefix())
 	require.NoError(t, err)
 	require.Lenf(t, resp.Kvs, 2, "expected keys %+v, got response keys %+v", keys, resp.Kvs)
 }
@@ -124,14 +124,14 @@ func TestLeasingPutInvalidateNew(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	_, err = lkv.Put(context.TODO(), "k", "v")
+	_, err = lkv.Put(t.Context(), "k", "v")
 	require.NoError(t, err)
 
-	lkvResp, err := lkv.Get(context.TODO(), "k")
+	lkvResp, err := lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	cResp, cerr := clus.Client(0).Get(context.TODO(), "k")
+	cResp, cerr := clus.Client(0).Get(t.Context(), "k")
 	require.NoError(t, cerr)
 	require.Truef(t, reflect.DeepEqual(lkvResp, cResp), `expected %+v, got response %+v`, cResp, lkvResp)
 }
@@ -142,21 +142,21 @@ func TestLeasingPutInvalidateExisting(t *testing.T) {
 	clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	_, err := clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err := clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 
 	lkv, closeLKV, err := leasing.NewKV(clus.Client(0), "pfx/")
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	_, err = lkv.Put(context.TODO(), "k", "v")
+	_, err = lkv.Put(t.Context(), "k", "v")
 	require.NoError(t, err)
 
-	lkvResp, err := lkv.Get(context.TODO(), "k")
+	lkvResp, err := lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	cResp, cerr := clus.Client(0).Get(context.TODO(), "k")
+	cResp, cerr := clus.Client(0).Get(t.Context(), "k")
 	require.NoError(t, cerr)
 	require.Truef(t, reflect.DeepEqual(lkvResp, cResp), `expected %+v, got response %+v`, cResp, lkvResp)
 }
@@ -171,19 +171,19 @@ func TestLeasingGetNoLeaseTTL(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	lresp, err := clus.Client(0).Grant(context.TODO(), 60)
+	lresp, err := clus.Client(0).Grant(t.Context(), 60)
 	require.NoError(t, err)
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "v", clientv3.WithLease(lresp.ID))
+	_, err = clus.Client(0).Put(t.Context(), "k", "v", clientv3.WithLease(lresp.ID))
 	require.NoError(t, err)
 
-	gresp, err := lkv.Get(context.TODO(), "k")
+	gresp, err := lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 	assert.Len(t, gresp.Kvs, 1)
 
 	clus.Members[0].Stop(t)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 	_, err = lkv.Get(ctx, "k")
 	cancel()
 	assert.Equal(t, err, ctx.Err())
@@ -200,22 +200,22 @@ func TestLeasingGetSerializable(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "cached", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "cached", "abc")
 	require.NoError(t, err)
-	_, err = lkv.Get(context.TODO(), "cached")
+	_, err = lkv.Get(t.Context(), "cached")
 	require.NoError(t, err)
 
 	clus.Members[1].Stop(t)
 
 	// don't necessarily try to acquire leasing key ownership for new key
-	resp, err := lkv.Get(context.TODO(), "uncached", clientv3.WithSerializable())
+	resp, err := lkv.Get(t.Context(), "uncached", clientv3.WithSerializable())
 	require.NoError(t, err)
 	require.Emptyf(t, resp.Kvs, `expected no keys, got response %+v`, resp)
 
 	clus.Members[0].Stop(t)
 
 	// leasing key ownership should have "cached" locally served
-	cachedResp, err := lkv.Get(context.TODO(), "cached", clientv3.WithSerializable())
+	cachedResp, err := lkv.Get(t.Context(), "cached", clientv3.WithSerializable())
 	require.NoError(t, err)
 	if len(cachedResp.Kvs) != 1 || string(cachedResp.Kvs[0].Value) != "abc" {
 		t.Fatalf(`expected "cached"->"abc", got response %+v`, cachedResp)
@@ -232,12 +232,12 @@ func TestLeasingPrevKey(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 	// acquire leasing key
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	resp, err := lkv.Put(context.TODO(), "k", "def", clientv3.WithPrevKV())
+	resp, err := lkv.Put(t.Context(), "k", "def", clientv3.WithPrevKV())
 	require.NoError(t, err)
 	if resp.PrevKv == nil || string(resp.PrevKv.Value) != "abc" {
 		t.Fatalf(`expected PrevKV.Value="abc", got response %+v`, resp)
@@ -254,19 +254,19 @@ func TestLeasingRevGet(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	putResp, err := clus.Client(0).Put(context.TODO(), "k", "abc")
+	putResp, err := clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
-	_, err = clus.Client(0).Put(context.TODO(), "k", "def")
+	_, err = clus.Client(0).Put(t.Context(), "k", "def")
 	require.NoError(t, err)
 
 	// check historic revision
-	getResp, gerr := lkv.Get(context.TODO(), "k", clientv3.WithRev(putResp.Header.Revision))
+	getResp, gerr := lkv.Get(t.Context(), "k", clientv3.WithRev(putResp.Header.Revision))
 	require.NoError(t, gerr)
 	if len(getResp.Kvs) != 1 || string(getResp.Kvs[0].Value) != "abc" {
 		t.Fatalf(`expected "k"->"abc" at rev=%d, got response %+v`, putResp.Header.Revision, getResp)
 	}
 	// check current revision
-	getResp, gerr = lkv.Get(context.TODO(), "k")
+	getResp, gerr = lkv.Get(t.Context(), "k")
 	require.NoError(t, gerr)
 	if len(getResp.Kvs) != 1 || string(getResp.Kvs[0].Value) != "def" {
 		t.Fatalf(`expected "k"->"def" at rev=%d, got response %+v`, putResp.Header.Revision, getResp)
@@ -283,10 +283,10 @@ func TestLeasingGetWithOpts(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 	// in cache
-	_, err = lkv.Get(context.TODO(), "k", clientv3.WithKeysOnly())
+	_, err = lkv.Get(t.Context(), "k", clientv3.WithKeysOnly())
 	require.NoError(t, err)
 
 	clus.Members[0].Stop(t)
@@ -300,7 +300,7 @@ func TestLeasingGetWithOpts(t *testing.T) {
 		clientv3.WithSerializable(),
 	}
 	for _, opt := range opts {
-		_, err = lkv.Get(context.TODO(), "k", opt)
+		_, err = lkv.Get(t.Context(), "k", opt)
 		require.NoError(t, err)
 	}
 
@@ -309,7 +309,7 @@ func TestLeasingGetWithOpts(t *testing.T) {
 		getOpts = append(getOpts, opts[rand.Intn(len(opts))])
 	}
 	getOpts = getOpts[:rand.Intn(len(opts))]
-	_, err = lkv.Get(context.TODO(), "k", getOpts...)
+	_, err = lkv.Get(t.Context(), "k", getOpts...)
 	require.NoError(t, err)
 }
 
@@ -325,7 +325,7 @@ func TestLeasingConcurrentPut(t *testing.T) {
 	defer closeLKV()
 
 	// force key into leasing key cache
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	// concurrently put through leasing client
@@ -333,7 +333,7 @@ func TestLeasingConcurrentPut(t *testing.T) {
 	putc := make(chan *clientv3.PutResponse, numPuts)
 	for i := 0; i < numPuts; i++ {
 		go func() {
-			resp, perr := lkv.Put(context.TODO(), "k", "abc")
+			resp, perr := lkv.Put(t.Context(), "k", "abc")
 			if perr != nil {
 				t.Error(perr)
 			}
@@ -349,7 +349,7 @@ func TestLeasingConcurrentPut(t *testing.T) {
 	}
 
 	// confirm Get gives most recently put revisions
-	getResp, gerr := lkv.Get(context.TODO(), "k")
+	getResp, gerr := lkv.Get(t.Context(), "k")
 	require.NoError(t, gerr)
 	if mr := getResp.Kvs[0].ModRevision; mr != maxRev {
 		t.Errorf("expected ModRevision %d, got %d", maxRev, mr)
@@ -368,16 +368,16 @@ func TestLeasingDisconnectedGet(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "cached", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "cached", "abc")
 	require.NoError(t, err)
 	// get key so it's cached
-	_, err = lkv.Get(context.TODO(), "cached")
+	_, err = lkv.Get(t.Context(), "cached")
 	require.NoError(t, err)
 
 	clus.Members[0].Stop(t)
 
 	// leasing key ownership should have "cached" locally served
-	cachedResp, err := lkv.Get(context.TODO(), "cached")
+	cachedResp, err := lkv.Get(t.Context(), "cached")
 	require.NoError(t, err)
 	if len(cachedResp.Kvs) != 1 || string(cachedResp.Kvs[0].Value) != "abc" {
 		t.Fatalf(`expected "cached"->"abc", got response %+v`, cachedResp)
@@ -393,20 +393,20 @@ func TestLeasingDeleteOwner(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 
 	// get+own / delete / get
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	_, err = lkv.Delete(context.TODO(), "k")
+	_, err = lkv.Delete(t.Context(), "k")
 	require.NoError(t, err)
-	resp, err := lkv.Get(context.TODO(), "k")
+	resp, err := lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	require.Emptyf(t, resp.Kvs, `expected "k" to be deleted, got response %+v`, resp)
 	// try to double delete
-	_, err = lkv.Delete(context.TODO(), "k")
+	_, err = lkv.Delete(t.Context(), "k")
 	require.NoError(t, err)
 }
 
@@ -423,17 +423,17 @@ func TestLeasingDeleteNonOwner(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV2()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 	// acquire ownership
-	_, err = lkv1.Get(context.TODO(), "k")
+	_, err = lkv1.Get(t.Context(), "k")
 	require.NoError(t, err)
 	// delete via non-owner
-	_, err = lkv2.Delete(context.TODO(), "k")
+	_, err = lkv2.Delete(t.Context(), "k")
 	require.NoError(t, err)
 
 	// key should be removed from lkv1
-	resp, err := lkv1.Get(context.TODO(), "k")
+	resp, err := lkv1.Get(t.Context(), "k")
 	require.NoError(t, err)
 	require.Emptyf(t, resp.Kvs, `expected "k" to be deleted, got response %+v`, resp)
 }
@@ -447,16 +447,16 @@ func TestLeasingOverwriteResponse(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 
-	resp, err := lkv.Get(context.TODO(), "k")
+	resp, err := lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	resp.Kvs[0].Key[0] = 'z'
 	resp.Kvs[0].Value[0] = 'z'
 
-	resp, err = lkv.Get(context.TODO(), "k")
+	resp, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	if string(resp.Kvs[0].Key) != "k" {
@@ -476,11 +476,11 @@ func TestLeasingOwnerPutResponse(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
-	_, gerr := lkv.Get(context.TODO(), "k")
+	_, gerr := lkv.Get(t.Context(), "k")
 	require.NoError(t, gerr)
-	presp, err := lkv.Put(context.TODO(), "k", "def")
+	presp, err := lkv.Put(t.Context(), "k", "def")
 	require.NoError(t, err)
 	if presp == nil {
 		t.Fatal("expected put response, got nil")
@@ -488,7 +488,7 @@ func TestLeasingOwnerPutResponse(t *testing.T) {
 
 	clus.Members[0].Stop(t)
 
-	gresp, gerr := lkv.Get(context.TODO(), "k")
+	gresp, gerr := lkv.Get(t.Context(), "k")
 	require.NoError(t, gerr)
 	if gresp.Kvs[0].ModRevision != presp.Header.Revision {
 		t.Errorf("expected mod revision %d, got %d", presp.Header.Revision, gresp.Kvs[0].ModRevision)
@@ -510,13 +510,13 @@ func TestLeasingTxnOwnerGetRange(t *testing.T) {
 	keyCount := rand.Intn(10) + 1
 	for i := 0; i < keyCount; i++ {
 		k := fmt.Sprintf("k-%d", i)
-		_, err = clus.Client(0).Put(context.TODO(), k, k+k)
+		_, err = clus.Client(0).Put(t.Context(), k, k+k)
 		require.NoError(t, err)
 	}
-	_, err = lkv.Get(context.TODO(), "k-")
+	_, err = lkv.Get(t.Context(), "k-")
 	require.NoError(t, err)
 
-	tresp, terr := lkv.Txn(context.TODO()).Then(clientv3.OpGet("k-", clientv3.WithPrefix())).Commit()
+	tresp, terr := lkv.Txn(t.Context()).Then(clientv3.OpGet("k-", clientv3.WithPrefix())).Commit()
 	require.NoError(t, terr)
 	resp := tresp.Responses[0].GetResponseRange()
 	require.Equalf(t, len(resp.Kvs), keyCount, "expected %d keys, got response %+v", keyCount, resp.Kvs)
@@ -549,11 +549,11 @@ func TestLeasingTxnOwnerGet(t *testing.T) {
 	presps := make([]*clientv3.PutResponse, keyCount)
 	for i := range presps {
 		k := fmt.Sprintf("k-%d", i)
-		presp, err := client.Put(context.TODO(), k, k+k)
+		presp, err := client.Put(t.Context(), k, k+k)
 		require.NoError(t, err)
 		presps[i] = presp
 
-		_, err = lkv.Get(context.TODO(), k)
+		_, err = lkv.Get(t.Context(), k)
 		require.NoError(t, err)
 		ops = append(ops, clientv3.OpGet(k))
 	}
@@ -575,7 +575,7 @@ func TestLeasingTxnOwnerGet(t *testing.T) {
 		elseOps = ops
 	}
 
-	tresp, terr := lkv.Txn(context.TODO()).
+	tresp, terr := lkv.Txn(t.Context()).
 		If(cmps...).
 		Then(thenOps...).
 		Else(elseOps...).Commit()
@@ -607,19 +607,19 @@ func TestLeasingTxnOwnerDeleteRange(t *testing.T) {
 	keyCount := rand.Intn(10) + 1
 	for i := 0; i < keyCount; i++ {
 		k := fmt.Sprintf("k-%d", i)
-		_, perr := clus.Client(0).Put(context.TODO(), k, k+k)
+		_, perr := clus.Client(0).Put(t.Context(), k, k+k)
 		require.NoError(t, perr)
 	}
 
 	// cache in lkv
-	resp, err := lkv.Get(context.TODO(), "k-", clientv3.WithPrefix())
+	resp, err := lkv.Get(t.Context(), "k-", clientv3.WithPrefix())
 	require.NoError(t, err)
 	require.Equalf(t, len(resp.Kvs), keyCount, "expected %d keys, got %d", keyCount, len(resp.Kvs))
 
-	_, terr := lkv.Txn(context.TODO()).Then(clientv3.OpDelete("k-", clientv3.WithPrefix())).Commit()
+	_, terr := lkv.Txn(t.Context()).Then(clientv3.OpDelete("k-", clientv3.WithPrefix())).Commit()
 	require.NoError(t, terr)
 
-	resp, err = lkv.Get(context.TODO(), "k-", clientv3.WithPrefix())
+	resp, err = lkv.Get(t.Context(), "k-", clientv3.WithPrefix())
 	require.NoError(t, err)
 	require.Emptyf(t, resp.Kvs, "expected no keys, got %d", len(resp.Kvs))
 }
@@ -633,17 +633,17 @@ func TestLeasingTxnOwnerDelete(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
 
 	// cache in lkv
-	_, gerr := lkv.Get(context.TODO(), "k")
+	_, gerr := lkv.Get(t.Context(), "k")
 	require.NoError(t, gerr)
 
-	_, terr := lkv.Txn(context.TODO()).Then(clientv3.OpDelete("k")).Commit()
+	_, terr := lkv.Txn(t.Context()).Then(clientv3.OpDelete("k")).Commit()
 	require.NoError(t, terr)
 
-	resp, err := lkv.Get(context.TODO(), "k")
+	resp, err := lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 	require.Emptyf(t, resp.Kvs, "expected no keys, got %d", len(resp.Kvs))
 }
@@ -657,9 +657,9 @@ func TestLeasingTxnOwnerIf(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	// served through cache
@@ -727,7 +727,7 @@ func TestLeasingTxnOwnerIf(t *testing.T) {
 	}
 
 	for i, tt := range tests {
-		tresp, terr := lkv.Txn(context.TODO()).If(tt.cmps...).Then(clientv3.OpGet("k")).Commit()
+		tresp, terr := lkv.Txn(t.Context()).If(tt.cmps...).Then(clientv3.OpGet("k")).Commit()
 		require.NoError(t, terr)
 		if tresp.Succeeded != tt.wSucceeded {
 			t.Errorf("#%d: expected succeeded %v, got %v", i, tt.wSucceeded, tresp.Succeeded)
@@ -752,15 +752,15 @@ func TestLeasingTxnCancel(t *testing.T) {
 	defer closeLKV2()
 
 	// acquire lease but disconnect so no revoke in time
-	_, err = lkv1.Get(context.TODO(), "k")
+	_, err = lkv1.Get(t.Context(), "k")
 	require.NoError(t, err)
 	clus.Members[0].Stop(t)
 
 	// wait for leader election, if any
-	_, err = clus.Client(1).Get(context.TODO(), "abc")
+	_, err = clus.Client(1).Get(t.Context(), "abc")
 	require.NoError(t, err)
 
-	ctx, cancel := context.WithCancel(context.TODO())
+	ctx, cancel := context.WithCancel(t.Context())
 	go func() {
 		time.Sleep(100 * time.Millisecond)
 		cancel()
@@ -782,19 +782,19 @@ func TestLeasingTxnNonOwnerPut(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV2()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "abc")
+	_, err = clus.Client(0).Put(t.Context(), "k", "abc")
 	require.NoError(t, err)
-	_, err = clus.Client(0).Put(context.TODO(), "k2", "123")
+	_, err = clus.Client(0).Put(t.Context(), "k2", "123")
 	require.NoError(t, err)
 	// cache in lkv
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
-	_, err = lkv.Get(context.TODO(), "k2")
+	_, err = lkv.Get(t.Context(), "k2")
 	require.NoError(t, err)
 	// invalidate via lkv2 txn
 	opArray := make([]clientv3.Op, 0)
 	opArray = append(opArray, clientv3.OpPut("k2", "456"))
-	tresp, terr := lkv2.Txn(context.TODO()).Then(
+	tresp, terr := lkv2.Txn(t.Context()).Then(
 		clientv3.OpTxn(nil, opArray, nil),
 		clientv3.OpPut("k", "def"),
 		clientv3.OpPut("k3", "999"), // + a key not in any cache
@@ -804,12 +804,12 @@ func TestLeasingTxnNonOwnerPut(t *testing.T) {
 		t.Fatalf("expected txn success, got %+v", tresp)
 	}
 	// check cache was invalidated
-	gresp, gerr := lkv.Get(context.TODO(), "k")
+	gresp, gerr := lkv.Get(t.Context(), "k")
 	require.NoError(t, gerr)
 	if len(gresp.Kvs) != 1 || string(gresp.Kvs[0].Value) != "def" {
 		t.Errorf(`expected value "def", got %+v`, gresp)
 	}
-	gresp, gerr = lkv.Get(context.TODO(), "k2")
+	gresp, gerr = lkv.Get(t.Context(), "k2")
 	require.NoError(t, gerr)
 	if len(gresp.Kvs) != 1 || string(gresp.Kvs[0].Value) != "456" {
 		t.Errorf(`expected value "def", got %+v`, gresp)
@@ -852,7 +852,7 @@ func TestLeasingTxnRandIfThenOrElse(t *testing.T) {
 	dat := make([]*clientv3.PutResponse, keyCount)
 	for i := 0; i < keyCount; i++ {
 		k, v := fmt.Sprintf("k-%d", i), fmt.Sprintf("%d", i)
-		dat[i], err1 = clus.Client(0).Put(context.TODO(), k, v)
+		dat[i], err1 = clus.Client(0).Put(t.Context(), k, v)
 		require.NoError(t, err1)
 	}
 
@@ -863,7 +863,7 @@ func TestLeasingTxnRandIfThenOrElse(t *testing.T) {
 		defer wg.Done()
 		for i := 0; i < keyCount/2; i++ {
 			k := fmt.Sprintf("k-%d", rand.Intn(keyCount))
-			if _, err := kv.Get(context.TODO(), k); err != nil {
+			if _, err := kv.Get(t.Context(), k); err != nil {
 				t.Error(err)
 			}
 			getc <- struct{}{}
@@ -911,7 +911,7 @@ func TestLeasingTxnRandIfThenOrElse(t *testing.T) {
 		elseOps = ops
 	}
 
-	tresp, terr := lkv1.Txn(context.TODO()).If(cmps...).Then(thenOps...).Else(elseOps...).Commit()
+	tresp, terr := lkv1.Txn(t.Context()).If(cmps...).Then(thenOps...).Else(elseOps...).Commit()
 	require.NoError(t, terr)
 	// cmps always succeed
 	require.Equalf(t, tresp.Succeeded, useThen, "expected succeeded=%v, got tresp=%+v", useThen, tresp)
@@ -921,7 +921,7 @@ func TestLeasingTxnRandIfThenOrElse(t *testing.T) {
 			if !op.IsPut() {
 				continue
 			}
-			resp, rerr := kv.Get(context.TODO(), string(op.KeyBytes()))
+			resp, rerr := kv.Get(t.Context(), string(op.KeyBytes()))
 			require.NoError(t, rerr)
 			if len(resp.Kvs) != 1 || string(resp.Kvs[0].Value) != "a" {
 				t.Fatalf(`%s: expected value="a", got %+v`, s, resp.Kvs)
@@ -942,11 +942,11 @@ func TestLeasingOwnerPutError(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	clus.Members[0].Stop(t)
-	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
+	ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
 	defer cancel()
 	resp, err := lkv.Put(ctx, "k", "v")
 	require.Errorf(t, err, "expected error, got response %+v", resp)
@@ -961,11 +961,11 @@ func TestLeasingOwnerDeleteError(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	clus.Members[0].Stop(t)
-	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
+	ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
 	defer cancel()
 	resp, err := lkv.Delete(ctx, "k")
 	require.Errorf(t, err, "expected error, got response %+v", resp)
@@ -981,7 +981,7 @@ func TestLeasingNonOwnerPutError(t *testing.T) {
 	defer closeLKV()
 
 	clus.Members[0].Stop(t)
-	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
+	ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
 	defer cancel()
 	resp, err := lkv.Put(ctx, "k", "v")
 	require.Errorf(t, err, "expected error, got response %+v", resp)
@@ -1005,20 +1005,20 @@ func testLeasingOwnerDelete(t *testing.T, del clientv3.Op) {
 	defer closeLKV()
 
 	for i := 0; i < 8; i++ {
-		_, err = clus.Client(0).Put(context.TODO(), fmt.Sprintf("key/%d", i), "123")
+		_, err = clus.Client(0).Put(t.Context(), fmt.Sprintf("key/%d", i), "123")
 		require.NoError(t, err)
 	}
 
-	_, err = lkv.Get(context.TODO(), "key/1")
+	_, err = lkv.Get(t.Context(), "key/1")
 	require.NoError(t, err)
 
-	opResp, delErr := lkv.Do(context.TODO(), del)
+	opResp, delErr := lkv.Do(t.Context(), del)
 	require.NoError(t, delErr)
 	delResp := opResp.Del()
 
 	// confirm keys are invalidated from cache and deleted on etcd
 	for i := 0; i < 8; i++ {
-		resp, err := lkv.Get(context.TODO(), fmt.Sprintf("key/%d", i))
+		resp, err := lkv.Get(t.Context(), fmt.Sprintf("key/%d", i))
 		require.NoError(t, err)
 		require.Emptyf(t, resp.Kvs, "expected no keys on key/%d, got %+v", i, resp)
 	}
@@ -1049,18 +1049,18 @@ func TestLeasingDeleteRangeBounds(t *testing.T) {
 	defer closeGetKv()
 
 	for _, k := range []string{"j", "m"} {
-		_, err = clus.Client(0).Put(context.TODO(), k, "123")
+		_, err = clus.Client(0).Put(t.Context(), k, "123")
 		require.NoError(t, err)
-		_, err = getkv.Get(context.TODO(), k)
+		_, err = getkv.Get(t.Context(), k)
 		require.NoError(t, err)
 	}
 
-	_, err = delkv.Delete(context.TODO(), "k", clientv3.WithPrefix())
+	_, err = delkv.Delete(t.Context(), "k", clientv3.WithPrefix())
 	require.NoError(t, err)
 
 	// leases still on server?
 	for _, k := range []string{"j", "m"} {
-		resp, geterr := clus.Client(0).Get(context.TODO(), "0/"+k, clientv3.WithPrefix())
+		resp, geterr := clus.Client(0).Get(t.Context(), "0/"+k, clientv3.WithPrefix())
 		require.NoError(t, geterr)
 		require.Lenf(t, resp.Kvs, 1, "expected leasing key, got %+v", resp)
 	}
@@ -1068,9 +1068,9 @@ func TestLeasingDeleteRangeBounds(t *testing.T) {
 	// j and m should still have leases registered since not under k*
 	clus.Members[0].Stop(t)
 
-	_, err = getkv.Get(context.TODO(), "j")
+	_, err = getkv.Get(t.Context(), "j")
 	require.NoError(t, err)
-	_, err = getkv.Get(context.TODO(), "m")
+	_, err = getkv.Get(t.Context(), "m")
 	require.NoError(t, err)
 }
 
@@ -1100,28 +1100,28 @@ func testLeasingDeleteRangeContend(t *testing.T, op clientv3.Op) {
 	const maxKey = 8
 	for i := 0; i < maxKey; i++ {
 		key := fmt.Sprintf("key/%d", i)
-		_, err = clus.Client(0).Put(context.TODO(), key, "123")
+		_, err = clus.Client(0).Put(t.Context(), key, "123")
 		require.NoError(t, err)
-		_, err = putkv.Get(context.TODO(), key)
+		_, err = putkv.Get(t.Context(), key)
 		require.NoError(t, err)
 	}
 
-	ctx, cancel := context.WithCancel(context.TODO())
+	ctx, cancel := context.WithCancel(t.Context())
 	donec := make(chan struct{})
 	go func(t *testing.T) {
 		defer close(donec)
 		for i := 0; ctx.Err() == nil; i++ {
 			key := fmt.Sprintf("key/%d", i%maxKey)
-			if _, err = putkv.Put(context.TODO(), key, "123"); err != nil {
+			if _, err = putkv.Put(t.Context(), key, "123"); err != nil {
 				t.Errorf("fail putting key %s: %v", key, err)
 			}
-			if _, err = putkv.Get(context.TODO(), key); err != nil {
+			if _, err = putkv.Get(t.Context(), key); err != nil {
 				t.Errorf("fail getting key %s: %v", key, err)
 			}
 		}
 	}(t)
 
-	_, delErr := delkv.Do(context.TODO(), op)
+	_, delErr := delkv.Do(t.Context(), op)
 	cancel()
 	<-donec
 	require.NoError(t, delErr)
@@ -1129,9 +1129,9 @@ func testLeasingDeleteRangeContend(t *testing.T, op clientv3.Op) {
 	// confirm keys on non-deleter match etcd
 	for i := 0; i < maxKey; i++ {
 		key := fmt.Sprintf("key/%d", i)
-		resp, err := putkv.Get(context.TODO(), key)
+		resp, err := putkv.Get(t.Context(), key)
 		require.NoError(t, err)
-		servResp, err := clus.Client(0).Get(context.TODO(), key)
+		servResp, err := clus.Client(0).Get(t.Context(), key)
 		require.NoError(t, err)
 		if !reflect.DeepEqual(resp.Kvs, servResp.Kvs) {
 			t.Errorf("#%d: expected %+v, got %+v", i, servResp.Kvs, resp.Kvs)
@@ -1153,12 +1153,12 @@ func TestLeasingPutGetDeleteConcurrent(t *testing.T) {
 	}
 
 	getdel := func(kv clientv3.KV) {
-		_, err := kv.Put(context.TODO(), "k", "abc")
+		_, err := kv.Put(t.Context(), "k", "abc")
 		require.NoError(t, err)
 		time.Sleep(time.Millisecond)
-		_, err = kv.Get(context.TODO(), "k")
+		_, err = kv.Get(t.Context(), "k")
 		require.NoError(t, err)
-		_, err = kv.Delete(context.TODO(), "k")
+		_, err = kv.Delete(t.Context(), "k")
 		require.NoError(t, err)
 		time.Sleep(2 * time.Millisecond)
 	}
@@ -1175,12 +1175,12 @@ func TestLeasingPutGetDeleteConcurrent(t *testing.T) {
 	}
 	wg.Wait()
 
-	resp, err := lkvs[0].Get(context.TODO(), "k")
+	resp, err := lkvs[0].Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	require.Emptyf(t, resp.Kvs, "expected no kvs, got %+v", resp.Kvs)
 
-	resp, err = clus.Client(0).Get(context.TODO(), "k")
+	resp, err = clus.Client(0).Get(t.Context(), "k")
 	require.NoError(t, err)
 	require.Emptyf(t, resp.Kvs, "expected no kvs, got %+v", resp.Kvs)
 }
@@ -1200,7 +1200,7 @@ func TestLeasingReconnectOwnerRevoke(t *testing.T) {
 	require.NoError(t, err2)
 	defer closeLKV2()
 
-	_, err := lkv1.Get(context.TODO(), "k")
+	_, err := lkv1.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	// force leader away from member 0
@@ -1208,7 +1208,7 @@ func TestLeasingReconnectOwnerRevoke(t *testing.T) {
 	clus.WaitLeader(t)
 	clus.Members[0].Restart(t)
 
-	cctx, cancel := context.WithCancel(context.TODO())
+	cctx, cancel := context.WithCancel(t.Context())
 	sdonec, pdonec := make(chan struct{}), make(chan struct{})
 	// make lkv1 connection choppy so Txn fails
 	go func() {
@@ -1260,25 +1260,25 @@ func TestLeasingReconnectOwnerRevokeCompact(t *testing.T) {
 	require.NoError(t, err2)
 	defer closeLKV2()
 
-	_, err := lkv1.Get(context.TODO(), "k")
+	_, err := lkv1.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	clus.Members[0].Stop(t)
 	clus.WaitLeader(t)
 
 	// put some more revisions for compaction
-	_, err = clus.Client(1).Put(context.TODO(), "a", "123")
+	_, err = clus.Client(1).Put(t.Context(), "a", "123")
 	require.NoError(t, err)
-	presp, err := clus.Client(1).Put(context.TODO(), "a", "123")
+	presp, err := clus.Client(1).Put(t.Context(), "a", "123")
 	require.NoError(t, err)
 	// compact while lkv1 is disconnected
 	rev := presp.Header.Revision
-	_, err = clus.Client(1).Compact(context.TODO(), rev)
+	_, err = clus.Client(1).Compact(t.Context(), rev)
 	require.NoError(t, err)
 
 	clus.Members[0].Restart(t)
 
-	cctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
+	cctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer cancel()
 	_, err = lkv2.Put(cctx, "k", "v")
 	require.NoError(t, err)
@@ -1298,11 +1298,11 @@ func TestLeasingReconnectOwnerConsistency(t *testing.T) {
 	defer closeLKV()
 	require.NoError(t, err)
 
-	_, err = lkv.Put(context.TODO(), "k", "x")
+	_, err = lkv.Put(t.Context(), "k", "x")
 	require.NoError(t, err)
-	_, err = lkv.Put(context.TODO(), "kk", "y")
+	_, err = lkv.Put(t.Context(), "kk", "y")
 	require.NoError(t, err)
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	for i := 0; i < 10; i++ {
@@ -1318,27 +1318,27 @@ func TestLeasingReconnectOwnerConsistency(t *testing.T) {
 		}()
 		switch rand.Intn(7) {
 		case 0:
-			_, err = lkv.Put(context.TODO(), "k", v)
+			_, err = lkv.Put(t.Context(), "k", v)
 		case 1:
-			_, err = lkv.Delete(context.TODO(), "k")
+			_, err = lkv.Delete(t.Context(), "k")
 		case 2:
-			txn := lkv.Txn(context.TODO()).Then(
+			txn := lkv.Txn(t.Context()).Then(
 				clientv3.OpGet("k"),
 				clientv3.OpDelete("k"),
 			)
 			_, err = txn.Commit()
 		case 3:
-			txn := lkv.Txn(context.TODO()).Then(
+			txn := lkv.Txn(t.Context()).Then(
 				clientv3.OpGet("k"),
 				clientv3.OpPut("k", v),
 			)
 			_, err = txn.Commit()
 		case 4:
-			_, err = lkv.Do(context.TODO(), clientv3.OpPut("k", v))
+			_, err = lkv.Do(t.Context(), clientv3.OpPut("k", v))
 		case 5:
-			_, err = lkv.Do(context.TODO(), clientv3.OpDelete("k"))
+			_, err = lkv.Do(t.Context(), clientv3.OpDelete("k"))
 		case 6:
-			_, err = lkv.Delete(context.TODO(), "k", clientv3.WithPrefix())
+			_, err = lkv.Delete(t.Context(), "k", clientv3.WithPrefix())
 		}
 		<-donec
 		if err != nil {
@@ -1347,9 +1347,9 @@ func TestLeasingReconnectOwnerConsistency(t *testing.T) {
 		}
 	}
 
-	lresp, lerr := lkv.Get(context.TODO(), "k")
+	lresp, lerr := lkv.Get(t.Context(), "k")
 	require.NoError(t, lerr)
-	cresp, cerr := clus.Client(0).Get(context.TODO(), "k")
+	cresp, cerr := clus.Client(0).Get(t.Context(), "k")
 	require.NoError(t, cerr)
 	require.Truef(t, reflect.DeepEqual(lresp.Kvs, cresp.Kvs), "expected %+v, got %+v", cresp, lresp)
 }
@@ -1368,10 +1368,10 @@ func TestLeasingTxnAtomicCache(t *testing.T) {
 		k := fmt.Sprintf("k-%d", i)
 		puts[i], gets[i] = clientv3.OpPut(k, k), clientv3.OpGet(k)
 	}
-	_, err = clus.Client(0).Txn(context.TODO()).Then(puts...).Commit()
+	_, err = clus.Client(0).Txn(t.Context()).Then(puts...).Commit()
 	require.NoError(t, err)
 	for i := range gets {
-		_, err = lkv.Do(context.TODO(), gets[i])
+		_, err = lkv.Do(t.Context(), gets[i])
 		require.NoError(t, err)
 	}
 
@@ -1385,7 +1385,7 @@ func TestLeasingTxnAtomicCache(t *testing.T) {
 	f := func() {
 		defer wgPutters.Done()
 		for i := 0; i < 10; i++ {
-			if _, txnerr := lkv.Txn(context.TODO()).Then(puts...).Commit(); txnerr != nil {
+			if _, txnerr := lkv.Txn(t.Context()).Then(puts...).Commit(); txnerr != nil {
 				select {
 				case txnerrCh <- txnerr:
 				default:
@@ -1403,7 +1403,7 @@ func TestLeasingTxnAtomicCache(t *testing.T) {
 				return
 			default:
 			}
-			tresp, err := lkv.Txn(context.TODO()).Then(gets...).Commit()
+			tresp, err := lkv.Txn(t.Context()).Then(gets...).Commit()
 			if err != nil {
 				t.Error(err)
 			}
@@ -1447,7 +1447,7 @@ func TestLeasingReconnectTxn(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	donec := make(chan struct{})
@@ -1461,7 +1461,7 @@ func TestLeasingReconnectTxn(t *testing.T) {
 		time.Sleep(10 * time.Millisecond)
 	}()
 
-	_, lerr := lkv.Txn(context.TODO()).
+	_, lerr := lkv.Txn(t.Context()).
 		If(clientv3.Compare(clientv3.Version("k"), "=", 0)).
 		Then(clientv3.OpGet("k")).
 		Commit()
@@ -1483,7 +1483,7 @@ func TestLeasingReconnectNonOwnerGet(t *testing.T) {
 	// populate a few keys so some leasing gets have keys
 	for i := 0; i < 4; i++ {
 		k := fmt.Sprintf("k-%d", i*2)
-		_, err = lkv.Put(context.TODO(), k, k[2:])
+		_, err = lkv.Put(t.Context(), k, k[2:])
 		require.NoError(t, err)
 	}
 
@@ -1498,7 +1498,7 @@ func TestLeasingReconnectNonOwnerGet(t *testing.T) {
 				time.Sleep(time.Millisecond)
 			}
 		}()
-		_, err = lkv.Get(context.TODO(), fmt.Sprintf("k-%d", i))
+		_, err = lkv.Get(t.Context(), fmt.Sprintf("k-%d", i))
 		<-donec
 		n++
 		if err != nil {
@@ -1507,9 +1507,9 @@ func TestLeasingReconnectNonOwnerGet(t *testing.T) {
 	}
 	for i := 0; i < n; i++ {
 		k := fmt.Sprintf("k-%d", i)
-		lresp, lerr := lkv.Get(context.TODO(), k)
+		lresp, lerr := lkv.Get(t.Context(), k)
 		require.NoError(t, lerr)
-		cresp, cerr := clus.Client(0).Get(context.TODO(), k)
+		cresp, cerr := clus.Client(0).Get(t.Context(), k)
 		require.NoError(t, cerr)
 		require.Truef(t, reflect.DeepEqual(lresp.Kvs, cresp.Kvs), "expected %+v, got %+v", cresp, lresp)
 	}
@@ -1524,20 +1524,20 @@ func TestLeasingTxnRangeCmp(t *testing.T) {
 	require.NoError(t, err)
 	defer closeLKV()
 
-	_, err = clus.Client(0).Put(context.TODO(), "k", "a")
+	_, err = clus.Client(0).Put(t.Context(), "k", "a")
 	require.NoError(t, err)
 	// k2 version = 2
-	_, err = clus.Client(0).Put(context.TODO(), "k2", "a")
+	_, err = clus.Client(0).Put(t.Context(), "k2", "a")
 	require.NoError(t, err)
-	_, err = clus.Client(0).Put(context.TODO(), "k2", "a")
+	_, err = clus.Client(0).Put(t.Context(), "k2", "a")
 	require.NoError(t, err)
 
 	// cache k
-	_, err = lkv.Get(context.TODO(), "k")
+	_, err = lkv.Get(t.Context(), "k")
 	require.NoError(t, err)
 
 	cmp := clientv3.Compare(clientv3.Version("k").WithPrefix(), "=", 1)
-	tresp, terr := lkv.Txn(context.TODO()).If(cmp).Commit()
+	tresp, terr := lkv.Txn(t.Context()).If(cmp).Commit()
 	require.NoError(t, terr)
 	require.Falsef(t, tresp.Succeeded, "expected Succeeded=false, got %+v", tresp)
 }
@@ -1559,7 +1559,7 @@ func TestLeasingDo(t *testing.T) {
 		clientv3.OpTxn(nil, nil, nil),
 	}
 	for i, op := range ops {
-		resp, resperr := lkv.Do(context.TODO(), op)
+		resp, resperr := lkv.Do(t.Context(), op)
 		if resperr != nil {
 			t.Errorf("#%d: failed (%v)", i, resperr)
 		}
@@ -1575,7 +1575,7 @@ func TestLeasingDo(t *testing.T) {
 		}
 	}
 
-	gresp, err := clus.Client(0).Get(context.TODO(), "a", clientv3.WithPrefix())
+	gresp, err := clus.Client(0).Get(t.Context(), "a", clientv3.WithPrefix())
 	require.NoError(t, err)
 	require.Emptyf(t, gresp.Kvs, "expected no keys, got %+v", gresp.Kvs)
 }
@@ -1593,13 +1593,13 @@ func TestLeasingTxnOwnerPutBranch(t *testing.T) {
 	treeOp := makePutTreeOp("tree", &n, 4)
 	for i := 0; i < n; i++ {
 		k := fmt.Sprintf("tree/%d", i)
-		_, err = clus.Client(0).Put(context.TODO(), k, "a")
+		_, err = clus.Client(0).Put(t.Context(), k, "a")
 		require.NoError(t, err)
-		_, err = lkv.Get(context.TODO(), k)
+		_, err = lkv.Get(t.Context(), k)
 		require.NoError(t, err)
 	}
 
-	_, err = lkv.Do(context.TODO(), treeOp)
+	_, err = lkv.Do(t.Context(), treeOp)
 	require.NoError(t, err)
 
 	// lkv shouldn't need to call out to server for updated leased keys
@@ -1607,9 +1607,9 @@ func TestLeasingTxnOwnerPutBranch(t *testing.T) {
 
 	for i := 0; i < n; i++ {
 		k := fmt.Sprintf("tree/%d", i)
-		lkvResp, err := lkv.Get(context.TODO(), k)
+		lkvResp, err := lkv.Get(t.Context(), k)
 		require.NoError(t, err)
-		clusResp, err := clus.Client(1).Get(context.TODO(), k)
+		clusResp, err := clus.Client(1).Get(t.Context(), k)
 		require.NoError(t, err)
 		require.Truef(t, reflect.DeepEqual(clusResp.Kvs, lkvResp.Kvs), "expected %+v, got %+v", clusResp.Kvs, lkvResp.Kvs)
 	}
@@ -1678,7 +1678,7 @@ func TestLeasingSessionExpire(t *testing.T) {
 	defer closeLKV2()
 
 	// acquire lease on abc
-	_, err = lkv.Get(context.TODO(), "abc")
+	_, err = lkv.Get(t.Context(), "abc")
 	require.NoError(t, err)
 
 	// down endpoint lkv uses for keepalives
@@ -1688,10 +1688,10 @@ func TestLeasingSessionExpire(t *testing.T) {
 	waitForExpireAck(t, lkv)
 	clus.Members[0].Restart(t)
 	integration2.WaitClientV3(t, lkv2)
-	_, err = lkv2.Put(context.TODO(), "abc", "def")
+	_, err = lkv2.Put(t.Context(), "abc", "def")
 	require.NoError(t, err)
 
-	resp, err := lkv.Get(context.TODO(), "abc")
+	resp, err := lkv.Get(t.Context(), "abc")
 	require.NoError(t, err)
 	require.Equal(t, "def", string(resp.Kvs[0].Value))
 }
@@ -1742,7 +1742,7 @@ func TestLeasingSessionExpireCancel(t *testing.T) {
 			require.NoError(t, err)
 			defer closeLKV()
 
-			_, err = lkv.Get(context.TODO(), "abc")
+			_, err = lkv.Get(t.Context(), "abc")
 			require.NoError(t, err)
 
 			// down endpoint lkv uses for keepalives
@@ -1751,7 +1751,7 @@ func TestLeasingSessionExpireCancel(t *testing.T) {
 			require.NoError(t, err)
 			waitForExpireAck(t, lkv)
 
-			ctx, cancel := context.WithCancel(context.TODO())
+			ctx, cancel := context.WithCancel(t.Context())
 			errc := make(chan error, 1)
 			go func() { errc <- tests[i](ctx, lkv) }()
 			// some delay to get past for ctx.Err() != nil {} loops
@@ -1788,7 +1788,7 @@ func waitForLeasingExpire(kv clientv3.KV, lkey string) error {
 func waitForExpireAck(t *testing.T, kv clientv3.KV) {
 	// wait for leasing client to acknowledge lost lease
 	for i := 0; i < 10; i++ {
-		ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 		_, err := kv.Get(ctx, "abc")
 		cancel()
 		if errors.Is(err, ctx.Err()) {
diff --git a/tests/integration/clientv3/maintenance_test.go b/tests/integration/clientv3/maintenance_test.go
index dc46bf182..62a204816 100644
--- a/tests/integration/clientv3/maintenance_test.go
+++ b/tests/integration/clientv3/maintenance_test.go
@@ -52,7 +52,7 @@ func TestMaintenanceHashKV(t *testing.T) {
 	defer clus.Terminate(t)
 
 	for i := 0; i < 3; i++ {
-		_, err := clus.RandClient().Put(context.Background(), "foo", "bar")
+		_, err := clus.RandClient().Put(t.Context(), "foo", "bar")
 		require.NoError(t, err)
 	}
 
@@ -60,9 +60,9 @@ func TestMaintenanceHashKV(t *testing.T) {
 	for i := 0; i < 3; i++ {
 		cli := clus.Client(i)
 		// ensure writes are replicated
-		_, err := cli.Get(context.TODO(), "foo")
+		_, err := cli.Get(t.Context(), "foo")
 		require.NoError(t, err)
-		hresp, err := cli.HashKV(context.Background(), clus.Members[i].GRPCURL, 0)
+		hresp, err := cli.HashKV(t.Context(), clus.Members[i].GRPCURL, 0)
 		require.NoError(t, err)
 		if hv == 0 {
 			hv = hresp.Hash
@@ -85,7 +85,7 @@ func TestCompactionHash(t *testing.T) {
 	cc, err := clus.ClusterClient(t)
 	require.NoError(t, err)
 
-	testutil.TestCompactionHash(context.Background(), t, hashTestCase{cc, clus.Members[0].GRPCURL}, 1000)
+	testutil.TestCompactionHash(t.Context(), t, hashTestCase{cc, clus.Members[0].GRPCURL}, 1000)
 }
 
 type hashTestCase struct {
@@ -131,13 +131,13 @@ func TestMaintenanceMoveLeader(t *testing.T) {
 	target := uint64(clus.Members[targetIdx].ID())
 
 	cli := clus.Client(targetIdx)
-	_, err := cli.MoveLeader(context.Background(), target)
+	_, err := cli.MoveLeader(t.Context(), target)
 	if !errors.Is(err, rpctypes.ErrNotLeader) {
 		t.Fatalf("error expected %v, got %v", rpctypes.ErrNotLeader, err)
 	}
 
 	cli = clus.Client(oldLeadIdx)
-	_, err = cli.MoveLeader(context.Background(), target)
+	_, err = cli.MoveLeader(t.Context(), target)
 	require.NoError(t, err)
 
 	leadIdx := clus.WaitLeader(t)
@@ -156,7 +156,7 @@ func TestMaintenanceSnapshotCancel(t *testing.T) {
 	defer clus.Terminate(t)
 
 	// reading snapshot with canceled context should error out
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 
 	// Since http2 spec defines the receive windows's size and max size of
 	// frame in the stream, the underlayer - gRPC client can pre-read data
@@ -213,7 +213,7 @@ func testMaintenanceSnapshotTimeout(t *testing.T, snapshot func(context.Context,
 	defer clus.Terminate(t)
 
 	// reading snapshot with deadline exceeded should error out
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 	defer cancel()
 
 	// Since http2 spec defines the receive windows's size and max size of
@@ -293,7 +293,7 @@ func testMaintenanceSnapshotErrorInflight(t *testing.T, snapshot func(context.Co
 	clus.Members[0].Restart(t)
 
 	// reading snapshot with canceled context should error out
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	rc1, err := snapshot(ctx, clus.RandClient())
 	require.NoError(t, err)
 	defer rc1.Close()
@@ -311,7 +311,7 @@ func testMaintenanceSnapshotErrorInflight(t *testing.T, snapshot func(context.Co
 	<-donec
 
 	// reading snapshot with deadline exceeded should error out
-	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
+	ctx, cancel = context.WithTimeout(t.Context(), time.Second)
 	defer cancel()
 	rc2, err := snapshot(ctx, clus.RandClient())
 	require.NoError(t, err)
@@ -335,11 +335,11 @@ func TestMaintenanceSnapshotWithVersionVersion(t *testing.T) {
 
 	// Put some keys to ensure that wal snapshot is triggered
 	for i := 0; i < 10; i++ {
-		clus.RandClient().Put(context.Background(), fmt.Sprintf("%d", i), "1")
+		clus.RandClient().Put(t.Context(), fmt.Sprintf("%d", i), "1")
 	}
 
 	// reading snapshot with canceled context should error out
-	resp, err := clus.RandClient().SnapshotWithVersion(context.Background())
+	resp, err := clus.RandClient().SnapshotWithVersion(t.Context())
 	require.NoError(t, err)
 	defer resp.Snapshot.Close()
 	if resp.Version != "3.7.0" {
@@ -356,7 +356,7 @@ func TestMaintenanceSnapshotContentDigest(t *testing.T) {
 	populateDataIntoCluster(t, clus, 3, 1024*1024)
 
 	// reading snapshot with canceled context should error out
-	resp, err := clus.RandClient().SnapshotWithVersion(context.Background())
+	resp, err := clus.RandClient().SnapshotWithVersion(t.Context())
 	require.NoError(t, err)
 	defer resp.Snapshot.Close()
 
@@ -416,7 +416,7 @@ func TestMaintenanceStatus(t *testing.T) {
 
 	prevID, leaderFound := uint64(0), false
 	for i := 0; i < 3; i++ {
-		resp, err := cli.Status(context.TODO(), eps[i])
+		resp, err := cli.Status(t.Context(), eps[i])
 		require.NoError(t, err)
 		t.Logf("Response from %v: %v", i, resp)
 		if resp.DbSizeQuota != storage.DefaultQuotaBytes {
diff --git a/tests/integration/clientv3/metrics_test.go b/tests/integration/clientv3/metrics_test.go
index 9d1d2e96f..31d004006 100644
--- a/tests/integration/clientv3/metrics_test.go
+++ b/tests/integration/clientv3/metrics_test.go
@@ -17,7 +17,6 @@ package clientv3test
 import (
 	"bufio"
 	"bytes"
-	"context"
 	"errors"
 	"io"
 	"net"
@@ -90,13 +89,13 @@ func TestV3ClientMetrics(t *testing.T) {
 	require.NoError(t, cerr)
 	defer cli.Close()
 
-	wc := cli.Watch(context.Background(), "foo")
+	wc := cli.Watch(t.Context(), "foo")
 
 	wBefore := sumCountersForMetricAndLabels(t, url, "grpc_client_msg_received_total", "Watch", "bidi_stream")
 
 	pBefore := sumCountersForMetricAndLabels(t, url, "grpc_client_started_total", "Put", "unary")
 
-	_, err = cli.Put(context.Background(), "foo", "bar")
+	_, err = cli.Put(t.Context(), "foo", "bar")
 	if err != nil {
 		t.Errorf("Error putting value in key store")
 	}
diff --git a/tests/integration/clientv3/mirror_auth_test.go b/tests/integration/clientv3/mirror_auth_test.go
index 1738fea5a..fb5d3a123 100644
--- a/tests/integration/clientv3/mirror_auth_test.go
+++ b/tests/integration/clientv3/mirror_auth_test.go
@@ -17,7 +17,6 @@
 package clientv3test
 
 import (
-	"context"
 	"reflect"
 	"testing"
 	"time"
@@ -39,13 +38,13 @@ func TestMirrorSync_Authenticated(t *testing.T) {
 	initialClient := clus.Client(0)
 
 	// Create a user to run the mirror process that only has access to /syncpath
-	initialClient.RoleAdd(context.Background(), "syncer")
-	initialClient.RoleGrantPermission(context.Background(), "syncer", "/syncpath", clientv3.GetPrefixRangeEnd("/syncpath"), clientv3.PermissionType(clientv3.PermReadWrite))
-	initialClient.UserAdd(context.Background(), "syncer", "syncfoo")
-	initialClient.UserGrantRole(context.Background(), "syncer", "syncer")
+	initialClient.RoleAdd(t.Context(), "syncer")
+	initialClient.RoleGrantPermission(t.Context(), "syncer", "/syncpath", clientv3.GetPrefixRangeEnd("/syncpath"), clientv3.PermissionType(clientv3.PermReadWrite))
+	initialClient.UserAdd(t.Context(), "syncer", "syncfoo")
+	initialClient.UserGrantRole(t.Context(), "syncer", "syncer")
 
 	// Seed /syncpath with some initial data
-	_, err := initialClient.KV.Put(context.TODO(), "/syncpath/foo", "bar")
+	_, err := initialClient.KV.Put(t.Context(), "/syncpath/foo", "bar")
 	require.NoError(t, err)
 
 	// Require authentication
@@ -65,7 +64,7 @@ func TestMirrorSync_Authenticated(t *testing.T) {
 
 	// Now run the sync process, create changes, and get the initial sync state
 	syncer := mirror.NewSyncer(syncClient, "/syncpath", 0)
-	gch, ech := syncer.SyncBase(context.TODO())
+	gch, ech := syncer.SyncBase(t.Context())
 	wkvs := []*mvccpb.KeyValue{{Key: []byte("/syncpath/foo"), Value: []byte("bar"), CreateRevision: 2, ModRevision: 2, Version: 1}}
 
 	for g := range gch {
@@ -79,10 +78,10 @@ func TestMirrorSync_Authenticated(t *testing.T) {
 	}
 
 	// Start a continuous sync
-	wch := syncer.SyncUpdates(context.TODO())
+	wch := syncer.SyncUpdates(t.Context())
 
 	// Update state
-	_, err = syncClient.KV.Put(context.TODO(), "/syncpath/foo", "baz")
+	_, err = syncClient.KV.Put(t.Context(), "/syncpath/foo", "baz")
 	require.NoError(t, err)
 
 	// Wait for the updated state to sync
diff --git a/tests/integration/clientv3/mirror_test.go b/tests/integration/clientv3/mirror_test.go
index e3bc9a730..4baacecb7 100644
--- a/tests/integration/clientv3/mirror_test.go
+++ b/tests/integration/clientv3/mirror_test.go
@@ -15,7 +15,6 @@
 package clientv3test
 
 import (
-	"context"
 	"fmt"
 	"reflect"
 	"sync"
@@ -36,11 +35,11 @@ func TestMirrorSync(t *testing.T) {
 	defer clus.Terminate(t)
 
 	c := clus.Client(0)
-	_, err := c.KV.Put(context.TODO(), "foo", "bar")
+	_, err := c.KV.Put(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 
 	syncer := mirror.NewSyncer(c, "", 0)
-	gch, ech := syncer.SyncBase(context.TODO())
+	gch, ech := syncer.SyncBase(t.Context())
 	wkvs := []*mvccpb.KeyValue{{Key: []byte("foo"), Value: []byte("bar"), CreateRevision: 2, ModRevision: 2, Version: 1}}
 
 	for g := range gch {
@@ -53,9 +52,9 @@ func TestMirrorSync(t *testing.T) {
 		t.Fatalf("unexpected error %v", e)
 	}
 
-	wch := syncer.SyncUpdates(context.TODO())
+	wch := syncer.SyncUpdates(t.Context())
 
-	_, err = c.KV.Put(context.TODO(), "foo", "bar")
+	_, err = c.KV.Put(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 
 	select {
@@ -76,7 +75,7 @@ func TestMirrorSyncBase(t *testing.T) {
 	defer cluster.Terminate(t)
 
 	cli := cluster.Client(0)
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	keyCh := make(chan string)
 	var wg sync.WaitGroup
diff --git a/tests/integration/clientv3/namespace_test.go b/tests/integration/clientv3/namespace_test.go
index 4ad7f60a2..b56d7adfc 100644
--- a/tests/integration/clientv3/namespace_test.go
+++ b/tests/integration/clientv3/namespace_test.go
@@ -15,7 +15,6 @@
 package clientv3test
 
 import (
-	"context"
 	"reflect"
 	"testing"
 
@@ -36,15 +35,15 @@ func TestNamespacePutGet(t *testing.T) {
 	c := clus.Client(0)
 	nsKV := namespace.NewKV(c.KV, "foo/")
 
-	_, err := nsKV.Put(context.TODO(), "abc", "bar")
+	_, err := nsKV.Put(t.Context(), "abc", "bar")
 	require.NoError(t, err)
-	resp, err := nsKV.Get(context.TODO(), "abc")
+	resp, err := nsKV.Get(t.Context(), "abc")
 	require.NoError(t, err)
 	if string(resp.Kvs[0].Key) != "abc" {
 		t.Errorf("expected key=%q, got key=%q", "abc", resp.Kvs[0].Key)
 	}
 
-	resp, err = c.Get(context.TODO(), "foo/abc")
+	resp, err = c.Get(t.Context(), "foo/abc")
 	require.NoError(t, err)
 	if string(resp.Kvs[0].Value) != "bar" {
 		t.Errorf("expected value=%q, got value=%q", "bar", resp.Kvs[0].Value)
@@ -61,16 +60,16 @@ func TestNamespaceWatch(t *testing.T) {
 	nsKV := namespace.NewKV(c.KV, "foo/")
 	nsWatcher := namespace.NewWatcher(c.Watcher, "foo/")
 
-	_, err := nsKV.Put(context.TODO(), "abc", "bar")
+	_, err := nsKV.Put(t.Context(), "abc", "bar")
 	require.NoError(t, err)
 
-	nsWch := nsWatcher.Watch(context.TODO(), "abc", clientv3.WithRev(1))
+	nsWch := nsWatcher.Watch(t.Context(), "abc", clientv3.WithRev(1))
 	wkv := &mvccpb.KeyValue{Key: []byte("abc"), Value: []byte("bar"), CreateRevision: 2, ModRevision: 2, Version: 1}
 	if wr := <-nsWch; len(wr.Events) != 1 || !reflect.DeepEqual(wr.Events[0].Kv, wkv) {
 		t.Errorf("expected namespaced event %+v, got %+v", wkv, wr.Events[0].Kv)
 	}
 
-	wch := c.Watch(context.TODO(), "foo/abc", clientv3.WithRev(1))
+	wch := c.Watch(t.Context(), "foo/abc", clientv3.WithRev(1))
 	wkv = &mvccpb.KeyValue{Key: []byte("foo/abc"), Value: []byte("bar"), CreateRevision: 2, ModRevision: 2, Version: 1}
 	if wr := <-wch; len(wr.Events) != 1 || !reflect.DeepEqual(wr.Events[0].Kv, wkv) {
 		t.Errorf("expected unnamespaced event %+v, got %+v", wkv, wr)
diff --git a/tests/integration/clientv3/naming/endpoints_test.go b/tests/integration/clientv3/naming/endpoints_test.go
index 649c05813..229ae6f7d 100644
--- a/tests/integration/clientv3/naming/endpoints_test.go
+++ b/tests/integration/clientv3/naming/endpoints_test.go
@@ -36,7 +36,7 @@ func TestEndpointManager(t *testing.T) {
 	if err != nil {
 		t.Fatal("failed to create EndpointManager", err)
 	}
-	ctx, watchCancel := context.WithCancel(context.Background())
+	ctx, watchCancel := context.WithCancel(t.Context())
 	defer watchCancel()
 	w, err := em.NewWatchChannel(ctx)
 	if err != nil {
@@ -44,7 +44,7 @@ func TestEndpointManager(t *testing.T) {
 	}
 
 	e1 := endpoints.Endpoint{Addr: "127.0.0.1", Metadata: "metadata"}
-	err = em.AddEndpoint(context.TODO(), "foo/a1", e1)
+	err = em.AddEndpoint(t.Context(), "foo/a1", e1)
 	if err != nil {
 		t.Fatal("failed to add foo", err)
 	}
@@ -63,7 +63,7 @@ func TestEndpointManager(t *testing.T) {
 
 	require.Truef(t, reflect.DeepEqual(us[0], wu), "up = %#v, want %#v", us[0], wu)
 
-	err = em.DeleteEndpoint(context.TODO(), "foo/a1")
+	err = em.DeleteEndpoint(t.Context(), "foo/a1")
 	require.NoErrorf(t, err, "failed to udpate %v", err)
 
 	us = <-w
@@ -94,13 +94,13 @@ func TestEndpointManagerAtomicity(t *testing.T) {
 		t.Fatal("failed to create EndpointManager", err)
 	}
 
-	err = em.Update(context.TODO(), []*endpoints.UpdateWithOpts{
+	err = em.Update(t.Context(), []*endpoints.UpdateWithOpts{
 		endpoints.NewAddUpdateOpts("foo/host", endpoints.Endpoint{Addr: "127.0.0.1:2000"}),
 		endpoints.NewAddUpdateOpts("foo/host2", endpoints.Endpoint{Addr: "127.0.0.1:2001"}),
 	})
 	require.NoError(t, err)
 
-	ctx, watchCancel := context.WithCancel(context.Background())
+	ctx, watchCancel := context.WithCancel(t.Context())
 	defer watchCancel()
 	w, err := em.NewWatchChannel(ctx)
 	require.NoError(t, err)
@@ -108,7 +108,7 @@ func TestEndpointManagerAtomicity(t *testing.T) {
 	updates := <-w
 	require.Lenf(t, updates, 2, "expected two updates, got %+v", updates)
 
-	_, err = c.Txn(context.TODO()).Then(etcd.OpDelete("foo/host"), etcd.OpDelete("foo/host2")).Commit()
+	_, err = c.Txn(t.Context()).Then(etcd.OpDelete("foo/host"), etcd.OpDelete("foo/host2")).Commit()
 	require.NoError(t, err)
 
 	updates = <-w
@@ -131,19 +131,19 @@ func TestEndpointManagerCRUD(t *testing.T) {
 	// Add
 	k1 := "foo/a1"
 	e1 := endpoints.Endpoint{Addr: "127.0.0.1", Metadata: "metadata1"}
-	err = em.AddEndpoint(context.TODO(), k1, e1)
+	err = em.AddEndpoint(t.Context(), k1, e1)
 	if err != nil {
 		t.Fatal("failed to add", k1, err)
 	}
 
 	k2 := "foo/a2"
 	e2 := endpoints.Endpoint{Addr: "127.0.0.2", Metadata: "metadata2"}
-	err = em.AddEndpoint(context.TODO(), k2, e2)
+	err = em.AddEndpoint(t.Context(), k2, e2)
 	if err != nil {
 		t.Fatal("failed to add", k2, err)
 	}
 
-	eps, err := em.List(context.TODO())
+	eps, err := em.List(t.Context())
 	if err != nil {
 		t.Fatal("failed to list foo")
 	}
@@ -152,12 +152,12 @@ func TestEndpointManagerCRUD(t *testing.T) {
 	require.Truef(t, reflect.DeepEqual(eps[k2], e2), "unexpected endpoints: %s", k2)
 
 	// Delete
-	err = em.DeleteEndpoint(context.TODO(), k1)
+	err = em.DeleteEndpoint(t.Context(), k1)
 	if err != nil {
 		t.Fatal("failed to delete", k2, err)
 	}
 
-	eps, err = em.List(context.TODO())
+	eps, err = em.List(t.Context())
 	if err != nil {
 		t.Fatal("failed to list foo")
 	}
@@ -171,12 +171,12 @@ func TestEndpointManagerCRUD(t *testing.T) {
 		{Update: endpoints.Update{Op: endpoints.Add, Key: k3, Endpoint: e3}},
 		{Update: endpoints.Update{Op: endpoints.Delete, Key: k2}},
 	}
-	err = em.Update(context.TODO(), updates)
+	err = em.Update(t.Context(), updates)
 	if err != nil {
 		t.Fatal("failed to update", err)
 	}
 
-	eps, err = em.List(context.TODO())
+	eps, err = em.List(t.Context())
 	if err != nil {
 		t.Fatal("failed to list foo")
 	}
diff --git a/tests/integration/clientv3/naming/resolver_test.go b/tests/integration/clientv3/naming/resolver_test.go
index 40dc17721..4a5e1a6ae 100644
--- a/tests/integration/clientv3/naming/resolver_test.go
+++ b/tests/integration/clientv3/naming/resolver_test.go
@@ -16,7 +16,6 @@ package naming_test
 
 import (
 	"bytes"
-	"context"
 	"fmt"
 	"strconv"
 	"testing"
@@ -60,12 +59,12 @@ func testEtcdGRPCResolver(t *testing.T, lbPolicy string) {
 	e1 := endpoints.Endpoint{Addr: s1.Addr()}
 	e2 := endpoints.Endpoint{Addr: s2.Addr()}
 
-	err = em.AddEndpoint(context.TODO(), "foo/e1", e1)
+	err = em.AddEndpoint(t.Context(), "foo/e1", e1)
 	if err != nil {
 		t.Fatal("failed to add foo", err)
 	}
 
-	err = em.AddEndpoint(context.TODO(), "foo/e2", e2)
+	err = em.AddEndpoint(t.Context(), "foo/e2", e2)
 	if err != nil {
 		t.Fatal("failed to add foo", err)
 	}
@@ -85,7 +84,7 @@ func testEtcdGRPCResolver(t *testing.T, lbPolicy string) {
 
 	// Send an initial request that should go to e1
 	c := testpb.NewTestServiceClient(conn)
-	resp, err := c.UnaryCall(context.TODO(), &testpb.SimpleRequest{}, grpc.WaitForReady(true))
+	resp, err := c.UnaryCall(t.Context(), &testpb.SimpleRequest{}, grpc.WaitForReady(true))
 	if err != nil {
 		t.Fatal("failed to invoke rpc to foo (e1)", err)
 	}
@@ -97,7 +96,7 @@ func testEtcdGRPCResolver(t *testing.T, lbPolicy string) {
 	lastResponse := []byte{'1'}
 	totalRequests := 3500
 	for i := 1; i < totalRequests; i++ {
-		resp, err := c.UnaryCall(context.TODO(), &testpb.SimpleRequest{}, grpc.WaitForReady(true))
+		resp, err := c.UnaryCall(t.Context(), &testpb.SimpleRequest{}, grpc.WaitForReady(true))
 		if err != nil {
 			t.Fatal("failed to invoke rpc to foo", err)
 		}
@@ -168,12 +167,12 @@ func TestEtcdEndpointManager(t *testing.T) {
 	e1 := endpoints.Endpoint{Addr: s1.Addr()}
 	e2 := endpoints.Endpoint{Addr: s2.Addr()}
 
-	em.AddEndpoint(context.Background(), "foo/e1", e1)
-	emOther.AddEndpoint(context.Background(), "foo_other/e2", e2)
+	em.AddEndpoint(t.Context(), "foo/e1", e1)
+	emOther.AddEndpoint(t.Context(), "foo_other/e2", e2)
 
-	epts, err := em.List(context.Background())
+	epts, err := em.List(t.Context())
 	require.NoError(t, err)
-	eptsOther, err := emOther.List(context.Background())
+	eptsOther, err := emOther.List(t.Context())
 	require.NoError(t, err)
 	assert.Len(t, epts, 1)
 	assert.Len(t, eptsOther, 1)
diff --git a/tests/integration/clientv3/ordering_kv_test.go b/tests/integration/clientv3/ordering_kv_test.go
index 2ba6aaa88..bc6792f22 100644
--- a/tests/integration/clientv3/ordering_kv_test.go
+++ b/tests/integration/clientv3/ordering_kv_test.go
@@ -15,7 +15,6 @@
 package clientv3test
 
 import (
-	"context"
 	"errors"
 	"testing"
 	"time"
@@ -45,7 +44,7 @@ func TestDetectKvOrderViolation(t *testing.T) {
 	cli, err := integration2.NewClient(t, cfg)
 	require.NoError(t, err)
 	defer func() { assert.NoError(t, cli.Close()) }()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err = clus.Client(0).Put(ctx, "foo", "bar")
 	require.NoError(t, err)
@@ -104,7 +103,7 @@ func TestDetectTxnOrderViolation(t *testing.T) {
 	cli, err := integration2.NewClient(t, cfg)
 	require.NoError(t, err)
 	defer func() { assert.NoError(t, cli.Close()) }()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err = clus.Client(0).Put(ctx, "foo", "bar")
 	require.NoError(t, err)
diff --git a/tests/integration/clientv3/ordering_util_test.go b/tests/integration/clientv3/ordering_util_test.go
index f7513190d..751309b27 100644
--- a/tests/integration/clientv3/ordering_util_test.go
+++ b/tests/integration/clientv3/ordering_util_test.go
@@ -15,7 +15,6 @@
 package clientv3test
 
 import (
-	"context"
 	"errors"
 	"testing"
 	"time"
@@ -44,7 +43,7 @@ func TestEndpointSwitchResolvesViolation(t *testing.T) {
 	require.NoError(t, err)
 	defer cli.Close()
 
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err = clus.Client(0).Put(ctx, "foo", "bar")
 	require.NoError(t, err)
@@ -104,7 +103,7 @@ func TestUnresolvableOrderViolation(t *testing.T) {
 	require.NoError(t, err)
 	defer cli.Close()
 	eps := cli.Endpoints()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	cli.SetEndpoints(clus.Members[0].GRPCURL)
 	time.Sleep(1 * time.Second)
diff --git a/tests/integration/clientv3/snapshot/v3_snapshot_test.go b/tests/integration/clientv3/snapshot/v3_snapshot_test.go
index a30671883..24d566b5d 100644
--- a/tests/integration/clientv3/snapshot/v3_snapshot_test.go
+++ b/tests/integration/clientv3/snapshot/v3_snapshot_test.go
@@ -103,14 +103,14 @@ func createSnapshotFile(t *testing.T, cfg *embed.Config, kvs []kv) (version stri
 	require.NoError(t, err)
 	defer cli.Close()
 	for i := range kvs {
-		ctx, cancel := context.WithTimeout(context.Background(), testutil.RequestTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), testutil.RequestTimeout)
 		_, err = cli.Put(ctx, kvs[i].k, kvs[i].v)
 		cancel()
 		require.NoError(t, err)
 	}
 
 	dbPath = filepath.Join(t.TempDir(), fmt.Sprintf("snapshot%d.db", time.Now().Nanosecond()))
-	version, err = snapshot.SaveWithVersion(context.Background(), zaptest.NewLogger(t), ccfg, dbPath)
+	version, err = snapshot.SaveWithVersion(t.Context(), zaptest.NewLogger(t), ccfg, dbPath)
 	require.NoError(t, err)
 	return version, dbPath
 }
diff --git a/tests/integration/clientv3/txn_test.go b/tests/integration/clientv3/txn_test.go
index 00266e49c..9f62ff5a3 100644
--- a/tests/integration/clientv3/txn_test.go
+++ b/tests/integration/clientv3/txn_test.go
@@ -36,7 +36,7 @@ func TestTxnError(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.RandClient()
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err := kv.Txn(ctx).Then(clientv3.OpPut("foo", "bar1"), clientv3.OpPut("foo", "bar2")).Commit()
 	if !errors.Is(err, rpctypes.ErrDuplicateKey) {
@@ -65,7 +65,7 @@ func TestTxnWriteFail(t *testing.T) {
 
 	txnc, getc := make(chan struct{}), make(chan struct{})
 	go func() {
-		ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 		defer cancel()
 		resp, err := kv.Txn(ctx).Then(clientv3.OpPut("foo", "bar")).Commit()
 		if err == nil {
@@ -82,7 +82,7 @@ func TestTxnWriteFail(t *testing.T) {
 		case <-txnc:
 		}
 		// and ensure the put didn't take
-		gresp, gerr := clus.Client(1).Get(context.TODO(), "foo")
+		gresp, gerr := clus.Client(1).Get(t.Context(), "foo")
 		if gerr != nil {
 			t.Error(gerr)
 		}
@@ -123,7 +123,7 @@ func TestTxnReadRetry(t *testing.T) {
 
 		donec := make(chan struct{}, 1)
 		go func() {
-			_, err := kv.Txn(context.TODO()).Then(thenOps[i]...).Commit()
+			_, err := kv.Txn(t.Context()).Then(thenOps[i]...).Commit()
 			if err != nil {
 				t.Errorf("expected response, got error %v", err)
 			}
@@ -149,7 +149,7 @@ func TestTxnSuccess(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.Client(0)
-	ctx := context.TODO()
+	ctx := t.Context()
 
 	_, err := kv.Txn(ctx).Then(clientv3.OpPut("foo", "bar")).Commit()
 	require.NoError(t, err)
@@ -168,11 +168,11 @@ func TestTxnCompareRange(t *testing.T) {
 	defer clus.Terminate(t)
 
 	kv := clus.Client(0)
-	fooResp, err := kv.Put(context.TODO(), "foo/", "bar")
+	fooResp, err := kv.Put(t.Context(), "foo/", "bar")
 	require.NoError(t, err)
-	_, err = kv.Put(context.TODO(), "foo/a", "baz")
+	_, err = kv.Put(t.Context(), "foo/a", "baz")
 	require.NoError(t, err)
-	tresp, terr := kv.Txn(context.TODO()).If(
+	tresp, terr := kv.Txn(t.Context()).If(
 		clientv3.Compare(
 			clientv3.CreateRevision("foo/"), "=", fooResp.Header.Revision).
 			WithPrefix(),
@@ -191,7 +191,7 @@ func TestTxnNested(t *testing.T) {
 
 	kv := clus.Client(0)
 
-	tresp, err := kv.Txn(context.TODO()).
+	tresp, err := kv.Txn(t.Context()).
 		If(clientv3.Compare(clientv3.Version("foo"), "=", 0)).
 		Then(
 			clientv3.OpPut("foo", "bar"),
@@ -203,12 +203,12 @@ func TestTxnNested(t *testing.T) {
 	}
 
 	// check txn writes were applied
-	resp, err := kv.Get(context.TODO(), "foo")
+	resp, err := kv.Get(t.Context(), "foo")
 	require.NoError(t, err)
 	if len(resp.Kvs) != 1 || string(resp.Kvs[0].Value) != "bar" {
 		t.Errorf("unexpected Get response %+v", resp)
 	}
-	resp, err = kv.Get(context.TODO(), "abc")
+	resp, err = kv.Get(t.Context(), "abc")
 	require.NoError(t, err)
 	if len(resp.Kvs) != 1 || string(resp.Kvs[0].Value) != "123" {
 		t.Errorf("unexpected Get response %+v", resp)
diff --git a/tests/integration/clientv3/user_test.go b/tests/integration/clientv3/user_test.go
index 8f3ef8380..18593dff0 100644
--- a/tests/integration/clientv3/user_test.go
+++ b/tests/integration/clientv3/user_test.go
@@ -36,16 +36,16 @@ func TestUserError(t *testing.T) {
 
 	authapi := clus.RandClient()
 
-	_, err := authapi.UserAdd(context.TODO(), "foo", "bar")
+	_, err := authapi.UserAdd(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 
-	_, err = authapi.UserAdd(context.TODO(), "foo", "bar")
+	_, err = authapi.UserAdd(t.Context(), "foo", "bar")
 	require.ErrorIsf(t, err, rpctypes.ErrUserAlreadyExist, "expected %v, got %v", rpctypes.ErrUserAlreadyExist, err)
 
-	_, err = authapi.UserDelete(context.TODO(), "not-exist-user")
+	_, err = authapi.UserDelete(t.Context(), "not-exist-user")
 	require.ErrorIsf(t, err, rpctypes.ErrUserNotFound, "expected %v, got %v", rpctypes.ErrUserNotFound, err)
 
-	_, err = authapi.UserGrantRole(context.TODO(), "foo", "test-role-does-not-exist")
+	_, err = authapi.UserGrantRole(t.Context(), "foo", "test-role-does-not-exist")
 	require.ErrorIsf(t, err, rpctypes.ErrRoleNotFound, "expected %v, got %v", rpctypes.ErrRoleNotFound, err)
 }
 
@@ -68,34 +68,34 @@ func TestAddUserAfterDelete(t *testing.T) {
 	defer authed.Close()
 
 	// add user
-	_, err = authed.UserAdd(context.TODO(), "foo", "bar")
+	_, err = authed.UserAdd(t.Context(), "foo", "bar")
 	require.NoError(t, err)
-	_, err = authapi.Authenticate(context.TODO(), "foo", "bar")
+	_, err = authapi.Authenticate(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 	// delete user
-	_, err = authed.UserDelete(context.TODO(), "foo")
+	_, err = authed.UserDelete(t.Context(), "foo")
 	require.NoError(t, err)
-	if _, err = authed.Authenticate(context.TODO(), "foo", "bar"); err == nil {
+	if _, err = authed.Authenticate(t.Context(), "foo", "bar"); err == nil {
 		t.Errorf("expect Authenticate error for old password")
 	}
 	// add user back
-	_, err = authed.UserAdd(context.TODO(), "foo", "bar")
+	_, err = authed.UserAdd(t.Context(), "foo", "bar")
 	require.NoError(t, err)
-	_, err = authed.Authenticate(context.TODO(), "foo", "bar")
+	_, err = authed.Authenticate(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 	// change password
-	_, err = authed.UserChangePassword(context.TODO(), "foo", "bar2")
+	_, err = authed.UserChangePassword(t.Context(), "foo", "bar2")
 	require.NoError(t, err)
-	_, err = authed.UserChangePassword(context.TODO(), "foo", "bar1")
+	_, err = authed.UserChangePassword(t.Context(), "foo", "bar1")
 	require.NoError(t, err)
 
-	if _, err = authed.Authenticate(context.TODO(), "foo", "bar"); err == nil {
+	if _, err = authed.Authenticate(t.Context(), "foo", "bar"); err == nil {
 		t.Errorf("expect Authenticate error for old password")
 	}
-	if _, err = authed.Authenticate(context.TODO(), "foo", "bar2"); err == nil {
+	if _, err = authed.Authenticate(t.Context(), "foo", "bar2"); err == nil {
 		t.Errorf("expect Authenticate error for old password")
 	}
-	_, err = authed.Authenticate(context.TODO(), "foo", "bar1")
+	_, err = authed.Authenticate(t.Context(), "foo", "bar1")
 	require.NoError(t, err)
 }
 
@@ -109,7 +109,7 @@ func TestUserErrorAuth(t *testing.T) {
 	authSetupRoot(t, authapi.Auth)
 
 	// unauthenticated client
-	_, err := authapi.UserAdd(context.TODO(), "foo", "bar")
+	_, err := authapi.UserAdd(t.Context(), "foo", "bar")
 	require.ErrorIsf(t, err, rpctypes.ErrUserEmpty, "expected %v, got %v", rpctypes.ErrUserEmpty, err)
 
 	// wrong id or password
@@ -130,18 +130,18 @@ func TestUserErrorAuth(t *testing.T) {
 	require.NoError(t, err)
 	defer authed.Close()
 
-	_, err = authed.UserList(context.TODO())
+	_, err = authed.UserList(t.Context())
 	require.NoError(t, err)
 }
 
 func authSetupRoot(t *testing.T, auth clientv3.Auth) {
-	_, err := auth.UserAdd(context.TODO(), "root", "123")
+	_, err := auth.UserAdd(t.Context(), "root", "123")
 	require.NoError(t, err)
-	_, err = auth.RoleAdd(context.TODO(), "root")
+	_, err = auth.RoleAdd(t.Context(), "root")
 	require.NoError(t, err)
-	_, err = auth.UserGrantRole(context.TODO(), "root", "root")
+	_, err = auth.UserGrantRole(t.Context(), "root", "root")
 	require.NoError(t, err)
-	_, err = auth.AuthEnable(context.TODO())
+	_, err = auth.AuthEnable(t.Context())
 	require.NoError(t, err)
 }
 
@@ -159,7 +159,7 @@ func TestGetTokenWithoutAuth(t *testing.T) {
 	var client *clientv3.Client
 
 	// make sure "auth" was disabled
-	_, err = authapi.AuthDisable(context.TODO())
+	_, err = authapi.AuthDisable(t.Context())
 	require.NoError(t, err)
 
 	// "Username" and "Password" must be used
diff --git a/tests/integration/clientv3/util.go b/tests/integration/clientv3/util.go
index 0e8411570..b7887339e 100644
--- a/tests/integration/clientv3/util.go
+++ b/tests/integration/clientv3/util.go
@@ -33,7 +33,7 @@ import (
 // Fatal on time-out.
 func MustWaitPinReady(t *testing.T, cli *clientv3.Client) {
 	// TODO: decrease timeout after balancer rewrite!!!
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	_, err := cli.Get(ctx, "foo")
 	cancel()
 	if err != nil {
@@ -109,7 +109,7 @@ func IsUnavailable(err error) bool {
 // populateDataIntoCluster populates the key-value pairs into cluster and the
 // key will be named by testing.T.Name()-index.
 func populateDataIntoCluster(t *testing.T, cluster *integration2.Cluster, numKeys int, valueSize int) {
-	ctx := context.Background()
+	ctx := t.Context()
 
 	for i := 0; i < numKeys; i++ {
 		_, err := cluster.RandClient().Put(ctx,
diff --git a/tests/integration/clientv3/watch_fragment_test.go b/tests/integration/clientv3/watch_fragment_test.go
index fd58fed81..05b20bd0a 100644
--- a/tests/integration/clientv3/watch_fragment_test.go
+++ b/tests/integration/clientv3/watch_fragment_test.go
@@ -17,7 +17,6 @@
 package clientv3test
 
 import (
-	"context"
 	"fmt"
 	"strings"
 	"testing"
@@ -81,7 +80,7 @@ func testWatchFragment(t *testing.T, fragment, exceedRecvLimit bool) {
 	errc := make(chan error)
 	for i := 0; i < 10; i++ {
 		go func(i int) {
-			_, err := cli.Put(context.TODO(),
+			_, err := cli.Put(t.Context(),
 				fmt.Sprint("foo", i),
 				strings.Repeat("a", 1024*1024),
 			)
@@ -97,7 +96,7 @@ func testWatchFragment(t *testing.T, fragment, exceedRecvLimit bool) {
 	if fragment {
 		opts = append(opts, clientv3.WithFragment())
 	}
-	wch := cli.Watch(context.TODO(), "foo", opts...)
+	wch := cli.Watch(t.Context(), "foo", opts...)
 
 	// expect 10 MiB watch response
 	select {
diff --git a/tests/integration/clientv3/watch_test.go b/tests/integration/clientv3/watch_test.go
index bdb913d32..a5bb23ad8 100644
--- a/tests/integration/clientv3/watch_test.go
+++ b/tests/integration/clientv3/watch_test.go
@@ -87,7 +87,7 @@ func testWatchMultiWatcher(t *testing.T, wctx *watchctx) {
 	for _, k := range keys {
 		// key watcher
 		go func(key string) {
-			ch := wctx.w.Watch(context.TODO(), key)
+			ch := wctx.w.Watch(t.Context(), key)
 			if ch == nil {
 				t.Errorf("expected watcher channel, got nil")
 			}
@@ -108,7 +108,7 @@ func testWatchMultiWatcher(t *testing.T, wctx *watchctx) {
 	}
 	// prefix watcher on "b" (bar and baz)
 	go func() {
-		prefixc := wctx.w.Watch(context.TODO(), "b", clientv3.WithPrefix())
+		prefixc := wctx.w.Watch(t.Context(), "b", clientv3.WithPrefix())
 		if prefixc == nil {
 			t.Errorf("expected watcher channel, got nil")
 		}
@@ -156,7 +156,7 @@ func testWatchMultiWatcher(t *testing.T, wctx *watchctx) {
 		<-readyc
 	}
 	// generate events
-	ctx := context.TODO()
+	ctx := t.Context()
 	for i := 0; i < numKeyUpdates; i++ {
 		for _, k := range keys {
 			v := fmt.Sprintf("%s-%d", k, i)
@@ -172,7 +172,7 @@ func TestWatchRange(t *testing.T) {
 }
 
 func testWatchRange(t *testing.T, wctx *watchctx) {
-	wctx.ch = wctx.w.Watch(context.TODO(), "a", clientv3.WithRange("c"))
+	wctx.ch = wctx.w.Watch(t.Context(), "a", clientv3.WithRange("c"))
 	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 	putAndWatch(t, wctx, "a", "a")
 	putAndWatch(t, wctx, "b", "b")
@@ -203,7 +203,7 @@ func testWatchReconnRequest(t *testing.T, wctx *watchctx) {
 		}
 	}()
 	// should reconnect when requesting watch
-	wctx.ch = wctx.w.Watch(context.TODO(), "a")
+	wctx.ch = wctx.w.Watch(t.Context(), "a")
 	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 
 	// wait for disconnections to stop
@@ -212,7 +212,7 @@ func testWatchReconnRequest(t *testing.T, wctx *watchctx) {
 
 	// spinning on dropping connections may trigger a leader election
 	// due to resource starvation; l-read to ensure the cluster is stable
-	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	_, err := wctx.kv.Get(ctx, "_")
 	require.NoError(t, err)
 	cancel()
@@ -228,7 +228,7 @@ func TestWatchReconnInit(t *testing.T) {
 }
 
 func testWatchReconnInit(t *testing.T, wctx *watchctx) {
-	wctx.ch = wctx.w.Watch(context.TODO(), "a")
+	wctx.ch = wctx.w.Watch(t.Context(), "a")
 	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 	wctx.clus.Members[wctx.wclientMember].Bridge().DropConnections()
 	// watcher should recover
@@ -242,7 +242,7 @@ func TestWatchReconnRunning(t *testing.T) {
 }
 
 func testWatchReconnRunning(t *testing.T, wctx *watchctx) {
-	wctx.ch = wctx.w.Watch(context.TODO(), "a")
+	wctx.ch = wctx.w.Watch(t.Context(), "a")
 	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 	putAndWatch(t, wctx, "a", "a")
 	// take down watcher connection
@@ -258,7 +258,7 @@ func TestWatchCancelImmediate(t *testing.T) {
 }
 
 func testWatchCancelImmediate(t *testing.T, wctx *watchctx) {
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	cancel()
 	wch := wctx.w.Watch(ctx, "a")
 	select {
@@ -275,7 +275,7 @@ func TestWatchCancelInit(t *testing.T) {
 }
 
 func testWatchCancelInit(t *testing.T, wctx *watchctx) {
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	wctx.ch = wctx.w.Watch(ctx, "a")
 	require.NotNilf(t, wctx.ch, "expected non-nil watcher channel")
 	cancel()
@@ -293,7 +293,7 @@ func TestWatchCancelRunning(t *testing.T) {
 }
 
 func testWatchCancelRunning(t *testing.T, wctx *watchctx) {
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	wctx.ch = wctx.w.Watch(ctx, "a")
 	require.NotNilf(t, wctx.ch, "expected non-nil watcher channel")
 	_, err := wctx.kv.Put(ctx, "a", "a")
@@ -318,7 +318,7 @@ func testWatchCancelRunning(t *testing.T, wctx *watchctx) {
 }
 
 func putAndWatch(t *testing.T, wctx *watchctx, key, val string) {
-	_, err := wctx.kv.Put(context.TODO(), key, val)
+	_, err := wctx.kv.Put(t.Context(), key, val)
 	require.NoError(t, err)
 	select {
 	case <-time.After(5 * time.Second):
@@ -339,16 +339,16 @@ func TestWatchResumeAfterDisconnect(t *testing.T) {
 	defer clus.Terminate(t)
 
 	cli := clus.Client(0)
-	_, err := cli.Put(context.TODO(), "b", "2")
+	_, err := cli.Put(t.Context(), "b", "2")
 	require.NoError(t, err)
-	_, err = cli.Put(context.TODO(), "a", "3")
+	_, err = cli.Put(t.Context(), "a", "3")
 	require.NoError(t, err)
 	// if resume is broken, it'll pick up this key first instead of a=3
-	_, err = cli.Put(context.TODO(), "a", "4")
+	_, err = cli.Put(t.Context(), "a", "4")
 	require.NoError(t, err)
 
 	// watch from revision 1
-	wch := clus.Client(0).Watch(context.Background(), "a", clientv3.WithRev(1), clientv3.WithCreatedNotify())
+	wch := clus.Client(0).Watch(t.Context(), "a", clientv3.WithRev(1), clientv3.WithCreatedNotify())
 	// response for the create watch request, no events are in this response
 	// the current revision of etcd should be 4
 	if resp, ok := <-wch; !ok || resp.Header.Revision != 4 {
@@ -396,7 +396,7 @@ func TestWatchResumeCompacted(t *testing.T) {
 
 	// create a waiting watcher at rev 1
 	w := clus.Client(0)
-	wch := w.Watch(context.Background(), "foo", clientv3.WithRev(1))
+	wch := w.Watch(t.Context(), "foo", clientv3.WithRev(1))
 	select {
 	case w := <-wch:
 		t.Errorf("unexpected message from wch %v", w)
@@ -410,10 +410,10 @@ func TestWatchResumeCompacted(t *testing.T) {
 	numPuts := 5
 	kv := clus.Client(1)
 	for i := 0; i < numPuts; i++ {
-		_, err := kv.Put(context.TODO(), "foo", "bar")
+		_, err := kv.Put(t.Context(), "foo", "bar")
 		require.NoError(t, err)
 	}
-	_, err := kv.Compact(context.TODO(), 3)
+	_, err := kv.Compact(t.Context(), 3)
 	require.NoError(t, err)
 
 	clus.Members[0].Restart(t)
@@ -470,15 +470,15 @@ func TestWatchCompactRevision(t *testing.T) {
 	// set some keys
 	kv := clus.RandClient()
 	for i := 0; i < 5; i++ {
-		_, err := kv.Put(context.TODO(), "foo", "bar")
+		_, err := kv.Put(t.Context(), "foo", "bar")
 		require.NoError(t, err)
 	}
 
 	w := clus.RandClient()
 
-	_, err := kv.Compact(context.TODO(), 4)
+	_, err := kv.Compact(t.Context(), 4)
 	require.NoError(t, err)
-	wch := w.Watch(context.Background(), "foo", clientv3.WithRev(2))
+	wch := w.Watch(t.Context(), "foo", clientv3.WithRev(2))
 
 	// get compacted error message
 	wresp, ok := <-wch
@@ -520,7 +520,7 @@ func testWatchWithProgressNotify(t *testing.T, watchOnPut bool) {
 	if watchOnPut {
 		opts = append(opts, clientv3.WithPrefix())
 	}
-	rch := wc.Watch(context.Background(), "foo", opts...)
+	rch := wc.Watch(t.Context(), "foo", opts...)
 
 	select {
 	case resp := <-rch: // wait for notification
@@ -532,7 +532,7 @@ func testWatchWithProgressNotify(t *testing.T, watchOnPut bool) {
 	}
 
 	kvc := clus.RandClient()
-	_, err := kvc.Put(context.TODO(), "foox", "bar")
+	_, err := kvc.Put(t.Context(), "foox", "bar")
 	require.NoError(t, err)
 
 	select {
@@ -568,7 +568,7 @@ func TestConfigurableWatchProgressNotifyInterval(t *testing.T) {
 	defer clus.Terminate(t)
 
 	opts := []clientv3.OpOption{clientv3.WithProgressNotify()}
-	rch := clus.RandClient().Watch(context.Background(), "foo", opts...)
+	rch := clus.RandClient().Watch(t.Context(), "foo", opts...)
 
 	timeout := 1 * time.Second // we expect to receive watch progress notify in 2 * progressInterval,
 	// but for CPU-starved situation it may take longer. So we use 1 second here for timeout.
@@ -609,10 +609,10 @@ func TestWatchRequestProgress(t *testing.T) {
 			var watchChans []clientv3.WatchChan
 
 			for _, prefix := range c.watchers {
-				watchChans = append(watchChans, wc.Watch(context.Background(), prefix, clientv3.WithPrefix()))
+				watchChans = append(watchChans, wc.Watch(t.Context(), prefix, clientv3.WithPrefix()))
 			}
 
-			_, err := wc.Put(context.Background(), "/a", "1")
+			_, err := wc.Put(t.Context(), "/a", "1")
 			require.NoError(t, err)
 
 			for _, rch := range watchChans {
@@ -625,10 +625,10 @@ func TestWatchRequestProgress(t *testing.T) {
 			}
 
 			// put a value not being watched to increment revision
-			_, err = wc.Put(context.Background(), "x", "1")
+			_, err = wc.Put(t.Context(), "x", "1")
 			require.NoError(t, err)
 
-			require.NoError(t, wc.RequestProgress(context.Background()))
+			require.NoError(t, wc.RequestProgress(t.Context()))
 
 			// verify all watch channels receive a progress notify
 			for _, rch := range watchChans {
@@ -651,7 +651,7 @@ func TestWatchEventType(t *testing.T) {
 	defer cluster.Terminate(t)
 
 	client := cluster.RandClient()
-	ctx := context.Background()
+	ctx := t.Context()
 	watchChan := client.Watch(ctx, "/", clientv3.WithPrefix())
 
 	_, err := client.Put(ctx, "/toDelete", "foo")
@@ -723,7 +723,7 @@ func TestWatchErrConnClosed(t *testing.T) {
 	donec := make(chan struct{})
 	go func() {
 		defer close(donec)
-		ch := cli.Watch(context.TODO(), "foo")
+		ch := cli.Watch(t.Context(), "foo")
 
 		if wr := <-ch; !IsCanceled(wr.Err()) {
 			t.Errorf("expected context canceled, got %v", wr.Err())
@@ -752,7 +752,7 @@ func TestWatchAfterClose(t *testing.T) {
 
 	donec := make(chan struct{})
 	go func() {
-		cli.Watch(context.TODO(), "foo")
+		cli.Watch(t.Context(), "foo")
 		if err := cli.Close(); err != nil && !errors.Is(err, context.Canceled) {
 			t.Errorf("expected %v, got %v", context.Canceled, err)
 		}
@@ -777,7 +777,7 @@ func TestWatchWithRequireLeader(t *testing.T) {
 	// ensure that it receives the update so watching after killing quorum
 	// is guaranteed to have the key.
 	liveClient := clus.Client(0)
-	_, err := liveClient.Put(context.TODO(), "foo", "bar")
+	_, err := liveClient.Put(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 
 	clus.Members[1].Stop(t)
@@ -793,8 +793,8 @@ func TestWatchWithRequireLeader(t *testing.T) {
 	// so proxy tests receive a leader loss event on its existing watch before creating a new watch.
 	time.Sleep(time.Duration(5*clus.Members[0].ElectionTicks) * tickDuration)
 
-	chLeader := liveClient.Watch(clientv3.WithRequireLeader(context.TODO()), "foo", clientv3.WithRev(1))
-	chNoLeader := liveClient.Watch(context.TODO(), "foo", clientv3.WithRev(1))
+	chLeader := liveClient.Watch(clientv3.WithRequireLeader(t.Context()), "foo", clientv3.WithRev(1))
+	chNoLeader := liveClient.Watch(t.Context(), "foo", clientv3.WithRev(1))
 
 	select {
 	case resp, ok := <-chLeader:
@@ -833,7 +833,7 @@ func TestWatchWithFilter(t *testing.T) {
 	defer cluster.Terminate(t)
 
 	client := cluster.RandClient()
-	ctx := context.Background()
+	ctx := t.Context()
 
 	wcNoPut := client.Watch(ctx, "a", clientv3.WithFilterPut())
 	wcNoDel := client.Watch(ctx, "a", clientv3.WithFilterDelete())
@@ -871,7 +871,7 @@ func TestWatchWithCreatedNotification(t *testing.T) {
 
 	client := cluster.RandClient()
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	createC := client.Watch(ctx, "a", clientv3.WithCreatedNotify())
 
@@ -891,7 +891,7 @@ func TestWatchWithCreatedNotificationDropConn(t *testing.T) {
 
 	client := cluster.RandClient()
 
-	wch := client.Watch(context.Background(), "a", clientv3.WithCreatedNotify())
+	wch := client.Watch(t.Context(), "a", clientv3.WithCreatedNotify())
 
 	resp := <-wch
 
@@ -924,7 +924,7 @@ func TestWatchCancelOnServer(t *testing.T) {
 	// until require leader watches get create responses to ensure the leadership
 	// watches have started.
 	for {
-		ctx, cancel := context.WithCancel(clientv3.WithRequireLeader(context.TODO()))
+		ctx, cancel := context.WithCancel(clientv3.WithRequireLeader(t.Context()))
 		ww := client.Watch(ctx, "a", clientv3.WithCreatedNotify())
 		wresp := <-ww
 		cancel()
@@ -937,7 +937,7 @@ func TestWatchCancelOnServer(t *testing.T) {
 	for i := 0; i < numWatches; i++ {
 		// force separate streams in client
 		md := metadata.Pairs("some-key", fmt.Sprintf("%d", i))
-		mctx := metadata.NewOutgoingContext(context.Background(), md)
+		mctx := metadata.NewOutgoingContext(t.Context(), md)
 		ctx, cancel := context.WithCancel(mctx)
 		cancels[i] = cancel
 		w := client.Watch(ctx, fmt.Sprintf("%d", i), clientv3.WithCreatedNotify())
@@ -996,17 +996,17 @@ func testWatchOverlapContextCancel(t *testing.T, f func(*integration2.Cluster))
 	for i := range ctxs {
 		// make unique stream
 		md := metadata.Pairs("some-key", fmt.Sprintf("%d", i))
-		ctxs[i] = metadata.NewOutgoingContext(context.Background(), md)
+		ctxs[i] = metadata.NewOutgoingContext(t.Context(), md)
 		// limits the maximum number of outstanding watchers per stream
 		ctxc[i] = make(chan struct{}, 2)
 	}
 
 	// issue concurrent watches on "abc" with cancel
 	cli := clus.RandClient()
-	_, err := cli.Put(context.TODO(), "abc", "def")
+	_, err := cli.Put(t.Context(), "abc", "def")
 	require.NoError(t, err)
 	ch := make(chan struct{}, n)
-	tCtx, cancelFunc := context.WithCancel(context.Background())
+	tCtx, cancelFunc := context.WithCancel(t.Context())
 	defer cancelFunc()
 	for i := 0; i < n; i++ {
 		go func() {
@@ -1059,7 +1059,7 @@ func TestWatchCancelAndCloseClient(t *testing.T) {
 	clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 	cli := clus.Client(0)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	wch := cli.Watch(ctx, "abc")
 	donec := make(chan struct{})
 	go func() {
@@ -1088,7 +1088,7 @@ func TestWatchStressResumeClose(t *testing.T) {
 	defer clus.Terminate(t)
 	cli := clus.Client(0)
 
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	// add more watches than can be resumed before the cancel
 	wchs := make([]clientv3.WatchChan, 2000)
 	for i := range wchs {
@@ -1107,7 +1107,7 @@ func TestWatchCancelDisconnected(t *testing.T) {
 	clus := integration2.NewCluster(t, &integration2.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 	cli := clus.Client(0)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	// add more watches than can be resumed before the cancel
 	wch := cli.Watch(ctx, "abc")
 	clus.Members[0].Stop(t)
@@ -1125,7 +1125,7 @@ func TestWatchClose(t *testing.T) {
 }
 
 func testWatchClose(t *testing.T, wctx *watchctx) {
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	wch := wctx.w.Watch(ctx, "a")
 	cancel()
 	require.NotNilf(t, wch, "expected watcher channel, got nil")
diff --git a/tests/integration/cluster_test.go b/tests/integration/cluster_test.go
index 9ba372bb8..8b98efd6c 100644
--- a/tests/integration/cluster_test.go
+++ b/tests/integration/cluster_test.go
@@ -130,12 +130,12 @@ func TestForceNewCluster(t *testing.T) {
 	c := integration.NewCluster(t, &integration.ClusterConfig{Size: 3, UseBridge: true})
 	defer c.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 	resp, err := c.Members[0].Client.Put(ctx, "/foo", "bar")
 	require.NoErrorf(t, err, "unexpected create error")
 	cancel()
 	// ensure create has been applied in this machine
-	ctx, cancel = context.WithTimeout(context.Background(), integration.RequestTimeout)
+	ctx, cancel = context.WithTimeout(t.Context(), integration.RequestTimeout)
 	watch := c.Members[0].Client.Watcher.Watch(ctx, "/foo", clientv3.WithRev(resp.Header.Revision-1))
 	for resp := range watch {
 		if len(resp.Events) != 0 {
@@ -156,7 +156,7 @@ func TestForceNewCluster(t *testing.T) {
 
 	// use new http client to init new connection
 	// ensure force restart keep the old data, and new Cluster can make progress
-	ctx, cancel = context.WithTimeout(context.Background(), integration.RequestTimeout)
+	ctx, cancel = context.WithTimeout(t.Context(), integration.RequestTimeout)
 	watch = c.Members[0].Client.Watcher.Watch(ctx, "/foo", clientv3.WithRev(resp.Header.Revision-1))
 	for resp := range watch {
 		if len(resp.Events) != 0 {
@@ -240,7 +240,7 @@ func TestIssue2904(t *testing.T) {
 	c.Members[2].Stop(t)
 
 	// send remove member-1 request to the Cluster.
-	ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 	// the proposal is not committed because member 1 is stopped, but the
 	// proposal is appended to leader'Server raft log.
 	c.Members[0].Client.MemberRemove(ctx, uint64(c.Members[2].Server.MemberID()))
@@ -312,7 +312,7 @@ func TestIssue3699(t *testing.T) {
 
 	t.Logf("Expecting successful put...")
 	// try to participate in Cluster
-	ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 	_, err := c.Members[0].Client.Put(ctx, "/foo", "bar")
 	require.NoErrorf(t, err, "unexpected error on Put")
 	cancel()
@@ -441,7 +441,7 @@ func clusterMustProgress(t *testing.T, members []*integration.Member) {
 	)
 	// retry in case of leader loss induced by slow CI
 	for i := 0; i < 3; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 		resp, err = members[0].Client.Put(ctx, key, "bar")
 		cancel()
 		if err == nil {
@@ -452,7 +452,7 @@ func clusterMustProgress(t *testing.T, members []*integration.Member) {
 	require.NoErrorf(t, err, "create on #0 error")
 
 	for i, m := range members {
-		mctx, mcancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+		mctx, mcancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 		watch := m.Client.Watcher.Watch(mctx, key, clientv3.WithRev(resp.Header.Revision-1))
 		for resp := range watch {
 			if len(resp.Events) != 0 {
@@ -494,16 +494,16 @@ func TestConcurrentRemoveMember(t *testing.T) {
 	c := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer c.Terminate(t)
 
-	addResp, err := c.Members[0].Client.MemberAddAsLearner(context.Background(), []string{"http://localhost:123"})
+	addResp, err := c.Members[0].Client.MemberAddAsLearner(t.Context(), []string{"http://localhost:123"})
 	require.NoError(t, err)
 	removeID := addResp.Member.ID
 	done := make(chan struct{})
 	go func() {
 		time.Sleep(time.Second / 2)
-		c.Members[0].Client.MemberRemove(context.Background(), removeID)
+		c.Members[0].Client.MemberRemove(t.Context(), removeID)
 		close(done)
 	}()
-	_, err = c.Members[0].Client.MemberRemove(context.Background(), removeID)
+	_, err = c.Members[0].Client.MemberRemove(t.Context(), removeID)
 	require.NoError(t, err)
 	<-done
 }
@@ -513,16 +513,16 @@ func TestConcurrentMoveLeader(t *testing.T) {
 	c := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer c.Terminate(t)
 
-	addResp, err := c.Members[0].Client.MemberAddAsLearner(context.Background(), []string{"http://localhost:123"})
+	addResp, err := c.Members[0].Client.MemberAddAsLearner(t.Context(), []string{"http://localhost:123"})
 	require.NoError(t, err)
 	removeID := addResp.Member.ID
 	done := make(chan struct{})
 	go func() {
 		time.Sleep(time.Second / 2)
-		c.Members[0].Client.MoveLeader(context.Background(), removeID)
+		c.Members[0].Client.MoveLeader(t.Context(), removeID)
 		close(done)
 	}()
-	_, err = c.Members[0].Client.MemberRemove(context.Background(), removeID)
+	_, err = c.Members[0].Client.MemberRemove(t.Context(), removeID)
 	require.NoError(t, err)
 	<-done
 }
diff --git a/tests/integration/corrupt_test.go b/tests/integration/corrupt_test.go
index ddd7234e2..ddbf2d3e6 100644
--- a/tests/integration/corrupt_test.go
+++ b/tests/integration/corrupt_test.go
@@ -38,7 +38,7 @@ func TestPeriodicCheck(t *testing.T) {
 	cc, err := clus.ClusterClient(t)
 	require.NoError(t, err)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	var totalRevisions int64 = 1210
 	var rev int64
@@ -74,7 +74,7 @@ func TestPeriodicCheckDetectsCorruption(t *testing.T) {
 	cc, err := clus.ClusterClient(t)
 	require.NoError(t, err)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	for i := 0; i < 10; i++ {
 		_, err = cc.Put(ctx, testutil.PickKey(int64(i)), fmt.Sprint(i))
@@ -112,7 +112,7 @@ func TestCompactHashCheck(t *testing.T) {
 	cc, err := clus.ClusterClient(t)
 	require.NoError(t, err)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	var totalRevisions int64 = 1210
 	var rev int64
@@ -149,7 +149,7 @@ func TestCompactHashCheckDetectCorruption(t *testing.T) {
 	cc, err := clus.ClusterClient(t)
 	require.NoError(t, err)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	for i := 0; i < 10; i++ {
 		_, err = cc.Put(ctx, testutil.PickKey(int64(i)), fmt.Sprint(i))
@@ -186,7 +186,7 @@ func TestCompactHashCheckDetectMultipleCorruption(t *testing.T) {
 	cc, err := clus.ClusterClient(t)
 	require.NoError(t, err)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	for i := 0; i < 10; i++ {
 		_, err = cc.Put(ctx, testutil.PickKey(int64(i)), fmt.Sprint(i))
diff --git a/tests/integration/embed/embed_test.go b/tests/integration/embed/embed_test.go
index 242f2b6a7..b7de67c7a 100644
--- a/tests/integration/embed/embed_test.go
+++ b/tests/integration/embed/embed_test.go
@@ -20,7 +20,6 @@
 package embed_test
 
 import (
-	"context"
 	"fmt"
 	"net/url"
 	"os"
@@ -57,7 +56,6 @@ func TestEmbedEtcd(t *testing.T) {
 		wpeers   int
 		wclients int
 	}{
-		{werr: "multiple discovery"},
 		{werr: "advertise-client-urls is required"},
 		{werr: "should be at least"},
 		{werr: "is too long"},
@@ -77,18 +75,17 @@ func TestEmbedEtcd(t *testing.T) {
 		tests[i].cfg.LogOutputs = []string{"/dev/null"}
 	}
 
-	tests[0].cfg.Durl = "abc"
-	setupEmbedCfg(&tests[1].cfg, []url.URL{urls[0]}, []url.URL{urls[1]})
-	tests[1].cfg.AdvertiseClientUrls = nil
-	tests[2].cfg.TickMs = tests[2].cfg.ElectionMs - 1
-	tests[3].cfg.ElectionMs = 999999
-	setupEmbedCfg(&tests[4].cfg, []url.URL{urls[2]}, []url.URL{urls[3]})
-	setupEmbedCfg(&tests[5].cfg, []url.URL{urls[4]}, []url.URL{urls[5], urls[6]})
-	setupEmbedCfg(&tests[6].cfg, []url.URL{urls[7], urls[8]}, []url.URL{urls[9]})
+	setupEmbedCfg(&tests[0].cfg, []url.URL{urls[0]}, []url.URL{urls[1]})
+	tests[0].cfg.AdvertiseClientUrls = nil
+	tests[1].cfg.TickMs = tests[2].cfg.ElectionMs - 1
+	tests[2].cfg.ElectionMs = 999999
+	setupEmbedCfg(&tests[3].cfg, []url.URL{urls[2]}, []url.URL{urls[3]})
+	setupEmbedCfg(&tests[4].cfg, []url.URL{urls[4]}, []url.URL{urls[5], urls[6]})
+	setupEmbedCfg(&tests[5].cfg, []url.URL{urls[7], urls[8]}, []url.URL{urls[9]})
 
 	dnsURL, _ := url.Parse("http://whatever.test:12345")
-	tests[7].cfg.ListenClientUrls = []url.URL{*dnsURL}
-	tests[8].cfg.ListenPeerUrls = []url.URL{*dnsURL}
+	tests[6].cfg.ListenClientUrls = []url.URL{*dnsURL}
+	tests[7].cfg.ListenPeerUrls = []url.URL{*dnsURL}
 
 	dir := filepath.Join(t.TempDir(), "embed-etcd")
 
@@ -162,7 +159,7 @@ func testEmbedEtcdGracefulStop(t *testing.T, secure bool) {
 	defer cli.Close()
 
 	// open watch connection
-	cli.Watch(context.Background(), "foo")
+	cli.Watch(t.Context(), "foo")
 
 	donec := make(chan struct{})
 	go func() {
diff --git a/tests/integration/grpc_test.go b/tests/integration/grpc_test.go
index e3b762c31..606262942 100644
--- a/tests/integration/grpc_test.go
+++ b/tests/integration/grpc_test.go
@@ -15,7 +15,6 @@
 package integration
 
 import (
-	"context"
 	tls "crypto/tls"
 	"fmt"
 	"strings"
@@ -105,7 +104,7 @@ func TestAuthority(t *testing.T) {
 
 				putRequestMethod := "/etcdserverpb.KV/Put"
 				for i := 0; i < 100; i++ {
-					_, err := kv.Put(context.TODO(), "foo", "bar")
+					_, err := kv.Put(t.Context(), "foo", "bar")
 					require.NoError(t, err)
 				}
 
diff --git a/tests/integration/hashkv_test.go b/tests/integration/hashkv_test.go
index 2aa8cc69e..a62625bf8 100644
--- a/tests/integration/hashkv_test.go
+++ b/tests/integration/hashkv_test.go
@@ -46,7 +46,7 @@ func TestCompactionHash(t *testing.T) {
 		},
 	}
 
-	testutil.TestCompactionHash(context.Background(), t, hashTestCase{cc, clus.Members[0].GRPCURL, client, clus.Members[0].Server}, 1000)
+	testutil.TestCompactionHash(t.Context(), t, hashTestCase{cc, clus.Members[0].GRPCURL, client, clus.Members[0].Server}, 1000)
 }
 
 type hashTestCase struct {
diff --git a/tests/integration/member_test.go b/tests/integration/member_test.go
index cc7a0545c..57a114989 100644
--- a/tests/integration/member_test.go
+++ b/tests/integration/member_test.go
@@ -91,7 +91,7 @@ func TestSnapshotAndRestartMember(t *testing.T) {
 
 	var err error
 	for i := 0; i < 120; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 		key := fmt.Sprintf("foo%d", i)
 		_, err = m.Client.Put(ctx, "/"+key, "bar")
 		require.NoErrorf(t, err, "#%d: create on %s error", i, m.URL())
@@ -102,7 +102,7 @@ func TestSnapshotAndRestartMember(t *testing.T) {
 
 	m.WaitOK(t)
 	for i := 0; i < 120; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 		key := fmt.Sprintf("foo%d", i)
 		resp, err := m.Client.Get(ctx, "/"+key)
 		require.NoErrorf(t, err, "#%d: get on %s error", i, m.URL())
@@ -145,7 +145,7 @@ func TestRemoveMemberAndWALReplay(t *testing.T) {
 
 	// Add some k/v to trigger snapshot
 	for i := 0; i < 15; i++ {
-		ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 		_, err := c.Members[0].Client.Put(ctx, fmt.Sprintf("k%d", i), fmt.Sprintf("v%d", i))
 		cancel()
 		require.NoErrorf(t, err, "failed to put key-value")
@@ -174,7 +174,7 @@ func checkMemberCount(t *testing.T, m *integration.Member, expectedMemberCount i
 	if len(membersFromBackend) != expectedMemberCount {
 		t.Errorf("Expect member count read from backend=%d, got %d", expectedMemberCount, len(membersFromBackend))
 	}
-	membersResp, err := m.Client.MemberList(context.Background())
+	membersResp, err := m.Client.MemberList(t.Context())
 	require.NoError(t, err)
 	if len(membersResp.Members) != expectedMemberCount {
 		t.Errorf("Expect len(MemberList)=%d, got %d", expectedMemberCount, len(membersResp.Members))
diff --git a/tests/integration/metrics_test.go b/tests/integration/metrics_test.go
index 1fc1117c5..29e04f412 100644
--- a/tests/integration/metrics_test.go
+++ b/tests/integration/metrics_test.go
@@ -15,7 +15,6 @@
 package integration
 
 import (
-	"context"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -61,7 +60,7 @@ func testMetricDbSizeDefrag(t *testing.T, name string) {
 	putreq := &pb.PutRequest{Key: []byte("k"), Value: make([]byte, 4096)}
 	for i := 0; i < numPuts; i++ {
 		time.Sleep(10 * time.Millisecond) // to execute multiple backend txn
-		_, err := kvc.Put(context.TODO(), putreq)
+		_, err := kvc.Put(t.Context(), putreq)
 		require.NoError(t, err)
 	}
 
@@ -82,12 +81,12 @@ func testMetricDbSizeDefrag(t *testing.T, name string) {
 
 	// clear out historical keys, in use bytes should free pages
 	creq := &pb.CompactionRequest{Revision: int64(numPuts), Physical: true}
-	_, kerr := kvc.Compact(context.TODO(), creq)
+	_, kerr := kvc.Compact(t.Context(), creq)
 	require.NoError(t, kerr)
 
 	validateAfterCompactionInUse := func() error {
 		// Put to move PendingPages to FreePages
-		_, verr := kvc.Put(context.TODO(), putreq)
+		_, verr := kvc.Put(t.Context(), putreq)
 		require.NoError(t, verr)
 		time.Sleep(500 * time.Millisecond)
 
@@ -114,7 +113,7 @@ func testMetricDbSizeDefrag(t *testing.T, name string) {
 	}
 
 	// defrag should give freed space back to fs
-	mc.Defragment(context.TODO(), &pb.DefragmentRequest{})
+	mc.Defragment(t.Context(), &pb.DefragmentRequest{})
 
 	afterDefrag, err := clus.Members[0].Metric(name + "_mvcc_db_total_size_in_bytes")
 	require.NoError(t, err)
@@ -174,11 +173,11 @@ func TestMetricsRangeDurationSeconds(t *testing.T) {
 		"my-namespace/foobar", "my-namespace/foobar1", "namespace/foobar1",
 	}
 	for _, key := range keys {
-		_, err := client.Put(context.Background(), key, "data")
+		_, err := client.Put(t.Context(), key, "data")
 		require.NoError(t, err)
 	}
 
-	_, err := client.Get(context.Background(), "", clientv3.WithFromKey())
+	_, err := client.Get(t.Context(), "", clientv3.WithFromKey())
 	require.NoError(t, err)
 
 	rangeDurationSeconds, err := clus.Members[0].Metric("etcd_server_range_duration_seconds")
diff --git a/tests/integration/proxy/grpcproxy/cluster_test.go b/tests/integration/proxy/grpcproxy/cluster_test.go
index da0c58624..b7276c78f 100644
--- a/tests/integration/proxy/grpcproxy/cluster_test.go
+++ b/tests/integration/proxy/grpcproxy/cluster_test.go
@@ -15,7 +15,6 @@
 package grpcproxy
 
 import (
-	"context"
 	"net"
 	"os"
 	"testing"
@@ -59,7 +58,7 @@ func TestClusterProxyMemberList(t *testing.T) {
 	time.Sleep(200 * time.Millisecond)
 
 	var mresp *clientv3.MemberListResponse
-	mresp, err = client.Cluster.MemberList(context.Background())
+	mresp, err = client.Cluster.MemberList(t.Context())
 	require.NoErrorf(t, err, "err %v, want nil", err)
 
 	require.Lenf(t, mresp.Members, 1, "len(mresp.Members) expected 1, got %d (%+v)", len(mresp.Members), mresp.Members)
@@ -73,7 +72,7 @@ func TestClusterProxyMemberList(t *testing.T) {
 	time.Sleep(200 * time.Millisecond)
 
 	// check add member succ
-	mresp, err = client.Cluster.MemberList(context.Background())
+	mresp, err = client.Cluster.MemberList(t.Context())
 	require.NoErrorf(t, err, "err %v, want nil", err)
 	require.Lenf(t, mresp.Members, 2, "len(mresp.Members) expected 2, got %d (%+v)", len(mresp.Members), mresp.Members)
 	assert.Contains(t, mresp.Members, &pb.Member{Name: hostname, ClientURLs: []string{newMemberAddr}})
@@ -84,7 +83,7 @@ func TestClusterProxyMemberList(t *testing.T) {
 	time.Sleep(200 * time.Millisecond)
 
 	// check delete member succ
-	mresp, err = client.Cluster.MemberList(context.Background())
+	mresp, err = client.Cluster.MemberList(t.Context())
 	require.NoErrorf(t, err, "err %v, want nil", err)
 	require.Lenf(t, mresp.Members, 1, "len(mresp.Members) expected 1, got %d (%+v)", len(mresp.Members), mresp.Members)
 	assert.Contains(t, mresp.Members, &pb.Member{Name: hostname, ClientURLs: []string{cts.caddr}})
diff --git a/tests/integration/proxy/grpcproxy/kv_test.go b/tests/integration/proxy/grpcproxy/kv_test.go
index 00bb326ff..8fd274f14 100644
--- a/tests/integration/proxy/grpcproxy/kv_test.go
+++ b/tests/integration/proxy/grpcproxy/kv_test.go
@@ -15,7 +15,6 @@
 package grpcproxy
 
 import (
-	"context"
 	"net"
 	"testing"
 	"time"
@@ -45,7 +44,7 @@ func TestKVProxyRange(t *testing.T) {
 	}
 	client, err := integration2.NewClient(t, cfg)
 	require.NoErrorf(t, err, "err = %v, want nil", err)
-	_, err = client.Get(context.Background(), "foo")
+	_, err = client.Get(t.Context(), "foo")
 	require.NoErrorf(t, err, "err = %v, want nil", err)
 	client.Close()
 }
diff --git a/tests/integration/revision_test.go b/tests/integration/revision_test.go
index 49ce4d214..6d40adc58 100644
--- a/tests/integration/revision_test.go
+++ b/tests/integration/revision_test.go
@@ -81,7 +81,7 @@ func testRevisionMonotonicWithFailures(t *testing.T, testDuration time.Duration,
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3, UseBridge: true})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), testDuration)
+	ctx, cancel := context.WithTimeout(t.Context(), testDuration)
 	defer cancel()
 
 	wg := sync.WaitGroup{}
@@ -104,7 +104,7 @@ func testRevisionMonotonicWithFailures(t *testing.T, testDuration time.Duration,
 	injectFailures(clus)
 	wg.Wait()
 	kv := clus.Client(0)
-	resp, err := kv.Get(context.Background(), "foo")
+	resp, err := kv.Get(t.Context(), "foo")
 	require.NoError(t, err)
 	t.Logf("Revision %d", resp.Header.Revision)
 }
diff --git a/tests/integration/snapshot/member_test.go b/tests/integration/snapshot/member_test.go
index 1c889df75..e25ab8853 100644
--- a/tests/integration/snapshot/member_test.go
+++ b/tests/integration/snapshot/member_test.go
@@ -56,7 +56,7 @@ func TestSnapshotV3RestoreMultiMemberAdd(t *testing.T) {
 
 	urls := newEmbedURLs(t, 2)
 	newCURLs, newPURLs := urls[:1], urls[1:]
-	_, err = cli.MemberAdd(context.Background(), []string{newPURLs[0].String()})
+	_, err = cli.MemberAdd(t.Context(), []string{newPURLs[0].String()})
 	require.NoError(t, err)
 
 	// wait for membership reconfiguration apply
@@ -89,7 +89,7 @@ func TestSnapshotV3RestoreMultiMemberAdd(t *testing.T) {
 	require.NoError(t, err)
 	defer cli2.Close()
 
-	ctx, cancel := context.WithTimeout(context.Background(), testutil.RequestTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), testutil.RequestTimeout)
 	mresp, err := cli2.MemberList(ctx)
 	cancel()
 	require.NoError(t, err)
@@ -97,7 +97,7 @@ func TestSnapshotV3RestoreMultiMemberAdd(t *testing.T) {
 
 	// make sure restored cluster has kept all data on recovery
 	var gresp *clientv3.GetResponse
-	ctx, cancel = context.WithTimeout(context.Background(), testutil.RequestTimeout)
+	ctx, cancel = context.WithTimeout(t.Context(), testutil.RequestTimeout)
 	gresp, err = cli2.Get(ctx, "foo", clientv3.WithPrefix())
 	cancel()
 	require.NoError(t, err)
diff --git a/tests/integration/snapshot/v3_snapshot_test.go b/tests/integration/snapshot/v3_snapshot_test.go
index d016d3bce..1c1106f02 100644
--- a/tests/integration/snapshot/v3_snapshot_test.go
+++ b/tests/integration/snapshot/v3_snapshot_test.go
@@ -86,7 +86,7 @@ func TestSnapshotV3RestoreSingle(t *testing.T) {
 	defer cli.Close()
 	for i := range kvs {
 		var gresp *clientv3.GetResponse
-		gresp, err = cli.Get(context.Background(), kvs[i].k)
+		gresp, err = cli.Get(t.Context(), kvs[i].k)
 		require.NoError(t, err)
 		require.Equalf(t, string(gresp.Kvs[0].Value), kvs[i].v, "#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[0].Value)
 	}
@@ -117,7 +117,7 @@ func TestSnapshotV3RestoreMulti(t *testing.T) {
 		defer cli.Close()
 		for i := range kvs {
 			var gresp *clientv3.GetResponse
-			gresp, err = cli.Get(context.Background(), kvs[i].k)
+			gresp, err = cli.Get(t.Context(), kvs[i].k)
 			require.NoError(t, err)
 			require.Equalf(t, string(gresp.Kvs[0].Value), kvs[i].v, "#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[0].Value)
 		}
@@ -182,7 +182,7 @@ func createSnapshotFile(t *testing.T, kvs []kv) string {
 	require.NoError(t, err)
 	defer cli.Close()
 	for i := range kvs {
-		ctx, cancel := context.WithTimeout(context.Background(), testutil.RequestTimeout)
+		ctx, cancel := context.WithTimeout(t.Context(), testutil.RequestTimeout)
 		_, err = cli.Put(ctx, kvs[i].k, kvs[i].v)
 		cancel()
 		require.NoError(t, err)
@@ -190,7 +190,7 @@ func createSnapshotFile(t *testing.T, kvs []kv) string {
 
 	sp := snapshot.NewV3(zaptest.NewLogger(t))
 	dpPath := filepath.Join(t.TempDir(), fmt.Sprintf("snapshot%d.db", time.Now().Nanosecond()))
-	_, err = sp.Save(context.Background(), ccfg, dpPath)
+	_, err = sp.Save(t.Context(), ccfg, dpPath)
 	require.NoError(t, err)
 	return dpPath
 }
diff --git a/tests/integration/tracing_test.go b/tests/integration/tracing_test.go
index 74f231a3a..503c224ac 100644
--- a/tests/integration/tracing_test.go
+++ b/tests/integration/tracing_test.go
@@ -38,6 +38,48 @@ import (
 func TestTracing(t *testing.T) {
 	testutil.SkipTestIfShortMode(t,
 		"Wal creation tests are depending on embedded etcd server so are integration-level tests.")
+
+	// Test Unary RPC tracing
+	t.Run("UnaryRPC", func(t *testing.T) {
+		testRPCTracing(t, "UnaryRPC", containsUnaryRPCSpan, func(cli *clientv3.Client) error {
+			// make a request with the instrumented client
+			resp, err := cli.Get(t.Context(), "key")
+			require.NoError(t, err)
+			require.Empty(t, resp.Kvs)
+			return nil
+		})
+	})
+
+	// Test Stream RPC tracing
+	t.Run("StreamRPC", func(t *testing.T) {
+		testRPCTracing(t, "StreamRPC", containsStreamRPCSpan, func(cli *clientv3.Client) error {
+			// Create a context with a reasonable timeout
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
+			defer cancel()
+
+			// Create a watch channel
+			watchChan := cli.Watch(ctx, "watch-key")
+
+			// Put a value to trigger the watch
+			_, err := cli.Put(t.Context(), "watch-key", "watch-value")
+			require.NoError(t, err)
+
+			// Wait for watch event
+			select {
+			case watchResp := <-watchChan:
+				require.NoError(t, watchResp.Err())
+				require.Len(t, watchResp.Events, 1)
+				t.Log("Received watch event successfully")
+			case <-time.After(5 * time.Second):
+				t.Fatal("Timed out waiting for watch event")
+			}
+			return nil
+		})
+	})
+}
+
+// testRPCTracing is a common test function for both Unary and Stream RPC tracing
+func testRPCTracing(t *testing.T, testName string, filterFunc func(*traceservice.ExportTraceServiceRequest) bool, clientAction func(*clientv3.Client) error) {
 	// set up trace collector
 	listener, err := net.Listen("tcp", "localhost:")
 	require.NoError(t, err)
@@ -48,7 +90,7 @@ func TestTracing(t *testing.T) {
 	srv := grpc.NewServer()
 	traceservice.RegisterTraceServiceServer(srv, &traceServer{
 		traceFound: traceFound,
-		filterFunc: containsNodeListSpan,
+		filterFunc: filterFunc,
 	})
 
 	go srv.Serve(listener)
@@ -76,7 +118,7 @@ func TestTracing(t *testing.T) {
 
 	// create a client that has tracing enabled
 	tracer := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.AlwaysSample()))
-	defer tracer.Shutdown(context.TODO())
+	defer tracer.Shutdown(t.Context())
 	tp := trace.TracerProvider(tracer)
 
 	tracingOpts := []otelgrpc.Option{
@@ -89,8 +131,7 @@ func TestTracing(t *testing.T) {
 	}
 
 	dialOptions := []grpc.DialOption{
-		grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor(tracingOpts...)),
-		grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(tracingOpts...)),
+		grpc.WithStatsHandler(otelgrpc.NewClientHandler(tracingOpts...)),
 	}
 	ccfg := clientv3.Config{DialOptions: dialOptions, Endpoints: []string{cfg.AdvertiseClientUrls[0].String()}}
 	cli, err := integration.NewClient(t, ccfg)
@@ -100,21 +141,21 @@ func TestTracing(t *testing.T) {
 	}
 	defer cli.Close()
 
-	// make a request with the instrumented client
-	resp, err := cli.Get(context.TODO(), "key")
+	// Execute the client action (either Unary or Stream RPC)
+	err = clientAction(cli)
 	require.NoError(t, err)
-	require.Empty(t, resp.Kvs)
 
 	// Wait for a span to be recorded from our request
 	select {
 	case <-traceFound:
+		t.Logf("%s trace found", testName)
 		return
 	case <-time.After(30 * time.Second):
 		t.Fatal("Timed out waiting for trace")
 	}
 }
 
-func containsNodeListSpan(req *traceservice.ExportTraceServiceRequest) bool {
+func containsUnaryRPCSpan(req *traceservice.ExportTraceServiceRequest) bool {
 	for _, resourceSpans := range req.GetResourceSpans() {
 		for _, attr := range resourceSpans.GetResource().GetAttributes() {
 			if attr.GetKey() != "service.name" && attr.GetValue().GetStringValue() != "integration-test-tracing" {
@@ -132,6 +173,20 @@ func containsNodeListSpan(req *traceservice.ExportTraceServiceRequest) bool {
 	return false
 }
 
+// containsStreamRPCSpan checks for Watch/Watch spans in trace data
+func containsStreamRPCSpan(req *traceservice.ExportTraceServiceRequest) bool {
+	for _, resourceSpans := range req.GetResourceSpans() {
+		for _, scoped := range resourceSpans.GetScopeSpans() {
+			for _, span := range scoped.GetSpans() {
+				if span.GetName() == "etcdserverpb.Watch/Watch" {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
 // traceServer implements TracesServiceServer
 type traceServer struct {
 	traceFound chan struct{}
@@ -142,7 +197,11 @@ type traceServer struct {
 func (t *traceServer) Export(ctx context.Context, req *traceservice.ExportTraceServiceRequest) (*traceservice.ExportTraceServiceResponse, error) {
 	emptyValue := traceservice.ExportTraceServiceResponse{}
 	if t.filterFunc(req) {
-		t.traceFound <- struct{}{}
+		select {
+		case t.traceFound <- struct{}{}:
+		default:
+			// Channel already notified
+		}
 	}
 	return &emptyValue, nil
 }
diff --git a/tests/integration/utl_wal_version_test.go b/tests/integration/utl_wal_version_test.go
index 4eb422615..b376f50f5 100644
--- a/tests/integration/utl_wal_version_test.go
+++ b/tests/integration/utl_wal_version_test.go
@@ -63,7 +63,7 @@ func TestEtcdVersionFromWAL(t *testing.T) {
 
 	// Once the cluster version has been updated, any entity's storage
 	// version should be align with cluster version.
-	ctx, cancel := context.WithTimeout(context.Background(), testutil.RequestTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), testutil.RequestTimeout)
 	_, err = cli.AuthStatus(ctx)
 	cancel()
 	if err != nil {
diff --git a/tests/integration/v3_alarm_test.go b/tests/integration/v3_alarm_test.go
index 9c1f7f152..35ea115ec 100644
--- a/tests/integration/v3_alarm_test.go
+++ b/tests/integration/v3_alarm_test.go
@@ -56,23 +56,23 @@ func TestV3StorageQuotaApply(t *testing.T) {
 
 	// test small put still works
 	smallbuf := make([]byte, 1024)
-	_, serr := kvc0.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf})
+	_, serr := kvc0.Put(t.Context(), &pb.PutRequest{Key: key, Value: smallbuf})
 	require.NoError(t, serr)
 
 	// test big put
 	bigbuf := make([]byte, quotasize)
-	_, err := kvc1.Put(context.TODO(), &pb.PutRequest{Key: key, Value: bigbuf})
+	_, err := kvc1.Put(t.Context(), &pb.PutRequest{Key: key, Value: bigbuf})
 	require.NoError(t, err)
 
 	// quorum get should work regardless of whether alarm is raised
-	_, err = kvc0.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	_, err = kvc0.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 
 	// wait until alarm is raised for sure-- poll the alarms
 	stopc := time.After(5 * time.Second)
 	for {
 		req := &pb.AlarmRequest{Action: pb.AlarmRequest_GET}
-		resp, aerr := clus.Members[0].Server.Alarm(context.TODO(), req)
+		resp, aerr := clus.Members[0].Server.Alarm(t.Context(), req)
 		require.NoError(t, aerr)
 		if len(resp.Alarms) != 0 {
 			break
@@ -85,7 +85,7 @@ func TestV3StorageQuotaApply(t *testing.T) {
 	}
 
 	// txn with non-mutating Ops should go through when NOSPACE alarm is raised
-	_, err = kvc0.Txn(context.TODO(), &pb.TxnRequest{
+	_, err = kvc0.Txn(t.Context(), &pb.TxnRequest{
 		Compare: []*pb.Compare{
 			{
 				Key:         key,
@@ -106,7 +106,7 @@ func TestV3StorageQuotaApply(t *testing.T) {
 	})
 	require.NoError(t, err)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), integration.RequestWaitTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), integration.RequestWaitTimeout)
 	defer cancel()
 
 	// small quota machine should reject put
@@ -122,7 +122,7 @@ func TestV3StorageQuotaApply(t *testing.T) {
 	clus.Members[1].Restart(t)
 	clus.WaitMembersForLeader(t, clus.Members)
 
-	_, err = kvc1.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf})
+	_, err = kvc1.Put(t.Context(), &pb.PutRequest{Key: key, Value: smallbuf})
 	require.Errorf(t, err, "alarmed instance should reject put after reset")
 }
 
@@ -140,21 +140,21 @@ func TestV3AlarmDeactivate(t *testing.T) {
 		Action:   pb.AlarmRequest_ACTIVATE,
 		Alarm:    pb.AlarmType_NOSPACE,
 	}
-	_, err := mt.Alarm(context.TODO(), alarmReq)
+	_, err := mt.Alarm(t.Context(), alarmReq)
 	require.NoError(t, err)
 
 	key := []byte("abc")
 	smallbuf := make([]byte, 512)
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: key, Value: smallbuf})
 	if err == nil && !eqErrGRPC(err, rpctypes.ErrGRPCNoSpace) {
 		t.Fatalf("put got %v, expected %v", err, rpctypes.ErrGRPCNoSpace)
 	}
 
 	alarmReq.Action = pb.AlarmRequest_DEACTIVATE
-	_, err = mt.Alarm(context.TODO(), alarmReq)
+	_, err = mt.Alarm(t.Context(), alarmReq)
 	require.NoError(t, err)
 
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: key, Value: smallbuf})
 	require.NoError(t, err)
 }
 
@@ -169,7 +169,7 @@ func TestV3CorruptAlarm(t *testing.T) {
 	for i := 0; i < 10; i++ {
 		go func() {
 			defer wg.Done()
-			if _, err := clus.Client(0).Put(context.TODO(), "k", "v"); err != nil {
+			if _, err := clus.Client(0).Put(t.Context(), "k", "v"); err != nil {
 				t.Error(err)
 			}
 		}()
@@ -194,11 +194,11 @@ func TestV3CorruptAlarm(t *testing.T) {
 	time.Sleep(time.Second * 2)
 
 	// Wait for cluster so Puts succeed in case member 0 was the leader.
-	_, err := clus.Client(1).Get(context.TODO(), "k")
+	_, err := clus.Client(1).Get(t.Context(), "k")
 	require.NoError(t, err)
-	_, err = clus.Client(1).Put(context.TODO(), "xyz", "321")
+	_, err = clus.Client(1).Put(t.Context(), "xyz", "321")
 	require.NoError(t, err)
-	_, err = clus.Client(1).Put(context.TODO(), "abc", "fed")
+	_, err = clus.Client(1).Put(t.Context(), "abc", "fed")
 	require.NoError(t, err)
 
 	// Restart with corruption checking enabled.
@@ -212,16 +212,16 @@ func TestV3CorruptAlarm(t *testing.T) {
 	time.Sleep(time.Second * 2)
 
 	clus.Members[0].WaitStarted(t)
-	resp0, err0 := clus.Client(0).Get(context.TODO(), "abc")
+	resp0, err0 := clus.Client(0).Get(t.Context(), "abc")
 	require.NoError(t, err0)
 	clus.Members[1].WaitStarted(t)
-	resp1, err1 := clus.Client(1).Get(context.TODO(), "abc")
+	resp1, err1 := clus.Client(1).Get(t.Context(), "abc")
 	require.NoError(t, err1)
 
 	require.NotEqualf(t, resp0.Kvs[0].ModRevision, resp1.Kvs[0].ModRevision, "matching ModRevision values")
 
 	for i := 0; i < 5; i++ {
-		presp, perr := clus.Client(0).Put(context.TODO(), "abc", "aaa")
+		presp, perr := clus.Client(0).Put(t.Context(), "abc", "aaa")
 		if perr != nil {
 			if eqErrGRPC(perr, rpctypes.ErrCorrupt) {
 				return
@@ -245,7 +245,7 @@ func TestV3CorruptAlarmWithLeaseCorrupted(t *testing.T) {
 	})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	lresp, err := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(ctx, &pb.LeaseGrantRequest{ID: 1, TTL: 60})
@@ -297,16 +297,16 @@ func TestV3CorruptAlarmWithLeaseCorrupted(t *testing.T) {
 	// Revoke lease should remove key except the member with corruption
 	_, err = integration.ToGRPC(clus.Members[0].Client).Lease.LeaseRevoke(ctx, &pb.LeaseRevokeRequest{ID: lresp.ID})
 	require.NoError(t, err)
-	resp0, err0 := clus.Members[1].Client.KV.Get(context.TODO(), "foo")
+	resp0, err0 := clus.Members[1].Client.KV.Get(t.Context(), "foo")
 	require.NoError(t, err0)
-	resp1, err1 := clus.Members[2].Client.KV.Get(context.TODO(), "foo")
+	resp1, err1 := clus.Members[2].Client.KV.Get(t.Context(), "foo")
 	require.NoError(t, err1)
 
 	require.NotEqualf(t, resp0.Header.Revision, resp1.Header.Revision, "matching Revision values")
 
 	// Wait for CorruptCheckTime
 	time.Sleep(time.Second)
-	presp, perr := clus.Client(0).Put(context.TODO(), "abc", "aaa")
+	presp, perr := clus.Client(0).Put(t.Context(), "abc", "aaa")
 	if perr != nil {
 		if eqErrGRPC(perr, rpctypes.ErrCorrupt) {
 			return
diff --git a/tests/integration/v3_auth_test.go b/tests/integration/v3_auth_test.go
index abd6c5e41..e92d21d7e 100644
--- a/tests/integration/v3_auth_test.go
+++ b/tests/integration/v3_auth_test.go
@@ -37,7 +37,7 @@ func TestV3AuthEmptyUserGet(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	api := integration.ToGRPC(clus.Client(0))
@@ -57,7 +57,7 @@ func TestV3AuthEmptyUserPut(t *testing.T) {
 	})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	api := integration.ToGRPC(clus.Client(0))
@@ -85,7 +85,7 @@ func TestV3AuthTokenWithDisable(t *testing.T) {
 	require.NoError(t, cerr)
 	defer c.Close()
 
-	rctx, cancel := context.WithCancel(context.TODO())
+	rctx, cancel := context.WithCancel(t.Context())
 	donec := make(chan struct{})
 	go func() {
 		defer close(donec)
@@ -95,7 +95,7 @@ func TestV3AuthTokenWithDisable(t *testing.T) {
 	}()
 
 	time.Sleep(10 * time.Millisecond)
-	_, err := c.AuthDisable(context.TODO())
+	_, err := c.AuthDisable(t.Context())
 	require.NoError(t, err)
 	time.Sleep(10 * time.Millisecond)
 
@@ -110,13 +110,13 @@ func TestV3AuthRevision(t *testing.T) {
 
 	api := integration.ToGRPC(clus.Client(0))
 
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	presp, perr := api.KV.Put(ctx, &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
 	cancel()
 	require.NoError(t, perr)
 	rev := presp.Header.Revision
 
-	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	ctx, cancel = context.WithTimeout(t.Context(), 5*time.Second)
 	aresp, aerr := api.Auth.UserAdd(ctx, &pb.AuthUserAddRequest{Name: "root", Password: "123", Options: &authpb.UserAddOptions{NoPassword: false}})
 	cancel()
 	require.NoError(t, aerr)
@@ -152,18 +152,18 @@ func testV3AuthWithLeaseRevokeWithRoot(t *testing.T, ccfg integration.ClusterCon
 	require.NoError(t, cerr)
 	defer rootc.Close()
 
-	leaseResp, err := rootc.Grant(context.TODO(), 2)
+	leaseResp, err := rootc.Grant(t.Context(), 2)
 	require.NoError(t, err)
 	leaseID := leaseResp.ID
 
-	_, err = rootc.Put(context.TODO(), "foo", "bar", clientv3.WithLease(leaseID))
+	_, err = rootc.Put(t.Context(), "foo", "bar", clientv3.WithLease(leaseID))
 	require.NoError(t, err)
 
 	// wait for lease expire
 	time.Sleep(3 * time.Second)
 
 	tresp, terr := rootc.TimeToLive(
-		context.TODO(),
+		t.Context(),
 		leaseID,
 		clientv3.WithAttachedKeys(),
 	)
@@ -208,17 +208,17 @@ func TestV3AuthWithLeaseRevoke(t *testing.T) {
 	require.NoError(t, cerr)
 	defer rootc.Close()
 
-	leaseResp, err := rootc.Grant(context.TODO(), 90)
+	leaseResp, err := rootc.Grant(t.Context(), 90)
 	require.NoError(t, err)
 	leaseID := leaseResp.ID
 	// permission of k3 isn't granted to user1
-	_, err = rootc.Put(context.TODO(), "k3", "val", clientv3.WithLease(leaseID))
+	_, err = rootc.Put(t.Context(), "k3", "val", clientv3.WithLease(leaseID))
 	require.NoError(t, err)
 
 	userc, cerr := integration.NewClient(t, clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "user1", Password: "user1-123"})
 	require.NoError(t, cerr)
 	defer userc.Close()
-	_, err = userc.Revoke(context.TODO(), leaseID)
+	_, err = userc.Revoke(t.Context(), leaseID)
 	if err == nil {
 		t.Fatal("revoking from user1 should be failed with permission denied")
 	}
@@ -257,24 +257,24 @@ func TestV3AuthWithLeaseAttach(t *testing.T) {
 	require.NoError(t, cerr)
 	defer user2c.Close()
 
-	leaseResp, err := user1c.Grant(context.TODO(), 90)
+	leaseResp, err := user1c.Grant(t.Context(), 90)
 	require.NoError(t, err)
 	leaseID := leaseResp.ID
 	// permission of k2 is also granted to user2
-	_, err = user1c.Put(context.TODO(), "k2", "val", clientv3.WithLease(leaseID))
+	_, err = user1c.Put(t.Context(), "k2", "val", clientv3.WithLease(leaseID))
 	require.NoError(t, err)
 
-	_, err = user2c.Revoke(context.TODO(), leaseID)
+	_, err = user2c.Revoke(t.Context(), leaseID)
 	require.NoError(t, err)
 
-	leaseResp, err = user1c.Grant(context.TODO(), 90)
+	leaseResp, err = user1c.Grant(t.Context(), 90)
 	require.NoError(t, err)
 	leaseID = leaseResp.ID
 	// permission of k1 isn't granted to user2
-	_, err = user1c.Put(context.TODO(), "k1", "val", clientv3.WithLease(leaseID))
+	_, err = user1c.Put(t.Context(), "k1", "val", clientv3.WithLease(leaseID))
 	require.NoError(t, err)
 
-	_, err = user2c.Revoke(context.TODO(), leaseID)
+	_, err = user2c.Revoke(t.Context(), leaseID)
 	if err == nil {
 		t.Fatal("revoking from user2 should be failed with permission denied")
 	}
@@ -282,11 +282,11 @@ func TestV3AuthWithLeaseAttach(t *testing.T) {
 
 func authSetupUsers(t *testing.T, auth pb.AuthClient, users []user) {
 	for _, user := range users {
-		_, err := auth.UserAdd(context.TODO(), &pb.AuthUserAddRequest{Name: user.name, Password: user.password, Options: &authpb.UserAddOptions{NoPassword: false}})
+		_, err := auth.UserAdd(t.Context(), &pb.AuthUserAddRequest{Name: user.name, Password: user.password, Options: &authpb.UserAddOptions{NoPassword: false}})
 		require.NoError(t, err)
-		_, err = auth.RoleAdd(context.TODO(), &pb.AuthRoleAddRequest{Name: user.role})
+		_, err = auth.RoleAdd(t.Context(), &pb.AuthRoleAddRequest{Name: user.role})
 		require.NoError(t, err)
-		_, err = auth.UserGrantRole(context.TODO(), &pb.AuthUserGrantRoleRequest{User: user.name, Role: user.role})
+		_, err = auth.UserGrantRole(t.Context(), &pb.AuthUserGrantRoleRequest{User: user.name, Role: user.role})
 		require.NoError(t, err)
 
 		if len(user.key) == 0 {
@@ -298,7 +298,7 @@ func authSetupUsers(t *testing.T, auth pb.AuthClient, users []user) {
 			Key:      []byte(user.key),
 			RangeEnd: []byte(user.end),
 		}
-		_, err = auth.RoleGrantPermission(context.TODO(), &pb.AuthRoleGrantPermissionRequest{Name: user.role, Perm: perm})
+		_, err = auth.RoleGrantPermission(t.Context(), &pb.AuthRoleGrantPermissionRequest{Name: user.role, Perm: perm})
 		require.NoError(t, err)
 	}
 }
@@ -313,7 +313,7 @@ func authSetupRoot(t *testing.T, auth pb.AuthClient) {
 		},
 	}
 	authSetupUsers(t, auth, root)
-	_, err := auth.AuthEnable(context.TODO(), &pb.AuthEnableRequest{})
+	_, err := auth.AuthEnable(t.Context(), &pb.AuthEnableRequest{})
 	require.NoError(t, err)
 }
 
@@ -326,12 +326,12 @@ func TestV3AuthNonAuthorizedRPCs(t *testing.T) {
 
 	key := "foo"
 	val := "bar"
-	_, err := nonAuthedKV.Put(context.TODO(), key, val)
+	_, err := nonAuthedKV.Put(t.Context(), key, val)
 	require.NoErrorf(t, err, "couldn't put key (%v)", err)
 
 	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
 
-	respput, err := nonAuthedKV.Put(context.TODO(), key, val)
+	respput, err := nonAuthedKV.Put(t.Context(), key, val)
 	require.Truef(t, eqErrGRPC(err, rpctypes.ErrGRPCUserEmpty), "could put key (%v), it should cause an error of permission denied", respput)
 }
 
@@ -355,13 +355,13 @@ func TestV3AuthOldRevConcurrent(t *testing.T) {
 	f := func(i int) {
 		defer wg.Done()
 		role, user := fmt.Sprintf("test-role-%d", i), fmt.Sprintf("test-user-%d", i)
-		_, err := c.RoleAdd(context.TODO(), role)
+		_, err := c.RoleAdd(t.Context(), role)
 		require.NoError(t, err)
-		_, err = c.RoleGrantPermission(context.TODO(), role, "\x00", clientv3.GetPrefixRangeEnd(""), clientv3.PermissionType(clientv3.PermReadWrite))
+		_, err = c.RoleGrantPermission(t.Context(), role, "\x00", clientv3.GetPrefixRangeEnd(""), clientv3.PermissionType(clientv3.PermReadWrite))
 		require.NoError(t, err)
-		_, err = c.UserAdd(context.TODO(), user, "123")
+		_, err = c.UserAdd(t.Context(), user, "123")
 		require.NoError(t, err)
-		_, err = c.Put(context.TODO(), "a", "b")
+		_, err = c.Put(t.Context(), "a", "b")
 		assert.NoError(t, err)
 	}
 	// needs concurrency to trigger
@@ -378,7 +378,7 @@ func TestV3AuthWatchErrorAndWatchId0(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 
 	users := []user{
diff --git a/tests/integration/v3_election_test.go b/tests/integration/v3_election_test.go
index d1d7e48d2..8bae7b0e0 100644
--- a/tests/integration/v3_election_test.go
+++ b/tests/integration/v3_election_test.go
@@ -57,7 +57,7 @@ func TestElectionWait(t *testing.T) {
 				}
 				b := concurrency.NewElection(session, "test-election")
 
-				cctx, cancel := context.WithCancel(context.TODO())
+				cctx, cancel := context.WithCancel(t.Context())
 				defer cancel()
 				s, ok := <-b.Observe(cctx)
 				if !ok {
@@ -83,7 +83,7 @@ func TestElectionWait(t *testing.T) {
 
 			e := concurrency.NewElection(session, "test-election")
 			ev := fmt.Sprintf("electval-%v", time.Now().UnixNano())
-			if err := e.Campaign(context.TODO(), ev); err != nil {
+			if err := e.Campaign(t.Context(), ev); err != nil {
 				t.Errorf("failed volunteer (%v)", err)
 			}
 			// wait for followers to accept leadership
@@ -94,7 +94,7 @@ func TestElectionWait(t *testing.T) {
 				}
 			}
 			// let next leader take over
-			if err := e.Resign(context.TODO()); err != nil {
+			if err := e.Resign(t.Context()); err != nil {
 				t.Errorf("failed resign (%v)", err)
 			}
 			// tell followers to start listening for next leader
@@ -116,7 +116,7 @@ func TestElectionFailover(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	cctx, cancel := context.WithCancel(context.TODO())
+	cctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	ss := make([]*concurrency.Session, 3)
@@ -132,7 +132,7 @@ func TestElectionFailover(t *testing.T) {
 
 	// first leader (elected)
 	e := concurrency.NewElection(ss[0], "test-election")
-	err := e.Campaign(context.TODO(), "foo")
+	err := e.Campaign(t.Context(), "foo")
 	require.NoErrorf(t, err, "failed volunteer")
 
 	// check first leader
@@ -145,7 +145,7 @@ func TestElectionFailover(t *testing.T) {
 	electedErrC := make(chan error, 1)
 	go func() {
 		ee := concurrency.NewElection(ss[1], "test-election")
-		eer := ee.Campaign(context.TODO(), "bar")
+		eer := ee.Campaign(t.Context(), "bar")
 		electedErrC <- eer // If eer != nil, the test will fail by calling t.Fatal(eer)
 	}()
 
@@ -180,13 +180,13 @@ func TestElectionSessionRecampaign(t *testing.T) {
 	defer session.Orphan()
 
 	e := concurrency.NewElection(session, "test-elect")
-	err = e.Campaign(context.TODO(), "abc")
+	err = e.Campaign(t.Context(), "abc")
 	require.NoError(t, err)
 	e2 := concurrency.NewElection(session, "test-elect")
-	err = e2.Campaign(context.TODO(), "def")
+	err = e2.Campaign(t.Context(), "def")
 	require.NoError(t, err)
 
-	ctx, cancel := context.WithCancel(context.TODO())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 	if resp := <-e.Observe(ctx); len(resp.Kvs) == 0 || string(resp.Kvs[0].Value) != "def" {
 		t.Fatalf("expected value=%q, got response %v", "def", resp)
@@ -203,12 +203,12 @@ func TestElectionOnPrefixOfExistingKey(t *testing.T) {
 	defer clus.Terminate(t)
 
 	cli := clus.RandClient()
-	_, err := cli.Put(context.TODO(), "testa", "value")
+	_, err := cli.Put(t.Context(), "testa", "value")
 	require.NoError(t, err)
 	s, serr := concurrency.NewSession(cli)
 	require.NoError(t, serr)
 	e := concurrency.NewElection(s, "test")
-	ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	err = e.Campaign(ctx, "abc")
 	cancel()
 	// after 5 seconds, deadlock results in
@@ -229,13 +229,13 @@ func TestElectionOnSessionRestart(t *testing.T) {
 	require.NoError(t, err)
 
 	e := concurrency.NewElection(session, "test-elect")
-	require.NoError(t, e.Campaign(context.TODO(), "abc"))
+	require.NoError(t, e.Campaign(t.Context(), "abc"))
 
 	// ensure leader is not lost to waiter on fail-over
 	waitSession, werr := concurrency.NewSession(cli)
 	require.NoError(t, werr)
 	defer waitSession.Orphan()
-	waitCtx, waitCancel := context.WithTimeout(context.TODO(), 5*time.Second)
+	waitCtx, waitCancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer waitCancel()
 	go concurrency.NewElection(waitSession, "test-elect").Campaign(waitCtx, "123")
 
@@ -245,9 +245,9 @@ func TestElectionOnSessionRestart(t *testing.T) {
 	defer newSession.Orphan()
 
 	newElection := concurrency.NewElection(newSession, "test-elect")
-	require.NoError(t, newElection.Campaign(context.TODO(), "def"))
+	require.NoError(t, newElection.Campaign(t.Context(), "def"))
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer cancel()
 	if resp := <-newElection.Observe(ctx); len(resp.Kvs) == 0 || string(resp.Kvs[0].Value) != "def" {
 		t.Errorf("expected value=%q, got response %v", "def", resp)
@@ -268,14 +268,14 @@ func TestElectionObserveCompacted(t *testing.T) {
 	defer session.Orphan()
 
 	e := concurrency.NewElection(session, "test-elect")
-	require.NoError(t, e.Campaign(context.TODO(), "abc"))
+	require.NoError(t, e.Campaign(t.Context(), "abc"))
 
-	presp, perr := cli.Put(context.TODO(), "foo", "bar")
+	presp, perr := cli.Put(t.Context(), "foo", "bar")
 	require.NoError(t, perr)
-	_, cerr := cli.Compact(context.TODO(), presp.Header.Revision)
+	_, cerr := cli.Compact(t.Context(), presp.Header.Revision)
 	require.NoError(t, cerr)
 
-	v, ok := <-e.Observe(context.TODO())
+	v, ok := <-e.Observe(t.Context())
 	if !ok {
 		t.Fatal("failed to observe on compacted revision")
 	}
@@ -368,7 +368,7 @@ func TestElectionWithAuthEnabled(t *testing.T) {
 			s.Orphan()
 
 			e := concurrency.NewElection(s, campaign.pfx)
-			eerr := e.Campaign(context.Background(), "whatever")
+			eerr := e.Campaign(t.Context(), "whatever")
 			if eerr != nil {
 				errC <- fmt.Errorf("[Campaign] %s: %w", campaign.name, eerr)
 			}
diff --git a/tests/integration/v3_failover_test.go b/tests/integration/v3_failover_test.go
index 03b829b74..f6daef67b 100644
--- a/tests/integration/v3_failover_test.go
+++ b/tests/integration/v3_failover_test.go
@@ -108,7 +108,7 @@ func putWithRetries(t *testing.T, cli *clientv3.Client, key, val string, retryCo
 		// put data test
 		err := func() error {
 			t.Log("Sanity test, putting data")
-			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
 			defer cancel()
 
 			if _, putErr := cli.Put(ctx, key, val); putErr != nil {
@@ -133,7 +133,7 @@ func getWithRetries(t *testing.T, cli *clientv3.Client, key, val string, retryCo
 		// get data test
 		err := func() error {
 			t.Log("Sanity test, getting data")
-			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
 			defer cancel()
 			resp, getErr := cli.Get(ctx, key)
 			if getErr != nil {
diff --git a/tests/integration/v3_grpc_inflight_test.go b/tests/integration/v3_grpc_inflight_test.go
index 3d8b446e5..35a9ec1fd 100644
--- a/tests/integration/v3_grpc_inflight_test.go
+++ b/tests/integration/v3_grpc_inflight_test.go
@@ -39,10 +39,10 @@ func TestV3MaintenanceDefragmentInflightRange(t *testing.T) {
 
 	cli := clus.RandClient()
 	kvc := integration.ToGRPC(cli).KV
-	_, err := kvc.Put(context.Background(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
+	_, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
 	require.NoError(t, err)
 
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
 
 	donec := make(chan struct{})
 	go func() {
@@ -51,7 +51,7 @@ func TestV3MaintenanceDefragmentInflightRange(t *testing.T) {
 	}()
 
 	mvc := integration.ToGRPC(cli).Maintenance
-	mvc.Defragment(context.Background(), &pb.DefragmentRequest{})
+	mvc.Defragment(t.Context(), &pb.DefragmentRequest{})
 	cancel()
 
 	<-donec
@@ -69,10 +69,10 @@ func TestV3KVInflightRangeRequests(t *testing.T) {
 	cli := clus.RandClient()
 	kvc := integration.ToGRPC(cli).KV
 
-	_, err := kvc.Put(context.Background(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
+	_, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
 	require.NoError(t, err)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
 
 	reqN := 10 // use 500+ for fast machine
 	var wg sync.WaitGroup
diff --git a/tests/integration/v3_grpc_test.go b/tests/integration/v3_grpc_test.go
index f7c331a55..ef7026763 100644
--- a/tests/integration/v3_grpc_test.go
+++ b/tests/integration/v3_grpc_test.go
@@ -51,12 +51,12 @@ func TestV3PutOverwrite(t *testing.T) {
 	key := []byte("foo")
 	reqput := &pb.PutRequest{Key: key, Value: []byte("bar"), PrevKv: true}
 
-	respput, err := kvc.Put(context.TODO(), reqput)
+	respput, err := kvc.Put(t.Context(), reqput)
 	require.NoErrorf(t, err, "couldn't put key")
 
 	// overwrite
 	reqput.Value = []byte("baz")
-	respput2, err := kvc.Put(context.TODO(), reqput)
+	respput2, err := kvc.Put(t.Context(), reqput)
 	require.NoErrorf(t, err, "couldn't put key")
 	require.Greaterf(t, respput2.Header.Revision, respput.Header.Revision, "expected newer revision on overwrite, got %v <= %v",
 		respput2.Header.Revision, respput.Header.Revision)
@@ -65,7 +65,7 @@ func TestV3PutOverwrite(t *testing.T) {
 	}
 
 	reqrange := &pb.RangeRequest{Key: key}
-	resprange, err := kvc.Range(context.TODO(), reqrange)
+	resprange, err := kvc.Range(t.Context(), reqrange)
 	require.NoErrorf(t, err, "couldn't get key")
 	require.Lenf(t, resprange.Kvs, 1, "expected 1 key, got %v", len(resprange.Kvs))
 
@@ -100,7 +100,7 @@ func TestV3PutRestart(t *testing.T) {
 	require.NoErrorf(t, cerr, "cannot create client")
 	clus.Members[stopIdx].ServerClient = c
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	reqput := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 	_, err := kvc.Put(ctx, reqput)
@@ -118,20 +118,20 @@ func TestV3CompactCurrentRev(t *testing.T) {
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 	preq := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 	for i := 0; i < 3; i++ {
-		_, err := kvc.Put(context.Background(), preq)
+		_, err := kvc.Put(t.Context(), preq)
 		require.NoErrorf(t, err, "couldn't put key")
 	}
 	// get key to add to proxy cache, if any
-	_, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	_, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	// compact on current revision
-	_, err = kvc.Compact(context.Background(), &pb.CompactionRequest{Revision: 4})
+	_, err = kvc.Compact(t.Context(), &pb.CompactionRequest{Revision: 4})
 	require.NoErrorf(t, err, "couldn't compact kv space")
 	// key still exists when linearized?
-	_, err = kvc.Range(context.Background(), &pb.RangeRequest{Key: []byte("foo")})
+	_, err = kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoErrorf(t, err, "couldn't get key after compaction")
 	// key still exists when serialized?
-	_, err = kvc.Range(context.Background(), &pb.RangeRequest{Key: []byte("foo"), Serializable: true})
+	_, err = kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo"), Serializable: true})
 	require.NoErrorf(t, err, "couldn't get serialized key after compaction")
 }
 
@@ -145,11 +145,11 @@ func TestV3HashKV(t *testing.T) {
 	mvc := integration.ToGRPC(clus.RandClient()).Maintenance
 
 	for i := 0; i < 10; i++ {
-		resp, err := kvc.Put(context.Background(), &pb.PutRequest{Key: []byte("foo"), Value: []byte(fmt.Sprintf("bar%d", i))})
+		resp, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte(fmt.Sprintf("bar%d", i))})
 		require.NoError(t, err)
 
 		rev := resp.Header.Revision
-		hresp, err := mvc.HashKV(context.Background(), &pb.HashKVRequest{Revision: 0})
+		hresp, err := mvc.HashKV(t.Context(), &pb.HashKVRequest{Revision: 0})
 		require.NoError(t, err)
 		if rev != hresp.Header.Revision {
 			t.Fatalf("Put rev %v != HashKV rev %v", rev, hresp.Header.Revision)
@@ -158,7 +158,7 @@ func TestV3HashKV(t *testing.T) {
 		prevHash := hresp.Hash
 		prevCompactRev := hresp.CompactRevision
 		for i := 0; i < 10; i++ {
-			hresp, err := mvc.HashKV(context.Background(), &pb.HashKVRequest{Revision: 0})
+			hresp, err := mvc.HashKV(t.Context(), &pb.HashKVRequest{Revision: 0})
 			require.NoError(t, err)
 			if rev != hresp.Header.Revision {
 				t.Fatalf("Put rev %v != HashKV rev %v", rev, hresp.Header.Revision)
@@ -248,7 +248,7 @@ func TestV3TxnTooManyOps(t *testing.T) {
 			tt(txn)
 		}
 
-		_, err := kvc.Txn(context.Background(), txn)
+		_, err := kvc.Txn(t.Context(), txn)
 		if !eqErrGRPC(err, rpctypes.ErrGRPCTooManyOps) {
 			t.Errorf("#%d: err = %v, want %v", i, err, rpctypes.ErrGRPCTooManyOps)
 		}
@@ -374,7 +374,7 @@ func TestV3TxnDuplicateKeys(t *testing.T) {
 	}
 	for i, tt := range tests {
 		txn := &pb.TxnRequest{Success: tt.txnSuccess}
-		_, err := kvc.Txn(context.Background(), txn)
+		_, err := kvc.Txn(t.Context(), txn)
 		if !eqErrGRPC(err, tt.werr) {
 			t.Errorf("#%d: err = %v, want %v", i, err, tt.werr)
 		}
@@ -389,12 +389,12 @@ func TestV3TxnRevision(t *testing.T) {
 
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 	pr := &pb.PutRequest{Key: []byte("abc"), Value: []byte("def")}
-	presp, err := kvc.Put(context.TODO(), pr)
+	presp, err := kvc.Put(t.Context(), pr)
 	require.NoError(t, err)
 
 	txnget := &pb.RequestOp{Request: &pb.RequestOp_RequestRange{RequestRange: &pb.RangeRequest{Key: []byte("abc")}}}
 	txn := &pb.TxnRequest{Success: []*pb.RequestOp{txnget}}
-	tresp, err := kvc.Txn(context.TODO(), txn)
+	tresp, err := kvc.Txn(t.Context(), txn)
 	require.NoError(t, err)
 
 	// did not update revision
@@ -404,7 +404,7 @@ func TestV3TxnRevision(t *testing.T) {
 
 	txndr := &pb.RequestOp{Request: &pb.RequestOp_RequestDeleteRange{RequestDeleteRange: &pb.DeleteRangeRequest{Key: []byte("def")}}}
 	txn = &pb.TxnRequest{Success: []*pb.RequestOp{txndr}}
-	tresp, err = kvc.Txn(context.TODO(), txn)
+	tresp, err = kvc.Txn(t.Context(), txn)
 	require.NoError(t, err)
 
 	// did not update revision
@@ -414,7 +414,7 @@ func TestV3TxnRevision(t *testing.T) {
 
 	txnput := &pb.RequestOp{Request: &pb.RequestOp_RequestPut{RequestPut: &pb.PutRequest{Key: []byte("abc"), Value: []byte("123")}}}
 	txn = &pb.TxnRequest{Success: []*pb.RequestOp{txnput}}
-	tresp, err = kvc.Txn(context.TODO(), txn)
+	tresp, err = kvc.Txn(t.Context(), txn)
 	require.NoError(t, err)
 
 	// updated revision
@@ -439,7 +439,7 @@ func TestV3TxnCmpHeaderRev(t *testing.T) {
 		go func() {
 			defer close(revc)
 			pr := &pb.PutRequest{Key: []byte("k"), Value: []byte("v")}
-			presp, err := kvc.Put(context.TODO(), pr)
+			presp, err := kvc.Put(t.Context(), pr)
 			errCh <- err
 			if err != nil {
 				return
@@ -461,7 +461,7 @@ func TestV3TxnCmpHeaderRev(t *testing.T) {
 		}
 		txn.Compare = append(txn.Compare, cmp)
 
-		tresp, err := kvc.Txn(context.TODO(), txn)
+		tresp, err := kvc.Txn(t.Context(), txn)
 		require.NoError(t, err)
 
 		prev := <-revc
@@ -486,7 +486,7 @@ func TestV3TxnRangeCompare(t *testing.T) {
 
 	// put keys, named by expected revision
 	for _, k := range []string{"/a/2", "/a/3", "/a/4", "/f/5"} {
-		_, err := clus.Client(0).Put(context.TODO(), k, "x")
+		_, err := clus.Client(0).Put(t.Context(), k, "x")
 		require.NoError(t, err)
 	}
 
@@ -578,7 +578,7 @@ func TestV3TxnRangeCompare(t *testing.T) {
 	for i, tt := range tests {
 		txn := &pb.TxnRequest{}
 		txn.Compare = append(txn.Compare, &tt.cmp)
-		tresp, err := kvc.Txn(context.TODO(), txn)
+		tresp, err := kvc.Txn(t.Context(), txn)
 		require.NoError(t, err)
 		if tt.wSuccess != tresp.Succeeded {
 			t.Errorf("#%d: expected %v, got %v", i, tt.wSuccess, tresp.Succeeded)
@@ -625,7 +625,7 @@ func TestV3TxnNestedPath(t *testing.T) {
 		txn = nextTxn
 	}
 
-	tresp, err := kvc.Txn(context.TODO(), topTxn)
+	tresp, err := kvc.Txn(t.Context(), topTxn)
 	require.NoError(t, err)
 
 	curTxnResp := tresp
@@ -650,7 +650,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 
 	// create lease
 	lc := integration.ToGRPC(clus.RandClient()).Lease
-	lresp, err := lc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lresp, err := lc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err)
 	require.Empty(t, lresp.Error)
 
@@ -663,7 +663,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 			func() error {
 				preq := putReq
 				preq.IgnoreValue = true
-				_, err := kvc.Put(context.TODO(), &preq)
+				_, err := kvc.Put(t.Context(), &preq)
 				return err
 			},
 			rpctypes.ErrGRPCKeyNotFound,
@@ -678,7 +678,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 				txn.Success = append(txn.Success, &pb.RequestOp{
 					Request: &pb.RequestOp_RequestPut{RequestPut: &preq},
 				})
-				_, err := kvc.Txn(context.TODO(), txn)
+				_, err := kvc.Txn(t.Context(), txn)
 				return err
 			},
 			rpctypes.ErrGRPCKeyNotFound,
@@ -686,7 +686,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 		},
 		{ // put success
 			func() error {
-				_, err := kvc.Put(context.TODO(), &putReq)
+				_, err := kvc.Put(t.Context(), &putReq)
 				return err
 			},
 			nil,
@@ -702,7 +702,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 				txn.Success = append(txn.Success, &pb.RequestOp{
 					Request: &pb.RequestOp_RequestPut{RequestPut: &preq},
 				})
-				_, err := kvc.Txn(context.TODO(), txn)
+				_, err := kvc.Txn(t.Context(), txn)
 				return err
 			},
 			nil,
@@ -712,7 +712,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 			func() error {
 				preq := putReq
 				preq.IgnoreValue = true
-				_, err := kvc.Put(context.TODO(), &preq)
+				_, err := kvc.Put(t.Context(), &preq)
 				return err
 			},
 			rpctypes.ErrGRPCValueProvided,
@@ -723,7 +723,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 				preq := putReq
 				preq.Value = nil
 				preq.IgnoreValue = true
-				presp, err := kvc.Put(context.TODO(), &preq)
+				presp, err := kvc.Put(t.Context(), &preq)
 				if err != nil {
 					return err
 				}
@@ -737,7 +737,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 		},
 		{ // revoke lease, ensure detached key doesn't get deleted
 			func() error {
-				_, err := lc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: lresp.ID})
+				_, err := lc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: lresp.ID})
 				return err
 			},
 			nil,
@@ -752,7 +752,7 @@ func TestV3PutIgnoreValue(t *testing.T) {
 		if tt.putErr != nil {
 			continue
 		}
-		rr, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: key})
+		rr, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: key})
 		if err != nil {
 			t.Fatalf("#%d: %v", i, err)
 		}
@@ -779,7 +779,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 
 	// create lease
 	lc := integration.ToGRPC(clus.RandClient()).Lease
-	lresp, err := lc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lresp, err := lc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err)
 	require.Empty(t, lresp.Error)
 
@@ -796,7 +796,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 			func() error {
 				preq := putReq
 				preq.IgnoreLease = true
-				_, err := kvc.Put(context.TODO(), &preq)
+				_, err := kvc.Put(t.Context(), &preq)
 				return err
 			},
 			rpctypes.ErrGRPCKeyNotFound,
@@ -811,7 +811,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 				txn.Success = append(txn.Success, &pb.RequestOp{
 					Request: &pb.RequestOp_RequestPut{RequestPut: &preq},
 				})
-				_, err := kvc.Txn(context.TODO(), txn)
+				_, err := kvc.Txn(t.Context(), txn)
 				return err
 			},
 			rpctypes.ErrGRPCKeyNotFound,
@@ -822,7 +822,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 			func() error {
 				preq := putReq
 				preq.Lease = lresp.ID
-				_, err := kvc.Put(context.TODO(), &preq)
+				_, err := kvc.Put(t.Context(), &preq)
 				return err
 			},
 			nil,
@@ -838,7 +838,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 				txn.Success = append(txn.Success, &pb.RequestOp{
 					Request: &pb.RequestOp_RequestPut{RequestPut: &preq},
 				})
-				_, err := kvc.Txn(context.TODO(), txn)
+				_, err := kvc.Txn(t.Context(), txn)
 				return err
 			},
 			nil,
@@ -850,7 +850,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 				preq := putReq
 				preq.Lease = lresp.ID
 				preq.IgnoreLease = true
-				_, err := kvc.Put(context.TODO(), &preq)
+				_, err := kvc.Put(t.Context(), &preq)
 				return err
 			},
 			rpctypes.ErrGRPCLeaseProvided,
@@ -859,7 +859,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 		},
 		{ // overwrite with previous value, ensure no prev-kv is returned and lease is detached
 			func() error {
-				presp, err := kvc.Put(context.TODO(), &putReq)
+				presp, err := kvc.Put(t.Context(), &putReq)
 				if err != nil {
 					return err
 				}
@@ -874,7 +874,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 		},
 		{ // revoke lease, ensure detached key doesn't get deleted
 			func() error {
-				_, err := lc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: lresp.ID})
+				_, err := lc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: lresp.ID})
 				return err
 			},
 			nil,
@@ -890,7 +890,7 @@ func TestV3PutIgnoreLease(t *testing.T) {
 		if tt.putErr != nil {
 			continue
 		}
-		rr, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: key})
+		rr, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: key})
 		if err != nil {
 			t.Fatalf("#%d: %v", i, err)
 		}
@@ -918,7 +918,7 @@ func TestV3PutMissingLease(t *testing.T) {
 	tests := []func(){
 		// put case
 		func() {
-			if presp, err := kvc.Put(context.TODO(), preq); err == nil {
+			if presp, err := kvc.Put(t.Context(), preq); err == nil {
 				t.Errorf("succeeded put key. req: %v. resp: %v", preq, presp)
 			}
 		},
@@ -930,7 +930,7 @@ func TestV3PutMissingLease(t *testing.T) {
 					RequestPut: preq,
 				},
 			})
-			if tresp, err := kvc.Txn(context.TODO(), txn); err == nil {
+			if tresp, err := kvc.Txn(t.Context(), txn); err == nil {
 				t.Errorf("succeeded txn success. req: %v. resp: %v", txn, tresp)
 			}
 		},
@@ -948,7 +948,7 @@ func TestV3PutMissingLease(t *testing.T) {
 				Key:    []byte("bar"),
 			}
 			txn.Compare = append(txn.Compare, cmp)
-			if tresp, err := kvc.Txn(context.TODO(), txn); err == nil {
+			if tresp, err := kvc.Txn(t.Context(), txn); err == nil {
 				t.Errorf("succeeded txn failure. req: %v. resp: %v", txn, tresp)
 			}
 		},
@@ -966,7 +966,7 @@ func TestV3PutMissingLease(t *testing.T) {
 					RequestPut: preq,
 				},
 			})
-			if tresp, err := kvc.Txn(context.TODO(), txn); err != nil {
+			if tresp, err := kvc.Txn(t.Context(), txn); err != nil {
 				t.Errorf("failed good txn. req: %v. resp: %v", txn, tresp)
 			}
 		},
@@ -976,7 +976,7 @@ func TestV3PutMissingLease(t *testing.T) {
 		f()
 		// key shouldn't have been stored
 		rreq := &pb.RangeRequest{Key: key}
-		rresp, err := kvc.Range(context.TODO(), rreq)
+		rresp, err := kvc.Range(t.Context(), rreq)
 		if err != nil {
 			t.Errorf("#%d. could not rangereq (%v)", i, err)
 		} else if len(rresp.Kvs) != 0 {
@@ -1059,7 +1059,7 @@ func TestV3DeleteRange(t *testing.T) {
 			ks := tt.keySet
 			for j := range ks {
 				reqput := &pb.PutRequest{Key: []byte(ks[j]), Value: []byte{}}
-				_, err := kvc.Put(context.TODO(), reqput)
+				_, err := kvc.Put(t.Context(), reqput)
 				if err != nil {
 					t.Fatalf("couldn't put key (%v)", err)
 				}
@@ -1070,7 +1070,7 @@ func TestV3DeleteRange(t *testing.T) {
 				RangeEnd: []byte(tt.end),
 				PrevKv:   tt.prevKV,
 			}
-			dresp, err := kvc.DeleteRange(context.TODO(), dreq)
+			dresp, err := kvc.DeleteRange(t.Context(), dreq)
 			if err != nil {
 				t.Fatalf("couldn't delete range on test %d (%v)", i, err)
 			}
@@ -1084,7 +1084,7 @@ func TestV3DeleteRange(t *testing.T) {
 			}
 
 			rreq := &pb.RangeRequest{Key: []byte{0x0}, RangeEnd: []byte{0xff}}
-			rresp, err := kvc.Range(context.TODO(), rreq)
+			rresp, err := kvc.Range(t.Context(), rreq)
 			if err != nil {
 				t.Errorf("couldn't get range on test %v (%v)", i, err)
 			}
@@ -1114,13 +1114,13 @@ func TestV3TxnInvalidRange(t *testing.T) {
 	preq := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 
 	for i := 0; i < 3; i++ {
-		_, err := kvc.Put(context.Background(), preq)
+		_, err := kvc.Put(t.Context(), preq)
 		if err != nil {
 			t.Fatalf("couldn't put key (%v)", err)
 		}
 	}
 
-	_, err := kvc.Compact(context.Background(), &pb.CompactionRequest{Revision: 2})
+	_, err := kvc.Compact(t.Context(), &pb.CompactionRequest{Revision: 2})
 	if err != nil {
 		t.Fatalf("couldn't compact kv space (%v)", err)
 	}
@@ -1140,14 +1140,14 @@ func TestV3TxnInvalidRange(t *testing.T) {
 		},
 	})
 
-	if _, err := kvc.Txn(context.TODO(), txn); !eqErrGRPC(err, rpctypes.ErrGRPCFutureRev) {
+	if _, err := kvc.Txn(t.Context(), txn); !eqErrGRPC(err, rpctypes.ErrGRPCFutureRev) {
 		t.Errorf("err = %v, want %v", err, rpctypes.ErrGRPCFutureRev)
 	}
 
 	// compacted rev
 	tv, _ := txn.Success[1].Request.(*pb.RequestOp_RequestRange)
 	tv.RequestRange.Revision = 1
-	if _, err := kvc.Txn(context.TODO(), txn); !eqErrGRPC(err, rpctypes.ErrGRPCCompacted) {
+	if _, err := kvc.Txn(t.Context(), txn); !eqErrGRPC(err, rpctypes.ErrGRPCCompacted) {
 		t.Errorf("err = %v, want %v", err, rpctypes.ErrGRPCCompacted)
 	}
 }
@@ -1164,7 +1164,7 @@ func TestV3TooLargeRequest(t *testing.T) {
 	largeV := make([]byte, 2*1024*1024)
 	preq := &pb.PutRequest{Key: []byte("foo"), Value: largeV}
 
-	_, err := kvc.Put(context.Background(), preq)
+	_, err := kvc.Put(t.Context(), preq)
 	if !eqErrGRPC(err, rpctypes.ErrGRPCRequestTooLarge) {
 		t.Errorf("err = %v, want %v", err, rpctypes.ErrGRPCRequestTooLarge)
 	}
@@ -1183,13 +1183,13 @@ func TestV3Hash(t *testing.T) {
 	preq := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 
 	for i := 0; i < 3; i++ {
-		_, err := kvc.Put(context.Background(), preq)
+		_, err := kvc.Put(t.Context(), preq)
 		if err != nil {
 			t.Fatalf("couldn't put key (%v)", err)
 		}
 	}
 
-	resp, err := m.Hash(context.Background(), &pb.HashRequest{})
+	resp, err := m.Hash(t.Context(), &pb.HashRequest{})
 	if err != nil || resp.Hash == 0 {
 		t.Fatalf("couldn't hash (%v, hash %d)", err, resp.Hash)
 	}
@@ -1202,7 +1202,7 @@ func TestV3HashRestart(t *testing.T) {
 	defer clus.Terminate(t)
 
 	cli := clus.RandClient()
-	resp, err := integration.ToGRPC(cli).Maintenance.Hash(context.Background(), &pb.HashRequest{})
+	resp, err := integration.ToGRPC(cli).Maintenance.Hash(t.Context(), &pb.HashRequest{})
 	if err != nil || resp.Hash == 0 {
 		t.Fatalf("couldn't hash (%v, hash %d)", err, resp.Hash)
 	}
@@ -1215,7 +1215,7 @@ func TestV3HashRestart(t *testing.T) {
 	waitForRestart(t, kvc)
 
 	cli = clus.RandClient()
-	resp, err = integration.ToGRPC(cli).Maintenance.Hash(context.Background(), &pb.HashRequest{})
+	resp, err = integration.ToGRPC(cli).Maintenance.Hash(t.Context(), &pb.HashRequest{})
 	if err != nil || resp.Hash == 0 {
 		t.Fatalf("couldn't hash (%v, hash %d)", err, resp.Hash)
 	}
@@ -1246,12 +1246,12 @@ func TestV3StorageQuotaAPI(t *testing.T) {
 
 	// test small put that fits in quota
 	smallbuf := make([]byte, 512)
-	_, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf})
+	_, err := kvc.Put(t.Context(), &pb.PutRequest{Key: key, Value: smallbuf})
 	require.NoError(t, err)
 
 	// test big put
 	bigbuf := make([]byte, quotasize)
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: key, Value: bigbuf})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: key, Value: bigbuf})
 	if !eqErrGRPC(err, rpctypes.ErrGRPCNoSpace) {
 		t.Fatalf("big put got %v, expected %v", err, rpctypes.ErrGRPCNoSpace)
 	}
@@ -1267,7 +1267,7 @@ func TestV3StorageQuotaAPI(t *testing.T) {
 	}
 	txnreq := &pb.TxnRequest{}
 	txnreq.Success = append(txnreq.Success, puttxn)
-	_, txnerr := kvc.Txn(context.TODO(), txnreq)
+	_, txnerr := kvc.Txn(t.Context(), txnreq)
 	if !eqErrGRPC(txnerr, rpctypes.ErrGRPCNoSpace) {
 		t.Fatalf("big txn got %v, expected %v", err, rpctypes.ErrGRPCNoSpace)
 	}
@@ -1499,14 +1499,14 @@ func TestV3RangeRequest(t *testing.T) {
 			for _, k := range tt.putKeys {
 				kvc := integration.ToGRPC(clus.RandClient()).KV
 				req := &pb.PutRequest{Key: []byte(k), Value: []byte("bar")}
-				if _, err := kvc.Put(context.TODO(), req); err != nil {
+				if _, err := kvc.Put(t.Context(), req); err != nil {
 					t.Fatalf("#%d: couldn't put key (%v)", i, err)
 				}
 			}
 
 			for j, req := range tt.reqs {
 				kvc := integration.ToGRPC(clus.RandClient()).KV
-				resp, err := kvc.Range(context.TODO(), &req)
+				resp, err := kvc.Range(t.Context(), &req)
 				if err != nil {
 					t.Errorf("#%d.%d: Range error: %v", i, j, err)
 					continue
@@ -1558,7 +1558,7 @@ func TestTLSGRPCRejectInsecureClient(t *testing.T) {
 
 	donec := make(chan error, 1)
 	go func() {
-		ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 		reqput := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 		_, perr := integration.ToGRPC(client).KV.Put(ctx, reqput)
 		cancel()
@@ -1603,7 +1603,7 @@ func TestTLSGRPCAcceptSecureAll(t *testing.T) {
 	defer client.Close()
 
 	reqput := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
-	if _, err := integration.ToGRPC(client).KV.Put(context.TODO(), reqput); err != nil {
+	if _, err := integration.ToGRPC(client).KV.Put(t.Context(), reqput); err != nil {
 		t.Fatalf("unexpected error on put over tls (%v)", err)
 	}
 }
@@ -1789,7 +1789,7 @@ func TestGRPCRequireLeader(t *testing.T) {
 	time.Sleep(time.Duration(3*integration.ElectionTicks) * config.TickDuration)
 
 	md := metadata.Pairs(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
-	ctx := metadata.NewOutgoingContext(context.Background(), md)
+	ctx := metadata.NewOutgoingContext(t.Context(), md)
 	reqput := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 	if _, err := integration.ToGRPC(client).KV.Put(ctx, reqput); rpctypes.ErrorDesc(err) != rpctypes.ErrNoLeader.Error() {
 		t.Errorf("err = %v, want %v", err, rpctypes.ErrNoLeader)
@@ -1810,7 +1810,7 @@ func TestGRPCStreamRequireLeader(t *testing.T) {
 
 	wAPI := integration.ToGRPC(client).Watch
 	md := metadata.Pairs(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
-	ctx := metadata.NewOutgoingContext(context.Background(), md)
+	ctx := metadata.NewOutgoingContext(t.Context(), md)
 	wStream, err := wAPI.Watch(ctx)
 	if err != nil {
 		t.Fatalf("wAPI.Watch error: %v", err)
@@ -1877,7 +1877,7 @@ func TestV3LargeRequests(t *testing.T) {
 			defer clus.Terminate(t)
 			kvcli := integration.ToGRPC(clus.Client(0)).KV
 			reqput := &pb.PutRequest{Key: []byte("foo"), Value: make([]byte, test.valueSize)}
-			_, err := kvcli.Put(context.TODO(), reqput)
+			_, err := kvcli.Put(t.Context(), reqput)
 			if !eqErrGRPC(err, test.expectError) {
 				t.Errorf("#%d: expected error %v, got %v", i, test.expectError, err)
 			}
@@ -1886,7 +1886,7 @@ func TestV3LargeRequests(t *testing.T) {
 			if test.expectError == nil {
 				reqget := &pb.RangeRequest{Key: []byte("foo")}
 				// limit receive call size with original value + gRPC overhead bytes
-				_, err = kvcli.Range(context.TODO(), reqget, grpc.MaxCallRecvMsgSize(test.valueSize+512*1024))
+				_, err = kvcli.Range(t.Context(), reqget, grpc.MaxCallRecvMsgSize(test.valueSize+512*1024))
 				if err != nil {
 					t.Errorf("#%d: range expected no error, got %v", i, err)
 				}
@@ -1938,7 +1938,7 @@ func TestV3AdditionalGRPCOptions(t *testing.T) {
 			defer clus.Terminate(t)
 			kvcli := integration.ToGRPC(clus.Client(0)).KV
 			reqput := &pb.PutRequest{Key: []byte("foo"), Value: make([]byte, test.valueSize)}
-			if _, err := kvcli.Put(context.TODO(), reqput); err != nil {
+			if _, err := kvcli.Put(t.Context(), reqput); err != nil {
 				var etcdErr rpctypes.EtcdError
 				if errors.As(err, &etcdErr) {
 					if err.Error() != status.Convert(test.expectError).Message() {
@@ -1952,7 +1952,7 @@ func TestV3AdditionalGRPCOptions(t *testing.T) {
 			if test.expectError == nil {
 				reqget := &pb.RangeRequest{Key: []byte("foo")}
 				// limit receive call size with original value + gRPC overhead bytes
-				_, err := kvcli.Range(context.TODO(), reqget, grpc.MaxCallRecvMsgSize(test.valueSize+512*1024))
+				_, err := kvcli.Range(t.Context(), reqget, grpc.MaxCallRecvMsgSize(test.valueSize+512*1024))
 				if err != nil {
 					t.Errorf("range expected no error, got %v", err)
 				}
@@ -1974,7 +1974,7 @@ func waitForRestart(t *testing.T, kvc pb.KVClient) {
 	// TODO: Remove retry loop once the new grpc load balancer provides retry.
 	var err error
 	for i := 0; i < 10; i++ {
-		if _, err = kvc.Range(context.TODO(), req, grpc.WaitForReady(true)); err != nil {
+		if _, err = kvc.Range(t.Context(), req, grpc.WaitForReady(true)); err != nil {
 			if status, ok := status.FromError(err); ok && status.Code() == codes.Unavailable {
 				time.Sleep(time.Millisecond * 250)
 			} else {
diff --git a/tests/integration/v3_kv_test.go b/tests/integration/v3_kv_test.go
index 1a5e7ce5e..7b6b9644e 100644
--- a/tests/integration/v3_kv_test.go
+++ b/tests/integration/v3_kv_test.go
@@ -15,7 +15,6 @@
 package integration
 
 import (
-	"context"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -34,15 +33,15 @@ func TestKVWithEmptyValue(t *testing.T) {
 
 	client := clus.RandClient()
 
-	_, err := client.Put(context.Background(), "my-namespace/foobar", "data")
+	_, err := client.Put(t.Context(), "my-namespace/foobar", "data")
 	require.NoError(t, err)
-	_, err = client.Put(context.Background(), "my-namespace/foobar1", "data")
+	_, err = client.Put(t.Context(), "my-namespace/foobar1", "data")
 	require.NoError(t, err)
-	_, err = client.Put(context.Background(), "namespace/foobar1", "data")
+	_, err = client.Put(t.Context(), "namespace/foobar1", "data")
 	require.NoError(t, err)
 
 	// Range over all keys.
-	resp, err := client.Get(context.Background(), "", clientv3.WithFromKey())
+	resp, err := client.Get(t.Context(), "", clientv3.WithFromKey())
 	require.NoError(t, err)
 	for _, kv := range resp.Kvs {
 		t.Log(string(kv.Key), "=", string(kv.Value))
@@ -50,18 +49,18 @@ func TestKVWithEmptyValue(t *testing.T) {
 
 	// Range over all keys in a namespace.
 	client.KV = namespace.NewKV(client.KV, "my-namespace/")
-	resp, err = client.Get(context.Background(), "", clientv3.WithFromKey())
+	resp, err = client.Get(t.Context(), "", clientv3.WithFromKey())
 	require.NoError(t, err)
 	for _, kv := range resp.Kvs {
 		t.Log(string(kv.Key), "=", string(kv.Value))
 	}
 
 	// Remove all keys without WithFromKey/WithPrefix func
-	_, err = client.Delete(context.Background(), "")
+	_, err = client.Delete(t.Context(), "")
 	// fatal error duo to without WithFromKey/WithPrefix func called.
 	require.Error(t, err)
 
-	respDel, err := client.Delete(context.Background(), "", clientv3.WithFromKey())
+	respDel, err := client.Delete(t.Context(), "", clientv3.WithFromKey())
 	// fatal error duo to with WithFromKey/WithPrefix func called.
 	require.NoError(t, err)
 	t.Logf("delete keys:%d", respDel.Deleted)
diff --git a/tests/integration/v3_leadership_test.go b/tests/integration/v3_leadership_test.go
index 148c2edf8..9e996e787 100644
--- a/tests/integration/v3_leadership_test.go
+++ b/tests/integration/v3_leadership_test.go
@@ -63,7 +63,7 @@ func testMoveLeader(t *testing.T, auto bool) {
 		require.NoError(t, err)
 	} else {
 		mvc := integration.ToGRPC(clus.Client(oldLeadIdx)).Maintenance
-		_, err := mvc.MoveLeader(context.TODO(), &pb.MoveLeaderRequest{TargetID: target})
+		_, err := mvc.MoveLeader(t.Context(), &pb.MoveLeaderRequest{TargetID: target})
 		require.NoError(t, err)
 	}
 
@@ -108,7 +108,7 @@ func TestMoveLeaderError(t *testing.T) {
 	target := uint64(clus.Members[(oldLeadIdx+2)%3].Server.MemberID())
 
 	mvc := integration.ToGRPC(clus.Client(followerIdx)).Maintenance
-	_, err := mvc.MoveLeader(context.TODO(), &pb.MoveLeaderRequest{TargetID: target})
+	_, err := mvc.MoveLeader(t.Context(), &pb.MoveLeaderRequest{TargetID: target})
 	if !eqErrGRPC(err, rpctypes.ErrGRPCNotLeader) {
 		t.Errorf("err = %v, want %v", err, rpctypes.ErrGRPCNotLeader)
 	}
@@ -136,7 +136,7 @@ func TestMoveLeaderToLearnerError(t *testing.T) {
 	learnerID := learners[0].ID
 	leaderIdx := clus.WaitLeader(t)
 	cli := clus.Client(leaderIdx)
-	_, err = cli.MoveLeader(context.Background(), learnerID)
+	_, err = cli.MoveLeader(t.Context(), learnerID)
 	if err == nil {
 		t.Fatalf("expecting leader transfer to learner to fail, got no error")
 	}
@@ -183,7 +183,7 @@ func TestTransferLeadershipWithLearner(t *testing.T) {
 
 func TestFirstCommitNotification(t *testing.T) {
 	integration.BeforeTest(t)
-	ctx := context.Background()
+	ctx := t.Context()
 	clusterSize := 3
 	cluster := integration.NewCluster(t, &integration.ClusterConfig{Size: clusterSize})
 	defer cluster.Terminate(t)
@@ -199,7 +199,7 @@ func TestFirstCommitNotification(t *testing.T) {
 		notifiers[i] = clusterMember.Server.FirstCommitInTermNotify()
 	}
 
-	_, err := oldLeaderClient.MoveLeader(context.Background(), newLeaderID)
+	_, err := oldLeaderClient.MoveLeader(t.Context(), newLeaderID)
 	if err != nil {
 		t.Errorf("got error during leadership transfer: %v", err)
 	}
diff --git a/tests/integration/v3_lease_test.go b/tests/integration/v3_lease_test.go
index 532e94f31..61cdb3d3e 100644
--- a/tests/integration/v3_lease_test.go
+++ b/tests/integration/v3_lease_test.go
@@ -48,7 +48,7 @@ func TestV3LeasePromote(t *testing.T) {
 	defer clus.Terminate(t)
 
 	// create lease
-	lresp, err := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 3})
+	lresp, err := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 3})
 	ttl := time.Duration(lresp.TTL) * time.Second
 	afterGrant := time.Now()
 	require.NoError(t, err)
@@ -103,7 +103,7 @@ func TestV3LeaseRevoke(t *testing.T) {
 	integration.BeforeTest(t)
 	testLeaseRemoveLeasedKey(t, func(clus *integration.Cluster, leaseID int64) error {
 		lc := integration.ToGRPC(clus.RandClient()).Lease
-		_, err := lc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: leaseID})
+		_, err := lc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: leaseID})
 		return err
 	})
 }
@@ -116,7 +116,7 @@ func TestV3LeaseGrantByID(t *testing.T) {
 
 	// create fixed lease
 	lresp, err := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(
-		context.TODO(),
+		t.Context(),
 		&pb.LeaseGrantRequest{ID: 1, TTL: 1})
 	if err != nil {
 		t.Errorf("could not create lease 1 (%v)", err)
@@ -127,7 +127,7 @@ func TestV3LeaseGrantByID(t *testing.T) {
 
 	// create duplicate fixed lease
 	_, err = integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(
-		context.TODO(),
+		t.Context(),
 		&pb.LeaseGrantRequest{ID: 1, TTL: 1})
 	if !eqErrGRPC(err, rpctypes.ErrGRPCLeaseExist) {
 		t.Error(err)
@@ -135,7 +135,7 @@ func TestV3LeaseGrantByID(t *testing.T) {
 
 	// create fresh fixed lease
 	lresp, err = integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(
-		context.TODO(),
+		t.Context(),
 		&pb.LeaseGrantRequest{ID: 2, TTL: 1})
 	if err != nil {
 		t.Errorf("could not create lease 2 (%v)", err)
@@ -179,7 +179,7 @@ func TestV3LeaseNegativeID(t *testing.T) {
 			clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 			defer clus.Terminate(t)
 
-			ctx, cancel := context.WithCancel(context.Background())
+			ctx, cancel := context.WithCancel(t.Context())
 			defer cancel()
 			cc := clus.RandClient()
 			lresp, err := integration.ToGRPC(cc).Lease.LeaseGrant(ctx, &pb.LeaseGrantRequest{ID: tc.leaseID, TTL: 300})
@@ -233,7 +233,7 @@ func TestV3LeaseExpire(t *testing.T) {
 	testLeaseRemoveLeasedKey(t, func(clus *integration.Cluster, leaseID int64) error {
 		// let lease lapse; wait for deleted key
 
-		ctx, cancel := context.WithCancel(context.Background())
+		ctx, cancel := context.WithCancel(t.Context())
 		defer cancel()
 		wStream, err := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 		if err != nil {
@@ -287,7 +287,7 @@ func TestV3LeaseKeepAlive(t *testing.T) {
 	testLeaseRemoveLeasedKey(t, func(clus *integration.Cluster, leaseID int64) error {
 		lc := integration.ToGRPC(clus.RandClient()).Lease
 		lreq := &pb.LeaseKeepAliveRequest{ID: leaseID}
-		ctx, cancel := context.WithCancel(context.Background())
+		ctx, cancel := context.WithCancel(t.Context())
 		defer cancel()
 		lac, err := lc.LeaseKeepAlive(ctx)
 		if err != nil {
@@ -309,7 +309,7 @@ func TestV3LeaseKeepAlive(t *testing.T) {
 			}
 			time.Sleep(time.Duration(lresp.TTL/2) * time.Second)
 		}
-		_, err = lc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: leaseID})
+		_, err = lc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: leaseID})
 		return err
 	})
 }
@@ -375,7 +375,7 @@ func TestV3LeaseCheckpoint(t *testing.T) {
 			defer clus.Terminate(t)
 
 			// create lease
-			ctx, cancel := context.WithCancel(context.Background())
+			ctx, cancel := context.WithCancel(t.Context())
 			defer cancel()
 			c := integration.ToGRPC(clus.RandClient())
 			lresp, err := c.Lease.LeaseGrant(ctx, &pb.LeaseGrantRequest{TTL: int64(tc.ttl.Seconds())})
@@ -428,7 +428,7 @@ func TestV3LeaseExists(t *testing.T) {
 	defer clus.Terminate(t)
 
 	// create lease
-	ctx0, cancel0 := context.WithCancel(context.Background())
+	ctx0, cancel0 := context.WithCancel(t.Context())
 	defer cancel0()
 	lresp, err := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(
 		ctx0,
@@ -447,7 +447,7 @@ func TestV3LeaseLeases(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx0, cancel0 := context.WithCancel(context.Background())
+	ctx0, cancel0 := context.WithCancel(t.Context())
 	defer cancel0()
 
 	// create leases
@@ -462,7 +462,7 @@ func TestV3LeaseLeases(t *testing.T) {
 	}
 
 	lresp, err := integration.ToGRPC(clus.RandClient()).Lease.LeaseLeases(
-		context.Background(),
+		t.Context(),
 		&pb.LeaseLeasesRequest{})
 	require.NoError(t, err)
 	for i := range lresp.Leases {
@@ -505,7 +505,7 @@ func testLeaseStress(t *testing.T, stresser func(context.Context, pb.LeaseClient
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	errc := make(chan error)
 
@@ -583,7 +583,7 @@ func TestV3PutOnNonExistLease(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	badLeaseID := int64(0x12345678)
@@ -601,14 +601,14 @@ func TestV3GetNonExistLease(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 	lc := integration.ToGRPC(clus.RandClient()).Lease
 	lresp, err := lc.LeaseGrant(ctx, &pb.LeaseGrantRequest{TTL: 10})
 	if err != nil {
 		t.Errorf("failed to create lease %v", err)
 	}
-	_, err = lc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: lresp.ID})
+	_, err = lc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: lresp.ID})
 	require.NoError(t, err)
 
 	leaseTTLr := &pb.LeaseTimeToLiveRequest{
@@ -639,7 +639,7 @@ func TestV3LeaseSwitch(t *testing.T) {
 	key := "foo"
 
 	// create lease
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 	lresp1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(ctx, &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err1)
@@ -658,7 +658,7 @@ func TestV3LeaseSwitch(t *testing.T) {
 	_, err = integration.ToGRPC(clus.RandClient()).Lease.LeaseRevoke(ctx, &pb.LeaseRevokeRequest{ID: lresp1.ID})
 	require.NoError(t, err)
 	rreq := &pb.RangeRequest{Key: []byte("foo")}
-	rresp, err := integration.ToGRPC(clus.RandClient()).KV.Range(context.TODO(), rreq)
+	rresp, err := integration.ToGRPC(clus.RandClient()).KV.Range(t.Context(), rreq)
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 1 {
 		t.Fatalf("unexpect removal of key")
@@ -667,7 +667,7 @@ func TestV3LeaseSwitch(t *testing.T) {
 	// revoke lease2 should remove key
 	_, err = integration.ToGRPC(clus.RandClient()).Lease.LeaseRevoke(ctx, &pb.LeaseRevokeRequest{ID: lresp2.ID})
 	require.NoError(t, err)
-	rresp, err = integration.ToGRPC(clus.RandClient()).KV.Range(context.TODO(), rreq)
+	rresp, err = integration.ToGRPC(clus.RandClient()).KV.Range(t.Context(), rreq)
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 0 {
 		t.Fatalf("lease removed but key remains")
@@ -688,7 +688,7 @@ func TestV3LeaseFailover(t *testing.T) {
 	lc := integration.ToGRPC(clus.Client(toIsolate)).Lease
 
 	// create lease
-	lresp, err := lc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 5})
+	lresp, err := lc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 5})
 	require.NoError(t, err)
 	require.Empty(t, lresp.Error)
 
@@ -698,7 +698,7 @@ func TestV3LeaseFailover(t *testing.T) {
 	lreq := &pb.LeaseKeepAliveRequest{ID: lresp.ID}
 
 	md := metadata.Pairs(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
-	mctx := metadata.NewOutgoingContext(context.Background(), md)
+	mctx := metadata.NewOutgoingContext(t.Context(), md)
 	ctx, cancel := context.WithCancel(mctx)
 	defer cancel()
 	lac, err := lc.LeaseKeepAlive(ctx)
@@ -743,7 +743,7 @@ func TestV3LeaseRequireLeader(t *testing.T) {
 	clus.Members[2].Stop(t)
 
 	md := metadata.Pairs(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
-	mctx := metadata.NewOutgoingContext(context.Background(), md)
+	mctx := metadata.NewOutgoingContext(t.Context(), md)
 	ctx, cancel := context.WithCancel(mctx)
 	defer cancel()
 	lac, err := lc.LeaseKeepAlive(ctx)
@@ -779,10 +779,10 @@ func TestV3LeaseRecoverAndRevoke(t *testing.T) {
 	kvc := integration.ToGRPC(clus.Client(0)).KV
 	lsc := integration.ToGRPC(clus.Client(0)).Lease
 
-	lresp, err := lsc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
+	lresp, err := lsc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
 	require.NoError(t, err)
 	require.Empty(t, lresp.Error)
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
 	require.NoError(t, err)
 
 	// restart server and ensure lease still exists
@@ -799,9 +799,9 @@ func TestV3LeaseRecoverAndRevoke(t *testing.T) {
 	defer nc.Close()
 
 	// revoke should delete the key
-	_, err = lsc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: lresp.ID})
+	_, err = lsc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: lresp.ID})
 	require.NoError(t, err)
-	rresp, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	rresp, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 0 {
 		t.Fatalf("lease removed but key remains")
@@ -818,14 +818,14 @@ func TestV3LeaseRevokeAndRecover(t *testing.T) {
 	kvc := integration.ToGRPC(clus.Client(0)).KV
 	lsc := integration.ToGRPC(clus.Client(0)).Lease
 
-	lresp, err := lsc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
+	lresp, err := lsc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
 	require.NoError(t, err)
 	require.Empty(t, lresp.Error)
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
 	require.NoError(t, err)
 
 	// revoke should delete the key
-	_, err = lsc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: lresp.ID})
+	_, err = lsc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: lresp.ID})
 	require.NoError(t, err)
 
 	// restart server and ensure revoked key doesn't exist
@@ -840,7 +840,7 @@ func TestV3LeaseRevokeAndRecover(t *testing.T) {
 	kvc = integration.ToGRPC(nc).KV
 	defer nc.Close()
 
-	rresp, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	rresp, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 0 {
 		t.Fatalf("lease removed but key remains")
@@ -858,14 +858,14 @@ func TestV3LeaseRecoverKeyWithDetachedLease(t *testing.T) {
 	kvc := integration.ToGRPC(clus.Client(0)).KV
 	lsc := integration.ToGRPC(clus.Client(0)).Lease
 
-	lresp, err := lsc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
+	lresp, err := lsc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
 	require.NoError(t, err)
 	require.Empty(t, lresp.Error)
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
 	require.NoError(t, err)
 
 	// overwrite lease with none
-	_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
+	_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
 	require.NoError(t, err)
 
 	// restart server and ensure lease still exists
@@ -882,9 +882,9 @@ func TestV3LeaseRecoverKeyWithDetachedLease(t *testing.T) {
 	defer nc.Close()
 
 	// revoke the detached lease
-	_, err = lsc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: lresp.ID})
+	_, err = lsc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: lresp.ID})
 	require.NoError(t, err)
-	rresp, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	rresp, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 1 {
 		t.Fatalf("only detached lease removed, key should remain")
@@ -902,12 +902,12 @@ func TestV3LeaseRecoverKeyWithMutipleLease(t *testing.T) {
 
 	var leaseIDs []int64
 	for i := 0; i < 2; i++ {
-		lresp, err := lsc.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
+		lresp, err := lsc.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: fiveMinTTL})
 		require.NoError(t, err)
 		require.Empty(t, lresp.Error)
 		leaseIDs = append(leaseIDs, lresp.ID)
 
-		_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
+		_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar"), Lease: lresp.ID})
 		require.NoError(t, err)
 	}
 
@@ -930,19 +930,19 @@ func TestV3LeaseRecoverKeyWithMutipleLease(t *testing.T) {
 	defer nc.Close()
 
 	// revoke the old lease
-	_, err = lsc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: leaseIDs[0]})
+	_, err = lsc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: leaseIDs[0]})
 	require.NoError(t, err)
 	// key should still exist
-	rresp, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	rresp, err := kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 1 {
 		t.Fatalf("only detached lease removed, key should remain")
 	}
 
 	// revoke the latest lease
-	_, err = lsc.LeaseRevoke(context.TODO(), &pb.LeaseRevokeRequest{ID: leaseIDs[1]})
+	_, err = lsc.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: leaseIDs[1]})
 	require.NoError(t, err)
-	rresp, err = kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
+	rresp, err = kvc.Range(t.Context(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 0 {
 		t.Fatalf("lease removed but key remains")
@@ -969,7 +969,7 @@ func testV3LeaseTimeToLiveWithLeaderChanged(t *testing.T, fpName string) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	oldLeadIdx := clus.WaitLeader(t)
@@ -1055,7 +1055,7 @@ func testLeaseRemoveLeasedKey(t *testing.T, act func(*integration.Cluster, int64
 
 	// confirm no key
 	rreq := &pb.RangeRequest{Key: []byte("foo")}
-	rresp, err := integration.ToGRPC(clus.RandClient()).KV.Range(context.TODO(), rreq)
+	rresp, err := integration.ToGRPC(clus.RandClient()).KV.Range(t.Context(), rreq)
 	require.NoError(t, err)
 	if len(rresp.Kvs) != 0 {
 		t.Fatalf("lease removed but key remains")
@@ -1065,9 +1065,9 @@ func testLeaseRemoveLeasedKey(t *testing.T, act func(*integration.Cluster, int64
 func leaseExist(t *testing.T, clus *integration.Cluster, leaseID int64) bool {
 	l := integration.ToGRPC(clus.RandClient()).Lease
 
-	_, err := l.LeaseGrant(context.Background(), &pb.LeaseGrantRequest{ID: leaseID, TTL: 5})
+	_, err := l.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{ID: leaseID, TTL: 5})
 	if err == nil {
-		_, err = l.LeaseRevoke(context.Background(), &pb.LeaseRevokeRequest{ID: leaseID})
+		_, err = l.LeaseRevoke(t.Context(), &pb.LeaseRevokeRequest{ID: leaseID})
 		if err != nil {
 			t.Fatalf("failed to check lease %v", err)
 		}
diff --git a/tests/integration/v3_stm_test.go b/tests/integration/v3_stm_test.go
index a7c0781f0..a5df7fae0 100644
--- a/tests/integration/v3_stm_test.go
+++ b/tests/integration/v3_stm_test.go
@@ -40,7 +40,7 @@ func TestSTMConflict(t *testing.T) {
 	keys := make([]string, 5)
 	for i := 0; i < len(keys); i++ {
 		keys[i] = fmt.Sprintf("foo-%d", i)
-		if _, err := etcdc.Put(context.TODO(), keys[i], "100"); err != nil {
+		if _, err := etcdc.Put(t.Context(), keys[i], "100"); err != nil {
 			t.Fatalf("could not make key (%v)", err)
 		}
 	}
@@ -90,7 +90,7 @@ func TestSTMConflict(t *testing.T) {
 	// ensure sum matches initial sum
 	sum := 0
 	for _, oldkey := range keys {
-		rk, err := etcdc.Get(context.TODO(), oldkey)
+		rk, err := etcdc.Get(t.Context(), oldkey)
 		if err != nil {
 			t.Fatalf("couldn't fetch key %s (%v)", oldkey, err)
 		}
@@ -120,7 +120,7 @@ func TestSTMPutNewKey(t *testing.T) {
 		t.Fatalf("error on stm txn (%v)", err)
 	}
 
-	resp, err := etcdc.Get(context.TODO(), "foo")
+	resp, err := etcdc.Get(t.Context(), "foo")
 	if err != nil {
 		t.Fatalf("error fetching key (%v)", err)
 	}
@@ -137,7 +137,7 @@ func TestSTMAbort(t *testing.T) {
 	defer clus.Terminate(t)
 
 	etcdc := clus.RandClient()
-	ctx, cancel := context.WithCancel(context.TODO())
+	ctx, cancel := context.WithCancel(t.Context())
 	applyf := func(stm concurrency.STM) error {
 		stm.Put("foo", "baz")
 		cancel()
@@ -151,7 +151,7 @@ func TestSTMAbort(t *testing.T) {
 		t.Fatalf("no error on stm txn")
 	}
 
-	resp, err := etcdc.Get(context.TODO(), "foo")
+	resp, err := etcdc.Get(t.Context(), "foo")
 	if err != nil {
 		t.Fatalf("error fetching key (%v)", err)
 	}
@@ -185,7 +185,7 @@ func TestSTMSerialize(t *testing.T) {
 			for _, k := range keys {
 				ops = append(ops, v3.OpPut(k, s))
 			}
-			if _, err := etcdc.Txn(context.TODO()).Then(ops...).Commit(); err != nil {
+			if _, err := etcdc.Txn(t.Context()).Then(ops...).Commit(); err != nil {
 				t.Errorf("couldn't put keys (%v)", err)
 			}
 			updatec <- struct{}{}
@@ -231,12 +231,12 @@ func TestSTMApplyOnConcurrentDeletion(t *testing.T) {
 	defer clus.Terminate(t)
 
 	etcdc := clus.RandClient()
-	_, err := etcdc.Put(context.TODO(), "foo", "bar")
+	_, err := etcdc.Put(t.Context(), "foo", "bar")
 	require.NoError(t, err)
 	donec, readyc := make(chan struct{}), make(chan struct{})
 	go func() {
 		<-readyc
-		_, derr := etcdc.Delete(context.TODO(), "foo")
+		_, derr := etcdc.Delete(t.Context(), "foo")
 		assert.NoError(t, derr)
 		close(donec)
 	}()
@@ -262,7 +262,7 @@ func TestSTMApplyOnConcurrentDeletion(t *testing.T) {
 		t.Fatalf("STM apply expected to run twice, got %d", try)
 	}
 
-	resp, err := etcdc.Get(context.TODO(), "foo2")
+	resp, err := etcdc.Get(t.Context(), "foo2")
 	if err != nil {
 		t.Fatalf("error fetching key (%v)", err)
 	}
@@ -279,7 +279,7 @@ func TestSTMSerializableSnapshotPut(t *testing.T) {
 
 	cli := clus.Client(0)
 	// key with lower create/mod revision than keys being updated
-	_, err := cli.Put(context.TODO(), "a", "0")
+	_, err := cli.Put(t.Context(), "a", "0")
 	require.NoError(t, err)
 
 	tries := 0
@@ -299,7 +299,7 @@ func TestSTMSerializableSnapshotPut(t *testing.T) {
 	_, err = concurrency.NewSTM(cli, applyf, iso)
 	require.NoError(t, err)
 
-	resp, err := cli.Get(context.TODO(), "b")
+	resp, err := cli.Get(t.Context(), "b")
 	require.NoError(t, err)
 	if resp.Kvs[0].Version != 2 {
 		t.Fatalf("bad version. got %+v, expected version 2", resp)
diff --git a/tests/integration/v3_watch_restore_test.go b/tests/integration/v3_watch_restore_test.go
index 9d98cb8dc..7ce49c000 100644
--- a/tests/integration/v3_watch_restore_test.go
+++ b/tests/integration/v3_watch_restore_test.go
@@ -64,7 +64,7 @@ func TestV3WatchRestoreSnapshotUnsync(t *testing.T) {
 	defer clus.Terminate(t)
 
 	// spawn a watcher before shutdown, and put it in synced watcher
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, errW := integration.ToGRPC(clus.Client(0)).Watch.Watch(ctx)
 	require.NoError(t, errW)
@@ -92,7 +92,7 @@ func TestV3WatchRestoreSnapshotUnsync(t *testing.T) {
 
 	// to trigger snapshot from the leader to the stopped follower
 	for i := 0; i < 15; i++ {
-		_, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
+		_, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
 		if err != nil {
 			t.Errorf("#%d: couldn't put key (%v)", i, err)
 		}
@@ -177,7 +177,7 @@ func TestV3WatchRestoreSnapshotUnsync(t *testing.T) {
 }
 
 func expectMemberLog(t *testing.T, m *integration.Member, timeout time.Duration, s string, count int) {
-	ctx, cancel := context.WithTimeout(context.TODO(), timeout)
+	ctx, cancel := context.WithTimeout(t.Context(), timeout)
 	defer cancel()
 
 	lines, err := m.LogObserver.Expect(ctx, s, count)
diff --git a/tests/integration/v3_watch_test.go b/tests/integration/v3_watch_test.go
index 21140da09..362b08dcc 100644
--- a/tests/integration/v3_watch_test.go
+++ b/tests/integration/v3_watch_test.go
@@ -233,7 +233,7 @@ func TestV3WatchFromCurrentRevision(t *testing.T) {
 			defer clus.Terminate(t)
 
 			wAPI := integration.ToGRPC(clus.RandClient()).Watch
-			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 			defer cancel()
 			wStream, err := wAPI.Watch(ctx)
 			if err != nil {
@@ -268,7 +268,7 @@ func TestV3WatchFromCurrentRevision(t *testing.T) {
 				for _, k := range tt.putKeys {
 					kvc := integration.ToGRPC(clus.RandClient()).KV
 					req := &pb.PutRequest{Key: []byte(k), Value: []byte("bar")}
-					if _, err := kvc.Put(context.TODO(), req); err != nil {
+					if _, err := kvc.Put(t.Context(), req); err != nil {
 						t.Errorf("#%d: couldn't put key (%v)", i, err)
 					}
 				}
@@ -320,7 +320,7 @@ func TestV3WatchFutureRevision(t *testing.T) {
 	defer clus.Terminate(t)
 
 	wAPI := integration.ToGRPC(clus.RandClient()).Watch
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, err := wAPI.Watch(ctx)
 	if err != nil {
@@ -349,7 +349,7 @@ func TestV3WatchFutureRevision(t *testing.T) {
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 	for {
 		req := &pb.PutRequest{Key: wkey, Value: []byte("bar")}
-		resp, rerr := kvc.Put(context.TODO(), req)
+		resp, rerr := kvc.Put(t.Context(), req)
 		if rerr != nil {
 			t.Fatalf("couldn't put key (%v)", rerr)
 		}
@@ -382,7 +382,7 @@ func TestV3WatchWrongRange(t *testing.T) {
 	defer clus.Terminate(t)
 
 	wAPI := integration.ToGRPC(clus.RandClient()).Watch
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, err := wAPI.Watch(ctx)
 	if err != nil {
@@ -436,7 +436,7 @@ func testV3WatchCancel(t *testing.T, startRev int64) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, errW := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 	if errW != nil {
@@ -478,7 +478,7 @@ func testV3WatchCancel(t *testing.T, startRev int64) {
 	}
 
 	kvc := integration.ToGRPC(clus.RandClient()).KV
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}); err != nil {
 		t.Errorf("couldn't put key (%v)", err)
 	}
 
@@ -496,7 +496,7 @@ func TestV3WatchCurrentPutOverlap(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, wErr := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 	if wErr != nil {
@@ -513,7 +513,7 @@ func TestV3WatchCurrentPutOverlap(t *testing.T) {
 			defer wg.Done()
 			kvc := integration.ToGRPC(clus.RandClient()).KV
 			req := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
-			if _, err := kvc.Put(context.TODO(), req); err != nil {
+			if _, err := kvc.Put(t.Context(), req); err != nil {
 				t.Errorf("couldn't put key (%v)", err)
 			}
 		}()
@@ -582,7 +582,7 @@ func TestV3WatchEmptyKey(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	ws, werr := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
@@ -599,7 +599,7 @@ func TestV3WatchEmptyKey(t *testing.T) {
 	// put a key with empty value
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 	preq := &pb.PutRequest{Key: []byte("foo")}
-	_, err = kvc.Put(context.TODO(), preq)
+	_, err = kvc.Put(t.Context(), preq)
 	require.NoError(t, err)
 
 	// check received PUT
@@ -636,7 +636,7 @@ func testV3WatchMultipleWatchers(t *testing.T, startRev int64) {
 
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, errW := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 	if errW != nil {
@@ -676,7 +676,7 @@ func testV3WatchMultipleWatchers(t *testing.T, startRev int64) {
 		ids[wresp.WatchId] = struct{}{}
 	}
 
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
 
@@ -704,7 +704,7 @@ func testV3WatchMultipleWatchers(t *testing.T, startRev int64) {
 	}
 
 	// now put one key that has only one matching watcher
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("fo"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("fo"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
 	wresp, err := wStream.Recv()
@@ -740,7 +740,7 @@ func testV3WatchMultipleEventsTxn(t *testing.T, startRev int64) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, wErr := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 	if wErr != nil {
@@ -771,7 +771,7 @@ func testV3WatchMultipleEventsTxn(t *testing.T, startRev int64) {
 		txn.Success = append(txn.Success, ru)
 	}
 
-	tresp, err := kvc.Txn(context.Background(), &txn)
+	tresp, err := kvc.Txn(t.Context(), &txn)
 	if err != nil {
 		t.Fatalf("kvc.Txn error: %v", err)
 	}
@@ -829,14 +829,14 @@ func TestV3WatchMultipleEventsPutUnsynced(t *testing.T) {
 
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo0"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo0"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo1"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo1"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, wErr := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 	if wErr != nil {
@@ -852,10 +852,10 @@ func TestV3WatchMultipleEventsPutUnsynced(t *testing.T) {
 		t.Fatalf("wStream.Send error: %v", err)
 	}
 
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo0"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo0"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo1"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo1"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
 
@@ -916,7 +916,7 @@ func TestV3WatchProgressOnMemberRestart(t *testing.T) {
 	defer clus.Terminate(t)
 
 	client := clus.RandClient()
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 
 	errC := make(chan error, 1)
@@ -1029,7 +1029,7 @@ func testV3WatchMultipleStreams(t *testing.T, startRev int64) {
 
 	streams := make([]pb.Watch_WatchClient, 5)
 	for i := range streams {
-		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 		defer cancel()
 		wStream, errW := wAPI.Watch(ctx)
 		if errW != nil {
@@ -1056,7 +1056,7 @@ func testV3WatchMultipleStreams(t *testing.T, startRev int64) {
 		}
 	}
 
-	if _, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}); err != nil {
+	if _, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}); err != nil {
 		t.Fatalf("couldn't put key (%v)", err)
 	}
 
@@ -1129,7 +1129,7 @@ func TestWatchWithProgressNotify(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 3})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	wStream, wErr := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
 	if wErr != nil {
@@ -1188,7 +1188,7 @@ func TestV3WatchClose(t *testing.T) {
 	wg.Add(100)
 	for i := 0; i < 100; i++ {
 		go func() {
-			ctx, cancel := context.WithCancel(context.TODO())
+			ctx, cancel := context.WithCancel(t.Context())
 			defer func() {
 				wg.Done()
 				cancel()
@@ -1219,7 +1219,7 @@ func TestV3WatchWithFilter(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	ws, werr := integration.ToGRPC(clus.RandClient()).Watch.Watch(ctx)
@@ -1247,7 +1247,7 @@ func TestV3WatchWithFilter(t *testing.T) {
 	// put a key with empty value
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 	preq := &pb.PutRequest{Key: []byte("foo")}
-	_, err = kvc.Put(context.TODO(), preq)
+	_, err = kvc.Put(t.Context(), preq)
 	require.NoError(t, err)
 
 	select {
@@ -1257,7 +1257,7 @@ func TestV3WatchWithFilter(t *testing.T) {
 	}
 
 	dreq := &pb.DeleteRangeRequest{Key: []byte("foo")}
-	_, err = kvc.DeleteRange(context.TODO(), dreq)
+	_, err = kvc.DeleteRange(t.Context(), dreq)
 	require.NoError(t, err)
 
 	select {
@@ -1281,7 +1281,7 @@ func TestV3WatchWithPrevKV(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	wctx, wcancel := context.WithCancel(context.Background())
+	wctx, wcancel := context.WithCancel(t.Context())
 	defer wcancel()
 
 	tests := []struct {
@@ -1299,7 +1299,7 @@ func TestV3WatchWithPrevKV(t *testing.T) {
 	}}
 	for i, tt := range tests {
 		kvc := integration.ToGRPC(clus.RandClient()).KV
-		_, err := kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte(tt.key), Value: []byte(tt.vals[0])})
+		_, err := kvc.Put(t.Context(), &pb.PutRequest{Key: []byte(tt.key), Value: []byte(tt.vals[0])})
 		require.NoError(t, err)
 
 		ws, werr := integration.ToGRPC(clus.RandClient()).Watch.Watch(wctx)
@@ -1317,7 +1317,7 @@ func TestV3WatchWithPrevKV(t *testing.T) {
 		_, err = ws.Recv()
 		require.NoError(t, err)
 
-		_, err = kvc.Put(context.TODO(), &pb.PutRequest{Key: []byte(tt.key), Value: []byte(tt.vals[1])})
+		_, err = kvc.Put(t.Context(), &pb.PutRequest{Key: []byte(tt.key), Value: []byte(tt.vals[1])})
 		require.NoError(t, err)
 
 		recv := make(chan *pb.WatchResponse, 1)
@@ -1351,7 +1351,7 @@ func TestV3WatchCancellation(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	cli := clus.RandClient()
@@ -1391,7 +1391,7 @@ func TestV3WatchCloseCancelRace(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	cli := clus.RandClient()
@@ -1436,7 +1436,7 @@ func TestV3WatchProgressWaitsForSync(t *testing.T) {
 	defer clus.Terminate(t)
 
 	client := clus.RandClient()
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 
 	// Write a couple values into key to make sure there's a
@@ -1499,7 +1499,7 @@ func TestV3WatchProgressWaitsForSyncNoEvents(t *testing.T) {
 	defer clus.Terminate(t)
 
 	client := clus.RandClient()
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer cancel()
 
 	resp, err := client.Put(ctx, "bar", "1")
@@ -1547,7 +1547,7 @@ func TestV3NoEventsLostOnCompact(t *testing.T) {
 	defer clus.Terminate(t)
 
 	client := clus.RandClient()
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	defer cancel()
 
 	// sendLoop throughput is rate-limited to 1 event per second
diff --git a/tests/integration/v3election_grpc_test.go b/tests/integration/v3election_grpc_test.go
index 25c7f846a..94e27e71d 100644
--- a/tests/integration/v3election_grpc_test.go
+++ b/tests/integration/v3election_grpc_test.go
@@ -15,7 +15,6 @@
 package integration
 
 import (
-	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -34,21 +33,21 @@ func TestV3ElectionCampaign(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	lease1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lease1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err1)
-	lease2, err2 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lease2, err2 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err2)
 
 	lc := integration.ToGRPC(clus.Client(0)).Election
 	req1 := &epb.CampaignRequest{Name: []byte("foo"), Lease: lease1.ID, Value: []byte("abc")}
-	l1, lerr1 := lc.Campaign(context.TODO(), req1)
+	l1, lerr1 := lc.Campaign(t.Context(), req1)
 	require.NoError(t, lerr1)
 
 	campaignc := make(chan struct{})
 	go func() {
 		defer close(campaignc)
 		req2 := &epb.CampaignRequest{Name: []byte("foo"), Lease: lease2.ID, Value: []byte("def")}
-		l2, lerr2 := lc.Campaign(context.TODO(), req2)
+		l2, lerr2 := lc.Campaign(t.Context(), req2)
 		if lerr2 != nil {
 			t.Error(lerr2)
 		}
@@ -63,7 +62,7 @@ func TestV3ElectionCampaign(t *testing.T) {
 		t.Fatalf("got leadership before resign")
 	}
 
-	_, uerr := lc.Resign(context.TODO(), &epb.ResignRequest{Leader: l1.Leader})
+	_, uerr := lc.Resign(t.Context(), &epb.ResignRequest{Leader: l1.Leader})
 	require.NoError(t, uerr)
 
 	select {
@@ -72,7 +71,7 @@ func TestV3ElectionCampaign(t *testing.T) {
 	case <-campaignc:
 	}
 
-	lval, lverr := lc.Leader(context.TODO(), &epb.LeaderRequest{Name: []byte("foo")})
+	lval, lverr := lc.Leader(t.Context(), &epb.LeaderRequest{Name: []byte("foo")})
 	require.NoError(t, lverr)
 
 	if string(lval.Kv.Value) != "def" {
@@ -93,7 +92,7 @@ func TestV3ElectionObserve(t *testing.T) {
 	observec := make(chan struct{}, 1)
 	go func() {
 		defer close(observec)
-		s, err := lc.Observe(context.Background(), &epb.LeaderRequest{Name: []byte("foo")})
+		s, err := lc.Observe(t.Context(), &epb.LeaderRequest{Name: []byte("foo")})
 		observec <- struct{}{}
 		if err != nil {
 			t.Error(err)
@@ -119,9 +118,9 @@ func TestV3ElectionObserve(t *testing.T) {
 		t.Fatalf("observe stream took too long to start")
 	}
 
-	lease1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lease1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err1)
-	c1, cerr1 := lc.Campaign(context.TODO(), &epb.CampaignRequest{Name: []byte("foo"), Lease: lease1.ID, Value: []byte("0")})
+	c1, cerr1 := lc.Campaign(t.Context(), &epb.CampaignRequest{Name: []byte("foo"), Lease: lease1.ID, Value: []byte("0")})
 	require.NoError(t, cerr1)
 
 	// overlap other leader so it waits on resign
@@ -129,18 +128,18 @@ func TestV3ElectionObserve(t *testing.T) {
 	go func() {
 		defer close(leader2c)
 
-		lease2, err2 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+		lease2, err2 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 		if err2 != nil {
 			t.Error(err2)
 		}
-		c2, cerr2 := lc.Campaign(context.TODO(), &epb.CampaignRequest{Name: []byte("foo"), Lease: lease2.ID, Value: []byte("5")})
+		c2, cerr2 := lc.Campaign(t.Context(), &epb.CampaignRequest{Name: []byte("foo"), Lease: lease2.ID, Value: []byte("5")})
 		if cerr2 != nil {
 			t.Error(cerr2)
 		}
 		for i := 6; i < 10; i++ {
 			v := []byte(fmt.Sprintf("%d", i))
 			req := &epb.ProclaimRequest{Leader: c2.Leader, Value: v}
-			if _, err := lc.Proclaim(context.TODO(), req); err != nil {
+			if _, err := lc.Proclaim(t.Context(), req); err != nil {
 				t.Error(err)
 			}
 		}
@@ -149,11 +148,11 @@ func TestV3ElectionObserve(t *testing.T) {
 	for i := 1; i < 5; i++ {
 		v := []byte(fmt.Sprintf("%d", i))
 		req := &epb.ProclaimRequest{Leader: c1.Leader, Value: v}
-		_, err := lc.Proclaim(context.TODO(), req)
+		_, err := lc.Proclaim(t.Context(), req)
 		require.NoError(t, err)
 	}
 	// start second leader
-	lc.Resign(context.TODO(), &epb.ResignRequest{Leader: c1.Leader})
+	lc.Resign(t.Context(), &epb.ResignRequest{Leader: c1.Leader})
 
 	select {
 	case <-observec:
diff --git a/tests/integration/v3lock_grpc_test.go b/tests/integration/v3lock_grpc_test.go
index f79e455f2..575c86567 100644
--- a/tests/integration/v3lock_grpc_test.go
+++ b/tests/integration/v3lock_grpc_test.go
@@ -15,7 +15,6 @@
 package integration
 
 import (
-	"context"
 	"testing"
 	"time"
 
@@ -33,18 +32,18 @@ func TestV3LockLockWaiter(t *testing.T) {
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	defer clus.Terminate(t)
 
-	lease1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lease1, err1 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err1)
-	lease2, err2 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(context.TODO(), &pb.LeaseGrantRequest{TTL: 30})
+	lease2, err2 := integration.ToGRPC(clus.RandClient()).Lease.LeaseGrant(t.Context(), &pb.LeaseGrantRequest{TTL: 30})
 	require.NoError(t, err2)
 
 	lc := integration.ToGRPC(clus.Client(0)).Lock
-	l1, lerr1 := lc.Lock(context.TODO(), &lockpb.LockRequest{Name: []byte("foo"), Lease: lease1.ID})
+	l1, lerr1 := lc.Lock(t.Context(), &lockpb.LockRequest{Name: []byte("foo"), Lease: lease1.ID})
 	require.NoError(t, lerr1)
 
 	lockc := make(chan struct{})
 	go func() {
-		l2, lerr2 := lc.Lock(context.TODO(), &lockpb.LockRequest{Name: []byte("foo"), Lease: lease2.ID})
+		l2, lerr2 := lc.Lock(t.Context(), &lockpb.LockRequest{Name: []byte("foo"), Lease: lease2.ID})
 		if lerr2 != nil {
 			t.Error(lerr2)
 		}
@@ -60,7 +59,7 @@ func TestV3LockLockWaiter(t *testing.T) {
 		t.Fatalf("locked before unlock")
 	}
 
-	_, uerr := lc.Unlock(context.TODO(), &lockpb.UnlockRequest{Key: l1.Key})
+	_, uerr := lc.Unlock(t.Context(), &lockpb.UnlockRequest{Key: l1.Key})
 	require.NoError(t, uerr)
 
 	select {
diff --git a/tests/robustness/client/client.go b/tests/robustness/client/client.go
index 21a51832d..9eb2141a9 100644
--- a/tests/robustness/client/client.go
+++ b/tests/robustness/client/client.go
@@ -16,6 +16,7 @@ package client
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"sync"
 	"time"
@@ -371,3 +372,81 @@ func toWatchEvent(event clientv3.Event) (watch model.WatchEvent) {
 	}
 	return watch
 }
+
+type ClientSet struct {
+	idProvider identity.Provider
+	baseTime   time.Time
+
+	mux     sync.Mutex
+	closed  bool
+	clients []*RecordingClient
+	reports []report.ClientReport
+}
+
+func NewSet(ids identity.Provider, baseTime time.Time) *ClientSet {
+	return &ClientSet{
+		idProvider: ids,
+		baseTime:   baseTime,
+
+		clients: []*RecordingClient{},
+	}
+}
+
+func (cs *ClientSet) NewClient(endpoints []string) (*RecordingClient, error) {
+	cs.mux.Lock()
+	defer cs.mux.Unlock()
+	if cs.closed {
+		return nil, errors.New("the clientset is already closed")
+	}
+	cli, err := NewRecordingClient(endpoints, cs.idProvider, cs.baseTime)
+	if err != nil {
+		return nil, err
+	}
+	cs.clients = append(cs.clients, cli)
+	return cli, nil
+}
+
+func (cs *ClientSet) Reports() []report.ClientReport {
+	cs.mux.Lock()
+	defer cs.mux.Unlock()
+	if !cs.closed {
+		cs.close()
+	}
+	if cs.reports == nil {
+		reports := cs.generateReports()
+		cs.reports = reports
+	}
+	return cs.reports
+}
+
+func (cs *ClientSet) Close() {
+	cs.mux.Lock()
+	defer cs.mux.Unlock()
+	cs.close()
+}
+
+func (cs *ClientSet) close() {
+	if cs.closed {
+		return
+	}
+	for _, c := range cs.clients {
+		c.Close()
+	}
+	cs.closed = true
+}
+
+func (cs *ClientSet) generateReports() []report.ClientReport {
+	reports := make([]report.ClientReport, 0, len(cs.clients))
+	for _, c := range cs.clients {
+		reports = append(reports, c.Report())
+	}
+	return reports
+}
+
+func (cs *ClientSet) IdentityProvider() identity.Provider {
+	return cs.idProvider
+}
+
+func (cs *ClientSet) BaseTime() time.Time {
+	return cs.baseTime
+}
diff --git a/tests/robustness/client/watch.go b/tests/robustness/client/watch.go
index 4b4b0de15..dd35de89c 100644
--- a/tests/robustness/client/watch.go
+++ b/tests/robustness/client/watch.go
@@ -18,17 +18,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"testing"
-	"time"
 
 	"go.uber.org/zap"
 	"golang.org/x/sync/errgroup"
 
-	"go.etcd.io/etcd/tests/v3/robustness/identity"
 	"go.etcd.io/etcd/tests/v3/robustness/report"
 )
 
-func CollectClusterWatchEvents(ctx context.Context, lg *zap.Logger, endpoints []string, maxRevisionChan <-chan int64, cfg WatchConfig, baseTime time.Time, ids identity.Provider) ([]report.ClientReport, error) {
+func CollectClusterWatchEvents(ctx context.Context, lg *zap.Logger, endpoints []string, maxRevisionChan <-chan int64, cfg WatchConfig, clientSet *ClientSet) error {
 	var g errgroup.Group
 	reports := make([]report.ClientReport, len(endpoints))
 	memberMaxRevisionChans := make([]chan int64, len(endpoints))
@@ -36,7 +33,7 @@ func CollectClusterWatchEvents(ctx context.Context, lg *zap.Logger, endpoints []
 		memberMaxRevisionChan := make(chan int64, 1)
 		memberMaxRevisionChans[i] = memberMaxRevisionChan
 		g.Go(func() error {
-			c, err := NewRecordingClient([]string{endpoint}, ids, baseTime)
+			c, err := clientSet.NewClient([]string{endpoint})
 			if err != nil {
 				return err
 			}
@@ -54,7 +51,7 @@ func CollectClusterWatchEvents(ctx context.Context, lg *zap.Logger, endpoints []
 		}
 		return nil
 	})
-	return reports, g.Wait()
+	return g.Wait()
 }
 
 type WatchConfig struct {
@@ -125,21 +122,3 @@ resetWatch:
 		}
 	}
 }
-
-func ValidateGotAtLeastOneProgressNotify(t *testing.T, reports []report.ClientReport, expectProgressNotify bool) {
-	gotProgressNotify := false
-external:
-	for _, r := range reports {
-		for _, op := range r.Watch {
-			for _, resp := range op.Responses {
-				if resp.IsProgressNotify {
-					gotProgressNotify = true
-					break external
-				}
-			}
-		}
-	}
-	if gotProgressNotify != expectProgressNotify {
-		t.Errorf("Progress notify does not match, expect: %v, got: %v", expectProgressNotify, gotProgressNotify)
-	}
-}
diff --git a/tests/robustness/main_test.go b/tests/robustness/main_test.go
index 55879cabe..4d1ef5295 100644
--- a/tests/robustness/main_test.go
+++ b/tests/robustness/main_test.go
@@ -20,6 +20,7 @@ import (
 	"math/rand"
 	"os"
 	"path/filepath"
+	"slices"
 	"strings"
 	"testing"
 	"time"
@@ -88,7 +89,11 @@ func TestRobustnessRegression(t *testing.T) {
 
 func testRobustness(ctx context.Context, t *testing.T, lg *zap.Logger, s scenarios.TestScenario, c *e2e.EtcdProcessCluster) {
 	serverDataPaths := report.ServerDataPaths(c)
-	r := report.TestReport{Logger: lg, ServersDataPath: serverDataPaths}
+	r := report.TestReport{
+		Logger:          lg,
+		ServersDataPath: serverDataPaths,
+		Traffic:         &report.TrafficDetail{ExpectUniqueRevision: s.Traffic.ExpectUniqueRevision()},
+	}
 	// t.Failed() returns false during panicking. We need to forcibly
 	// save data on panicking.
 	// Refer to: https://github.com/golang/go/issues/49929
@@ -109,11 +114,6 @@ func testRobustness(ctx context.Context, t *testing.T, lg *zap.Logger, s scenari
 		t.Error(err)
 	}
 
-	failpointImpactingWatch := s.Failpoint == failpoint.SleepBeforeSendWatchResponse
-	if !failpointImpactingWatch {
-		watchProgressNotifyEnabled := c.Cfg.ServerConfig.WatchProgressNotifyInterval != 0
-		client.ValidateGotAtLeastOneProgressNotify(t, r.Client, s.Watch.RequestProgress || watchProgressNotifyEnabled)
-	}
 	validateConfig := validate.Config{ExpectRevisionUnique: s.Traffic.ExpectUniqueRevision()}
 	result := validate.ValidateAndReturnVisualize(lg, validateConfig, r.Client, persistedRequests, 5*time.Minute)
 	r.Visualize = result.Linearization.Visualize
@@ -128,7 +128,7 @@ func runScenario(ctx context.Context, t *testing.T, s scenarios.TestScenario, lg
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 	g := errgroup.Group{}
-	var operationReport, watchReport, failpointClientReport []report.ClientReport
+	var failpointClientReport []report.ClientReport
 	failpointInjected := make(chan report.FailpointInjection, 1)
 
 	// using baseTime time-measuring operation to get monotonic clock reading
@@ -152,19 +152,22 @@ func runScenario(ctx context.Context, t *testing.T, s scenarios.TestScenario, lg
 		}
 		return nil
 	})
+	trafficSet := client.NewSet(ids, baseTime)
+	defer trafficSet.Close()
 	maxRevisionChan := make(chan int64, 1)
 	g.Go(func() error {
 		defer close(maxRevisionChan)
-		operationReport = traffic.SimulateTraffic(ctx, t, lg, clus, s.Profile, s.Traffic, failpointInjected, baseTime, ids)
+		operationReport := traffic.SimulateTraffic(ctx, t, lg, clus, s.Profile, s.Traffic, failpointInjected, trafficSet)
 		maxRevision := report.OperationsMaxRevision(operationReport)
 		maxRevisionChan <- maxRevision
 		lg.Info("Finished simulating Traffic", zap.Int64("max-revision", maxRevision))
 		return nil
 	})
+	watchSet := client.NewSet(ids, baseTime)
+	defer watchSet.Close()
 	g.Go(func() error {
-		var err error
 		endpoints := processEndpoints(clus)
-		watchReport, err = client.CollectClusterWatchEvents(ctx, lg, endpoints, maxRevisionChan, s.Watch, baseTime, ids)
+		err := client.CollectClusterWatchEvents(ctx, lg, endpoints, maxRevisionChan, s.Watch, watchSet)
 		return err
 	})
 	err := g.Wait()
@@ -176,7 +179,7 @@ func runScenario(ctx context.Context, t *testing.T, s scenarios.TestScenario, lg
 	if err != nil {
 		t.Error(err)
 	}
-	return append(operationReport, append(failpointClientReport, watchReport...)...)
+	return slices.Concat(trafficSet.Reports(), watchSet.Reports(), failpointClientReport)
 }
 
 func randomizeTime(base time.Duration, jitter time.Duration) time.Duration {
diff --git a/tests/robustness/model/describe.go b/tests/robustness/model/describe.go
index 3d73257f6..1ffdc563f 100644
--- a/tests/robustness/model/describe.go
+++ b/tests/robustness/model/describe.go
@@ -16,6 +16,9 @@ package model
 
 import (
 	"fmt"
+	"maps"
+	"slices"
+	"sort"
 	"strings"
 
 	clientv3 "go.etcd.io/etcd/client/v3"
@@ -39,12 +42,9 @@ func describeEtcdResponse(request EtcdRequest, response MaybeEtcdResponse) strin
 		return fmt.Sprintf("%s, rev: %d", describeRangeResponse(request.Range.RangeOptions, *response.Range), response.Revision)
 	case Txn:
 		return fmt.Sprintf("%s, rev: %d", describeTxnResponse(request.Txn, response.Txn), response.Revision)
-	case LeaseGrant, LeaseRevoke, Defragment:
-		if response.Revision == 0 {
-			return "ok"
-		}
+	case LeaseGrant, LeaseRevoke:
 		return fmt.Sprintf("ok, rev: %d", response.Revision)
-	case Compact:
+	case Compact, Defragment:
 		return "ok"
 	default:
 		return fmt.Sprintf("<! unknown request type: %q !>", request.Type)
@@ -85,31 +85,44 @@ func describeEtcdRequest(request EtcdRequest) string {
 func describeEtcdState(state EtcdState) string {
 	descHTML := make([]string, 0)
 
-	descHTML = append(descHTML, fmt.Sprintf("<p style=\"margin: 0.25em 0;\">Revision: %d, CompactRevision: %d</p>", state.Revision, state.CompactRevision))
+	descHTML = append(descHTML, fmt.Sprintf("<p style=\"margin: 0.25em 0;\">state, rev: %d, compactRev: %d</p>", state.Revision, state.CompactRevision))
 
 	if len(state.KeyValues) > 0 {
-		descHTML = append(descHTML, "<ul style=\"margin: 0.25em 0;\">")
-
-		for k, v := range state.KeyValues {
-			keyValDesc := make([]string, 0)
+		descHTML = append(descHTML, "keys: <ul style=\"margin: 0.25em 0;\">")
 
-			keyValDesc = append(keyValDesc, fmt.Sprintf("<li style=\"margin: 0.25em 0;\"><strong>%s</strong>: ", k))
+		keys := slices.Collect(maps.Keys(state.KeyValues))
+		sort.Strings(keys)
+		for _, key := range keys {
+			descHTML = append(descHTML, fmt.Sprintf("<li style=\"margin: 0.25em 0;\"><strong>%s</strong> - ", key))
 
-			if v.Value.Value != "" {
-				keyValDesc = append(keyValDesc, fmt.Sprintf("Value: %q, ", v.Value.Value))
+			value := state.KeyValues[key]
+			if value.Value.Value != "" {
+				descHTML = append(descHTML, fmt.Sprintf("val: %q, ", value.Value.Value))
 			}
-			if v.Value.Hash != 0 {
-				keyValDesc = append(keyValDesc, fmt.Sprintf("Hash: %d, ", v.Value.Hash))
+			if value.Value.Hash != 0 {
+				descHTML = append(descHTML, fmt.Sprintf("hash: %d, ", value.Value.Hash))
+			}
+			lease := state.KeyLeases[key]
+			if lease != 0 {
+				descHTML = append(descHTML, fmt.Sprintf("lease: %d, ", lease))
 			}
 
-			keyValDesc = append(keyValDesc, fmt.Sprintf("ModRevision: %d, Version: %d</li>", v.ModRevision, v.Version))
-
-			descHTML = append(descHTML, strings.Join(keyValDesc, ""))
+			descHTML = append(descHTML, fmt.Sprintf("mod: %d, ver: %d</li>", value.ModRevision, value.Version))
 		}
 
 		descHTML = append(descHTML, "</ul>")
 	}
 
+	if len(state.Leases) > 0 {
+		descHTML = append(descHTML, "leases: <ul style=\"margin: 0.25em 0;\">")
+		leases := slices.Collect(maps.Keys(state.Leases))
+		slices.Sort(leases)
+		for _, lease := range leases {
+			descHTML = append(descHTML, fmt.Sprintf("<li style=\"margin: 0.25em 0;\"><strong>%d</strong></li>", lease))
+		}
+		descHTML = append(descHTML, "</ul>")
+	}
+
 	return strings.Join(descHTML, "")
 }
 
diff --git a/tests/robustness/model/describe_test.go b/tests/robustness/model/describe_test.go
index 8ef2439ad..8cea7e833 100644
--- a/tests/robustness/model/describe_test.go
+++ b/tests/robustness/model/describe_test.go
@@ -18,9 +18,9 @@ import (
 	"errors"
 	"testing"
 
-	"go.etcd.io/etcd/api/v3/mvccpb"
-
 	"github.com/stretchr/testify/assert"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
 )
 
 func TestModelDescribe(t *testing.T) {
@@ -126,8 +126,8 @@ func TestModelDescribe(t *testing.T) {
 		},
 		{
 			req:            defragmentRequest(),
-			resp:           defragmentResponse(10),
-			expectDescribe: `defragment() -> ok, rev: 10`,
+			resp:           defragmentResponse(),
+			expectDescribe: `defragment() -> ok`,
 		},
 		{
 			req:            listRequest("key11", 0),
diff --git a/tests/robustness/model/deterministic.go b/tests/robustness/model/deterministic.go
index 70aaa5819..b4e789296 100644
--- a/tests/robustness/model/deterministic.go
+++ b/tests/robustness/model/deterministic.go
@@ -16,9 +16,7 @@ package model
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
-	"hash/fnv"
 	"html"
 	"maps"
 	"reflect"
@@ -122,122 +120,152 @@ func freshEtcdState() EtcdState {
 
 // Step handles a successful request, returning updated state and response it would generate.
 func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
-	// TODO: Avoid copying when TXN only has read operations
-	if request.Type == Range {
-		if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
-			resp := s.getRange(request.Range.RangeOptions)
-			return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
-		}
-		if request.Range.Revision > s.Revision {
-			return s, MaybeEtcdResponse{Error: ErrEtcdFutureRev.Error()}
-		}
-		if request.Range.Revision < s.CompactRevision {
-			return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
-		}
-		return s, MaybeEtcdResponse{Persisted: true, PersistedRevision: s.Revision}
+	switch request.Type {
+	case Range:
+		return s.stepRange(request)
+	case Txn:
+		return s.stepTxn(request)
+	case LeaseGrant:
+		return s.stepLeaseGrant(request)
+	case LeaseRevoke:
+		return s.stepLeaseRevoke(request)
+	case Defragment:
+		return s.stepDefragment()
+	case Compact:
+		return s.stepCompact(request)
+	default:
+		panic(fmt.Sprintf("Unknown request type: %v", request.Type))
+	}
+}
+
+func (s EtcdState) stepRange(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
+		resp := s.getRange(request.Range.RangeOptions)
+		return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
+	}
+	if request.Range.Revision > s.Revision {
+		return s, MaybeEtcdResponse{Error: ErrEtcdFutureRev.Error()}
 	}
+	if request.Range.Revision < s.CompactRevision {
+		return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
+	}
+	return s, MaybeEtcdResponse{Persisted: true, PersistedRevision: s.Revision}
+}
 
+func (s EtcdState) stepTxn(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	// TODO: Avoid copying when TXN only has read operations
 	newState := s.DeepCopy()
-	switch request.Type {
-	case Txn:
-		failure := false
-		for _, cond := range request.Txn.Conditions {
-			val := newState.KeyValues[cond.Key]
-			if cond.ExpectedVersion > 0 {
-				if val.Version != cond.ExpectedVersion {
-					failure = true
-					break
-				}
-			} else if val.ModRevision != cond.ExpectedRevision {
+	failure := false
+	for _, cond := range request.Txn.Conditions {
+		val := newState.KeyValues[cond.Key]
+		if cond.ExpectedVersion > 0 {
+			if val.Version != cond.ExpectedVersion {
 				failure = true
 				break
 			}
+		} else if val.ModRevision != cond.ExpectedRevision {
+			failure = true
+			break
 		}
-		operations := request.Txn.OperationsOnSuccess
-		if failure {
-			operations = request.Txn.OperationsOnFailure
-		}
-		opResp := make([]EtcdOperationResult, len(operations))
-		increaseRevision := false
-		for i, op := range operations {
-			switch op.Type {
-			case RangeOperation:
-				opResp[i] = EtcdOperationResult{
-					RangeResponse: newState.getRange(op.Range),
-				}
-			case PutOperation:
-				_, leaseExists := newState.Leases[op.Put.LeaseID]
-				if op.Put.LeaseID != 0 && !leaseExists {
-					break
-				}
-				ver := int64(1)
-				if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
-					ver = val.Version + 1
-				}
-				newState.KeyValues[op.Put.Key] = ValueRevision{
-					Value:       op.Put.Value,
-					ModRevision: newState.Revision + 1,
-					Version:     ver,
-				}
+	}
+	operations := request.Txn.OperationsOnSuccess
+	if failure {
+		operations = request.Txn.OperationsOnFailure
+	}
+	opResp := make([]EtcdOperationResult, len(operations))
+	increaseRevision := false
+	for i, op := range operations {
+		switch op.Type {
+		case RangeOperation:
+			opResp[i] = EtcdOperationResult{
+				RangeResponse: newState.getRange(op.Range),
+			}
+		case PutOperation:
+			_, leaseExists := newState.Leases[op.Put.LeaseID]
+			if op.Put.LeaseID != 0 && !leaseExists {
+				break
+			}
+			ver := int64(1)
+			if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
+				ver = val.Version + 1
+			}
+			newState.KeyValues[op.Put.Key] = ValueRevision{
+				Value:       op.Put.Value,
+				ModRevision: newState.Revision + 1,
+				Version:     ver,
+			}
+			increaseRevision = true
+			newState = detachFromOldLease(newState, op.Put.Key)
+			if leaseExists {
+				newState = attachToNewLease(newState, op.Put.LeaseID, op.Put.Key)
+			}
+		case DeleteOperation:
+			if _, ok := newState.KeyValues[op.Delete.Key]; ok {
+				delete(newState.KeyValues, op.Delete.Key)
 				increaseRevision = true
-				newState = detachFromOldLease(newState, op.Put.Key)
-				if leaseExists {
-					newState = attachToNewLease(newState, op.Put.LeaseID, op.Put.Key)
-				}
-			case DeleteOperation:
-				if _, ok := newState.KeyValues[op.Delete.Key]; ok {
-					delete(newState.KeyValues, op.Delete.Key)
-					increaseRevision = true
-					newState = detachFromOldLease(newState, op.Delete.Key)
-					opResp[i].Deleted = 1
-				}
-			default:
-				panic("unsupported operation")
+				newState = detachFromOldLease(newState, op.Delete.Key)
+				opResp[i].Deleted = 1
 			}
+		default:
+			panic("unsupported operation")
 		}
-		if increaseRevision {
-			newState.Revision++
-		}
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Txn: &TxnResponse{Failure: failure, Results: opResp}, Revision: newState.Revision}}
-	case LeaseGrant:
-		lease := EtcdLease{
-			LeaseID: request.LeaseGrant.LeaseID,
-			Keys:    map[string]struct{}{},
-		}
-		newState.Leases[request.LeaseGrant.LeaseID] = lease
+	}
+	if increaseRevision {
+		newState.Revision++
+	}
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Txn: &TxnResponse{Failure: failure, Results: opResp}, Revision: newState.Revision}}
+}
+
+func (s EtcdState) stepLeaseGrant(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	newState := s.DeepCopy()
+	// Empty LeaseID means the request failed and client didn't get response. Ignore it as client cannot use lease without knowing its id.
+	if request.LeaseGrant.LeaseID == 0 {
 		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
-	case LeaseRevoke:
-		// Delete the keys attached to the lease
-		keyDeleted := false
-		for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys {
-			// same as delete.
-			if _, ok := newState.KeyValues[key]; ok {
-				if !keyDeleted {
-					keyDeleted = true
-				}
-				delete(newState.KeyValues, key)
-				delete(newState.KeyLeases, key)
+	}
+	lease := EtcdLease{
+		LeaseID: request.LeaseGrant.LeaseID,
+		Keys:    map[string]struct{}{},
+	}
+	newState.Leases[request.LeaseGrant.LeaseID] = lease
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
+}
+
+func (s EtcdState) stepLeaseRevoke(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	newState := s.DeepCopy()
+	// Delete the keys attached to the lease
+	keyDeleted := false
+	for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys {
+		// same as delete.
+		if _, ok := newState.KeyValues[key]; ok {
+			if !keyDeleted {
+				keyDeleted = true
 			}
+			delete(newState.KeyValues, key)
+			delete(newState.KeyLeases, key)
 		}
-		// delete the lease
-		delete(newState.Leases, request.LeaseRevoke.LeaseID)
-		if keyDeleted {
-			newState.Revision++
-		}
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseRevoke: &LeaseRevokeResponse{}}}
-	case Defragment:
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: newState.Revision}}
-	case Compact:
-		if request.Compact.Revision <= newState.CompactRevision {
-			return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
-		}
-		newState.CompactRevision = request.Compact.Revision
-		// Set fake revision as compaction returns non-linearizable revision.
-		// TODO: Model non-linearizable response revision in model.
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Compact: &CompactResponse{}, Revision: -1}}
-	default:
-		panic(fmt.Sprintf("Unknown request type: %v", request.Type))
 	}
+	// delete the lease
+	delete(newState.Leases, request.LeaseRevoke.LeaseID)
+	if keyDeleted {
+		newState.Revision++
+	}
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseRevoke: &LeaseRevokeResponse{}}}
+}
+
+func (s EtcdState) stepDefragment() (EtcdState, MaybeEtcdResponse) {
+	return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: RevisionForNonLinearizableResponse}}
+}
+
+func (s EtcdState) stepCompact(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	newState := s.DeepCopy()
+	if request.Compact.Revision <= newState.CompactRevision {
+		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
+	}
+	if request.Compact.Revision > newState.Revision {
+		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrFutureRev.Error()}}
+	}
+	newState.CompactRevision = request.Compact.Revision
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Compact: &CompactResponse{}, Revision: RevisionForNonLinearizableResponse}}
 }
 
 func (s EtcdState) getRange(options RangeOptions) RangeResponse {
@@ -285,204 +313,3 @@ func attachToNewLease(s EtcdState, leaseID int64, key string) EtcdState {
 	s.Leases[leaseID].Keys[key] = leased
 	return s
 }
-
-type RequestType string
-
-const (
-	Range       RequestType = "range"
-	Txn         RequestType = "txn"
-	LeaseGrant  RequestType = "leaseGrant"
-	LeaseRevoke RequestType = "leaseRevoke"
-	Defragment  RequestType = "defragment"
-	Compact     RequestType = "compact"
-)
-
-type EtcdRequest struct {
-	Type        RequestType
-	LeaseGrant  *LeaseGrantRequest
-	LeaseRevoke *LeaseRevokeRequest
-	Range       *RangeRequest
-	Txn         *TxnRequest
-	Defragment  *DefragmentRequest
-	Compact     *CompactRequest
-}
-
-func (r *EtcdRequest) IsRead() bool {
-	if r.Type == Range {
-		return true
-	}
-	if r.Type != Txn {
-		return false
-	}
-	for _, op := range append(r.Txn.OperationsOnSuccess, r.Txn.OperationsOnFailure...) {
-		if op.Type != RangeOperation {
-			return false
-		}
-	}
-	return true
-}
-
-type RangeRequest struct {
-	RangeOptions
-	Revision int64
-}
-
-type RangeOptions struct {
-	Start string
-	End   string
-	Limit int64
-}
-
-type PutOptions struct {
-	Key     string
-	Value   ValueOrHash
-	LeaseID int64
-}
-
-type DeleteOptions struct {
-	Key string
-}
-
-type TxnRequest struct {
-	Conditions          []EtcdCondition
-	OperationsOnSuccess []EtcdOperation
-	OperationsOnFailure []EtcdOperation
-}
-
-type EtcdCondition struct {
-	Key              string
-	ExpectedRevision int64
-	ExpectedVersion  int64
-}
-
-type EtcdOperation struct {
-	Type   OperationType
-	Range  RangeOptions
-	Put    PutOptions
-	Delete DeleteOptions
-}
-
-type OperationType string
-
-const (
-	RangeOperation  OperationType = "range-operation"
-	PutOperation    OperationType = "put-operation"
-	DeleteOperation OperationType = "delete-operation"
-)
-
-type LeaseGrantRequest struct {
-	LeaseID int64
-}
-type LeaseRevokeRequest struct {
-	LeaseID int64
-}
-type DefragmentRequest struct{}
-
-// MaybeEtcdResponse extends EtcdResponse to include partial information about responses to a request.
-// Possible response state information:
-// * Normal response. Client observed response. Only EtcdResponse is set.
-// * Persisted. Client didn't observe response, but we know it was persisted by etcd. Only Persisted is set
-// * Persisted with Revision. Client didn't observe response, but we know that it was persisted, and it's revision. Both Persisted and PersistedRevision is set.
-// * Error response. Client observed error, but we don't know if it was persisted. Only Error is set.
-type MaybeEtcdResponse struct {
-	EtcdResponse
-	Persisted         bool
-	PersistedRevision int64
-	Error             string
-}
-
-var ErrEtcdFutureRev = errors.New("future rev")
-
-type EtcdResponse struct {
-	Txn         *TxnResponse
-	Range       *RangeResponse
-	LeaseGrant  *LeaseGrantReponse
-	LeaseRevoke *LeaseRevokeResponse
-	Defragment  *DefragmentResponse
-	Compact     *CompactResponse
-	ClientError string
-	Revision    int64
-}
-
-func Match(r1, r2 MaybeEtcdResponse) bool {
-	r1Revision := r1.Revision
-	if r1.Persisted {
-		r1Revision = r1.PersistedRevision
-	}
-	r2Revision := r2.Revision
-	if r2.Persisted {
-		r2Revision = r2.PersistedRevision
-	}
-	return (r1.Persisted && r1.PersistedRevision == 0) || (r2.Persisted && r2.PersistedRevision == 0) || ((r1.Persisted || r2.Persisted) && (r1.Error != "" || r2.Error != "" || r1Revision == r2Revision)) || reflect.DeepEqual(r1, r2)
-}
-
-type TxnResponse struct {
-	Failure bool
-	Results []EtcdOperationResult
-}
-
-type RangeResponse struct {
-	KVs   []KeyValue
-	Count int64
-}
-
-type LeaseGrantReponse struct {
-	LeaseID int64
-}
-type (
-	LeaseRevokeResponse struct{}
-	DefragmentResponse  struct{}
-)
-
-type EtcdOperationResult struct {
-	RangeResponse
-	Deleted int64
-}
-
-type KeyValue struct {
-	Key string
-	ValueRevision
-}
-
-var leased = struct{}{}
-
-type EtcdLease struct {
-	LeaseID int64
-	Keys    map[string]struct{}
-}
-
-func (el EtcdLease) DeepCopy() EtcdLease {
-	return EtcdLease{
-		LeaseID: el.LeaseID,
-		Keys:    maps.Clone(el.Keys),
-	}
-}
-
-type ValueRevision struct {
-	Value       ValueOrHash `json:",omitempty"`
-	ModRevision int64       `json:",omitempty"`
-	Version     int64       `json:",omitempty"`
-}
-
-type ValueOrHash struct {
-	Value string `json:",omitempty"`
-	Hash  uint32 `json:",omitempty"`
-}
-
-func ToValueOrHash(value string) ValueOrHash {
-	v := ValueOrHash{}
-	if len(value) < 20 {
-		v.Value = value
-	} else {
-		h := fnv.New32a()
-		h.Write([]byte(value))
-		v.Hash = h.Sum32()
-	}
-	return v
-}
-
-type CompactResponse struct{}
-
-type CompactRequest struct {
-	Revision int64
-}
diff --git a/tests/robustness/model/deterministic_test.go b/tests/robustness/model/deterministic_test.go
index 6e7512623..ea7980c7d 100644
--- a/tests/robustness/model/deterministic_test.go
+++ b/tests/robustness/model/deterministic_test.go
@@ -381,27 +381,27 @@ var commonTestScenarios = []modelTestCase{
 			{req: getRequest("key"), resp: getResponse("key", "4", 4, 4)},
 			{req: compareRevisionAndPutRequest("key", 4, "5"), resp: compareRevisionAndPutResponse(true, 5)},
 			{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
-			{req: defragmentRequest(), resp: defragmentResponse(6)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 		},
 	},
 	{
 		name: "Defragment success between all other request types",
 		operations: []testOperation{
-			{req: defragmentRequest(), resp: defragmentResponse(1)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: leaseGrantRequest(1), resp: leaseGrantResponse(1)},
-			{req: defragmentRequest(), resp: defragmentResponse(1)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: putWithLeaseRequest("key", "1", 1), resp: putResponse(2)},
-			{req: defragmentRequest(), resp: defragmentResponse(2)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: leaseRevokeRequest(1), resp: leaseRevokeResponse(3)},
-			{req: defragmentRequest(), resp: defragmentResponse(3)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: putRequest("key", "4"), resp: putResponse(4)},
-			{req: defragmentRequest(), resp: defragmentResponse(4)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: getRequest("key"), resp: getResponse("key", "4", 4, 4)},
-			{req: defragmentRequest(), resp: defragmentResponse(4)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: compareRevisionAndPutRequest("key", 4, "5"), resp: compareRevisionAndPutResponse(true, 5)},
-			{req: defragmentRequest(), resp: defragmentResponse(5)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 			{req: deleteRequest("key"), resp: deleteResponse(1, 6)},
-			{req: defragmentRequest(), resp: defragmentResponse(6)},
+			{req: defragmentRequest(), resp: defragmentResponse()},
 		},
 	},
 }
diff --git a/tests/robustness/model/history.go b/tests/robustness/model/history.go
index f6119984f..338c7a23c 100644
--- a/tests/robustness/model/history.go
+++ b/tests/robustness/model/history.go
@@ -171,6 +171,12 @@ func (h *AppendableHistory) AppendTxn(cmp []clientv3.Cmp, clientOnSuccessOps, cl
 	h.appendSuccessful(request, start, end, txnResponse(results, resp.Succeeded, revision))
 }
 
+func (h *AppendableHistory) appendClientError(request EtcdRequest, start, end time.Duration, err error) {
+	h.appendSuccessful(request, start, end, MaybeEtcdResponse{
+		EtcdResponse: EtcdResponse{ClientError: err.Error()},
+	})
+}
+
 func (h *AppendableHistory) appendSuccessful(request EtcdRequest, start, end time.Duration, response MaybeEtcdResponse) {
 	op := porcupine.Operation{
 		ClientId: h.streamID,
@@ -259,28 +265,24 @@ func (h *AppendableHistory) AppendDefragment(start, end time.Duration, resp *cli
 		h.appendFailed(request, start, end, err)
 		return
 	}
-	var revision int64
-	if resp != nil && resp.Header != nil {
-		revision = resp.Header.Revision
-	}
-	h.appendSuccessful(request, start, end, defragmentResponse(revision))
+	h.appendSuccessful(request, start, end, defragmentResponse())
 }
 
 func (h *AppendableHistory) AppendCompact(rev int64, start, end time.Duration, resp *clientv3.CompactResponse, err error) {
 	request := compactRequest(rev)
 	if err != nil {
 		if strings.Contains(err.Error(), mvcc.ErrCompacted.Error()) {
-			h.appendSuccessful(request, start, end, MaybeEtcdResponse{
-				EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()},
-			})
+			h.appendClientError(request, start, end, mvcc.ErrCompacted)
+			return
+		}
+		if strings.Contains(err.Error(), mvcc.ErrFutureRev.Error()) {
+			h.appendClientError(request, start, end, mvcc.ErrFutureRev)
 			return
 		}
 		h.appendFailed(request, start, end, err)
 		return
 	}
-	// Set fake revision as compaction returns non-linearizable revision.
-	// TODO: Model non-linearizable response revision in model.
-	h.appendSuccessful(request, start, end, compactResponse(-1))
+	h.appendSuccessful(request, start, end, compactResponse())
 }
 
 func (h *AppendableHistory) appendFailed(request EtcdRequest, start, end time.Duration, err error) {
@@ -469,16 +471,16 @@ func defragmentRequest() EtcdRequest {
 	return EtcdRequest{Type: Defragment, Defragment: &DefragmentRequest{}}
 }
 
-func defragmentResponse(revision int64) MaybeEtcdResponse {
-	return MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: revision}}
+func defragmentResponse() MaybeEtcdResponse {
+	return MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: RevisionForNonLinearizableResponse}}
 }
 
 func compactRequest(rev int64) EtcdRequest {
 	return EtcdRequest{Type: Compact, Compact: &CompactRequest{Revision: rev}}
 }
 
-func compactResponse(revision int64) MaybeEtcdResponse {
-	return MaybeEtcdResponse{EtcdResponse: EtcdResponse{Compact: &CompactResponse{}, Revision: revision}}
+func compactResponse() MaybeEtcdResponse {
+	return MaybeEtcdResponse{EtcdResponse: EtcdResponse{Compact: &CompactResponse{}, Revision: RevisionForNonLinearizableResponse}}
 }
 
 type History struct {
diff --git a/tests/robustness/model/non_deterministic.go b/tests/robustness/model/non_deterministic.go
index 68e18002c..b9516342c 100644
--- a/tests/robustness/model/non_deterministic.go
+++ b/tests/robustness/model/non_deterministic.go
@@ -15,8 +15,10 @@
 package model
 
 import (
+	"cmp"
 	"fmt"
 	"reflect"
+	"slices"
 	"strings"
 
 	"github.com/anishathalye/porcupine"
@@ -41,9 +43,21 @@ var NonDeterministicModel = porcupine.Model{
 	},
 	DescribeState: func(st any) string {
 		etcdStates := st.(nonDeterministicState)
-		desc := make([]string, len(etcdStates))
+		desc := make([]string, 0, len(etcdStates))
 
-		for _, s := range etcdStates {
+		slices.SortFunc(etcdStates, func(i, j EtcdState) int {
+			if c := cmp.Compare(i.Revision, j.Revision); c != 0 {
+				return c
+			}
+			return cmp.Compare(i.CompactRevision, j.CompactRevision)
+		})
+
+		for i, s := range etcdStates {
+			// Describe just 3 first states before truncating
+			if i >= 3 {
+				desc = append(desc, "...truncated...")
+				break
+			}
 			desc = append(desc, describeEtcdState(s))
 		}
 
diff --git a/tests/robustness/model/replay.go b/tests/robustness/model/replay.go
index bed69d12f..ab2ca79eb 100644
--- a/tests/robustness/model/replay.go
+++ b/tests/robustness/model/replay.go
@@ -74,7 +74,7 @@ func toWatchEvents(prevState *EtcdState, request EtcdRequest, response MaybeEtcd
 		} else {
 			ops = request.Txn.OperationsOnSuccess
 		}
-		for _, op := range ops {
+		for i, op := range ops {
 			switch op.Type {
 			case RangeOperation:
 			case DeleteOperation:
@@ -85,7 +85,7 @@ func toWatchEvents(prevState *EtcdState, request EtcdRequest, response MaybeEtcd
 					},
 					Revision: response.Revision,
 				}
-				if _, ok := prevState.KeyValues[op.Delete.Key]; ok {
+				if response.Txn.Results[i].Deleted != 0 {
 					events = append(events, e)
 				}
 			case PutOperation:
diff --git a/tests/robustness/model/types.go b/tests/robustness/model/types.go
new file mode 100644
index 000000000..0f605659a
--- /dev/null
+++ b/tests/robustness/model/types.go
@@ -0,0 +1,228 @@
+// Copyright 2023 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package model
+
+import (
+	"errors"
+	"hash/fnv"
+	"maps"
+	"reflect"
+)
+
+// RevisionForNonLinearizableResponse is a fake revision value used to
+// separate responses for requests that are not linearizable,
+// thus we need to ignore it in model when checking linearization.
+const RevisionForNonLinearizableResponse = -1
+
+type RequestType string
+
+const (
+	Range       RequestType = "range"
+	Txn         RequestType = "txn"
+	LeaseGrant  RequestType = "leaseGrant"
+	LeaseRevoke RequestType = "leaseRevoke"
+	Defragment  RequestType = "defragment"
+	Compact     RequestType = "compact"
+)
+
+type EtcdRequest struct {
+	Type        RequestType
+	LeaseGrant  *LeaseGrantRequest
+	LeaseRevoke *LeaseRevokeRequest
+	Range       *RangeRequest
+	Txn         *TxnRequest
+	Defragment  *DefragmentRequest
+	Compact     *CompactRequest
+}
+
+func (r *EtcdRequest) IsRead() bool {
+	if r.Type == Range {
+		return true
+	}
+	if r.Type != Txn {
+		return false
+	}
+	for _, op := range append(r.Txn.OperationsOnSuccess, r.Txn.OperationsOnFailure...) {
+		if op.Type != RangeOperation {
+			return false
+		}
+	}
+	return true
+}
+
+type RangeRequest struct {
+	RangeOptions
+	Revision int64
+}
+
+type RangeOptions struct {
+	Start string
+	End   string
+	Limit int64
+}
+
+type PutOptions struct {
+	Key     string
+	Value   ValueOrHash
+	LeaseID int64
+}
+
+type DeleteOptions struct {
+	Key string
+}
+
+type TxnRequest struct {
+	Conditions          []EtcdCondition
+	OperationsOnSuccess []EtcdOperation
+	OperationsOnFailure []EtcdOperation
+}
+
+type EtcdCondition struct {
+	Key              string
+	ExpectedRevision int64
+	ExpectedVersion  int64
+}
+
+type EtcdOperation struct {
+	Type   OperationType
+	Range  RangeOptions
+	Put    PutOptions
+	Delete DeleteOptions
+}
+
+type OperationType string
+
+const (
+	RangeOperation  OperationType = "range-operation"
+	PutOperation    OperationType = "put-operation"
+	DeleteOperation OperationType = "delete-operation"
+)
+
+type LeaseGrantRequest struct {
+	LeaseID int64
+}
+type LeaseRevokeRequest struct {
+	LeaseID int64
+}
+type DefragmentRequest struct{}
+
+// MaybeEtcdResponse extends EtcdResponse to include partial information about responses to a request.
+// Possible response state information:
+// * Normal response. Client observed response. Only EtcdResponse is set.
+// * Persisted. Client didn't observe response, but we know it was persisted by etcd. Only Persisted is set
+// * Persisted with Revision. Client didn't observe response, but we know that it was persisted, and it's revision. Both Persisted and PersistedRevision is set.
+// * Error response. Client observed error, but we don't know if it was persisted. Only Error is set.
+type MaybeEtcdResponse struct {
+	EtcdResponse
+	Persisted         bool
+	PersistedRevision int64
+	Error             string
+}
+
+var ErrEtcdFutureRev = errors.New("future rev")
+
+type EtcdResponse struct {
+	Txn         *TxnResponse
+	Range       *RangeResponse
+	LeaseGrant  *LeaseGrantReponse
+	LeaseRevoke *LeaseRevokeResponse
+	Defragment  *DefragmentResponse
+	Compact     *CompactResponse
+	ClientError string
+	Revision    int64
+}
+
+func Match(r1, r2 MaybeEtcdResponse) bool {
+	r1Revision := r1.Revision
+	if r1.Persisted {
+		r1Revision = r1.PersistedRevision
+	}
+	r2Revision := r2.Revision
+	if r2.Persisted {
+		r2Revision = r2.PersistedRevision
+	}
+	return (r1.Persisted && r1.PersistedRevision == 0) || (r2.Persisted && r2.PersistedRevision == 0) || ((r1.Persisted || r2.Persisted) && (r1.Error != "" || r2.Error != "" || r1Revision == r2Revision)) || reflect.DeepEqual(r1, r2)
+}
+
+type TxnResponse struct {
+	Failure bool
+	Results []EtcdOperationResult
+}
+
+type RangeResponse struct {
+	KVs   []KeyValue
+	Count int64
+}
+
+type LeaseGrantReponse struct {
+	LeaseID int64
+}
+type (
+	LeaseRevokeResponse struct{}
+	DefragmentResponse  struct{}
+)
+
+type EtcdOperationResult struct {
+	RangeResponse
+	Deleted int64
+}
+
+type KeyValue struct {
+	Key string
+	ValueRevision
+}
+
+var leased = struct{}{}
+
+type EtcdLease struct {
+	LeaseID int64
+	Keys    map[string]struct{}
+}
+
+func (el EtcdLease) DeepCopy() EtcdLease {
+	return EtcdLease{
+		LeaseID: el.LeaseID,
+		Keys:    maps.Clone(el.Keys),
+	}
+}
+
+type ValueRevision struct {
+	Value       ValueOrHash `json:",omitempty"`
+	ModRevision int64       `json:",omitempty"`
+	Version     int64       `json:",omitempty"`
+}
+
+type ValueOrHash struct {
+	Value string `json:",omitempty"`
+	Hash  uint32 `json:",omitempty"`
+}
+
+func ToValueOrHash(value string) ValueOrHash {
+	v := ValueOrHash{}
+	if len(value) < 20 {
+		v.Value = value
+	} else {
+		h := fnv.New32a()
+		h.Write([]byte(value))
+		v.Hash = h.Sum32()
+	}
+	return v
+}
+
+type CompactResponse struct{}
+
+type CompactRequest struct {
+	Revision int64
+}
diff --git a/tests/robustness/report/report.go b/tests/robustness/report/report.go
index daf488f71..e02a8b9e1 100644
--- a/tests/robustness/report/report.go
+++ b/tests/robustness/report/report.go
@@ -15,8 +15,10 @@
 package report
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
+	"path"
 	"path/filepath"
 
 	"go.uber.org/zap"
@@ -30,6 +32,7 @@ type TestReport struct {
 	ServersDataPath map[string]string
 	Client          []ClientReport
 	Visualize       func(lg *zap.Logger, path string) error
+	Traffic         *TrafficDetail
 }
 
 func (r *TestReport) Report(path string) error {
@@ -50,6 +53,11 @@ func (r *TestReport) Report(path string) error {
 			return err
 		}
 	}
+	if r.Traffic != nil {
+		if err := persistTrafficDetail(r.Logger, path, *r.Traffic); err != nil {
+			return err
+		}
+	}
 	if r.Visualize != nil {
 		if err := r.Visualize(r.Logger, filepath.Join(path, "history.html")); err != nil {
 			return err
@@ -74,3 +82,28 @@ func memberDataDir(member e2e.EtcdProcess) string {
 	}
 	return member.Config().DataDirPath
 }
+
+type TrafficDetail struct {
+	ExpectUniqueRevision bool `json:"expectuniquerevision,omitempty"`
+}
+
+const trafficDetailFileName = "traffic.json"
+
+func persistTrafficDetail(lg *zap.Logger, p string, td TrafficDetail) error {
+	lg.Info("Saving traffic configuration details", zap.String("path", path.Join(p, trafficDetailFileName)))
+	b, err := json.Marshal(td)
+	if err != nil {
+		return nil
+	}
+	return os.WriteFile(filepath.Join(p, trafficDetailFileName), b, 0o644)
+}
+
+func LoadTrafficDetail(p string) (TrafficDetail, error) {
+	var detail TrafficDetail
+	b, err := os.ReadFile(filepath.Join(p, trafficDetailFileName))
+	if err != nil {
+		return TrafficDetail{}, err
+	}
+	err = json.Unmarshal(b, &detail)
+	return detail, err
+}
diff --git a/tests/robustness/scenarios/scenarios.go b/tests/robustness/scenarios/scenarios.go
index 9be207f82..cac904281 100644
--- a/tests/robustness/scenarios/scenarios.go
+++ b/tests/robustness/scenarios/scenarios.go
@@ -243,7 +243,8 @@ func Regression(t *testing.T) []TestScenario {
 			MinimalQPS:                     50,
 			MaximalQPS:                     100,
 			BurstableQPS:                   100,
-			ClientCount:                    8,
+			MemberClientCount:              6,
+			ClusterClientCount:             2,
 			MaxNonUniqueRequestConcurrency: 3,
 		}.WithoutCompaction(),
 		Failpoint: failpoint.BatchCompactBeforeSetFinishedCompactPanic,
diff --git a/tests/robustness/traffic/etcd.go b/tests/robustness/traffic/etcd.go
index b2e753fe9..5483e48bb 100644
--- a/tests/robustness/traffic/etcd.go
+++ b/tests/robustness/traffic/etcd.go
@@ -75,21 +75,6 @@ var (
 			{Choice: Delete, Weight: 50},
 		},
 	}
-	EtcdAntithesis Traffic = etcdTraffic{
-		keyCount:     10,
-		largePutSize: 32769,
-		leaseTTL:     DefaultLeaseTTL,
-		// Please keep the sum of weights equal 100.
-		requests: []random.ChoiceWeight[etcdRequestType]{
-			{Choice: Get, Weight: 30},
-			{Choice: Put, Weight: 35},
-			{Choice: StaleGet, Weight: 10},
-			{Choice: StaleList, Weight: 10},
-			{Choice: Delete, Weight: 5},
-			{Choice: MultiOpTxn, Weight: 5},
-			{Choice: PutWithLease, Weight: 5},
-		},
-	}
 )
 
 type etcdTraffic struct {
@@ -124,13 +109,13 @@ func (t etcdTraffic) Name() string {
 	return "Etcd"
 }
 
-func (t etcdTraffic) RunTrafficLoop(ctx context.Context, c *client.RecordingClient, limiter *rate.Limiter, ids identity.Provider, lm identity.LeaseIDStorage, nonUniqueWriteLimiter ConcurrencyLimiter, finish <-chan struct{}) {
+func (t etcdTraffic) RunTrafficLoop(ctx context.Context, c *client.RecordingClient, limiter *rate.Limiter, ids identity.Provider, lm identity.LeaseIDStorage, nonUniqueWriteLimiter ConcurrencyLimiter, keyStore *keyStore, finish <-chan struct{}) {
 	lastOperationSucceeded := true
 	var lastRev int64
 	var requestType etcdRequestType
 	client := etcdTrafficClient{
 		etcdTraffic:  t,
-		keyPrefix:    "key",
+		keyStore:     keyStore,
 		client:       c,
 		limiter:      limiter,
 		idProvider:   ids,
@@ -211,7 +196,7 @@ func filterOutNonUniqueEtcdWrites(choices []random.ChoiceWeight[etcdRequestType]
 
 type etcdTrafficClient struct {
 	etcdTraffic
-	keyPrefix    string
+	keyStore     *keyStore
 	client       *client.RecordingClient
 	limiter      *rate.Limiter
 	idProvider   identity.Provider
@@ -226,57 +211,58 @@ func (c etcdTrafficClient) Request(ctx context.Context, request etcdRequestType,
 	switch request {
 	case StaleGet:
 		var resp *clientv3.GetResponse
-		resp, err = c.client.Get(opCtx, c.randomKey(), clientv3.WithRev(lastRev))
+		resp, err = c.client.Get(opCtx, c.keyStore.GetKey(), clientv3.WithRev(lastRev))
 		if err == nil {
 			rev = resp.Header.Revision
 		}
 	case Get:
 		var resp *clientv3.GetResponse
-		resp, err = c.client.Get(opCtx, c.randomKey(), clientv3.WithRev(0))
+		resp, err = c.client.Get(opCtx, c.keyStore.GetKey(), clientv3.WithRev(0))
 		if err == nil {
 			rev = resp.Header.Revision
 		}
 	case List:
 		var resp *clientv3.GetResponse
-		resp, err = c.client.Range(ctx, c.keyPrefix, clientv3.GetPrefixRangeEnd(c.keyPrefix), 0, limit)
+		resp, err = c.client.Range(ctx, c.keyStore.GetPrefix(), clientv3.GetPrefixRangeEnd(c.keyStore.GetPrefix()), 0, limit)
 		if resp != nil {
+			c.keyStore.SyncKeys(resp)
 			rev = resp.Header.Revision
 		}
 	case StaleList:
 		var resp *clientv3.GetResponse
-		resp, err = c.client.Range(ctx, c.keyPrefix, clientv3.GetPrefixRangeEnd(c.keyPrefix), lastRev, limit)
+		resp, err = c.client.Range(ctx, c.keyStore.GetPrefix(), clientv3.GetPrefixRangeEnd(c.keyStore.GetPrefix()), lastRev, limit)
 		if resp != nil {
 			rev = resp.Header.Revision
 		}
 	case Put:
 		var resp *clientv3.PutResponse
-		resp, err = c.client.Put(opCtx, c.randomKey(), fmt.Sprintf("%d", c.idProvider.NewRequestID()))
+		resp, err = c.client.Put(opCtx, c.keyStore.GetKey(), fmt.Sprintf("%d", c.idProvider.NewRequestID()))
 		if resp != nil {
 			rev = resp.Header.Revision
 		}
 	case LargePut:
 		var resp *clientv3.PutResponse
-		resp, err = c.client.Put(opCtx, c.randomKey(), random.RandString(c.largePutSize))
+		resp, err = c.client.Put(opCtx, c.keyStore.GetKey(), random.RandString(c.largePutSize))
 		if resp != nil {
 			rev = resp.Header.Revision
 		}
 	case Delete:
 		var resp *clientv3.DeleteResponse
-		resp, err = c.client.Delete(opCtx, c.randomKey())
+		resp, err = c.client.Delete(opCtx, c.keyStore.GetKeyForDelete())
 		if resp != nil {
 			rev = resp.Header.Revision
 		}
 	case MultiOpTxn:
 		var resp *clientv3.TxnResponse
 		resp, err = c.client.Txn(opCtx).Then(
-			c.pickMultiTxnOps()...,
+			c.pickMultiTxnOps(c.keyStore)...,
 		).Commit()
 		if resp != nil {
 			rev = resp.Header.Revision
 		}
 	case CompareAndSet:
 		var kv *mvccpb.KeyValue
-		key := c.randomKey()
+		key := c.keyStore.GetKey()
 		var resp *clientv3.GetResponse
 		resp, err = c.client.Get(opCtx, key, clientv3.WithRev(0))
 		if err == nil {
@@ -318,7 +304,7 @@ func (c etcdTrafficClient) Request(ctx context.Context, request etcdRequestType,
 		if leaseID != 0 {
 			putCtx, putCancel := context.WithTimeout(ctx, RequestTimeout)
 			var resp *clientv3.PutResponse
-			resp, err = c.client.PutWithLease(putCtx, c.randomKey(), fmt.Sprintf("%d", c.idProvider.NewRequestID()), leaseID)
+			resp, err = c.client.PutWithLease(putCtx, c.keyStore.GetKey(), fmt.Sprintf("%d", c.idProvider.NewRequestID()), leaseID)
 			putCancel()
 			if resp != nil {
 				rev = resp.Header.Revision
@@ -349,8 +335,7 @@ func (c etcdTrafficClient) Request(ctx context.Context, request etcdRequestType,
 	return rev, err
 }
 
-func (c etcdTrafficClient) pickMultiTxnOps() (ops []clientv3.Op) {
-	keys := rand.Perm(c.keyCount)
+func (c etcdTrafficClient) pickMultiTxnOps(keyStore *keyStore) (ops []clientv3.Op) {
 	opTypes := make([]model.OperationType, 4)
 
 	atLeastOnePut := false
@@ -365,8 +350,10 @@ func (c etcdTrafficClient) pickMultiTxnOps() (ops []clientv3.Op) {
 		opTypes[0] = model.PutOperation
 	}
 
+	keys := keyStore.GetKeysForMultiTxnOps(opTypes)
+
 	for i, opType := range opTypes {
-		key := c.key(keys[i])
+		key := keys[i]
 		switch opType {
 		case model.RangeOperation:
 			ops = append(ops, clientv3.OpGet(key))
@@ -382,14 +369,6 @@ func (c etcdTrafficClient) pickMultiTxnOps() (ops []clientv3.Op) {
 	return ops
 }
 
-func (c etcdTrafficClient) randomKey() string {
-	return c.key(rand.Int())
-}
-
-func (c etcdTrafficClient) key(i int) string {
-	return fmt.Sprintf("%s%d", c.keyPrefix, i%c.keyCount)
-}
-
 func (t etcdTraffic) pickOperationType() model.OperationType {
 	roll := rand.Int() % 100
 	if roll < 10 {
diff --git a/tests/robustness/traffic/key_store.go b/tests/robustness/traffic/key_store.go
new file mode 100644
index 000000000..95b6769d0
--- /dev/null
+++ b/tests/robustness/traffic/key_store.go
@@ -0,0 +1,164 @@
+// Copyright 2023 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package traffic
+
+import (
+	"fmt"
+	"math/rand"
+	"slices"
+	"sort"
+	"strconv"
+	"sync"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/tests/v3/robustness/model"
+)
+
+// Stores the key pool to use for operations. This allows keys used in Put and Delete operations to be more unique by probabilistically swapping out used keys.
+type keyStore struct {
+	mu sync.Mutex
+
+	counter        int
+	keys           []string
+	keyPrefix      string
+	latestRevision int64
+}
+
+func NewKeyStore(size int, keyPrefix string) *keyStore {
+	k := &keyStore{
+		keys:      make([]string, size),
+		counter:   0,
+		keyPrefix: keyPrefix,
+	}
+
+	// Fill with default values i.e. key0-key9
+	for ; k.counter < len(k.keys); k.counter++ {
+		k.keys[k.counter] = fmt.Sprintf("%s%d", k.keyPrefix, k.counter)
+	}
+
+	return k
+}
+
+func (k *keyStore) GetKey() string {
+	k.mu.Lock()
+	defer k.mu.Unlock()
+
+	useKey := k.keys[rand.Intn(len(k.keys))]
+
+	return useKey
+}
+
+func (k *keyStore) GetKeyForDelete() string {
+	k.mu.Lock()
+	defer k.mu.Unlock()
+
+	useKeyIndex := rand.Intn(len(k.keys))
+	useKey := k.keys[useKeyIndex]
+
+	k.replaceKey(useKeyIndex)
+
+	return useKey
+}
+
+func (k *keyStore) GetKeysForMultiTxnOps(ops []model.OperationType) []string {
+	k.mu.Lock()
+	defer k.mu.Unlock()
+
+	numOps := len(ops)
+
+	if numOps > len(k.keys) {
+		panic("GetKeysForMultiTxnOps: number of operations is more than the key pool size")
+	}
+
+	keys := make([]string, numOps)
+
+	permutedKeyIndexes := rand.Perm(len(k.keys))
+	for i, op := range ops {
+		keys[i] = k.keys[permutedKeyIndexes[i]]
+
+		if op == model.DeleteOperation {
+			k.replaceKey(permutedKeyIndexes[i])
+		}
+	}
+
+	return keys
+}
+
+func (k *keyStore) GetPrefix() string {
+	k.mu.Lock()
+	defer k.mu.Unlock()
+
+	return k.keyPrefix
+}
+
+// SyncKeys reconciles our local key store with the keys currently in etcd.
+//
+// SyncKeys resolves this by:
+//  1. Getting the list request result of all the keys.
+//  2. Adding any keys that exist in etcd but are missing in the key store.
+//  3. Maintaining the key store size by removing the higher numbered keys.
+//
+// Notice that higher numbered keys will eventually be added
+// again into the keystore, so it is safe to temporarily remove them from the.
+// key store.
+func (k *keyStore) SyncKeys(resp *clientv3.GetResponse) {
+	k.mu.Lock()
+	defer k.mu.Unlock()
+
+	if resp.Header.GetRevision() < k.latestRevision {
+		return
+	}
+
+	k.latestRevision = resp.Header.GetRevision()
+
+	listKeys := make([]string, len(resp.Kvs))
+	for i, kv := range resp.Kvs {
+		listKeys[i] = string(kv.Key)
+	}
+
+	for _, key := range k.keys {
+		if !slices.Contains(listKeys, key) {
+			listKeys = append(listKeys, key)
+		}
+	}
+
+	sort.Slice(listKeys, func(i, j int) bool {
+		keyNumI := k.getKeyNum(listKeys[i])
+		keyNumJ := k.getKeyNum(listKeys[j])
+		return keyNumI < keyNumJ
+	})
+
+	k.keys = listKeys[:len(k.keys)]
+
+	lastKeyNum := k.getKeyNum(k.keys[len(k.keys)-1])
+	k.counter = lastKeyNum + 1
+}
+
+func (k *keyStore) getKeyNum(key string) int {
+	if len(key) < len(k.keyPrefix) {
+		return 0
+	}
+
+	numStr := key[len(k.keyPrefix):]
+	num, _ := strconv.Atoi(numStr)
+	return num
+}
+
+func (k *keyStore) replaceKey(index int) {
+	if rand.Intn(100) < 90 {
+		k.keys[index] = fmt.Sprintf("%s%d", k.keyPrefix, k.counter)
+		k.counter++
+	}
+}
diff --git a/tests/robustness/traffic/kubernetes.go b/tests/robustness/traffic/kubernetes.go
index fe1386fa2..8d0373919 100644
--- a/tests/robustness/traffic/kubernetes.go
+++ b/tests/robustness/traffic/kubernetes.go
@@ -71,7 +71,7 @@ func (t kubernetesTraffic) ExpectUniqueRevision() bool {
 	return true
 }
 
-func (t kubernetesTraffic) RunTrafficLoop(ctx context.Context, c *client.RecordingClient, limiter *rate.Limiter, ids identity.Provider, lm identity.LeaseIDStorage, nonUniqueWriteLimiter ConcurrencyLimiter, finish <-chan struct{}) {
+func (t kubernetesTraffic) RunTrafficLoop(ctx context.Context, c *client.RecordingClient, limiter *rate.Limiter, ids identity.Provider, lm identity.LeaseIDStorage, nonUniqueWriteLimiter ConcurrencyLimiter, keyStore *keyStore, finish <-chan struct{}) {
 	kc := kubernetes.Client{Client: &clientv3.Client{KV: c}}
 	s := newStorage()
 	keyPrefix := "/registry/" + t.resource + "/"
diff --git a/tests/robustness/traffic/traffic.go b/tests/robustness/traffic/traffic.go
index 102a2b380..4ae8710c8 100644
--- a/tests/robustness/traffic/traffic.go
+++ b/tests/robustness/traffic/traffic.go
@@ -43,52 +43,65 @@ var (
 		MinimalQPS:                     100,
 		MaximalQPS:                     200,
 		BurstableQPS:                   1000,
-		ClientCount:                    8,
+		MemberClientCount:              6,
+		ClusterClientCount:             2,
 		MaxNonUniqueRequestConcurrency: 3,
 	}
 	HighTrafficProfile = Profile{
 		MinimalQPS:                     100,
 		MaximalQPS:                     1000,
 		BurstableQPS:                   1000,
-		ClientCount:                    8,
+		MemberClientCount:              6,
+		ClusterClientCount:             2,
 		MaxNonUniqueRequestConcurrency: 3,
 	}
 )
 
-func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2e.EtcdProcessCluster, profile Profile, traffic Traffic, failpointInjected <-chan report.FailpointInjection, baseTime time.Time, ids identity.Provider) []report.ClientReport {
-	mux := sync.Mutex{}
+func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2e.EtcdProcessCluster, profile Profile, traffic Traffic, failpointInjected <-chan report.FailpointInjection, clientSet *client.ClientSet) []report.ClientReport {
 	endpoints := clus.EndpointsGRPC()
 
 	lm := identity.NewLeaseIDStorage()
 	// Use the highest MaximalQPS of all traffic profiles as burst otherwise actual traffic may be accidentally limited
 	limiter := rate.NewLimiter(rate.Limit(profile.MaximalQPS), profile.BurstableQPS)
 
-	r, err := CheckEmptyDatabaseAtStart(ctx, lg, endpoints, ids, baseTime)
+	err := CheckEmptyDatabaseAtStart(ctx, lg, endpoints, clientSet)
 	require.NoError(t, err)
-	reports := []report.ClientReport{r}
 
 	wg := sync.WaitGroup{}
 	nonUniqueWriteLimiter := NewConcurrencyLimiter(profile.MaxNonUniqueRequestConcurrency)
 	finish := make(chan struct{})
+
+	keyStore := NewKeyStore(10, "key")
+
 	lg.Info("Start traffic")
-	startTime := time.Since(baseTime)
-	for i := 0; i < profile.ClientCount; i++ {
+	startTime := time.Since(clientSet.BaseTime())
+	for i := range profile.MemberClientCount {
+		wg.Add(1)
+
+		c, nerr := clientSet.NewClient([]string{endpoints[i%len(endpoints)]})
+		require.NoError(t, nerr)
+		go func(c *client.RecordingClient) {
+			defer wg.Done()
+			defer c.Close()
+
+			traffic.RunTrafficLoop(ctx, c, limiter, clientSet.IdentityProvider(), lm, nonUniqueWriteLimiter, keyStore, finish)
+		}(c)
+	}
+	for range profile.ClusterClientCount {
 		wg.Add(1)
-		c, nerr := client.NewRecordingClient([]string{endpoints[i%len(endpoints)]}, ids, baseTime)
+
+		c, nerr := clientSet.NewClient(endpoints)
 		require.NoError(t, nerr)
 		go func(c *client.RecordingClient) {
 			defer wg.Done()
 			defer c.Close()
 
-			traffic.RunTrafficLoop(ctx, c, limiter, ids, lm, nonUniqueWriteLimiter, finish)
-			mux.Lock()
-			reports = append(reports, c.Report())
-			mux.Unlock()
+			traffic.RunTrafficLoop(ctx, c, limiter, clientSet.IdentityProvider(), lm, nonUniqueWriteLimiter, keyStore, finish)
 		}(c)
 	}
 	if !profile.ForbidCompaction {
 		wg.Add(1)
-		c, nerr := client.NewRecordingClient(endpoints, ids, baseTime)
+		c, nerr := clientSet.NewClient(endpoints)
 		if nerr != nil {
 			t.Fatal(nerr)
 		}
@@ -102,9 +115,6 @@ func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2
 			}
 
 			traffic.RunCompactLoop(ctx, c, compactionPeriod, finish)
-			mux.Lock()
-			reports = append(reports, c.Report())
-			mux.Unlock()
 		}(c)
 	}
 	var fr *report.FailpointInjection
@@ -118,16 +128,16 @@ func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2
 	close(finish)
 	wg.Wait()
 	lg.Info("Finished traffic")
-	endTime := time.Since(baseTime)
+	endTime := time.Since(clientSet.BaseTime())
 
 	time.Sleep(time.Second)
 	// Ensure that last operation succeeds
-	cc, err := client.NewRecordingClient(endpoints, ids, baseTime)
+	cc, err := clientSet.NewClient(endpoints)
 	require.NoError(t, err)
 	defer cc.Close()
 	_, err = cc.Put(ctx, "tombstone", "true")
 	require.NoErrorf(t, err, "Last operation failed, validation requires last operation to succeed")
-	reports = append(reports, cc.Report())
+	reports := clientSet.Reports()
 
 	totalStats := CalculateStats(reports, startTime, endTime)
 	beforeFailpointStats := CalculateStats(reports, startTime, fr.Start)
@@ -183,7 +193,8 @@ type Profile struct {
 	MaximalQPS                     float64
 	BurstableQPS                   int
 	MaxNonUniqueRequestConcurrency int
-	ClientCount                    int
+	MemberClientCount              int
+	ClusterClientCount             int
 	ForbidCompaction               bool
 	CompactPeriod                  time.Duration
 }
@@ -199,15 +210,15 @@ func (p Profile) WithCompactionPeriod(cp time.Duration) Profile {
 }
 
 type Traffic interface {
-	RunTrafficLoop(ctx context.Context, c *client.RecordingClient, qpsLimiter *rate.Limiter, ids identity.Provider, lm identity.LeaseIDStorage, nonUniqueWriteLimiter ConcurrencyLimiter, finish <-chan struct{})
+	RunTrafficLoop(ctx context.Context, c *client.RecordingClient, qpsLimiter *rate.Limiter, ids identity.Provider, lm identity.LeaseIDStorage, nonUniqueWriteLimiter ConcurrencyLimiter, keyStore *keyStore, finish <-chan struct{})
 	RunCompactLoop(ctx context.Context, c *client.RecordingClient, period time.Duration, finish <-chan struct{})
 	ExpectUniqueRevision() bool
 }
 
-func CheckEmptyDatabaseAtStart(ctx context.Context, lg *zap.Logger, endpoints []string, ids identity.Provider, baseTime time.Time) (report.ClientReport, error) {
-	c, err := client.NewRecordingClient(endpoints, ids, baseTime)
+func CheckEmptyDatabaseAtStart(ctx context.Context, lg *zap.Logger, endpoints []string, cs *client.ClientSet) error {
+	c, err := cs.NewClient(endpoints)
 	if err != nil {
-		return report.ClientReport{}, err
+		return err
 	}
 	defer c.Close()
 	for {
@@ -219,9 +230,9 @@ func CheckEmptyDatabaseAtStart(ctx context.Context, lg *zap.Logger, endpoints []
 			continue
 		}
 		if resp.Header.Revision != 1 {
-			return report.ClientReport{}, validate.ErrNotEmptyDatabase
+			return validate.ErrNotEmptyDatabase
 		}
 		break
 	}
-	return c.Report(), nil
+	return nil
 }
diff --git a/tests/robustness/validate/operations.go b/tests/robustness/validate/operations.go
index 2d5597d57..79de6b48f 100644
--- a/tests/robustness/validate/operations.go
+++ b/tests/robustness/validate/operations.go
@@ -16,7 +16,6 @@ package validate
 
 import (
 	"errors"
-	"fmt"
 	"time"
 
 	"github.com/anishathalye/porcupine"
@@ -31,63 +30,46 @@ var (
 	errFutureRevRespRequested = errors.New("request about a future rev with response")
 )
 
-type Result struct {
-	Assumptions       error
-	Linearization     LinearizationResult
-	WatchError        error
-	SerializableError error
-}
-
-func (r Result) Error() error {
-	if r.Assumptions != nil {
-		return fmt.Errorf("validation assumptions failed: %w", r.Assumptions)
+func validateLinearizableOperationsAndVisualize(lg *zap.Logger, operations []porcupine.Operation, timeout time.Duration) LinearizationResult {
+	lg.Info("Validating linearizable operations", zap.Duration("timeout", timeout))
+	start := time.Now()
+	check, info := porcupine.CheckOperationsVerbose(model.NonDeterministicModel, operations, timeout)
+	result := LinearizationResult{
+		Info:  info,
+		Model: model.NonDeterministicModel,
 	}
-	switch r.Linearization.Linearizable {
-	case porcupine.Illegal:
-		return errors.New("linearization failed")
-	case porcupine.Unknown:
-		return errors.New("linearization timed out")
+	switch check {
 	case porcupine.Ok:
+		result.Status = Success
+		lg.Info("Linearization success", zap.Duration("duration", time.Since(start)))
+	case porcupine.Unknown:
+		result.Status = Failure
+		result.Message = "timed out"
+		result.Timeout = true
+		lg.Error("Linearization timed out", zap.Duration("duration", time.Since(start)))
+	case porcupine.Illegal:
+		result.Status = Failure
+		result.Message = "illegal"
+		lg.Error("Linearization illegal", zap.Duration("duration", time.Since(start)))
 	default:
-		return fmt.Errorf("unknown linearization result %q", r.Linearization.Linearizable)
-	}
-	if r.WatchError != nil {
-		return fmt.Errorf("watch validation failed: %w", r.WatchError)
-	}
-	if r.SerializableError != nil {
-		return fmt.Errorf("serializable validation failed: %w", r.SerializableError)
+		result.Status = Failure
+		result.Message = "unknown"
 	}
-	return nil
-}
-
-type LinearizationResult struct {
-	Info         porcupine.LinearizationInfo
-	Model        porcupine.Model
-	Linearizable porcupine.CheckResult
+	return result
 }
 
-func (r LinearizationResult) Visualize(lg *zap.Logger, path string) error {
-	lg.Info("Saving visualization", zap.String("path", path))
-	err := porcupine.VisualizePath(r.Model, r.Info, path)
+func validateSerializableOperations(lg *zap.Logger, operations []porcupine.Operation, replay *model.EtcdReplay) Result {
+	lg.Info("Validating serializable operations")
+	start := time.Now()
+	err := validateSerializableOperationsError(lg, operations, replay)
 	if err != nil {
-		return fmt.Errorf("failed to visualize, err: %w", err)
-	}
-	return nil
-}
-
-func validateLinearizableOperationsAndVisualize(
-	operations []porcupine.Operation,
-	timeout time.Duration,
-) (results LinearizationResult) {
-	result, info := porcupine.CheckOperationsVerbose(model.NonDeterministicModel, operations, timeout)
-	return LinearizationResult{
-		Info:         info,
-		Model:        model.NonDeterministicModel,
-		Linearizable: result,
+		lg.Error("Serializable validation failed", zap.Duration("duration", time.Since(start)), zap.Error(err))
 	}
+	lg.Info("Serializable validation success", zap.Duration("duration", time.Since(start)))
+	return ResultFromError(err)
 }
 
-func validateSerializableOperations(lg *zap.Logger, operations []porcupine.Operation, replay *model.EtcdReplay) (lastErr error) {
+func validateSerializableOperationsError(lg *zap.Logger, operations []porcupine.Operation, replay *model.EtcdReplay) (lastErr error) {
 	for _, read := range operations {
 		request := read.Input.(model.EtcdRequest)
 		response := read.Output.(model.MaybeEtcdResponse)
diff --git a/tests/robustness/validate/operations_test.go b/tests/robustness/validate/operations_test.go
index d096be6f1..e5f8cbe28 100644
--- a/tests/robustness/validate/operations_test.go
+++ b/tests/robustness/validate/operations_test.go
@@ -241,13 +241,9 @@ func TestValidateSerializableOperations(t *testing.T) {
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
 			replay := model.NewReplay(tc.persistedRequests)
-			err := validateSerializableOperations(zaptest.NewLogger(t), tc.operations, replay)
-			var errStr string
-			if err != nil {
-				errStr = err.Error()
-			}
-			if errStr != tc.expectError {
-				t.Errorf("validateSerializableOperations(...), got: %q, want: %q", err, tc.expectError)
+			result := validateSerializableOperations(zaptest.NewLogger(t), tc.operations, replay)
+			if result.Message != tc.expectError {
+				t.Errorf("validateSerializableOperations(...), got: %q, want: %q", result.Message, tc.expectError)
 			}
 		})
 	}
@@ -395,9 +391,9 @@ func shuffleHistory(history []porcupine.Operation, shuffleCount int) [][]porcupi
 
 func validateShuffles(b *testing.B, lg *zap.Logger, shuffles [][]porcupine.Operation, duration time.Duration) {
 	for i := 0; i < len(shuffles); i++ {
-		result := validateLinearizableOperationsAndVisualize(shuffles[i], duration)
-		if result.Linearizable != porcupine.Ok {
-			b.Fatalf("Not linearizable: %v", result.Linearizable)
+		result := validateLinearizableOperationsAndVisualize(lg, shuffles[i], duration)
+		if err := result.Error(); err != nil {
+			b.Fatalf("Not linearizable: %v", err)
 		}
 	}
 }
diff --git a/tests/robustness/validate/patch_history.go b/tests/robustness/validate/patch_history.go
index da75212b8..ac5df56c2 100644
--- a/tests/robustness/validate/patch_history.go
+++ b/tests/robustness/validate/patch_history.go
@@ -164,6 +164,7 @@ func uniquePutReturnTime(allOperations []porcupine.Operation, persistedRequests
 		case model.LeaseGrant:
 		case model.LeaseRevoke:
 		case model.Compact:
+		case model.Defragment:
 		default:
 			panic(fmt.Sprintf("Unknown request type: %q", request.Type))
 		}
diff --git a/tests/robustness/validate/patch_history_test.go b/tests/robustness/validate/patch_history_test.go
index 0ab448ea0..45294d3b8 100644
--- a/tests/robustness/validate/patch_history_test.go
+++ b/tests/robustness/validate/patch_history_test.go
@@ -412,7 +412,7 @@ func TestPatchHistory(t *testing.T) {
 					Watch:    tc.watchOperations,
 				},
 			}
-			operations, _ := prepareAndCategorizeOperations(reports)
+			operations, _, _ := prepareAndCategorizeOperations(reports)
 			patched := patchLinearizableOperations(operations, reports, tc.persistedRequest)
 			if diff := cmp.Diff(tc.expectedRemainingOperations, patched,
 				cmpopts.EquateEmpty(),
diff --git a/tests/robustness/validate/result.go b/tests/robustness/validate/result.go
new file mode 100644
index 000000000..f0d18ce8e
--- /dev/null
+++ b/tests/robustness/validate/result.go
@@ -0,0 +1,110 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package validate
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/anishathalye/porcupine"
+	"go.uber.org/zap"
+)
+
+type RobustnessResult struct {
+	Assumptions   Result
+	Linearization LinearizationResult
+	Watch         Result
+	Serializable  Result
+}
+
+type Result struct {
+	Status  ResultStatus
+	Message string
+}
+
+type ResultStatus string
+
+var (
+	Unknown ResultStatus
+	Success ResultStatus = "Success"
+	Failure ResultStatus = "Failure"
+)
+
+func (r RobustnessResult) Error() error {
+	if err := r.Assumptions.Error(); err != nil {
+		return fmt.Errorf("assumptions: %w", err)
+	}
+	if err := r.Linearization.Error(); err != nil {
+		return fmt.Errorf("linearization: %w", err)
+	}
+	if err := r.Watch.Error(); err != nil {
+		return fmt.Errorf("watch: %w", err)
+	}
+	if err := r.Serializable.Error(); err != nil {
+		return fmt.Errorf("serializable: %w", err)
+	}
+	return nil
+}
+
+func ResultFromError(err error) Result {
+	if err != nil {
+		return Result{
+			Status:  Failure,
+			Message: err.Error(),
+		}
+	}
+	return Result{
+		Status: Success,
+	}
+}
+
+func (r Result) Error() error {
+	if r.Status == Failure {
+		if r.Message != "" {
+			return errors.New(r.Message)
+		}
+		return errors.New("failure")
+	}
+	return nil
+}
+
+type LinearizationResult struct {
+	Info  porcupine.LinearizationInfo
+	Model porcupine.Model
+	Result
+	Timeout bool
+}
+
+func (r *LinearizationResult) Visualize(lg *zap.Logger, path string) error {
+	lg.Info("Saving visualization", zap.String("path", path))
+	err := porcupine.VisualizePath(r.Model, r.Info, path)
+	if err != nil {
+		return fmt.Errorf("failed to visualize, err: %w", err)
+	}
+	return nil
+}
+
+func (r *LinearizationResult) AddToVisualization(serializable []porcupine.Operation) {
+	annotations := []porcupine.Annotation{}
+	for _, op := range serializable {
+		annotations = append(annotations, porcupine.Annotation{
+			ClientId:    op.ClientId,
+			Start:       op.Call,
+			End:         op.Return,
+			Description: r.Model.DescribeOperation(op.Input, op.Output),
+		})
+	}
+	r.Info.AddAnnotations(annotations)
+}
diff --git a/tests/robustness/validate/validate.go b/tests/robustness/validate/validate.go
index 1b9ed7790..3b3af2f8e 100644
--- a/tests/robustness/validate/validate.go
+++ b/tests/robustness/validate/validate.go
@@ -29,12 +29,12 @@ import (
 
 var ErrNotEmptyDatabase = errors.New("non empty database at start, required by model used for linearizability validation")
 
-func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.ClientReport, persistedRequests []model.EtcdRequest, timeout time.Duration) (result Result) {
-	result.Assumptions = checkValidationAssumptions(reports)
-	if result.Assumptions != nil {
+func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.ClientReport, persistedRequests []model.EtcdRequest, timeout time.Duration) (result RobustnessResult) {
+	result.Assumptions = ResultFromError(checkValidationAssumptions(reports))
+	if result.Assumptions.Error() != nil {
 		return result
 	}
-	linearizableOperations, serializableOperations := prepareAndCategorizeOperations(reports)
+	linearizableOperations, serializableOperations, operationsForVisualization := prepareAndCategorizeOperations(reports)
 	// We are passing in the original reports and linearizableOperations with modified return time.
 	// The reason is that linearizableOperations are those dedicated for linearization, which requires them to have returnTime set to infinity as required by pourcupine.
 	// As for the report, the original report is used so the consumer doesn't need to track what patching was done or not.
@@ -42,22 +42,10 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli
 		linearizableOperations = patchLinearizableOperations(linearizableOperations, reports, persistedRequests)
 	}
 
-	lg.Info("Validating linearizable operations", zap.Duration("timeout", timeout))
-	start := time.Now()
-	result.Linearization = validateLinearizableOperationsAndVisualize(linearizableOperations, timeout)
-	switch result.Linearization.Linearizable {
-	case porcupine.Illegal:
-		lg.Error("Linearization failed", zap.Duration("duration", time.Since(start)))
-	case porcupine.Unknown:
-		lg.Error("Linearization has timed out", zap.Duration("duration", time.Since(start)))
-	case porcupine.Ok:
-		lg.Info("Linearization success", zap.Duration("duration", time.Since(start)))
-	default:
-		panic(fmt.Sprintf("Unknown Linearization result %s", result.Linearization.Linearizable))
-	}
-
+	result.Linearization = validateLinearizableOperationsAndVisualize(lg, linearizableOperations, timeout)
+	result.Linearization.AddToVisualization(operationsForVisualization)
 	// Skip other validations if model is not linearizable, as they are expected to fail too and obfuscate the logs.
-	if result.Linearization.Linearizable != porcupine.Ok {
+	if result.Linearization.Error() != nil {
 		lg.Info("Skipping other validations as linearization failed")
 		return result
 	}
@@ -66,25 +54,8 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli
 		return result
 	}
 	replay := model.NewReplay(persistedRequests)
-
-	lg.Info("Validating watch")
-	start = time.Now()
-	result.WatchError = validateWatch(lg, cfg, reports, replay)
-	if result.WatchError == nil {
-		lg.Info("Watch validation success", zap.Duration("duration", time.Since(start)))
-	} else {
-		lg.Error("Watch validation failed", zap.Duration("duration", time.Since(start)), zap.Error(result.WatchError))
-	}
-
-	lg.Info("Validating serializable operations")
-	start = time.Now()
-	result.SerializableError = validateSerializableOperations(lg, serializableOperations, replay)
-	if result.SerializableError == nil {
-		lg.Info("Serializable validation success", zap.Duration("duration", time.Since(start)))
-	} else {
-		lg.Error("Serializable validation failed", zap.Duration("duration", time.Since(start)), zap.Error(result.SerializableError))
-	}
-
+	result.Watch = validateWatch(lg, cfg, reports, replay)
+	result.Serializable = validateSerializableOperations(lg, serializableOperations, replay)
 	return result
 }
 
@@ -92,27 +63,52 @@ type Config struct {
 	ExpectRevisionUnique bool
 }
 
-func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable []porcupine.Operation, serializable []porcupine.Operation) {
+func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable, serializable, forVisualization []porcupine.Operation) {
 	for _, report := range reports {
 		for _, op := range report.KeyValue {
 			request := op.Input.(model.EtcdRequest)
 			response := op.Output.(model.MaybeEtcdResponse)
-			// serializable operations include only Range requests on non-zero revision
-			if request.Type == model.Range && request.Range.Revision != 0 {
+			if isSerializable(request, response) {
 				serializable = append(serializable, op)
 			}
-			// Remove failed read requests as they are not relevant for linearization.
-			if response.Error == "" || !request.IsRead() {
-				// For linearization, we set the return time of failed requests to MaxInt64.
-				// Failed requests can still be persisted, however we don't know when request has taken effect.
-				if response.Error != "" {
-					op.Return = math.MaxInt64
-				}
-				linearizable = append(linearizable, op)
+			// Operation that will not be linearized need to be added separetly to visualization.
+			if !isLinearizable(request, response) {
+				forVisualization = append(forVisualization, op)
+				continue
+			}
+			// For linearization, we set the return time of failed requests to MaxInt64.
+			// Failed requests can still be persisted, however we don't know when request has taken effect.
+			if response.Error != "" {
+				op.Return = math.MaxInt64
 			}
+			linearizable = append(linearizable, op)
 		}
 	}
-	return linearizable, serializable
+	return linearizable, serializable, forVisualization
+}
+
+func isLinearizable(request model.EtcdRequest, response model.MaybeEtcdResponse) bool {
+	// Cannot test response for request without side effect.
+	if request.IsRead() && response.Error != "" {
+		return false
+	}
+	// Defragment is not linearizable
+	if request.Type == model.Defragment {
+		return false
+	}
+	return true
+}
+
+func isSerializable(request model.EtcdRequest, response model.MaybeEtcdResponse) bool {
+	// Cannot test response for request without side effect.
+	if request.IsRead() && response.Error != "" {
+		return false
+	}
+	// Test range requests about stale revision
+	if request.Type == model.Range && request.Range.Revision != 0 {
+		return true
+	}
+	return false
 }
 
 func checkValidationAssumptions(reports []report.ClientReport) error {
diff --git a/tests/robustness/validate/validate_test.go b/tests/robustness/validate/validate_test.go
index 22ff3509a..70b159590 100644
--- a/tests/robustness/validate/validate_test.go
+++ b/tests/robustness/validate/validate_test.go
@@ -169,7 +169,7 @@ func TestValidateAndReturnVisualize(t *testing.T) {
 				},
 			},
 			persistedRequests: []model.EtcdRequest{putRequest("key", "value")},
-			expectError:       "watch validation failed",
+			expectError:       "watch: broke Reliable",
 		},
 	}
 	for _, tc := range tcs {
@@ -1978,13 +1978,9 @@ func TestValidateWatch(t *testing.T) {
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
 			replay := model.NewReplay(tc.persistedRequests)
-			err := validateWatch(zaptest.NewLogger(t), tc.config, tc.reports, replay)
-			var errStr string
-			if err != nil {
-				errStr = err.Error()
-			}
-			if errStr != tc.expectError {
-				t.Errorf("validateWatch(...), got: %q, want: %q", err, tc.expectError)
+			result := validateWatch(zaptest.NewLogger(t), tc.config, tc.reports, replay)
+			if result.Message != tc.expectError {
+				t.Errorf("validateWatch(...), got: %q, want: %q", result.Message, tc.expectError)
 			}
 		})
 	}
diff --git a/tests/robustness/validate/watch.go b/tests/robustness/validate/watch.go
index bba43c66e..5f8e8b77c 100644
--- a/tests/robustness/validate/watch.go
+++ b/tests/robustness/validate/watch.go
@@ -16,6 +16,7 @@ package validate
 
 import (
 	"errors"
+	"time"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
@@ -37,7 +38,18 @@ var (
 	errBrokeFilter       = errors.New("event not matching watch filter")
 )
 
-func validateWatch(lg *zap.Logger, cfg Config, reports []report.ClientReport, replay *model.EtcdReplay) error {
+func validateWatch(lg *zap.Logger, cfg Config, reports []report.ClientReport, replay *model.EtcdReplay) Result {
+	lg.Info("Validating watch")
+	start := time.Now()
+	err := validateWatchError(lg, cfg, reports, replay)
+	if err != nil {
+		lg.Error("Watch validation failed", zap.Duration("duration", time.Since(start)), zap.Error(err))
+	}
+	lg.Info("Watch validation success", zap.Duration("duration", time.Since(start)))
+	return ResultFromError(err)
+}
+
+func validateWatchError(lg *zap.Logger, cfg Config, reports []report.ClientReport, replay *model.EtcdReplay) error {
 	// Validate etcd watch properties defined in https://etcd.io/docs/v3.6/learning/api_guarantees/#watch-apis
 	for _, r := range reports {
 		err := validateFilter(lg, r)