diff mbox series

[layerindex-web] layerindex: wait for a running update in the Update Layer button

Message ID 20261114045644.91749-1-tim.orling@konsulko.com
State Under Review
Delegated to: Tim Orling
Headers show
Series [layerindex-web] layerindex: wait for a running update in the Update Layer button | expand

Commit Message

Tim Orling Nov. 14, 2026, 4:56 a.m. UTC
When another update held the layer index lock, e.g. the regular full
update, the update started by the Update Layer button gave up after 30
seconds. Wait up to 12 hours instead. While waiting, lock_file() retried
without pausing, using a whole CPU, and logged every 5 seconds; have it
pause between tries, and log once and then once a minute, so that the
task page shows that the update is waiting for another one.

[YOCTO #15395]

AI-Generated: Claude Opus 5.5
Signed-off-by: Tim Orling <tim.orling@konsulko.com>
---
This applies on top of "[PATCH 0/3] Fix the Update Layer button" (bug
15395), and on master-next. It is the part of the bug about waiting
for a running update.

Tested on a test instance with a Celery worker: with the layer index
lock held for 100 seconds, the Update Layer button's task log showed
"Waiting for another update to finish" at 0 and 60 seconds, and the
update then ran and succeeded. Before, it failed with "Layer index lock
timeout expired" after 30 seconds.

 layerindex/utils.py         | 12 ++++++------
 layerindex/views.py         |  4 +++-
 tests/test_update_button.py |  2 ++
 tests/test_utils.py         | 24 ++++++++++++++++++++++++
 4 files changed, 35 insertions(+), 7 deletions(-)


base-commit: 08e58cdece30019a6f9e5d14af16ebe20afa6177
prerequisite-patch-id: 23026c7697b245494ae240bea1d68b9fcaa8ba63
prerequisite-patch-id: 26ce753e3fb271485dbcf24e2c2e518acb4bcb82
prerequisite-patch-id: 74a73c75f97102b56617334eff8faae8c94e24df
diff mbox series

Patch

diff --git a/layerindex/utils.py b/layerindex/utils.py
index fd29de0..f10c501 100644
--- a/layerindex/utils.py
+++ b/layerindex/utils.py
@@ -451,8 +451,7 @@  class ListHandler(logging.Handler):
 
 def lock_file(fn, timeout=30, logger=None):
     start = time.time()
-    last = start
-    counter = 1
+    last = None
     while True:
         lock = open(fn, 'w')
         try:
@@ -463,11 +462,12 @@  def lock_file(fn, timeout=30, logger=None):
             current = time.time()
             if current - start > timeout:
                 return None
-            # Print a message in every 5 seconds
-            if logger and (current - last > 5):
+            # Say why we are waiting, then once a minute
+            if logger and (last is None or current - last >= 60):
                 last = current
-                logger.info('Trying to get lock on %s (tried %s seconds) ...' % (fn, (5 * counter)))
-                counter += 1
+                logger.info('Waiting for another update to finish (lock %s held, waited %d of up to %d seconds) ...'
+                            % (fn, current - start, timeout))
+            time.sleep(1)
 
 def unlock_file(lock):
     fcntl.flock(lock, fcntl.LOCK_UN)
diff --git a/layerindex/views.py b/layerindex/views.py
index e7728ca..8584620 100644
--- a/layerindex/views.py
+++ b/layerindex/views.py
@@ -147,7 +147,9 @@  def update_layer_view(request, template_name, branch='master', slug=None):
 
     from celery import uuid
 
-    cmd = 'layerindex/update.py --layer %s --branch %s --update %%update%%;' % (layeritem.name, branch)
+    # Wait for an update that is already running (e.g. the regular full
+    # update, which can take hours) rather than failing after 30 seconds
+    cmd = 'layerindex/update.py --layer %s --branch %s --update %%update%% --timeout 43200;' % (layeritem.name, branch)
 
     task_id = uuid()
     # Create this here first, because inside the task we don't have all of the required info
diff --git a/tests/test_update_button.py b/tests/test_update_button.py
index bb3b84a..cde153f 100644
--- a/tests/test_update_button.py
+++ b/tests/test_update_button.py
@@ -40,6 +40,8 @@  def test_update_layer_creates_one_update(staff_client, monkeypatch):
     assert Update.objects.filter(task_id=task_id).exists()
     # update.py records its work in the view's Update rather than a new one
     assert '--update %update%' in cmd
+    # and waits for an update that is already running
+    assert '--timeout 43200' in cmd
 
 
 @pytest.mark.django_db
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 95b5e04..396d5ef 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -43,6 +43,30 @@  def test_is_deps_satisfied(fake_bb, req_ver, collections, expected):
     assert utils.is_deps_satisfied('core', req_ver, collections) is expected
 
 
+def test_lock_file_waits_without_spinning(tmp_path, caplog):
+    # [YOCTO #15395] the Update Layer button waits for a running update
+    import fcntl
+    import logging
+    import threading
+    import time
+    fn = str(tmp_path / 'layerindex.lock')
+    held = open(fn, 'w')
+    fcntl.flock(held, fcntl.LOCK_EX)
+    logger = logging.getLogger('test')
+    with caplog.at_level(logging.INFO):
+        start, cpu = time.time(), time.process_time()
+        assert utils.lock_file(fn, timeout=2, logger=logger) is None
+        assert time.time() - start >= 2
+        assert time.process_time() - cpu < 0.5
+    assert len([r for r in caplog.records if 'Waiting for another update' in r.message]) == 1
+
+    threading.Timer(1, lambda: fcntl.flock(held, fcntl.LOCK_UN)).start()
+    lock = utils.lock_file(fn, timeout=10)
+    assert lock is not None
+    utils.unlock_file(lock)
+    held.close()
+
+
 def make_layer(path):
     (path / 'conf').mkdir(parents=True)
     (path / 'conf' / 'layer.conf').write_text('')