fix: local deployments preserve running components across bookkeeping loss - #1819
fix: local deployments preserve running components across bookkeeping loss#1819aws-kevinrickard wants to merge 3 commits into
Conversation
Add two test classes proving that Nucleus silently drops a thing group's root components when that group's GroupToRootComponents config entry is absent, even when listThingGroupsForDevice confirms the device still belongs to the group. ThingGroupMembershipPreservationTest: exercises the passive-absence path via DefaultDeploymentTask.getNonTargetGroupToRootPackagesMap() — a group whose local config entry is lost (for any reason) is invisible to the merge logic, dropping its component from the root package set. GroupToRootComponentsCleanupPreservationTest: exercises the active- deletion path via DeploymentService.cleanupGroupData() — any group absent from a single deployment run's GroupMembership snapshot has its GroupToRootComponents entry explicitly removed, regardless of whether the device actually left the group. Both tests encode correct (bug-free) behavior and currently FAIL against mainline, proving the bug exists. They will PASS once the code is fixed to reconcile against actual reported membership rather than relying solely on local config presence.
The set of root components a deployment keeps was computed exclusively from local bookkeeping (GroupToRootComponents). A running root component whose group record is absent - for any reason, including loss of persisted state - was invisible to that computation and silently removed by the merge, even when the cloud membership API confirmed the device still belongs to the group. Destruction of group state was automatic; restoration required a manual cloud redeployment. On devices that auto-submit a local deployment at startup, a single missing record was enough to uninstall running components right after a restart. Removing a running root component now requires positive evidence: - a local deployment explicitly lists it in rootComponentsToRemove; - a cloud deployment targets a group the component is attributed to and its (authoritative) document no longer includes it; or - membership freshly fetched from the cloud in the same run shows the device no longer belongs to any group the component is attributed to. Running roots not accounted for by the document or the preserved group records, and lacking such evidence, are kept at their running version. They participate in dependency resolution only; no group bookkeeping is written for them, so the authoritative repair path (a cloud deployment for their group) is unchanged. The nucleus component and builtin services are out of scope of the preservation. Additionally, an unknown membership response (device not configured to talk to the cloud) is no longer treated as an authoritative "member of no thing groups": it falls back to the persisted membership info, the same as the fetch-failure paths, instead of dropping every thing group from preservation and arming record cleanup. The legitimate removal flows are unchanged and covered by tests: a device that actually left a group has that group's components removed by the next deployment that fetches membership, a cloud deployment remains authoritative for its target group's root set, and explicit local removals are always honored.
cleanupGroupData deletes any group's GroupToRootComponents and GroupToLastDeployment records that are absent from the GroupMembership snapshot. That snapshot is rebuilt from a fresh cloud fetch by the deployment task's own run, and is consumed and removed when the deployment completes. A deployment that completes after a Nucleus restart (bootstrap deployment, e.g. a nucleus upgrade) finishes in a boot session where the snapshot may not have survived the restart. Running cleanup there deleted records for groups the device still belongs to, purely because the evidence was gone; the affected groups' components were then removed by the next deployment's resolution. On multi-group devices, every nucleus upgrade was exposed to this. Deployments completing after a restart now skip the cleanup. They have no fresh membership information, so they have no basis for deleting records; the next regular deployment fetches membership and performs the deferred hygiene. Cleanup behavior for regular completions is unchanged, including removal of records for groups the device has actually left, and the exemption of device-scoped and local-deployment records.
| return Collections.emptySet(); | ||
| } | ||
| try { | ||
| LocalOverrideRequest request = SerializerFactory.getFailSafeJsonObjectMapper() |
There was a problem hiding this comment.
Recommendation generated by Amazon CodeGuru Reviewer. Leave feedback on this recommendation by replying to the comment or by reacting to the comment using emoji.
The ObjectMapper.readValue() method can throw a JsonProcessingException, but this exception is not handled properly here. By simply logging the error and returning an empty value, the exception is ignored. This can mask potential JSON parsing issues, leading to unexpected application behavior or data corruption because the system may continue with invalid or incomplete data. To fix this, catch JsonProcessingException and either handle it appropriately or rethrow it as a custom runtime exception. https://www.ibm.com/support/pages/best-practice-catching-and-re-throwing-java-exceptions
|
Due to Finding 1 below, this change can't be made. Closing Findings 1. (Discussion, should be acknowledged in the PR) After total bookkeeping loss, a cloud redeployment of a component's own group cannot remove it. If both 2. (Behavior change beyond the incident scenario) Components provisioned via initial config file are now preserved. Any non-builtin direct dependency of 3. (Edge case) 4. (Tradeoff worth stating) Preservation converts silent removal into potential deployment failure. A preserved component is pinned to its exact running version; if a new deployment's dependency closure conflicts with that pin, resolution now throws 5. (Nit) Test method naming deviates from repo convention. The codebase uses 6. (Nits)
|
|
Reworked per the review findings and continued in #1826 (this PR could not be reopened after the rework was pushed to the branch). Key change: the fix is rescoped to local deployments only — cloud deployment removal semantics are unchanged, addressing finding 1. Findings 2–6 are also addressed; details in #1826's description. |
Summary
Fixes silent removal of running components by local deployments when thing-group bookkeeping has been lost, plus two related defects in group-state handling. Cloud deployment semantics are unchanged: a component is removed when the cloud deployment no longer contains it.
The defects
GroupToRootComponentsrecords. A running root component whose record was absent — for any reason, including loss of persisted state — was silently removed by a local deployment's merge, even whenListThingGroupsForCoreDeviceconfirmed the device still belongs to the group. Local deployments are explicit add/remove deltas (rootComponentVersionsToAdd/rootComponentsToRemove); they have no authority for implicit removals. Observed in the field as a device's components being uninstalled by an auto-submitted startup local deployment right after a restart.Optional.empty()membership response (device not configured to talk to the cloud) was coerced to "member of no thing groups", dropping every recorded group from preservation and arming record cleanup.DeploymentService.cleanupGroupData()deletes records absent from theGroupMembershipsnapshot. Deployments completing after a Nucleus restart (bootstrap, e.g. nucleus upgrades) run in a boot session where that snapshot may not have survived — deleting records of groups the device still belongs to. Multi-group devices were exposed on every nucleus upgrade.The fix
A local deployment now removes a running root component only with positive evidence: the request explicitly lists it in
rootComponentsToRemove, or membership freshly fetched from the cloud in the same run shows the device no longer belongs to any group the component is attributed to (viaComponentToGroups). Running roots lacking such evidence are preserved at their running version, with a warning log. Preservation is resolution-only (no bookkeeping writes), fails closed for versionless services, and excludes the nucleus component and builtin services. Unknown membership falls back to persisted membership info (same as the existing fetch-failure paths). Deployments completing after a restart skip record cleanup; the next regular deployment performs the deferred hygiene with fresh data.Behavior notes
thing/) and local-deployment records remain exempt from membership-based cleanup.Tests
ThingGroupMembershipPreservationTest: 3 regression tests (verified failing with the fix disabled) — lost-record local deployment, unknown-membership handling, post-restart cleanup; plus control, legitimate-flow, fail-closed, and cloud-semantics pinning tests (verified passing both with and without the fix).GroupToRootComponentsCleanupPreservationTest: exercises the realDeploymentServicecleanup path (the original revision inlined a copy of the logic and could not observe a fix).The original revision's headline test asserted that a lost record's components could be restored from the membership response alone; that is not implementable (membership returns only group names, and no device data-plane API fetches a group's component list without its deployment ID, lost in the same scenarios). The reworked contract preserves what is running during local deployments instead.
mvn testrun with checkstyle/PMD/spotbugs active: 1192 tests, the only failure is a pre-existing one unrelated to this change (verified identical on the unmodified branch).MultiGroupDeploymentTestintegration suite at exact parity with the unmodified-branch baseline.Related
Companion to #1824, which fixes the loss of persisted configuration (builtin service config protection) that produced the missing-record state in the field. #1824 prevents the state loss; this PR removes the local-deployment amplification of it. Also related: #1823 (characterization tests for the #1824 defect).