Name the sizes a node can ask for

Raw cpus and gpus are a property of the machines an installation has, so a node
written against a cluster quietly stops meaning anything when the cluster is
replaced. A node says "gpu-small" instead, and what that is stored here —
editable, and read again every time the node is built, so changing the flavor
changes what the next run gets.

Memory joins the schema properly (`ram`, in MB, accepting "2G"), along with
`duration_s` for how long a node is expected to take. That one is recorded and
shown and nothing else yet: a statement for whoever is planning around the node,
not a limit — the limit is still `timeout`.

A flavor and a number for the same thing is refused, compared by value so an
editor writing the whole object back with its defaults still round-trips. A name
nothing stores is refused at the save, which covers the canvas and `fluksio
sync` at once, and deleting one a node still asks for says which node.

Four sizes are seeded on an installation that has none, and never re-seeded:
re-adding one somebody deliberately removed is an argument nobody wins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6HeySA27EkGANZN95QySW
This commit is contained in:
2026-08-27 08:59:10 +02:00
co-authored by Claude Opus 5
parent 6ff56533f5
commit a82f88cf0a
15 changed files with 720 additions and 19 deletions
+43
View File
@@ -26,14 +26,21 @@ from __future__ import annotations
import logging
import os
import re
import threading
from collections.abc import Callable
from dataclasses import dataclass
from sqlmodel import Session
from fluksio.flow.schemas import Resources
logger = logging.getLogger(__name__)
#: What a flavor may be called. Dashes allowed, unlike a flow or node name —
#: "gpu-small" reads better than "gpu_small" on a dropdown.
FLAVOR_NAME = re.compile(r"^[a-z][a-z0-9_-]*$")
#: Every spelling of "how many threads may you use" that a scientific stack
#: reads out of the environment at import. Set together, because a process
#: usually pulls in more than one of them.
@@ -175,6 +182,42 @@ def derive_env(wanted: Resources, allocation: Allocation) -> dict[str, str]:
return env
class UnknownFlavor(ValueError):
"""A node asks for a size that is not stored here."""
def resolve_flavor(wanted: Resources) -> Resources:
"""The concrete numbers behind a declaration.
Read when the node is built rather than stored on it, so editing a flavor
changes what the next run gets. A node that names one that has been deleted
is an error rather than a default: running a training step against a size
nobody chose is worse than a node that says what is wrong with it.
"""
if not wanted.flavor:
return wanted
# Imported here: this module is the books, and the books have no business
# knowing about the database until somebody asks for a stored size.
from fluksio.core.db import engine
from fluksio.models import Flavor
with Session(engine) as session:
row = session.get(Flavor, wanted.flavor)
if row is None:
raise UnknownFlavor(
f"flavor '{wanted.flavor}' does not exist — `fluksio flavors` lists them"
)
# Built fresh rather than copied, so the flavor-and-numbers check runs on
# the result and this cannot quietly produce something invalid.
return Resources(
cpus=row.cpus,
gpus=row.gpus,
ram=row.ram,
env=wanted.env,
duration_s=wanted.duration_s,
)
def fair_share_env(cpus: int, workers: int) -> dict[str, str]:
"""Thread caps for the shared pool, where nothing declared anything.