Restart component when a revision changes its generated unit - #1163
Open
allanli4 wants to merge 2 commits into
Open
Restart component when a revision changes its generated unit#1163allanli4 wants to merge 2 commits into
allanli4 wants to merge 2 commits into
Conversation
A revision that keeps the component version but edits the recipe rewrites the systemd unit and then never applies it: the redeploy gate keys only on version-string equality and a configurationUpdate key, so the component is skipped, systemd is never reloaded, and the process keeps running under the old identity. RequiresPrivilege is the visible case, since it maps to the unit's User= and Group=. convert_to_unit now reports whether generating the units changed any unit file on disk, compared before create_unit_file's O_TRUNC discards the previous content, and the gate honours it. An unchanged recipe generates identical content, so healthy components are not restarted needlessly.
CI's clang-tidy rejected the previous commit: adding a per-phase change check to convert_to_unit pushed its cognitive complexity to 27, over the threshold of 25. Moving the check next to the write it belongs with removes the three added branches and returns the function to its previous complexity, rather than suppressing the checker. Also applies clang-format 19.1.7, which CI's formatting check requires and which is not installed on the development host. Behaviour preserving: the same comparison runs for the same phases in the same order, before the same O_TRUNC write.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A component revision that keeps the same version but edits the recipe rewrites the generated systemd
unit and then never applies it.
RequiresPrivilegeis the visible case, since it maps to the unit'sUser=/Group=: the file on disk getsUser=root, the process keeps running asggcore, and thedeployment reports success.
Root cause.
convert_to_unit()(deployment_handler.c:3607) regenerates units unconditionally,above the redeploy gate. The gate at
:3620wascomponent_updated || is_component_config_updated(...),where
component_updatedis decided by version-string equality alone (:3310-3334, next to a TODOnaming this exact gap) and
is_component_config_updated()inspects only the deployment document'sconfigurationUpdate. Neither consults the recipe. On a same-version revision both are false,gghealthd reports the component
RUNNING, and:3674logs"Component ... is already running. Will not redeploy."— so the component never enterscomponents_to_deployand everything behindif (components_to_deploy.map.len != 0)is skipped as a set: thesystemctl stop, thelink, theenable, and the onlydaemon-reload. The trailingsystemctl start greengrass-lite.targetis ano-op on an already-active unit.
This also explains the reporter's workaround: removing the component deletes
services/<name>wholesale(
stale_component.c:207), so the next deployment finds no previous version and treats it as new.The change.
convert_to_unit()now reports whether generating the units actually changed any unitfile, via a new
HasPhase.unit_changed, and the gate honours it. The comparison must happen inside unitgeneration because
create_unit_file()opens withO_TRUNC: old and new content coexist only until itruns, and nothing persists a digest between deployments, so afterwards no caller could recover it.
Everything the issue asks for — regenerate,
daemon-reload, restart — is then done by code that alreadyexisted and was simply unreached. The production change is one conditional, one comparison helper,
and one struct field.
Keying on "did the generated unit change?" rather than on
RequiresPrivilegespecifically is bothsmaller and more general: it covers every recipe field the unit encodes, and it makes the no-op case
free, because an unchanged recipe generates byte-identical content and so restarts nothing.
Related Issue
Fixes #954
Type of Change
Checklist
nix flake check -Llocally— could not be run:
nixis not installed on the dev host, nor areclang-format,clang-tidy,cmake-format,gersemi,prettier,cspellorshellcheck. Soclang-tidy,iwyu,cmake-lint,spelling,build-clang,build-musl-pi, the formatter check and UAT runhere in CI for the first time. Style was conformed to
.editorconfigand surrounding code byinspection. Flagging rather than ticking.
documentation describes the redeploy decision or
RequiresPrivilegeadoption semantics.RequiresPrivilegeappears only in three example fixtures underdocs/examples/supported_lifecyle_types/, anddocs/design/ggdeploymentd-design.mdhas nodescription of restart or redeploy behaviour.
aws-greengrass-testing
(if applicable) — not added there. Two inline unit tests ship in this PR, and the end-to-end
behaviour was verified on a device (below). Happy to add a UAT case if you'd prefer it there.
Documentation Updates
docs/folder — not needed; see above.reboot consequence below is accepted as intended behaviour, it may deserve a public-doc note.
Testing
aws-greengrass-testing repository
Unit — two inline tests in
modules/recipe2unit/src/parser.c;recipe2unit-inline-testgoes 4 → 6tests, 0 failures.
revised_unit_content_reported_as_changedcovers the reported case and gates thefirst deployment as a hard precondition, so it cannot pass because no unit was ever written.
unrevised_unit_content_reported_as_unchangedcovers the no-op case.Each test was verified to bind one direction of the behaviour: neutralising the comparison to always
report changed fails only the second test, and to never report changed fails only the first. Full suite
and build are unchanged from base — the same 3 pre-existing
Not Runtargets and the same 13pre-existing failed objects, all missing system libraries on the dev host (OpenSSL 3.x headers,
systemd/sd-bus.h, olduriparser). Zero diagnostics in the touched files.On a device (EC2, Ubuntu 24.04, both
.debs built on the host, torn down afterwards). Same version1.0.0both steps, recipe edited in place, component deployed alone:d3c2fa3fstep 1User=ggcored3c2fa3fstep 2User=rootUser=ggcoreUser=rootThe MainPID is the discriminator: unchanged on base proves the process was never restarted; changed on
the patched build proves it was. Both directions passed the same precondition gate, and the running
ggdeploymentdbinary was asserted by hash against the expected artifact in each.Note for anyone reproducing: a version bump does not reproduce this. It leaves
component_updatedtrue and routes around the defect entirely.
Additional Notes
Two consequences worth your decision, neither hidden:
bootstrapphase. Entering
components_to_deploymeansprocess_bootstrap_phase()runs, and it ends insystemctl rebootwhen it counts any bootstrap component (bootstrap_manager.c:632). SinceComponentDescriptionis embedded in the unit asDescription=, even a cosmetic edit qualifies. Aversion bump on such a component already reboots today, so this makes same-version edits behave like
version bumps — but it is a policy call about how aggressively lite should adopt a revision, and it's
yours, not mine. The narrower alternative is to exclude cosmetic fields from the comparison; I did
not take it because it needs a field allowlist that is silently wrong the moment a field is added.
services/<name>/versionis written unconditionally at
:3384, well before the gate, so if a deployment fails after unitgeneration, a retry sees a matching version and matching on-disk units and skips the component again.
This is not introduced here — at base the same-version case never worked at all — but it bounds the
fix. The robust form persists a digest of the generated units alongside the version, which belongs
with the standing TODO at
:3709aboutHasPhasebeing populated and then ignored.One question for you:
HasPhase.unit_changedputs a field that is not about phases on a type calledHasPhase. I chose it over a new out-param becauseHasPhaseis already the out-param bundle thecaller declares, so no signature change is needed and the
test_modules/recipe2unit-testexample driverkeeps compiling (verified with
-DBUILD_EXAMPLES=1). Happy to rename the type if you'd rather.Correctness note: the comparison uses
gg_file_read, which returns the true byte count at EOF. Thesibling
gg_file_read_exactwould have made every sub-2048-byte unit report "changed", silently turningthis into "restart everything on every deployment". Failure directions are all conservative — an absent
or unreadable unit counts as changed — so the fix can cause an unnecessary restart but never miss a real
change. This does rely on unit generation being deterministic for a fixed recipe, which it is today
(no timestamp, pid or ordering dependence); a future non-deterministic field would break the no-op case.
Out of scope, filed separately in spirit:
unit_file_generator.c:761fchowns a component's workdirectory to the configured
posixUserunconditionally, never consultingis_root, so a privilegedcomponent runs as root with a
ggcore-owned work directory. That is the second half of what #954reports, but it is wrong on a first deployment too, so it is an independent defect and folding it in
would have widened this diff.
This PR was prepared with AI assistance; every claim above was verified by execution on the device or
in the test suite.
By submitting this pull request, I confirm that you can use, modify, copy, and
redistribute this contribution, under the terms of your choice.