Compare commits

...
6 Commits
Author SHA1 Message Date
deeaitch 6d63457737 Update diagrams links 2026-10-11 09:26:20 -04:00
deeaitch aeeb5fdf3c Conver diagrams to png 2026-10-11 07:41:07 -04:00
deeaitch ba8c8231ad Adding changelog. 2026-10-11 07:32:01 -04:00
deeaitch d401538820 update main .md file with links to simulation diagrams 2026-10-11 07:22:08 -04:00
deeaitch d65910a7c5 diagrams and simulation 2026-10-11 07:21:11 -04:00
deeaitch bf9871eb9e Closes #2 2026-10-11 06:46:48 -04:00
21 changed files with 1128 additions and 5 deletions

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
# Changelog
Reader-facing changes to the articles, architecture decisions, diagrams, and practical examples, newest first. Entries are grouped by commit date and summarize the Git history; issue references are included where recorded in commit messages. File links open the current material, while commit links show the historical changes.
When adding or revising material, add a short entry under the relevant date with links to the affected files and any related issue or commit.
## 2026-10-11
### Storage layout
- Added [chapter 05: Storage Layout](docs/05-storage-layout.md), covering system slots, persistent device data, shared-data compatibility, update and recovery state, storage capacity, and failure scenarios.
- Added [ADR-016: Separate System Software and Persistent Device Data](adr/ADR-016-separate-system-software-and-persistent-device-data.md) and [ADR-017: Keep Recovery State Outside Replaceable Software](adr/ADR-017-keep-recovery-state-outside-replaceable-software.md).
- Updated the [series overview](docs/00-series-overview.md) and the continuation from [chapter 04](docs/04-starting-an-updated-system-or-service.md).
History: [bf9871e](https://git.himro.ca/deeaitch/ota-reference-design/commit/bf9871eb9ee5f122f41750242dda12097a4364ad), which records closing [issue #2](https://git.himro.ca/deeaitch/ota-reference-design/issues/2).
### Diagrams and A/B simulation
- Added three diagrams for chapters 03–05, with editable PlantUML sources and PNG previews:
- OTA architecture: [source](diagrams/plantuml/ota-architecture.puml), [preview](diagrams/rendered/ota-architecture.png).
- A/B trial boot, confirmation, and recovery: [source](diagrams/plantuml/ab-trial-boot.puml), [preview](diagrams/rendered/ab-trial-boot.png).
- A/B storage layout: [source](diagrams/plantuml/ab-storage-layout.puml), [preview](diagrams/rendered/ab-storage-layout.png).
- Added [diagram rendering instructions](diagrams/readme.md) and links from the corresponding chapters.
- Added a [C++17 A/B trial boot simulation](examples/simulations/ab-trial-boot/readme.md) with six scenarios: successful confirmation, failed health checks, exhausted attempts, restart during trial, interrupted preparation, and recovery failure. The example includes saved boot state, [state and scenario checks](examples/simulations/ab-trial-boot/tests.cpp), and [CMake/CTest integration](examples/simulations/ab-trial-boot/CMakeLists.txt). It models logical transitions assuming valid completed writes; power-loss safety is outside its scope.
- Linked the practical material from the [README](readme.md) and [series overview](docs/00-series-overview.md).
History: [d65910a](https://git.himro.ca/deeaitch/ota-reference-design/commit/d65910a7c5157efecf61adf6b0d860845191a687), [d401538](https://git.himro.ca/deeaitch/ota-reference-design/commit/d401538820c2695ae545198642ec8e63e77ff906).
## 2026-10-05
- Added [chapter 04: Starting an Updated System or Service](docs/04-starting-an-updated-system-or-service.md), explaining startup, trial boot, health checks, confirmation, service recovery, and remembering update outcomes.
- Added [ADR-014: Trial Boot and Confirmation](adr/ADR-014-trial-boot-and-confirmation.md) and [ADR-015: Service Checks and Package Downgrade](adr/ADR-015-service-checks-and-package-downgrade.md), and clarified component rollback in [ADR-005](adr/ADR-005-package-based-updates.md).
- Added related ADR links to chapter 04 and updated the [learning path](docs/00-series-overview.md).
- Removed status and date fields from the [ADR template](adr/template.md) and existing decisions that contained them.
History: [eb363ec](https://git.himro.ca/deeaitch/ota-reference-design/commit/eb363ec42d2f5637a7b5c0f1e5ecf9830ff2c5f9) references [issue #1](https://git.himro.ca/deeaitch/ota-reference-design/issues/1); [af37c88](https://git.himro.ca/deeaitch/ota-reference-design/commit/af37c881625aa43e9aa75dd4241ed5ff019d6525), [abe9873](https://git.himro.ca/deeaitch/ota-reference-design/commit/abe9873e7c84e7e85cf14537750722968354d550).
## 2026-09-21
- Added and simplified [chapter 03: OTA System Architecture](docs/03-system-architecture.md), describing server and device responsibilities and the update flow.
- Consolidated the update-check decision into [ADR-011: Device-Initiated Update Checks](adr/ADR-011-device-initiated-update-checks.md). Added heading-only placeholders for [ADR-012: Server-Initiated Updates](adr/ADR-012-server-initiated-updates.md) and [ADR-013: Update Notifications with Device-Initiated Checks](adr/ADR-013-update-notifications-with-device-initiated-checks.md).
- Reworked [chapter 02: Choosing an OTA Update Strategy](docs/02-update_strategies.md) into a shorter comparison of update units and delivery mechanisms.
- Added decisions for [independent application updates](adr/ADR-007-independent-application-updates.md), [binary delta delivery](adr/ADR-008-binary-delta-delivery.md), [versioned filesystem trees](adr/ADR-009-versioned-filesystem-trees.md), and [hybrid strategies](adr/ADR-010-hybrid-update-strategies.md).
History: [98edac4](https://git.himro.ca/deeaitch/ota-reference-design/commit/98edac44538b42635feac2a5c9f153dc22439c41), [252a7e4](https://git.himro.ca/deeaitch/ota-reference-design/commit/252a7e4c273fdd9583950d20438fed87fcc812fa), [572d1f7](https://git.himro.ca/deeaitch/ota-reference-design/commit/572d1f7acc776d023b8d4c214b4bad52e062e918), [3c94145](https://git.himro.ca/deeaitch/ota-reference-design/commit/3c941454910a358c7978fc1c196051edc33ab552), [13a88b7](https://git.himro.ca/deeaitch/ota-reference-design/commit/13a88b7270f2c8bd825f170fab72c60e2190564b), [931efea](https://git.himro.ca/deeaitch/ota-reference-design/commit/931efeabc73152d2315cb8674da78ca932cf34ba), [1ebebb8](https://git.himro.ca/deeaitch/ota-reference-design/commit/1ebebb86e27e8881f53131f332c1a6b832f58c71).
## 2026-08-03
- Added the first version of [chapter 02: Choosing an OTA Update Strategy](docs/02-update_strategies.md).
- Documented [full-system image updates](adr/ADR-004-full-system-image-updates.md), [package-based updates](adr/ADR-005-package-based-updates.md), and [container-based updates](adr/ADR-006-container-based-updates.md) as architecture decisions.
- Revised the strategy discussion to cover application-level updates through package-based updates at that stage of the series.
- Added the [guide to architecture decision records](adr/readme.md).
History: [2b49cbe](https://git.himro.ca/deeaitch/ota-reference-design/commit/2b49cbe01003276841ad510a546b2831a0136d4b), [7a74d9f](https://git.himro.ca/deeaitch/ota-reference-design/commit/7a74d9fca35897db26840b4bc03a833d5b0e75a4), [155cca5](https://git.himro.ca/deeaitch/ota-reference-design/commit/155cca5eeb3a66f71ee1f58a793a0d4fece4994f), [be12197](https://git.himro.ca/deeaitch/ota-reference-design/commit/be12197ac07d7c9ca57089ec887ba6280edd0067), [37583d8](https://git.himro.ca/deeaitch/ota-reference-design/commit/37583d820a98d91aed763a57bbc2917e99817173), [2da217c](https://git.himro.ca/deeaitch/ota-reference-design/commit/2da217c0cefd736ed2485a77ff850b4c66294b47).
## 2026-07-29
- Started the repository with the [series overview](docs/00-series-overview.md) and [chapter 01: What Is an OTA Update?](docs/01-what-is-an-ota-update.md).
- Established the [repository name](adr/ADR-001-repository-name.md), [MIT license](adr/ADR-002-license.md), and [educational reference-design scope](adr/ADR-003-reference-design.md) in the first architecture decisions.
- Added the [ADR template](adr/template.md), organized decisions under `adr/`, and expanded the [README](readme.md) to explain the project.
History: [initial commit](https://git.himro.ca/deeaitch/ota-reference-design/commit/eae04d851bb8d461909cd26def0f52581657cda6), [series overview](https://git.himro.ca/deeaitch/ota-reference-design/commit/7a5e040c38d7c31d53cf0aaf719163ef7f93128d), [OTA introduction](https://git.himro.ca/deeaitch/ota-reference-design/commit/5b8566e69c91b0634c23b368305e3c14d41dfc24), [README update](https://git.himro.ca/deeaitch/ota-reference-design/commit/556163a92b18969d7210f460dc0cf918d8f25653).
@@ -0,0 +1,74 @@
# ADR-016: Separate System Software and Persistent Device Data
## Context
This example uses system images and an A/B layout. The device keeps the current system while preparing a new version in the other slot.
An image installer can replace all contents of its target area. Device identity, calibration, settings, and application data must survive that operation. Both retained system versions may need these files.
The device has enough storage for two system versions and a separate persistent data area. The product can define which data formats the retained versions support.
## Decision
Store system software in the system slots. Store device identity, calibration, device settings, and application data outside the areas replaced by ordinary system image updates.
Keep the running system files read-only in this example. Programs write changing files to the persistent data area. The platform defines the locations and access rules for those files.
An image may include default configuration. Device settings are stored separately, with a defined rule for how the application combines them with the defaults. A normal system release must not overwrite device identity or calibration.
Share persistent data between the slots only when the supported recovery versions can use it. Before changing a data format, define and test compatibility or a restore path. If recovery uses a saved copy, define what happens to data written after that copy was made.
The product must state when returning to an older system stops being supported. Confirmation alone does not prove that later data changes remain compatible with that system.
## Decision Drivers
- Installing a system image must not erase information that belongs to the device.
- Returning to the previous system must preserve a usable data path.
- The boundary between release files and changing device files must be clear.
- Normal application writes should not change the system software under trial.
## Alternatives Considered
### Keep Device Data Inside Each System Image
This is a simple layout, but replacing an image would erase the data unless the installer copies it elsewhere first. The copying and restore steps would need their own failure handling.
### Keep a Separate Data Copy for Each Slot
This can help isolate data changes during a trial. It needs more storage and rules for copying data, choosing the active copy, and handling new records after a return to the old slot. It may be appropriate when shared data cannot support both versions.
### Keep One Writable System With All Data
This can suit package-based updates. It does not meet this example's requirement to prepare and replace a complete system image while retaining the current system.
## Consequences
### Positive
- System image replacement preserves device-specific files.
- New application records can remain available after a return to the previous system.
- The areas that the image installer may change are easier to define.
### Negative
- The platform must arrange paths and access to persistent files.
- Shared data needs compatibility rules for retained software versions.
- A shared data failure can affect both slots.
- Copies or backups used for data recovery need extra storage and cleanup rules.
## Applicability and Limits
Use this approach when system software is replaced as an image and device data must survive that replacement.
Separation may use partitions or other storage areas outside the installer's target. A directory inside a partition being overwritten is not sufficient.
This choice does not protect against failure of the whole storage device. It also does not make data writes safe under power loss or make migrations reversible. Those need separate mechanisms and tests.
Key storage and access protection need a security design. Placing identity files outside a system slot is only a rule about their lifetime.
## Related Documents
- [Chapter 05: Storage Layout](../docs/05-storage-layout.md)
- [ADR-004: Full-System Image Updates](ADR-004-full-system-image-updates.md)
- [ADR-014: Trial Boot and Confirmation for System Updates](ADR-014-trial-boot-and-confirmation.md)
- [ADR-017: Keep Recovery State Outside Replaceable Software](ADR-017-keep-recovery-state-outside-replaceable-software.md)
@@ -0,0 +1,74 @@
# ADR-017: Keep Recovery State Outside Replaceable Software
## Context
Trial boot and service downgrade both depend on saved state. After a restart, the device needs to know which version is under trial, whether it was confirmed, and whether recovery is in progress.
If that state exists only inside the software being replaced, installation or recovery may erase it. If boot control cannot read it before Linux starts, it cannot use it to handle an early startup failure.
This example assumes that the platform provides storage accessible to boot control and persistent storage accessible to the OTA manager or service supervisor. These may be different areas.
## Decision
Keep state required for recovery outside the software areas that the relevant update or recovery operation replaces.
Keep boot selection, trial attempts, and confirmation in a location and format supported by boot control. The Linux component that requests a trial or confirms the system must use the platform's defined boot control interface.
Keep installation progress, service recovery progress, failed version blocks, and pending reports in persistent storage accessible to the OTA manager or supervisor. Package installation, removal scripts, and cleanup must preserve these records while they are needed.
Do not require one common file for every reader. Define who writes each record and how the readers agree on its meaning. Both retained software versions must understand any state they need after recovery.
Use a storage method that handles interrupted writes. Readers must detect invalid or incomplete records. The platform must define a recovery action when no valid record remains; unreadable state must not be interpreted as successful confirmation.
Define when each record may be removed. A report may be removed after its delivery is acknowledged. A failed version block follows the explicit removal policy in ADR-015. Recovery progress must remain available until the recorded operation has reached a known outcome.
## Decision Drivers
- Restarting or replacing software must not erase the information needed to recover.
- Boot control must make decisions before the OTA manager starts.
- Offline recovery must retain progress and results locally.
- Interrupted state writes must have a defined outcome.
## Alternatives Considered
### Store State Only Inside the Target System Slot
Replacing that slot may erase the state. Boot control may also be unable to read it when the new system fails. This does not meet the recovery requirements.
### Store All State in the Application's Data Files
This may be convenient for reporting. It is unsuitable when boot control cannot read those files or when package operations can remove them. Separate records are used according to their readers and lifetime.
### Keep the Authoritative State Only on the Server
The server can track deployment progress, but it may be unreachable during boot or recovery. The device needs local state to complete those steps while offline.
## Consequences
### Positive
- Boot and service recovery retain the information they need across replacement and restart.
- Each reader has state it can access at the required time.
- Reports can wait locally for the server connection to return.
### Negative
- The platform needs tested methods for saving and reading state under power loss.
- Boot control and Linux must agree on state formats and ownership.
- Multiple records may need coordination if an operation is interrupted between writes.
- Persistent storage needs space limits and retention rules.
## Applicability and Limits
Use this approach when recovery decisions depend on state that must survive software replacement or an interrupted operation.
This ADR defines where state belongs and who must read it. It does not select a filesystem, record format, or bootloader command. Those choices belong in the platform example and must be tested under power loss.
Separating state from software does not guarantee that the state is valid. It also does not make several writes one atomic operation. The update design must handle the intermediate states those writes can leave behind.
## Related Documents
- [Chapter 05: Storage Layout](../docs/05-storage-layout.md)
- [ADR-014: Trial Boot and Confirmation for System Updates](ADR-014-trial-boot-and-confirmation.md)
- [ADR-015: Service Checks and Package Downgrade](ADR-015-service-checks-and-package-downgrade.md)
- [ADR-016: Separate System Software and Persistent Device Data](ADR-016-separate-system-software-and-persistent-device-data.md)
+43
View File
@@ -0,0 +1,43 @@
@startuml
title A/B storage: logical areas and access responsibilities
left to right direction
skinparam shadowing false
skinparam monochrome true
actor "Installer" as installer
actor "Boot control" as boot
actor "OTA manager /\nconfirmation program" as manager
actor "Running system\nand applications" as runtime
rectangle "Device storage (logical areas, not a partition table)" {
rectangle "Shared boot components\nSeparate update and recovery method" as shared
database "Boot state\nNormal slot, trial target, attempts, confirmation" as state
rectangle "Slot A: retained system\nKernel + device tree + root filesystem" as A
rectangle "Slot B: update target\nKernel + device tree + root filesystem" as B
database "Persistent device data" as data {
rectangle "Identity and calibration\nRestricted access" as identity
rectangle "Configuration and application data\nMust support the retained version" as appdata
rectangle "OTA progress, failure blocks\nand pending reports" as progress
}
rectangle "Download and work area\nDisposable preparation files" as work
}
installer --> B : replace inactive slot
installer --> work : use temporary files
boot --> shared : execute
boot --> state : read / save before Linux starts
boot --> A : select matching components
boot --> B : select matching components
manager --> state : request trial / confirm\nthrough platform interface
manager --> progress : read / save
runtime --> appdata : read / write
runtime --> identity : authorized access only
legend bottom
A is running while B is prepared. Preserve A, boot state and device data.
Persistent device data survives replacement of either system slot.
Shared data is not rolled back by selecting A.
Both system versions must understand the recovery records they read.
Separate logical areas do not guarantee safe writes or whole-device recovery.
endlegend
@enduml
+52
View File
@@ -0,0 +1,52 @@
@startuml
title A/B trial boot and recovery
skinparam shadowing false
skinparam monochrome true
state "A confirmed" as A
state "Prepare B\nA remains the normal boot choice" as Preparing
state "B prepared and verified" as Prepared
state "B trial pending\nSaved target and attempt limit" as Pending
state "B trial running\nUnconfirmed" as Trial
state "B confirmed" as Confirmed
state "Recovery to A pending" as Recovering
state "A recovery running" as Recovery
state "A recovery checked" as Recovered
state "Separate recovery required" as Failed
[*] --> A
A --> Preparing : begin installation
Preparing --> Preparing : reset during preparation; boot A\nB is not eligible for trial
Preparing --> A : abandon incomplete or failed preparation
Preparing --> Prepared : installation and verification complete
Prepared --> Pending : save trial request through boot control
Pending --> Trial : reset [attempts left > 0]; save attempts left - 1\nbefore handing control to B
Trial --> Confirmed : local checks pass within time limit;\nsave confirmation
Trial --> Recovering : local checks fail; save recovery request
Trial --> Pending : reset before confirmation\n(power loss or covered watchdog)
Pending --> Recovering : reset; no attempts left
Recovering --> Recovery : boot control selects A on reset
Recovery --> Recovered : local recovery checks pass;\nsave outcome
Recovery --> Failed : recovery checks fail or startup fails\n(platform recovery policy)
Confirmed --> [*]
Recovered --> [*]
Failed --> [*]
note right of Pending
Boot state is outside A and B.
Attempts are consumed before starting B.
Invalid records require a platform-defined
recovery path, never implicit confirmation.
end note
note right of Recovery
A must still be compatible with shared data.
Do not endlessly alternate failed systems.
end note
legend bottom
Preparation interruption assumes the installer preserves A and boot state.
State transitions assume valid durable records; torn writes need platform testing.
Checks and recovery are local; reports wait for connectivity.
endlegend
@enduml
+38
View File
@@ -0,0 +1,38 @@
@startuml
title OTA roles and device boundary
left to right direction
skinparam shadowing false
skinparam monochrome true
skinparam packageStyle rectangle
package "Release and deployment services" {
component "Build, test and publish" as release
component "Artifact storage" as artifacts
component "Deployment management" as deployment
}
package "Device" {
component "OTA manager" as manager
component "Installer" as installer
component "Boot control" as boot
component "Local health checks" as checks
database "Persistent update progress\nand pending reports" as progress
}
release --> artifacts : publish artifact and\nverification information
release --> deployment : register release and compatibility
manager --> deployment
manager --> installer : authorize preparation
installer --> artifacts : download and verify
manager --> boot : request trial through\nplatform interface
checks --> boot : confirm after local checks pass\nor request recovery after failure
manager --> progress : save / resume progress;\nqueue reports while offline
manager --> checks : arrange checks after activation
legend bottom
OTA manager -> deployment management: request releases; report running version and outcome.
Boot control runs before the OTA manager and handles exhausted trial attempts.
Hangs need a covered reset path. Checks and recovery do not require the server.
Roles may be combined in one tool. This view uses a system update with trial boot.
endlegend
@enduml
+17
View File
@@ -0,0 +1,17 @@
# OTA diagrams
PlantUML sources accompany chapters 03–05. The diagrams describe the illustrative A/B design, not a required platform or partition table.
| View | Source | Preview |
|---|---|---|
| Roles and the server/device boundary | [PlantUML](plantuml/ota-architecture.puml) | [PNG](rendered/ota-architecture.png) |
| Trial boot, confirmation and recovery | [PlantUML](plantuml/ab-trial-boot.puml) | [PNG](rendered/ab-trial-boot.png) |
| Storage areas and access responsibilities | [PlantUML](plantuml/ab-storage-layout.puml) | [PNG](rendered/ab-storage-layout.png) |
From the repository root, regenerate previews with a local PlantUML installation and Graphviz:
```sh
JAVA_TOOL_OPTIONS=-Djava.awt.headless=true plantuml -tpng -o ../rendered diagrams/plantuml/*.puml
```
Keep sources and previews together when changing a diagram. No external rendering service is needed. State transitions assume valid durable records; these pictures do not establish power-loss safety. The [A/B simulation](../examples/simulations/ab-trial-boot/readme.md) explores the same logical transitions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+10 -4
View File
@@ -71,7 +71,7 @@ The emphasis is always on understanding the underlying design rather than memori
## Repository Structure
The structure below shows the planned repository. The `diagrams/` and `examples/` directories will be added as the practical material develops.
The structure below includes the current documentation, PlantUML diagrams, and A/B simulation. Hardware demonstrations and later chapters will be added as the practical material develops.
```text
ota-reference-design/
@@ -82,15 +82,16 @@ ota-reference-design/
│ ├── 02-update_strategies.md
│ ├── 03-system-architecture.md
│ ├── 04-starting-an-updated-system-or-service.md
│ ├── 05-storage-layout.md
│ ├── ...
│
├── diagrams/
│ └── plantuml/
│ ├── plantuml/
│ └── rendered/
│
├── examples/
│ ├── raspberry-pi/
│ ├── qemu/
│ └── simulations/
│ └── ab-trial-boot/
│
├── adr/
│ ├── ADR-001-repository-name.md
@@ -110,6 +111,9 @@ The chapters are designed to build on each other. The current chapters are:
- [02. Choosing an OTA Update Strategy](02-update_strategies.md)
- [03. OTA System Architecture](03-system-architecture.md)
- [04. Starting an Updated System or Service](04-starting-an-updated-system-or-service.md)
- [05. Storage Layout](05-storage-layout.md)
To catch up on additions and revisions, see the [changelog](../CHANGELOG.md), with dated summaries and links to chapters, diagrams, simulations, and related history.
The learning path below also includes planned chapters.
@@ -194,6 +198,8 @@ Where possible, theoretical discussions are accompanied by practical material, i
The objective is to connect high-level architecture with practical implementation.
Available now: [PlantUML diagrams and PNG previews](../diagrams/readme.md) and a [C++ A/B trial boot simulation](../examples/simulations/ab-trial-boot/readme.md). Raspberry Pi and QEMU demonstrations are planned.
---
## Intended Audience
+6
View File
@@ -45,6 +45,12 @@ Alongside the update files, it needs a few details: which version this is, which
These are roles, not a shopping list of separate services. One tool may cover several of them. What matters is knowing who handles each step.
The following view shows the roles and the server/device boundary for the system-update example used in the next chapter.
![OTA roles and device boundary](../diagrams/rendered/ota-architecture.png)
[PlantUML source](../diagrams/plantuml/ota-architecture.puml)
## 3. One Update, Start to Finish
Imagine a device running version 1. Version 2 is ready. In this example, the device can prepare the new system while keeping the previous one available for recovery.
@@ -99,6 +99,12 @@ Returning to a previous version is called **rollback**. Keeping A available make
[ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) explains when to choose this policy and what the platform must support.
The state diagram separates an explicit health-check failure from a startup failure that prevents the checks from running. It also shows the checks after returning to A.
![A/B trial boot and recovery](../diagrams/rendered/ab-trial-boot.png)
[PlantUML source](../diagrams/plantuml/ab-trial-boot.puml) · [Runnable simulation and failure scenarios](../examples/simulations/ab-trial-boot/readme.md)
## 5. Example: Starting an Updated Service
Now imagine a device with a stable system platform and an independently updated communication service. Version 1 works. Version 2 arrives as a package.
@@ -194,4 +200,4 @@ These Architecture Decision Records (ADRs) explain when to choose the approaches
Both examples follow the same pattern: start the new version, check its required functions, then keep it or recover. They use different mechanisms because a system boot and a service restart fail in different ways.
Next, we will look at storage layout: where software versions, device data, and update state live, and what must remain available when an update fails.
Next, we will look at [storage layout](05-storage-layout.md): where software versions, device data, and update state live, and what must remain available when an update fails.
+220
View File
@@ -0,0 +1,220 @@
# 05. Storage Layout: Software Versions, Persistent Data, and Update State
The previous chapter followed the startup of a new system or service. It also showed why the device may need to return to an older version.
That return is only possible if the needed software, data, and update state are still available.
A **storage layout** describes where these things live. It also defines which parts an update may change. We will use the same two examples: an A/B system update and an independent service package update.
## 1. Different Files Have Different Jobs
Before choosing partitions or directories, separate the contents by purpose.
| Contents | Example | What an update must preserve |
|---|---|---|
| System software | Kernel, system libraries, services | A usable previous version during a trial |
| Device identity and calibration | Device keys, sensor calibration | The information that belongs to this device |
| Configuration | Network settings, application settings | Settings needed by the versions that may run |
| Application data | Measurements, a local database | Data needed for operation and recovery |
| Update state | Trial target, confirmation, failure block | Enough information to choose the next safe action |
| Recovery files | Previous service package and verification information | The files needed to restore the previous service |
| Temporary files | Partial downloads, work files | Nothing essential; incomplete work may be discarded |
These groups do not all need separate partitions. A **partition** is a defined region of a storage device. Several groups may use different directories in one partition.
The important question is what an operation can replace or erase. If the installer writes a complete partition image, a separate directory inside that partition will not protect the files in it.
## 2. An Example A/B Layout
In this example, each system slot holds a matching kernel, device tree, and root filesystem. The boot control mechanism selects them together.
The device tree describes hardware for the kernel. The root filesystem holds the system files Linux needs. Some platforms store these parts differently. The example shows their roles, rather than a required partition table.
```text
Device storage: logical areas
|
+-- Shared boot components
| `-- Code needed to select and load a system
|
+-- Boot state
| `-- Trial target, attempts left, confirmation
|
+-- System slot A
| `-- Kernel A + device tree A + root filesystem A
|
+-- System slot B
| `-- Kernel B + device tree B + root filesystem B
|
+-- Persistent device data
| +-- Device identity and calibration
| +-- Configuration and application data
| `-- OTA progress, failure blocks, pending reports
|
`-- Download and work area
`-- Temporary files used to prepare an update
```
**Persistent** means that information remains after a restart. Here, persistent device data also remains when either system slot is replaced.
Suppose A is running and B will receive the next version. The installer may replace B. It must preserve A and the data needed to start and use A again.
The running system in this example keeps its system files read-only. Files that change during normal operation go into the persistent data area. A read-only filesystem prevents ordinary writes to those system files. It does not protect them from every failure or from an installer that writes directly to the storage device.
The layout still has shared parts. If an update damages the shared boot components, neither slot may start. Updating those components needs its own safe installation and recovery method.
Two slots on one storage device also do not protect against failure of that whole device.
The following diagram adds access responsibilities to the logical layout. A is running while the installer prepares B. Trial requests and confirmation use the platform's boot control interface.
![A/B storage areas and access responsibilities](../diagrams/rendered/ab-storage-layout.png)
[PlantUML source](../diagrams/plantuml/ab-storage-layout.puml) · [Simulation of saved boot state across resets](../examples/simulations/ab-trial-boot/readme.md)
## 3. What Lives Outside the System Slots?
A system release provides common software. Device identity and calibration belong to an individual device. Installing a system image must not replace them with values from the release build.
Configuration has two possible sources. The system image can provide default settings. Settings chosen for this device live outside the image. The design must explain how the application uses both.
Application data also lives outside the replaceable system slots in this example. Updating the system should not erase stored measurements or a local database.
Separation does not mean that every program may write every file. The platform must define access rules. For example, an ordinary application should not be able to replace the device's identity keys.
[ADR-016: Separate System Software and Persistent Device Data](../adr/ADR-016-separate-system-software-and-persistent-device-data.md) explains the conditions for this choice.
## 4. Shared Data Must Work With Both Versions
Both A and B can use the same persistent data. This saves space and keeps new measurements available after a return to A. It also creates a compatibility requirement.
Imagine this sequence:
```text
A uses database format 1
|
B starts and changes the database to format 2
|
B fails its health checks
|
The device returns to A
|
Can A still read the database?
```
If the answer is no, keeping A was not enough.
A **data migration** changes stored data into a new format. Before allowing one, define how recovery will work. Possible approaches include:
- Keep a format that both versions can use during the trial.
- Prepare a separate copy for the new version and keep the old copy for recovery.
- Save a backup and use a tested restore procedure.
- Delay a change that the old version cannot read until the device no longer promises a return to it.
Each approach has a cost. Copies and backups need space. Restoring old data may lose measurements or settings written after the copy was made. Delaying a migration only helps if the new version can work before the change.
Configuration needs the same care. A new setting may be harmless to an old application, or it may stop that application from starting.
The product must define how long the previous version remains a supported recovery choice. Passing health checks does not make every later data change safe for that version.
## 5. Update State Must Be Available to Its Readers
Chapter 04 introduced saved trial attempts, confirmation, recovery progress, and failure blocks. These records have different readers.
| State | Who needs it? | When must it be available? |
|---|---|---|
| Selected system and trial attempts | Boot control | Before the selected Linux system starts |
| System confirmation | Boot control and the program that confirms the system | During confirmation and on later boots |
| Installation and recovery progress | OTA manager or service supervisor | After a restart or interrupted operation |
| Failed package version block | Update policy owner | Before another automatic installation |
| Pending result report | OTA manager | When reporting can resume |
Putting all these records in one file is not always possible. Boot control may not be able to read the filesystem used by the OTA manager. The platform must choose a location and format each reader supports.
State needed to recover from replacing B must not exist only inside B. State needed by both systems must remain available when either slot is replaced.
Saving state also needs a method that handles power loss. After an interrupted write, the reader must recognize a valid record or follow a defined recovery path. The platform must define what happens when no valid record remains. It must not treat unreadable state as proof that the new version was confirmed.
Boot control and Linux must also agree on what the records mean. Both retained system versions need to understand the state they may read during recovery.
These requirements are described in [ADR-017: Keep Recovery State Outside Replaceable Software](../adr/ADR-017-keep-recovery-state-outside-replaceable-software.md). The next chapter will look more closely at safe writes and activation.
## 6. Storage for an Independent Service Update
The package example uses a stable system platform. The service files may be replaced in the running filesystem. It does not gain A/B protection just because its data is stored separately.
A possible layout is:
```text
Stable system platform
|
+-- Installed service files
|
+-- Service configuration and data
|
+-- Local recovery files
| `-- Exact previous package + verification information
|
+-- Supervisor state
| `-- Update progress, failed version block, pending report
|
`-- Download and work area
`-- New package and temporary files
```
The supervisor must remain able to run when the service fails. Its recovery files and state must survive the package operations it controls. They must not be deleted by the service's removal scripts or by ordinary cache cleanup.
Keeping the previous package locally allows recovery while offline. The device must also retain any dependencies and installation inputs needed for the supported downgrade. A single package file may not be enough.
Before installing version 2, check that version 1 can still use the resulting configuration and data. Package installation scripts may change these files even before the service starts.
[ADR-015: Service Checks and Package Downgrade](../adr/ADR-015-service-checks-and-package-downgrade.md) defines the recovery policy for this example.
## 7. Plan for Space and Cleanup
The update needs more space than the final installed software alone. Depending on the method, it may need:
- The running version and the prepared new version.
- A downloaded artifact and files used during installation.
- Previous packages and their required dependencies.
- A data copy or backup used for recovery.
- Saved state, reports, and normal application data.
Some installers write verified download data directly into an inactive slot. Others keep a complete download first. The storage plan must match the chosen method.
Check the available space before starting. Normal services may keep writing data during an update, so the design also needs limits or reserved space for essential state. Logs and downloads must not consume all space needed for recovery records.
Cleanup needs clear rules. A partial download can usually be removed or downloaded again. A previous package needed for recovery cannot be treated as an ordinary disposable cache file.
For A/B, the inactive slot may become the target of the next update after the current system is confirmed. Replacing it removes that older recovery version. The update policy must make this change deliberately.
Never delete the only usable recovery files to make room for an unconfirmed update.
## 8. Check the Layout Against Failures
For each operation, ask which areas it can change and what remains usable if it stops halfway.
| Failure | What this design needs |
|---|---|
| Power is lost while writing B | A and the information needed to select it remain usable. B is not selected as a ready system. |
| Power is lost while saving trial state | Boot control detects a valid record or follows its defined recovery path. |
| B changes shared data and then fails | A can use the data, or a tested data recovery path is available. |
| The download area fills up | The update stops safely; essential state can still be saved. |
| Service installation is interrupted | The supervisor can read its progress and follow the package manager's recovery rules. |
| Cleanup runs during service recovery | Required packages, verification information, and recovery state are retained. |
| Shared boot code or the storage device fails | A separate recovery method is needed; two system slots alone are insufficient. |
This table describes requirements. A storage diagram does not prove they are met. Practical examples will need to test interrupted writes, installation, startup, and recovery.
## 9. Related Architecture Decisions
| Question | Further reading |
|---|---|
| When should a system get a trial boot? | [ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) |
| How is a failed service package restored? | [ADR-015: Service Checks and Package Downgrade](../adr/ADR-015-service-checks-and-package-downgrade.md) |
| When should device data be separate from system software? | [ADR-016: Separate System Software and Persistent Device Data](../adr/ADR-016-separate-system-software-and-persistent-device-data.md) |
| Where should recovery state live? | [ADR-017: Keep Recovery State Outside Replaceable Software](../adr/ADR-017-keep-recovery-state-outside-replaceable-software.md) |
## 10. What Comes Next?
A useful storage layout makes the boundaries clear: what the update replaces, what it preserves, and what recovery can still read.
Next, we will look at atomic updates. We will ask how to prepare and activate a new version without exposing an incomplete one, and how the device makes a safe decision after an interrupted operation.
@@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.20)
project(ab_trial_boot LANGUAGES CXX)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(ab-simulation simulation.cpp)
target_compile_features(ab-simulation PUBLIC cxx_std_17)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(ab-simulation PRIVATE -Wall -Wextra -Wpedantic)
endif()
add_executable(ab-simulate main.cpp)
target_link_libraries(ab-simulate PRIVATE ab-simulation)
include(CTest)
if(BUILD_TESTING)
add_executable(ab-simulation-tests tests.cpp)
target_link_libraries(ab-simulation-tests PRIVATE ab-simulation)
add_test(NAME recovery-invariants COMMAND ab-simulation-tests)
add_test(NAME all-scenarios COMMAND ab-simulate all)
add_test(NAME cli-help COMMAND ab-simulate --help)
add_test(NAME cli-invalid-scenario COMMAND ab-simulate invalid)
set_tests_properties(cli-invalid-scenario PROPERTIES WILL_FAIL TRUE)
endif()
@@ -0,0 +1,30 @@
#include "simulation.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
int main(int argc, char* argv[]) {
const std::string selected = argc > 1 ? argv[1] : "all";
if (argc > 2 || selected == "--help" || selected == "-h" ||
(selected != "all" && std::find(ota::scenarios().begin(), ota::scenarios().end(),
selected) == ota::scenarios().end())) {
std::ostream& output = (argc == 2 && (selected == "--help" || selected == "-h"))
? std::cout : std::cerr;
output << "Usage: ab-simulate [all";
for (const auto& scenario : ota::scenarios()) output << '|' << scenario;
output << "]\n";
return &output == &std::cout ? 0 : 2;
}
try {
for (const auto& scenario : ota::scenarios()) {
if (selected != "all" && selected != scenario) continue;
std::cout << "\n=== " << scenario << " ===\n";
ota::TemporaryDirectory directory;
ota::run_scenario(scenario, directory.path(), &std::cout);
}
} catch (const std::exception& error) {
std::cerr << "Simulation failed: " << error.what() << '\n';
return 1;
}
}
@@ -0,0 +1,43 @@
# A/B trial boot simulation
This C++17 standard-library example follows the [trial boot diagram](../../../diagrams/plantuml/ab-trial-boot.puml), [chapter 04](../../../docs/04-starting-an-updated-system-or-service.md), and [ADR-014](../../../adr/ADR-014-trial-boot-and-confirmation.md). It illustrates boot decisions; it does not install software or access a device. No third-party libraries are required.
Build and run from the repository root with CMake 3.20 or later and a C++17 compiler. These commands use a Linux temporary build directory to keep generated files outside the repository:
```sh
cmake -S examples/simulations/ab-trial-boot -B /tmp/ota-ab-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/ota-ab-build
/tmp/ota-ab-build/ab-simulate all
/tmp/ota-ab-build/ab-simulate attempts-exhausted
ctest --test-dir /tmp/ota-ab-build --output-on-failure
```
| Scenario | Expected result |
|---|---|
| `success` | Checks pass; B is confirmed and selected on the next boot. |
| `health-failure` | B fails checks; reset selects A; recovery checks pass. |
| `attempts-exhausted` | B never reaches checks; two unconfirmed attempts lead to A. |
| `restart-during-trial` | A reset preserves the consumed attempt; the second trial passes. |
| `interrupted-preparation` | Incomplete B remains ineligible; reset selects A. |
| `recovery-failure` | A also fails; stop at the boundary of a separate recovery policy. |
With no argument, `ab-simulate` runs all scenarios. Use `--help` to list the accepted arguments. Invalid arguments return a nonzero exit status.
The attempt limit of two is an illustrative choice. Each reset reconstructs the device from a versioned text record (`boot-state.txt`) in a temporary directory. The running slot is volatile. The saved normal slot stays A until B is confirmed. Boot control saves each consumed attempt before returning B as the selected system. A failed health check requests recovery immediately rather than spending the remaining trial attempts.
The example keeps boot state outside the simulated slots, as shown in the [storage diagram](../../../diagrams/plantuml/ab-storage-layout.puml). Each scenario uses a separate temporary directory, removed when it finishes. The model covers one A-to-B update; it does not model subsequent releases.
## Assumptions and limits
Installation and artifact verification are represented by one preparation operation. Health checks are supplied boolean outcomes, including a timeout represented as failure. Resets are explicit calls; watchdog coverage and real startup timing are not simulated. Recovery assumes that A and shared data remain usable, unless the recovery-failure scenario supplies a failed check.
The text file is a teaching stand-in for boot state, using standard C++ streams and `std::filesystem`. It records the phase, normal slot, optional trial slot, attempts remaining, slot readiness, and outcome. Replacement uses a temporary file and rename, but does not flush storage, implement redundant records, or recover invalid records. Unreadable records stop the simulation with an error; they are never treated as confirmation. This is not a power-loss-safe boot-state implementation. The simulation assumes completed valid writes; it does not test interruption within a write, coordination with OTA progress, data migration, signatures, bootloader integration, or hardware failure. These need platform-specific implementation and failure-injection tests.
Outcomes remain local in the model. There is no server dependency or reporting transport. A real implementation must retain pending reports separately and send them when connectivity returns.
## Source and checks
- [simulation.hpp](simulation.hpp) and [simulation.cpp](simulation.cpp): typed state model, saved records, reset behavior, and scenarios.
- [main.cpp](main.cpp): command-line runner.
- [tests.cpp](tests.cpp): scenario outcomes and invariants for saved attempts, incomplete slots, confirmation, and unreadable state. Checks remain enabled in Release builds.
- [CMakeLists.txt](CMakeLists.txt): builds the runner and registers the invariant and CLI checks with CTest.
@@ -0,0 +1,233 @@
#include "simulation.hpp"
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <random>
#include <stdexcept>
#include <utility>
namespace ota {
namespace {
constexpr const char* phase_names[] = {
"A confirmed", "Prepare B", "B prepared and verified", "B trial pending",
"B trial running", "B confirmed", "Recovery to A pending",
"A recovery running", "A recovery checked", "Separate recovery required"
};
Phase read_phase(const std::string& value) {
for (int i = 0; i < 10; ++i) {
if (value == phase_names[i]) return static_cast<Phase>(i);
}
throw std::runtime_error("Invalid saved phase");
}
Slot read_slot(const std::string& value) {
if (value == "A") return Slot::A;
if (value == "B") return Slot::B;
throw std::runtime_error("Invalid saved slot");
}
void require(bool condition, const char* message) {
if (!condition) throw std::logic_error(message);
}
} // namespace
const char* name(Phase phase) { return phase_names[static_cast<int>(phase)]; }
const char* name(Slot slot) { return slot == Slot::A ? "A" : "B"; }
Device::Device(const std::filesystem::path& directory)
: path_(directory / "boot-state.txt") {
if (!std::filesystem::exists(path_)) {
save();
return;
}
std::ifstream input(path_);
std::string header, phase, normal, trial;
int a_ready = 0, b_ready = 0;
if (!(input >> header >> std::quoted(phase) >> normal >> trial
>> state_.attempts_left >> a_ready >> b_ready
>> std::quoted(state_.outcome)) || header != "OTA_AB_STATE_V1") {
throw std::runtime_error("Unreadable saved boot state");
}
input >> std::ws;
if (!input.eof() || state_.attempts_left < 0 ||
(a_ready != 0 && a_ready != 1) || (b_ready != 0 && b_ready != 1)) {
throw std::runtime_error("Invalid saved boot state");
}
state_.phase = read_phase(phase);
state_.normal = read_slot(normal);
if (trial != "none") state_.trial = read_slot(trial);
state_.a_ready = a_ready != 0;
state_.b_ready = b_ready != 0;
}
void Device::save() const {
auto temporary = path_;
temporary += ".tmp";
std::ofstream output(temporary, std::ios::trunc);
output << "OTA_AB_STATE_V1\n" << std::quoted(name(state_.phase)) << '\n'
<< name(state_.normal) << '\n'
<< (state_.trial ? name(*state_.trial) : "none") << '\n'
<< state_.attempts_left << '\n'
<< state_.a_ready << ' ' << state_.b_ready << '\n'
<< std::quoted(state_.outcome) << '\n';
output.close();
if (!output) throw std::runtime_error("Cannot save boot state");
// Teaching stand-in: rename alone does not guarantee power-loss durability.
std::filesystem::rename(temporary, path_);
}
void Device::prepare(bool complete) {
require(state_.phase == Phase::AConfirmed || state_.phase == Phase::PreparingB,
"Cannot prepare B in this phase");
state_.b_ready = false;
state_.phase = Phase::PreparingB;
save();
if (complete) {
// Represents complete installation AND artifact verification.
state_.b_ready = true;
state_.phase = Phase::BPrepared;
save();
}
}
void Device::request_trial(int attempts) {
require(state_.phase == Phase::BPrepared && state_.b_ready,
"B must be prepared and verified before trial");
require(attempts > 0, "The trial needs at least one attempt");
state_.phase = Phase::TrialPending;
state_.trial = Slot::B;
state_.attempts_left = attempts;
save();
}
Slot Device::boot() {
require(state_.phase != Phase::SeparateRecoveryRequired,
"Separate recovery is required; do not alternate failed systems");
if (state_.trial) {
if (state_.attempts_left > 0) {
--state_.attempts_left;
state_.phase = Phase::TrialRunning;
// Persist the consumed attempt BEFORE handing control to B.
save();
running_ = *state_.trial;
return *running_;
}
state_.phase = Phase::RecoveryPending;
state_.trial.reset();
save();
}
if (state_.phase == Phase::RecoveryPending) {
state_.phase = Phase::RecoveryRunning;
save();
}
running_ = state_.normal;
return *running_;
}
void Device::check(bool healthy) {
require(running_.has_value(), "Boot before checking the running system");
require(state_.phase == Phase::TrialRunning || state_.phase == Phase::RecoveryRunning,
"Checks require a running trial or recovery");
if (state_.phase == Phase::TrialRunning) {
state_.trial.reset();
state_.attempts_left = 0;
if (healthy) {
state_.phase = Phase::BConfirmed;
state_.normal = Slot::B;
state_.outcome = "B confirmed";
} else {
state_.phase = Phase::RecoveryPending;
}
} else if (healthy) {
state_.phase = Phase::RecoveryChecked;
state_.outcome = "Recovered to A";
} else {
state_.phase = Phase::SeparateRecoveryRequired;
state_.outcome = "Recovery A failed";
}
save();
}
TemporaryDirectory::TemporaryDirectory() {
std::random_device random;
for (int attempt = 0; attempt < 100; ++attempt) {
auto candidate = std::filesystem::temp_directory_path() /
("ota-ab-simulation-" + std::to_string(random()) + "-" + std::to_string(random()));
if (std::filesystem::create_directory(candidate)) {
path_ = std::move(candidate);
return;
}
}
throw std::runtime_error("Cannot create a unique temporary directory");
}
TemporaryDirectory::~TemporaryDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::vector<std::string>& scenarios() {
static const std::vector<std::string> names = {
"success", "health-failure", "attempts-exhausted", "restart-during-trial",
"interrupted-preparation", "recovery-failure"
};
return names;
}
Device run_scenario(const std::string& scenario, const std::filesystem::path& directory,
std::ostream* output) {
if (std::find(scenarios().begin(), scenarios().end(), scenario) == scenarios().end()) {
throw std::invalid_argument("Unknown scenario: " + scenario);
}
Device device(directory);
auto show = [&](const std::string& event) {
if (!output) return;
const auto& state = device.state();
*output << event << ": " << name(state.phase) << "; normal=" << name(state.normal)
<< "; attempts_left=" << state.attempts_left << "; running="
<< (device.running() ? name(*device.running()) : "none") << '\n';
};
auto reset = [&] {
device = Device(directory);
const auto selected = device.boot();
show(std::string("Reset -> ") + name(selected));
};
auto message = [&](const char* text) { if (output) *output << text << '\n'; };
show("Initial state");
if (scenario == "interrupted-preparation") {
device.prepare(false);
show("Interrupted installation: B remains ineligible");
reset();
return device;
}
device.prepare();
show("Installation and verification complete");
device.request_trial(2);
show("Trial requested");
reset();
if (scenario == "attempts-exhausted" || scenario == "restart-during-trial") {
message("No confirmation: simulate a reset caused by power loss or watchdog");
reset();
}
if (scenario == "attempts-exhausted") {
message("B still cannot reach health checks; simulate another reset");
reset();
device.check(true);
show("Recovery checks pass");
} else if (scenario == "health-failure" || scenario == "recovery-failure") {
device.check(false);
show("B health checks fail");
reset();
device.check(scenario != "recovery-failure");
show("Recovery checks complete");
} else {
device.check(true);
show("B health checks pass; confirmation saved");
reset();
}
return device;
}
} // namespace ota
@@ -0,0 +1,68 @@
#pragma once
#include <filesystem>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
namespace ota {
enum class Phase {
AConfirmed, PreparingB, BPrepared, TrialPending, TrialRunning,
BConfirmed, RecoveryPending, RecoveryRunning, RecoveryChecked,
SeparateRecoveryRequired
};
enum class Slot { A, B };
const char* name(Phase phase);
const char* name(Slot slot);
struct State {
Phase phase = Phase::AConfirmed;
Slot normal = Slot::A;
std::optional<Slot> trial;
int attempts_left = 0;
bool a_ready = true;
bool b_ready = false;
std::string outcome;
};
class Device {
public:
explicit Device(const std::filesystem::path& directory);
const State& state() const { return state_; }
std::optional<Slot> running() const { return running_; }
void prepare(bool complete = true);
void request_trial(int attempts = 2);
Slot boot();
void check(bool healthy);
private:
void save() const;
State state_;
std::filesystem::path path_;
// Volatile: reconstructing a Device requires another boot before checks.
std::optional<Slot> running_;
};
// Own only a freshly created directory and clean it up at scope exit.
class TemporaryDirectory {
public:
TemporaryDirectory();
~TemporaryDirectory();
TemporaryDirectory(const TemporaryDirectory&) = delete;
TemporaryDirectory& operator=(const TemporaryDirectory&) = delete;
const std::filesystem::path& path() const { return path_; }
private:
std::filesystem::path path_;
};
const std::vector<std::string>& scenarios();
Device run_scenario(const std::string& scenario,
const std::filesystem::path& directory,
std::ostream* output = nullptr);
} // namespace ota
@@ -0,0 +1,124 @@
#include "simulation.hpp"
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
void expect(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template <class Operation>
void expect_rejected(Operation operation) {
try {
operation();
} catch (const std::logic_error&) {
return;
}
throw std::runtime_error("Invalid operation was accepted");
}
void scenario_outcomes() {
struct Expected {
const char* scenario;
ota::Phase phase;
ota::Slot normal;
const char* outcome;
};
const Expected expected[] = {
{"success", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
{"health-failure", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
{"attempts-exhausted", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
{"restart-during-trial", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
{"interrupted-preparation", ota::Phase::PreparingB, ota::Slot::A, ""},
{"recovery-failure", ota::Phase::SeparateRecoveryRequired, ota::Slot::A,
"Recovery A failed"}
};
for (const auto& result : expected) {
ota::TemporaryDirectory directory;
auto device = ota::run_scenario(result.scenario, directory.path());
const ota::Device restored(directory.path());
const auto& state = restored.state();
expect(state.phase == result.phase, "Unexpected saved phase");
expect(state.normal == result.normal, "Unexpected normal slot");
expect(state.outcome == result.outcome, "Unexpected saved outcome");
expect(state.a_ready, "The retained slot was modified");
if (state.phase == ota::Phase::SeparateRecoveryRequired) {
expect_rejected([&] { device.boot(); });
}
}
}
void saved_attempts() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare();
device.request_trial(2);
for (int left : {1, 0}) {
device = ota::Device(directory.path());
expect(device.boot() == ota::Slot::B, "Expected trial boot of B");
const ota::Device restored(directory.path());
expect(restored.state().attempts_left == left, "Attempt was not saved before boot");
expect(restored.state().normal == ota::Slot::A, "B was confirmed prematurely");
}
device = ota::Device(directory.path());
expect(device.boot() == ota::Slot::A, "Exhausted trial did not return to A");
expect(device.state().phase == ota::Phase::RecoveryRunning, "Recovery checks skipped");
}
void incomplete_slot() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare(false);
device = ota::Device(directory.path());
expect(!device.state().b_ready, "Incomplete B was marked ready");
expect_rejected([&] { device.request_trial(); });
expect(device.boot() == ota::Slot::A, "Incomplete B was selected");
}
void confirmation_requires_boot() {
ota::TemporaryDirectory directory;
ota::Device device(directory.path());
device.prepare();
expect_rejected([&] { device.request_trial(0); });
expect_rejected([&] { device.request_trial(-1); });
device.request_trial();
expect_rejected([&] { device.check(true); });
device.boot();
ota::Device restored(directory.path());
expect(!restored.running(), "Running slot survived reconstruction");
expect_rejected([&] { restored.check(true); });
device.check(true);
expect_rejected([&] { device.check(false); });
}
void unreadable_state() {
ota::TemporaryDirectory directory;
{
std::ofstream output(directory.path() / "boot-state.txt");
output << "incomplete record\n";
}
try {
const ota::Device device(directory.path());
} catch (const std::runtime_error&) {
return;
}
throw std::runtime_error("Unreadable state was accepted");
}
} // namespace
int main() {
try {
scenario_outcomes();
saved_attempts();
incomplete_slot();
confirmation_requires_boot();
unreadable_state();
std::cout << "All recovery invariant checks passed\n";
} catch (const std::exception& error) {
std::cerr << "Test failed: " << error.what() << '\n';
return 1;
}
}
+4
View File
@@ -22,6 +22,10 @@ This repository is not intended to be a production-ready framework. Instead, it
The repository is being developed incrementally as a structured series of articles, diagrams, architecture decisions, and practical examples.
Start with the [series overview](docs/00-series-overview.md). The current practical material includes [PlantUML diagrams](diagrams/readme.md) and an [A/B trial boot simulation](examples/simulations/ab-trial-boot/readme.md) with startup and recovery failure scenarios.
See the [changelog](CHANGELOG.md) for dated updates to the chapters, architecture decisions, diagrams, and simulations, with links to the material and related history.
## License