Skip to content

fix(agent): run interrupted software list requests again on restart - #4388

Open
jarhodes314 wants to merge 3 commits into
thin-edge:mainfrom
jarhodes314:jarhodes314/fix-resume-builtin-executing
Open

jarhodes314 wants to merge 3 commits into
thin-edge:mainfrom
jarhodes314:jarhodes314/fix-resume-builtin-executing

Conversation

@jarhodes314

@jarhodes314 jarhodes314 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Proposed changes

When tedge-agent exits while software_list requests are waiting in the software manager's in-memory queue (e.g. the RestartRequired exit after a self-update), those requests are lost. The restarted agent resumes them at executing, an await-operation-completion step waiting for a builtin operation that will never answer, so they never complete. With #4361, the c8y mapper skips new software_list requests while one is pending, so Cumulocity is left with a stale software list.

 WorkflowSupervisor::resume_command
   AwaitingAgentRestart -> on_success
+  AwaitOperationCompletion with no sub-command
+    if the step leading here is builtin:software_list
+      move back to that step  // the request is sent again to the software manager
   _ -> unchanged

 SoftwareManagerActor::process_pending_sm_operation
   pending SoftwareUpdateCommand -> report failed
-  pending SoftwareListCommand -> report failed
+  pending SoftwareListCommand -> log and drop  // re-run by the workflow resume

The software manager change is needed alongside the resume change: for the request the software manager was running when the agent stopped, its "cancelled due to unexpected agent restart" failure completed the command first, then the result of the re-sent request was published on top of a failed (possibly already cleared) command.

