Skip to content

test: builtin service dependencies - #1823

Draft
saranyailla wants to merge 1 commit into
mainfrom
builtin
Draft

test: builtin service dependencies#1823
saranyailla wants to merge 1 commit into
mainfrom
builtin

Conversation

@saranyailla

Copy link
Copy Markdown
Member

Issue #, if available:

Description of changes:

Why is this change necessary:

How was this change tested:

  • Updated or added new unit tests.
  • Updated or added new integration tests.
  • Updated or added new end-to-end tests.
  • If my code makes a remote network call, it was tested with a proxy.

Any additional information or context required to review the change:

Documentation Checklist:

  • Updated the README if applicable.

Compatibility Checklist:

  • I confirm that the change is backwards compatible.
  • Any modification or deletion of public interfaces does not impact other plugin components.
  • For external library version updates, I have reviewed its change logs and Nucleus does not consume
    any deprecated method or type.

Refer to Compatibility Guidelines for more information.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

aws-kevinrickard added a commit to aws-kevinrickard/aws-greengrass-nucleus that referenced this pull request Jul 17, 2026
Add two baseline tests for the eviction-filter behavior the builtin
protection layers rest on: a default dependency which was never listed
in the dependencies topic survives a topic write omitting it (with its
default flag untouched), and re-adding an evicted dependency with
isDefault=true restores its protection against subsequent omissions —
the mechanism by which launch() repairs the dependency graph at boot.

Adapted from the characterization test suite in aws-greengrass#1823, which this
change set supersedes.

Co-authored-by: saranyailla <nukai@amazon.com>
aws-kevinrickard added a commit to aws-kevinrickard/aws-greengrass-nucleus that referenced this pull request Jul 17, 2026
Add two baseline tests for the eviction-filter behavior the builtin
protection layers rest on: a default dependency which was never listed
in the dependencies topic survives a topic write omitting it (with its
default flag untouched), and re-adding an evicted dependency with
isDefault=true restores its protection against subsequent omissions —
the mechanism by which launch() repairs the dependency graph at boot.

Adapted from the characterization test suite in aws-greengrass#1823, which this
change set supersedes.

Co-authored-by: saranyailla <nukai@amazon.com>
aws-kevinrickard added a commit that referenced this pull request Jul 20, 2026
…1824)

* fix: preserve default flag when re-adding dependencies from topic

Builtin autostart services are injected into main's dependency map at
launch as default dependencies. When a deployment writes the
services/main/dependencies topic, setupDependencies re-added every
listed service with isDefault=false, silently erasing that protection.
Any later topic update lacking the builtins then evicted them from the
dependency graph while their services kept running.

This happens in practice when a device's first-ever deployment fails
after activation and rolls back: the rollback snapshot's dependencies
topic value predates the deployment and lists only the nucleus, so
replaying it evicts every builtin service. Because deployment merge
protection for builtin services is keyed on kernel.orderedDependencies(),
the next deployment then deletes the builtins' entire config subtrees
(including DeploymentService group membership state and the
FleetStatusService sequence number) while the services keep writing to
the detached config nodes. Subsequent tlog truncations omit the detached
subtrees, so the loss surfaces only at the next restart, where the
device loses its thing group state and deployments tear down installed
components.

Preserve the existing isDefault flag when a dependency is re-added from
the topic so default dependencies can never be evicted by a topic
update. The flag was not read anywhere except the eviction filter, and
launch() re-injects builtins as default at every boot, so no behavior
other than the erroneous eviction is affected.

* fix: key builtin merge protection on static builtin names

Deployment merges protect builtin services' config with per-service
MERGE overrides keyed on kernel.orderedDependencies(). The live
dependency graph is not a safe key: a builtin service can be
erroneously missing from it while still running, in which case the
services-level REPLACE wildcard silently deletes the builtin's entire
config subtree (deployment group state, fleet status sequence number,
telemetry state) out from under the running service.

Derive the protected name set from static builtin identity
(KernelLifecycle autostart builtin annotations) and union it with the
graph-derived set. Autostart plugin builtins, whose names are not
statically known, remain covered by the graph-derived set. Protecting
a builtin that is not currently in the graph is harmless since a MERGE
override only retains existing config.

