Give connectors a store that survives restarts, and a way to clear it

`ConnectorNode.store` keeps short strings per connector package in the
engine's database (`connector_state`, migration f8c3e5a91d27), so the nodes of
one package share them and `fluksio serve` needs nothing new: get, set and
delete, at most 64 kB a value and 256 keys a package. A superuser lists the
keys (never the values) and clears them through `/connectors/{node_type}/state`,
or under Stored state in a connector node's panel. Documented in the connector
contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
2026-10-06 17:19:48 +02:00
co-authored by Claude Opus 5.5
parent b086d5d22b
commit 847abd67d6
15 changed files with 710 additions and 4 deletions
+41 -1
View File
@@ -201,6 +201,45 @@ a remote panel wants a frame every second or two; higher rates than that are
for the local network, and full-rate video still belongs on the camera's own
stream.
## Keeping state across restarts
A connection, a session or a device handle lives in the connector's own
attributes and goes with the process. The few things that must not, such as
the certificate a device presented on first contact or a token it paired with,
go in `self.store`:
```python
pem = await asyncio.to_thread(self.store.get, f"cert:{self.config.host}")
await asyncio.to_thread(self.store.set, f"cert:{self.config.host}", pem)
await asyncio.to_thread(self.store.delete, f"cert:{self.config.host}")
```
- Keys and values are strings; `get` returns `None` for a key that is not
there, and `delete` returns whether there was one.
- The store belongs to the connector's **top-level package**, not to a node:
every node of the package sees the same keys, so two nodes on one bridge
share what was stored for it. Put the device in the key.
- It lives in the engine's database, so it survives a restart and needs
nothing `fluksio serve` does not have. It is available before `start`.
- Limits: a key is 1 to 200 characters without `/` or whitespace, a value at
most 64 kB, and a package at most 256 keys. Past any of them, `set` raises
`ValueError`. Bytes worth keeping belong in `save_artifact`.
- Every call is a database round trip, so on the event loop it goes through
`asyncio.to_thread`.
A superuser can see the keys (never the values) and clear them, all of them or
one at a time, under *Stored state* in a connector node's settings, or through
the API, which names the connector by any node type it installs:
```sh
curl -s $FLUKSIO/connectors/hue/state -H "Authorization: Bearer $TOKEN"
curl -X DELETE $FLUKSIO/connectors/hue/state/cert:10.0.0.2 -H "Authorization: Bearer $TOKEN"
curl -X DELETE $FLUKSIO/connectors/hue/state -H "Authorization: Bearer $TOKEN"
```
So a connector has to cope with a key disappearing: clearing is how somebody
tells it to start over.
## Lifecycle
```python
@@ -239,7 +278,8 @@ load is reported separately and never runs.
- **Hold state that belongs in the flow.** Logic nodes are stateless and run in
parallel; a connector may hold a connection, a session, or a device handle,
but a value another node needs belongs in a message.
and keep what has to outlive a restart in `self.store`, but a value another
node needs belongs in a message.
- **Block the event loop.** `poll`, `start` and `stop` are async and run on the
loop. Use `asyncio.to_thread` for blocking I/O.
- **Read credentials from the environment.** They come through parameters, so