Repository: dragonflyoss/Dragonfly2
Pull request #1020 (2022-01-20 13:05:32)

CDN RPC server test
Signed-off-by: sunwp <244372610@qq.com>

<!--- Provide a general summary of your changes in the Title above -->

## Description
add cdn rpc server test

<!--- Describe your changes in detail -->

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## Screenshots (if appropriate):

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] Code compiles correctly.
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.


codecov-commenter: # [Codecov](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) Report
> Merging [#1020](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) (77dc47b) into [main](https://codecov.io/gh/dragonflyoss/Dragonfly2/commit/2889ea30943165eb67f164a97c97eadcdf7dfb49?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) (2889ea3) will **increase** coverage by `0.47%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/graphs/tree.svg?width=650&height=150&src=pr&token=PUICJYSAT7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss)](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss)

```diff
@@            Coverage Diff             @@
##             main    #1020      +/-   ##
==========================================
+ Coverage   45.64%   46.11%   +0.47%     
==========================================
  Files         128      130       +2     
  Lines        9107     9311     +204     
==========================================
+ Hits         4157     4294     +137     
- Misses       4479     4533      +54     
- Partials      471      484      +13     
```

| Flag | Coverage Δ | |
|---|---|---|
| Object-compatibility-e2etests | `?` | |
| e2etests | `?` | |
| unittests | `46.11% <ø> (+0.47%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) | Coverage Δ | |
|---|---|---|
| [client/daemon/peer/peertask\_conductor.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9wZWVyL3BlZXJ0YXNrX2NvbmR1Y3Rvci5nbw==) | `56.96% <0.00%> (-0.39%)` | :arrow_down: |
| [scheduler/config/config.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-c2NoZWR1bGVyL2NvbmZpZy9jb25maWcuZ28=) | `0.00% <0.00%> (ø)` | |
| [cdn/rpcserver/rpcserver.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2RuL3JwY3NlcnZlci9ycGNzZXJ2ZXIuZ28=) | `59.37% <0.00%> (ø)` | |
| [cdn/rpcserver/config.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2RuL3JwY3NlcnZlci9jb25maWcuZ28=) | `0.00% <0.00%> (ø)` | |
| [scheduler/resource/resource.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-c2NoZWR1bGVyL3Jlc291cmNlL3Jlc291cmNlLmdv) | `82.60% <0.00%> (+0.79%)` | :arrow_up: |
| [scheduler/resource/task.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-c2NoZWR1bGVyL3Jlc291cmNlL3Rhc2suZ28=) | `81.33% <0.00%> (+1.33%)` | :arrow_up: |
| [client/daemon/peer/peertask\_manager.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9wZWVyL3BlZXJ0YXNrX21hbmFnZXIuZ28=) | `44.15% <0.00%> (+2.26%)` | :arrow_up: |
| [scheduler/service/callback.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-c2NoZWR1bGVyL3NlcnZpY2UvY2FsbGJhY2suZ28=) | `70.96% <0.00%> (+2.75%)` | :arrow_up: |
| [scheduler/resource/cdn.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-c2NoZWR1bGVyL3Jlc291cmNlL2Nkbi5nbw==) | `54.20% <0.00%> (+5.53%)` | :arrow_up: |
| [scheduler/resource/cdn\_mock.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1020/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-c2NoZWR1bGVyL3Jlc291cmNlL2Nkbl9tb2NrLmdv) | `23.21% <0.00%> (+23.21%)` | :arrow_up: |


Patch:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 7efb02cf..8a927a2b 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -2,13 +2,18 @@ name: Docker
 
 on:
   push:
+    branches:
+      - main
     tags:
       - v*
 
 jobs:
-  push_scheduler_to_registry:
-    name: Push Scheduler Image
+  push_image_to_registry:
+    name: Push Image
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        module: ["manager", "scheduler", "cdn", "dfdaemon"]
     timeout-minutes: 10
     steps:
       - name: Check out code
@@ -20,7 +25,7 @@ jobs:
         id: get_version
         run: |
           VERSION=${GITHUB_REF#refs/tags/}
-          if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
+          if [[ ${GITHUB_REF} == "refs/heads/main" ]]; then
             VERSION=latest
           fi
           echo ::set-output name=VERSION::${VERSION}
@@ -31,65 +36,9 @@ jobs:
         run: |
           echo "::set-output name=git_revision::$(git rev-parse --short HEAD)"
 
-      - name: Login Docker Hub
-        uses: docker/login-action@v1
-        with:
-          registry: docker.io
-          username: ${{ secrets.DOCKER_USERNAME }}
-          password: ${{ secrets.DOCKER_PASSWORD }}
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v1 
-        with:
-          registry: ghcr.io
-          username: ${{ github.repository_owner }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
       - name: PrepareReg Names
         run: |
-          echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
-          echo IMAGE_TAG=$(echo ${{ github.ref }} | tr '[:upper:]' '[:lower:]' | awk '{split($0,a,"/"); print a[3]}') >> $GITHUB_ENV
-
-      - name: Push to Docker Hub
-        uses: docker/build-push-action@v2
-        with:
-          context: .
-          file: build/images/scheduler/Dockerfile
-          labels: |-
-            org.opencontainers.image.source=https://github.com/${{ github.repository }}
-            org.opencontainers.image.revision=${{ github.sha }}
-          build-args: |
-            GITVERSION=git-${{ steps.vars.outputs.git_revision }}
-            VERSION=${{ steps.get_version.outputs.VERSION }}
-          tags: |
-            dragonflyoss/scheduler:${{ steps.get_version.outputs.VERSION }}
-            ghcr.io/${{ env.IMAGE_REPOSITORY }}/scheduler:${{ steps.get_version.outputs.VERSION }}
-          push: true
-
-  push_dfdaemon_to_registry:
-    name: Push Dfdaemon Image
-    runs-on: ubuntu-latest
-    timeout-minutes: 10
-    steps:
-      - name: Check out code
-        uses: actions/checkout@v2
-        with:
-          submodules: recursive
-
-      - name: Get Version
-        id: get_version
-        run: |
-          VERSION=${GITHUB_REF#refs/tags/}
-          if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
-            VERSION=latest
-          fi
-          echo ::set-output name=VERSION::${VERSION}
-
-      - name: Get Git Revision
-        id: vars
-        shell: bash
-        run: |
-          echo "::set-output name=git_revision::$(git rev-parse --short HEAD)"
+           echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
 
       - name: Login Docker Hub
         uses: docker/login-action@v1
@@ -105,136 +54,11 @@ jobs:
           username: ${{ github.repository_owner }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: PrepareReg Names
-        run: |
-          echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
-          echo IMAGE_TAG=$(echo ${{ github.ref }} | tr '[:upper:]' '[:lower:]' | awk '{split($0,a,"/"); print a[3]}') >> $GITHUB_ENV
-
-      - name: Push to Docker Hub
-        uses: docker/build-push-action@v2
-        with:
-          context: .
-          file: build/images/dfdaemon/Dockerfile
-          labels: |-
-            org.opencontainers.image.source=https://github.com/${{ github.repository }}
-            org.opencontainers.image.revision=${{ github.sha }}
-          build-args: |
-            GITVERSION=git-${{ steps.vars.outputs.git_revision }}
-            VERSION=${{ steps.get_version.outputs.VERSION }}
-          tags: |
-            dragonflyoss/dfdaemon:${{ steps.get_version.outputs.VERSION }}
-            ghcr.io/${{ env.IMAGE_REPOSITORY }}/dfdaemon:${{ steps.get_version.outputs.VERSION }}
-          push: true
-
-  push_cdn_to_registry:
-    name: Push CDN Image
-    runs-on: ubuntu-latest
-    timeout-minutes: 10
-    steps:
-      - name: Check out code
-        uses: actions/checkout@v2
-        with:
-          submodules: recursive
-
-      - name: Get Version
-        id: get_version
-        run: |
-          VERSION=${GITHUB_REF#refs/tags/}
-          if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
-            VERSION=latest
-          fi
-          echo ::set-output name=VERSION::${VERSION}
-
-      - name: Get Git Revision
-        id: vars
-        shell: bash
-        run: |
-          echo "::set-output name=git_revision::$(git rev-parse --short HEAD)"
-
-      - name: Login Docker Hub
-        uses: docker/login-action@v1
-        with:
-          registry: docker.io
-          username: ${{ secrets.DOCKER_USERNAME }}
-          password: ${{ secrets.DOCKER_PASSWORD }}
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v1 
-        with:
-          registry: ghcr.io
-          username: ${{ github.repository_owner }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: PrepareReg Names
-        run: |
-          echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
-          echo IMAGE_TAG=$(echo ${{ github.ref }} | tr '[:upper:]' '[:lower:]' | awk '{split($0,a,"/"); print a[3]}') >> $GITHUB_ENV
-
-      - name: Push to Docker Hub
-        uses: docker/build-push-action@v2
-        with:
-          context: .
-          file: build/images/cdn/Dockerfile
-          labels: |-
-            org.opencontainers.image.source=https://github.com/${{ github.repository }}
-            org.opencontainers.image.revision=${{ github.sha }}
-          build-args: |
-            GITVERSION=git-${{ steps.vars.outputs.git_revision }}
-            VERSION=${{ steps.get_version.outputs.VERSION }}
-          tags: |
-            dragonflyoss/cdn:${{ steps.get_version.outputs.VERSION }}
-            ghcr.io/${{ env.IMAGE_REPOSITORY }}/cdn:${{ steps.get_version.outputs.VERSION }}
-          push: true
-
-  push_manager_to_registry:
-    name: Push Manager Image
-    runs-on: ubuntu-latest
-    timeout-minutes: 10
-    steps:
-      - name: Check out code
-        uses: actions/checkout@v2
-        with:
-          submodules: recursive
-
-      - name: Get Version
-        id: get_version
-        run: |
-          VERSION=${GITHUB_REF#refs/tags/}
-          if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
-            VERSION=latest
-          fi
-          echo ::set-output name=VERSION::${VERSION}
-
-      - name: Get Git Revision
-        id: vars
-        shell: bash
-        run: |
-          echo "::set-output name=git_revision::$(git rev-parse --short HEAD)"
-
-      - name: Login Docker Hub
-        uses: docker/login-action@v1
-        with:
-          registry: docker.io
-          username: ${{ secrets.DOCKER_USERNAME }}
-          password: ${{ secrets.DOCKER_PASSWORD }}
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v1 
-        with:
-          registry: ghcr.io
-          username: ${{ github.repository_owner }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: PrepareReg Names
-        run: |
-          echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
-          echo IMAGE_TAG=$(echo ${{ github.ref }} | tr '[:upper:]' '[:lower:]' | awk '{split($0,a,"/"); print a[3]}') >> $GITHUB_ENV
-
-      - name: Push to Docker Hub
+      - name: Push to Registry
         uses: docker/build-push-action@v2
         with:
           context: .
-          file: build/images/manager/Dockerfile
+          file: build/images/${{ matrix.module }}/Dockerfile
           labels: |-
             org.opencontainers.image.source=https://github.com/${{ github.repository }}
             org.opencontainers.image.revision=${{ github.sha }}
@@ -242,6 +66,6 @@ jobs:
             GITVERSION=git-${{ steps.vars.outputs.git_revision }}
             VERSION=${{ steps.get_version.outputs.VERSION }}
           tags: |
-            dragonflyoss/manager:${{ steps.get_version.outputs.VERSION }}
-            ghcr.io/${{ env.IMAGE_REPOSITORY }}/manager:${{ steps.get_version.outputs.VERSION }}
+            dragonflyoss/${{ matrix.module }}:${{ steps.get_version.outputs.VERSION }}
+            ghcr.io/${{ env.IMAGE_REPOSITORY }}/${{ matrix.module }}:${{ steps.get_version.outputs.VERSION }}
           push: true
diff --git a/Makefile b/Makefile
index 1d1509b4..74001788 100644
--- a/Makefile
+++ b/Makefile
@@ -127,6 +127,12 @@ build-manager: build-dirs
 	./hack/build.sh manager
 .PHONY: build-manager
 
+# Build manager console
+build-manager-console: build-dirs
+	@echo "Begin to build manager."
+	./hack/build.sh manager-console
+.PHONY: build-manager-console
+
 # Install cdn
 install-cdn:
 	@echo "Begin to install cdn."
@@ -203,8 +209,8 @@ test-coverage:
 
 # Run github actions E2E tests with coverage
 actions-e2e-test-coverage:
-	@ginkgo -v -r --race --failFast -cover test/e2e --trace --progress
-	@cat test/e2e/*.coverprofile >> coverage.txt
+	@ginkgo -v -r --race --fail-fast -cover --trace --progress test/e2e
+	@cat coverprofile.out >> coverage.txt
 .PHONY: actions-e2e-test-coverage
 
 # Install E2E tests environment
@@ -214,13 +220,13 @@ install-e2e-test:
 
 # Run E2E tests
 e2e-test: install-e2e-test
-	@ginkgo -v -r --race --failFast test/e2e --trace --progress
+	@ginkgo -v -r --race --fail-fast --cover --trace --progress test/e2e
 .PHONY: e2e-test
 
 # Run E2E tests with coverage
 e2e-test-coverage: install-e2e-test
-	@ginkgo -v -r --race --failFast -cover test/e2e --trace --progress
-	@cat test/e2e/*.coverprofile >> coverage.txt
+	@ginkgo -v -r --race --fail-fast --cover --trace --progress test/e2e
+	@cat coverprofile.out >> coverage.txt
 .PHONY: e2e-test-coverage
 
 # Clean E2E tests
@@ -304,6 +310,7 @@ help:
 	@echo "make build-dfget-linux              build linux dfget"
 	@echo "make build-scheduler                build scheduler"
 	@echo "make build-manager                  build manager"
+	@echo "make build-manager-console          build manager console"
 	@echo "make install-cdn                    install CDN"
 	@echo "make install-dfget                  install dfget"
 	@echo "make install-scheduler              install scheduler"
diff --git a/build/images/base/README.md b/build/images/base/README.md
index fefa2790..f9a594d9 100644
--- a/build/images/base/README.md
+++ b/build/images/base/README.md
@@ -5,8 +5,15 @@
 ### Build Base Image
 
 ```shell
-BASE_IMAGE=dragonflyoss/base:bpftrace-v0.13.0-go-v1.16.6
+docker build -t dragonflyoss/base:bpftrace-v0.13.0-go-v1.17.4 -f Dockerfile .
+```
+
+### Run Debug Container
+
+```shell
+BASE_IMAGE=dragonflyoss/base:bpftrace-v0.13.0-go-v1.17.4
 
+# run debug container
 docker run -ti -v /usr/src:/usr/src:ro \
     -v /lib/modules/:/lib/modules:ro \
     -v /sys/kernel/debug/:/sys/kernel/debug:rw \
@@ -22,7 +29,7 @@ export GOTAGS="debug"
 # gcflags for dlv
 export GOGCFLAGS="all=-N -l"
 # base image
-export BASE_IMAGE=dragonflyoss/base:bpftrace-v0.13.0-go-v1.16.6
+export BASE_IMAGE=dragonflyoss/base:bpftrace-v0.13.0-go-v1.17.4
 
 make docker-build
 ```
diff --git a/cdn/supervisor/mocks/mock_cdn_service.go b/cdn/supervisor/mocks/mock_cdn_service.go
new file mode 100644
index 00000000..41100827
--- /dev/null
+++ b/cdn/supervisor/mocks/mock_cdn_service.go
@@ -0,0 +1,82 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: d7y.io/dragonfly/v2/cdn/supervisor (interfaces: CDNService)
+
+// Package progress is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	reflect "reflect"
+
+	task "d7y.io/dragonfly/v2/cdn/supervisor/task"
+	gomock "github.com/golang/mock/gomock"
+)
+
+// MockCDNService is a mock of CDNService interface.
+type MockCDNService struct {
+	ctrl     *gomock.Controller
+	recorder *MockCDNServiceMockRecorder
+}
+
+// MockCDNServiceMockRecorder is the mock recorder for MockCDNService.
+type MockCDNServiceMockRecorder struct {
+	mock *MockCDNService
+}
+
+// NewMockCDNService creates a new mock instance.
+func NewMockCDNService(ctrl *gomock.Controller) *MockCDNService {
+	mock := &MockCDNService{ctrl: ctrl}
+	mock.recorder = &MockCDNServiceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCDNService) EXPECT() *MockCDNServiceMockRecorder {
+	return m.recorder
+}
+
+// GetSeedPieces mocks base method.
+func (m *MockCDNService) GetSeedPieces(arg0 string) ([]*task.PieceInfo, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetSeedPieces", arg0)
+	ret0, _ := ret[0].([]*task.PieceInfo)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetSeedPieces indicates an expected call of GetSeedPieces.
+func (mr *MockCDNServiceMockRecorder) GetSeedPieces(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSeedPieces", reflect.TypeOf((*MockCDNService)(nil).GetSeedPieces), arg0)
+}
+
+// GetSeedTask mocks base method.
+func (m *MockCDNService) GetSeedTask(arg0 string) (*task.SeedTask, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetSeedTask", arg0)
+	ret0, _ := ret[0].(*task.SeedTask)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetSeedTask indicates an expected call of GetSeedTask.
+func (mr *MockCDNServiceMockRecorder) GetSeedTask(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSeedTask", reflect.TypeOf((*MockCDNService)(nil).GetSeedTask), arg0)
+}
+
+// RegisterSeedTask mocks base method.
+func (m *MockCDNService) RegisterSeedTask(arg0 context.Context, arg1 string, arg2 *task.SeedTask) (*task.SeedTask, <-chan *task.PieceInfo, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegisterSeedTask", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*task.SeedTask)
+	ret1, _ := ret[1].(<-chan *task.PieceInfo)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// RegisterSeedTask indicates an expected call of RegisterSeedTask.
+func (mr *MockCDNServiceMockRecorder) RegisterSeedTask(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterSeedTask", reflect.TypeOf((*MockCDNService)(nil).RegisterSeedTask), arg0, arg1, arg2)
+}
diff --git a/cdn/supervisor/service.go b/cdn/supervisor/service.go
index 49ebf75c..3f967e92 100644
--- a/cdn/supervisor/service.go
+++ b/cdn/supervisor/service.go
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+//go:generate mockgen -destination ./mocks/mock_cdn_service.go -package mocks d7y.io/dragonfly/v2/cdn/supervisor CDNService
+
 package supervisor
 
 import (
diff --git a/client/daemon/peer/peertask_piecetask_poller.go b/client/daemon/peer/peertask_piecetask_poller.go
index 8011d72a..bd8c9f81 100644
--- a/client/daemon/peer/peertask_piecetask_poller.go
+++ b/client/daemon/peer/peertask_piecetask_poller.go
@@ -94,7 +94,8 @@ retry:
 	ptc.Debugf("try get piece task from peer %s, piece num: %d, limit: %d\"", peer.PeerId, request.StartNum, request.Limit)
 	p, err := poller.getPieceTasksByPeer(span, curPeerPacket, peer, request)
 	if err == nil {
-		ptc.Infof("got piece task from peer %s ok, pieces length: %d, totalPiece: %d", peer.PeerId, len(p.PieceInfos), p.TotalPiece)
+		ptc.Infof("got piece task from peer %s ok, pieces length: %d, totalPiece: %d, content length: %d, piece md5 sign: %s",
+			peer.PeerId, len(p.PieceInfos), p.TotalPiece, p.ContentLength, p.PieceMd5Sign)
 		span.SetAttributes(config.AttributeGetPieceCount.Int(len(p.PieceInfos)))
 		return p, nil
 	}
diff --git a/client/daemon/storage/local_storage.go b/client/daemon/storage/local_storage.go
index 22d235b5..af8b220a 100644
--- a/client/daemon/storage/local_storage.go
+++ b/client/daemon/storage/local_storage.go
@@ -174,7 +174,7 @@ func (t *localTaskStore) UpdateTask(ctx context.Context, req *UpdateTaskRequest)
 		t.TotalPieces = req.TotalPieces
 		t.Debugf("update total pieces: %d", t.TotalPieces)
 	}
-	if len(t.PieceMd5Sign) == 0 {
+	if len(t.PieceMd5Sign) == 0 && len(req.PieceMd5Sign) > 0 {
 		t.PieceMd5Sign = req.PieceMd5Sign
 		t.Debugf("update piece md5 sign: %s", t.PieceMd5Sign)
 	}
diff --git a/go.mod b/go.mod
index fe570543..75529c04 100644
--- a/go.mod
+++ b/go.mod
@@ -42,8 +42,7 @@ require (
 	github.com/mcuadros/go-gin-prometheus v0.1.0
 	github.com/mitchellh/mapstructure v1.4.1
 	github.com/montanaflynn/stats v0.6.6
-	github.com/onsi/ginkgo v1.16.5
-	github.com/onsi/gomega v1.14.0
+	github.com/onsi/gomega v1.18.0
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
 	github.com/pkg/errors v0.9.1
@@ -70,7 +69,7 @@ require (
 	golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
 	golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/sys v0.0.0-20211124211545-fe61309f8881
+	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
 	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
 	google.golang.org/api v0.39.0
 	google.golang.org/grpc v1.39.0
@@ -155,6 +154,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
+	github.com/onsi/ginkgo/v2 v2.1.0 // indirect
 	github.com/opencontainers/image-spec v1.0.2 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pelletier/go-toml v1.8.1 // indirect
diff --git a/go.sum b/go.sum
index ffd95775..82dcc0cd 100644
--- a/go.sum
+++ b/go.sum
@@ -377,6 +377,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -649,6 +650,9 @@ github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISq
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/ginkgo/v2 v2.1.0 h1:Rj+Was8Gt/ICSZY/CihVKK2cGy3rDqBYKSxVnmOXZzI=
+github.com/onsi/ginkgo/v2 v2.1.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -658,6 +662,10 @@ github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuK
 github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
 github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI=
 github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
+github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.18.0 h1:ngbYoRctxjl8SiF7XgP0NxBFbfHcg3wfHMMaFHWwMTM=
+github.com/onsi/gomega v1.18.0/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
@@ -1119,6 +1127,8 @@ golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
diff --git a/hack/build.sh b/hack/build.sh
index 93d21078..68d1b0e4 100755
--- a/hack/build.sh
+++ b/hack/build.sh
@@ -103,6 +103,14 @@ build-manager-docker() {
     build-docker ${MANAGER_BINARY_NAME} manager
 }
 
+build-manager-console() {
+    set -x
+    consoleDir=$(echo $curDir | sed 's#hack#manager/console#')
+    docker run --workdir=/build \
+        --rm -v ${consoleDir}:/build node:12-alpine \
+        sh -c "npm install --loglevel warn --progress false && npm run build"
+}
+
 main() {
     create-dirs
     if [[ "1" == "${USE_DOCKER}" ]]; then
@@ -120,6 +128,9 @@ main() {
         manager)
             build-manager-docker
             ;;
+        manager-console)
+            build-manager-console
+            ;;
         *)
             build-dfget-docker
             build-cdn-docker
@@ -142,6 +153,9 @@ main() {
         manager)
             build-manager-local
             ;;
+        manager-console)
+            build-manager-console
+            ;;
         *)
             build-dfget-local
             build-cdn-local
diff --git a/hack/docker-build.sh b/hack/docker-build.sh
index 3ca72654..a8c410f1 100755
--- a/hack/docker-build.sh
+++ b/hack/docker-build.sh
@@ -41,8 +41,6 @@ git-submodule() {
 }
 
 main() {
-    git-submodule
-
     case "${1-}" in
     cdn)
         docker-build cdn
@@ -54,6 +52,7 @@ main() {
         docker-build scheduler
         ;;
     manager)
+        git-submodule
         docker-build manager
     esac
 }
diff --git a/hack/gen-buildx.sh b/hack/gen-buildx.sh
new file mode 100755
index 00000000..822b77fa
--- /dev/null
+++ b/hack/gen-buildx.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+components="cdn scheduler manager"
+
+set -x
+
+for c in ${components}; do
+  file=build/images/"${c}"/Dockerfile
+  sed -i '1i# syntax=docker/dockerfile:1.3' "${file}"
+  sed -i "s#RUN make build-$c && make install-$c#RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/go/ export GOPATH=/root/go \&\& make build-$c \&\& make install-$c#" "${file}"
+done
+
+# dfdaemon is subcommand, need to update alone
+sed -i '1i# syntax=docker/dockerfile:1.3' build/images/dfdaemon/Dockerfile
+sed -i "s#RUN make build-dfget && make install-dfget#RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/go/ export GOPATH=/root/go \&\& make build-dfget \&\& make install-dfget#" build/images/dfdaemon/Dockerfile
+
+# buildx need "--load" to export images to docker
+sed -i 's/docker build/docker build --load/' hack/docker-build.sh
+sed -i 's/docker build/docker build --load/' test/tools/no-content-length/build.sh
+
+# TODO build console in https://github.com/dragonflyoss/console, and build image with github action cache
+# remove npm build, use "make build-manager-console" separated
+# sed -i '12,18d' build/images/manager/Dockerfile
+# sed -i 's#COPY --from=console-builder /build/dist /opt/dragonfly/manager/console/dist#COPY ./manager/console/dist /opt/dragonfly/manager/console/dist#' build/images/manager/Dockerfile
\ No newline at end of file
diff --git a/scheduler/resource/cdn.go b/scheduler/resource/cdn.go
index eac5b68c..1c19ed2d 100644
--- a/scheduler/resource/cdn.go
+++ b/scheduler/resource/cdn.go
@@ -222,14 +222,24 @@ func (c *cdnClient) OnNotify(data *config.DynconfigData) {
 		return
 	}
 
-	// Update dynamic data
-	c.data = data
+	// If only the ip of the cdn host is changed,
+	// the cdn peer needs to be cleared.
+	diff := diffCDNs(c.data.CDNs, data.CDNs)
+	for _, v := range diff {
+		id := idgen.CDNHostID(v.Hostname, v.Port)
+		if host, ok := c.hostManager.Load(id); ok {
+			host.LeavePeers()
+		}
+	}
 
 	// Update host manager
 	for _, host := range cdnsToHosts(data.CDNs) {
 		c.hostManager.Store(host)
 	}
 
+	// Update dynamic data
+	c.data = data
+
 	// Update grpc cdn addresses
 	c.UpdateState(cdnsToNetAddrs(data.CDNs))
 	logger.Infof("cdn addresses have been updated: %v", ips)
@@ -274,6 +284,44 @@ func cdnsToNetAddrs(cdns []*config.CDN) []dfnet.NetAddr {
 	return netAddrs
 }
 
+// diffCDNs get cdns with the same HostID but different IP
+func diffCDNs(cx []*config.CDN, cy []*config.CDN) []*config.CDN {
+	var diff []*config.CDN
+	for _, x := range cx {
+		for _, y := range cy {
+			if x.Hostname != y.Hostname {
+				continue
+			}
+
+			if x.Port != y.Port {
+				continue
+			}
+
+			if x.IP == y.IP {
+				continue
+			}
+
+			diff = append(diff, x)
+		}
+	}
+
+	for _, x := range cx {
+		found := false
+		for _, y := range cy {
+			if idgen.CDNHostID(x.Hostname, x.Port) == idgen.CDNHostID(y.Hostname, y.Port) {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			diff = append(diff, x)
+		}
+	}
+
+	return diff
+}
+
 // getCDNIPs get ips by []*config.CDN.
 func getCDNIPs(cdns []*config.CDN) []string {
 	ips := []string{}
diff --git a/scheduler/resource/host.go b/scheduler/resource/host.go
index 6e80c9fa..d80f60a5 100644
--- a/scheduler/resource/host.go
+++ b/scheduler/resource/host.go
@@ -165,6 +165,27 @@ func (h *Host) LenPeers() int {
 	return len
 }
 
+// LeavePeers set peer state to PeerStateLeave
+func (h *Host) LeavePeers() {
+	h.Peers.Range(func(key, value interface{}) bool {
+		if peer, ok := value.(*Peer); ok {
+			if err := peer.FSM.Event(PeerEventDownloadFailed); err != nil {
+				peer.Log.Errorf("peer fsm event failed: %v", err)
+				return true
+			}
+
+			if err := peer.FSM.Event(PeerEventLeave); err != nil {
+				peer.Log.Errorf("peer fsm event failed: %v", err)
+				return true
+			}
+
+			peer.Log.Info("peer has been left")
+		}
+
+		return true
+	})
+}
+
 // FreeUploadLoad return free upload load of host
 func (h *Host) FreeUploadLoad() int32 {
 	return h.UploadLoadLimit.Load() - int32(h.LenPeers())
diff --git a/scheduler/resource/task.go b/scheduler/resource/task.go
index 651dc8c3..e14d8392 100644
--- a/scheduler/resource/task.go
+++ b/scheduler/resource/task.go
@@ -124,7 +124,7 @@ func NewTask(id, url string, backToSourceLimit int, meta *base.UrlMeta) *Task {
 	t.FSM = fsm.NewFSM(
 		TaskStatePending,
 		fsm.Events{
-			{Name: TaskEventDownload, Src: []string{TaskStatePending, TaskStateFailed}, Dst: TaskStateRunning},
+			{Name: TaskEventDownload, Src: []string{TaskStatePending, TaskStateSucceeded, TaskStateFailed}, Dst: TaskStateRunning},
 			{Name: TaskEventDownloadSucceeded, Src: []string{TaskStateRunning, TaskStateFailed}, Dst: TaskStateSucceeded},
 			{Name: TaskEventDownloadFailed, Src: []string{TaskStateRunning}, Dst: TaskStateFailed},
 		},
@@ -186,6 +186,26 @@ func (t *Task) LenPeers() int {
 	return len
 }
 
+// LenAvailablePeers return length of peers without state is PeerStateLeave
+func (t *Task) LenAvailablePeers() int {
+	var len int
+	t.Peers.Range(func(_, v interface{}) bool {
+		peer, ok := v.(*Peer)
+		if !ok {
+			return true
+		}
+
+		if peer.FSM.Is(PeerStateLeave) {
+			return true
+		}
+
+		len++
+		return true
+	})
+
+	return len
+}
+
 // LoadCDNPeer return latest cdn peer in peers sync map
 func (t *Task) LoadCDNPeer() (*Peer, bool) {
 	var peers []*Peer
diff --git a/scheduler/service/callback.go b/scheduler/service/callback.go
index aee32134..cff5c4fb 100644
--- a/scheduler/service/callback.go
+++ b/scheduler/service/callback.go
@@ -221,26 +221,18 @@ func (c *callback) PieceFail(ctx context.Context, peer *resource.Peer, piece *rp
 		peer.Log.Infof("parent %s is cdn", piece.DstPid)
 		fallthrough
 	case base.Code_CDNTaskNotFound:
-		// CDN peer exists in the scheduler, but the peer information in the cdn service has been recycled.
-		// Scheduler need to redownload cdn peer.
-		if err := parent.FSM.Event(resource.PeerEventRestart); err != nil {
-			peer.Log.Errorf("peer fsm event failed: %v", err)
-			break
-		}
-
+		c.PeerFail(ctx, parent)
 		go func() {
 			parent.Log.Info("cdn restart seed task")
-			parent, endOfPiece, err := c.resource.CDN().TriggerTask(context.Background(), parent.Task)
+			cdnPeer, endOfPiece, err := c.resource.CDN().TriggerTask(context.Background(), parent.Task)
 			if err != nil {
 				peer.Log.Errorf("retrigger task failed: %v", err)
-
 				c.TaskFail(ctx, parent.Task)
-				c.PeerFail(ctx, parent)
 				return
 			}
 
-			c.TaskSuccess(ctx, parent.Task, endOfPiece)
-			c.PeerSuccess(ctx, parent)
+			c.TaskSuccess(ctx, cdnPeer.Task, endOfPiece)
+			c.PeerSuccess(ctx, cdnPeer)
 		}()
 	default:
 	}
diff --git a/scheduler/service/service.go b/scheduler/service/service.go
index 6f9170c7..9ea73cfb 100644
--- a/scheduler/service/service.go
+++ b/scheduler/service/service.go
@@ -88,7 +88,7 @@ func (s *service) CDN() resource.CDN {
 func (s *service) RegisterTask(ctx context.Context, req *rpcscheduler.PeerTaskRequest) (*resource.Task, error) {
 	task := resource.NewTask(idgen.TaskID(req.Url, req.UrlMeta), req.Url, s.config.Scheduler.BackSourceCount, req.UrlMeta)
 	task, ok := s.resource.TaskManager().LoadOrStore(task)
-	if ok && (task.FSM.Is(resource.TaskStateRunning) || task.FSM.Is(resource.TaskStateSucceeded)) {
+	if ok && (task.FSM.Is(resource.TaskStateRunning) || task.FSM.Is(resource.TaskStateSucceeded)) && task.LenAvailablePeers() != 0 {
 		// Task is healthy and can be reused
 		task.UpdateAt.Store(time.Now())
 		task.Log.Infof("reuse task and status is %s", task.FSM.Current())
@@ -106,13 +106,7 @@ func (s *service) RegisterTask(ctx context.Context, req *rpcscheduler.PeerTaskRe
 		peer, endOfPiece, err := s.resource.CDN().TriggerTask(context.Background(), task)
 		if err != nil {
 			task.Log.Errorf("trigger task failed: %v", err)
-
-			// Update the peer status first to help task return the error code to the peer that is downloading
-			// If init cdn fails, peer is nil
 			s.callback.TaskFail(ctx, task)
-			if peer != nil {
-				s.callback.PeerFail(ctx, peer)
-			}
 			return
 		}
 


Test patch:
diff --git a/.github/workflows/compatibility-e2e.yml b/.github/workflows/compatibility-e2e.yml
index ee965e29..64d23216 100644
--- a/.github/workflows/compatibility-e2e.yml
+++ b/.github/workflows/compatibility-e2e.yml
@@ -41,10 +41,16 @@ jobs:
       - name: Get dependencies
         run: |
           go mod download github.com/go-task/slim-sprig
-          go install github.com/onsi/ginkgo/ginkgo
+          go install github.com/onsi/ginkgo/v2/ginkgo@v2.1.0
           mkdir -p /tmp/artifact
           go mod vendor
 
+      - name: Setup buildx
+        uses: docker/setup-buildx-action@v1
+        id: buildx
+        with:
+          install: true
+
       - name: Setup Kind
         uses: engineerd/setup-kind@v0.5.0
         with:
@@ -53,11 +59,9 @@ jobs:
 
       - name: Build images
         run: |
+          hack/gen-buildx.sh
           make docker-build docker-build-testing-tools
           docker pull dragonflyoss/${{ matrix.module }}:${{ env.DRAGONFLY_STABLE_IMAGE_TAG }}
-
-      - name: Prepare kind environment
-        run: |
           make kind-load
           kind load docker-image dragonflyoss/${{ matrix.module }}:${{ env.DRAGONFLY_STABLE_IMAGE_TAG }}
 
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 2c4e5011..cb984f82 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -36,9 +36,14 @@ jobs:
       - name: Get dependencies
         run: |
           go mod download github.com/go-task/slim-sprig
-          go install github.com/onsi/ginkgo/ginkgo
+          go install github.com/onsi/ginkgo/v2/ginkgo@v2.1.0
           mkdir -p /tmp/artifact
-          go mod vendor
+
+      - name: Setup buildx
+        uses: docker/setup-buildx-action@v1
+        id: buildx
+        with:
+          install: true
 
       - name: Setup Kind
         uses: engineerd/setup-kind@v0.5.0
@@ -47,10 +52,10 @@ jobs:
           config: ${{ env.KIND_CONFIG_PATH }}
 
       - name: Build images
-        run: make docker-build docker-build-testing-tools
-
-      - name: Prepare kind environment
-        run: make kind-load
+        run: |
+          hack/gen-buildx.sh
+          make docker-build docker-build-testing-tools
+          make kind-load
 
       - name: Setup dragonfly
         run: |
diff --git a/cdn/rpcserver/rpcserver_test.go b/cdn/rpcserver/rpcserver_test.go
new file mode 100644
index 00000000..081f4210
--- /dev/null
+++ b/cdn/rpcserver/rpcserver_test.go
@@ -0,0 +1,347 @@
+/*
+ *     Copyright 2020 The Dragonfly 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 rpcserver
+
+import (
+	"context"
+	"fmt"
+	"sort"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/stretchr/testify/assert"
+
+	"d7y.io/dragonfly/v2/cdn/supervisor/mocks"
+	"d7y.io/dragonfly/v2/cdn/supervisor/task"
+	"d7y.io/dragonfly/v2/internal/dferrors"
+	"d7y.io/dragonfly/v2/pkg/rpc/base"
+	"d7y.io/dragonfly/v2/pkg/rpc/cdnsystem"
+	cdnRPCServer "d7y.io/dragonfly/v2/pkg/rpc/cdnsystem/server"
+	"d7y.io/dragonfly/v2/pkg/util/net/iputils"
+	"d7y.io/dragonfly/v2/pkg/util/rangeutils"
+)
+
+func TestServer_ObtainSeeds(t *testing.T) {
+	type args struct {
+		ctx context.Context
+		req *cdnsystem.SeedRequest
+		psc chan *cdnsystem.PieceSeed
+	}
+	tests := []struct {
+		name             string
+		createCallArgs   func() args
+		createCallObject func(t *testing.T, args args) cdnRPCServer.SeederServer
+		wantErr          assert.ErrorAssertionFunc
+	}{
+		{
+			name: "obtain piece seeds success",
+			createCallObject: func(t *testing.T, args args) cdnRPCServer.SeederServer {
+				ctrl := gomock.NewController(t)
+				cdnServiceMock := mocks.NewMockCDNService(ctrl)
+				regTask := task.NewSeedTask(args.req.TaskId, args.req.Url, args.req.UrlMeta)
+				cdnServiceMock.EXPECT().RegisterSeedTask(gomock.Any(), gomock.Any(), gomock.Eq(regTask)).DoAndReturn(
+					func(ctx context.Context, clientAddr string, registerTask *task.SeedTask) (*task.SeedTask, <-chan *task.PieceInfo, error) {
+						registerTask.CdnStatus = task.StatusRunning
+						registerTask.TotalPieceCount = 5
+						registerTask.SourceFileLength = 10000
+						pieceChan := make(chan *task.PieceInfo)
+						go func() {
+							pieceChan <- &task.PieceInfo{
+								PieceNum: 0,
+								PieceMd5: "xxxxxmd5",
+								PieceRange: &rangeutils.Range{
+									StartIndex: 0,
+									EndIndex:   99,
+								},
+								OriginRange: &rangeutils.Range{
+									StartIndex: 0,
+									EndIndex:   99,
+								},
+								PieceLen:   100,
+								PieceStyle: 0,
+							}
+							close(pieceChan)
+						}()
+						return registerTask, pieceChan, nil
+					}).Times(1)
+				cdnServiceMock.EXPECT().GetSeedTask(regTask.ID).DoAndReturn(func(taskID string) (*task.SeedTask, error) {
+					regTask.CdnStatus = task.StatusSuccess
+					return regTask, nil
+				}).Times(1)
+				server, _ := New(Config{}, cdnServiceMock)
+				return server
+			},
+			createCallArgs: func() args {
+				return args{
+					ctx: context.Background(),
+					req: &cdnsystem.SeedRequest{
+						TaskId:  "task1",
+						Url:     "https://www.dragonfly.com",
+						UrlMeta: nil,
+					},
+					psc: make(chan *cdnsystem.PieceSeed),
+				}
+			},
+			wantErr: assert.NoError,
+		}, {
+			name: "task download fail",
+			createCallArgs: func() args {
+				return args{
+					ctx: context.Background(),
+					req: &cdnsystem.SeedRequest{
+						TaskId:  "task1",
+						Url:     "https://www.dragonfly.com",
+						UrlMeta: nil,
+					},
+					psc: make(chan *cdnsystem.PieceSeed),
+				}
+			},
+			createCallObject: func(t *testing.T, args args) cdnRPCServer.SeederServer {
+				ctrl := gomock.NewController(t)
+				cdnServiceMock := mocks.NewMockCDNService(ctrl)
+				regTask := task.NewSeedTask(args.req.TaskId, args.req.Url, args.req.UrlMeta)
+				cdnServiceMock.EXPECT().RegisterSeedTask(gomock.Any(), gomock.Any(), gomock.Eq(regTask)).DoAndReturn(
+					func(ctx context.Context, clientAddr string, registerTask *task.SeedTask) (*task.SeedTask, <-chan *task.PieceInfo, error) {
+						registerTask.CdnStatus = task.StatusRunning
+						registerTask.TotalPieceCount = 5
+						registerTask.SourceFileLength = 10000
+						pieceChan := make(chan *task.PieceInfo)
+						go func() {
+							pieceChan <- &task.PieceInfo{
+								PieceNum: 0,
+								PieceMd5: "xxxxxmd5",
+								PieceRange: &rangeutils.Range{
+									StartIndex: 0,
+									EndIndex:   99,
+								},
+								OriginRange: &rangeutils.Range{
+									StartIndex: 0,
+									EndIndex:   99,
+								},
+								PieceLen:   100,
+								PieceStyle: 0,
+							}
+							close(pieceChan)
+						}()
+						return registerTask, pieceChan, nil
+					}).Times(1)
+				cdnServiceMock.EXPECT().GetSeedTask(regTask.ID).DoAndReturn(func(taskID string) (*task.SeedTask, error) {
+					regTask.CdnStatus = task.StatusFailed
+					return regTask, nil
+				}).Times(1)
+				server, _ := New(Config{}, cdnServiceMock)
+				return server
+			},
+			wantErr: assert.Error,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			args := tt.createCallArgs()
+			svr := tt.createCallObject(t, args)
+			go func() {
+				for seed := range args.psc {
+					fmt.Println(seed)
+				}
+			}()
+			tt.wantErr(t, svr.ObtainSeeds(args.ctx, args.req, args.psc), fmt.Sprintf("ObtainSeeds(%v, %v, %v)", args.ctx, args.req, args.psc))
+		})
+	}
+}
+
+func TestServer_GetPieceTasks(t *testing.T) {
+	type args struct {
+		ctx context.Context
+		req *base.PieceTaskRequest
+	}
+	tests := []struct {
+		name             string
+		createCallObject func(t *testing.T, args args) cdnRPCServer.SeederServer
+		createCallArgs   func() args
+		wantErr          assert.ErrorAssertionFunc
+		wantPiecePacket  *base.PiecePacket
+	}{
+		{
+			name: "task not found",
+			createCallObject: func(t *testing.T, args args) cdnRPCServer.SeederServer {
+				ctrl := gomock.NewController(t)
+				cdnServiceMock := mocks.NewMockCDNService(ctrl)
+				cdnServiceMock.EXPECT().GetSeedTask(args.req.TaskId).Return(nil, dferrors.Newf(base.Code_CDNTaskNotFound, "failed to get task(%s)",
+					args.req.TaskId)).Times(1)
+				cdnServiceMock.EXPECT().GetSeedPieces(gomock.Any()).Times(0)
+				server, _ := New(Config{}, cdnServiceMock)
+				return server
+			},
+			createCallArgs: func() args {
+				return args{
+					ctx: context.Background(),
+					req: &base.PieceTaskRequest{
+						TaskId:   "task1",
+						SrcPid:   "srcPeerID",
+						DstPid:   "dstPeerID",
+						StartNum: 0,
+						Limit:    10,
+					},
+				}
+			},
+			wantErr:         assert.Error,
+			wantPiecePacket: nil,
+		},
+		{
+			name: "success get pieces",
+			createCallObject: func(t *testing.T, args args) cdnRPCServer.SeederServer {
+				ctrl := gomock.NewController(t)
+				cdnServiceMock := mocks.NewMockCDNService(ctrl)
+				testTask := &task.SeedTask{
+					ID:               args.req.TaskId,
+					RawURL:           "https://www.dragonfly.com",
+					TaskURL:          "https://www.dragonfly.com",
+					SourceFileLength: 250,
+					CdnFileLength:    250,
+					PieceSize:        100,
+					CdnStatus:        task.StatusSuccess,
+					TotalPieceCount:  3,
+					SourceRealDigest: "xxxxx111",
+					PieceMd5Sign:     "bbbbb222",
+					Digest:           "xxxxx111",
+					Tag:              "xx",
+					Range:            "",
+					Filter:           "",
+					Header:           nil,
+					Pieces: map[uint32]*task.PieceInfo{
+						0: {
+							PieceNum: 0,
+							PieceMd5: "xxxx0",
+							PieceRange: &rangeutils.Range{
+								StartIndex: 0,
+								EndIndex:   99,
+							},
+							OriginRange: &rangeutils.Range{
+								StartIndex: 0,
+								EndIndex:   99,
+							},
+							PieceLen:   100,
+							PieceStyle: 0,
+						},
+						1: {
+							PieceNum: 1,
+							PieceMd5: "xxxx1",
+							PieceRange: &rangeutils.Range{
+								StartIndex: 100,
+								EndIndex:   199,
+							},
+							OriginRange: &rangeutils.Range{
+								StartIndex: 100,
+								EndIndex:   199,
+							},
+							PieceLen:   100,
+							PieceStyle: 0,
+						},
+						2: {
+							PieceNum: 2,
+							PieceMd5: "xxxx2",
+							PieceRange: &rangeutils.Range{
+								StartIndex: 200,
+								EndIndex:   299,
+							},
+							OriginRange: &rangeutils.Range{
+								StartIndex: 200,
+								EndIndex:   249,
+							},
+							PieceLen:   100,
+							PieceStyle: 0,
+						},
+					},
+				}
+				cdnServiceMock.EXPECT().GetSeedTask(args.req.TaskId).DoAndReturn(func(taskID string) (seedTask *task.SeedTask, err error) {
+					return testTask, nil
+				})
+				cdnServiceMock.EXPECT().GetSeedPieces(args.req.TaskId).DoAndReturn(func(taskID string) (pieces []*task.PieceInfo, err error) {
+					for u := range testTask.Pieces {
+						pieces = append(pieces, testTask.Pieces[u])
+					}
+					sort.Slice(pieces, func(i, j int) bool {
+						return pieces[i].PieceNum < pieces[j].PieceNum
+					})
+					return pieces, nil
+				})
+				server, _ := New(Config{AdvertiseIP: iputils.IPv4, DownloadPort: DefaultDownloadPort}, cdnServiceMock)
+				return server
+			},
+			createCallArgs: func() args {
+				return args{
+					ctx: context.Background(),
+					req: &base.PieceTaskRequest{
+						TaskId:   "task2",
+						SrcPid:   "srcPeerID",
+						DstPid:   "dstPeerID",
+						StartNum: 0,
+						Limit:    4,
+					},
+				}
+			},
+			wantErr: assert.NoError,
+			wantPiecePacket: &base.PiecePacket{
+				TaskId:  "task2",
+				DstPid:  "dstPeerID",
+				DstAddr: fmt.Sprintf("%s:%d", iputils.IPv4, DefaultDownloadPort),
+				PieceInfos: []*base.PieceInfo{
+					{
+						PieceNum:    0,
+						RangeStart:  0,
+						RangeSize:   100,
+						PieceMd5:    "xxxx0",
+						PieceOffset: 0,
+						PieceStyle:  0,
+					}, {
+						PieceNum:    1,
+						RangeStart:  100,
+						RangeSize:   100,
+						PieceMd5:    "xxxx1",
+						PieceOffset: 100,
+						PieceStyle:  0,
+					}, {
+						PieceNum:    2,
+						RangeStart:  200,
+						RangeSize:   100,
+						PieceMd5:    "xxxx2",
+						PieceOffset: 200,
+						PieceStyle:  0,
+					},
+				},
+				TotalPiece:    3,
+				ContentLength: 250,
+				PieceMd5Sign:  "bbbbb222",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			args := tt.createCallArgs()
+			svr := tt.createCallObject(t, args)
+			gotPiecePacket, err := svr.GetPieceTasks(args.ctx, args.req)
+			if !tt.wantErr(t, err, fmt.Sprintf("GetPieceTasks(%v, %v)", args.ctx, args.req)) {
+				return
+			}
+			assert.True(t, cmp.Equal(tt.wantPiecePacket, gotPiecePacket, cmpopts.IgnoreUnexported(base.PiecePacket{}), cmpopts.IgnoreUnexported(base.PieceInfo{})),
+				"want: %v, actual: %v", tt.wantPiecePacket, gotPiecePacket)
+		})
+	}
+}
diff --git a/client/daemon/test/mock/peer/peertask_manager.go b/client/daemon/test/mock/peer/peertask_manager.go
index 94a09033..2551a5d1 100644
--- a/client/daemon/test/mock/peer/peertask_manager.go
+++ b/client/daemon/test/mock/peer/peertask_manager.go
@@ -10,6 +10,7 @@ import (
 	reflect "reflect"
 
 	peer "d7y.io/dragonfly/v2/client/daemon/peer"
+	storage "d7y.io/dragonfly/v2/client/daemon/storage"
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 	gomock "github.com/golang/mock/gomock"
 )
@@ -188,6 +189,20 @@ func (mr *MockTaskMockRecorder) GetPieceMd5Sign() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPieceMd5Sign", reflect.TypeOf((*MockTask)(nil).GetPieceMd5Sign))
 }
 
+// GetStorage mocks base method.
+func (m *MockTask) GetStorage() storage.TaskStorageDriver {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetStorage")
+	ret0, _ := ret[0].(storage.TaskStorageDriver)
+	return ret0
+}
+
+// GetStorage indicates an expected call of GetStorage.
+func (mr *MockTaskMockRecorder) GetStorage() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStorage", reflect.TypeOf((*MockTask)(nil).GetStorage))
+}
+
 // GetTaskID mocks base method.
 func (m *MockTask) GetTaskID() string {
 	m.ctrl.T.Helper()
diff --git a/hack/install-e2e-test.sh b/hack/install-e2e-test.sh
index 629eaa27..13469be7 100755
--- a/hack/install-e2e-test.sh
+++ b/hack/install-e2e-test.sh
@@ -65,7 +65,7 @@ install-ginkgo() {
       print_step_info "ginkgo has been installed"
   else
       go mod download github.com/go-task/slim-sprig
-      go install github.com/onsi/ginkgo/ginkgo
+      go install github.com/onsi/ginkgo/v2/ginkgo@v2.1.0
   fi
 }
 
diff --git a/scheduler/resource/cdn_test.go b/scheduler/resource/cdn_test.go
index f9954fdb..7fb3d961 100644
--- a/scheduler/resource/cdn_test.go
+++ b/scheduler/resource/cdn_test.go
@@ -165,23 +165,77 @@ func TestCDNClient_OnNotify(t *testing.T) {
 		mock func(dynconfig *configmocks.MockDynconfigInterfaceMockRecorder, hostManager *MockHostManagerMockRecorder)
 	}{
 		{
-			name: "notify client",
+			name: "notify client without different cdns",
 			data: &config.DynconfigData{
 				CDNs: []*config.CDN{{
-					ID: 1,
-					IP: "0.0.0.0",
+					ID:       1,
+					Hostname: "foo",
+					IP:       "0.0.0.0",
+					Port:     8080,
 				}},
 			},
 			mock: func(dynconfig *configmocks.MockDynconfigInterfaceMockRecorder, hostManager *MockHostManagerMockRecorder) {
 				gomock.InOrder(
 					dynconfig.Get().Return(&config.DynconfigData{
 						CDNs: []*config.CDN{{
-							ID: 1,
-							IP: "127.0.0.1",
+							ID:       1,
+							Hostname: "foo",
+							IP:       "0.0.0.0",
+							Port:     8080,
+						}},
+					}, nil).Times(1),
+					hostManager.Store(gomock.Any()).Return().Times(1),
+					dynconfig.Register(gomock.Any()).Return().Times(1),
+				)
+			},
+		},
+		{
+			name: "notify client with different cdns",
+			data: &config.DynconfigData{
+				CDNs: []*config.CDN{{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "0.0.0.0",
+				}},
+			},
+			mock: func(dynconfig *configmocks.MockDynconfigInterfaceMockRecorder, hostManager *MockHostManagerMockRecorder) {
+				mockHost := NewHost(mockRawHost)
+				gomock.InOrder(
+					dynconfig.Get().Return(&config.DynconfigData{
+						CDNs: []*config.CDN{{
+							ID:       1,
+							Hostname: "foo",
+							IP:       "127.0.0.1",
+						}},
+					}, nil).Times(1),
+					hostManager.Store(gomock.Any()).Return().Times(1),
+					dynconfig.Register(gomock.Any()).Return().Times(1),
+					hostManager.Load(gomock.Any()).Return(mockHost, true).Times(1),
+					hostManager.Store(gomock.Any()).Return().Times(1),
+				)
+			},
+		},
+		{
+			name: "notify client with different cdns and load host failed",
+			data: &config.DynconfigData{
+				CDNs: []*config.CDN{{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "0.0.0.0",
+				}},
+			},
+			mock: func(dynconfig *configmocks.MockDynconfigInterfaceMockRecorder, hostManager *MockHostManagerMockRecorder) {
+				gomock.InOrder(
+					dynconfig.Get().Return(&config.DynconfigData{
+						CDNs: []*config.CDN{{
+							ID:       1,
+							Hostname: "foo",
+							IP:       "127.0.0.1",
 						}},
 					}, nil).Times(1),
 					hostManager.Store(gomock.Any()).Return().Times(1),
 					dynconfig.Register(gomock.Any()).Return().Times(1),
+					hostManager.Load(gomock.Any()).Return(nil, false).Times(1),
 					hostManager.Store(gomock.Any()).Return().Times(1),
 				)
 			},
@@ -364,6 +418,207 @@ func TestCDNClient_cdnsToNetAddrs(t *testing.T) {
 	}
 }
 
+func TestCDNClient_diffCDNs(t *testing.T) {
+	tests := []struct {
+		name   string
+		cx     []*config.CDN
+		cy     []*config.CDN
+		expect func(t *testing.T, diff []*config.CDN)
+	}{
+		{
+			name: "same cdn list",
+			cx: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			cy: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			expect: func(t *testing.T, diff []*config.CDN) {
+				assert := assert.New(t)
+				assert.EqualValues(diff, []*config.CDN(nil))
+			},
+		},
+		{
+			name: "different hostname",
+			cx: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "bar",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			cy: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			expect: func(t *testing.T, diff []*config.CDN) {
+				assert := assert.New(t)
+				assert.EqualValues(diff, []*config.CDN{
+					{
+						ID:       1,
+						Hostname: "bar",
+						IP:       "127.0.0.1",
+						Port:     8080,
+					},
+				})
+			},
+		},
+		{
+			name: "different port",
+			cx: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8081,
+				},
+			},
+			cy: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			expect: func(t *testing.T, diff []*config.CDN) {
+				assert := assert.New(t)
+				assert.EqualValues(diff, []*config.CDN{
+					{
+						ID:       1,
+						Hostname: "foo",
+						IP:       "127.0.0.1",
+						Port:     8081,
+					},
+				})
+			},
+		},
+		{
+			name: "different ip",
+			cx: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "0.0.0.0",
+					Port:     8080,
+				},
+			},
+			cy: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			expect: func(t *testing.T, diff []*config.CDN) {
+				assert := assert.New(t)
+				assert.EqualValues(diff, []*config.CDN{
+					{
+						ID:       1,
+						Hostname: "foo",
+						IP:       "0.0.0.0",
+						Port:     8080,
+					},
+				})
+			},
+		},
+		{
+			name: "remove y cdn",
+			cx: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+				{
+					ID:       2,
+					Hostname: "bar",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			cy: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			expect: func(t *testing.T, diff []*config.CDN) {
+				assert := assert.New(t)
+				assert.EqualValues(diff, []*config.CDN{
+					{
+						ID:       2,
+						Hostname: "bar",
+						IP:       "127.0.0.1",
+						Port:     8080,
+					},
+				})
+			},
+		},
+		{
+			name: "remove x cdn",
+			cx: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "foo",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			cy: []*config.CDN{
+				{
+					ID:       1,
+					Hostname: "baz",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+				{
+					ID:       2,
+					Hostname: "bar",
+					IP:       "127.0.0.1",
+					Port:     8080,
+				},
+			},
+			expect: func(t *testing.T, diff []*config.CDN) {
+				assert := assert.New(t)
+				assert.EqualValues(diff, []*config.CDN{
+					{
+						ID:       1,
+						Hostname: "foo",
+						IP:       "127.0.0.1",
+						Port:     8080,
+					},
+				})
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			tc.expect(t, diffCDNs(tc.cx, tc.cy))
+		})
+	}
+}
+
 func TestCDNClient_getCDNIPs(t *testing.T) {
 	tests := []struct {
 		name   string
diff --git a/scheduler/resource/host_test.go b/scheduler/resource/host_test.go
index 614ccd09..c68a6922 100644
--- a/scheduler/resource/host_test.go
+++ b/scheduler/resource/host_test.go
@@ -361,6 +361,54 @@ func TestHost_LenPeers(t *testing.T) {
 	}
 }
 
+func TestHost_LeavePeers(t *testing.T) {
+	tests := []struct {
+		name    string
+		rawHost *scheduler.PeerHost
+		options []HostOption
+		expect  func(t *testing.T, host *Host, mockPeer *Peer)
+	}{
+		{
+			name:    "leave peers",
+			rawHost: mockRawHost,
+			expect: func(t *testing.T, host *Host, mockPeer *Peer) {
+				assert := assert.New(t)
+				host.StorePeer(mockPeer)
+				assert.Equal(host.LenPeers(), 1)
+				host.LeavePeers()
+				host.Peers.Range(func(_, value interface{}) bool {
+					peer := value.(*Peer)
+					assert.True(peer.FSM.Is(PeerStateLeave))
+					return true
+				})
+			},
+		},
+		{
+			name:    "peers is empty ",
+			rawHost: mockRawHost,
+			expect: func(t *testing.T, host *Host, mockPeer *Peer) {
+				assert := assert.New(t)
+				assert.Equal(host.LenPeers(), 0)
+				host.LeavePeers()
+				host.Peers.Range(func(_, value interface{}) bool {
+					assert.Fail("host peers is not empty")
+					return true
+				})
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			host := NewHost(tc.rawHost, tc.options...)
+			mockTask := NewTask(mockTaskID, mockTaskURL, mockTaskBackToSourceLimit, mockTaskURLMeta)
+			mockPeer := NewPeer(mockPeerID, mockTask, host)
+
+			tc.expect(t, host, mockPeer)
+		})
+	}
+}
+
 func TestHost_FreeUploadLoad(t *testing.T) {
 	tests := []struct {
 		name    string
diff --git a/scheduler/resource/task_test.go b/scheduler/resource/task_test.go
index d0e125a4..a684f6ba 100644
--- a/scheduler/resource/task_test.go
+++ b/scheduler/resource/task_test.go
@@ -359,6 +359,58 @@ func TestTask_LenPeers(t *testing.T) {
 	}
 }
 
+func TestTask_LenAvailablePeers(t *testing.T) {
+	tests := []struct {
+		name              string
+		id                string
+		urlMeta           *base.UrlMeta
+		url               string
+		backToSourceLimit int
+		expect            func(t *testing.T, task *Task, mockPeer *Peer)
+	}{
+		{
+			name:              "len available peers",
+			id:                mockTaskID,
+			urlMeta:           mockTaskURLMeta,
+			url:               mockTaskURL,
+			backToSourceLimit: mockTaskBackToSourceLimit,
+			expect: func(t *testing.T, task *Task, mockPeer *Peer) {
+				assert := assert.New(t)
+				task.StorePeer(mockPeer)
+				mockPeer.ID = idgen.PeerID("0.0.0.0")
+				task.StorePeer(mockPeer)
+				assert.Equal(task.LenAvailablePeers(), 2)
+				task.StorePeer(mockPeer)
+				assert.Equal(task.LenAvailablePeers(), 2)
+				mockPeer.FSM.SetState(PeerStateLeave)
+				task.StorePeer(mockPeer)
+				assert.Equal(task.LenAvailablePeers(), 0)
+			},
+		},
+		{
+			name:              "peer does not exist",
+			id:                mockTaskID,
+			urlMeta:           mockTaskURLMeta,
+			url:               mockTaskURL,
+			backToSourceLimit: mockTaskBackToSourceLimit,
+			expect: func(t *testing.T, task *Task, mockPeer *Peer) {
+				assert := assert.New(t)
+				assert.Equal(task.LenAvailablePeers(), 0)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			mockHost := NewHost(mockRawHost)
+			task := NewTask(mockTaskID, mockTaskURL, mockTaskBackToSourceLimit, mockTaskURLMeta)
+			mockPeer := NewPeer(mockPeerID, task, mockHost)
+
+			tc.expect(t, task, mockPeer)
+		})
+	}
+}
+
 func TestTask_CDNPeers(t *testing.T) {
 	tests := []struct {
 		name   string
diff --git a/scheduler/service/service_test.go b/scheduler/service/service_test.go
index f2960af1..569dc736 100644
--- a/scheduler/service/service_test.go
+++ b/scheduler/service/service_test.go
@@ -138,6 +138,8 @@ func TestService_RegisterTask(t *testing.T) {
 			},
 			run: func(svc Service, req *rpcscheduler.PeerTaskRequest, mockTask *resource.Task, mockPeer *resource.Peer, taskManager resource.TaskManager, cdn resource.CDN, mr *resource.MockResourceMockRecorder, mt *resource.MockTaskManagerMockRecorder, mc *resource.MockCDNMockRecorder) {
 				mockTask.FSM.SetState(resource.TaskStateRunning)
+				mockTask.StorePeer(mockPeer)
+				mockPeer.FSM.SetState(resource.PeerStateRunning)
 				gomock.InOrder(
 					mr.TaskManager().Return(taskManager).Times(1),
 					mt.LoadOrStore(gomock.Any()).Return(mockTask, true).Times(1),
@@ -157,6 +159,8 @@ func TestService_RegisterTask(t *testing.T) {
 			},
 			run: func(svc Service, req *rpcscheduler.PeerTaskRequest, mockTask *resource.Task, mockPeer *resource.Peer, taskManager resource.TaskManager, cdn resource.CDN, mr *resource.MockResourceMockRecorder, mt *resource.MockTaskManagerMockRecorder, mc *resource.MockCDNMockRecorder) {
 				mockTask.FSM.SetState(resource.TaskStateSucceeded)
+				mockTask.StorePeer(mockPeer)
+				mockPeer.FSM.SetState(resource.PeerStateRunning)
 				gomock.InOrder(
 					mr.TaskManager().Return(taskManager).Times(1),
 					mt.LoadOrStore(gomock.Any()).Return(mockTask, true).Times(1),
diff --git a/test/e2e/concurrency_test.go b/test/e2e/concurrency_test.go
index b97fad01..597143a8 100644
--- a/test/e2e/concurrency_test.go
+++ b/test/e2e/concurrency_test.go
@@ -19,8 +19,8 @@ package e2e
 import (
 	"fmt"
 
-	. "github.com/onsi/ginkgo" //nolint
-	. "github.com/onsi/gomega" //nolint
+	. "github.com/onsi/ginkgo/v2" //nolint
+	. "github.com/onsi/gomega"    //nolint
 
 	"d7y.io/dragonfly/v2/test/e2e/e2eutil"
 )
diff --git a/test/e2e/containerd_test.go b/test/e2e/containerd_test.go
index 3c48afd6..7c7e8c5b 100644
--- a/test/e2e/containerd_test.go
+++ b/test/e2e/containerd_test.go
@@ -19,8 +19,8 @@ package e2e
 import (
 	"fmt"
 
-	. "github.com/onsi/ginkgo" //nolint
-	. "github.com/onsi/gomega" //nolint
+	. "github.com/onsi/ginkgo/v2" //nolint
+	. "github.com/onsi/gomega"    //nolint
 
 	"d7y.io/dragonfly/v2/test/e2e/e2eutil"
 )
diff --git a/test/e2e/dfget_test.go b/test/e2e/dfget_test.go
index 6ff78836..10b0f91d 100644
--- a/test/e2e/dfget_test.go
+++ b/test/e2e/dfget_test.go
@@ -21,8 +21,8 @@ import (
 	"strings"
 	"time"
 
-	. "github.com/onsi/ginkgo" //nolint
-	. "github.com/onsi/gomega" //nolint
+	. "github.com/onsi/ginkgo/v2" //nolint
+	. "github.com/onsi/gomega"    //nolint
 
 	"d7y.io/dragonfly/v2/test/e2e/e2eutil"
 )
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index 9bc23222..e8546262 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -23,8 +23,8 @@ import (
 	"strings"
 	"testing"
 
-	. "github.com/onsi/ginkgo" //nolint
-	. "github.com/onsi/gomega" //nolint
+	. "github.com/onsi/ginkgo/v2" //nolint
+	. "github.com/onsi/gomega"    //nolint
 
 	"d7y.io/dragonfly/v2/test/e2e/e2eutil"
 	_ "d7y.io/dragonfly/v2/test/e2e/manager"
diff --git a/test/e2e/manager/preheat.go b/test/e2e/manager/preheat.go
index c0f7fbc7..e6fcf486 100644
--- a/test/e2e/manager/preheat.go
+++ b/test/e2e/manager/preheat.go
@@ -25,8 +25,8 @@ import (
 	"time"
 
 	machineryv1tasks "github.com/RichardKnop/machinery/v1/tasks"
-	. "github.com/onsi/ginkgo" //nolint
-	. "github.com/onsi/gomega" //nolint
+	. "github.com/onsi/ginkgo/v2" //nolint
+	. "github.com/onsi/gomega"    //nolint
 
 	internaljob "d7y.io/dragonfly/v2/internal/job"
 	"d7y.io/dragonfly/v2/manager/model"
diff --git a/test/testdata/charts/config.yaml b/test/testdata/charts/config.yaml
index d2521155..d4475762 100644
--- a/test/testdata/charts/config.yaml
+++ b/test/testdata/charts/config.yaml
@@ -3,38 +3,59 @@ scheduler:
   tag: latest
   replicas: 3
   extraVolumeMounts:
+  - name: logs
+    mountPath: "/var/log/"
   - name: artifact
     mountPath: /tmp/artifact
   extraVolumes:
+  - name: logs
+    emptyDir: { }
   - name: artifact
     hostPath:
       path: /tmp/artifact
+  config:
+    verbose: true
 
 cdn:
   image: d7yio/cdn
   tag: latest
   replicas: 3
   extraVolumeMounts:
+  - name: logs
+    mountPath: "/var/log/nginx"
+    subPath: nginx
+  - name: logs
+    mountPath: "/var/log/dragonfly"
+    subPath: dragonfly
   - name: artifact
     mountPath: /tmp/artifact
   extraVolumes:
+  - name: logs
+    emptyDir: { }
   - name: artifact
     hostPath:
       path: /tmp/artifact
+  config:
+    verbose: true
 
 dfdaemon:
   image: d7yio/dfdaemon
   tag: latest
   extraVolumeMounts:
+  - name: logs
+    mountPath: "/var/log/"
   - name: artifact
     mountPath: /tmp/artifact
   extraVolumes:
+  - name: logs
+    emptyDir: { }
   - name: artifact
     hostPath:
       path: /tmp/artifact
   config:
     verbose: true
     pprofPort: 9999
+    metrics: 127.0.0.1:8888
     scheduler:
       disableAutoBackSource: true
     proxy:
@@ -58,9 +79,15 @@ manager:
   tag: latest
   replicas: 3
   extraVolumeMounts:
+  - name: logs
+    mountPath: "/var/log/"
   - name: artifact
     mountPath: /tmp/artifact
   extraVolumes:
+  - name: logs
+    emptyDir: { }
   - name: artifact
     hostPath:
       path: /tmp/artifact
+  config:
+    verbose: true