diagrams and simulation
This commit is contained in:
1 parent
bf9871eb9e
commit
d65910a7c5
18 files changed
+926
-4
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) | [SVG](rendered/ota-architecture.svg) |
|
||||
| Trial boot, confirmation and recovery | [PlantUML](plantuml/ab-trial-boot.puml) | [SVG](rendered/ab-trial-boot.svg) |
|
||||
| Storage areas and access responsibilities | [PlantUML](plantuml/ab-storage-layout.puml) | [SVG](rendered/ab-storage-layout.svg) |
|
||||
|
||||
From the repository root, regenerate previews with a local PlantUML installation and Graphviz:
|
||||
|
||||
```sh
|
||||
JAVA_TOOL_OPTIONS=-Djava.awt.headless=true plantuml -tsvg -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.
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="1029px" preserveAspectRatio="none" style="width:766px;height:1029px;" version="1.1" viewBox="0 0 766 1029" width="766px" zoomAndPan="magnify"><defs/><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="451" x="169" y="19.2419">A/B storage: logical areas and access responsibilities</text><!--MD5=[840fabc209cb087dc2fa62a6d2b2ef1b]
|
||||
cluster Device storage (logical areas, not a partition table)--><rect fill="#FFFFFF" height="831" style="stroke: #000000; stroke-width: 1.5;" width="372" x="383" y="48.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="350" x="394" y="65.4818">Device storage (logical areas, not a partition table)</text><!--MD5=[dce4c2b5694dd28223e7b1d160ba96c4]
|
||||
cluster data--><path d="M425,552.5158 C425,542.5158 569,542.5158 569,542.5158 C569,542.5158 713,542.5158 713,552.5158 L713,845.5158 C713,855.5158 569,855.5158 569,855.5158 C569,855.5158 425,855.5158 425,845.5158 L425,552.5158 " fill="#FFFFFF" style="stroke: #000000; stroke-width: 1.5;"/><path d="M425,552.5158 C425,562.5158 569,562.5158 569,562.5158 C569,562.5158 713,562.5158 713,552.5158 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="153" x="492.5" y="579.4818">Persistent device data</text><!--MD5=[8b2faa79b5ce75f56ca47380ca6b1f00]
|
||||
entity shared--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="279" x="429.5" y="365.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="169" x="439.5" y="390.4818">Shared boot components</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="259" x="439.5" y="409.5497">Separate update and recovery method</text><!--MD5=[f350ee6f6cc17b9109e91461bf56a5ea]
|
||||
entity state--><path d="M399,469.0158 C399,459.0158 569,459.0158 569,459.0158 C569,459.0158 739,459.0158 739,469.0158 L739,516.1517 C739,526.1517 569,526.1517 569,526.1517 C569,526.1517 399,526.1517 399,516.1517 L399,469.0158 " fill="#F8F8F8" style="stroke: #000000; stroke-width: 1.5;"/><path d="M399,469.0158 C399,479.0158 569,479.0158 569,479.0158 C569,479.0158 739,479.0158 739,469.0158 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="67" x="409" y="497.9818">Boot state</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="320" x="409" y="517.0497">Normal slot, trial target, attempts, confirmation</text><!--MD5=[770389ab9df153645bc6ef28055eb2b5]
|
||||
entity A--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="269" x="434.5" y="272.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="154" x="444.5" y="297.4818">Slot A: retained system</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="249" x="444.5" y="316.5497">Kernel + device tree + root filesystem</text><!--MD5=[b2d39259b6aeabc16b394deeb0238426]
|
||||
entity B--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="269" x="434.5" y="179.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="139" x="444.5" y="204.4818">Slot B: update target</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="249" x="444.5" y="223.5497">Kernel + device tree + root filesystem</text><!--MD5=[64a9df9efdb85e10fd4da2b3e755ab03]
|
||||
entity work--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="210" x="464" y="86.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="167" x="474" y="111.4818">Download and work area</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="190" x="474" y="130.5497">Disposable preparation files</text><!--MD5=[e7744e8fbc1b270a63d01eb8a46af6a2]
|
||||
entity identity--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="178" x="480" y="688.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="158" x="490" y="713.4818">Identity and calibration</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="116" x="490" y="732.5497">Restricted access</text><!--MD5=[7518041a300050aac907e20a36cd28cf]
|
||||
entity appdata--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="256" x="441" y="781.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="236" x="451" y="806.4818">Configuration and application data</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="231" x="451" y="825.5497">Must support the retained version</text><!--MD5=[63a986ea2eddc5539a0918540cb05aed]
|
||||
entity progress--><rect fill="#F8F8F8" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="210" x="464" y="595.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="190" x="474" y="620.4818">OTA progress, failure blocks</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="140" x="474" y="639.5497">and pending reports</text><!--MD5=[cfa4e52b1a5e747220fbc0f640efedd4]
|
||||
entity installer--><ellipse cx="80.5" cy="131.5158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,139.5158 L80.5,166.5158 M67.5,147.5158 L93.5,147.5158 M80.5,166.5158 L67.5,181.5158 M80.5,166.5158 L93.5,181.5158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="56" x="52.5" y="198.9818">Installer</text><!--MD5=[cb76ffbfbf2ad469590be32074c51c9a]
|
||||
entity boot--><ellipse cx="80.5" cy="326.5158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,334.5158 L80.5,361.5158 M67.5,342.5158 L93.5,342.5158 M80.5,361.5158 L67.5,376.5158 M80.5,361.5158 L93.5,376.5158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="81" x="40" y="393.9818">Boot control</text><!--MD5=[2b2d5ad518bf6746de4ffeb5be0566d4]
|
||||
entity manager--><ellipse cx="80.5" cy="563.0158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,571.0158 L80.5,598.0158 M67.5,579.0158 L93.5,579.0158 M80.5,598.0158 L67.5,613.0158 M80.5,598.0158 L93.5,613.0158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="102" x="6" y="630.4818">OTA manager /</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="149" x="6" y="649.5497">confirmation program</text><!--MD5=[6857f13236cb144e56befd21da1781a5]
|
||||
entity runtime--><ellipse cx="80.5" cy="715.0158" fill="#F8F8F8" rx="8" ry="8" style="stroke: #383838; stroke-width: 1.5;"/><path d="M80.5,723.0158 L80.5,750.0158 M67.5,731.0158 L93.5,731.0158 M80.5,750.0158 L67.5,765.0158 M80.5,750.0158 L93.5,765.0158 " fill="none" style="stroke: #383838; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="109" x="24.5" y="782.4818">Running system</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="112" x="24.5" y="801.5497">and applications</text><!--MD5=[185f47122682ca8ce8f24fc040241013]
|
||||
link installer to B--><path d="M108.5,170.1058 C129.25,175.6158 158.71,182.7358 185,186.5158 C265.32,198.0458 355.93,203.5858 428.84,206.2258 " fill="none" id="installer->B" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="434.06,206.4058,425.2147,202.0745,429.0634,206.2203,424.9177,210.069,434.06,206.4058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="117" x="217.5" y="182.4129">replace inactive slot</text><!--MD5=[5e4f21d9383ed71105fa8830a37cd002]
|
||||
link installer to work--><path d="M108.7,159.8858 C174.46,153.5358 345.01,137.0558 458.75,126.0658 " fill="none" id="installer->work" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="463.78,125.5858,454.4426,122.4533,458.8023,126.0579,455.1978,130.4176,463.78,125.5858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="114" x="219" y="132.4129">use temporary files</text><!--MD5=[45b867fa1f8d8aa4541f3cf2eef528d9]
|
||||
link boot to shared--><path d="M121.29,371.2058 C140.33,377.0958 163.59,383.3458 185,386.5158 C263.54,398.1458 352.12,400.6758 424.3,400.0558 " fill="none" id="boot->shared" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="429.47,400.0058,420.436,396.0831,424.4702,400.0487,420.5046,404.0828,429.47,400.0058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="47" x="252.5" y="382.4129">execute</text><!--MD5=[e0595da2e168bbec6fe2445f1534f63e]
|
||||
link boot to state--><path d="M121.01,390.0958 C139.42,403.6858 162.25,418.3958 185,427.5158 C250.58,453.8158 326.64,469.4758 393.6,478.7958 " fill="none" id="boot->state" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="398.82,479.5158,390.4403,474.3408,393.8655,478.8428,389.3635,482.268,398.82,479.5158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="180" x="186" y="423.4129">read / save before Linux starts</text><!--MD5=[b889d15dcac99864b6524b7c7532a9ac]
|
||||
link boot to A--><path d="M121.31,352.9358 C187.76,345.2858 324.3,329.5658 429.03,317.5158 " fill="none" id="boot->A" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="434.3,316.9058,424.9021,313.9597,429.3327,317.4768,425.8156,321.9074,434.3,316.9058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="173" x="189.5" y="322.4129">select matching components</text><!--MD5=[d8162b56ad1fdfbf80f85e7eafe33a3d]
|
||||
link boot to B--><path d="M121.09,332.4658 C139.89,321.4258 163.01,309.0558 185,300.5158 C263.44,270.0558 354.95,247.6158 428.86,232.5358 " fill="none" id="boot->B" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="434.15,231.4558,424.5319,229.3363,429.251,232.4556,426.1316,237.1747,434.15,231.4558" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="173" x="189.5" y="244.4129">select matching components</text><!--MD5=[ddcc9cc09abe2ea6c86e27d5ca5369f5]
|
||||
link manager to state--><path d="M155.12,586.7158 C224.08,570.9858 329.25,546.9858 416.18,527.1558 " fill="none" id="manager->state" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="421.09,526.0358,411.4259,524.1372,416.2152,527.1476,413.2048,531.9369,421.09,526.0358" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="129" x="211.5" y="519.4129">request trial / confirm</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="160" x="196" y="537.1189">through platform interface</text><!--MD5=[e5613818b242e6cfc0b7814e671c619e]
|
||||
link manager to progress--><path d="M155.12,606.6958 C235.81,610.1758 366.08,615.7958 458.7,619.7958 " fill="none" id="manager->progress" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="463.91,620.0258,455.0895,615.6442,458.9146,619.8118,454.747,623.6368,463.91,620.0258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="65" x="243.5" y="604.4129">read / save</text><!--MD5=[b7c32426f5b9e92879708d4d6251b8ea]
|
||||
link runtime to appdata--><path d="M136.68,766.5758 C152.26,769.4458 169.26,772.3358 185,774.5158 C268.02,786.0158 361.67,794.8858 435.59,800.9558 " fill="none" id="runtime->appdata" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="440.88,801.3858,432.2287,796.6789,435.896,800.986,431.5889,804.6533,440.88,801.3858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="68" x="242" y="770.4129">read / write</text><!--MD5=[1e5b522c1f502a1b8cbd35bf97fc6c8c]
|
||||
link runtime to identity--><path d="M136.83,751.1958 C219.12,744.7658 374.77,732.6058 474.83,724.7958 " fill="none" id="runtime->identity" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="479.85,724.4058,470.5589,721.1384,474.866,724.8057,471.1987,729.1128,479.85,724.4058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="135" x="208.5" y="730.4129">authorized access only</text><rect fill="#DDDDDD" height="105.3396" rx="5" ry="5" style="stroke: #000000; stroke-width: 1.0;" width="534" x="127" y="913.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="487" x="133" y="933.4818">A is running while B is prepared. Preserve A, boot state and device data.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="440" x="133" y="952.5497">Persistent device data survives replacement of either system slot.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="303" x="133" y="971.6176">Shared data is not rolled back by selecting A.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="477" x="133" y="990.6855">Both system versions must understand the recovery records they read.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="522" x="133" y="1009.7534">Separate logical areas do not guarantee safe writes or whole-device recovery.</text><!--MD5=[fbf2d48276166d3e3c575df5d661f0a2]
|
||||
@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
|
||||
|
||||
PlantUML version 1.2020.02(Sun Mar 01 05:22:07 EST 2020)
|
||||
(GPL source distribution)
|
||||
Java Runtime: OpenJDK Runtime Environment
|
||||
JVM: OpenJDK 64-Bit Server VM
|
||||
Java Version: 21.0.12.1+1-1-deb13u1-Debian
|
||||
Operating System: Linux
|
||||
Default Encoding: UTF-8
|
||||
Language: en
|
||||
Country: null
|
||||
--></g></svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="1165px" preserveAspectRatio="none" style="width:1029px;height:1165px;" version="1.1" viewBox="0 0 1029 1165" width="1029px" zoomAndPan="magnify"><defs/><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="229" x="404" y="19.2419">A/B trial boot and recovery</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="102" x="683.79" y="169.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="683.79" x2="785.79" y1="198.5838" y2="198.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="693.79" y="189.4818">A confirmed</text><rect fill="#F8F8F8" height="58.1358" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="247" x="466.29" y="32.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="466.29" x2="713.29" y1="80.6517" y2="80.6517"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="66" x="556.79" y="52.4818">Prepare B</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="227" x="476.29" y="71.5497">A remains the normal boot choice</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="181" x="237.29" y="169.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="237.29" x2="418.29" y1="198.5838" y2="198.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="161" x="247.29" y="189.4818">B prepared and verified</text><rect fill="#F8F8F8" height="58.1358" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="226" x="214.79" y="310.0158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="214.79" x2="440.79" y1="358.1517" y2="358.1517"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="101" x="277.29" y="329.9818">B trial pending</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="206" x="224.79" y="349.0497">Saved target and attempt limit</text><rect fill="#F8F8F8" height="58.1358" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="119" x="121.29" y="476.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="121.29" x2="240.29" y1="524.6517" y2="524.6517"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="99" x="131.29" y="496.4818">B trial running</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="88" x="136.79" y="515.5497">Unconfirmed</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="102" x="7.79" y="711.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="7.79" x2="109.79" y1="740.5838" y2="740.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="17.79" y="731.4818">B confirmed</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="171" x="291.29" y="631.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="291.29" x2="462.29" y1="660.5838" y2="660.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="151" x="301.29" y="651.4818">Recovery to A pending</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="149" x="302.29" y="791.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="302.29" x2="451.29" y1="820.5838" y2="820.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="129" x="312.29" y="811.4818">A recovery running</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="149" x="368.29" y="938.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="368.29" x2="517.29" y1="967.5838" y2="967.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="129" x="378.29" y="958.4818">A recovery checked</text><rect fill="#F8F8F8" height="50" rx="12.5" ry="12.5" style="stroke: #383838; stroke-width: 1.5;" width="204" x="111.79" y="938.5158"/><line style="stroke: #383838; stroke-width: 1.5;" x1="111.79" x2="315.79" y1="967.5838" y2="967.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="184" x="121.79" y="958.4818">Separate recovery required</text><ellipse cx="1008.79"Line truncated
|
||||
link *start to A--><path d="M1006.77,71.4758 C1002.61,87.7058 991.54,121.8958 968.79,139.5158 C917.66,179.1458 842.54,190.5658 791.3,193.4258 " fill="none" id="*start->A" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="786.11,193.6858,795.3036,197.2183,791.1034,193.429,794.8927,189.2289,786.11,193.6858" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[c262a729ace56793ce804c80164b78bb]
|
||||
link A to Preparing--><path d="M683.6,189.4458 C650.05,183.8158 608.51,170.5558 586.79,139.5158 C578.12,127.1158 577.7,110.6458 579.93,96.1158 " fill="none" id="A->Preparing" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="580.86,90.8658,575.3541,99.0319,579.9894,95.7895,583.2319,100.4248,580.86,90.8658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="103" x="587.79" y="135.4129">begin installation</text><!--MD5=[8ac39ec8ceeada5156d86e8c940ca0fe]
|
||||
link Preparing to A--><path d="M655.3,90.6258 C669.76,98.8358 684.21,108.8158 695.79,120.5158 C708.06,132.8958 717.45,149.9358 723.95,164.4358 " fill="none" id="Preparing->A" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="726.04,169.2458,726.1343,159.3974,724.0532,164.6575,718.7931,162.5764,726.04,169.2458" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="249" x="710.79" y="135.4129">abandon incomplete or failed preparation</text><!--MD5=[07f470d462742561469e04f57962dde0]
|
||||
link Preparing to Preparing--><path d="M713.4,49.6658 C734.05,51.3258 748.29,55.2758 748.29,61.5158 C748.29,67.2258 736.39,71.0158 718.58,72.8858 " fill="none" id="Preparing->Preparing" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="713.4,73.3658,722.7285,76.5249,718.379,72.908,721.9959,68.5585,713.4,73.3658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="193" x="754.29" y="57.9129">reset during preparation; boot A</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="135" x="783.29" y="75.6189">B is not eligible for trial</text><!--MD5=[14c61493ce9a076f09f2189989cfcc27]
|
||||
link Preparing to Prepared--><path d="M466.24,80.8158 C416.86,90.4458 367.51,103.7658 349.79,120.5158 C337.87,131.7958 332.34,149.2658 329.8,164.2958 " fill="none" id="Preparing->Prepared" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="329.05,169.2858,334.3666,160.9953,329.8071,164.3435,326.4588,159.784,329.05,169.2858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="220" x="350.79" y="135.4129">installation and verification complete</text><!--MD5=[9d3566f475c31f8d28eabcacf9587b86]
|
||||
link Prepared to Pending--><path d="M327.79,219.6858 C327.79,243.0658 327.79,278.6758 327.79,304.7258 " fill="none" id="Prepared->Pending" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="327.79,309.8058,331.79,300.8058,327.79,304.8058,323.79,300.8058,327.79,309.8058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="233" x="328.79" y="264.4129">save trial request through boot control</text><!--MD5=[2b80e139dec28a8ca7d421262724a74e]
|
||||
link Pending to Trial--><path d="M214.47,350.3858 C135.44,360.2358 39.83,378.3058 14.79,409.5158 C-18.81,451.3958 56.27,478.2758 116.14,492.3858 " fill="none" id="Pending->Trial" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="121.15,493.5458,113.285,487.6178,116.279,492.4174,111.4794,495.4114,121.15,493.5458" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="268" x="15.79" y="424.4129">reset [attempts left > 0]; save attempts left - 1</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="165" x="67.29" y="442.1189">before handing control to B</text><!--MD5=[83187eb056d347350e70da73cc8be795]
|
||||
link Trial to Pending--><path d="M240.34,482.2458 C257.83,473.3358 275.76,461.5458 288.79,446.5158 C306.79,425.7758 316.75,396.0258 322.09,373.2358 " fill="none" id="Trial->Pending" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="323.23,368.1958,317.3509,376.0975,322.1318,373.0737,325.1555,377.8547,323.23,368.1958" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="152" x="336.79" y="424.4129">reset before confirmation</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="202" x="311.79" y="442.1189">(power loss or covered watchdog)</text><!--MD5=[123c6cef540f58bde17912cb320a2a04]
|
||||
link Trial to Confirmed--><path d="M121.13,518.1258 C95.2,526.4458 67.26,540.5958 51.79,564.5158 C24.03,607.4358 36.74,669.8958 48.07,706.2458 " fill="none" id="Trial->Confirmed" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="49.73,711.4158,50.7935,701.6246,48.2046,706.6542,43.1749,704.0653,49.73,711.4158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="202" x="52.79" y="579.4129">local checks pass within time limit;</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="107" x="100.29" y="597.1189">save confirmation</text><!--MD5=[788b26dbcee76df69ae24d57af56fb68]
|
||||
link Trial to Recovering--><path d="M224.17,534.5358 C237.29,543.6058 251.47,554.0458 263.79,564.5158 C281.28,579.3758 282.32,586.6458 299.79,601.5158 C310.76,610.8458 323.3,620.1558 335.04,628.3558 " fill="none" id="Trial->Recovering" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="339.32,631.3258,334.2093,622.9068,335.2132,628.4739,329.6461,629.4778,339.32,631.3258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="227" x="300.79" y="588.4129">local checks fail; save recovery request</text><!--MD5=[8cd5eda113903aa9882c5f6ff29c10bf]
|
||||
link Pending to Recovering--><path d="M420.65,368.0558 C467.5,383.0958 515.89,400.3758 522.79,409.5158 C574.26,477.7258 582.36,531.9058 532.79,601.5158 C517.44,623.0758 492.87,636.1058 467.72,643.9558 " fill="none" id="Pending->Recovering" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="462.69,645.4558,472.4581,646.7144,467.4811,644.0257,470.1698,639.0487,462.69,645.4558" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="132" x="567.79" y="510.9129">reset; no attempts left</text><!--MD5=[09dc23c1c901f79ee75b5b595eb0935b]
|
||||
link Recovering to Recovery--><path d="M376.79,681.8558 C376.79,710.0058 376.79,756.3158 376.79,786.3658 " fill="none" id="Recovering->Recovery" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="376.79,791.4058,380.79,782.4058,376.79,786.4058,372.79,782.4058,376.79,791.4058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="181" x="377.79" y="741.9129">boot control selects A on reset</text><!--MD5=[0ce13a800b8a1f17080f923bfff08aab]
|
||||
link Recovery to Recovered--><path d="M394.33,841.7658 C400.38,850.8158 406.87,861.3658 411.79,871.5158 C421.45,891.4158 429.42,915.1058 434.86,933.3258 " fill="none" id="Recovery->Recovered" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="436.32,938.3058,437.6132,928.5423,434.9069,933.5097,429.9394,930.8033,436.32,938.3058" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="158" x="427.79" y="886.4129">local recovery checks pass;</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="84" x="464.79" y="904.1189">save outcome</text><!--MD5=[f5fa9e9e750dea8a99a710e758197f77]
|
||||
link Recovery to Failed--><path d="M302.24,820.0058 C264.54,825.5258 221.8,839.2058 197.79,871.5158 C184.65,889.2058 190.22,914.1058 198.2,933.3958 " fill="none" id="Recovery->Failed" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="200.28,938.1658,200.3743,928.3174,198.2932,933.5775,193.0331,931.4964,200.28,938.1658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="204" x="198.79" y="886.4129">recovery checks fail or startup fails</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="153" x="224.29" y="904.1189">(platform recovery policy)</text><!--MD5=[4470f671c244ee8b6362840b5fcd1a01]
|
||||
link Confirmed to *end--><path d="M55.63,761.7058 C50.69,809.4758 46.14,917.4358 93.79,988.5158 C119.37,1026.6658 173.31,1046.7958 199.25,1054.6058 " fill="none" id="Confirmed->*end" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="204.34,1056.0858,196.82,1049.7258,199.54,1054.6858,194.58,1057.4058,204.34,1056.0858" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[95cc10cd26e44889f83ba83c5e220ef1]
|
||||
link Recovered to *end--><path d="M384.12,988.6058 C331.5,1010.1958 258.07,1040.3458 227.74,1052.7858 " fill="none" id="Recovered->*end" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="222.92,1054.7658,232.764,1055.0745,227.5505,1052.8794,229.7456,1047.6658,222.92,1054.7658" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[3c91aadb35076bbec70261ad1b979304]
|
||||
link Failed to *end--><path d="M213.79,988.7258 C213.79,1006.3558 213.79,1029.6458 213.79,1044.3558 " fill="none" id="Failed->*end" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="213.79,1049.3558,217.79,1040.3558,213.79,1044.3558,209.79,1040.3558,213.79,1049.3558" style="stroke: #383838; stroke-width: 1.0;"/><rect fill="#DDDDDD" height="67.2038" rx="5" ry="5" style="stroke: #000000; stroke-width: 1.0;" width="562" x="237" y="1087.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="501" x="243" y="1107.4818">Preparation interruption assumes the installer preserves A and boot state.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="550" x="243" y="1126.5497">State transitions assume valid durable records; torn writes need platform testing.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="399" x="243" y="1145.6176">Checks and recovery are local; reports wait for connectivity.</text><!--MD5=[d4e2cf11ff88427b3c50e83f198cfc1a]
|
||||
@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
|
||||
|
||||
PlantUML version 1.2020.02(Sun Mar 01 05:22:07 EST 2020)
|
||||
(GPL source distribution)
|
||||
Java Runtime: OpenJDK Runtime Environment
|
||||
JVM: OpenJDK 64-Bit Server VM
|
||||
Java Version: 21.0.12.1+1-1-deb13u1-Debian
|
||||
Operating System: Linux
|
||||
Default Encoding: UTF-8
|
||||
Language: en
|
||||
Country: null
|
||||
--></g></svg>
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="765px" preserveAspectRatio="none" style="width:1092px;height:765px;" version="1.1" viewBox="0 0 1092 765" width="1092px" zoomAndPan="magnify"><defs/><g><text fill="#000000" font-family="sans-serif" font-size="18" lengthAdjust="spacingAndGlyphs" textLength="265" x="419.5" y="19.2419">OTA roles and device boundary</text><!--MD5=[a9e6cbe5f305c80ab86c0c23625bafb4]
|
||||
cluster Release and deployment services--><polygon fill="#FFFFFF" points="395,48.5158,630,48.5158,637,73.5838,1070,73.5838,1070,233.5158,395,233.5158,395,48.5158" style="stroke: #000000; stroke-width: 1.5;"/><line style="stroke: #000000; stroke-width: 1.5;" x1="395" x2="637" y1="73.5838" y2="73.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="229" x="399" y="65.4818">Release and deployment services</text><!--MD5=[eba421ac8e20ac42f3ee759fe6c4cf96]
|
||||
cluster Device--><polygon fill="#FFFFFF" points="22,257.5158,73,257.5158,80,282.5838,1023,282.5838,1023,634.5158,22,634.5158,22,257.5158" style="stroke: #000000; stroke-width: 1.5;"/><line style="stroke: #000000; stroke-width: 1.5;" x1="22" x2="80" y1="282.5838" y2="282.5838"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="45" x="26" y="274.4818">Device</text><!--MD5=[57fb6a492e484ef8c93e363da48f07ef]
|
||||
entity release--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="173" x="411" y="140.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="406" y="145.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="406" y="169.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="153" x="421" y="164.9818">Build, test and publish</text><!--MD5=[9b50232802efdec09056d2fe94d4716d]
|
||||
entity artifacts--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="124" x="894.5" y="170.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="889.5" y="175.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="889.5" y="199.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="104" x="904.5" y="194.9818">Artifact storage</text><!--MD5=[389badd7c35f4ea505f5befbc26a5052]
|
||||
entity deployment--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="195" x="859" y="87.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="854" y="92.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="854" y="116.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="175" x="869" y="111.9818">Deployment management</text><!--MD5=[2b2d5ad518bf6746de4ffeb5be0566d4]
|
||||
entity manager--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="113" x="38" y="398.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="33" y="403.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="33" y="427.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="93" x="48" y="422.9818">OTA manager</text><!--MD5=[cfa4e52b1a5e747220fbc0f640efedd4]
|
||||
entity installer--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="76" x="459.5" y="398.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="454.5" y="403.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="454.5" y="427.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="56" x="469.5" y="422.9818">Installer</text><!--MD5=[cb76ffbfbf2ad469590be32074c51c9a]
|
||||
entity boot--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="101" x="906" y="520.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="901" y="525.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="901" y="549.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="81" x="916" y="544.9818">Boot control</text><!--MD5=[e132700e034a4f2f51ed9e2a9fef93f1]
|
||||
entity checks--><rect fill="#F8F8F8" height="39.0679" style="stroke: #383838; stroke-width: 1.5;" width="150" x="422.5" y="472.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="417.5" y="477.0158"/><rect fill="#F8F8F8" height="5" style="stroke: #383838; stroke-width: 1.5;" width="10" x="417.5" y="501.0838"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="130" x="432.5" y="496.9818">Local health checks</text><!--MD5=[63a986ea2eddc5539a0918540cb05aed]
|
||||
entity progress--><path d="M396,306.0158 C396,296.0158 497.5,296.0158 497.5,296.0158 C497.5,296.0158 599,296.0158 599,306.0158 L599,353.1517 C599,363.1517 497.5,363.1517 497.5,363.1517 C497.5,363.1517 396,363.1517 396,353.1517 L396,306.0158 " fill="#F8F8F8" style="stroke: #000000; stroke-width: 1.5;"/><path d="M396,306.0158 C396,316.0158 497.5,316.0158 497.5,316.0158 C497.5,316.0158 599,316.0158 599,306.0158 " fill="none" style="stroke: #000000; stroke-width: 1.5;"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="183" x="406" y="334.9818">Persistent update progress</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="140" x="406" y="354.0497">and pending reports</text><!--MD5=[54cda06c1d8843fb6ff1b57076ef2079]
|
||||
link release to artifacts--><path d="M584.15,178.9158 C599.03,181.6158 614.41,183.9858 629,185.5158 C718.7,194.9158 823.02,194.1758 889.14,192.2758 " fill="none" id="release->artifacts" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="894.23,192.1258,885.1194,188.3845,889.232,192.2686,885.3479,196.3813,894.23,192.1258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="115" x="671.5" y="163.4129">publish artifact and</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="140" x="659" y="181.1189">verification information</text><!--MD5=[954388852704645441443e24d4a7d174]
|
||||
link release to deployment--><path d="M584.19,149.5758 C660.57,140.7158 772.47,127.7458 853.45,118.3558 " fill="none" id="release->deployment" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="858.74,117.7358,849.3421,114.7897,853.7727,118.3068,850.2556,122.7374,858.74,117.7358" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="198" x="630" y="118.4129">register release and compatibility</text><!--MD5=[43a73133501dd7da1265b9492ab9c514]
|
||||
link manager to deployment--><path d="M107.23,397.9558 C141.78,342.4958 249.36,184.2158 387,122.5158 C538.26,54.7058 735.61,68.9558 853.56,86.7958 " fill="none" id="manager->deployment" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="858.77,87.5958,850.4794,82.2792,853.8276,86.8388,849.2681,90.187,858.77,87.5958" style="stroke: #383838; stroke-width: 1.0;"/><!--MD5=[7fc8070790f1222e2683050d3de11d7a]
|
||||
link manager to installer--><path d="M151.26,417.5158 C232.48,417.5158 381.57,417.5158 454.33,417.5158 " fill="none" id="manager->installer" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="459.43,417.5158,450.43,413.5158,454.43,417.5158,450.43,421.5158,459.43,417.5158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="131" x="208" y="413.4129">authorize preparation</text><!--MD5=[88d9015c92c57aa4fad28cffb4a9fd80]
|
||||
link installer to artifacts--><path d="M535.75,405.1958 C554.9,398.5058 578.48,389.7358 599,380.5158 C718.78,326.6958 853.8,249.5858 917.88,211.9258 " fill="none" id="installer->artifacts" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="922.37,209.2858,912.5871,210.424,918.0662,211.8309,916.6593,217.31,922.37,209.2858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="122" x="668" y="263.4129">download and verify</text><!--MD5=[041cd83e0169fe8848d028533b469304]
|
||||
link manager to boot--><path d="M116.74,437.0258 C133.32,451.5158 157.43,471.0258 181,484.5158 C266.39,533.3658 290.34,547.1958 387,565.5158 C572.91,600.7558 798.83,568.7158 900.85,550.3358 " fill="none" id="manager->boot" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="905.91,549.4258,896.3432,547.0856,900.9893,550.3127,897.7622,554.9588,905.91,549.4258" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="125" x="435" y="543.4129">request trial through</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="107" x="444" y="561.1189">platform interface</text><!--MD5=[c0dd462a887d2095200a8326f34ac787]
|
||||
link checks to boot--><path d="M572.69,499.3058 C664.45,508.9458 818.54,525.1258 900.44,533.7358 " fill="none" id="checks->boot" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="905.73,534.2858,897.2033,529.3567,900.758,533.7572,896.3575,537.3119,905.73,534.2858" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="180" x="639" y="483.4129">confirm after local checks pass</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="188" x="635" y="501.1189">or request recovery after failure</text><!--MD5=[e5613818b242e6cfc0b7814e671c619e]
|
||||
link manager to progress--><path d="M151.35,398.8658 C161.17,395.8758 171.34,392.9658 181,390.5158 C250.17,372.9658 328.97,357.7258 390.52,346.8758 " fill="none" id="manager->progress" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="395.67,345.9658,386.1177,343.5671,390.7439,346.8226,387.4885,351.4488,395.67,345.9658" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="142" x="202.5" y="330.4129">save / resume progress;</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="159" x="194" y="348.1189">queue reports while offline</text><!--MD5=[a165eaa03005a3447a7d5b3956d8aa72]
|
||||
link manager to checks--><path d="M151.26,436.5158 C161.1,439.4658 171.29,442.2758 181,444.5158 C260.38,462.8458 352.72,475.6258 416.91,483.1858 " fill="none" id="manager->checks" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="422.25,483.8158,413.7815,478.7874,417.2845,483.2291,412.8428,486.7322,422.25,483.8158" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="183" x="182" y="440.4129">arrange checks after activation</text><rect fill="#DDDDDD" height="86.2717" rx="5" ry="5" style="stroke: #000000; stroke-width: 1.0;" width="678" x="212.5" y="668.5158"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="666" x="218.5" y="688.4818">OTA manager -> deployment management: request releases; report running version and outcome.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="548" x="218.5" y="707.5497">Boot control runs before the OTA manager and handles exhausted trial attempts.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="546" x="218.5" y="726.6176">Hangs need a covered reset path. Checks and recovery do not require the server.</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="557" x="218.5" y="745.6855">Roles may be combined in one tool. This view uses a system update with trial boot.</text><!--MD5=[eb0d27d0bff260dba490671681420f7d]
|
||||
@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
|
||||
|
||||
PlantUML version 1.2020.02(Sun Mar 01 05:22:07 EST 2020)
|
||||
(GPL source distribution)
|
||||
Java Runtime: OpenJDK Runtime Environment
|
||||
JVM: OpenJDK 64-Bit Server VM
|
||||
Java Version: 21.0.12.1+1-1-deb13u1-Debian
|
||||
Operating System: Linux
|
||||
Default Encoding: UTF-8
|
||||
Language: en
|
||||
Country: null
|
||||
--></g></svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -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/
|
||||
@@ -86,12 +86,12 @@ ota-reference-design/
|
||||
│ ├── ...
|
||||
│
|
||||
├── diagrams/
|
||||
│ └── plantuml/
|
||||
│ ├── plantuml/
|
||||
│ └── rendered/
|
||||
│
|
||||
├── examples/
|
||||
│ ├── raspberry-pi/
|
||||
│ ├── qemu/
|
||||
│ └── simulations/
|
||||
│ └── ab-trial-boot/
|
||||
│
|
||||
├── adr/
|
||||
│ ├── ADR-001-repository-name.md
|
||||
@@ -196,6 +196,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 SVG previews](../diagrams/readme.md) and a [Python A/B trial boot simulation](../examples/simulations/ab-trial-boot/readme.md). Raspberry Pi and QEMU demonstrations are planned.
|
||||
|
||||
---
|
||||
|
||||
## Intended Audience
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
[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.
|
||||
|
||||

|
||||
|
||||
[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.
|
||||
|
||||
@@ -64,6 +64,12 @@ The layout still has shared parts. If an update damages the shared boot componen
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
[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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ 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.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Reference in new issue
Block a user