dnf/0047-Add-clean-command-lock-demand.patch
Aleš Matěj 80607cb274 Add dedicated lock for clean command
Resolves: RHEL-128440
2026-07-14 14:20:31 +02:00

215 lines
8.2 KiB
Diff

From cdff23db5f4099e344a2fcac4bc230836a3949ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
Date: Wed, 8 Jul 2026 13:47:31 +0200
Subject: [PATCH 3/4] Add clean command lock demand
---
dnf/cli/demand.py | 14 ++++++
dnf/cli/main.py | 98 +++++++++++++++++++++++-----------------
dnf/lock.py | 5 ++
tests/cli/test_demand.py | 2 +
4 files changed, 77 insertions(+), 42 deletions(-)
diff --git a/dnf/cli/demand.py b/dnf/cli/demand.py
index ac19930b..2dc3a3d4 100644
--- a/dnf/cli/demand.py
+++ b/dnf/cli/demand.py
@@ -20,6 +20,15 @@
from __future__ import unicode_literals
+from enum import Enum
+
+
+class CleanCommandLock(Enum):
+ """Clean command lock modes. :api"""
+ NONE = 0
+ READ = 1
+ WRITE = 2
+
class _Default(object):
def __init__(self, default, choices=None):
@@ -70,3 +79,8 @@ class DemandSheet(object):
# repositories packages (e.g. versionlock).
# If it stays None, the demands.resolving is used as a fallback.
plugin_filtering_enabled = _BoolDefault(None)
+
+ # Determines if/how to lock the clean command lock.
+ # This lock is used to ensure clean command (which uses WRITE mode) running concurrently
+ # with another dnf process doesn't delete required data (packages).
+ clean_command_lock = _Default(CleanCommandLock.NONE, tuple(CleanCommandLock))
diff --git a/dnf/cli/main.py b/dnf/cli/main.py
index c63baaaa..2537a46d 100644
--- a/dnf/cli/main.py
+++ b/dnf/cli/main.py
@@ -24,6 +24,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.conf import Conf
from dnf.cli.cli import Cli
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import ucd
from dnf.cli.utils import show_lock_owner
@@ -33,6 +34,7 @@ import dnf.cli
import dnf.cli.cli
import dnf.cli.option_parser
import dnf.exceptions
+import dnf.lock
import dnf.i18n
import dnf.logging
import dnf.util
@@ -43,6 +45,7 @@ import logging
import os
import os.path
import sys
+from contextlib import nullcontext
logger = logging.getLogger("dnf")
@@ -118,52 +121,63 @@ def cli_run(cli, base):
else:
f.close()
- try:
- cli.run()
- except dnf.exceptions.LockError:
- raise
- except (IOError, OSError) as e:
- return ex_IOError(e)
+ lock = None
+ if cli.demands.clean_command_lock == CleanCommandLock.NONE:
+ lock = nullcontext()
+ elif cli.demands.clean_command_lock == CleanCommandLock.READ:
+ lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, True)
+ elif cli.demands.clean_command_lock == CleanCommandLock.WRITE:
+ lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, False)
+ else:
+ raise RuntimeError('Invalid demands.clean_command_lock: %s' % cli.demands.clean_command_lock)
- if cli.demands.resolving:
+ with lock:
try:
- ret = resolving(cli, base)
- except dnf.exceptions.DepsolveError as e:
- ex_Error(e)
- msg = ""
- if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True):
- msg += _("try to add '{}' to command line to replace conflicting "
- "packages").format("--allowerasing")
- if cli.base.conf.strict:
- if not msg:
- msg += _("try to add '{}' to skip uninstallable packages").format(
- "--skip-broken")
- else:
- msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken")
- if cli.base.conf.best:
- prio = cli.base.conf._get_priority("best")
- if prio <= dnf.conf.PRIO_MAINCONFIG:
+ cli.run()
+ except dnf.exceptions.LockError:
+ raise
+ except (IOError, OSError) as e:
+ return ex_IOError(e)
+
+ if cli.demands.resolving:
+ try:
+ ret = resolving(cli, base)
+ except dnf.exceptions.DepsolveError as e:
+ ex_Error(e)
+ msg = ""
+ if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True):
+ msg += _("try to add '{}' to command line to replace conflicting "
+ "packages").format("--allowerasing")
+ if cli.base.conf.strict:
if not msg:
- msg += _("try to add '{}' to use not only best candidate packages").format(
- "--nobest")
+ msg += _("try to add '{}' to skip uninstallable packages").format(
+ "--skip-broken")
else:
- msg += _(" or '{}' to use not only best candidate packages").format(
- "--nobest")
- if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types:
- if not msg:
- msg += _("try to add '{}' to load additional filelists metadata").format(
- "--setopt=optional_metadata_types=filelists")
- else:
- msg += _(" or '{}' to load additional filelists metadata").format(
- "--setopt=optional_metadata_types=filelists")
- if msg:
- logger.info("({})".format(msg))
- raise
- if ret:
- return ret
-
- cli.command.run_transaction()
- return cli.demands.success_exit_status
+ msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken")
+ if cli.base.conf.best:
+ prio = cli.base.conf._get_priority("best")
+ if prio <= dnf.conf.PRIO_MAINCONFIG:
+ if not msg:
+ msg += _("try to add '{}' to use not only best candidate packages").format(
+ "--nobest")
+ else:
+ msg += _(" or '{}' to use not only best candidate packages").format(
+ "--nobest")
+ if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types:
+ if not msg:
+ msg += _("try to add '{}' to load additional filelists metadata").format(
+ "--setopt=optional_metadata_types=filelists")
+ else:
+ msg += _(" or '{}' to load additional filelists metadata").format(
+ "--setopt=optional_metadata_types=filelists")
+ if msg:
+ logger.info("({})".format(msg))
+ raise
+ if ret:
+ return ret
+
+ cli.command.run_transaction()
+ return cli.demands.success_exit_status
def resolving(cli, base):
diff --git a/dnf/lock.py b/dnf/lock.py
index cb189c35..aae8a51b 100644
--- a/dnf/lock.py
+++ b/dnf/lock.py
@@ -63,6 +63,11 @@ def build_log_lock(logdir, exit_on_lock):
'log', not exit_on_lock)
+def build_clean_command_lock(cachedir, exit_on_lock, read):
+ return FileLock(os.path.join(_fit_lock_dir(cachedir), 'clean_command.lock'),
+ 'clean command', not exit_on_lock, read)
+
+
class ProcessLock(object):
def __init__(self, target, description, blocking=False):
self.blocking = blocking
diff --git a/tests/cli/test_demand.py b/tests/cli/test_demand.py
index fc9cfcec..7f99100a 100644
--- a/tests/cli/test_demand.py
+++ b/tests/cli/test_demand.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import dnf.cli.demand
+from dnf.cli.demand import CleanCommandLock
import tests.support
@@ -42,6 +43,7 @@ class DemandTest(tests.support.TestCase):
self.assertFalse(demands.sack_activation)
self.assertFalse(demands.root_user)
self.assertEqual(demands.success_exit_status, 0)
+ self.assertEqual(demands.clean_command_lock, CleanCommandLock.NONE)
def test_independence(self):
d1 = dnf.cli.demand.DemandSheet()
--
2.54.0