Repository: dragonflyoss/dragonfly

Issue:
Display persistent cache information of peers in the console
### Feature request:

<!-- Please describe the feature request and why you would like to have it -->

### Use case:

<!-- Please add a concrete use case to demonstrate how such a feature would add value for the user. -->

### UI Example:

<!-- If this is about a new command or command line options, please let us know how you would add it to UI (in the code block below). -->


Hints from issue comments:

Solution patch:
diff --git a/api/manager/docs.go b/api/manager/docs.go
index 5835394b..35f1e758 100644
--- a/api/manager/docs.go
+++ b/api/manager/docs.go
@@ -1477,9 +1477,9 @@ const docTemplate = `{
                 }
             }
         },
-        "/api/v1/persistent-caches": {
+        "/api/v1/persistent-cache-tasks": {
             "get": {
-                "description": "Get PersistentCaches",
+                "description": "Get PersistentCacheTasks",
                 "consumes": [
                     "application/json"
                 ],
@@ -1487,10 +1487,17 @@ const docTemplate = `{
                     "application/json"
                 ],
                 "tags": [
-                    "PersistentCache"
+                    "PersistentCacheTask"
                 ],
-                "summary": "Get PersistentCaches",
+                "summary": "Get PersistentCacheTasks",
                 "parameters": [
+                    {
+                        "type": "integer",
+                        "description": "scheduler cluster id",
+                        "name": "scheduler_cluster_id",
+                        "in": "query",
+                        "required": true
+                    },
                     {
                         "type": "integer",
                         "default": 0,
@@ -1516,7 +1523,7 @@ const docTemplate = `{
                         "schema": {
                             "type": "array",
                             "items": {
-                                "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse"
+                                "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheTask"
                             }
                         }
                     },
@@ -1532,9 +1539,9 @@ const docTemplate = `{
                 }
             }
         },
