diff mbox series

[layerindex-web] rrs_upgrade_history: don't save a failed commit as the checkpoint

Message ID 20261115174243.10055-2-tim.orling@konsulko.com
State Under Review
Delegated to: Tim Orling
Headers show
Series [layerindex-web] rrs_upgrade_history: don't save a failed commit as the checkpoint | expand

Commit Message

Tim Orling Nov. 15, 2026, 5:42 p.m. UTC
A commit whose analysis failed was still saved as the layer branch's
checkpoint, so it was never analysed again. On a test instance a
container that ran out of processes failed every commit for two hours
and moved three checkpoints to the latest commit. Stop the layer branch
at a failed commit instead, so the next run retries it, and exit with
status 1. A commit that times out is still skipped.

[YOCTO #16515]

AI-Generated: Claude Opus 5.5
Signed-off-by: Tim Orling <tim.orling@konsulko.com>
---
This applies on top of "rrs_upgrade_history: skip a commit that takes too
long to analyse" (v2), which is on master-next.

Tested on a test instance: run in a container without --init and with
--pids-limit 80, the analysis started failing after about 70 commits;
each layer branch stopped at its first failed commit, kept the last good
one as its checkpoint, and the script exited 1.

 rrs/tools/rrs_upgrade_history.py  | 22 ++++++++++++++---
 tests/test_rrs_upgrade_history.py | 40 +++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 3 deletions(-)
diff mbox series

Patch

diff --git a/rrs/tools/rrs_upgrade_history.py b/rrs/tools/rrs_upgrade_history.py
index b20ef4e..fff98d2 100755
--- a/rrs/tools/rrs_upgrade_history.py
+++ b/rrs/tools/rrs_upgrade_history.py
@@ -97,11 +97,17 @@  def run_internal(maintplanlayerbranch, commit, commitdate, options, logger, bitb
         ret, output = utils.run_command_interruptible(cmd, timeout=options.commit_timeout or None)
     except subprocess.TimeoutExpired:
         logger.error('Analysing commit %s timed out after %d seconds; skipping it' % (commit, options.commit_timeout))
-        return
+        return True
     if ret == 254:
         # Interrupted by user, break out of loop
         logger.info('Update interrupted, exiting')
         sys.exit(254)
+    if ret:
+        # The caller then doesn't save this commit as the checkpoint, so the
+        # next run tries it again (AI-generated, Claude Opus 5.5)
+        logger.error('Analysing commit %s failed with exit code %d' % (commit, ret))
+        return False
+    return True
 
 """
     Upgrade history handler.
@@ -137,6 +143,7 @@  def upgrade_history(options, logger):
     if not lockfile:
         logger.error("Layer index lock timeout expired")
         sys.exit(1)
+    failed = False
     try:
         for maintplan in maintplans:
             # Check releases and milestones
@@ -221,7 +228,10 @@  def upgrade_history(options, logger):
 
                     ct, ctepoch = commit_list.pop(0).split()
                     ctdate = datetime.fromtimestamp(int(ctepoch))
-                    run_internal(maintplanbranch, ct, ctdate, options, logger, bitbake_map, initial=True)
+                    if not run_internal(maintplanbranch, ct, ctdate, options, logger, bitbake_map, initial=True):
+                        logger.error('%s: initial import failed, skipping this layer branch' % layerbranch)
+                        failed = True
+                        continue
 
                 if layerbranch.vcs_subdir:
                     layersubdir_start = layerbranch.vcs_subdir
@@ -263,13 +273,19 @@  def upgrade_history(options, logger):
                             logger.debug("Skipping commit %s" % ct)
                             continue
                         logger.debug("Analysing commit %s ..." % ct)
-                        run_internal(maintplanbranch, ct, ctdate, options, logger, bitbake_map)
+                        if not run_internal(maintplanbranch, ct, ctdate, options, logger, bitbake_map):
+                            # Stop here so the next run starts again from this commit
+                            logger.error('%s: stopping at commit %s' % (layerbranch, ct))
+                            failed = True
+                            break
                         if not (options.dry_run or options.filter_files):
                             maintplanbranch.upgrade_rev = ct
                             maintplanbranch.upgrade_date = ctdate
                             maintplanbranch.save()
     finally:
         utils.unlock_file(lockfile)
+    if failed:
+        sys.exit(1)
 
 if __name__=="__main__":
     parser = optparse.OptionParser(usage = """%prog [options]""")
diff --git a/tests/test_rrs_upgrade_history.py b/tests/test_rrs_upgrade_history.py
index 6bf3582..7788c33 100644
--- a/tests/test_rrs_upgrade_history.py
+++ b/tests/test_rrs_upgrade_history.py
@@ -41,3 +41,43 @@  def test_bitbake_revision_without_limit():
     log = logging.getLogger('test')
     assert remapped_bitbake_rev('a', datetime(2030, 1, 1), {'a': ('b', None)}, log) == 'b'
     assert remapped_bitbake_rev('c', datetime(2030, 1, 1), {'a': ('b', None)}, log) == 'c'
+
+
+def run_internal_with(monkeypatch, result):
+    import subprocess
+    import types
+    from layerindex import utils
+    import rrs_upgrade_history
+
+    def run(cmd, timeout=None):
+        if isinstance(result, Exception):
+            raise result
+        return result, ''
+
+    # The test settings don't install the rrs app; run_internal() only needs
+    # Release for an initial import
+    monkeypatch.setitem(sys.modules, 'rrs.models', types.SimpleNamespace(Release=None))
+    monkeypatch.setattr(utils, 'runcmd', lambda *args, **kwargs: 'abc123')
+    monkeypatch.setattr(utils, 'run_command_interruptible', run)
+    plan_branch = types.SimpleNamespace(python3_switch_date=datetime(2016, 6, 2),
+                                        python3_environment=None,
+                                        layerbranch=types.SimpleNamespace(id=1))
+    options = types.SimpleNamespace(commit_timeout=5, filter_files='', dry_run=False, loglevel=logging.INFO)
+    return rrs_upgrade_history.run_internal(plan_branch, 'deadbeef', datetime(2026, 2, 27),
+                                            options, logging.getLogger('test'), {})
+
+
+def test_run_internal_success(monkeypatch):
+    assert run_internal_with(monkeypatch, 0) is True
+
+
+def test_run_internal_failure(monkeypatch, caplog):
+    # [YOCTO #16515] a failed commit must not be saved as the checkpoint
+    with caplog.at_level(logging.ERROR):
+        assert run_internal_with(monkeypatch, 1) is False
+    assert 'deadbeef failed with exit code 1' in caplog.text
+
+
+def test_run_internal_timeout_is_skipped(monkeypatch):
+    import subprocess
+    assert run_internal_with(monkeypatch, subprocess.TimeoutExpired('cmd', 5)) is True