Skip to content

Commit 153579b

Browse files
HyeockJinKimclaude
andauthored
feat(BA-939): Add dependency verification for storage proxy (#6760)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7b2041 commit 153579b

22 files changed

Lines changed: 785 additions & 3 deletions

File tree

changes/6760.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add dependency verification for storage proxy

src/ai/backend/common/message_queue/redis_queue/subscriber.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async def _read_broadcast_messages_loop(self) -> None:
116116
"""
117117
Background task to read broadcast messages from subscribed channels.
118118
"""
119-
log.info("Starting read broadcast messages loop for channels {}", self._channels)
119+
log.debug("Starting read broadcast messages loop for channels {}", self._channels)
120120

121121
while not self._closed:
122122
try:

src/ai/backend/storage/cli/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,8 @@ def main(
5555
@main.group(cls=LazyGroup, import_name="ai.backend.storage.cli.config:cli")
5656
def config() -> None:
5757
"""Command set for configuration management."""
58+
59+
60+
@main.group(cls=LazyGroup, import_name="ai.backend.storage.cli.dependencies:cli")
61+
def dependencies() -> None:
62+
"""Command set for dependency verification."""
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import sys
5+
from pathlib import Path
6+
7+
import click
8+
9+
from ai.backend.common.dependencies.stacks.visualizing import VisualizingDependencyStack
10+
11+
from ..dependencies.composer import StorageDependencyComposer
12+
from .context import CLIContext
13+
14+
15+
@click.group()
16+
def cli() -> None:
17+
"""Dependency verification commands for storage proxy setup."""
18+
pass
19+
20+
21+
@cli.command(name="verify")
22+
@click.option(
23+
"--no-timestamps",
24+
is_flag=True,
25+
help="Hide timestamps in output",
26+
)
27+
@click.option(
28+
"--log-level",
29+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False),
30+
default="WARNING",
31+
help="Set the logging level (default: WARNING)",
32+
)
33+
@click.pass_obj
34+
def verify(
35+
cli_ctx: CLIContext,
36+
no_timestamps: bool,
37+
log_level: str,
38+
) -> None:
39+
"""
40+
Verify all storage proxy dependencies can be initialized successfully.
41+
42+
This command attempts to initialize all storage proxy dependencies (infrastructure,
43+
components) and reports any failures during the setup process.
44+
45+
The output shows dependency initialization with status indicators:
46+
- ▶ Starting (for composers)
47+
- ✓ Completed (for dependencies)
48+
- ✗ Failed (for dependencies)
49+
50+
Examples:
51+
52+
# Verify all dependencies with default WARNING log level
53+
$ backend.ai storage dependencies verify
54+
55+
# Hide timestamps in output
56+
$ backend.ai storage dependencies verify --no-timestamps
57+
58+
# Set log level to DEBUG to see detailed initialization logs
59+
$ backend.ai storage dependencies verify --log-level DEBUG
60+
"""
61+
import logging as std_logging
62+
63+
# Set logging level BEFORE any dependency initialization
64+
std_logging.basicConfig(
65+
level=getattr(std_logging, log_level.upper()),
66+
format="%(levelname)s:%(name)s:%(message)s",
67+
force=True,
68+
)
69+
70+
config_path = cli_ctx.config_path
71+
72+
async def _verify_dependencies() -> bool:
73+
"""Verify all storage proxy dependencies can be initialized."""
74+
print("\n" + "=" * 60)
75+
print("Storage Proxy Dependency Verification")
76+
print("=" * 60)
77+
print(f"Config: {config_path or 'default locations'}")
78+
print(f"Log Level: {log_level.upper()}")
79+
print("=" * 60 + "\n")
80+
81+
# Create visualizing stack
82+
stack = VisualizingDependencyStack(
83+
output=sys.stdout,
84+
show_timestamps=not no_timestamps,
85+
)
86+
87+
try:
88+
async with stack:
89+
from ..dependencies.composer import DependencyInput
90+
91+
dependency_input = DependencyInput(
92+
config_path=config_path or Path("storage-proxy.toml"),
93+
)
94+
composer = StorageDependencyComposer()
95+
96+
# Initialize all dependencies through the composer
97+
await stack.enter_composer(composer, dependency_input)
98+
99+
except Exception as e:
100+
print(f"\n❌ Dependency initialization failed: {e}\n")
101+
102+
# Print summary
103+
print()
104+
stack.print_summary()
105+
106+
return not stack.has_failures()
107+
108+
# Run the verification
109+
success = asyncio.run(_verify_dependencies())
110+
111+
if success:
112+
print("All dependencies verified successfully!\n")
113+
sys.exit(0)
114+
else:
115+
print("Some dependencies failed. Check the output above for details.\n")
116+
sys.exit(1)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from __future__ import annotations
2+
3+
from .composer import DependencyResources, StorageDependencyComposer
4+
5+
__all__ = [
6+
"DependencyResources",
7+
"StorageDependencyComposer",
8+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
from .composer import BootstrapComposer, BootstrapInput, BootstrapResources
4+
from .config import ConfigProvider, ConfigProviderInput
5+
6+
__all__ = [
7+
"BootstrapComposer",
8+
"BootstrapInput",
9+
"BootstrapResources",
10+
"ConfigProvider",
11+
"ConfigProviderInput",
12+
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncIterator
4+
from contextlib import asynccontextmanager
5+
from dataclasses import dataclass
6+
from pathlib import Path
7+
8+
from ai.backend.common.dependencies import DependencyComposer, DependencyStack
9+
from ai.backend.storage.config.unified import StorageProxyUnifiedConfig
10+
11+
from .config import ConfigProvider, ConfigProviderInput
12+
13+
14+
@dataclass
15+
class BootstrapInput:
16+
"""Input required for bootstrap stage."""
17+
18+
config_path: Path
19+
20+
21+
@dataclass
22+
class BootstrapResources:
23+
"""Container for bootstrap stage resources."""
24+
25+
config: StorageProxyUnifiedConfig
26+
27+
28+
class BootstrapComposer(DependencyComposer[BootstrapInput, BootstrapResources]):
29+
"""Composes bootstrap dependencies."""
30+
31+
@property
32+
def stage_name(self) -> str:
33+
return "bootstrap"
34+
35+
@asynccontextmanager
36+
async def compose(
37+
self,
38+
stack: DependencyStack,
39+
setup_input: BootstrapInput,
40+
) -> AsyncIterator[BootstrapResources]:
41+
"""Compose bootstrap dependencies."""
42+
# Load config
43+
config_provider = ConfigProvider()
44+
config = await stack.enter_dependency(
45+
config_provider,
46+
ConfigProviderInput(config_path=setup_input.config_path),
47+
)
48+
49+
yield BootstrapResources(config=config)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncIterator
4+
from contextlib import asynccontextmanager
5+
from dataclasses import dataclass
6+
from pathlib import Path
7+
8+
from ai.backend.common.dependencies import DependencyProvider
9+
from ai.backend.storage.config.loaders import load_local_config
10+
from ai.backend.storage.config.unified import StorageProxyUnifiedConfig
11+
12+
13+
@dataclass
14+
class ConfigProviderInput:
15+
"""Input for config provider."""
16+
17+
config_path: Path
18+
19+
20+
class ConfigProvider(DependencyProvider[ConfigProviderInput, StorageProxyUnifiedConfig]):
21+
"""Provider for storage proxy configuration."""
22+
23+
@property
24+
def stage_name(self) -> str:
25+
return "config"
26+
27+
@asynccontextmanager
28+
async def provide(
29+
self, setup_input: ConfigProviderInput
30+
) -> AsyncIterator[StorageProxyUnifiedConfig]:
31+
"""Load and provide storage proxy configuration."""
32+
config = load_local_config(setup_input.config_path)
33+
yield config
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncIterator
4+
from contextlib import asynccontextmanager
5+
from dataclasses import dataclass
6+
from pathlib import Path
7+
8+
from ai.backend.common.dependencies import DependencyComposer, DependencyStack
9+
10+
from .bootstrap import BootstrapComposer, BootstrapInput, BootstrapResources
11+
from .infrastructure.composer import (
12+
InfrastructureComposer,
13+
InfrastructureComposerInput,
14+
InfrastructureResources,
15+
)
16+
from .storage.composer import StorageComposer, StorageComposerInput, StorageResources
17+
18+
19+
@dataclass
20+
class DependencyInput:
21+
"""Input required for complete dependency setup."""
22+
23+
config_path: Path
24+
25+
26+
@dataclass
27+
class DependencyResources:
28+
"""All dependency resources for storage proxy."""
29+
30+
bootstrap: BootstrapResources
31+
infrastructure: InfrastructureResources
32+
storage: StorageResources
33+
34+
35+
class StorageDependencyComposer(DependencyComposer[DependencyInput, DependencyResources]):
36+
"""Main composer for all storage proxy dependencies."""
37+
38+
@property
39+
def stage_name(self) -> str:
40+
return "storage-proxy"
41+
42+
@asynccontextmanager
43+
async def compose(
44+
self,
45+
stack: DependencyStack,
46+
setup_input: DependencyInput,
47+
) -> AsyncIterator[DependencyResources]:
48+
"""Compose all storage proxy dependencies."""
49+
# Stage 1: Bootstrap (config loading)
50+
bootstrap = await stack.enter_composer(
51+
BootstrapComposer(),
52+
BootstrapInput(config_path=setup_input.config_path),
53+
)
54+
55+
# Stage 2: Infrastructure (etcd, redis)
56+
infrastructure = await stack.enter_composer(
57+
InfrastructureComposer(),
58+
InfrastructureComposerInput(local_config=bootstrap.config),
59+
)
60+
61+
# Stage 3: Storage (storage-pool)
62+
storage = await stack.enter_composer(
63+
StorageComposer(),
64+
StorageComposerInput(local_config=bootstrap.config),
65+
)
66+
67+
yield DependencyResources(
68+
bootstrap=bootstrap,
69+
infrastructure=infrastructure,
70+
storage=storage,
71+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from __future__ import annotations
2+
3+
from .composer import InfrastructureComposer, InfrastructureComposerInput, InfrastructureResources
4+
from .etcd import EtcdProvider
5+
from .redis import RedisProvider, StorageProxyValkeyClients
6+
7+
__all__ = [
8+
"EtcdProvider",
9+
"InfrastructureComposer",
10+
"InfrastructureComposerInput",
11+
"InfrastructureResources",
12+
"RedisProvider",
13+
"StorageProxyValkeyClients",
14+
]

0 commit comments

Comments
 (0)