MADSci Manager Services
MADSci has 7 manager services, all built on AbstractManagerBase[SettingsT] with FastAPI via classy_fastapi.Routable. Each follows the pattern: Settings class -> Server class -> Client class.
Reference Files
Detailed material is in reference/. Read the relevant file when the task touches that area:
- Per-Manager Notes — domain-specific notes for each of the 7 managers (Event/Experiment/Resource/Data/Workcell/Location/Lab). See reference/managers.md.
- Database Handler Reference — the 4 handler ABCs and their real + in-memory implementations. See reference/db-handlers.md.
Key Files
| File | Purpose |
|------|---------|
| src/madsci_common/madsci/common/manager_base.py | AbstractManagerBase - common server lifecycle, health, CORS, OTEL |
| src/madsci_common/madsci/common/types/manager_types.py | ManagerSettings, ManagerHealth base types |
| src/madsci_common/madsci/common/types/base_types.py | MadsciBaseSettings, prefixed_alias_generator() |
| src/madsci_common/madsci/common/db_handlers/ | 4 handler ABCs + real + in-memory implementations |
| src/madsci_*/madsci/*/*_server.py | Each manager's server implementation |
| src/madsci_client/madsci/client/ | All client implementations |
| src/madsci_client/madsci/client/client_mixin.py | MadsciClientMixin for lazy client access |
The 7 Managers
| Manager | Port | Database | Handler Params | Key Domain |
|---------|------|----------|----------------|------------|
| Lab (Squid) | 8000 | None | N/A | Dashboard, service discovery, health aggregation |
| Event | 8001 | FerretDB | document_handler | Distributed event logging, retention, analytics |
| Experiment | 8002 | FerretDB | document_handler | Experiment lifecycle (start/pause/cancel/end) |
| Resource | 8003 | PostgreSQL | postgres_handler | Inventory, containers (Queue/Stack/Slot), templates |
| Data | 8004 | FerretDB + S3 | document_handler, object_storage_handler | DataPoints (value/file), dual storage |
| Workcell | 8005 | FerretDB + Valkey | document_handler, cache_handler | Workflow execution, scheduling, node coordination |
| Location | 8006 | FerretDB + Valkey | document_handler, cache_handler | Location management, transfer planning, representations |
Manager Implementation Pattern
1. Settings Class
from madsci.common.types.manager_types import ManagerSettings
from madsci.common.types.base_types import prefixed_alias_generator, prefixed_model_validator
class MyManagerSettings(ManagerSettings):
model_config = SettingsConfigDict(
env_prefix="MYMANAGER_",
alias_generator=prefixed_alias_generator("mymanager"),
populate_by_name=True,
)
_accept_prefixed_keys = prefixed_model_validator("mymanager")
server_url: AnyUrl = Field(default="http://localhost:8007/")
document_db_url: AnyUrl = Field(default="mongodb://localhost:27017/")
database_name: str = Field(default="madsci_mymanager")
Key points:
env_prefix: Environment variable prefix (e.g.,MYMANAGER_SERVER_URL)prefixed_alias_generator: Enables prefixed keys in YAML (e.g.,mymanager_server_url)prefixed_model_validator: Accepts prefixed keys during model initialization- Code uses unprefixed names (
server_url), YAML/export uses prefixed (mymanager_server_url) model_dump(by_alias=True)-> prefixed keys;model_dump()-> unprefixed
2. Server Class
from madsci.common.manager_base import AbstractManagerBase
class MyManager(AbstractManagerBase[MyManagerSettings]):
SETTINGS_CLASS = MyManagerSettings
def initialize(self, **kwargs):
"""Called during __init__ after base setup. Set up DB connections, etc."""
self._document_handler = kwargs.get("document_handler") or PyDocumentStorageHandler(
url=str(self.settings.document_db_url),
database_name=self.settings.database_name,
)
def get_health(self) -> MyManagerHealth:
"""Override to add DB connectivity checks."""
try:
connected = self._document_handler.ping()
return MyManagerHealth(healthy=connected, db_connected=connected)
except Exception as e:
return MyManagerHealth(healthy=False, description=str(e))
@get("/items")
def list_items(self) -> list[Item]:
"""Custom endpoint using classy_fastapi @get/@post decorators."""
...
@post("/items")
def create_item(self, item: Item) -> Item:
...
AbstractManagerBase provides:
GET /health-> callsget_health()GET /settings-> exports settings with secret redaction- CORS middleware (configurable)
- Rate limiting middleware
- OpenTelemetry instrumentation (if enabled)
- Ownership context middleware
- Registry identity resolution (stable IDs across restarts)
self.logger(EventClient) for structured loggingself.span(name, attributes)context manager for OTEL tracingcreate_server()andrun_server()for lifecycle management
3. Client Class
from madsci.common.utils import create_http_session
from pydantic import AnyUrl
class MyManagerClient:
def __init__(self, server_url: AnyUrl, config=None, event_client=None):
self.server_url = str(server_url)
self._session = None # Lazy HTTP session
def list_items(self, timeout=None) -> list[Item]:
response = self._get("/items", timeout=timeout)
return [Item(**item) for item in response.json()]
def create_item(self, item: Item, timeout=None) -> Item:
response = self._post("/items", json=item.model_dump(), timeout=timeout)
return Item(**response.json())
def close(self):
"""Release HTTP session resources."""
if self._session:
self._session.close()
All clients have:
- Lazy HTTP session initialization
close()method for cleanup- Optional
EventClientinjection for logging - Configurable retry behavior
Settings and Configuration
Prefixed Alias System
Each manager uses prefixed_alias_generator() for YAML-friendly configuration:
# settings.yaml (shared across managers)
event_server_url: http://localhost:8001/
event_database_name: madsci_events
resource_postgres_db_url: postgresql://localhost/resources
workcell_cache_url: redis://localhost:6379/
# In code, use unprefixed names:
self.settings.server_url # "http://localhost:8001/"
self.settings.database_name # "madsci_events"
Secret Marking and Redaction
# Type-based:
password: SecretStr
# Metadata-based:
api_key: str = Field(json_schema_extra={"secret": True})
# Safe export (redacts secrets):
settings.model_dump_safe()
# Settings endpoint uses:
get_settings_export(include_secrets=False) # Default
Environment Variable Precedence
CLI args > init kwargs > env vars > .env > file secrets > JSON > TOML > YAML > field defaults
Deprecated: Manager Definition Types
All *ManagerDefinition classes (e.g., ManagerDefinition, LabManagerDefinition, WorkcellManagerDefinition) are deprecated as of v0.7.0 and will be removed. They emit MadsciDeprecationWarning on instantiation.
- Use
ManagerSettingssubclasses for all configuration (e.g.,EventManagerSettings,WorkcellManagerSettings) - Use
WorkcellInfofor workcell runtime state (notWorkcellManagerDefinition) - Definition files (
*.manager.yaml) are replaced bysettings.yaml+ environment variables - Run
madsci migrateto convert legacy definition files
EventClient Dual Nature
EventClient serves as both a logging interface and an HTTP client:
# Logging (always works, even without Event Manager)
self.logger.info("Operation started", event_type=EventType.WORKFLOW_START, workflow_id=wf_id)
self.logger.warning("Threshold exceeded", value=42.5)
self.logger.error("Connection failed", error=str(e))
# HTTP client (requires Event Manager)
events = self.event_client.get_events(number=10)
event = self.event_client.get_event(event_id)
Structured logging best practice:
# Good: kwargs are queryable
self.logger.info("Step completed", step_index=3, duration_ms=150)
# Bad: f-string data is not queryable
self.logger.info(f"Step 3 completed in 150ms")
Health Check Pattern
def get_health(self) -> MyManagerHealth:
health = MyManagerHealth(healthy=True)
try:
health.db_connected = self._document_handler.ping()
health.total_items = self._get_count()
except Exception as e:
health.healthy = False
health.description = f"Database error: {e}"
return health
ManagerHealth uses model_config = ConfigDict(extra="allow") so subclasses can freely add fields.
Testing Managers
from madsci.common.db_handlers import (
InMemoryDocumentStorageHandler,
InMemoryCacheHandler,
SQLiteHandler,
)
def test_event_manager():
manager = EventManager(
settings=EventManagerSettings(enable_registry_resolution=False),
document_handler=InMemoryDocumentStorageHandler(),
)
app = manager.create_server()
client = TestClient(app)
# Test endpoints
response = client.get("/health")
assert response.status_code == 200
assert response.json()["healthy"] is True
def test_workcell_manager():
manager = WorkcellManager(
settings=WorkcellManagerSettings(enable_registry_resolution=False),
document_handler=InMemoryDocumentStorageHandler(),
cache_handler=InMemoryCacheHandler(),
)
...
def test_resource_manager():
manager = ResourceManager(
settings=ResourceManagerSettings(enable_registry_resolution=False),
postgres_handler=SQLiteHandler(),
)
...
Critical test pattern: Always set enable_registry_resolution=False to prevent registry lockfile usage in tests.
OpenTelemetry Integration
Per-manager configuration via environment:
EVENT_OTEL_ENABLED=true
EVENT_OTEL_SERVICE_NAME="madsci.event"
EVENT_OTEL_EXPORTER="otlp" # or "console"
EVENT_OTEL_ENDPOINT="http://localhost:4317"
EVENT_OTEL_PROTOCOL="grpc" # or "http"
In code:
with self.span("process_data", attributes={"data.size": 100}) as span:
result = process(data)
span.set_attribute("result.count", len(result))
Common Pitfalls
- ULID not UUID: Use
new_ulid_str()for all IDs - prefixed vs unprefixed: Code uses
self.settings.server_url, YAML usesevent_server_url model_dump(by_alias=True): Produces prefixed keys for YAML export- Registry in tests: Always
enable_registry_resolution=False - Handler injection: Pass handlers via constructor for dependency injection (production and test)
- FerretDB, not MongoDB: The system uses FerretDB (MongoDB wire protocol on PostgreSQL). pymongo client works unchanged.
- Valkey, not Redis: Drop-in Redis replacement. redis-py client works unchanged.
- EventClient recursive logging: Event Manager's own EventClient must have
event_server_url=None - AnyUrl trailing slash: All URLs stored via Pydantic AnyUrl get a trailing slash
- Secret fields: Use
json_schema_extra={"secret": True}orSecretStrtype