Skip to content

Commit 6ee6ba9

Browse files
feat: add debug.list_tasks to expose GET /v1/tasks (#2059)
Adds a list_tasks() method to the sync and async debug namespace that calls the server's distributed-tasks endpoint, so users can inspect long-running background operations (e.g. reindexing) tracked across cluster nodes without shelling out to curl. Includes DistributedTask/DistributedTaskUnit pydantic models mirroring the server's DistributedTasks OpenAPI schema, mock tests covering the populated and empty-response cases, and integration test stubs following the existing debug test conventions.
1 parent 17a9887 commit 6ee6ba9

7 files changed

Lines changed: 132 additions & 8 deletions

File tree

integration/test_client_debug.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,20 @@ def test_get_object_multi_node(
6363
debug_obj = client.debug.get_object_over_rest(collection.name, uuid, node_name=node_name)
6464
assert debug_obj is not None
6565
assert str(debug_obj.uuid) == str(uuid)
66+
67+
68+
def test_list_tasks(client_factory: ClientFactory) -> None:
69+
client = client_factory()
70+
71+
tasks = client.debug.list_tasks()
72+
73+
assert isinstance(tasks, dict)
74+
75+
76+
@pytest.mark.asyncio
77+
async def test_list_tasks_async(async_client_factory: AsyncClientFactory) -> None:
78+
client = await async_client_factory()
79+
80+
tasks = await client.debug.list_tasks()
81+
82+
assert isinstance(tasks, dict)

mock_tests/test_debug.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import weaviate
2+
from weaviate.classes.debug import DistributedTask
3+
from pytest_httpserver import HTTPServer
4+
5+
6+
def test_list_tasks(weaviate_client: weaviate.WeaviateClient, weaviate_mock: HTTPServer) -> None:
7+
weaviate_mock.expect_request("/v1/tasks").respond_with_json(
8+
{
9+
"reindex": [
10+
{
11+
"id": "task-1",
12+
"version": 1,
13+
"status": "running",
14+
"startedAt": "2026-01-01T00:00:00Z",
15+
"finishedNodes": ["node1"],
16+
"payload": {"collection": "MyCollection"},
17+
},
18+
{
19+
"id": "task-2",
20+
"version": 1,
21+
"status": "finished",
22+
"startedAt": "2026-01-01T00:00:00Z",
23+
"finishedAt": "2026-01-01T00:05:00Z",
24+
"finishedNodes": ["node1", "node2"],
25+
},
26+
]
27+
}
28+
)
29+
30+
tasks = weaviate_client.debug.list_tasks()
31+
32+
assert list(tasks.keys()) == ["reindex"]
33+
assert len(tasks["reindex"]) == 2
34+
35+
first = tasks["reindex"][0]
36+
assert isinstance(first, DistributedTask)
37+
assert first.id == "task-1"
38+
assert first.status == "running"
39+
assert first.finished_at is None
40+
assert first.finished_nodes == ["node1"]
41+
assert first.payload == {"collection": "MyCollection"}
42+
43+
second = tasks["reindex"][1]
44+
assert second.id == "task-2"
45+
assert second.status == "finished"
46+
assert second.finished_nodes == ["node1", "node2"]
47+
48+
49+
def test_list_tasks_empty(
50+
weaviate_client: weaviate.WeaviateClient, weaviate_mock: HTTPServer
51+
) -> None:
52+
weaviate_mock.expect_request("/v1/tasks").respond_with_json({})
53+
54+
tasks = weaviate_client.debug.list_tasks()
55+
56+
assert tasks == {}

weaviate/classes/debug.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from weaviate.debug.types import DebugRESTObject
1+
from weaviate.debug.types import DebugRESTObject, DistributedTask, DistributedTaskUnit
22

33
__all__ = [
44
"DebugRESTObject",
5+
"DistributedTask",
6+
"DistributedTaskUnit",
57
]

weaviate/debug/async_.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Optional
1+
from typing import Dict, List, Optional
22

33
from weaviate.classes.config import ConsistencyLevel
44
from weaviate.connect.v4 import ConnectionAsync
5-
from weaviate.debug.types import DebugRESTObject
5+
from weaviate.debug.types import DebugRESTObject, DistributedTask
66
from weaviate.types import UUID
77

88
from .executor import _DebugExecutor
@@ -17,3 +17,4 @@ class _DebugAsync(_DebugExecutor[ConnectionAsync]):
1717
node_name: Optional[str] = None,
1818
tenant: Optional[str] = None,
1919
) -> Optional[DebugRESTObject]: ...
20+
async def list_tasks(self) -> Dict[str, List[DistributedTask]]: ...

weaviate/debug/executor.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from typing import Dict, Generic, Optional
1+
from typing import Dict, Generic, List, Optional
22

33
from httpx import Response
44

55
from weaviate.classes.config import ConsistencyLevel
66
from weaviate.connect import executor
77
from weaviate.connect.v4 import ConnectionType, _ExpectedStatusCodes
8-
from weaviate.debug.types import DebugRESTObject
8+
from weaviate.debug.types import DebugRESTObject, DistributedTask
99
from weaviate.types import UUID
1010

1111

@@ -50,3 +50,24 @@ def resp(response: Response) -> Optional[DebugRESTObject]:
5050
error_msg="Object was not retrieved",
5151
status_codes=_ExpectedStatusCodes(ok_in=[200, 404], error="get object"),
5252
)
53+
54+
def list_tasks(self) -> executor.Result[Dict[str, List[DistributedTask]]]:
55+
"""Use the REST API endpoint /tasks to list all distributed tasks currently active or available in the cluster.
56+
57+
Distributed tasks are long-running background operations, such as reindexing, that are tracked
58+
across the cluster's nodes. The returned mapping is keyed by task namespace.
59+
"""
60+
61+
def resp(response: Response) -> Dict[str, List[DistributedTask]]:
62+
return {
63+
namespace: [DistributedTask(**task) for task in tasks]
64+
for namespace, tasks in response.json().items()
65+
}
66+
67+
return executor.execute(
68+
response_callback=resp,
69+
method=self._connection.get,
70+
path="/tasks",
71+
error_msg="Distributed tasks were not retrieved",
72+
status_codes=_ExpectedStatusCodes(ok_in=200, error="list distributed tasks"),
73+
)

