Rename Installation to Instance
Follows the portal: the noun is "instance" everywhere the app says it — UI strings, CLI output, error details, docs and comments. The wire keys (`instance_id`, `instance_token`) and the hub route this calls move with it. An existing cloud.json is adopted rather than refused: without the key alias the dataclass fails to parse, which the caller swallows and reads as "never enrolled" instead of "reconnect". `instance_key` on a node type becomes `target_key`. It means the outside thing a node points at, which is a different sense of the word, and keeping both would put two meanings of "instance" in one codebase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
@@ -358,7 +358,7 @@ class AlertManager:
|
||||
await asyncio.to_thread(self.publish, message, alert.model_dump())
|
||||
|
||||
async def _send_webpush(self, alert: Alert) -> None:
|
||||
"""Wake every browser that subscribed to this installation.
|
||||
"""Wake every browser that subscribed to this instance.
|
||||
|
||||
No settings of its own: a browser subscribes by pressing a button on
|
||||
the alerts screen, and this channel goes to whichever ones did. Saying
|
||||
|
||||
@@ -98,7 +98,7 @@ HOOK_PREFIX = "/hooks"
|
||||
NODE_STOP_TIMEOUT = 5.0
|
||||
|
||||
# How long a rebuild asked for by a request waits for one already running.
|
||||
# Generous on purpose: a rebuild of a populated installation reconnects every
|
||||
# Generous on purpose: a rebuild of a populated instance reconnects every
|
||||
# node and takes the better part of ten seconds, and a caller queued behind a
|
||||
# healthy one of those should not be turned away. Past that the controller is
|
||||
# wedged rather than busy, and an error the caller can act on beats a request
|
||||
@@ -106,7 +106,7 @@ NODE_STOP_TIMEOUT = 5.0
|
||||
REBUILD_WAIT = 15.0
|
||||
|
||||
# How long a caller waits for a *flow* rebuild that is already running. A
|
||||
# per-flow rebuild reconnects one flow's nodes rather than the installation's,
|
||||
# per-flow rebuild reconnects one flow's nodes rather than the instance's,
|
||||
# so this is a queueing budget — several of them back to back, which is what
|
||||
# seeding does — not the room a single one needs. Fifteen seconds was sized for
|
||||
# the whole-pipeline rebuild and would let a wedge sit unreported.
|
||||
@@ -680,7 +680,7 @@ class FlowController:
|
||||
another flow owns — but the wiring is derived from message names, so
|
||||
swapping one flow's nodes into the graph and deriving the edges again
|
||||
is enough. What that saves is the reconnecting: the cost of a rebuild
|
||||
on a populated installation is every node opening its socket again,
|
||||
on a populated instance is every node opening its socket again,
|
||||
and only one flow's have changed.
|
||||
|
||||
A flow the store no longer has is taken out instead of replaced.
|
||||
@@ -1522,12 +1522,12 @@ class FlowController:
|
||||
key = None
|
||||
if node_type is not None:
|
||||
try:
|
||||
key = node_type.cls.instance_key(node_def.params)
|
||||
key = node_type.cls.target_key(node_def.params)
|
||||
except Exception:
|
||||
# A plugin's own grouping is not worth the whole graph;
|
||||
# this node just stands on its own.
|
||||
logger.exception(
|
||||
"instance_key failed for node type '%s'", node_def.type
|
||||
"target_key failed for node type '%s'", node_def.type
|
||||
)
|
||||
gid = f"{node_def.type}:{key}" if key else member
|
||||
gid_of[member] = gid
|
||||
|
||||
@@ -81,7 +81,7 @@ WidgetType = Literal[
|
||||
INPUT_WIDGETS = {"button", "switch", "slider", "input", "dropdown", "color"}
|
||||
|
||||
#: What a colour widget puts on the wire, by the format it was configured for.
|
||||
#: The default is what the Node-RED installation this ports already sends its
|
||||
#: The default is what the Node-RED instance this ports already sends its
|
||||
#: DMX encoders — ``[h, s, v]``, hue in degrees and the other two in percent —
|
||||
#: and the two alternatives exist because fixtures differ. Mirrored in the
|
||||
#: client (``frontend/src/components/Dashboard/ColorWidget.tsx``).
|
||||
@@ -197,7 +197,7 @@ class WidgetDef(BaseModel):
|
||||
differ and a change node per tile is not the answer:
|
||||
|
||||
- ``hsv`` (the default) — ``[h, s, v]``, hue 0-360 degrees, saturation and
|
||||
value 0-100 percent. What the Node-RED installation this ports feeds its
|
||||
value 0-100 percent. What the Node-RED instance this ports feeds its
|
||||
3CH/4CH DMX encoders, which divide by 360 and by 100.
|
||||
- ``rgb`` — ``[r, g, b]``, each 0-255. The conventional range; the
|
||||
reference's own encoders produce it after converting.
|
||||
@@ -479,7 +479,7 @@ class DashboardDef(BaseModel):
|
||||
def _flatten(cls, data: Any) -> Any:
|
||||
"""Read a document written as pages and sections as one grid.
|
||||
|
||||
Stored dashboards live in each installation's git repository, so the
|
||||
Stored dashboards live in each instance's git repository, so the
|
||||
old shape is normalised on the way in rather than migrated: an
|
||||
untouched document keeps working, and the next save writes it flat.
|
||||
"""
|
||||
@@ -502,7 +502,7 @@ class DashboardDef(BaseModel):
|
||||
picker records the payload type beside the name, so neither the editor
|
||||
nor a wall panel has to fetch the catalogue to know the wiring is
|
||||
wrong. A name this build does not know is left alone rather than
|
||||
refused — an older installation reading a newer document simply does
|
||||
refused — an older instance reading a newer document simply does
|
||||
not act on it.
|
||||
"""
|
||||
for name, setting in self.settings.items():
|
||||
|
||||
@@ -49,7 +49,7 @@ DUE_RESERVE = 2
|
||||
# check whether a cascade slot has freed.
|
||||
DUE_CLAIM_BLOCK_MS = 200
|
||||
#: How many cascades may be in flight, unless the service is given a number.
|
||||
#: Sustained throughput is this over the mean cascade time, so an installation
|
||||
#: Sustained throughput is this over the mean cascade time, so an instance
|
||||
#: whose nodes wait on a network rather than a CPU may want more of them —
|
||||
#: `FLOW_MAX_CASCADES` is where that is said.
|
||||
MAX_CASCADES = 4
|
||||
|
||||
@@ -97,7 +97,7 @@ def venv_dir() -> Path:
|
||||
def venv_python() -> str:
|
||||
"""The interpreter node code runs on.
|
||||
|
||||
Falls back to the engine's own when there is no venv — an installation that
|
||||
Falls back to the engine's own when there is no venv — an instance that
|
||||
could not build one still runs python nodes, it just cannot add packages
|
||||
to them.
|
||||
"""
|
||||
|
||||
@@ -179,7 +179,7 @@ class Node:
|
||||
idempotent: bool = True
|
||||
|
||||
@classmethod
|
||||
def instance_key(cls, params: dict[str, Any]) -> str | None:
|
||||
def target_key(cls, params: dict[str, Any]) -> str | None:
|
||||
"""Which outside thing these parameters point at, if any.
|
||||
|
||||
Two nodes with the same key talk to the same broker topic, URL or
|
||||
|
||||
@@ -189,7 +189,7 @@ class HttpNode(Node):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def instance_key(cls, params: dict[str, Any]) -> str | None:
|
||||
def target_key(cls, params: dict[str, Any]) -> str | None:
|
||||
# ponytail: a webhook's stored url is the path before the flow name is
|
||||
# prefixed onto it, so two flows both receiving on "/tick" merge into
|
||||
# one neuron. Key on mode as well if that ever misleads.
|
||||
|
||||
@@ -18,7 +18,7 @@ def _influxdb() -> Any:
|
||||
"""The client library, which is a `fluksio[server]` extra.
|
||||
|
||||
Imported per use rather than at module level, because the node type is
|
||||
registered at boot and an installation with no InfluxDB behind it should
|
||||
registered at boot and an instance with no InfluxDB behind it should
|
||||
not have to carry the library to start.
|
||||
"""
|
||||
try:
|
||||
@@ -198,7 +198,7 @@ class InfluxDbNode(Node):
|
||||
queries: dict[str, dict[str, Any]] = {}
|
||||
|
||||
@classmethod
|
||||
def instance_key(cls, params: dict[str, Any]) -> str | None:
|
||||
def target_key(cls, params: dict[str, Any]) -> str | None:
|
||||
"""The bucket, which is the thing several flows share."""
|
||||
url, bucket = params.get("url"), params.get("bucket")
|
||||
return f"{url}/{bucket}" if url and bucket else None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Inject: the node that starts something, on a timer or on request.
|
||||
|
||||
Node-RED's *inject* is the most placed trigger in a real installation — mostly
|
||||
Node-RED's *inject* is the most placed trigger in a real instance — mostly
|
||||
as a button someone presses, sometimes on an interval, occasionally once when
|
||||
everything comes up.
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Anything here could be written as a `python` node — that is what the function
|
||||
node is for. These exist because the same handful of shapes account for most of
|
||||
a real installation, and a rule you fill in is easier to read on a canvas, and
|
||||
a real instance, and a rule you fill in is easier to read on a canvas, and
|
||||
to change, than five lines of code repeated eighty times.
|
||||
"""
|
||||
|
||||
@@ -115,7 +115,7 @@ class ChangeNode(Node):
|
||||
"""Reshape a value on its way past: scale, offset, map, or replace.
|
||||
|
||||
Node-RED's *change*, which is the second most common node in a real
|
||||
installation after the function.
|
||||
instance after the function.
|
||||
"""
|
||||
|
||||
class Params(BaseModel):
|
||||
|
||||
@@ -24,7 +24,7 @@ def _aiomqtt() -> Any:
|
||||
"""The client library, which is a `fluksio[server]` extra.
|
||||
|
||||
Imported per use rather than at module level, because the node type is
|
||||
registered at boot and an installation that talks to no broker should not
|
||||
registered at boot and an instance that talks to no broker should not
|
||||
have to carry the library to start.
|
||||
"""
|
||||
try:
|
||||
@@ -218,7 +218,7 @@ class MqttNode(Node):
|
||||
json_key: str | dict[str, str] = ""
|
||||
|
||||
@classmethod
|
||||
def instance_key(cls, params: dict[str, Any]) -> str | None:
|
||||
def target_key(cls, params: dict[str, Any]) -> str | None:
|
||||
"""The broker and topic, which is one physical thing.
|
||||
|
||||
A publisher and a subscriber on the same topic get the same key on
|
||||
|
||||
@@ -45,7 +45,7 @@ class PanelDef(BaseModel):
|
||||
|
||||
|
||||
class PanelsConfig(BaseModel):
|
||||
"""Every panel this installation knows about."""
|
||||
"""Every panel this instance knows about."""
|
||||
|
||||
panels: list[PanelDef] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ def _derive(
|
||||
|
||||
The two lookups come out of the same walk: who consumes a message, and
|
||||
which node an id names. Both were linear scans over every node in the
|
||||
installation, on the per-message path.
|
||||
instance, on the per-message path.
|
||||
"""
|
||||
# A message may have several producers; every one of them is upstream
|
||||
# of the nodes consuming it.
|
||||
|
||||
@@ -10,7 +10,7 @@ counted at all.
|
||||
One question, then, asked once here: of every machine attached — this one and
|
||||
each worker — which could grant what this node asked for, and which of those
|
||||
has it free right now. A worker reports its inventory when it attaches, so the
|
||||
answer covers the whole installation rather than the host the engine happens to
|
||||
answer covers the whole instance rather than the host the engine happens to
|
||||
be on.
|
||||
|
||||
Two properties are worth stating because they are what make the waiting safe:
|
||||
@@ -178,7 +178,7 @@ class Placer:
|
||||
"""Every machine's size, as something to measure a request against.
|
||||
|
||||
A provisioner's job shapes count: a machine it can start on demand is
|
||||
one this installation has, even when nothing is attached yet.
|
||||
one this instance has, even when nothing is attached yet.
|
||||
"""
|
||||
shapes = []
|
||||
for target in self.targets():
|
||||
|
||||
@@ -348,7 +348,7 @@ class SlurmProvisioner:
|
||||
|
||||
|
||||
def load_provisioners(path: Path, events: EventBus | None = None) -> list[Any]:
|
||||
"""Read the configured clusters. An installation with none has no file."""
|
||||
"""Read the configured clusters. An instance with none has no file."""
|
||||
if not path.exists():
|
||||
return []
|
||||
try:
|
||||
|
||||
@@ -296,7 +296,7 @@ def _from_run(
|
||||
if artifacts is not None and artifacts.path(row.digest) is None:
|
||||
raise RunRejected(
|
||||
f"Parameter '{key}': run '{run_id}' made '{output}', but its "
|
||||
"bytes are gone from this installation's store"
|
||||
"bytes are gone from this instance's store"
|
||||
)
|
||||
return {
|
||||
"digest": row.digest,
|
||||
@@ -327,7 +327,7 @@ def _from_digest(
|
||||
if artifacts is not None and artifacts.path(digest) is None:
|
||||
raise RunRejected(
|
||||
f"Parameter '{key}': '{digest}' is known but its bytes are gone "
|
||||
"from this installation's store"
|
||||
"from this instance's store"
|
||||
)
|
||||
return {
|
||||
"digest": row.digest,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Web Push: an alert reaching a phone that has this installation installed.
|
||||
"""Web Push: an alert reaching a phone that has this instance installed.
|
||||
|
||||
The browser hands us a subscription — an endpoint URL at its own push service,
|
||||
plus two keys — and from then on the engine can wake that device without it
|
||||
holding a connection open. Two pieces of crypto are involved and both are
|
||||
specified: the payload is encrypted to the subscription's keys (RFC 8291,
|
||||
aes128gcm) so the push service carries something it cannot read, and the
|
||||
request is signed with this installation's own keypair (VAPID, RFC 8292) so the
|
||||
request is signed with this instance's own keypair (VAPID, RFC 8292) so the
|
||||
service knows who is sending.
|
||||
|
||||
Only ``http-ece`` is new here; the signing is `pyjwt` and the request is
|
||||
@@ -13,7 +13,7 @@ Only ``http-ece`` is new here; the signing is `pyjwt` and the request is
|
||||
this in one call, but brings `requests` *and* `aiohttp` with it — two more HTTP
|
||||
stacks on a machine that may well be a Raspberry Pi.
|
||||
|
||||
The keypair is this installation's identity to the push services and lives with
|
||||
The keypair is this instance's identity to the push services and lives with
|
||||
the subscriptions in one file. Losing it means every browser has to subscribe
|
||||
again; it is regenerated on the spot if the file goes missing.
|
||||
"""
|
||||
@@ -62,7 +62,7 @@ class Subscription(BaseModel):
|
||||
|
||||
|
||||
class _Store(BaseModel):
|
||||
#: This installation's VAPID private key: the raw P-256 scalar, base64url.
|
||||
#: This instance's VAPID private key: the raw P-256 scalar, base64url.
|
||||
private_key: str = ""
|
||||
subscriptions: list[Subscription] = Field(default_factory=list)
|
||||
|
||||
@@ -113,7 +113,7 @@ def _public_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
|
||||
|
||||
|
||||
def public_key() -> str:
|
||||
"""This installation's VAPID public key, generating the pair on first ask.
|
||||
"""This instance's VAPID public key, generating the pair on first ask.
|
||||
|
||||
Base64url of the uncompressed point, which is the shape
|
||||
`pushManager.subscribe` wants for `applicationServerKey`. Blocking.
|
||||
@@ -126,7 +126,7 @@ def public_key() -> str:
|
||||
key.private_numbers().private_value.to_bytes(32, "big")
|
||||
)
|
||||
_write(store)
|
||||
logger.info("Generated this installation's web push keypair")
|
||||
logger.info("Generated this instance's web push keypair")
|
||||
return _b64url(_public_bytes(_private_key(store)))
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ def subscriptions() -> list[Subscription]:
|
||||
|
||||
|
||||
def _vapid_headers(key: ec.EllipticCurvePrivateKey, endpoint: str) -> dict[str, str]:
|
||||
"""Prove to the push service which installation is sending (RFC 8292)."""
|
||||
"""Prove to the push service which instance is sending (RFC 8292)."""
|
||||
origin = urlparse(endpoint)
|
||||
token = jwt.encode(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user