Skip to content

Commit

Permalink
Refactor mobu state management
Browse files Browse the repository at this point in the history
In preparation for adding new functionality to run a single
business once to completion and report the results, refactor mobu
to use a ProcessContext and RequestContext similar to Gafaelfawr
and the new lab controller. This provides a hook to create a
per-request Factory, which will be needed by subsequent changes.

Mostly this is just moving code around, but add documentation for
the FlockManager class and use the proper FastAPI function for
getting the URL of a resource.
  • Loading branch information
rra committed Mar 21, 2023
1 parent 35efcca commit ad0fd2f
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 157 deletions.
26 changes: 0 additions & 26 deletions src/mobu/autostart.py

This file was deleted.

86 changes: 86 additions & 0 deletions src/mobu/dependencies/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Request context dependency for FastAPI.
This dependency gathers a variety of information into a single object for the
convenience of writing request handlers. It also provides a place to store a
`structlog.BoundLogger` that can gather additional context during processing,
including from dependencies.
"""

from dataclasses import dataclass
from typing import Optional

from fastapi import Depends, Request
from safir.dependencies.gafaelfawr import auth_logger_dependency
from structlog.stdlib import BoundLogger

from ..factory import ProcessContext
from ..services.manager import FlockManager

__all__ = [
"ContextDependency",
"RequestContext",
"context_dependency",
]


@dataclass(slots=True)
class RequestContext:
"""Holds the incoming request and its surrounding context."""

request: Request
"""Incoming request."""

logger: BoundLogger
"""Request loger, rebound with discovered context."""

manager: FlockManager
"""Global singleton flock manager."""


class ContextDependency:
"""Provide a per-request context as a FastAPI dependency.
Each request gets a `RequestContext`. To save overhead, the portions of
the context that are shared by all requests are collected into the single
process-global `~mobu.factory.ProcessContext` and reused with each
request.
"""

def __init__(self) -> None:
self._process_context: Optional[ProcessContext] = None

async def __call__(
self,
request: Request,
logger: BoundLogger = Depends(auth_logger_dependency),
) -> RequestContext:
"""Create a per-request context."""
if not self._process_context:
raise RuntimeError("ContextDependency not initialized")
return RequestContext(
request=request,
logger=logger,
manager=self._process_context.manager,
)

@property
def process_context(self) -> ProcessContext:
if not self._process_context:
raise RuntimeError("ContextDependency not initialized")
return self._process_context

async def initialize(self) -> None:
"""Initialize the process-wide shared context."""
if self._process_context:
await self._process_context.aclose()
self._process_context = ProcessContext()

async def aclose(self) -> None:
"""Clean up the per-process configuration."""
if self._process_context:
await self._process_context.aclose()
self._process_context = None


context_dependency = ContextDependency()
"""The dependency that will return the per-request context."""
81 changes: 0 additions & 81 deletions src/mobu/dependencies/manager.py

This file was deleted.

74 changes: 74 additions & 0 deletions src/mobu/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Component factory and process-wide status for mobu."""

from __future__ import annotations

from typing import Optional

import structlog
from aiohttp import ClientSession
from safir.slack.webhook import SlackWebhookClient
from structlog import BoundLogger

from .config import config
from .services.manager import FlockManager

__all__ = ["Factory", "ProcessContext"]


class ProcessContext:
"""Per-process application context.
This object caches all of the per-process singletons that can be reused
for every request.
Attributes
----------
manager
Manager for all running flocks.
session
Shared HTTP client session.
"""

def __init__(self) -> None:
self.session = ClientSession()
self.manager = FlockManager(self.session, structlog.get_logger("mobu"))

async def aclose(self) -> None:
"""Clean up a process context.
Called before shutdown to free resources.
"""
await self.manager.aclose()
await self.session.close()


class Factory:
"""Component factory for mobu.
Uses the contents of a `ProcessContext` to construct the components of an
application on demand.
Parameters
----------
context
Shared process context.
"""

def __init__(
self, context: ProcessContext, logger: Optional[BoundLogger] = None
) -> None:
self._context = context
self._logger = logger if logger else structlog.get_logger("mobu")

def create_slack_webhook_client(self) -> SlackWebhookClient | None:
"""Create a Slack webhook client if configured for Slack alerting.
Returns
-------
SlackWebhookClient or None
Newly-created Slack client, or `None` if Slack alerting is not
configured.
"""
if not config.alert_hook or config.alert_hook == "None":
return None
return SlackWebhookClient(config.alert_hook, "Mobu", self._logger)
Loading

0 comments on commit ad0fd2f

Please sign in to comment.