Backend conventions#
This section covers the conventions that keep the SAF GLOW Engine backend stateless, predictable, and deployable across multiple processes.
No module-level globals#
The GLOW API server is stateless—each request may be handled by a different process. Module-level mutable state is never shared between requests and leads to subtle bugs in multi-process deployments.
# Breaks in multi-process deployment
_cache = {} # Module-level mutable state — never shared across processes
class AnalysisStep(StepModel):
temperature: float = 20.0
@transaction(self=StepSpec(upload=["temperature"]))
def run(self) -> None:
_cache["last_run"] = self.temperature # This is lost between requests
class AnalysisStep(StepModel):
"""Analysis step — state is persisted via step fields, not globals."""
temperature: float = 20.0
last_run_temperature: float = 0.0
@transaction(self=StepSpec(download=["temperature"], upload=["last_run_temperature"]))
def run(self) -> None:
self.last_run_temperature = self.temperature
Never store session data in memory#
For the same reason, never use in-memory session stores, caches, or singletons to hold per-user or per-project data. SAF GLOW Engine persists all state through step fields—use them.
_sessions = {} # Lost on restart, not shared across workers
class SessionStep(StepModel):
user_id: str = ""
@transaction(self=StepSpec(download=["user_id"]))
def track_user(self) -> None:
_sessions[self.user_id] = {"logged_in": True}
class SessionStep(StepModel):
"""Session state persisted through GLOW step fields."""
user_id: str = ""
is_logged_in: bool = False
@transaction(self=StepSpec(download=["user_id"], upload=["is_logged_in"]))
def track_user(self) -> None:
self.is_logged_in = True
File naming convention#
Use the <name>_step.py naming convention for step model files. This makes it
immediately clear which modules define step models versus business logic or utilities.
solution/
├── setup_step.py ← step model
├── analysis_step.py ← step model
├── results_step.py ← step model
├── logic/
│ ├── solver.py ← business logic (not a step)
│ └── post_process.py ← business logic (not a step)
└── definition.py ← solution definition (StepsModel)
All fields must have defaults#
Every field on a StepModel must have a default value. Fields without defaults cause
serialization failures when SAF GLOW Engine creates a new project.
class AnalysisStep(StepModel):
temperature: float # No default — will fail on project creation
results: list[float] # No default — will fail on project creation
class AnalysisStep(StepModel):
"""Analysis step model with explicit defaults."""
temperature: float = 20.0
pressure: float = 101.325
results: list[float] = []
notes: str | None = None
@transaction(self=StepSpec(download=["temperature", "pressure"], upload=["results"]))
def run(self) -> None:
"""Run the analysis computation."""
self.results = [self.temperature * 1.5, self.pressure * 0.9]
Import strategy for heavy packages#
Import heavy packages (NumPy, SciPy, PyAnsys clients) inside transaction methods if they are used in only one place. Import at module level if used across multiple methods in the same file.
This keeps module import time fast and avoids loading unnecessary dependencies during SAF GLOW Engine’s solution analysis phase.
class MeshStep(StepModel):
"""Step that uses NumPy only in one transaction."""
mesh_data: list[float] = []
@transaction(self=StepSpec(upload=["mesh_data"]))
def generate_mesh(self) -> None:
import numpy as np # Imported here — used only in this method
self.mesh_data = np.linspace(0, 1, 100).tolist()
import numpy as np # Used in multiple methods below
class SimulationStep(StepModel):
"""Step where NumPy is used across several transactions."""
input_data: list[float] = []
normalized: list[float] = []
statistics: dict[str, float] = {}
@transaction(self=StepSpec(download=["input_data"], upload=["normalized"]))
def normalize(self) -> None:
arr = np.array(self.input_data)
self.normalized = ((arr - arr.min()) / (arr.max() - arr.min())).tolist()
@transaction(self=StepSpec(download=["input_data"], upload=["statistics"]))
def compute_stats(self) -> None:
arr = np.array(self.input_data)
self.statistics = {"mean": float(arr.mean()), "std": float(arr.std())}