-        "/api/v1/persistent-caches/{scheduler_cluster_id}/{task_id}": {
+        "/api/v1/persistent-cache-tasks/{id}": {
             "get": {
-                "description": "Get PersistentCache by id",
+                "description": "Get PersistentCacheTask by id",
                 "consumes": [
                     "application/json"
                 ],
@@ -1542,21 +1549,21 @@ const docTemplate = `{
                     "application/json"
                 ],
                 "tags": [
-                    "PersistentCache"
+                    "PersistentCacheTask"
                 ],
-                "summary": "Get PersistentCache",
+                "summary": "Get PersistentCacheTask",
                 "parameters": [
                     {
-                        "type": "string",
+                        "type": "integer",
                         "description": "scheduler cluster id",
                         "name": "scheduler_cluster_id",
-                        "in": "path",
+                        "in": "query",
                         "required": true
                     },
                     {
                         "type": "string",
                         "description": "task id",
-                        "name": "task_id",
+                        "name": "id",
                         "in": "path",
                         "required": true
                     }
@@ -1565,7 +1572,7 @@ const docTemplate = `{
                     "200": {
                         "description": "OK",
                         "schema": {
-                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse"
+                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheTask"
                         }
                     },
                     "400": {
@@ -1580,7 +1587,7 @@ const docTemplate = `{
                 }
             },
             "delete": {
-                "description": "Destroy PersistentCache by id",
+                "description": "Destroy PersistentCacheTask by id",
                 "consumes": [
                     "application/json"
                 ],
@@ -1588,21 +1595,21 @@ const docTemplate = `{
                     "application/json"
                 ],
                 "tags": [
-                    "PersistentCache"
+                    "PersistentCacheTask"
                 ],
-                "summary": "Destroy PersistentCache",
+                "summary": "Destroy PersistentCacheTask",
                 "parameters": [
                     {
-                        "type": "string",
+                        "type": "integer",
                         "description": "scheduler cluster id",
                         "name": "scheduler_cluster_id",
-                        "in": "path",
+                        "in": "query",
                         "required": true
                     },
                     {
                         "type": "string",
                         "description": "task id",
-                        "name": "task_id",
+                        "name": "id",
                         "in": "path",
                         "required": true
                     }
@@ -5202,66 +5209,6 @@ const docTemplate = `{
                 }
             }
         },
-        "d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse": {
-            "type": "object",
-            "properties": {
-                "application": {
-                    "description": "Application of persistent cache task.",
-                    "type": "string"
-                },
-                "content_length": {
-                    "description": "ContentLength is persistent cache task total content length.",
-                    "type": "integer"
-                },
-                "created_at": {
-                    "description": "CreatedAt is persistent cache task create time.",
-                    "type": "string"
-                },
-                "persistent_cache_peers": {
-                    "description": "PersistentCachePeers is the list of persistent cache peers.",
-                    "type": "array",
-                    "items": {
-                        "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeer"
-                    }
-                },
-                "persistent_replica_count": {
-                    "description": "PersistentReplicaCount is replica count of the persistent cache task.",
-                    "type": "integer"
-                },
-                "piece_length": {
-                    "description": "PieceLength is persistent cache task piece length.",
-                    "type": "integer"
-                },
-                "state": {
-                    "description": "State is persistent cache task state.",
-                    "type": "string"
-                },
-                "tag": {
-                    "description": "Tag is used to distinguish different persistent cache tasks.",
-                    "type": "string"
-                },
-                "task_id": {
-                    "description": "TaskID is task id.",
-                    "type": "string"
-                },
-                "total_piece_count": {
-                    "description": "TotalPieceCount is total piece count.",
-                    "type": "integer"
-                },
-                "ttl": {
-                    "description": "TTL is persistent cache task time to live.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/time.Duration"
-                        }
-                    ]
-                },
-                "updated_at": {
-                    "description": "UpdatedAt is persistent cache task update time.",
-                    "type": "string"
-                }
-            }
-        },
         "d7y_io_dragonfly_v2_manager_types.GetV1PreheatResponse": {
             "type": "object",
             "properties": {
@@ -5279,63 +5226,7 @@ const docTemplate = `{
                 }
             }
         },
-        "d7y_io_dragonfly_v2_manager_types.PersistentCachePeer": {
-            "type": "object",
-            "properties": {
-                "block_parents": {
-                    "description": "BlockParents is bad parents ids.",
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
-                "cost": {
-                    "description": "Cost is the cost of downloading.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/time.Duration"
-                        }
-                    ]
-                },
-                "created_at": {
-                    "description": "CreatedAt is persistent cache peer create time.",
-                    "type": "string"
-                },
-                "finished_pieces": {
-                    "description": "FinishedPieces is finished pieces bitset.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/bitset.BitSet"
-                        }
-                    ]
-                },
-                "host": {
-                    "description": "Host is the peer host.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeerHost"
-                        }
-                    ]
-                },
-                "id": {
-                    "description": "ID is persistent cache peer id.",
-                    "type": "string"
-                },
-                "persistent": {
-                    "description": "Persistent is whether the peer is persistent.",
-                    "type": "boolean"
-                },
-                "state": {
-                    "description": "State is persistent cache peer state.",
-                    "type": "string"
-                },
-                "updated_at": {
-                    "description": "UpdatedAt is persistent cache peer update time.",
-                    "type": "string"
-                }
-            }
-        },
-        "d7y_io_dragonfly_v2_manager_types.PersistentCachePeerHost": {
+        "d7y_io_dragonfly_v2_manager_types.PersistentCacheHost": {
             "type": "object",
             "properties": {
                 "announce_interval": {
@@ -5612,6 +5503,122 @@ const docTemplate = `{
                 }
             }
         },
+        "d7y_io_dragonfly_v2_manager_types.PersistentCachePeer": {
+            "type": "object",
+            "properties": {
+                "block_parents": {
+                    "description": "BlockParents is bad parents ids.",
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "cost": {
+                    "description": "Cost is the cost of downloading.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/time.Duration"
+                        }
+                    ]
+                },
+                "created_at": {
+                    "description": "CreatedAt is persistent cache peer create time.",
+                    "type": "string"
+                },
+                "finished_pieces": {
+                    "description": "FinishedPieces is finished pieces bitset.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/bitset.BitSet"
+                        }
+                    ]
+                },
+                "host": {
+                    "description": "Host is the peer host.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheHost"
+                        }
+                    ]
+                },
+                "id": {
+                    "description": "ID is persistent cache peer id.",
+                    "type": "string"
+                },
+                "persistent": {
+                    "description": "Persistent is whether the peer is persistent.",
+                    "type": "boolean"
+                },
+                "state": {
+                    "description": "State is persistent cache peer state.",
+                    "type": "string"
+                },
+                "updated_at": {
+                    "description": "UpdatedAt is persistent cache peer update time.",
+                    "type": "string"
+                }
+            }
+        },
+        "d7y_io_dragonfly_v2_manager_types.PersistentCacheTask": {
+            "type": "object",
+            "properties": {
+                "application": {
+                    "description": "Application of persistent cache task.",
+                    "type": "string"
+                },
+                "content_length": {
+                    "description": "ContentLength is persistent cache task total content length.",
+                    "type": "integer"
+                },
+                "created_at": {
+                    "description": "CreatedAt is persistent cache task create time.",
+                    "type": "string"
+                },
+                "id": {
+                    "description": "ID is task id.",
+                    "type": "string"
+                },
+                "peers": {
+                    "description": "PersistentCachePeers is the list of persistent cache peers.",
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeer"
+                    }
+                },
+                "persistent_replica_count": {
+                    "description": "PersistentReplicaCount is replica count of the persistent cache task.",
+                    "type": "integer"
+                },
+                "piece_length": {
+                    "description": "PieceLength is persistent cache task piece length.",
+                    "type": "integer"
+                },
+                "state": {
+                    "description": "State is persistent cache task state.",
+                    "type": "string"
+                },
+                "tag": {
+                    "description": "Tag is used to distinguish different persistent cache tasks.",
+                    "type": "string"
+                },
+                "total_piece_count": {
+                    "description": "TotalPieceCount is total piece count.",
+                    "type": "integer"
+                },
+                "ttl": {
+                    "description": "TTL is persistent cache task time to live.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/time.Duration"
+                        }
+                    ]
+                },
+                "updated_at": {
+                    "description": "UpdatedAt is persistent cache task update time.",
+                    "type": "string"
+                }
+            }
+        },
         "d7y_io_dragonfly_v2_manager_types.PriorityConfig": {
             "type": "object",
             "required": [
diff --git a/api/manager/swagger.json b/api/manager/swagger.json
index 689c67cb..5eebf79a 100644
--- a/api/manager/swagger.json
+++ b/api/manager/swagger.json
@@ -1471,9 +1471,9 @@
                 }
             }
         },
-        "/api/v1/persistent-caches": {
+        "/api/v1/persistent-cache-tasks": {
             "get": {
-                "description": "Get PersistentCaches",
+                "description": "Get PersistentCacheTasks",
                 "consumes": [
                     "application/json"
                 ],
@@ -1481,10 +1481,17 @@
                     "application/json"
                 ],
                 "tags": [
-                    "PersistentCache"
+                    "PersistentCacheTask"
                 ],
-                "summary": "Get PersistentCaches",
+                "summary": "Get PersistentCacheTasks",
                 "parameters": [
+                    {
+                        "type": "integer",
+                        "description": "scheduler cluster id",
+                        "name": "scheduler_cluster_id",
+                        "in": "query",
+                        "required": true
+                    },
                     {
                         "type": "integer",
                         "default": 0,
@@ -1510,7 +1517,7 @@
                         "schema": {
                             "type": "array",
                             "items": {
-                                "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse"
+                                "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheTask"
                             }
                         }
                     },
@@ -1526,9 +1533,9 @@
                 }
             }
         },
-        "/api/v1/persistent-caches/{scheduler_cluster_id}/{task_id}": {
+        "/api/v1/persistent-cache-tasks/{id}": {
             "get": {
-                "description": "Get PersistentCache by id",
+                "description": "Get PersistentCacheTask by id",
                 "consumes": [
                     "application/json"
                 ],
@@ -1536,21 +1543,21 @@
                     "application/json"
                 ],
                 "tags": [
-                    "PersistentCache"
+                    "PersistentCacheTask"
                 ],
-                "summary": "Get PersistentCache",
+                "summary": "Get PersistentCacheTask",
                 "parameters": [
                     {
-                        "type": "string",
+                        "type": "integer",
                         "description": "scheduler cluster id",
                         "name": "scheduler_cluster_id",
-                        "in": "path",
+                        "in": "query",
                         "required": true
                     },
                     {
                         "type": "string",
                         "description": "task id",
-                        "name": "task_id",
+                        "name": "id",
                         "in": "path",
                         "required": true
                     }
@@ -1559,7 +1566,7 @@
                     "200": {
                         "description": "OK",
                         "schema": {
-                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse"
+                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheTask"
                         }
                     },
                     "400": {
@@ -1574,7 +1581,7 @@
                 }
             },
             "delete": {
-                "description": "Destroy PersistentCache by id",
+                "description": "Destroy PersistentCacheTask by id",
                 "consumes": [
                     "application/json"
                 ],
@@ -1582,21 +1589,21 @@
                     "application/json"
                 ],
                 "tags": [
-                    "PersistentCache"
+                    "PersistentCacheTask"
                 ],
-                "summary": "Destroy PersistentCache",
+                "summary": "Destroy PersistentCacheTask",
                 "parameters": [
                     {
-                        "type": "string",
+                        "type": "integer",
                         "description": "scheduler cluster id",
                         "name": "scheduler_cluster_id",
-                        "in": "path",
+                        "in": "query",
                         "required": true
                     },
                     {
                         "type": "string",
                         "description": "task id",
-                        "name": "task_id",
+                        "name": "id",
                         "in": "path",
                         "required": true
                     }
@@ -5196,66 +5203,6 @@
                 }
             }
         },
-        "d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse": {
-            "type": "object",
-            "properties": {
-                "application": {
-                    "description": "Application of persistent cache task.",
-                    "type": "string"
-                },
-                "content_length": {
-                    "description": "ContentLength is persistent cache task total content length.",
-                    "type": "integer"
-                },
-                "created_at": {
-                    "description": "CreatedAt is persistent cache task create time.",
-                    "type": "string"
-                },
-                "persistent_cache_peers": {
-                    "description": "PersistentCachePeers is the list of persistent cache peers.",
-                    "type": "array",
-                    "items": {
-                        "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeer"
-                    }
-                },
-                "persistent_replica_count": {
-                    "description": "PersistentReplicaCount is replica count of the persistent cache task.",
-                    "type": "integer"
-                },
-                "piece_length": {
-                    "description": "PieceLength is persistent cache task piece length.",
-                    "type": "integer"
-                },
-                "state": {
-                    "description": "State is persistent cache task state.",
-                    "type": "string"
-                },
-                "tag": {
-                    "description": "Tag is used to distinguish different persistent cache tasks.",
-                    "type": "string"
-                },
-                "task_id": {
-                    "description": "TaskID is task id.",
-                    "type": "string"
-                },
-                "total_piece_count": {
-                    "description": "TotalPieceCount is total piece count.",
-                    "type": "integer"
-                },
-                "ttl": {
-                    "description": "TTL is persistent cache task time to live.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/time.Duration"
-                        }
-                    ]
-                },
-                "updated_at": {
-                    "description": "UpdatedAt is persistent cache task update time.",
-                    "type": "string"
-                }
-            }
-        },
         "d7y_io_dragonfly_v2_manager_types.GetV1PreheatResponse": {
             "type": "object",
             "properties": {
@@ -5273,63 +5220,7 @@
                 }
             }
         },
-        "d7y_io_dragonfly_v2_manager_types.PersistentCachePeer": {
-            "type": "object",
-            "properties": {
-                "block_parents": {
-                    "description": "BlockParents is bad parents ids.",
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
-                "cost": {
-                    "description": "Cost is the cost of downloading.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/time.Duration"
-                        }
-                    ]
-                },
-                "created_at": {
-                    "description": "CreatedAt is persistent cache peer create time.",
-                    "type": "string"
-                },
-                "finished_pieces": {
-                    "description": "FinishedPieces is finished pieces bitset.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/bitset.BitSet"
-                        }
-                    ]
-                },
-                "host": {
-                    "description": "Host is the peer host.",
-                    "allOf": [
-                        {
-                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeerHost"
-                        }
-                    ]
-                },
-                "id": {
-                    "description": "ID is persistent cache peer id.",
-                    "type": "string"
-                },
-                "persistent": {
-                    "description": "Persistent is whether the peer is persistent.",
-                    "type": "boolean"
-                },
-                "state": {
-                    "description": "State is persistent cache peer state.",
-                    "type": "string"
-                },
-                "updated_at": {
-                    "description": "UpdatedAt is persistent cache peer update time.",
-                    "type": "string"
-                }
-            }
-        },
-        "d7y_io_dragonfly_v2_manager_types.PersistentCachePeerHost": {
+        "d7y_io_dragonfly_v2_manager_types.PersistentCacheHost": {
             "type": "object",
             "properties": {
                 "announce_interval": {
@@ -5606,6 +5497,122 @@
                 }
             }
         },
+        "d7y_io_dragonfly_v2_manager_types.PersistentCachePeer": {
+            "type": "object",
+            "properties": {
+                "block_parents": {
+                    "description": "BlockParents is bad parents ids.",
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "cost": {
+                    "description": "Cost is the cost of downloading.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/time.Duration"
+                        }
+                    ]
+                },
+                "created_at": {
+                    "description": "CreatedAt is persistent cache peer create time.",
+                    "type": "string"
+                },
+                "finished_pieces": {
+                    "description": "FinishedPieces is finished pieces bitset.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/bitset.BitSet"
+                        }
+                    ]
+                },
+                "host": {
+                    "description": "Host is the peer host.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheHost"
+                        }
+                    ]
+                },
+                "id": {
+                    "description": "ID is persistent cache peer id.",
+                    "type": "string"
+                },
+                "persistent": {
+                    "description": "Persistent is whether the peer is persistent.",
+                    "type": "boolean"
+                },
+                "state": {
+                    "description": "State is persistent cache peer state.",
+                    "type": "string"
+                },
+                "updated_at": {
+                    "description": "UpdatedAt is persistent cache peer update time.",
+                    "type": "string"
+                }
+            }
+        },
+        "d7y_io_dragonfly_v2_manager_types.PersistentCacheTask": {
+            "type": "object",
+            "properties": {
+                "application": {
+                    "description": "Application of persistent cache task.",
+                    "type": "string"
+                },
+                "content_length": {
+                    "description": "ContentLength is persistent cache task total content length.",
+                    "type": "integer"
+                },
+                "created_at": {
+                    "description": "CreatedAt is persistent cache task create time.",
+                    "type": "string"
+                },
+                "id": {
+                    "description": "ID is task id.",
+                    "type": "string"
+                },
+                "peers": {
+                    "description": "PersistentCachePeers is the list of persistent cache peers.",
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeer"
+                    }
+                },
+                "persistent_replica_count": {
+                    "description": "PersistentReplicaCount is replica count of the persistent cache task.",
+                    "type": "integer"
+                },
+                "piece_length": {
+                    "description": "PieceLength is persistent cache task piece length.",
+                    "type": "integer"
+                },
+                "state": {
+                    "description": "State is persistent cache task state.",
+                    "type": "string"
+                },
+                "tag": {
+                    "description": "Tag is used to distinguish different persistent cache tasks.",
+                    "type": "string"
+                },
+                "total_piece_count": {
+                    "description": "TotalPieceCount is total piece count.",
+                    "type": "integer"
+                },
+                "ttl": {
+                    "description": "TTL is persistent cache task time to live.",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/time.Duration"
+                        }
+                    ]
+                },
+                "updated_at": {
+                    "description": "UpdatedAt is persistent cache task update time.",
+                    "type": "string"
+                }
+            }
+        },
         "d7y_io_dragonfly_v2_manager_types.PriorityConfig": {
             "type": "object",
             "required": [
diff --git a/api/manager/swagger.yaml b/api/manager/swagger.yaml
index c2b347c8..8f00717e 100644
--- a/api/manager/swagger.yaml
+++ b/api/manager/swagger.yaml
@@ -745,49 +745,6 @@ definitions:
       updated_at:
         type: string
     type: object
-  d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse:
-    properties:
-      application:
-        description: Application of persistent cache task.
-        type: string
-      content_length:
-        description: ContentLength is persistent cache task total content length.
-        type: integer
-      created_at:
-        description: CreatedAt is persistent cache task create time.
-        type: string
-      persistent_cache_peers:
-        description: PersistentCachePeers is the list of persistent cache peers.
-        items:
-          $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeer'
-        type: array
-      persistent_replica_count:
-        description: PersistentReplicaCount is replica count of the persistent cache
-          task.
-        type: integer
-      piece_length:
-        description: PieceLength is persistent cache task piece length.
-        type: integer
-      state:
-        description: State is persistent cache task state.
-        type: string
-      tag:
-        description: Tag is used to distinguish different persistent cache tasks.
-        type: string
-      task_id:
-        description: TaskID is task id.
-        type: string
-      total_piece_count:
-        description: TotalPieceCount is total piece count.
-        type: integer
-      ttl:
-        allOf:
-        - $ref: '#/definitions/time.Duration'
-        description: TTL is persistent cache task time to live.
-      updated_at:
-        description: UpdatedAt is persistent cache task update time.
-        type: string
-    type: object
   d7y_io_dragonfly_v2_manager_types.GetV1PreheatResponse:
     properties:
       finishTime:
@@ -799,42 +756,7 @@ definitions:
       status:
         type: string
     type: object
-  d7y_io_dragonfly_v2_manager_types.PersistentCachePeer:
-    properties:
-      block_parents:
-        description: BlockParents is bad parents ids.
-        items:
-          type: string
-        type: array
-      cost:
-        allOf:
-        - $ref: '#/definitions/time.Duration'
-        description: Cost is the cost of downloading.
-      created_at:
-        description: CreatedAt is persistent cache peer create time.
-        type: string
-      finished_pieces:
-        allOf:
-        - $ref: '#/definitions/bitset.BitSet'
-        description: FinishedPieces is finished pieces bitset.
-      host:
-        allOf:
-        - $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeerHost'
-        description: Host is the peer host.
-      id:
-        description: ID is persistent cache peer id.
-        type: string
-      persistent:
-        description: Persistent is whether the peer is persistent.
-        type: boolean
-      state:
-        description: State is persistent cache peer state.
-        type: string
-      updated_at:
-        description: UpdatedAt is persistent cache peer update time.
-        type: string
-    type: object
-  d7y_io_dragonfly_v2_manager_types.PersistentCachePeerHost:
+  d7y_io_dragonfly_v2_manager_types.PersistentCacheHost:
     properties:
       announce_interval:
         allOf:
@@ -1037,6 +959,84 @@ definitions:
         description: UpdatedAt is host update time.
         type: string
     type: object
+  d7y_io_dragonfly_v2_manager_types.PersistentCachePeer:
+    properties:
+      block_parents:
+        description: BlockParents is bad parents ids.
+        items:
+          type: string
+        type: array
+      cost:
+        allOf:
+        - $ref: '#/definitions/time.Duration'
+        description: Cost is the cost of downloading.
+      created_at:
+        description: CreatedAt is persistent cache peer create time.
+        type: string
+      finished_pieces:
+        allOf:
+        - $ref: '#/definitions/bitset.BitSet'
+        description: FinishedPieces is finished pieces bitset.
+      host:
+        allOf:
+        - $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheHost'
+        description: Host is the peer host.
+      id:
+        description: ID is persistent cache peer id.
+        type: string
+      persistent:
+        description: Persistent is whether the peer is persistent.
+        type: boolean
+      state:
+        description: State is persistent cache peer state.
+        type: string
+      updated_at:
+        description: UpdatedAt is persistent cache peer update time.
+        type: string
+    type: object
+  d7y_io_dragonfly_v2_manager_types.PersistentCacheTask:
+    properties:
+      application:
+        description: Application of persistent cache task.
+        type: string
+      content_length:
+        description: ContentLength is persistent cache task total content length.
+        type: integer
+      created_at:
+        description: CreatedAt is persistent cache task create time.
+        type: string
+      id:
+        description: ID is task id.
+        type: string
+      peers:
+        description: PersistentCachePeers is the list of persistent cache peers.
+        items:
+          $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCachePeer'
+        type: array
+      persistent_replica_count:
+        description: PersistentReplicaCount is replica count of the persistent cache
+          task.
+        type: integer
+      piece_length:
+        description: PieceLength is persistent cache task piece length.
+        type: integer
+      state:
+        description: State is persistent cache task state.
+        type: string
+      tag:
+        description: Tag is used to distinguish different persistent cache tasks.
+        type: string
+      total_piece_count:
+        description: TotalPieceCount is total piece count.
+        type: integer
+      ttl:
+        allOf:
+        - $ref: '#/definitions/time.Duration'
+        description: TTL is persistent cache task time to live.
+      updated_at:
+        description: UpdatedAt is persistent cache task update time.
+        type: string
+    type: object
   d7y_io_dragonfly_v2_manager_types.PriorityConfig:
     properties:
       urls:
@@ -2352,12 +2352,17 @@ paths:
       summary: Get Permissions
       tags:
       - Permission
-  /api/v1/persistent-caches:
+  /api/v1/persistent-cache-tasks:
     get:
       consumes:
       - application/json
-      description: Get PersistentCaches
+      description: Get PersistentCacheTasks
       parameters:
+      - description: scheduler cluster id
+        in: query
+        name: scheduler_cluster_id
+        required: true
+        type: integer
       - default: 0
         description: current page
         in: query
@@ -2379,7 +2384,7 @@ paths:
           description: OK
           schema:
             items:
-              $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse'
+              $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheTask'
             type: array
         "400":
           description: Bad Request
@@ -2387,23 +2392,23 @@ paths:
           description: Not Found
         "500":
           description: Internal Server Error
-      summary: Get PersistentCaches
+      summary: Get PersistentCacheTasks
       tags:
-      - PersistentCache
-  /api/v1/persistent-caches/{scheduler_cluster_id}/{task_id}:
+      - PersistentCacheTask
+  /api/v1/persistent-cache-tasks/{id}:
     delete:
       consumes:
       - application/json
-      description: Destroy PersistentCache by id
+      description: Destroy PersistentCacheTask by id
       parameters:
       - description: scheduler cluster id
-        in: path
+        in: query
         name: scheduler_cluster_id
         required: true
-        type: string
+        type: integer
       - description: task id
         in: path
-        name: task_id
+        name: id
         required: true
         type: string
       produces:
@@ -2417,22 +2422,22 @@ paths:
           description: Not Found
         "500":
           description: Internal Server Error
-      summary: Destroy PersistentCache
+      summary: Destroy PersistentCacheTask
       tags:
-      - PersistentCache
+      - PersistentCacheTask
     get:
       consumes:
       - application/json
-      description: Get PersistentCache by id
+      description: Get PersistentCacheTask by id
       parameters:
       - description: scheduler cluster id
-        in: path
+        in: query
         name: scheduler_cluster_id
         required: true
-        type: string
+        type: integer
       - description: task id
         in: path
-        name: task_id
+        name: id
         required: true
         type: string
       produces:
@@ -2441,16 +2446,16 @@ paths:
         "200":
           description: OK
           schema:
-            $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.GetPersistentCacheResponse'
+            $ref: '#/definitions/d7y_io_dragonfly_v2_manager_types.PersistentCacheTask'
         "400":
           description: Bad Request
         "404":
           description: Not Found
         "500":
           description: Internal Server Error
-      summary: Get PersistentCache
+      summary: Get PersistentCacheTask
       tags:
-      - PersistentCache
+      - PersistentCacheTask
   /api/v1/personal-access-tokens:
     get:
       consumes:
diff --git a/manager/handlers/persistent_cache.go b/manager/handlers/persistent_cache.go
deleted file mode 100644
index f4a31320..00000000
--- a/manager/handlers/persistent_cache.go
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- *     Copyright 2025 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 handlers
-
-import (
-	"net/http"
-
-	"github.com/gin-gonic/gin"
-
-	"d7y.io/dragonfly/v2/manager/types"
-)
-
-// @Summary Destroy PersistentCache
-// @Description Destroy PersistentCache by id
-// @Tags PersistentCache
-// @Accept json
-// @Produce json
-// @Param scheduler_cluster_id path string true "scheduler cluster id"
-// @Param task_id path string true "task id"
-// @Success 200
-// @Failure 400
-// @Failure 404
-// @Failure 500
-// @Router /api/v1/persistent-caches/{scheduler_cluster_id}/{task_id} [delete]
-func (h *Handlers) DestroyPersistentCache(ctx *gin.Context) {
-	var params types.PersistentCacheParams
-	if err := ctx.ShouldBindUri(&params); err != nil {
-		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
-		return
-	}
-
-	if err := h.service.DestroyPersistentCache(ctx.Request.Context(), params); err != nil {
-		ctx.Error(err) // nolint: errcheck
-		return
-	}
-
-	ctx.Status(http.StatusOK)
-}
-
-// @Summary Get PersistentCache
-// @Description Get PersistentCache by id
-// @Tags PersistentCache
-// @Accept json
-// @Produce json
-// @Param scheduler_cluster_id path string true "scheduler cluster id"
-// @Param task_id path string true "task id"
-// @Success 200 {object} types.GetPersistentCacheResponse
-// @Failure 400
-// @Failure 404
-// @Failure 500
-// @Router /api/v1/persistent-caches/{scheduler_cluster_id}/{task_id} [get]
-func (h *Handlers) GetPersistentCache(ctx *gin.Context) {
-	var params types.PersistentCacheParams
-	if err := ctx.ShouldBindUri(&params); err != nil {
-		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
-		return
-	}
-
-	persistentCache, err := h.service.GetPersistentCache(ctx.Request.Context(), params)
-	if err != nil {
-		ctx.Error(err) // nolint: errcheck
-		return
-	}
-
-	ctx.JSON(http.StatusOK, persistentCache)
-}
-
-// @Summary Get PersistentCaches
-// @Description Get PersistentCaches
-// @Tags PersistentCache
-// @Accept json
-// @Produce json
-// @Param page query int true "current page" default(0)
-// @Param per_page query int true "return max item count, default 10, max 50" default(10) minimum(2) maximum(50)
-// @Success 200 {object} []types.GetPersistentCacheResponse
-// @Failure 400
-// @Failure 404
-// @Failure 500
-// @Router /api/v1/persistent-caches [get]
-func (h *Handlers) GetPersistentCaches(ctx *gin.Context) {
-	var query types.GetPersistentCachesQuery
-	if err := ctx.ShouldBindQuery(&query); err != nil {
-		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
-		return
-	}
-
-	h.setPaginationDefault(&query.Page, &query.PerPage)
-	persistentCaches, count, err := h.service.GetPersistentCaches(ctx.Request.Context(), query)
-	if err != nil {
-		ctx.Error(err) // nolint: errcheck
-		return
-	}
-
-	h.setPaginationLinkHeader(ctx, query.Page, query.PerPage, int(count))
-	ctx.JSON(http.StatusOK, persistentCaches)
-}
diff --git a/manager/handlers/persistent_cache_task.go b/manager/handlers/persistent_cache_task.go
new file mode 100644
index 00000000..b29e30a9
--- /dev/null
+++ b/manager/handlers/persistent_cache_task.go
@@ -0,0 +1,123 @@
+/*
+ *     Copyright 2025 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 handlers
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+
+	"d7y.io/dragonfly/v2/manager/types"
+)
+
+// @Summary Destroy PersistentCacheTask
+// @Description Destroy PersistentCacheTask by id
+// @Tags PersistentCacheTask
+// @Accept json
+// @Produce json
+// @Param scheduler_cluster_id query uint true "scheduler cluster id"
+// @Param id path string true "task id"
+// @Success 200
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /api/v1/persistent-cache-tasks/{id} [delete]
+func (h *Handlers) DestroyPersistentCacheTask(ctx *gin.Context) {
+	var params types.PersistentCacheTaskParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	var query types.DestroyPersistentCacheTaskQuery
+	if err := ctx.ShouldBindQuery(&query); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	if err := h.service.DestroyPersistentCacheTask(ctx.Request.Context(), query.SchedulerClusterID, params.ID); err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.Status(http.StatusOK)
+}
+
+// @Summary Get PersistentCacheTask
+// @Description Get PersistentCacheTask by id
+// @Tags PersistentCacheTask
+// @Accept json
+// @Produce json
+// @Param scheduler_cluster_id query uint true "scheduler cluster id"
+// @Param id path string true "task id"
+// @Success 200 {object} types.PersistentCacheTask
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /api/v1/persistent-cache-tasks/{id} [get]
+func (h *Handlers) GetPersistentCacheTask(ctx *gin.Context) {
+	var params types.PersistentCacheTaskParams
+	if err := ctx.ShouldBindUri(&params); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	var query types.GetPersistentCacheTaskQuery
+	if err := ctx.ShouldBindQuery(&query); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	persistentCacheTask, err := h.service.GetPersistentCacheTask(ctx.Request.Context(), query.SchedulerClusterID, params.ID)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	ctx.JSON(http.StatusOK, persistentCacheTask)
+}
+
+// @Summary Get PersistentCacheTasks
+// @Description Get PersistentCacheTasks
+// @Tags PersistentCacheTask
+// @Accept json
+// @Produce json
+// @Param scheduler_cluster_id query uint true "scheduler cluster id"
+// @Param page query int true "current page" default(0)
+// @Param per_page query int true "return max item count, default 10, max 50" default(10) minimum(2) maximum(50)
+// @Success 200 {object} []types.PersistentCacheTask
+// @Failure 400
+// @Failure 404
+// @Failure 500
+// @Router /api/v1/persistent-cache-tasks [get]
+func (h *Handlers) GetPersistentCacheTasks(ctx *gin.Context) {
+	var query types.GetPersistentCacheTasksQuery
+	if err := ctx.ShouldBindQuery(&query); err != nil {
+		ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()})
+		return
+	}
+
+	h.setPaginationDefault(&query.Page, &query.PerPage)
+	persistentCacheTasks, count, err := h.service.GetPersistentCacheTasks(ctx.Request.Context(), query)
+	if err != nil {
+		ctx.Error(err) // nolint: errcheck
+		return
+	}
+
+	h.setPaginationLinkHeader(ctx, query.Page, query.PerPage, int(count))
+	ctx.JSON(http.StatusOK, persistentCacheTasks)
+}
diff --git a/manager/router/router.go b/manager/router/router.go
index a55b76ab..ec2b2eb1 100644
--- a/manager/router/router.go
+++ b/manager/router/router.go
@@ -233,11 +233,11 @@ func Init(cfg *config.Config, logDir string, service service.Service, database *
 	pat.GET(":id", h.GetPersonalAccessToken)
 	pat.GET("", h.GetPersonalAccessTokens)
 
-	// Persistent Cache.
-	pc := apiv1.Group("/persistent-caches", jwt.MiddlewareFunc(), rbac)
-	pc.DELETE(":id", h.DestroyPersistentCache)
-	pc.GET(":id", h.GetPersistentCache)
-	pc.GET("", h.GetPersistentCaches)
+	// Persistent Cache Task.
+	pc := apiv1.Group("/persistent-cache-tasks", jwt.MiddlewareFunc(), rbac)
+	pc.DELETE(":id", h.DestroyPersistentCacheTask)
+	pc.GET(":id", h.GetPersistentCacheTask)
+	pc.GET("", h.GetPersistentCacheTasks)
 
 	// Open API router.
 	oapiv1 := r.Group("/oapi/v1")
diff --git a/manager/service/mocks/service_mock.go b/manager/service/mocks/service_mock.go
index fac7d19e..703941af 100644
--- a/manager/service/mocks/service_mock.go
+++ b/manager/service/mocks/service_mock.go
@@ -497,18 +497,18 @@ func (mr *MockServiceMockRecorder) DestroyPeer(arg0, arg1 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DestroyPeer", reflect.TypeOf((*MockService)(nil).DestroyPeer), arg0, arg1)
 }
 
-// DestroyPersistentCache mocks base method.
-func (m *MockService) DestroyPersistentCache(arg0 context.Context, arg1 types.PersistentCacheParams) error {
+// DestroyPersistentCacheTask mocks base method.
+func (m *MockService) DestroyPersistentCacheTask(arg0 context.Context, arg1 uint, arg2 string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DestroyPersistentCache", arg0, arg1)
+	ret := m.ctrl.Call(m, "DestroyPersistentCacheTask", arg0, arg1, arg2)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// DestroyPersistentCache indicates an expected call of DestroyPersistentCache.
-func (mr *MockServiceMockRecorder) DestroyPersistentCache(arg0, arg1 any) *gomock.Call {
+// DestroyPersistentCacheTask indicates an expected call of DestroyPersistentCacheTask.
+func (mr *MockServiceMockRecorder) DestroyPersistentCacheTask(arg0, arg1, arg2 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DestroyPersistentCache", reflect.TypeOf((*MockService)(nil).DestroyPersistentCache), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DestroyPersistentCacheTask", reflect.TypeOf((*MockService)(nil).DestroyPersistentCacheTask), arg0, arg1, arg2)
 }
 
 // DestroyPersonalAccessToken mocks base method.
@@ -826,35 +826,35 @@ func (mr *MockServiceMockRecorder) GetPermissions(arg0, arg1 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissions", reflect.TypeOf((*MockService)(nil).GetPermissions), arg0, arg1)
 }
 
-// GetPersistentCache mocks base method.
-func (m *MockService) GetPersistentCache(arg0 context.Context, arg1 types.PersistentCacheParams) (*types.GetPersistentCacheResponse, error) {
+// GetPersistentCacheTask mocks base method.
+func (m *MockService) GetPersistentCacheTask(arg0 context.Context, arg1 uint, arg2 string) (*types.PersistentCacheTask, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetPersistentCache", arg0, arg1)
-	ret0, _ := ret[0].(*types.GetPersistentCacheResponse)
+	ret := m.ctrl.Call(m, "GetPersistentCacheTask", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*types.PersistentCacheTask)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// GetPersistentCache indicates an expected call of GetPersistentCache.
-func (mr *MockServiceMockRecorder) GetPersistentCache(arg0, arg1 any) *gomock.Call {
+// GetPersistentCacheTask indicates an expected call of GetPersistentCacheTask.
+func (mr *MockServiceMockRecorder) GetPersistentCacheTask(arg0, arg1, arg2 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPersistentCache", reflect.TypeOf((*MockService)(nil).GetPersistentCache), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPersistentCacheTask", reflect.TypeOf((*MockService)(nil).GetPersistentCacheTask), arg0, arg1, arg2)
 }
 
-// GetPersistentCaches mocks base method.
-func (m *MockService) GetPersistentCaches(arg0 context.Context, arg1 types.GetPersistentCachesQuery) ([]types.GetPersistentCacheResponse, int64, error) {
+// GetPersistentCacheTasks mocks base method.
+func (m *MockService) GetPersistentCacheTasks(arg0 context.Context, arg1 types.GetPersistentCacheTasksQuery) ([]*types.PersistentCacheTask, int64, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetPersistentCaches", arg0, arg1)
-	ret0, _ := ret[0].([]types.GetPersistentCacheResponse)
+	ret := m.ctrl.Call(m, "GetPersistentCacheTasks", arg0, arg1)
+	ret0, _ := ret[0].([]*types.PersistentCacheTask)
 	ret1, _ := ret[1].(int64)
 	ret2, _ := ret[2].(error)
 	return ret0, ret1, ret2
 }
 
-// GetPersistentCaches indicates an expected call of GetPersistentCaches.
-func (mr *MockServiceMockRecorder) GetPersistentCaches(arg0, arg1 any) *gomock.Call {
+// GetPersistentCacheTasks indicates an expected call of GetPersistentCacheTasks.
+func (mr *MockServiceMockRecorder) GetPersistentCacheTasks(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPersistentCaches", reflect.TypeOf((*MockService)(nil).GetPersistentCaches), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPersistentCacheTasks", reflect.TypeOf((*MockService)(nil).GetPersistentCacheTasks), arg0, arg1)
 }
 
 // GetPersonalAccessToken mocks base method.
diff --git a/manager/service/persistent_cache.go b/manager/service/persistent_cache.go
deleted file mode 100644
index 58761ada..00000000
--- a/manager/service/persistent_cache.go
+++ /dev/null
@@ -1,712 +0,0 @@
-/*
- *     Copyright 2025 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 service
-
-import (
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/bits-and-blooms/bitset"
-
-	logger "d7y.io/dragonfly/v2/internal/dflog"
-	"d7y.io/dragonfly/v2/manager/types"
-	pkgredis "d7y.io/dragonfly/v2/pkg/redis"
-)
-
-// DestroyPersistentCache deletes a persistent cache task from Redis based on query parameters.
-func (s *service) DestroyPersistentCache(ctx context.Context, params types.PersistentCacheParams) error {
-	taskKey := pkgredis.MakePersistentCacheTaskKeyInScheduler(params.SchedulerClusterID, params.TaskID)
-	return s.rdb.Del(ctx, taskKey).Err()
-}
-
-// GetPersistentCache retrieves a persistent cache task from Redis based on query parameters.
-func (s *service) GetPersistentCache(ctx context.Context, params types.PersistentCacheParams) (*types.GetPersistentCacheResponse, error) {
-	// Get task data from Redis.
-	taskKey := pkgredis.MakePersistentCacheTaskKeyInScheduler(params.SchedulerClusterID, params.TaskID)
-
-	rawTask, err := s.rdb.HGetAll(ctx, taskKey).Result()
-	if err != nil {
-		logger.Warnf("getting task %s failed from redis: %v", taskKey, err)
-		return nil, err
-	}
-
-	if len(rawTask) == 0 {
-		logger.Warnf("task %s not found in redis", taskKey)
-		return nil, errors.New("task not found")
-	}
-
-	// Parse task data.
-	response, err := s.parseTaskData(ctx, rawTask)
-	if err != nil {
-		logger.Warnf("parse task %s data failed: %v", taskKey, err)
-		return nil, err
-	}
-
-	// Load peers for this task.
-	peers, err := s.loadPeersForTask(ctx, taskKey, params.SchedulerClusterID)
-	if err != nil {
-		logger.Warnf("load peers for task %s failed: %v", taskKey, err)
-	}
-
-	response.PersistentCachePeers = peers
-
-	return &response, nil
-}
-
-// GetPersistentCaches retrieves persistent cache tasks from Redis based on query parameters.
-func (s *service) GetPersistentCaches(ctx context.Context, q types.GetPersistentCachesQuery) ([]types.GetPersistentCacheResponse, int64, error) {
-	var responses []types.GetPersistentCacheResponse
-
-	// Get all scheduler cluster IDs if none specified.
-	schedulerClusterIDs := q.SchedulerClusterIDs
-	if len(schedulerClusterIDs) == 0 {
-		// Get all available scheduler cluster IDs.
-		var cursor uint64
-		for {
-			// Scan all keys with prefix, for example:
-			// "scheduler:scheduler-clusters:1".
-			prefix := fmt.Sprintf("%s:", pkgredis.MakeNamespaceKeyInScheduler(pkgredis.SchedulerClustersNamespace))
-			keys, cursor, err := s.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", prefix), 10).Result()
-			if err != nil {
-				logger.Errorf("scan scheduler clusterIDs failed: %v", err)
-				return nil, 0, err
-			}
-
-			for _, key := range keys {
-				// If context is done, return error.
-				if err := ctx.Err(); err != nil {
-					return nil, 0, err
-				}
-
-				// Remove prefix from key.
-				suffix := strings.TrimPrefix(key, prefix)
-				if suffix == "" {
-					logger.Error("invalid key")
-					continue
-				}
-
-				// Extract scheduler clusterID from suffix.
-				// If suffix does not contain ":", it is a scheduler clusterID.
-				if !strings.ContainsRune(suffix, ':') {
-					schedulerClusterID, err := strconv.ParseUint(suffix, 10, 32)
-					if err != nil {
-						logger.Errorf("parse scheduler clusterID failed: %v", err)
-						continue
-					}
-					schedulerClusterIDs = append(schedulerClusterIDs, uint(schedulerClusterID))
-				}
-			}
-
-			if cursor == 0 {
-				break
-			}
-		}
-
-		if len(schedulerClusterIDs) == 0 {
-			return nil, 0, errors.New("no scheduler cluster found")
-		}
-	}
-
-	// Collect all task IDs from all specified clusters.
-	var allTaskKeys []string
-	for _, schedulerClusterID := range schedulerClusterIDs {
-		// Get all task keys in the cluster.
-		var cursor uint64
-		for {
-			var (
-				taskKeys []string
-				err      error
-			)
-
-			// For example, if {prefix} is "scheduler:scheduler-clusters:1:persistent-cache-tasks:", keys could be:
-			// "{prefix}{taskID}:persistent-cache-peers", "{prefix}{taskID}:persistent-peers" and "{prefix}{taskID}".
-			// Scan all keys with prefix.
-			prefix := fmt.Sprintf("%s:", pkgredis.MakePersistentCacheTasksInScheduler(schedulerClusterID))
-			taskKeys, cursor, err = s.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", prefix), 10).Result()
-			if err != nil {
-				logger.Error("scan tasks failed")
-				continue
-			}
-
-			taskIDs := make(map[string]struct{})
-			for _, taskKey := range taskKeys {
-				// If context is done, return error.
-				if err := ctx.Err(); err != nil {
-					continue
-				}
-
-				// Remove prefix from task key.
-				suffix := strings.TrimPrefix(taskKey, prefix)
-				if suffix == "" {
-					logger.Error("invalid task key")
-					continue
-				}
-
-				// suffix is a non-empty string like:
-				// "{taskID}:persistent-cache-peers", "{taskID}:persistent-peers" and "{taskID}".
-				// Extract taskID from suffix and avoid duplicate taskID.
-				taskID := strings.Split(suffix, ":")[0]
-				if _, ok := taskIDs[taskID]; ok {
-					continue
-				}
-				taskIDs[taskID] = struct{}{}
-
-				allTaskKeys = append(allTaskKeys, taskKey)
-			}
-
-			if cursor == 0 {
-				break
-			}
-		}
-	}
-
-	// Calculate total count and pagination.
-	totalCount := int64(len(allTaskKeys))
-	startIndex := (q.Page - 1) * q.PerPage
-	endIndex := startIndex + q.PerPage
-
-	if startIndex >= int(totalCount) {
-		return []types.GetPersistentCacheResponse{}, totalCount, nil
-	}
-
-	if endIndex > int(totalCount) {
-		endIndex = int(totalCount)
-	}
-
-	// Get paginated task IDs and keys.
-	taskKeys := allTaskKeys[startIndex:endIndex]
-
-	// Process each task.
-	for _, taskKey := range taskKeys {
-		schedulerClusterID, err := pkgredis.ExtractSchedulerClusterIDFromPersistentCacheTaskKey(taskKey)
-		if err != nil {
-			logger.Warnf("extract scheduler cluster ID from persistent cache task key %s failed: %v", taskKey, err)
-			continue
-		}
-
-		// Get task data from Redis.
-		rawTask, err := s.rdb.HGetAll(ctx, taskKey).Result()
-		if err != nil {
-			logger.Warnf("getting task %s failed from redis: %v", taskKey, err)
-			continue
-		}
-
-		if len(rawTask) == 0 {
-			logger.Warnf("task %s not found in redis", taskKey)
-			continue
-		}
-
-		// Parse task data.
-		response, err := s.parseTaskData(ctx, rawTask)
-		if err != nil {
-			logger.Warnf("parse task %s data failed: %v", taskKey, err)
-			continue
-		}
-
-		// Load peers for this task.
-		peers, err := s.loadPeersForTask(ctx, taskKey, schedulerClusterID)
-		if err != nil {
-			logger.Warnf("load peers for task %s failed: %v", taskKey, err)
-		}
-
-		response.PersistentCachePeers = peers
-		responses = append(responses, response)
-	}
-
-	return responses, totalCount, nil
-}
-
-// parseTaskData parses raw task data from Redis and returns a GetPersistentCacheResponse.
-func (s *service) parseTaskData(ctx context.Context, rawTask map[string]string) (types.GetPersistentCacheResponse, error) {
-	var response types.GetPersistentCacheResponse
-
-	// Set task ID.
-	response.TaskID = rawTask["id"]
-
-	// Parse PersistentReplicaCount.
-	if persistentReplicaCount, err := strconv.ParseUint(rawTask["persistent_replica_count"], 10, 64); err == nil {
-		response.PersistentReplicaCount = persistentReplicaCount
-	} else {
-		logger.Warnf("parsing persistent replica count failed: %v", err)
-	}
-
-	// Set Tag and Application.
-	response.Tag = rawTask["tag"]
-	response.Application = rawTask["application"]
-
-	// Parse PieceLength.
-	if pieceLength, err := strconv.ParseUint(rawTask["piece_length"], 10, 64); err == nil {
-		response.PieceLength = pieceLength
-	} else {
-		logger.Warnf("parsing piece length failed: %v", err)
-	}
-
-	// Parse ContentLength.
-	if contentLength, err := strconv.ParseUint(rawTask["content_length"], 10, 64); err == nil {
-		response.ContentLength = contentLength
-	} else {
-		logger.Warnf("parsing content length failed: %v", err)
-	}
-
-	// Parse TotalPieceCount.
-	if totalPieceCount, err := strconv.ParseUint(rawTask["total_piece_count"], 10, 32); err == nil {
-		response.TotalPieceCount = uint32(totalPieceCount)
-	} else {
-		logger.Warnf("parsing total piece count failed: %v", err)
-	}
-
-	// Set State.
-	response.State = rawTask["state"]
-
-	// Parse TTL.
-	if ttl, err := strconv.ParseInt(rawTask["ttl"], 10, 64); err == nil {
-		response.TTL = time.Duration(ttl)
-	} else {
-		logger.Warnf("parsing ttl failed: %v", err)
-	}
-
-	// Parse CreatedAt.
-	if createdAt, err := time.Parse(time.RFC3339, rawTask["created_at"]); err == nil {
-		response.CreatedAt = createdAt
-	} else {
-		logger.Warnf("parsing created at failed: %v", err)
-	}
-
-	// Parse UpdatedAt.
-	if updatedAt, err := time.Parse(time.RFC3339, rawTask["updated_at"]); err == nil {
-		response.UpdatedAt = updatedAt
-	} else {
-		logger.Warnf("parsing updated at failed: %v", err)
-	}
-
-	return response, nil
-}
-
-// loadPeersForTask loads peers for a specific task from Redis.
-func (s *service) loadPeersForTask(ctx context.Context, taskID string, schedulerClusterID uint) ([]types.PersistentCachePeer, error) {
-	var peers []types.PersistentCachePeer
-
-	// Get peer IDs for the task.
-	peerIDs, err := s.rdb.SMembers(ctx, pkgredis.MakePersistentCachePeersOfPersistentCacheTaskInScheduler(schedulerClusterID, taskID)).Result()
-	if err != nil {
-		return nil, err
-	}
-
-	// Process each peer.
-	for _, peerID := range peerIDs {
-		// Get peer data from Redis.
-		rawPeer, err := s.rdb.HGetAll(ctx, pkgredis.MakePersistentCachePeerKeyInScheduler(schedulerClusterID, peerID)).Result()
-		if err != nil {
-			logger.Warnf("getting peer %s failed from redis: %v", peerID, err)
-			continue
-		}
-
-		if len(rawPeer) == 0 {
-			logger.Warnf("peer %s not found in redis", peerID)
-			continue
-		}
-
-		// Parse peer data.
-		peer, err := s.parsePeerData(ctx, rawPeer, schedulerClusterID)
-		if err != nil {
-			logger.Warnf("parse peer %s data failed: %v", peerID, err)
-			continue
-		}
-
-		peers = append(peers, peer)
-	}
-
-	return peers, nil
-}
-
-// parsePeerData parses raw peer data from Redis and returns a PersistentCachePeer.
-func (s *service) parsePeerData(ctx context.Context, rawPeer map[string]string, schedulerClusterID uint) (types.PersistentCachePeer, error) {
-	var peer types.PersistentCachePeer
-
-	// Set ID.
-	peer.ID = rawPeer["id"]
-
-	// Parse Persistent.
-	if persistent, err := strconv.ParseBool(rawPeer["persistent"]); err == nil {
-		peer.Persistent = persistent
-	} else {
-		logger.Warnf("parsing persistent failed: %v", err)
-	}
-
-	// Parse FinishedPieces.
-	finishedPieces := &bitset.BitSet{}
-	if err := finishedPieces.UnmarshalBinary([]byte(rawPeer["finished_pieces"])); err == nil {
-		peer.FinishedPieces = finishedPieces
-	} else {
-		logger.Warnf("unmarshal finished pieces failed: %v", err)
-	}
-
-	// Set State.
-	peer.State = rawPeer["state"]
-
-	// Parse BlockParents.
-	blockParents := []string{}
-	if err := json.Unmarshal([]byte(rawPeer["block_parents"]), &blockParents); err == nil {
-		peer.BlockParents = blockParents
-	} else {
-		logger.Warnf("unmarshal block parents failed: %v", err)
-	}
-
-	// Parse Cost.
-	if cost, err := strconv.ParseInt(rawPeer["cost"], 10, 64); err == nil {
-		peer.Cost = time.Duration(cost)
-	} else {
-		logger.Warnf("parsing cost failed: %v", err)
-	}
-
-	// Parse CreatedAt.
-	if createdAt, err := time.Parse(time.RFC3339, rawPeer["created_at"]); err == nil {
-		peer.CreatedAt = createdAt
-	} else {
-		logger.Warnf("parsing created at failed: %v", err)
-	}
-
-	// Parse UpdatedAt.
-	if updatedAt, err := time.Parse(time.RFC3339, rawPeer["updated_at"]); err == nil {
-		peer.UpdatedAt = updatedAt
-	} else {
-		logger.Warnf("parsing updated at failed: %v", err)
-	}
-
-	// Load host data if host_id is available.
-	if hostID, ok := rawPeer["host_id"]; ok && hostID != "" {
-		host, err := s.loadHostData(ctx, hostID, schedulerClusterID)
-		if err != nil {
-			logger.Warnf("load host %s data failed: %v", hostID, err)
-		} else {
-			peer.Host = host
-		}
-	}
-
-	return peer, nil
-}
-
-// loadHostData loads host data from Redis.
-func (s *service) loadHostData(ctx context.Context, hostID string, schedulerClusterID uint) (types.PersistentCachePeerHost, error) {
-	var host types.PersistentCachePeerHost
-
-	// Get host data from Redis.
-	rawHost, err := s.rdb.HGetAll(ctx, pkgredis.MakePersistentCacheHostKeyInScheduler(schedulerClusterID, hostID)).Result()
-	if err != nil {
-		return host, err
-	}
-
-	if len(rawHost) == 0 {
-		return host, nil
-	}
-
-	// Set basic host information.
-	host.ID = rawHost["id"]
-	host.Type = rawHost["type"]
-	host.Hostname = rawHost["hostname"]
-	host.IP = rawHost["ip"]
-	host.OS = rawHost["os"]
-	host.Platform = rawHost["platform"]
-	host.PlatformFamily = rawHost["platform_family"]
-	host.PlatformVersion = rawHost["platform_version"]
-	host.KernelVersion = rawHost["kernel_version"]
-
-	// Parse integer fields.
-	if port, err := strconv.ParseInt(rawHost["port"], 10, 32); err == nil {
-		host.Port = int32(port)
-	} else {
-		logger.Warnf("parsing port failed: %v", err)
-	}
-
-	if downloadPort, err := strconv.ParseInt(rawHost["download_port"], 10, 32); err == nil {
-		host.DownloadPort = int32(downloadPort)
-	} else {
-		logger.Warnf("parsing download port failed: %v", err)
-	}
-
-	// Parse boolean fields.
-	if disableShared, err := strconv.ParseBool(rawHost["disable_shared"]); err == nil {
-		host.DisableShared = disableShared
-	} else {
-		logger.Warnf("parsing disable shared failed: %v", err)
-	}
-
-	// Parse CPU information.
-	if cpuLogicalCount, err := strconv.ParseUint(rawHost["cpu_logical_count"], 10, 32); err == nil {
-		host.CPU.LogicalCount = uint32(cpuLogicalCount)
-	} else {
-		logger.Warnf("parsing cpu logical count failed: %v", err)
-	}
-
-	if cpuPhysicalCount, err := strconv.ParseUint(rawHost["cpu_physical_count"], 10, 32); err == nil {
-		host.CPU.PhysicalCount = uint32(cpuPhysicalCount)
-	} else {
-		logger.Warnf("parsing cpu physical count failed: %v", err)
-	}
-
-	if cpuPercent, err := strconv.ParseFloat(rawHost["cpu_percent"], 64); err == nil {
-		host.CPU.Percent = cpuPercent
-	} else {
-		logger.Warnf("parsing cpu percent failed: %v", err)
-	}
-
-	if cpuProcessPercent, err := strconv.ParseFloat(rawHost["cpu_process_percent"], 64); err == nil {
-		host.CPU.ProcessPercent = cpuProcessPercent
-	} else {
-		logger.Warnf("parsing cpu process percent failed: %v", err)
-	}
-
-	// Parse CPU Times.
-	if cpuTimesUser, err := strconv.ParseFloat(rawHost["cpu_times_user"], 64); err == nil {
-		host.CPU.Times.User = cpuTimesUser
-	} else {
-		logger.Warnf("parsing cpu times user failed: %v", err)
-	}
-
-	if cpuTimesSystem, err := strconv.ParseFloat(rawHost["cpu_times_system"], 64); err == nil {
-		host.CPU.Times.System = cpuTimesSystem
-	} else {
-		logger.Warnf("parsing cpu times system failed: %v", err)
-	}
-
-	if cpuTimesIdle, err := strconv.ParseFloat(rawHost["cpu_times_idle"], 64); err == nil {
-		host.CPU.Times.Idle = cpuTimesIdle
-	} else {
-		logger.Warnf("parsing cpu times idle failed: %v", err)
-	}
-
-	if cpuTimesNice, err := strconv.ParseFloat(rawHost["cpu_times_nice"], 64); err == nil {
-		host.CPU.Times.Nice = cpuTimesNice
-	} else {
-		logger.Warnf("parsing cpu times nice failed: %v", err)
-	}
-
-	if cpuTimesIowait, err := strconv.ParseFloat(rawHost["cpu_times_iowait"], 64); err == nil {
-		host.CPU.Times.Iowait = cpuTimesIowait
-	} else {
-		logger.Warnf("parsing cpu times iowait failed: %v", err)
-	}
-
-	if cpuTimesIrq, err := strconv.ParseFloat(rawHost["cpu_times_irq"], 64); err == nil {
-		host.CPU.Times.Irq = cpuTimesIrq
-	} else {
-		logger.Warnf("parsing cpu times irq failed: %v", err)
-	}
-
-	if cpuTimesSoftirq, err := strconv.ParseFloat(rawHost["cpu_times_softirq"], 64); err == nil {
-		host.CPU.Times.Softirq = cpuTimesSoftirq
-	} else {
-		logger.Warnf("parsing cpu times softirq failed: %v", err)
-	}
-
-	if cpuTimesSteal, err := strconv.ParseFloat(rawHost["cpu_times_steal"], 64); err == nil {
-		host.CPU.Times.Steal = cpuTimesSteal
-	} else {
-		logger.Warnf("parsing cpu times steal failed: %v", err)
-	}
-
-	if cpuTimesGuest, err := strconv.ParseFloat(rawHost["cpu_times_guest"], 64); err == nil {
-		host.CPU.Times.Guest = cpuTimesGuest
-	} else {
-		logger.Warnf("parsing cpu times guest failed: %v", err)
-	}
-
-	if cpuTimesGuestNice, err := strconv.ParseFloat(rawHost["cpu_times_guest_nice"], 64); err == nil {
-		host.CPU.Times.GuestNice = cpuTimesGuestNice
-	} else {
-		logger.Warnf("parsing cpu times guest nice failed: %v", err)
-	}
-
-	// Parse Memory information.
-	if memoryTotal, err := strconv.ParseUint(rawHost["memory_total"], 10, 64); err == nil {
-		host.Memory.Total = memoryTotal
-	} else {
-		logger.Warnf("parsing memory total failed: %v", err)
-	}
-
-	if memoryAvailable, err := strconv.ParseUint(rawHost["memory_available"], 10, 64); err == nil {
-		host.Memory.Available = memoryAvailable
-	} else {
-		logger.Warnf("parsing memory available failed: %v", err)
-	}
-
-	if memoryUsed, err := strconv.ParseUint(rawHost["memory_used"], 10, 64); err == nil {
-		host.Memory.Used = memoryUsed
-	} else {
-		logger.Warnf("parsing memory used failed: %v", err)
-	}
-
-	if memoryUsedPercent, err := strconv.ParseFloat(rawHost["memory_used_percent"], 64); err == nil {
-		host.Memory.UsedPercent = memoryUsedPercent
-	} else {
-		logger.Warnf("parsing memory used percent failed: %v", err)
-	}
-
-	if memoryProcessUsedPercent, err := strconv.ParseFloat(rawHost["memory_process_used_percent"], 64); err == nil {
-		host.Memory.ProcessUsedPercent = memoryProcessUsedPercent
-	} else {
-		logger.Warnf("parsing memory process used percent failed: %v", err)
-	}
-
-	if memoryFree, err := strconv.ParseUint(rawHost["memory_free"], 10, 64); err == nil {
-		host.Memory.Free = memoryFree
-	} else {
-		logger.Warnf("parsing memory free failed: %v", err)
-	}
-
-	// Parse Network information.
-	if tcpConnectionCount, err := strconv.ParseUint(rawHost["tcp_connection_count"], 10, 32); err == nil {
-		host.Network.TCPConnectionCount = uint32(tcpConnectionCount)
-	} else {
-		logger.Warnf("parsing tcp connection count failed: %v", err)
-	}
-
-	if uploadTCPConnectionCount, err := strconv.ParseUint(rawHost["upload_tcp_connection_count"], 10, 32); err == nil {
-		host.Network.UploadTCPConnectionCount = uint32(uploadTCPConnectionCount)
-	} else {
-		logger.Warnf("parsing upload tcp connection count failed: %v", err)
-	}
-
-	host.Network.Location = rawHost["location"]
-	host.Network.IDC = rawHost["idc"]
-
-	if downloadRate, err := strconv.ParseUint(rawHost["download_rate"], 10, 64); err == nil {
-		host.Network.DownloadRate = downloadRate
-	} else {
-		logger.Warnf("parsing download rate failed: %v", err)
-	}
-
-	if downloadRateLimit, err := strconv.ParseUint(rawHost["download_rate_limit"], 10, 64); err == nil {
-		host.Network.DownloadRateLimit = downloadRateLimit
-	} else {
-		logger.Warnf("parsing download rate limit failed: %v", err)
-	}
-
-	if uploadRate, err := strconv.ParseUint(rawHost["upload_rate"], 10, 64); err == nil {
-		host.Network.UploadRate = uploadRate
-	} else {
-		logger.Warnf("parsing upload rate failed: %v", err)
-	}
-
-	if uploadRateLimit, err := strconv.ParseUint(rawHost["upload_rate_limit"], 10, 64); err == nil {
-		host.Network.UploadRateLimit = uploadRateLimit
-	} else {
-		logger.Warnf("parsing upload rate limit failed: %v", err)
-	}
-
-	// Parse Disk information.
-	if diskTotal, err := strconv.ParseUint(rawHost["disk_total"], 10, 64); err == nil {
-		host.Disk.Total = diskTotal
-	} else {
-		logger.Warnf("parsing disk total failed: %v", err)
-	}
-
-	if diskFree, err := strconv.ParseUint(rawHost["disk_free"], 10, 64); err == nil {
-		host.Disk.Free = diskFree
-	} else {
-		logger.Warnf("parsing disk free failed: %v", err)
-	}
-
-	if diskUsed, err := strconv.ParseUint(rawHost["disk_used"], 10, 64); err == nil {
-		host.Disk.Used = diskUsed
-	} else {
-		logger.Warnf("parsing disk used failed: %v", err)
-	}
-
-	if diskUsedPercent, err := strconv.ParseFloat(rawHost["disk_used_percent"], 64); err == nil {
-		host.Disk.UsedPercent = diskUsedPercent
-	} else {
-		logger.Warnf("parsing disk used percent failed: %v", err)
-	}
-
-	if diskInodesTotal, err := strconv.ParseUint(rawHost["disk_inodes_total"], 10, 64); err == nil {
-		host.Disk.InodesTotal = diskInodesTotal
-	} else {
-		logger.Warnf("parsing disk inodes total failed: %v", err)
-	}
-
-	if diskInodesUsed, err := strconv.ParseUint(rawHost["disk_inodes_used"], 10, 64); err == nil {
-		host.Disk.InodesUsed = diskInodesUsed
-	} else {
-		logger.Warnf("parsing disk inodes used failed: %v", err)
-	}
-
-	if diskInodesFree, err := strconv.ParseUint(rawHost["disk_inodes_free"], 10, 64); err == nil {
-		host.Disk.InodesFree = diskInodesFree
-	} else {
-		logger.Warnf("parsing disk inodes free failed: %v", err)
-	}
-
-	if diskInodesUsedPercent, err := strconv.ParseFloat(rawHost["disk_inodes_used_percent"], 64); err == nil {
-		host.Disk.InodesUsedPercent = diskInodesUsedPercent
-	} else {
-		logger.Warnf("parsing disk inodes used percent failed: %v", err)
-	}
-
-	if diskWriteBandwidth, err := strconv.ParseUint(rawHost["disk_write_bandwidth"], 10, 64); err == nil {
-		host.Disk.WriteBandwidth = diskWriteBandwidth
-	} else {
-		logger.Warnf("parsing disk write bandwidth failed: %v", err)
-	}
-
-	if diskReadBandwidth, err := strconv.ParseUint(rawHost["disk_read_bandwidth"], 10, 64); err == nil {
-		host.Disk.ReadBandwidth = diskReadBandwidth
-	} else {
-		logger.Warnf("parsing disk read bandwidth failed: %v", err)
-	}
-
-	// Parse Build information.
-	host.Build.GitVersion = rawHost["build_git_version"]
-	host.Build.GitCommit = rawHost["build_git_commit"]
-	host.Build.GoVersion = rawHost["build_go_version"]
-	host.Build.Platform = rawHost["build_platform"]
-
-	// Parse SchedulerClusterID.
-	if schedulerClusterID, err := strconv.ParseUint(rawHost["scheduler_cluster_id"], 10, 64); err == nil {
-		host.SchedulerClusterID = schedulerClusterID
-	} else {
-		logger.Warnf("parsing scheduler cluster id failed: %v", err)
-	}
-
-	// Parse AnnounceInterval.
-	if announceInterval, err := strconv.ParseInt(rawHost["announce_interval"], 10, 64); err == nil {
-		host.AnnounceInterval = time.Duration(announceInterval)
-	} else {
-		logger.Warnf("parsing announce interval failed: %v", err)
-	}
-
-	// Parse CreatedAt.
-	if createdAt, err := time.Parse(time.RFC3339, rawHost["created_at"]); err == nil {
-		host.CreatedAt = createdAt
-	} else {
-		logger.Warnf("parsing created at failed: %v", err)
-	}
-
-	// Parse UpdatedAt.
-	if updatedAt, err := time.Parse(time.RFC3339, rawHost["updated_at"]); err == nil {
-		host.UpdatedAt = updatedAt
-	} else {
-		logger.Warnf("parsing updated at failed: %v", err)
-	}
-
-	return host, nil
-}
diff --git a/manager/service/persistent_cache_task.go b/manager/service/persistent_cache_task.go
new file mode 100644
index 00000000..4618d4ff
--- /dev/null
+++ b/manager/service/persistent_cache_task.go
@@ -0,0 +1,588 @@
+/*
+ *     Copyright 2025 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 service
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/bits-and-blooms/bitset"
+
+	logger "d7y.io/dragonfly/v2/internal/dflog"
+	"d7y.io/dragonfly/v2/manager/types"
+	pkgredis "d7y.io/dragonfly/v2/pkg/redis"
+)
+
+// DestroyPersistentCacheTask deletes a persistent cache task from Redis based on query parameters.
+func (s *service) DestroyPersistentCacheTask(ctx context.Context, schedulerClusterID uint, id string) error {
+	return s.rdb.Del(ctx, pkgredis.MakePersistentCacheTaskKeyInScheduler(schedulerClusterID, id)).Err()
+}
+
+// GetPersistentCacheTask retrieves a persistent cache task from Redis based on query parameters.
+func (s *service) GetPersistentCacheTask(ctx context.Context, schedulerClusterID uint, id string) (*types.PersistentCacheTask, error) {
+	return s.loadTask(ctx, schedulerClusterID, id)
+}
+
+// GetPersistentCacheTasks retrieves persistent cache tasks from Redis based on query parameters.
+func (s *service) GetPersistentCacheTasks(ctx context.Context, q types.GetPersistentCacheTasksQuery) ([]*types.PersistentCacheTask, int64, error) {
+	tasks, err := s.loadAllTasks(ctx, q.SchedulerClusterID)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	return tasks, int64(len(tasks)), nil
+}
+
+// loadAllTasks loads all persistent cache tasks from Redis based on the provided scheduler cluster ID.
+func (s *service) loadAllTasks(ctx context.Context, schedulerClusterID uint) ([]*types.PersistentCacheTask, error) {
+	var (
+		tasks  []*types.PersistentCacheTask
+		cursor uint64
+	)
+
+	for {
+		var (
+			taskKeys []string
+			err      error
+		)
+
+		// Example: If {prefix} is "scheduler:scheduler-clusters:1:persistent-cache-tasks:".
+		//
+		// Keys include:
+		// - "{prefix}{taskID}:persistent-cache-peers"
+		// - "{prefix}{taskID}:persistent-peers"
+		// - "{prefix}{taskID}"
+		//
+		// Purpose: Scan all keys starting with {prefix}.
+		prefix := fmt.Sprintf("%s:", pkgredis.MakePersistentCacheTasksInScheduler(schedulerClusterID))
+		taskKeys, cursor, err = s.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", prefix), 10).Result()
+		if err != nil {
+			logger.Error("scan tasks failed")
+			return nil, err
+		}
+
+		taskIDs := make(map[string]struct{})
+		for _, taskKey := range taskKeys {
+			// If context is done, return error.
+			if err := ctx.Err(); err != nil {
+				return nil, err
+			}
+
+			// Remove prefix from task key.
+			suffix := strings.TrimPrefix(taskKey, prefix)
+			if suffix == "" {
+				logger.Error("invalid task key")
+				continue
+			}
+
+			// Suffix is a non-empty string.
+			//
+			// Keys include:
+			// - "{taskID}:persistent-cache-peers"
+			// - "{taskID}:persistent-peers"
+			// - "{taskID}"
+			//
+			// Purpose: Extract taskID from suffix and ensure no duplicate taskIDs.
+			taskID := strings.Split(suffix, ":")[0]
+			if _, ok := taskIDs[taskID]; ok {
+				continue
+			}
+			taskIDs[taskID] = struct{}{}
+
+			task, err := s.loadTask(ctx, schedulerClusterID, taskID)
+			if err != nil {
+				logger.WithTaskID(taskID).Error("load task failed")
+				continue
+			}
+
+			tasks = append(tasks, task)
+		}
+
+		if cursor == 0 {
+			break
+		}
+	}
+
+	return tasks, nil
+}
+
+// loadTask loads a task from Redis based on the provided key.
+func (s *service) loadTask(ctx context.Context, schedulerClusterID uint, id string) (*types.PersistentCacheTask, error) {
+	taskKey := pkgredis.MakePersistentCacheTaskKeyInScheduler(schedulerClusterID, id)
+	rawTask, err := s.rdb.HGetAll(ctx, taskKey).Result()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(rawTask) == 0 {
+		return nil, errors.New("task not found")
+	}
+
+	task := &types.PersistentCacheTask{
+		ID:          rawTask["id"],
+		Tag:         rawTask["tag"],
+		Application: rawTask["application"],
+		State:       rawTask["state"],
+	}
+
+	// Parse PersistentReplicaCount.
+	persistentReplicaCount, err := strconv.ParseUint(rawTask["persistent_replica_count"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	task.PersistentReplicaCount = persistentReplicaCount
+
+	// Parse PieceLength.
+	pieceLength, err := strconv.ParseUint(rawTask["piece_length"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	task.PieceLength = pieceLength
+
+	// Parse ContentLength.
+	contentLength, err := strconv.ParseUint(rawTask["content_length"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	task.ContentLength = contentLength
+
+	// Parse TotalPieceCount.
+	totalPieceCount, err := strconv.ParseUint(rawTask["total_piece_count"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	task.TotalPieceCount = uint32(totalPieceCount)
+
+	// Parse TTL.
+	ttl, err := strconv.ParseInt(rawTask["ttl"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	task.TTL = time.Duration(ttl)
+
+	// Parse CreatedAt.
+	createdAt, err := time.Parse(time.RFC3339, rawTask["created_at"])
+	if err != nil {
+		return nil, err
+	}
+	task.CreatedAt = createdAt
+
+	// Parse UpdatedAt.
+	updatedAt, err := time.Parse(time.RFC3339, rawTask["updated_at"])
+	if err != nil {
+		return nil, err
+	}
+	task.UpdatedAt = updatedAt
+
+	peers, err := s.loadAllPeersByTaskID(ctx, schedulerClusterID, task.ID)
+	if err != nil {
+		return nil, err
+	}
+	task.Peers = peers
+	return task, nil
+}
+
+// loadAllPeersByTaskID loads all peers associated with a task ID from Redis.
+func (s *service) loadAllPeersByTaskID(ctx context.Context, schedulerClusterID uint, taskID string) ([]*types.PersistentCachePeer, error) {
+	var peers []*types.PersistentCachePeer
+	peerIDs, err := s.rdb.SMembers(ctx, pkgredis.MakePersistentCachePeersOfPersistentCacheTaskInScheduler(schedulerClusterID, taskID)).Result()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, peerID := range peerIDs {
+		peer, err := s.loadPeer(ctx, schedulerClusterID, peerID)
+		if err != nil {
+			logger.Warnf("load peer %s failed: %v", peer.ID, err)
+			continue
+		}
+
+		peers = append(peers, peer)
+	}
+
+	return peers, nil
+}
+
+// loadPeer loads a peer from Redis based on the provided key.
+func (s *service) loadPeer(ctx context.Context, schedulerClusterID uint, id string) (*types.PersistentCachePeer, error) {
+	peerKey := pkgredis.MakePersistentCachePeerKeyInScheduler(schedulerClusterID, id)
+	rawPeer, err := s.rdb.HGetAll(ctx, peerKey).Result()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(rawPeer) == 0 {
+		return nil, errors.New("peer not found")
+	}
+
+	peer := &types.PersistentCachePeer{
+		ID:    rawPeer["id"],
+		State: rawPeer["state"],
+	}
+
+	// Parse Persistent.
+	persistent, err := strconv.ParseBool(rawPeer["persistent"])
+	if err != nil {
+		return nil, err
+	}
+	peer.Persistent = persistent
+
+	// Parse FinishedPieces.
+	finishedPieces := &bitset.BitSet{}
+	if err := finishedPieces.UnmarshalBinary([]byte(rawPeer["finished_pieces"])); err != nil {
+		return nil, err
+	}
+	peer.FinishedPieces = finishedPieces
+
+	// Parse BlockParents.
+	blockParents := []string{}
+	if err := json.Unmarshal([]byte(rawPeer["block_parents"]), &blockParents); err != nil {
+		return nil, err
+	}
+	peer.BlockParents = blockParents
+
+	// Parse Cost.
+	cost, err := strconv.ParseInt(rawPeer["cost"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	peer.Cost = time.Duration(cost)
+
+	// Parse CreatedAt.
+	createdAt, err := time.Parse(time.RFC3339, rawPeer["created_at"])
+	if err != nil {
+		return nil, err
+	}
+	peer.CreatedAt = createdAt
+
+	// Parse UpdatedAt.
+	updatedAt, err := time.Parse(time.RFC3339, rawPeer["updated_at"])
+	if err != nil {
+		return nil, err
+	}
+	peer.UpdatedAt = updatedAt
+
+	host, err := s.loadHost(ctx, schedulerClusterID, rawPeer["host_id"])
+	if err != nil {
+		return nil, err
+	}
+	peer.Host = host
+	return peer, nil
+}
+
+// loadHost loads a host from Redis based on the provided key.
+func (s *service) loadHost(ctx context.Context, schedulerClusterID uint, id string) (*types.PersistentCacheHost, error) {
+	hostKey := pkgredis.MakePersistentCacheHostKeyInScheduler(schedulerClusterID, id)
+	rawHost, err := s.rdb.HGetAll(ctx, hostKey).Result()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(rawHost) == 0 {
+		return nil, errors.New("host not found")
+	}
+
+	host := &types.PersistentCacheHost{
+		ID:                 rawHost["id"],
+		Type:               rawHost["type"],
+		Hostname:           rawHost["hostname"],
+		IP:                 rawHost["ip"],
+		OS:                 rawHost["os"],
+		Platform:           rawHost["platform"],
+		PlatformFamily:     rawHost["platform_family"],
+		PlatformVersion:    rawHost["platform_version"],
+		KernelVersion:      rawHost["kernel_version"],
+		SchedulerClusterID: schedulerClusterID,
+	}
+
+	// Parse integer fields.
+	port, err := strconv.ParseInt(rawHost["port"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	host.Port = int32(port)
+
+	// Parse download port.
+	downloadPort, err := strconv.ParseInt(rawHost["download_port"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	host.DownloadPort = int32(downloadPort)
+
+	// Parse boolean fields.
+	disableShared, err := strconv.ParseBool(rawHost["disable_shared"])
+	if err != nil {
+		return nil, err
+	}
+	host.DisableShared = disableShared
+
+	// Parse CPU information.
+	cpuLogicalCount, err := strconv.ParseUint(rawHost["cpu_logical_count"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.LogicalCount = uint32(cpuLogicalCount)
+
+	cpuPhysicalCount, err := strconv.ParseUint(rawHost["cpu_physical_count"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.PhysicalCount = uint32(cpuPhysicalCount)
+
+	cpuPercent, err := strconv.ParseFloat(rawHost["cpu_percent"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Percent = cpuPercent
+
+	cpuProcessPercent, err := strconv.ParseFloat(rawHost["cpu_process_percent"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.ProcessPercent = cpuProcessPercent
+
+	// Parse CPU Times.
+	cpuTimesUser, err := strconv.ParseFloat(rawHost["cpu_times_user"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.User = cpuTimesUser
+
+	cpuTimesSystem, err := strconv.ParseFloat(rawHost["cpu_times_system"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.System = cpuTimesSystem
+
+	cpuTimesIdle, err := strconv.ParseFloat(rawHost["cpu_times_idle"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Idle = cpuTimesIdle
+
+	cpuTimesNice, err := strconv.ParseFloat(rawHost["cpu_times_nice"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Nice = cpuTimesNice
+
+	cpuTimesIowait, err := strconv.ParseFloat(rawHost["cpu_times_iowait"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Iowait = cpuTimesIowait
+
+	cpuTimesIrq, err := strconv.ParseFloat(rawHost["cpu_times_irq"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Irq = cpuTimesIrq
+
+	cpuTimesSoftirq, err := strconv.ParseFloat(rawHost["cpu_times_softirq"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Softirq = cpuTimesSoftirq
+
+	cpuTimesSteal, err := strconv.ParseFloat(rawHost["cpu_times_steal"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Steal = cpuTimesSteal
+
+	cpuTimesGuest, err := strconv.ParseFloat(rawHost["cpu_times_guest"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.Guest = cpuTimesGuest
+
+	cpuTimesGuestNice, err := strconv.ParseFloat(rawHost["cpu_times_guest_nice"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.CPU.Times.GuestNice = cpuTimesGuestNice
+
+	// Parse Memory information.
+	memoryTotal, err := strconv.ParseUint(rawHost["memory_total"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Memory.Total = memoryTotal
+
+	memoryAvailable, err := strconv.ParseUint(rawHost["memory_available"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Memory.Available = memoryAvailable
+
+	memoryUsed, err := strconv.ParseUint(rawHost["memory_used"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Memory.Used = memoryUsed
+
+	memoryUsedPercent, err := strconv.ParseFloat(rawHost["memory_used_percent"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Memory.UsedPercent = memoryUsedPercent
+
+	memoryProcessUsedPercent, err := strconv.ParseFloat(rawHost["memory_process_used_percent"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Memory.ProcessUsedPercent = memoryProcessUsedPercent
+
+	memoryFree, err := strconv.ParseUint(rawHost["memory_free"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Memory.Free = memoryFree
+
+	// Parse Network information.
+	tcpConnectionCount, err := strconv.ParseUint(rawHost["tcp_connection_count"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	host.Network.TCPConnectionCount = uint32(tcpConnectionCount)
+
+	uploadTCPConnectionCount, err := strconv.ParseUint(rawHost["upload_tcp_connection_count"], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	host.Network.UploadTCPConnectionCount = uint32(uploadTCPConnectionCount)
+	host.Network.Location = rawHost["location"]
+	host.Network.IDC = rawHost["idc"]
+
+	downloadRate, err := strconv.ParseUint(rawHost["download_rate"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Network.DownloadRate = downloadRate
+
+	downloadRateLimit, err := strconv.ParseUint(rawHost["download_rate_limit"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Network.DownloadRateLimit = downloadRateLimit
+
+	uploadRate, err := strconv.ParseUint(rawHost["upload_rate"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Network.UploadRate = uploadRate
+
+	uploadRateLimit, err := strconv.ParseUint(rawHost["upload_rate_limit"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Network.UploadRateLimit = uploadRateLimit
+
+	// Parse Disk information.
+	diskTotal, err := strconv.ParseUint(rawHost["disk_total"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.Total = diskTotal
+
+	diskFree, err := strconv.ParseUint(rawHost["disk_free"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.Free = diskFree
+
+	diskUsed, err := strconv.ParseUint(rawHost["disk_used"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.Used = diskUsed
+
+	diskUsedPercent, err := strconv.ParseFloat(rawHost["disk_used_percent"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.UsedPercent = diskUsedPercent
+
+	diskInodesTotal, err := strconv.ParseUint(rawHost["disk_inodes_total"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.InodesTotal = diskInodesTotal
+
+	diskInodesUsed, err := strconv.ParseUint(rawHost["disk_inodes_used"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.InodesUsed = diskInodesUsed
+
+	diskInodesFree, err := strconv.ParseUint(rawHost["disk_inodes_free"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.InodesFree = diskInodesFree
+
+	diskInodesUsedPercent, err := strconv.ParseFloat(rawHost["disk_inodes_used_percent"], 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.InodesUsedPercent = diskInodesUsedPercent
+
+	diskWriteBandwidth, err := strconv.ParseUint(rawHost["disk_write_bandwidth"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.WriteBandwidth = diskWriteBandwidth
+
+	diskReadBandwidth, err := strconv.ParseUint(rawHost["disk_read_bandwidth"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.Disk.ReadBandwidth = diskReadBandwidth
+
+	// Parse Build information.
+	host.Build.GitVersion = rawHost["build_git_version"]
+	host.Build.GitCommit = rawHost["build_git_commit"]
+	host.Build.GoVersion = rawHost["build_go_version"]
+	host.Build.Platform = rawHost["build_platform"]
+
+	// Parse AnnounceInterval.
+	announceInterval, err := strconv.ParseInt(rawHost["announce_interval"], 10, 64)
+	if err != nil {
+		return nil, err
+	}
+	host.AnnounceInterval = time.Duration(announceInterval)
+
+	// Parse CreatedAt.
+	createdAt, err := time.Parse(time.RFC3339, rawHost["created_at"])
+	if err != nil {
+		return nil, err
+	}
+	host.CreatedAt = createdAt
+
+	// Parse UpdatedAt.
+	updatedAt, err := time.Parse(time.RFC3339, rawHost["updated_at"])
+	if err != nil {
+		return nil, err
+	}
+	host.UpdatedAt = updatedAt
+	return host, nil
+}
diff --git a/manager/service/service.go b/manager/service/service.go
index b3fbcd0f..cf0e7773 100644
--- a/manager/service/service.go
+++ b/manager/service/service.go
@@ -138,9 +138,9 @@ type Service interface {
 	GetPersonalAccessToken(context.Context, uint) (*models.PersonalAccessToken, error)
 	GetPersonalAccessTokens(context.Context, types.GetPersonalAccessTokensQuery) ([]models.PersonalAccessToken, int64, error)
 
-	DestroyPersistentCache(context.Context, types.PersistentCacheParams) error
-	GetPersistentCache(context.Context, types.PersistentCacheParams) (*types.GetPersistentCacheResponse, error)
-	GetPersistentCaches(context.Context, types.GetPersistentCachesQuery) ([]types.GetPersistentCacheResponse, int64, error)
+	DestroyPersistentCacheTask(context.Context, uint, string) error
+	GetPersistentCacheTask(context.Context, uint, string) (*types.PersistentCacheTask, error)
+	GetPersistentCacheTasks(context.Context, types.GetPersistentCacheTasksQuery) ([]*types.PersistentCacheTask, int64, error)
 }
 
 type service struct {
diff --git a/manager/types/persistent_cache.go b/manager/types/persistent_cache_task.go
similarity index 90%
rename from manager/types/persistent_cache.go
rename to manager/types/persistent_cache_task.go
index cd870ad6..c4db2e82 100644
--- a/manager/types/persistent_cache.go
+++ b/manager/types/persistent_cache_task.go
@@ -22,17 +22,24 @@ import (
 	"github.com/bits-and-blooms/bitset"
 )
 
-type PersistentCacheParams struct {
+type PersistentCacheTaskParams struct {
+	// ID is the task id of the persistent cache.
+	ID string `uri:"id" binding:"required"`
+}
+
+type DestroyPersistentCacheTaskQuery struct {
 	// SchedulerClusterID is the scheduler cluster id of the persistent cache.
-	SchedulerClusterID uint `uri:"scheduler_cluster_id" binding:"required"`
+	SchedulerClusterID uint `json:"scheduler_cluster_id" binding:"required"`
+}
 
-	// TaskID is the task id of the persistent cache.
-	TaskID string `uri:"task_id" binding:"required"`
+type GetPersistentCacheTaskQuery struct {
+	// SchedulerClusterID is the scheduler cluster id of the persistent cache.
+	SchedulerClusterID uint `json:"scheduler_cluster_id" binding:"required"`
 }
 
-type GetPersistentCachesQuery struct {
-	// SchedulerClusterIDs is the scheduler cluster ids of the persistent cache.
-	SchedulerClusterIDs []uint `json:"scheduler_cluster_ids" binding:"omitempty"`
+type GetPersistentCacheTasksQuery struct {
+	// SchedulerClusterID is the scheduler cluster id of the persistent cache.
+	SchedulerClusterID uint `json:"scheduler_cluster_id" binding:"required"`
 
 	// Page is the page number of the persistent cache list.
 	Page int `form:"page" binding:"omitempty,gte=1"`
@@ -41,9 +48,9 @@ type GetPersistentCachesQuery struct {
 	PerPage int `form:"per_page" binding:"omitempty,gte=1,lte=10000000"`
 }
 
-type GetPersistentCacheResponse struct {
-	// TaskID is task id.
-	TaskID string `json:"task_id" binding:"omitempty"`
+type PersistentCacheTask struct {
+	// ID is task id.
+	ID string `json:"id" binding:"omitempty"`
 
 	// PersistentReplicaCount is replica count of the persistent cache task.
 	PersistentReplicaCount uint64 `json:"persistent_replica_count" binding:"omitempty"`
@@ -76,7 +83,7 @@ type GetPersistentCacheResponse struct {
 	UpdatedAt time.Time `json:"updated_at" binding:"omitempty"`
 
 	// PersistentCachePeers is the list of persistent cache peers.
-	PersistentCachePeers []PersistentCachePeer `json:"persistent_cache_peers" binding:"omitempty"`
+	Peers []*PersistentCachePeer `json:"peers" binding:"omitempty"`
 }
 
 type PersistentCachePeer struct {
@@ -105,10 +112,10 @@ type PersistentCachePeer struct {
 	UpdatedAt time.Time `json:"updated_at" binding:"omitempty"`
 
 	// Host is the peer host.
-	Host PersistentCachePeerHost `json:"host" binding:"omitempty"`
+	Host *PersistentCacheHost `json:"host" binding:"omitempty"`
 }
 
-type PersistentCachePeerHost struct {
+type PersistentCacheHost struct {
 	// ID is host id.
 	ID string `json:"id" binding:"omitempty"`
 
@@ -290,7 +297,7 @@ type PersistentCachePeerHost struct {
 	} `json:"build" binding:"omitempty"`
 
 	// SchedulerClusterID is the scheduler cluster id matched by scopes.
-	SchedulerClusterID uint64 `json:"scheduler_cluster_id" binding:"omitempty"`
+	SchedulerClusterID uint `json:"scheduler_cluster_id" binding:"omitempty"`
 
 	// AnnounceInterval is the interval between host announces to scheduler.
 	AnnounceInterval time.Duration `json:"announce_interval" binding:"omitempty"`
diff --git a/pkg/redis/redis.go b/pkg/redis/redis.go
index cc07c336..eebb8203 100644
--- a/pkg/redis/redis.go
+++ b/pkg/redis/redis.go
@@ -19,8 +19,6 @@ package redis
 import (
 	"context"
 	"fmt"
-	"strconv"
-	"strings"
 
 	"github.com/redis/go-redis/v9"
 
@@ -193,19 +191,3 @@ func MakePersistentCacheHostsInScheduler(schedulerClusterID uint) string {
 func MakePersistentCachePeersOfPersistentCacheHostInScheduler(schedulerClusterID uint, hostID string) string {
 	return MakeKeyInScheduler(SchedulerClustersNamespace, fmt.Sprintf("%d:%s:%s:%s", schedulerClusterID, PersistentCacheHostsNamespace, hostID, PersistentCachePeersNamespace))
 }
-
-// ExtractSchedulerClusterIDFromPersistentCacheTaskKey extracts the scheduler cluster ID from a persistent cache task key.
-func ExtractSchedulerClusterIDFromPersistentCacheTaskKey(key string) (uint, error) {
-	parts := strings.Split(key, ":")
-	if len(parts) != 5 {
-		return 0, fmt.Errorf("invalid persistent cache task key: %s", key)
-	}
-
-	// For example, if the key is "scheduler:scheduler-clusters:1:persistent-cache-tasks:123456789", the scheduler cluster ID is 1.
-	clusterID, err := strconv.ParseUint(parts[2], 10, 32)
-	if err != nil {
-		return 0, fmt.Errorf("invalid persistent cache task key: %s", key)
-	}
-
-	return uint(clusterID), nil
-}
diff --git a/scheduler/resource/persistentcache/host_manager.go b/scheduler/resource/persistentcache/host_manager.go
index acd91eaa..03b58427 100644
--- a/scheduler/resource/persistentcache/host_manager.go
+++ b/scheduler/resource/persistentcache/host_manager.go
@@ -594,7 +594,7 @@ return true
 	}
 
 	// Prepare arguments.
-	args := []interface{}{
+	args := []any{
 		host.ID,
 		host.Type.Name(),
 		host.Hostname,
@@ -696,7 +696,7 @@ return true
 	}
 
 	// Prepare arguments.
-	args := []interface{}{
+	args := []any{
 		hostID,
 	}
 
diff --git a/scheduler/resource/persistentcache/peer_manager.go b/scheduler/resource/persistentcache/peer_manager.go
index 00816750..6bb7fed2 100644
--- a/scheduler/resource/persistentcache/peer_manager.go
+++ b/scheduler/resource/persistentcache/peer_manager.go
@@ -257,7 +257,7 @@ return true
 	}
 
 	// Prepare arguments.
-	args := []interface{}{
+	args := []any{
 		peer.ID,
 		peer.Persistent,
 		string(finishedPieces),
@@ -340,7 +340,7 @@ return true
 	}
 
 	// Prepare arguments.
-	args := []interface{}{
+	args := []any{
 		peerID,
 		peer.Persistent,
 		peer.Task.ID,
diff --git a/scheduler/resource/persistentcache/task_manager.go b/scheduler/resource/persistentcache/task_manager.go
index 9a20515d..3cec04ca 100644
--- a/scheduler/resource/persistentcache/task_manager.go
+++ b/scheduler/resource/persistentcache/task_manager.go
@@ -206,7 +206,7 @@ return true
 	}
 
 	// Prepare arguments.
-	args := []interface{}{
+	args := []any{
 		task.ID,
 		task.PersistentReplicaCount,
 		task.Tag,
@@ -250,9 +250,14 @@ func (t *taskManager) LoadAll(ctx context.Context) ([]*Task, error) {
 			err      error
 		)
 
-		// For example, if {prefix} is "scheduler:scheduler-clusters:1:persistent-cache-tasks:", keys could be:
-		// "{prefix}{taskID}:persistent-cache-peers", "{prefix}{taskID}:persistent-peers" and "{prefix}{taskID}".
-		// Scan all keys with prefix.
+		// Example: If {prefix} is "scheduler:scheduler-clusters:1:persistent-cache-tasks:".
+		//
+		// Keys include:
+		// - "{prefix}{taskID}:persistent-cache-peers"
+		// - "{prefix}{taskID}:persistent-peers"
+		// - "{prefix}{taskID}"
+		//
+		// Purpose: Scan all keys starting with {prefix}.
 		prefix := fmt.Sprintf("%s:", pkgredis.MakePersistentCacheTasksInScheduler(t.config.Manager.SchedulerClusterID))
 		taskKeys, cursor, err = t.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", prefix), 10).Result()
 		if err != nil {
@@ -274,9 +279,14 @@ func (t *taskManager) LoadAll(ctx context.Context) ([]*Task, error) {
 				continue
 			}
 
-			// suffix is a non-empty string like:
-			// "{taskID}:persistent-cache-peers", "{taskID}:persistent-peers" and "{taskID}".
-			// Extract taskID from suffix and avoid duplicate taskID.
+			// Suffix is a non-empty string.
+			//
+			// Keys include:
+			// - "{taskID}:persistent-cache-peers"
+			// - "{taskID}:persistent-peers"
+			// - "{taskID}"
+			//
+			// Purpose: Extract taskID from suffix and ensure no duplicate taskIDs.
 			taskID := strings.Split(suffix, ":")[0]
 			if _, ok := taskIDs[taskID]; ok {
 				continue
diff --git a/scheduler/resource/persistentcache/task_manager_mock.go b/scheduler/resource/persistentcache/task_manager_mock.go
index 906dd919..fd68201a 100644
--- a/scheduler/resource/persistentcache/task_manager_mock.go
+++ b/scheduler/resource/persistentcache/task_manager_mock.go
@@ -84,34 +84,34 @@ func (mr *MockTaskManagerMockRecorder) LoadAll(arg0 any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAll", reflect.TypeOf((*MockTaskManager)(nil).LoadAll), arg0)
 }
 
-// LoadCurrentReplicaCount mocks base method.
-func (m *MockTaskManager) LoadCurrentReplicaCount(arg0 context.Context, arg1 string) (uint64, error) {
+// LoadCurrentPersistentReplicaCount mocks base method.
+func (m *MockTaskManager) LoadCurrentPersistentReplicaCount(arg0 context.Context, arg1 string) (uint64, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "LoadCurrentReplicaCount", arg0, arg1)
+	ret := m.ctrl.Call(m, "LoadCurrentPersistentReplicaCount", arg0, arg1)
 	ret0, _ := ret[0].(uint64)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// LoadCurrentReplicaCount indicates an expected call of LoadCurrentReplicaCount.
-func (mr *MockTaskManagerMockRecorder) LoadCurrentReplicaCount(arg0, arg1 any) *gomock.Call {
+// LoadCurrentPersistentReplicaCount indicates an expected call of LoadCurrentPersistentReplicaCount.
+func (mr *MockTaskManagerMockRecorder) LoadCurrentPersistentReplicaCount(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadCurrentReplicaCount", reflect.TypeOf((*MockTaskManager)(nil).LoadCurrentReplicaCount), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadCurrentPersistentReplicaCount", reflect.TypeOf((*MockTaskManager)(nil).LoadCurrentPersistentReplicaCount), arg0, arg1)
 }
 
-// LoadCurrentPersistentReplicaCount mocks base method.
-func (m *MockTaskManager) LoadCurrentPersistentReplicaCount(arg0 context.Context, arg1 string) (uint64, error) {
+// LoadCurrentReplicaCount mocks base method.
+func (m *MockTaskManager) LoadCurrentReplicaCount(arg0 context.Context, arg1 string) (uint64, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "LoadCurrentPersistentReplicaCount", arg0, arg1)
+	ret := m.ctrl.Call(m, "LoadCurrentReplicaCount", arg0, arg1)
 	ret0, _ := ret[0].(uint64)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// LoadCurrentPersistentReplicaCount indicates an expected call of LoadCurrentPersistentReplicaCount.
-func (mr *MockTaskManagerMockRecorder) LoadCurrentPersistentReplicaCount(arg0, arg1 any) *gomock.Call {
+// LoadCurrentReplicaCount indicates an expected call of LoadCurrentReplicaCount.
+func (mr *MockTaskManagerMockRecorder) LoadCurrentReplicaCount(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadCurrentPersistentReplicaCount", reflect.TypeOf((*MockTaskManager)(nil).LoadCurrentPersistentReplicaCount), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadCurrentReplicaCount", reflect.TypeOf((*MockTaskManager)(nil).LoadCurrentReplicaCount), arg0, arg1)
 }
 
 // Store mocks base method.



Test patch:
diff --git a/manager/handlers/persistent_cache_test.go b/manager/handlers/persistent_cache_test.go
deleted file mode 100644
index eaae0395..00000000
--- a/manager/handlers/persistent_cache_test.go
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- *     Copyright 2025 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 handlers
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/gin-gonic/gin"
-	"github.com/stretchr/testify/assert"
-	"go.uber.org/mock/gomock"
-
-	"d7y.io/dragonfly/v2/manager/service/mocks"
-	"d7y.io/dragonfly/v2/manager/types"
-)
-
-var (
-	mockPersistentCacheParams = types.PersistentCacheParams{
-		SchedulerClusterID: 1,
-		TaskID:             "task-1",
-	}
-	mockGetPersistentCacheResponse = &types.GetPersistentCacheResponse{
-		TaskID: "task-1",
-		State:  "SUCCESS",
-	}
-	mockGetPersistentCachesResponse = []types.GetPersistentCacheResponse{
-		{
-			TaskID: "task-1",
-			State:  "SUCCESS",
-		},
-	}
-)
-
-func mockPersistentCacheRouter(h *Handlers) *gin.Engine {
-	r := gin.Default()
-	r.DELETE("/persistent-caches/:scheduler_cluster_id/:task_id", h.DestroyPersistentCache)
-	r.GET("/persistent-caches/:scheduler_cluster_id/:task_id", h.GetPersistentCache)
-	r.GET("/persistent-caches", h.GetPersistentCaches)
-	return r
-}
-
-func TestHandlers_DestroyPersistentCache(t *testing.T) {
-	tests := []struct {
-		name   string
-		req    *http.Request
-		mock   func(ms *mocks.MockServiceMockRecorder)
-		expect func(t *testing.T, w *httptest.ResponseRecorder)
-	}{
-		{
-			name: "unprocessable entity",
-			req:  httptest.NewRequest(http.MethodDelete, "/persistent-caches/invalid/task-1", nil),
-			mock: func(ms *mocks.MockServiceMockRecorder) {},
-			expect: func(t *testing.T, w *httptest.ResponseRecorder) {
-				assert := assert.New(t)
-				assert.Equal(http.StatusUnprocessableEntity, w.Code)
-			},
-		},
-		{
-			name: "success",
-			req:  httptest.NewRequest(http.MethodDelete, "/persistent-caches/1/task-1", nil),
-			mock: func(ms *mocks.MockServiceMockRecorder) {
-				ms.DestroyPersistentCache(gomock.Any(), gomock.Eq(mockPersistentCacheParams)).Return(nil).Times(1)
-			},
-			expect: func(t *testing.T, w *httptest.ResponseRecorder) {
-				assert := assert.New(t)
-				assert.Equal(http.StatusOK, w.Code)
-			},
-		},
-	}
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			ctl := gomock.NewController(t)
-			defer ctl.Finish()
-			svc := mocks.NewMockService(ctl)
-			w := httptest.NewRecorder()
-			h := New(svc)
-			mockRouter := mockPersistentCacheRouter(h)
-
-			tc.mock(svc.EXPECT())
-			mockRouter.ServeHTTP(w, tc.req)
-			tc.expect(t, w)
-		})
-	}
-}
-
-func TestHandlers_GetPersistentCache(t *testing.T) {
-	tests := []struct {
-		name   string
-		req    *http.Request
-		mock   func(ms *mocks.MockServiceMockRecorder)
-		expect func(t *testing.T, w *httptest.ResponseRecorder)
-	}{
-		{
-			name: "unprocessable entity",
-			req:  httptest.NewRequest(http.MethodGet, "/persistent-caches/invalid/task-1", nil),
-			mock: func(ms *mocks.MockServiceMockRecorder) {},
-			expect: func(t *testing.T, w *httptest.ResponseRecorder) {
-				assert := assert.New(t)
-				assert.Equal(http.StatusUnprocessableEntity, w.Code)
-			},
-		},
-		{
-			name: "success",
-			req:  httptest.NewRequest(http.MethodGet, "/persistent-caches/1/task-1", nil),
-			mock: func(ms *mocks.MockServiceMockRecorder) {
-				ms.GetPersistentCache(gomock.Any(), gomock.Eq(mockPersistentCacheParams)).Return(mockGetPersistentCacheResponse, nil).Times(1)
-			},
-			expect: func(t *testing.T, w *httptest.ResponseRecorder) {
-				assert := assert.New(t)
-				assert.Equal(http.StatusOK, w.Code)
-				assert.Contains(w.Body.String(), `"task_id":"task-1"`)
-				assert.Contains(w.Body.String(), `"state":"SUCCESS"`)
-			},
-		},
-	}
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			ctl := gomock.NewController(t)
-			defer ctl.Finish()
-			svc := mocks.NewMockService(ctl)
-			w := httptest.NewRecorder()
-			h := New(svc)
-			mockRouter := mockPersistentCacheRouter(h)
-
-			tc.mock(svc.EXPECT())
-			mockRouter.ServeHTTP(w, tc.req)
-			tc.expect(t, w)
-		})
-	}
-}
-
-func TestHandlers_GetPersistentCaches(t *testing.T) {
-	tests := []struct {
-		name   string
-		req    *http.Request
-		mock   func(ms *mocks.MockServiceMockRecorder)
-		expect func(t *testing.T, w *httptest.ResponseRecorder)
-	}{
-		{
-			name: "success",
-			req:  httptest.NewRequest(http.MethodGet, "/persistent-caches?page=1&per_page=10", nil),
-			mock: func(ms *mocks.MockServiceMockRecorder) {
-				ms.GetPersistentCaches(gomock.Any(), gomock.Any()).Return(mockGetPersistentCachesResponse, int64(1), nil).Times(1)
-			},
-			expect: func(t *testing.T, w *httptest.ResponseRecorder) {
-				assert := assert.New(t)
-				assert.Equal(http.StatusOK, w.Code)
-				assert.Contains(w.Body.String(), `"task_id":"task-1"`)
-				assert.Contains(w.Body.String(), `"state":"SUCCESS"`)
-				assert.Contains(w.Header().Get("Link"), "page=1")
-			},
-		},
-	}
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			ctl := gomock.NewController(t)
-			defer ctl.Finish()
-			svc := mocks.NewMockService(ctl)
-			w := httptest.NewRecorder()
-			h := New(svc)
-			mockRouter := mockPersistentCacheRouter(h)
-
-			tc.mock(svc.EXPECT())
-			mockRouter.ServeHTTP(w, tc.req)
-			tc.expect(t, w)
-		})
-	}
-}