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:
+4
-1
@@ -204,7 +204,10 @@ curl -X POST $FLUKSIO/alerts/test/my-phone -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Secrets are write-only over the API: you can list names and set values, never
|
||||
read one back. Secrets and `POST /modules/apply` are a superuser's.
|
||||
read one back. Secrets and `POST /modules/apply` are a superuser's, and so is
|
||||
what a connector keeps across restarts: `GET` or `DELETE
|
||||
/connectors/{node_type}/state`, or `DELETE /connectors/{node_type}/state/{key}`
|
||||
for one key (see [the connector contract](../reference/connector-contract.md#keeping-state-across-restarts)).
|
||||
|
||||
## Observability
|
||||
|
||||
|
||||
@@ -143,3 +143,4 @@ raised exception shows there with its traceback and marks the node down.
|
||||
- `poll()` returns `None` rather than raising when there is simply nothing new.
|
||||
- `write()` returns quickly; the thread it runs on is the scheduler's.
|
||||
- Health reported when the connection comes and goes.
|
||||
- Anything that has to outlive a restart in `self.store`, keyed by device.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user