Skip to content

Commit c34c428

Browse files
committed
initial implementation
1 parent 576fecf commit c34c428

File tree

13 files changed

+1241
-12
lines changed

13 files changed

+1241
-12
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
"""Backend registry for managing multiple backend configurations.
2+
3+
This module provides a registry for managing multiple scheduler backends.
4+
Each backend is a complete execution environment with its own scheduler,
5+
execution manager, and optionally database manager.
6+
"""
7+
8+
import logging
9+
from dataclasses import dataclass
10+
from typing import Any, Dict, List, Optional, Type
11+
12+
from traitlets.config import LoggingConfigurable
13+
14+
from jupyter_scheduler.backends import BackendConfig, DescribeBackend
15+
from jupyter_scheduler.environments import EnvironmentManager
16+
from jupyter_scheduler.orm import create_tables
17+
from jupyter_scheduler.scheduler import BaseScheduler
18+
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
def import_class(class_path: str) -> Type:
24+
"""Import a class from a fully qualified class path.
25+
26+
Parameters
27+
----------
28+
class_path : str
29+
Fully qualified class path (e.g., "jupyter_scheduler.scheduler.Scheduler")
30+
31+
Returns
32+
-------
33+
Type
34+
The imported class
35+
"""
36+
module_path, class_name = class_path.rsplit(".", 1)
37+
module = __import__(module_path, fromlist=[class_name])
38+
return getattr(module, class_name)
39+
40+
41+
@dataclass
42+
class BackendInstance:
43+
"""A running instance of a backend with initialized scheduler.
44+
45+
Attributes
46+
----------
47+
config : BackendConfig
48+
The configuration used to create this backend
49+
scheduler : BaseScheduler
50+
The initialized scheduler instance for this backend
51+
"""
52+
53+
config: BackendConfig
54+
scheduler: BaseScheduler
55+
56+
57+
class BackendRegistry:
58+
"""Registry managing multiple backend configurations.
59+
60+
This class is responsible for:
61+
- Storing and managing multiple backend configurations
62+
- Creating and initializing backend instances (schedulers)
63+
- Routing requests to the appropriate backend based on ID or file extension
64+
65+
Parameters
66+
----------
67+
configs : List[BackendConfig]
68+
List of backend configurations to register
69+
default_backend : str
70+
The ID of the default backend to use when none is specified
71+
"""
72+
73+
def __init__(self, configs: List[BackendConfig], default_backend: str):
74+
self._configs = configs
75+
self._backends: Dict[str, BackendInstance] = {}
76+
self._default = default_backend
77+
self._extension_map: Dict[str, List[str]] = {}
78+
79+
def initialize(
80+
self,
81+
root_dir: str,
82+
environments_manager: EnvironmentManager,
83+
db_url: str,
84+
config: Optional[Any] = None,
85+
):
86+
"""Instantiate all backends from configs.
87+
88+
Parameters
89+
----------
90+
root_dir : str
91+
The Jupyter server root directory
92+
environments_manager : EnvironmentManager
93+
The environment manager instance to use
94+
db_url : str
95+
Default database URL (used if backend doesn't specify its own)
96+
config : Any, optional
97+
Traitlets config object
98+
"""
99+
for cfg in self._configs:
100+
try:
101+
instance = self._create_backend(
102+
cfg, root_dir, environments_manager, db_url, config
103+
)
104+
self._backends[cfg.id] = instance
105+
106+
# Build extension map for auto-selection
107+
for ext in cfg.file_extensions:
108+
ext_lower = ext.lower().lstrip(".")
109+
if ext_lower not in self._extension_map:
110+
self._extension_map[ext_lower] = []
111+
self._extension_map[ext_lower].append(cfg.id)
112+
113+
logger.info(f"Initialized backend: {cfg.id} ({cfg.name})")
114+
except Exception as e:
115+
logger.error(f"Failed to initialize backend {cfg.id}: {e}")
116+
raise
117+
118+
def _create_backend(
119+
self,
120+
cfg: BackendConfig,
121+
root_dir: str,
122+
environments_manager: EnvironmentManager,
123+
global_db_url: str,
124+
config: Optional[Any] = None,
125+
) -> BackendInstance:
126+
"""Create a backend instance from configuration.
127+
128+
Parameters
129+
----------
130+
cfg : BackendConfig
131+
The backend configuration
132+
root_dir : str
133+
The Jupyter server root directory
134+
environments_manager : EnvironmentManager
135+
The environment manager instance
136+
global_db_url : str
137+
Default database URL (used if backend doesn't specify its own)
138+
config : Any, optional
139+
Traitlets config object
140+
141+
Returns
142+
-------
143+
BackendInstance
144+
The initialized backend instance
145+
"""
146+
scheduler_class = import_class(cfg.scheduler_class)
147+
148+
# Use backend-specific db_url if provided, otherwise use global
149+
backend_db_url = cfg.db_url or global_db_url
150+
151+
# Create database tables if using SQLAlchemy-based scheduler
152+
# Skip if db_url suggests non-SQLAlchemy backend (e.g., k8s://)
153+
if backend_db_url and not backend_db_url.startswith(("k8s://", "kubernetes://")):
154+
create_tables(backend_db_url)
155+
156+
# Instantiate the scheduler
157+
scheduler = scheduler_class(
158+
root_dir=root_dir,
159+
environments_manager=environments_manager,
160+
db_url=backend_db_url,
161+
config=config,
162+
)
163+
164+
# Override execution_manager_class if specified in config
165+
if cfg.execution_manager_class:
166+
scheduler.execution_manager_class = import_class(cfg.execution_manager_class)
167+
168+
return BackendInstance(config=cfg, scheduler=scheduler)
169+
170+
def get_backend(self, backend_id: str) -> Optional[BackendInstance]:
171+
"""Get a backend by its ID.
172+
173+
Parameters
174+
----------
175+
backend_id : str
176+
The backend ID to look up
177+
178+
Returns
179+
-------
180+
BackendInstance or None
181+
The backend instance if found, None otherwise
182+
"""
183+
return self._backends.get(backend_id)
184+
185+
def get_default(self) -> BackendInstance:
186+
"""Get the default backend.
187+
188+
Returns
189+
-------
190+
BackendInstance
191+
The default backend instance
192+
193+
Raises
194+
------
195+
KeyError
196+
If the default backend is not found
197+
"""
198+
if self._default not in self._backends:
199+
raise KeyError(f"Default backend '{self._default}' not found in registry")
200+
return self._backends[self._default]
201+
202+
def get_for_file(self, input_uri: str) -> BackendInstance:
203+
"""Auto-select backend based on file extension.
204+
205+
If multiple backends support the file type, returns the one with
206+
highest priority. If no backend matches the extension, returns
207+
the default backend.
208+
209+
Parameters
210+
----------
211+
input_uri : str
212+
The input file URI/path
213+
214+
Returns
215+
-------
216+
BackendInstance
217+
The selected backend instance
218+
"""
219+
# Extract file extension
220+
ext = ""
221+
if "." in input_uri:
222+
ext = input_uri.rsplit(".", 1)[-1].lower()
223+
224+
candidates = self._extension_map.get(ext, [])
225+
if candidates:
226+
# Return highest priority backend
227+
candidate_instances = [self._backends[bid] for bid in candidates]
228+
return max(candidate_instances, key=lambda b: b.config.priority)
229+
230+
return self.get_default()
231+
232+
def list_backends(self) -> List[DescribeBackend]:
233+
"""Return list of backends for API/UI.
234+
235+
Returns
236+
-------
237+
List[DescribeBackend]
238+
List of backend descriptions for frontend consumption
239+
"""
240+
return [
241+
DescribeBackend(
242+
id=b.config.id,
243+
name=b.config.name,
244+
description=b.config.description,
245+
file_extensions=b.config.file_extensions,
246+
is_default=b.config.is_default,
247+
)
248+
for b in self._backends.values()
249+
]
250+
251+
def list_backend_instances(self) -> List[BackendInstance]:
252+
"""Return list of all backend instances.
253+
254+
Returns
255+
-------
256+
List[BackendInstance]
257+
List of all backend instances
258+
"""
259+
return list(self._backends.values())
260+
261+
def __len__(self) -> int:
262+
"""Return the number of registered backends."""
263+
return len(self._backends)
264+
265+
def __contains__(self, backend_id: str) -> bool:
266+
"""Check if a backend ID is registered."""
267+
return backend_id in self._backends

