Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/source/serve/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ Content-Type: application/json
schema.ProxyStatus
schema.TargetGroup
schema.Target
schema.DeploymentNode
schema.DeploymentTopology
```

## Observability
Expand Down
55 changes: 55 additions & 0 deletions python/ray/serve/_private/application_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
APIType,
ApplicationStatus,
DeploymentDetails,
DeploymentNode,
DeploymentTopology,
LoggingConfig,
ServeApplicationSchema,
)
Expand Down Expand Up @@ -935,6 +937,45 @@ def _reconcile_target_deployments(self) -> None:

return target_state_changed

def get_deployment_topology(self) -> Optional[DeploymentTopology]:
"""Get the deployment topology for this application.

Returns:
The deployment topology, or None if not yet built.
"""
if not self.target_deployments:
return None

nodes = {}

# Using target deployments because we wish to build best effort topology based on current state.
for deployment_name in self.target_deployments:
deployment_id = DeploymentID(name=deployment_name, app_name=self._name)

# Get outbound deployment names from deployment state
outbound_deployment = (
self._deployment_state_manager.get_deployment_outbound_deployments(
deployment_id
)
) or []

# Create node for this deployment
node = DeploymentNode(
name=deployment_name,
outbound_deployments=[
{"name": dep.name, "app_name": dep.app_name}
for dep in outbound_deployment
],
is_ingress=(deployment_name == self._ingress_deployment_name),
)
nodes[deployment_name] = node

return DeploymentTopology(
app_name=self._name,
ingress_deployment=self._ingress_deployment_name,
nodes=nodes,
)

def update(self) -> Tuple[bool, bool]:
"""Attempts to reconcile this application to match its target state.

Expand Down Expand Up @@ -1280,6 +1321,20 @@ def list_deployment_details(self, name: str) -> Dict[str, DeploymentDetails]:
return {}
return self._application_states[name].list_deployment_details()

def get_deployment_topology(self, app_name: str) -> Optional[DeploymentTopology]:
"""Get the deployment topology for an application.

Args:
app_name: Name of the application.

Returns:
The deployment topology for the application, or None if the application
doesn't exist or the topology hasn't been built yet.
"""
if app_name not in self._application_states:
return None
return self._application_states[app_name].get_deployment_topology()

def update(self):
"""Update each application state."""
apps_to_be_deleted = []
Expand Down
3 changes: 3 additions & 0 deletions python/ray/serve/_private/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,9 @@ def get_serve_instance_details(self, source: Optional[APIType] = None) -> Dict:
deployments=self.application_state_manager.list_deployment_details(
app_name
),
deployment_topology=self.application_state_manager.get_deployment_topology(
app_name
),
)

# NOTE(zcin): We use exclude_unset here because we explicitly and intentionally
Expand Down
42 changes: 42 additions & 0 deletions python/ray/serve/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,43 @@ def get_valid_user_values(cls):
return [cls.IMPERATIVE.value, cls.DECLARATIVE.value]


@PublicAPI(stability="alpha")
class DeploymentNode(BaseModel):
"""Represents a node in the deployment DAG.

Each node represents a deployment and tracks which other deployments it calls.
"""

name: str = Field(description="The name of the deployment.")
# using DeploymentID instead of deployment name because outbound dependencies can be in different apps
outbound_deployments: List[dict] = Field(
default_factory=list,
description="The deployment IDs that this deployment calls (outbound dependencies).",
)
is_ingress: bool = Field(
default=False, description="Whether this is the ingress deployment."
)


@PublicAPI(stability="alpha")
class DeploymentTopology(BaseModel):
"""Represents the dependency graph of deployments in an application.

The topology shows which deployments call which other deployments,
with the ingress deployment as the entry point.
"""

app_name: str = Field(
description="The name of the application this topology belongs to."
)
nodes: Dict[str, DeploymentNode] = Field(
description="The adjacency list of deployment nodes."
)
ingress_deployment: Optional[str] = Field(
default=None, description="The name of the ingress deployment (entry point)."
)


@PublicAPI(stability="stable")
class ApplicationDetails(BaseModel, extra=Extra.forbid, frozen=True):
"""Detailed info about a Serve application."""
Expand Down Expand Up @@ -1172,6 +1209,11 @@ class ApplicationDetails(BaseModel, extra=Extra.forbid, frozen=True):
"route_prefix", allow_reuse=True
)(_route_prefix_format)

deployment_topology: Optional[DeploymentTopology] = Field(
default=None,
description="The deployment topology showing how deployments in this application call each other.",
)


@PublicAPI(stability="stable")
class ProxyDetails(ServeActorDetails, frozen=True):
Expand Down
1 change: 1 addition & 0 deletions python/ray/serve/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ py_test_module_list(
"test_controller_recovery.py",
"test_deploy_2.py",
"test_deployment_scheduler.py",
"test_deployment_topology.py",
"test_failure.py",
"test_handle_1.py",
"test_handle_2.py",
Expand Down
11 changes: 11 additions & 0 deletions python/ray/serve/tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,17 @@ def autoscaling_app():
],
}
},
"deployment_topology": {
"app_name": "default",
"nodes": {
"autoscaling_app": {
"name": "autoscaling_app",
"outbound_deployments": [],
"is_ingress": True,
}
},
"ingress_deployment": "autoscaling_app",
},
}
},
"target_capacity": None,
Expand Down
Loading