weaviate/debug/sync.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Optional
1+
from typing import Dict, List, Optional
22

33
from weaviate.classes.config import ConsistencyLevel
44
from weaviate.connect.v4 import ConnectionSync
5-
from weaviate.debug.types import DebugRESTObject
5+
from weaviate.debug.types import DebugRESTObject, DistributedTask
66
from weaviate.types import UUID
77

88
from .executor import _DebugExecutor
@@ -17,3 +17,4 @@ class _Debug(_DebugExecutor[ConnectionSync]):
1717
node_name: Optional[str] = None,
1818
tenant: Optional[str] = None,
1919
) -> Optional[DebugRESTObject]: ...
20+
def list_tasks(self) -> Dict[str, List[DistributedTask]]: ...

weaviate/debug/types.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import Any, Dict, Optional
2+
from typing import Any, Dict, List, Optional
33

44
from pydantic import BaseModel, Field
55

@@ -15,3 +15,29 @@ class DebugRESTObject(BaseModel):
1515
uuid: uuid_package.UUID = Field(..., alias="id")
1616
vector: Optional[list[float]] = Field(None)
1717
vectors: Optional[Dict[str, list[float]]] = Field(None)
18+
19+
20+
class DistributedTaskUnit(BaseModel):
21+
"""A unit of a distributed task."""
22+
23+
id: str = Field(...) # noqa: A003
24+
node_id: str = Field(..., alias="nodeId")
25+
status: str = Field(...)
26+
progress: Optional[float] = Field(None)
27+
error: Optional[str] = Field(None)
28+
updated_at: Optional[datetime] = Field(None, alias="updatedAt")
29+
finished_at: Optional[datetime] = Field(None, alias="finishedAt")
30+
31+
32+
class DistributedTask(BaseModel):
33+
"""Metadata about a distributed task running in the cluster."""
34+
35+
id: str = Field(...) # noqa: A003
36+
version: int = Field(...)
37+
status: str = Field(...)
38+
started_at: datetime = Field(..., alias="startedAt")
39+
finished_at: Optional[datetime] = Field(None, alias="finishedAt")
40+
finished_nodes: List[str] = Field(default_factory=list, alias="finishedNodes")
41+
error: Optional[str] = Field(None)
42+
payload: Optional[Dict[str, Any]] = Field(None)
43+
units: Optional[List[DistributedTaskUnit]] = Field(None)

0 commit comments

Comments
 (0)