Tests:

  • New Robot suite tests/tedge_agent/software_list_resume (no cloud credentials needed): a test plugin makes list slow, one software_list request is being run by the software manager while a second is queued, then tedge-agent is restarted. Both must reach successful and none may be published as failed. On main, both requests stay at executing. With only the resume change, robot-running is published as failed and then with its currentSoftwareList. With this PR, it passes.
  • software_list_interrupted_by_an_agent_restart_is_run_again_on_resume: the workflow actor is aborted after sending the request and respawned on the same state dir; the request must reach the software manager again. Fails on main.
  • software_list_interrupted_by_a_restart_is_not_reported_as_failed: the first message from the software manager on startup must be SoftwareCommandMetadata, not a failed SoftwareListCommand. Fails on main.
  • software_list_of_a_legacy_builtin_workflow_interrupted_by_an_agent_restart_is_run_again_on_resume: guards that legacy action = "builtin" workflows are still re-run once the failure is dropped.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Improvement (general improvements like code refactoring that doesn't explicitly fix a bug or add any new functionality)
  • Documentation Update (if none of the other choices apply)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Paste Link to the issue

Needed by #4361, whose Tedge Self Update tests fail because of this bug.

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA (in all commits with git commit -s. You can activate automatic signing by running just prepare-dev once)
  • I ran just format as mentioned in CODING_GUIDELINES
  • I used just check as mentioned in CODING_GUIDELINES
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Further comments

Merge Danger

Door: two-way

No persisted format changes; reverting restores the previous behaviour.

Blast Radius: software_list

A software_list interrupted by an agent restart is now re-run on every restart rather than failed once, so during an agent crash loop such a request shows as executing until the loop ends, rather than failed.

Limits

Only software_list is re-run, since re-running a software_update is not safe. Custom workflows using builtin:software_list are covered too, as the trigger step is looked up in the workflow. Workflows using the legacy action = "builtin" form were already re-run on resume, as their command board stays at scheduled. Child devices running an older agent still have the bug.

Alternative considered

A mapper-side alternative (have the c8y mapper clear pending software_list requests when the agent restarts) was rejected: it clears commands that haven't reached a terminal state, which breaks the command contract, and the agent still publishes the late result of a cleared command, bringing the cleared topic back.

🤖 Generated with Claude Code

A software_list request waiting in the software manager queue when the
agent exits, e.g. after a self-update, was lost, and the restarted agent
kept the command at executing forever.

On resume, a software_list command awaiting its builtin operation is now
moved back to the step that triggers that operation, so the request is
sent again to the software manager. Other builtin operations are left
unchanged, as re-running a software update is not safe.

The software manager no longer reports as failed a software list request
it was running when the agent stopped. That failure completed the command
before the result of the request sent again on resume was published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: James Rhodes <jarhodes314@gmail.com>
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.56604% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/core/tedge_api/src/workflow/supervisor.rs 85.91% 4 Missing and 6 partials ⚠️

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
Contributor

Robot Results

✅ Passed ❌ Failed ⏭️ Skipped Total Pass % ⏱️ Duration
1048 0 8 1048 100 2h33m14.630215s

@Bravo555 Bravo555 self-assigned this Oct 26, 2026
Comment on lines +241 to +266
/// Return the builtin operation triggered just before the current state of a command,
/// along with the state triggering this builtin operation
fn builtin_operation_trigger_state(
&self,
command_state: &GenericCommandState,
) -> Option<(&OperationName, &StateName)> {
let operation_name = command_state.operation()?;
let version = command_state.workflow_version()?;
let entry = self.commands.entry(&command_state.topic.name)?;
let workflow = self
.workflows
.get(&(entry.entity_type, operation_name.as_str().into()))?
.get(version)
.ok()?;
workflow
.states
.iter()
.find_map(|(state, action)| match action {
OperationAction::BuiltInOperation(operation, handlers)
if handlers.on_exec.status == command_state.status =>
{
Some((operation, state))
}
_ => None,
})
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: What if in a custom workflow there's more than 1 possible state that triggered command we're awaiting?

For example:

operation = "software_list"

[init]
script = "/etc/tedge/operations/pick-action.sh"
on_stdout = ["list_first", "update_first"]

[list_first]
operation = "builtin:software_list"
on_exec = "executing"

[update_first]
operation = "builtin:software_update"
on_exec = "executing"

[executing]
action = "await-operation-completion"
on_success = "successful"

[successful]
action = "cleanup"

[failed]
action = "cleanup"

From my understanding, because we find and return the first state that matches (which starts a builtin operation and has on_exec to current state), and iteration order of the hashmap is unstable, when actually running the workflow, we could possibly enter executing via update_first, but then on restart execution state could be moved back to list_first. (though i'm not 100%, I haven't actually tried that)

Default sotfware_list workflow will not suffer from this, but I think this change seems to brake such (IMO valid) custom workflows. I see possible ways forward:

  • [AI recommended] for the purposes of this PR, add check if there is only 1 possible previous state we could transition from, and if not, fall back to the previous behaviour
  • consider such workflows, using one state to await multiple different operations, invalid
  • or, if we still want this retry to work in custom workflows, I think there's no other way but to actually record at runtime which states we go through and use that runtime information to go back to the previous state, which would be much more complex change but potentially reusable for other scenarios

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a reasonably rare case. IMO the core behaviour we're trying to fix here is "the agent drives the operation to a terminal state". In my mind, this means either resuming operations or marking them as failed if we can't do that (e.g. with this case where there's an ambiguity). @albinsuresh, do you have thoughts here?

Recording the state transitions the workflow took would be neat and I can see how that would be useful (we could feasibly show a user the trace of an operation somewhat like home assistant does for automations). I don't think that's a thing this PR wants to go into though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AI] The exact workflow above is rejected when the agent loads it. Roughly what tedge-agent does on loading a workflow file:

load(workflow_file)
  for each state in workflow
    if state.action is builtin:<op> and <op> != workflow.operation
      error "The `builtin:<op>` cannot be invoked from `<workflow.operation>`, but only from `<op>`"
      register a placeholder workflow for <workflow.operation>
        that fails every command with "Invalid operation workflow definition ..."

I ran tedge-agent with it to confirm: a software_list command goes init → executing → failed with that reason, so it never reaches the resume code. There's now a test that loads your workflow and checks it's rejected.

Your point still found a real case though: the awaiting state can be shared with a sub-operation step (operation = "some_custom_op", on_exec = "executing"). That step persists the command at executing before publishing the sub-command, so if the agent stops in between, resume saw no sub-command and moved the command back to the builtin:software_list step, running a software list the command never asked for. Resume now only re-runs the builtin when every step leading to the awaiting state is builtin:software_list, and otherwise leaves the command as it was before this PR (test: command_awaiting_a_state_shared_by_a_builtin_and_a_sub_operation_is_resumed_unchanged). Several builtin:software_list steps sharing the state are still re-run: any of them sends the same request and waits in the same state, which another test checks.

On failing commands we can't resume: in that ambiguous case resume can't tell a lost sub-command from one that is about to arrive, as the sub-command init is published retained with QoS 1 and is only processed after resume. Failing it there would fail commands that would have completed. The place to guarantee a terminal state is the timeout_second/on_timeout of await-operation-completion, which the docs describe but the agent doesn't implement yet (TODO: Register a timeout event in the workflow actor). I'll raise that as a follow-up rather than grow this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AI] Follow-up raised as #4397: await-operation-completion ignores timeout_second and on_timeout.

…start

Signed-off-by: James Rhodes <jarhodes314@gmail.com>
@Bravo555 Bravo555 removed their assignment Oct 27, 2026
@jarhodes314 jarhodes314 added this to the 2.1.0 milestone Oct 28, 2026
…start

Signed-off-by: James Rhodes <jarhodes314@gmail.com>

This branch is waiting to be deployed

1 inactive (outdated) and 1 waiting deployments
Test Pull Request — 17813ad4 Waiting Oct 28, 2026 by jarhodes314 via Approve #8813
Test Auto — 74870203 Deployed Oct 23, 2026 by jarhodes314 via Test x86_64 #8785
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.

2 participants