Skip to content

Bugfix/28537460 Fix Auto assessment status stuck in-progress - #386

Merged
SathishMSFT merged 14 commits into
masterfrom
bugfix/28537460-type-simple
Oct 6, 2026
Merged

SathishMSFT merged 14 commits into
masterfrom
bugfix/28537460-type-simple

Conversation

@SathishMSFT

Copy link
Copy Markdown
Member

Issue:

MsftLinuxPatchAutoAssess.service declared Type=forking, but the shell wrapper it executes runs Core in the foreground and never daemonizes. systemd therefore treated the entire assessment as service startup and SIGTERM'd the cgroup at TimeoutStartSec (default 90s), before Core could write terminal status. Azure Update Manager was left showing a stale "In Progress" assessment.

fix:

  1. The fix we are doing here is to declare MsftLinuxPatchAutoAssess.service to Type=simple. This makes Auto assess service to run without terminating after 90 seconds.

  2. This introduces newer issue. What if the AutoAssess service stalls/ hung indefinitely:
    To fix this part, we call the bash script for auto assessment with a time out which sends termination signal at 50 minutes and kills the process at 55 minutes (if it's still running).

With both these fixes, Auto assessment service can run to completion taking > 30 seconds and upto 50 minutes. In our queries, we found that on-demand assessment operation`s p99 was close to 10 minutes. This should be sufficiently enough complete the auto assesses operations and set the status to terminal state.

test:

  1. Added unit tests. could not simulate time out as tests are in windows. so only check of script details.
  2. verified the same in ubuntu 22, RHEL8 and SLES12 VMs. And also in ARC

SathishMSFT and others added 9 commits September 9, 2026 13:19
…ug #28537460)

MsftLinuxPatchAutoAssess.service declared Type=forking, but the shell wrapper it
executes runs Core in the foreground and never daemonizes. systemd therefore
treated the entire assessment as service startup and SIGTERM'd the cgroup at
TimeoutStartSec (default 90s), before Core could write terminal status. Azure
Update Manager was left showing a stale "In Progress" assessment.

Measured impact before the fix: 17.36% of auto-assessment runs were killed
mid-flight; 20.5% of ~266k Azure Linux VMs had at least one stuck run in 24h.
Over a month, 19,179 VMs across 8,275 subscriptions were persistently stuck, a
median of 26 days out of 31. Killed runs were hard-capped at 89.94s with zero
runs observed between 92s and 600s - a guillotine at the deadline, not a tail.

Changes

1. ServiceManager generates Type=simple, which matches how the wrapper actually
   behaves. This also removes a circular wait: under forking, systemctl start
   blocked for the whole startup window, so a ConfigurePatching call issued
   during an in-flight assessment stalled until TimeoutStartSec while the
   assessment itself waited on that same Core operation. Raising the timeout
   made that stall worse, not better; Type=simple removes it entirely.

2. The generated wrapper bounds each run with GNU timeout (50m budget, 3m kill
   grace). Type=simple alone would trade a common failure for a rarer permanent
   one: a hung run keeps the unit active forever, and systemd will not start a
   second instance of an active unit, so every later hourly timer fire becomes a
   silent no-op. Under forking this self-healed by accident because the unit
   went failed and the timer restarted it.

   The bound lives in the wrapper rather than the unit because it must hold on
   every systemd version in the fleet. Roughly 6.9% of the periodic-assessment
   Linux fleet - 19,050 machines - runs pre-229 systemd (EL7 219, SLES 12 228,
   Amazon Linux 2 219), where unit-level runtime limits are silently ignored
   rather than rejected. coreutils timeout is present on all of them.

   timeout is called unconditionally, with no "command -v" guard. It is already
   an unguarded hard dependency in EnvHealthManager.check_sudo_status and
   Bootstrapper.check_sudo_status ("timeout 10 sudo id"), and that sudo check
   runs inside ActionHandler.setup with raise_if_not_sudo=True before this
   wrapper is generated - so a machine without timeout fails setup outright and
   a fallback branch would be unreachable while being the only unbounded path in
   the design. If timeout ever were missing, exec fails with 127 and the unit
   goes failed, which the timer retries: loud and bounded rather than hanging.

   Budget + grace (3180s) is sized to complete before the next hourly fire so
   that fire always finds an inactive unit. The invariant is asserted in tests.

Deliberately not included

   No SuccessExitStatus. When the wrapper bounds a runaway, timeout exits 124
   and the unit enters failed. That is left visible on purpose: Core is
   SIGTERM'd without a signal handler and cannot report the timeout itself, so
   the failed unit is the only fleet-visible evidence the bound fired. A run
   exceeding 50 minutes against a p99 of ~35 seconds is genuinely abnormal. The
   timer restarts a failed unit regardless, so masking it buys nothing. The
   rationale is recorded on create_service_unit_file.

Verification

   Live on Azure VMs across Ubuntu 22.04 (systemd 249), RHEL 8.9 (239) and
   SLES 12 SP5 (228), with the extension generating the unit and wrapper itself:
   normal runs return systemctl start in 0s and complete in 20-35s; a hung run
   is bounded and exits 124; no orphan processes; the next timer fire recovers.
   An InstallPatches issued during an in-flight assessment succeeded in 285s
   with 0s ConfigurePatching stall, where forking deadlocked for 600s and
   returned CompletedWithWarnings.

   Unit tests: ServiceManager, both LifecycleManagers, ConfigurePatchingProcessor
   and ProcessHandler. Tests pin Type=simple on the production creation path, the
   wrapper shape, the absence of any conditional fallback, that the budget
   constants are ints, and that budget + grace stays inside the timer interval.
   test_auto_assess_sh_actually_terminates_a_hung_run executes the generated
   script against a stub Core that sleeps 120s and asserts it is killed at the
   budget with exit 124; it is POSIX-only and skips on Windows, where CI runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91444677-8618-4dc0-8cf1-f415f7cb7185
Independent review flagged two issues in Test_ProcessHandler.

assertNotIn("else", ...) was a load-bearing check written as a four-letter
substring match. It is brittle - an "#else" in a comment would break it - and
more importantly it had a real hole: a case/esac fallback contains neither
"command -v timeout" nor "else", so it passed the old assertions while still
introducing an unbounded execution path. Verified: a case/esac variant passes
the old check and fails the new one.

Assert the structure instead. The wrapper must be exactly one exec, and that
exec must be the bounded one on the final line. Any conditional fallback adds a
second exec or displaces the last line, so this catches the whole class rather
than two spellings of it.

Also removes an unused "import time", left behind when a live-timing test was
deleted.

Tests only - no behavioural change, so the existing live verification across
Azure VMs and Arc-enabled servers (Ubuntu 22.04, RHEL 8.10, SLES 12 SP5) still
applies. Full extension suite: 121 tests OK. Core ServiceManager, both
LifecycleManagers and ConfigurePatchingProcessor: 36 passed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91444677-8618-4dc0-8cf1-f415f7cb7185
…37460)

The existing comment explained why the run must be bounded but not what the
bound has to stay under, or where that number comes from. A future editor
raising the budget would read "must be externally bounded", conclude the run is
still bounded, and not realise that exceeding the hourly timer interval silently
restores the original bug - a hung run keeps the unit active, systemd refuses a
second instance, and every later fire is a no-op.

The interval lives in a different package (core AUTO_ASSESSMENT_CRON_INTERVAL,
passed explicitly by TimerManager.create_and_set_timer_idem), so nothing near
these constants hints at it. Name it here instead of restating what the constant
names already say.

Comment only - no code, test, or behaviour change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 91444677-8618-4dc0-8cf1-f415f7cb7185
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.03%. Comparing base (c81c495) to head (4cc7d47).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #386      +/-   ##
==========================================
+ Coverage   94.98%   95.03%   +0.04%     
==========================================
  Files         113      113              
  Lines       21955    21994      +39     
==========================================
+ Hits        20855    20901      +46     
+ Misses       1100     1093       -7     
Flag Coverage Δ
python27 95.03% <100.00%> (+0.04%) ⬆️
python312 95.03% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The generated shim assumes timeout is present with no guard/fallback, and the new test currently hard-codes the absence of such a defensive check, making the runtime behavior and future hardening more fragile.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes systemd service semantics for MsftLinuxPatchAutoAssess.service by switching the unit to Type=simple and bounding auto-assessment runtime so assessments can complete and reliably reach a terminal status without being SIGTERM’d by TimeoutStartSec.

Changes:

  • Change generated systemd unit default Type= from forking to simple for the auto-assessment service.
  • Wrap the auto-assessment shim execution in timeout with TERM + KILL grace to prevent indefinite hangs.
  • Add unit tests to validate the generated unit file and shim script content.
File summaries
File Description
src/extension/src/ProcessHandler.py Generates the auto-assess shim to run Core under timeout with TERM/KILL bounds.
src/extension/src/Constants.py Introduces timeout/grace constants used for bounding auto-assessment runtime.
src/extension/tests/Test_ProcessHandler.py Adds a unit test verifying the generated shim includes bounded timeout execution.
src/core/src/core_logic/ServiceManager.py Changes systemd unit generation default from Type=forking to Type=simple.
src/core/tests/Test_ServiceManager.py Adds tests asserting the generated unit uses Type=simple (and not forking/notify).
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/extension/src/ProcessHandler.py
Comment thread src/extension/tests/Test_ProcessHandler.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread src/extension/src/Constants.py
Comment thread src/extension/src/Constants.py
@nikhim-um
nikhim-um self-requested a review September 17, 2026 06:46
@GAURAVRAMRAKHYANI

Copy link
Copy Markdown
Contributor

Have you manually tested the scenario where the auto-assessment remains stuck for more than 50 minutes and verified that the process is successfully terminated?

@SathishMSFT SathishMSFT changed the title Bugfix/28537460 type simple Bugfix/28537460 Fix Auto assessment status stuck in-progress Sep 17, 2026
Comment thread src/extension/src/Constants.py
@SathishMSFT

Copy link
Copy Markdown
Member Author

Have you manually tested the scenario where the auto-assessment remains stuck for more than 50 minutes and verified that the process is successfully terminated?

Verified by manually setting smaller timeout and adding delay in the package manager commands

@yashnap yashnap left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please rebase

@rane-rajasi Rajasi Rane (rane-rajasi) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you attach your test logs in the PR? If not already done, run this on Azure VM/s and fetch logs under /var/log/azure/Microsoft.CPlat.Core.LinuxPatchExtension/
.core.log
.aa.core.log

Attest the time taken for both a regular patch operation and an auto assess operation to complete. (Auto assessments run hourly)

@SathishMSFT

Copy link
Copy Markdown
Member Author

2.core.log
2.aa.core.log
Attaching log files requested by Rajasi.
Time taken for Config Patching: 62 seconds.
Time taken for auto-assessment run: 77 seconds.

@SathishMSFT
SathishMSFT merged commit 3066008 into master Oct 6, 2026
10 of 11 checks passed
@SathishMSFT
SathishMSFT deleted the bugfix/28537460-type-simple branch October 6, 2026 05:41
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.

6 participants