Also add the same builtin protection to the rollback merge behavior,
which previously had none: replaying a snapshot that lacks a builtin's
config (e.g. one dumped while the builtin's subtree was detached)
discarded the builtin's entire config. Values present in the snapshot
are restored exactly as before; the only behavior change is that nodes
newly created under a builtin during the failed deployment are retained
rather than discarded.

createRollbackMergeBehavior is widened from private to protected for
testability, matching createDeploymentMergeBehavior.

* fix: carry builtin services forward in main config by static identity

KernelConfigResolver.getMainConfig carried builtin services into main's
resolved dependencies only when they were present in the live dependency
graph (kernel.getMain().getDependencies()). If a builtin was erroneously
missing from the graph while its service was still running, every
subsequent deployment resolution omitted it, so the damage was
self-sustaining: the dependencies topic never listed the builtins again
until the next restart re-injected them at launch.

Union the static autostart builtin names (as name:HARD) with the
graph-derived carry-forward. On a healthy device the graph already
contains every autostart builtin, so the resolved output is unchanged.
On a damaged device, merging the resolved config now repairs the
dependency graph — the dependencies topic subscriber re-adds the missing
builtins — instead of perpetuating the damage until reboot. Builtins
present in the graph keep their actual dependency type; the static union
only applies to names absent from the graph. Autostart builtins are
compiled into the nucleus, so a statically added name always resolves.

* fix: restore default flag for autostart builtins re-added to main

When a deployment resolution repairs a damaged dependency graph by
listing a previously evicted autostart builtin in main's dependencies
topic, the subscriber re-added it as a non-default dependency, since no
prior dependency info existed to preserve the flag from. The healed
state was therefore weaker than a freshly launched one: a later topic
update omitting the builtins (for example another rollback) could evict
them again.

Treat the statically known autostart builtin names as default
dependencies when main re-adds them from the topic. This is exactly the
set launch() injects as default at every boot, so healing now restores
launch-equivalent protection. Other services declaring the same names
keep the existing removable-dependency behavior.

* refactor: extract shared builtin protection name set helper

Deduplicate the static-plus-graph builtin name union used by both the
deployment and rollback merge behaviors into a single helper so the two
protection sites cannot drift. Also guard the static name derivation
against a builtin class missing its annotation, and fix test import
ordering.

* refactor: address review feedback on builtin protection

Document in getBuiltinServiceNamesToProtect the residual asymmetry for
annotation-discovered plugin autostart builtins (protected only while
present in the dependency graph and not healed by the static
carry-forward). Assert the preserved default flag directly in the
flag-preservation test, pinning that contract in isolation from the
static-name default restoration for main. Use getServiceName
consistently in the new code paths.

* test: pin default dependency eviction filter invariants

Add two baseline tests for the eviction-filter behavior the builtin
protection layers rest on: a default dependency which was never listed
in the dependencies topic survives a topic write omitting it (with its
default flag untouched), and re-adding an evicted dependency with
isDefault=true restores its protection against subsequent omissions —
the mechanism by which launch() repairs the dependency graph at boot.

Adapted from the characterization test suite in #1823, which this
change set supersedes.

Co-authored-by: saranyailla <nukai@amazon.com>

---------

Co-authored-by: saranyailla <nukai@amazon.com>
yitingb pushed a commit that referenced this pull request Aug 3, 2026
…1824)

* fix: preserve default flag when re-adding dependencies from topic

Builtin autostart services are injected into main's dependency map at
launch as default dependencies. When a deployment writes the
services/main/dependencies topic, setupDependencies re-added every
listed service with isDefault=false, silently erasing that protection.
Any later topic update lacking the builtins then evicted them from the
dependency graph while their services kept running.

This happens in practice when a device's first-ever deployment fails
after activation and rolls back: the rollback snapshot's dependencies
topic value predates the deployment and lists only the nucleus, so
replaying it evicts every builtin service. Because deployment merge
protection for builtin services is keyed on kernel.orderedDependencies(),
the next deployment then deletes the builtins' entire config subtrees
(including DeploymentService group membership state and the
FleetStatusService sequence number) while the services keep writing to
the detached config nodes. Subsequent tlog truncations omit the detached
subtrees, so the loss surfaces only at the next restart, where the
device loses its thing group state and deployments tear down installed
components.