jupyter_scheduler/backends.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Backend configuration models for multi-backend support.
2+
3+
A backend bundles a tightly-coupled set of classes:
4+
- scheduler_class
5+
- execution_manager_class
6+
- database_manager_class (optional)
7+
8+
This allows multiple execution backends to be configured and selected at job creation time.
9+
"""
10+
11+
from dataclasses import dataclass, field
12+
from typing import Dict, List, Optional, Any
13+
14+
from jupyter_scheduler.pydantic_v1 import BaseModel
15+
16+
17+
@dataclass
18+
class BackendConfig:
19+
"""Configuration for a backend instance.
20+
21+
Attributes
22+
----------
23+
id : str
24+
Unique identifier for this backend (e.g., "local", "kubernetes", "sagemaker")
25+
name : str
26+
Human-readable display name (e.g., "Local Execution")
27+
description : str
28+
Help text for UI
29+
scheduler_class : str
30+
Fully qualified class name for the scheduler
31+
execution_manager_class : str
32+
Fully qualified class name for the execution manager
33+
database_manager_class : str, optional
34+
Fully qualified class name for the database manager. If None, uses default SQLAlchemy.
35+
db_url : str, optional
36+
Backend-specific database URL. If None, uses the global db_url.
37+
file_extensions : list of str
38+
List of file extensions this backend supports (e.g., ["ipynb", "py"]).
39+
Empty list means all extensions are supported.
40+
is_default : bool
41+
Whether this is the default backend when no backend is specified.
42+
priority : int
43+
Priority for auto-selection when multiple backends support the same file type.
44+
Higher priority wins.
45+
metadata : dict, optional
46+
Additional backend-specific metadata (e.g., {"requires_s3": "true"})
47+
"""
48+
49+
id: str
50+
name: str
51+
description: str
52+
scheduler_class: str
53+
execution_manager_class: str
54+
database_manager_class: Optional[str] = None
55+
db_url: Optional[str] = None
56+
file_extensions: List[str] = field(default_factory=list)
57+
is_default: bool = False
58+
priority: int = 0
59+
metadata: Optional[Dict[str, Any]] = None
60+
61+
62+
class DescribeBackend(BaseModel):
63+
"""Backend information exposed to frontend via API.
64+
65+
This is the response model for GET /scheduler/backends.
66+
"""
67+
68+
id: str
69+
name: str
70+
description: str
71+
file_extensions: List[str]
72+
is_default: bool
73+
74+
class Config:
75+
"""Pydantic config."""
76+
77+
orm_mode = True

0 commit comments

Comments
 (0)