-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
12 changed files
with
353 additions
and
157 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.""" |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.