How-to guides
Task-focused recipes. Each assumes the component builds and runs (see the tutorial). For the
model and the why, see explanation.md; for every field, reference/. Each
instance is a component.instances[] entry; complete configs are in
sample-configurations.md.
Replicate a directory to S3
Section titled “Replicate a directory to S3”Point an instance’s egress at an s3 destination (compiled in by default — dest-s3 is in the default
feature set):
"egress": [ { "type": "s3", "bucket": "acme-plant-telemetry", "prefix": "site42/csv/", // key = prefix + source-relative path (subtree preserved) "region": "us-east-1", "accelerate": true, // S3 Transfer Acceleration endpoint "checksumAlgorithm": "CRC32C", // flexible/trailing checksum (default CRC32C; or SHA256) "multipart": { "thresholdBytes": 16777216, "partSizeBytes": 16777216, "maxConcurrentParts": 4 }} ]- Credentials — ambient by default. Omit
credentialsand the S3 backend inherits the platform’s provider chain: Greengrass TokenExchangeService device role, Kubernetes IRSA / env / node role, or HOST env / shared profile / instance role. Scope IAM tobucket/prefix/*withPutObject+ the multipart actions (add KMS if you setsse: "aws:kms"). - Explicit credentials come from the vault, never inline:
"credentials": {"$secret": "s3-uploader"}. The resolved secret is never logged. - Files above
multipart.thresholdBytesupload as parallel multipart (parts persisted for resume); smaller files use a singlePutObject.unsignedPayload: trueskips the payload-signing pass on those simple PUTs over TLS. UseendpointUrlfor MinIO / S3-compatible / GovCloud.
Full field table: reference › destinations › S3.
Schedule a nightly upload window
Section titled “Schedule a nightly upload window”By default replication is immediate (files go as they become ready). To hold work for an overnight
window, set schedule.mode: "window". open/close accept either standard cron or an English sugar
phrase, and are timezone/DST-aware:
"schedule": { "mode": "window", "open": "0 22 * * *", // 22:00 — cron, or the phrase "between 10pm and 6am" on `open` alone "close": "0 6 * * *", // 06:00 next day — an overnight span is inferred from the two times "timezone": "America/Chicago", "onWindowClose": "pauseResume" // pauseResume (default) | finishCurrent}- Overnight needs no special flag — a
closetime-of-day earlier thanopenis derived as spanning midnight from the two crons. The whole span can also be one phrase onopen:"open": "between 10pm and 6am"(noclose). onWindowClosegoverns a transfer still running when the window closes:pauseResumepauses it and resumes next window (if the destination supports resume, else it finishes the current file);finishCurrentlets in-flight files finish but admits no new ones until the next open.- DST is handled by evaluating the cron in
timezone(falls back tocomponent.global.defaults.timezone, then UTC). UsedurationMinsinstead ofclosefor a fixed-length window (e.g."durationMins": 480); setting bothcloseanddurationMinsis ambiguous and skips the instance. - For a point trigger instead of a span, use
"mode": "cron"with anexpression(e.g."daily at 2am"), which releases all ready work at each fire.
English phrases (see src/schedule/sugar.rs): hourly, daily, every day at 2am, every 15 minutes,
every 15 minutes on weekdays, every weekday at 8:00, every monday at 7:15, weekly on friday at 17:30, between 9am and 5pm.
Cap upload bandwidth
Section titled “Cap upload bandwidth”There are two independent byte-rate caps; both accept a human rate string like "20MB/s" or "5MB/s":
// per-instance:"limits": { "maxBandwidth": "5MB/s", "maxConcurrentFiles": 4 }
// process-wide aggregate (component.global) — a token bucket EVERY transfer also passes:"component": { "global": { "limits": { "maxBandwidth": "50MB/s", "maxConcurrentFiles": 64 } } }- A transfer is gated by both its instance cap and the global bucket, so the global cap bounds total egress no matter how many instances run.
maxConcurrentFiles(per-instance and thecomponent.globaldefault of 64) bounds in-flight files. When more instances contend for the global slot pool than there are slots, each instance’spriority(default100, lower = admitted first, FIFO among equals) decides admission order — see explanation › Cross-instance priority.prioritydoes not weight the byte-rate; give a class its ownmaxBandwidthif it needs more of the budget.
Choose a readiness strategy
Section titled “Choose a readiness strategy”A newly-seen file may still be mid-write. ingress.readiness.strategy picks how “done” is decided:
| Strategy | When ready | Config |
|---|---|---|
stability (default) |
size + mtime unchanged for quietSecs |
{ "strategy": "stability", "quietSecs": 5 } |
marker |
a companion marker file appears (e.g. FILE.done for FILE) |
{ "strategy": "marker", "suffix": ".done" } |
rename |
the file is renamed/moved into the watch dir (atomic-publish producers) | { "strategy": "rename" } |
glob |
anything not matched by a temp/exclude glob is ready immediately | { "strategy": "glob", "ready": ["**/*.csv"] } |
Use stability for unknown producers, marker/rename for cooperative ones (no quiet-period wait), and
glob when files land complete. Only ready files enter the durable queue. See
explanation › Readiness.
Quarantine files that exhaust their retry budget
Section titled “Quarantine files that exhaust their retry budget”By default an exhausted file is left in place (onExhausted: "retainInPlace", re-tried on the next
trigger). To move failures aside instead, set quarantine with a failedDir:
"completion": { "onSuccess": "delete", "onExhausted": "quarantine", "failedDir": "/data/failed" // required for quarantine}On exhaustion the source is moved to failedDir alongside an .error.json sidecar describing the failure,
and a file-quarantined event (severity critical, context.quarantinePath) is emitted. Quarantined
items also show up in get-status under failed.items[] with state: "quarantined" and a
quarantinedAt. A retry budget is exhausted per retry.giveUpAfter (time) or an optional
retry.maxAttempts (count).
Survive a multi-day disconnection
Section titled “Survive a multi-day disconnection”Two mechanisms tolerate an endpoint being down for hours to days — configure the time budget and lean on resume:
"retry": { "baseDelayMs": 1000, "maxDelayMs": 900000, "giveUpAfter": "7d" }giveUpAfter(default7d) is a time budget, not an attempt cap — the file keeps retrying on thebaseDelayMs→maxDelayMsbackoff across the whole outage and is only given up when the budget elapses.- Resume is automatic: interrupted transfers resume from a persisted checkpoint (S3 multipart parts, ranged-PUT / append / session / staged blocks per backend), so a reconnect after a long gap continues rather than restarting the file.
No circuit-breaker. There is no destination circuit-breaker (staggered reconnects,
Disconnected/Reconnectedalarm events, or aget-statuslinkfield): nodisconnectedevent is emitted andget-statushas nolinkfield. Do not build alerting on those; instead watchreplication-failedevents (each carrieswillRetry: trueandnextAttemptAt) and thefailed/inProgresstallies fromget-status. See explanation › Resilience.
Activate / deactivate an instance from the control plane
Section titled “Activate / deactivate an instance from the control plane”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/plant-csv-to-s3/cmd/set-activationheader.name set-activationbody { "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.
persist: true(the default) writes the override to durable state so it survives restart (runtime state wins over configenabled);persist: falseis a runtime-only flip.reset: true(instead ofactive) clears the persisted override, reverting to configenabled.- A deactivated instance’s forced
triggeris a no-op and its scheduler admits nothing; it still answersget-status. Each transition emits aninstance-activated/instance-deactivatedevent.
Build a realtime UI on the event stream
Section titled “Build a realtime UI on the event stream”There is no retained state snapshot to hydrate from (the UNS state class is reserved to the library’s
RUNNING/STOPPED keepalive). Build a live view by combining two sources:
- Subscribe to the event stream for a device (or the whole fleet):
ecv1/+/FileReplicator/+/evt/#. Apply eachtype(file-ready,replication-started,replication-progress,replication-completed,replication-failed,retries-exhausted,file-archived/file-deleted/file-quarantined, …) to your in-memory model.replication-progresscarriespercent/bytesDone(throttled). - Prime and re-sync with
get-status— call it on connect (and periodically) to get the exactawaiting/inProgress/replicated/faileddocument a late subscriber would otherwise have missed. Keep your own timestamped app-layer cache as the retain substitute.
Both the event context shapes and the get-status document schema are in the
data-types reference.
Monitor replication throughput and backlog
Section titled “Monitor replication throughput and backlog”Route metrics with the standard metricEmission section. With metricEmission.target: "messaging", the
library publishes every metric group to ecv1/{device}/FileReplicator/metric/{metricName}; with
CloudWatch or Prometheus, the same groups and dimensions are used by those targets.
Key groups:
| Question | Metric group / measures |
|---|---|
| Are files completing? | fileReplicator.filesReplicated and bytesReplicated are durable cumulative totals; filesReplicatedInterval and bytesReplicatedInterval are per-completion deltas. |
| Is the source discovering work? | FileReplicatorDiscovery.filesDiscovered, filesReady, scanErrors, permissionDenied, plus scanDurationMs. |
| Is work backing up? | FileReplicatorQueue.queueDepthReady, queueDepthInProgress, queueDepthFailed, oldestQueuedAgeMs, retryBacklog, bytesQueued. |
| Are transfers healthy? | FileReplicatorTransfer.filesStarted, filesReplicated, filesFailed, transferDurationMs, throughputBytesPerSec, retryAttempts, verificationFailures, resumeRecoveries. |
| Is a destination constrained? | FileReplicatorDestination.linkConnected, connectFailures, authFailures, writeFailures, throttleDelayMs, bandwidthLimitBytesPerSec. |
| Is the schedule holding work? | FileReplicatorSchedule.instanceActive, windowOpen, scheduleSkipped, admissionBlocked, filesReleased. |
For CloudWatch, keep dashboards grouped by the published dimensions (instance, destinationType,
result, mode, and readinessStrategy). Do not add filenames, paths, object keys, bucket names, or raw
errors as dimensions; those are intentionally kept in events/status documents, not metrics.
Bridge status to the cloud for fleet-wide visibility
Section titled “Bridge status to the cloud for fleet-wide visibility”Every device publishes under the same fixed UNS grammar
ecv1/{device}/FileReplicator/[{instance}/]{class}[/…] — the instance segment is present only for
per-instance events and absent for component-scope traffic — so a small cloud-side subscription set sees
every device:
ecv1/+/FileReplicator/evt/# # component-scope events (component-ready, scope-"all")ecv1/+/FileReplicator/+/evt/# # per-instance events, every instance, every deviceecv1/+/FileReplicator/state # the library RUNNING/STOPPED keepalive per deviceecv1/+/FileReplicator/metric/# # compatibility and operational metrics{device} is the ThingName (-t), {component} is the short UNS token FileReplicator. There is no
configurable prefix and no legacy alias. To fold status into a fleet dashboard, bridge these topics
northbound and fan get-status requests to each device’s command inbox as needed. Envelope tags (e.g.
enterprise/site) travel in the protobuf envelope for grouping — they are not topic segments. See the
messaging interface reference.