Preserve the existing isDefault flag when a dependency is re-added from
the topic so default dependencies can never be evicted by a topic
update. The flag was not read anywhere except the eviction filter, and
launch() re-injects builtins as default at every boot, so no behavior
other than the erroneous eviction is affected.

* fix: key builtin merge protection on static builtin names

Deployment merges protect builtin services' config with per-service
MERGE overrides keyed on kernel.orderedDependencies(). The live
dependency graph is not a safe key: a builtin service can be
erroneously missing from it while still running, in which case the
services-level REPLACE wildcard silently deletes the builtin's entire
config subtree (deployment group state, fleet status sequence number,
telemetry state) out from under the running service.

Derive the protected name set from static builtin identity
(KernelLifecycle autostart builtin annotations) and union it with the
graph-derived set. Autostart plugin builtins, whose names are not
statically known, remain covered by the graph-derived set. Protecting
a builtin that is not currently in the graph is harmless since a MERGE
override only retains existing config.

Also add the same builtin protection to the rollback merge behavior,
which previously had none: replaying a snapshot that lacks a builtin's
config (e.g. one dumped while the builtin's subtree was detached)
discarded the builtin's entire config. Values present in the snapshot
are restored exactly as before; the only behavior change is that nodes
newly created under a builtin during the failed deployment are retained
rather than discarded.

createRollbackMergeBehavior is widened from private to protected for
testability, matching createDeploymentMergeBehavior.

* fix: carry builtin services forward in main config by static identity

KernelConfigResolver.getMainConfig carried builtin services into main's
resolved dependencies only when they were present in the live dependency
graph (kernel.getMain().getDependencies()). If a builtin was erroneously
missing from the graph while its service was still running, every
subsequent deployment resolution omitted it, so the damage was
self-sustaining: the dependencies topic never listed the builtins again
until the next restart re-injected them at launch.

Union the static autostart builtin names (as name:HARD) with the
graph-derived carry-forward. On a healthy device the graph already
contains every autostart builtin, so the resolved output is unchanged.
On a damaged device, merging the resolved config now repairs the
dependency graph — the dependencies topic subscriber re-adds the missing
builtins — instead of perpetuating the damage until reboot. Builtins
present in the graph keep their actual dependency type; the static union
only applies to names absent from the graph. Autostart builtins are
compiled into the nucleus, so a statically added name always resolves.

* fix: restore default flag for autostart builtins re-added to main

When a deployment resolution repairs a damaged dependency graph by
listing a previously evicted autostart builtin in main's dependencies
topic, the subscriber re-added it as a non-default dependency, since no
prior dependency info existed to preserve the flag from. The healed
state was therefore weaker than a freshly launched one: a later topic
update omitting the builtins (for example another rollback) could evict
them again.

Treat the statically known autostart builtin names as default
dependencies when main re-adds them from the topic. This is exactly the
set launch() injects as default at every boot, so healing now restores
launch-equivalent protection. Other services declaring the same names
keep the existing removable-dependency behavior.

* refactor: extract shared builtin protection name set helper

Deduplicate the static-plus-graph builtin name union used by both the
deployment and rollback merge behaviors into a single helper so the two
protection sites cannot drift. Also guard the static name derivation
against a builtin class missing its annotation, and fix test import
ordering.

* refactor: address review feedback on builtin protection

Document in getBuiltinServiceNamesToProtect the residual asymmetry for
annotation-discovered plugin autostart builtins (protected only while
present in the dependency graph and not healed by the static
carry-forward). Assert the preserved default flag directly in the
flag-preservation test, pinning that contract in isolation from the
static-name default restoration for main. Use getServiceName
consistently in the new code paths.

* test: pin default dependency eviction filter invariants

Add two baseline tests for the eviction-filter behavior the builtin
protection layers rest on: a default dependency which was never listed
in the dependencies topic survives a topic write omitting it (with its
default flag untouched), and re-adding an evicted dependency with
isDefault=true restores its protection against subsequent omissions —
the mechanism by which launch() repairs the dependency graph at boot.

Adapted from the characterization test suite in #1823, which this
change set supersedes.

Co-authored-by: saranyailla <nukai@amazon.com>

---------

Co-authored-by: saranyailla <nukai@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant