Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ first.
minted by the library's `commands()`/`events()` facades — no hand-rolled topic builder, no
configurable prefix, no legacy alias (all retired in the UNS-core migration; see
`docs/reference/messaging-interface.md`). `state`/`cfg`/`metric` are reserved, library-owned classes.
- **Command verbs declare their addressing scope** (core 0.5.0, `DESIGN-scoped-commands.md` D-SC-2;
register entry in DESIGN §16): `get-status` and `trigger` are `CommandScope::Both` — no instance named
means the component-wide answer, one named means that instance — and `set-activation` is
`CommandScope::Instance` (it has no "all" form; `INSTANCE_REQUIRED`/`UNKNOWN_INSTANCE` stay
component-side per D-SC-4). An instance is named by the topic (the library's per-instance inbox) or by
the `instance` body field; the topic wins, and `src/control.rs`'s `address()` folds it into the body
selector so every handler reads one selector.
- Durable **write-ahead** state → crash-safe move+delete with checksum-verify-before-complete.
- **Long-outage tolerant** (hours–~2d): time-based `giveUpAfter` (default 7d, not attempt caps, shipped) and
resume in-flight (shipped). The **disconnection circuit-breaker** (§13.4) is still **not implemented** —
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ dest-azure = ["dep:azure_storage_blobs", "dep:azure_storage", "dep:azure_core",
dest-gcs = ["dep:reqwest", "reqwest/json", "dep:base64", "edgecommons/credentials"]

[dependencies]
# edgecommons Rust library, pinned to a commit on edgecommons/edgecommons (private). Building standalone needs
# read access to that repo. The fetch goes through the git CLI (see .cargo/config.toml); locally use your
# gh/git credential helper, and CI's reusable workflow rewrites the https URL with EDGECOMMONS_READ_TOKEN.
# edgecommons Rust library, pinned to the `rust-lib/v0.5.0` release tag on edgecommons/edgecommons — a
# public repo, so the fetch needs no credentials in CI or locally.
#
# UNS MIGRATION NOTE: this code REQUIRES the UNS core (top-level `identity`, `gg.uns()`, `gg.commands()`,
# the reserved-class guard, the `data()`/`events()`/`app()` class facades) plus the rebranded Rust crate
# metadata. The pin below points at the current core `main`; LOCAL DEV may still build against the sibling
# checkout via the gitignored `.cargo/config.toml` `paths` override, which replaces this source outright.
edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "36a70c48b65b35f77bfab70d3a73869debdfc407", default-features = false }
# This code REQUIRES the UNS core (top-level `identity`, `gg.uns()`, `gg.commands()`, the reserved-class
# guard, the `data()`/`events()`/`app()` class facades) and 0.5.0's scoped command registration
# (`register(verb, scope, handler)`). LOCAL DEV may build against the sibling checkout via the gitignored
# `.cargo/config.toml` `[patch]` override, which replaces this source outright — never commit a
# `Cargo.lock` regenerated while that override is active.
edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "a14a3285573ef2bb6a531e1e1936c6dc40a85ef4", default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "time", "sync", "fs"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
31 changes: 27 additions & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,10 +1045,33 @@ the UNS core this validity guarantee is enforced by the library that mints the t
> The hand-rolled local control dispatcher below was replaced by verb registrations on the edgecommons
> **command inbox** (`gg.commands()`, `ecv1/{device}/FileReplicator/cmd/#`), which now provides the
> generic control framework core previously lacked. Concretely: **`get-config` is retired** (the library's
> built-in `get-configuration` verb answers it, redacted — plus `ping` / `reload-config`); the
> instance-in-topic scoping (`…/cmd/instances/{id}/…`) became an instance **body field**
> (`cmd/get-status` + `{instance?}`); verbs are `cmd/get-status`, `cmd/trigger`, `cmd/set-activation`. The
> table below is the ORIGINAL design; the `get-status` reply *shape* it describes is still accurate.
> built-in `get-configuration` verb answers it, redacted — plus `ping` / `reload-config`); verbs are
> `cmd/get-status`, `cmd/trigger`, `cmd/set-activation`. The table below is the ORIGINAL design; the
> `get-status` reply *shape* it describes is still accurate.
>
> **Instance addressing (core 0.5.0 scoped commands, core `DESIGN-scoped-commands.md`).** Each verb
> declares an addressing scope at registration, which the library enforces before dispatch:
>
> | Verb | Scope | No instance named | One instance named |
> |---|---|---|---|
> | `get-status` | `Both` | component-wide roster + summary | that instance's document |
> | `trigger` | `Both` | every active instance | that instance |
> | `set-activation` | `Instance` | `INSTANCE_REQUIRED` | that instance |
>
> `get-status`/`trigger` are `Both` because "no instance named" has always carried a meaningful
> *component-wide* answer here rather than being a default to resolve — D-SC-3's dual-semantics use.
> `set-activation` is `Instance`: it never had an "all" form, and per D-SC-4 the "no instance named"
> policy (`INSTANCE_REQUIRED`) and the unknown-id check (`UNKNOWN_INSTANCE`) stay component-side,
> because the library owns addressing and has no configuration knowledge.
>
> An instance is named **either** by the delivery topic
> (`ecv1/{device}/FileReplicator/{instance}/cmd/{verb}`, the library's per-instance inbox) **or** by the
> `instance` body field over the component topic. The topic wins (D-SC-4), and the library rejects a
> request whose topic and `body.instance` disagree with `BAD_ARGS`. `src/control.rs`'s `address()` folds
> the topic-addressed instance into the body selector, so the handlers keep reading exactly one
> selector. The original design's instance-in-topic scoping is therefore back — as the library's UNS
> instance inbox rather than a hand-rolled route — alongside the body field that replaced it in the UNS
> migration.

Uses the edgecommons request/reply primitive (`request`/`reply`, `reply_to`). At design time core had **no
generic control framework** — each component wired its own handlers — so this speced a small local **control
Expand Down
13 changes: 9 additions & 4 deletions docs/how-to-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,21 @@ lean on resume:

## Activate / deactivate an instance from the control plane

Pause or resume one instance at runtime without a redeploy, via the `set-activation` command (it has **no**
"all" form — `instance` is required):
Pause or resume one instance at runtime without a redeploy, via the `set-activation` command. It is an
**instance-scoped** verb with **no** "all" form: name the instance in the topic, or in the `instance`
body field over the component topic.

```
request topic ecv1/<device>/FileReplicator/cmd/set-activation
request topic ecv1/<device>/FileReplicator/plant-csv-to-s3/cmd/set-activation
header.name set-activation
body { "instance": "plant-csv-to-s3", "active": false, "persist": true }
body { "active": false, "persist": true }
reply body { "ok": true, "result": { "instance": "plant-csv-to-s3", "active": false, "persisted": true } }
```

The equivalent over the component topic — `ecv1/<device>/FileReplicator/cmd/set-activation` with
`{ "instance": "plant-csv-to-s3", "active": false, "persist": true }` — is accepted unchanged. Naming
no instance at all answers `INSTANCE_REQUIRED`.

Send the request through an edgecommons client API or another protobuf-aware producer. The body and reply
above are decoded JSON content inside the EdgeCommons command envelope; raw MQTT JSON is not accepted as a
normal command message.
Expand Down
10 changes: 6 additions & 4 deletions docs/reference/data-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ unsigned 64-bit (a JavaScript consumer may lose precision above 2^53). Timestamp

## The `get-status` document

`get-status` (`…/cmd/get-status`) returns different shapes depending on the request body:
`get-status` returns different shapes depending on which instance, if any, the request names:

- **`{}`** (no `instance`) → a **component-wide** document: a roster of every instance plus a summary.
- **`{ "instance": "<id>" }`** → that one instance's **per-instance** document (or its **disabled** document
if it was disabled at startup). An unknown id is the error `UNKNOWN_INSTANCE`.
- **No instance named** — `…/FileReplicator/cmd/get-status` with no `instance` body field → a
**component-wide** document: a roster of every instance plus a summary.
- **One instance named** — `…/FileReplicator/{id}/cmd/get-status`, or the component topic with
`{ "instance": "<id>" }` → that one instance's **per-instance** document (or its **disabled**
document if it was disabled at startup). An unknown id is the error `UNKNOWN_INSTANCE`.

The reply is always wrapped by the command contract: `{ "ok": true, "result": <document> }`.

Expand Down
51 changes: 33 additions & 18 deletions docs/reference/messaging-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ ecv1/{device}/{component}/[{instance}/]{class}[/{channel…}]
`.` — `com.mbreissi.edgecommons.FileReplicator` → **`FileReplicator`**), not the `file-replicator`
registry slug.
- `{instance}` — OPTIONAL. Present (a `component.instances[].id`) for instance-scoped traffic — a
replication instance's own events (`file-ready`, `replication-*`, …) ride `gg.instance(id).events()`.
**Absent** for component-scope traffic: the built-in command verbs, `ComponentReady`, the scope-`"all"`
`trigger`/`get-status` events, and the library's own `state`/`cfg`/`metric` keepalives.
replication instance's own events (`file-ready`, `replication-*`, …) ride `gg.instance(id).events()`,
and a command addressed at one instance rides that instance's own command inbox.
**Absent** for component-scope traffic: a command addressed at the whole component, `ComponentReady`,
the scope-`"all"` `trigger`/`get-status` events, and the library's own `state`/`cfg`/`metric`
keepalives.
- `{class}` ∈ `cmd` (inbound commands, request/reply) · `evt` (event stream) · the **reserved**,
library-owned `state`/`cfg`/`metric`/`log` (this component never publishes to them directly).

Expand All @@ -27,24 +29,37 @@ There is no configurable topic prefix and no legacy alias — the UNS grammar ab

## Commands (`cmd`, request/reply via `reply_to`)

Registered on the component-scope command inbox (`ecv1/{device}/FileReplicator/cmd/#`); an instance token
is optional and present only for explicit multi-instance addressing. Scoping an instance is a request-body
field, not a topic segment (mirroring how opcua-adapter/
modbus-adapter address their multi-instance `sb/*` verbs and telemetry-processor's `route`/`pause`/
`resume`). Publish commands with the edgecommons client APIs (`MessageBuilder` + `MessagingService`
request/reply, or an equivalent protobuf producer), not by sending JSON text to MQTT. Every decoded reply
body is `{"ok": true, "result": <value>}` or `{"ok": false, "error": {"code", "message"}}` (the
edgecommons command-inbox contract — the request's `header.name` MUST equal the verb).
Two inboxes carry commands: the component one, `ecv1/{device}/FileReplicator/cmd/#`, and one per
replication instance, `ecv1/{device}/FileReplicator/{instance}/cmd/#`. Publish commands with the
edgecommons client APIs (`MessageBuilder` + `MessagingService` request/reply, or an equivalent protobuf
producer), not by sending JSON text to MQTT. Every decoded reply body is
`{"ok": true, "result": <value>}` or `{"ok": false, "error": {"code", "message"}}` (the edgecommons
command-inbox contract — the request's `header.name` MUST equal the verb).

| Verb | Topic | Body | Result / error codes |
| Verb | Scope | Body | Result / error codes |
|---|---|---|---|
| `get-status` | `…/cmd/get-status` | `{ "instance"?: string }` | omitted `instance` → component-wide roster+summary; present → that instance's document; `UNKNOWN_INSTANCE` |
| `trigger` | `…/cmd/trigger` | `{ "instance"?: string, "ignoreWindow"?: bool }` | accepted + counts; `UNKNOWN_INSTANCE` |
| `set-activation` | `…/cmd/set-activation` | `{ "instance": string, "active"?: bool, "persist"?: bool, "reset"?: bool }` | new state; `INSTANCE_REQUIRED` (no "all" form), `UNKNOWN_INSTANCE`, `INVALID_REQUEST` (neither `active` nor `reset`), `ACTIVATION_FAILED` |
| `get-status` | both | `{ "instance"?: string }` | no instance named → component-wide roster+summary; one named → that instance's document; `UNKNOWN_INSTANCE` |
| `trigger` | both | `{ "instance"?: string, "ignoreWindow"?: bool }` | accepted + counts; `UNKNOWN_INSTANCE` |
| `set-activation` | instance | `{ "instance": string, "active"?: bool, "persist"?: bool, "reset"?: bool }` | new state; `INSTANCE_REQUIRED` (no "all" form), `UNKNOWN_INSTANCE`, `INVALID_REQUEST` (neither `active` nor `reset`), `ACTIVATION_FAILED` |

The library's own built-in verbs are also available: `ping` (liveness), `reload-config` (re-fetch +
re-apply), and **`get-configuration`** — returns the **redacted** effective config (`{"config":
<redacted>}`, with secrets replaced rather than left as unresolved `$secret` refs).
### Addressing an instance

Each verb declares an addressing **scope**, which the library enforces before the verb runs:

- **`both`** (`get-status`, `trigger`) — either inbox is meaningful. Address the component
(`…/FileReplicator/cmd/{verb}`) for the fleet-wide answer, or one instance
(`…/FileReplicator/{instance}/cmd/{verb}`) for that instance alone.
- **`instance`** (`set-activation`) — this verb has no "all" form. It answers `INSTANCE_REQUIRED`
when neither the topic nor the body names an instance.

The `instance` body field remains the way to target one instance over the component inbox. When the
topic names an instance it wins; a request that names one instance in the topic and a different one
in `body.instance` is rejected with `BAD_ARGS` before the verb runs.

The library's own built-in verbs are also available on both inboxes: `ping` (liveness),
`reload-config` (re-fetch + re-apply), and **`get-configuration`** — returns the **redacted**
effective config (`{"config": <redacted>}`, with secrets replaced rather than left as unresolved
`$secret` refs).

## Events (`evt`, `evt/{severity}/{type}`)

Expand Down
Loading
Loading