From 32308070e3f7ba179362833afb37aef835250a93 Mon Sep 17 00:00:00 2001 From: Michal Mielewczyk Date: Thu, 16 Oct 2025 14:44:08 +0200 Subject: [PATCH 1/4] test api: cast ALRU inertia to int in cache api The casadm api expects that the ALRU params as ints, so the conversion from Size to int must happen in the higher level api Signed-off-by: Michal Mielewczyk --- test/functional/api/cas/cache.py | 8 ++++++-- test/functional/api/cas/casadm.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/test/functional/api/cas/cache.py b/test/functional/api/cas/cache.py index e12b77177..a348bada9 100644 --- a/test/functional/api/cas/cache.py +++ b/test/functional/api/cas/cache.py @@ -30,7 +30,7 @@ from connection.utils.output import Output from storage_devices.device import Device from test_tools.os_tools import sync -from type_def.size import Size +from type_def.size import Size, Unit class Cache: @@ -210,7 +210,11 @@ def set_params_alru(self, alru_params: FlushParametersAlru) -> Output: else None ), (alru_params.dirty_ratio_threshold if alru_params.dirty_ratio_threshold else None), - (alru_params.dirty_ratio_inertia if alru_params.dirty_ratio_inertia else None), + ( + int(alru_params.dirty_ratio_inertia.get_value(Unit.MebiByte)) + if alru_params.dirty_ratio_inertia + else None + ), ) def set_promotion_policy(self, policy: PromotionPolicy) -> Output: diff --git a/test/functional/api/cas/casadm.py b/test/functional/api/cas/casadm.py index 9894bbc12..02b14edee 100644 --- a/test/functional/api/cas/casadm.py +++ b/test/functional/api/cas/casadm.py @@ -187,7 +187,7 @@ def set_param_cleaning_alru( flush_max_buffers: int | None = None, activity_threshold: int | None = None, dirty_ratio_threshold: int | None = None, - dirty_ratio_inertia: Size | None = None, + dirty_ratio_inertia: int | None = None, shortcut: bool = False, ) -> Output: _wake_up = str(wake_up) if wake_up is not None else None @@ -195,7 +195,7 @@ def set_param_cleaning_alru( _flush_max_buffers = str(flush_max_buffers) if flush_max_buffers is not None else None _activity_threshold = str(activity_threshold) if activity_threshold is not None else None _dirty_ratio_threshold = str(dirty_ratio_threshold) if dirty_ratio_threshold is not None else None - _dirty_ratio_inertia = str(dirty_ratio_inertia.get_value(Unit.MebiByte)) if dirty_ratio_inertia is not None else None + _dirty_ratio_inertia = str(dirty_ratio_inertia) if dirty_ratio_inertia is not None else None output = TestRun.executor.run( set_param_cleaning_alru_cmd( cache_id=str(cache_id), From e6aa6db025393ff0aea3a9d1e5520f539733febb Mon Sep 17 00:00:00 2001 From: Michal Mielewczyk Date: Thu, 16 Oct 2025 10:31:51 +0200 Subject: [PATCH 2/4] test alru: test description fixed by black Signed-off-by: Michal Mielewczyk --- .../tests/lazy_writes/cleaning_policy/test_alru.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py b/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py index 9f659dd04..3b37c9c84 100644 --- a/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py +++ b/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py @@ -25,13 +25,13 @@ @pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) def test_alru_no_idle(): """ - title: Test ALRU with activity threshold set to 0 - description: | - Verify that ALRU is able to perform cleaning if cache is under constant load and - activity threshold is set to 0. Constant load is performed by using fio instance running - in background. - pass_criteria: - - Dirty cache lines are cleaned successfully. + title: Test ALRU with activity threshold set to 0 + description: | + Verify that ALRU is able to perform cleaning if cache is under constant load and + activity threshold is set to 0. Constant load is performed by using fio instance running + in background. + pass_criteria: + - Dirty cache lines are cleaned successfully. """ with TestRun.step("Prepare configuration"): From 39c66ba5228b41064307e920a63a0eb67e828e4b Mon Sep 17 00:00:00 2001 From: Michal Mielewczyk Date: Wed, 15 Oct 2025 21:21:04 +0200 Subject: [PATCH 3/4] tests: ALRU: don't clean if dirty < threshold Co-authored-by: aider (gpt-5) Signed-off-by: Michal Mielewczyk --- .../lazy_writes/cleaning_policy/test_alru.py | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py b/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py index 3b37c9c84..4f530b08f 100644 --- a/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py +++ b/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py @@ -1,6 +1,6 @@ # # Copyright(c) 2020-2021 Intel Corporation -# Copyright(c) 2024 Huawei Technologies Co., Ltd. +# Copyright(c) 2024-2025 Huawei Technologies Co., Ltd. # SPDX-License-Identifier: BSD-3-Clause # @@ -10,7 +10,13 @@ from datetime import timedelta from api.cas import casadm -from api.cas.cache_config import CacheMode, CleaningPolicy, FlushParametersAlru, SeqCutOffPolicy +from api.cas.cache_config import ( + CacheMode, + CleaningPolicy, + FlushParametersAlru, + SeqCutOffPolicy, + CacheLineSize, +) from core.test_run import TestRun from storage_devices.disk import DiskType, DiskTypeSet, DiskTypeLowerThan from test_tools.fio.fio import Fio @@ -118,3 +124,89 @@ def prepare(): ) return cache, core + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +@pytest.mark.parametrize("inertia", [Size.zero(), Size(1500, Unit.MiB)]) +@pytest.mark.parametrizex( + "cache_line_size", + [CacheLineSize.LINE_4KiB, CacheLineSize.LINE_64KiB], +) +def test_alru_dirty_ratio_inertia_no_cleaning_if_dirty_below_threshold( + cache_line_size: CacheLineSize, inertia: Unit +): + """ + title: Test ALRU dirty ratio inertia — no cleaning below threshold + description: | + Verify that ALRU cleaning is not triggered when the number of dirty cache lines is lower than + (threshold - intertia) + pass_criteria: + - The cleaning is not triggered when dirty data doesn't exceed the specified threshold. + """ + with TestRun.step("Prepare disks for cache and core"): + cache_dev = TestRun.disks["cache"] + core_dev = TestRun.disks["core"] + + cache_dev.create_partitions([Size(3, Unit.GiB)]) + core_dev.create_partitions([Size(10, Unit.GiB)]) + + with TestRun.step("Disable udev"): + Udev.disable() + + with TestRun.step("Start cache and add core"): + cache = casadm.start_cache( + cache_dev.partitions[0], + cache_line_size=cache_line_size, + force=True, + cache_mode=CacheMode.WB, + ) + core = cache.add_core(core_dev.partitions[0]) + + with TestRun.step("Set ALRU and disable sequential cutoff"): + cache.set_seq_cutoff_policy(SeqCutOffPolicy.never) + cache.set_cleaning_policy(CleaningPolicy.alru) + + with TestRun.step(f"Set alru params"): + cache.set_params_alru( + FlushParametersAlru( + staleness_time=Time(seconds=3600), + wake_up_time=Time(seconds=1), + activity_threshold=Time(milliseconds=1000), + dirty_ratio_threshold=90, + dirty_ratio_inertia=inertia, + ) + ) + + with TestRun.step("Run write workload to reach ~2GiB (≈66% of 3GiB) dirty data"): + fio = ( + Fio() + .create_command() + .io_engine(IoEngine.libaio) + .size(cache.size * 0.66) + .block_size(Size(4, Unit.KiB)) + .target(core) + .direct() + .read_write(ReadWrite.randwrite) + ) + fio.run() + + with TestRun.step("Capture baseline dirty usage after I/O settles"): + time.sleep(2) + dirty_before_pct = cache.get_statistics(percentage_val=True).usage_stats.dirty + dirty_before = cache.get_statistics().usage_stats.dirty + if dirty_before_pct <= 60: + TestRun.fail( + f"Exception: Precondition not met: dirty cache lines must exceed 60% after " + f"I/O settles (dirty={dirty_before}, dirty%={dirty_before_pct}%). Aborting test." + ) + + with TestRun.step("Idle and verify dirty cache lines do not change and remain below threshold"): + time.sleep(30) + dirty_after = cache.get_statistics().usage_stats.dirty + + if dirty_before > dirty_after: + TestRun.fail( + f"No flushing shall occur when dirty < threshold (dirty before={dirty_before}, " + f"dirty after={dirty_after}, threshold={dirty_ratio_threshold}%)" + ) From 8e374bbda6a6ea1ba019f57465c6c68efd213251 Mon Sep 17 00:00:00 2001 From: Michal Mielewczyk Date: Thu, 16 Oct 2025 11:42:34 +0200 Subject: [PATCH 4/4] test: ALRU: clean below (threshold - inertia) Co-authored-by: aider (gpt-5) Signed-off-by: Michal Mielewczyk --- .../lazy_writes/cleaning_policy/test_alru.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py b/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py index 4f530b08f..5b086e9d0 100644 --- a/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py +++ b/test/functional/tests/lazy_writes/cleaning_policy/test_alru.py @@ -210,3 +210,118 @@ def test_alru_dirty_ratio_inertia_no_cleaning_if_dirty_below_threshold( f"No flushing shall occur when dirty < threshold (dirty before={dirty_before}, " f"dirty after={dirty_after}, threshold={dirty_ratio_threshold}%)" ) + + +@pytest.mark.require_disk("cache", DiskTypeSet([DiskType.optane, DiskType.nand])) +@pytest.mark.require_disk("core", DiskTypeLowerThan("cache")) +@pytest.mark.parametrize("inertia", [Size.zero(), Size(600, Unit.MiB)]) +def test_alru_dirty_threshold_cleans_until_threshold_minus_inertia(inertia: Size): + """ + title: Test ALRU threshold cleaning – start > 50%, stop at (threshold - inertia) + description: | + Verify that with ALRU cleaning configured with 50% dirty threshold and + specified inertia in MiB, cleaning starts when dirty exceeds the threshold + and stops after reaching (threshold - inertia). + pass_criteria: + - Cleaning starts once dirty exceeds 50%. + - Dirty decreases over time after stopping I/O. + - Cleaning stops at or below (threshold - inertia). + - If dirty does not decrease for 20 consecutive checks, fail the test. + """ + + target_dirty_ratio_threshold = 50 + # 3 GiB cache -> 50% = 1536 MiB; inertia is in MiB + threshold_size = Size(1536, Unit.MiB) + target_cleaning_threshold = threshold_size - inertia + + with TestRun.step("Prepare 3GiB cache and 10GiB core, disable udev, start cache WB, add core"): + cache_dev = TestRun.disks["cache"] + core_dev = TestRun.disks["core"] + + cache_dev.create_partitions([Size(3, Unit.GiB)]) + core_dev.create_partitions([Size(10, Unit.GiB)]) + + with TestRun.step("Disable udev"): + Udev.disable() + + with TestRun.step("Start cache and add core"): + cache = casadm.start_cache(cache_dev.partitions[0], force=True, cache_mode=CacheMode.WB) + core = cache.add_core(core_dev.partitions[0]) + + with TestRun.step("Set ALRU and disable sequential cutoff"): + cache.set_seq_cutoff_policy(SeqCutOffPolicy.never) + cache.set_cleaning_policy(CleaningPolicy.alru) + + with TestRun.step( + f"Configure ALRU: dirty threshold=50%, inertia={inertia}, " + f"default wakeup, staleness_time=3600s" + ): + cache.set_params_alru( + FlushParametersAlru( + staleness_time=Time(seconds=3600), + dirty_ratio_threshold=target_dirty_ratio_threshold, + dirty_ratio_inertia=inertia, + ) + ) + + with TestRun.step("Start background fio (4 GiB randwrite)"): + fio = ( + Fio() + .create_command() + .io_engine(IoEngine.libaio) + .size(Size(4, Unit.GiB)) + .block_size(Size(4, Unit.KiB)) + .target(core) + .direct() + .read_write(ReadWrite.randwrite) + ) + fio.run_in_background() + + with TestRun.step("Wait until cache dirty >= 90%, then stop workload"): + start_ts = time.time() + while True: + time.sleep(2) + dirty_pct = cache.get_statistics(percentage_val=True).usage_stats.dirty + if dirty_pct >= 90: + break + if time.time() - start_ts > 5 * 60: + TestRun.fail( + f"Exception: Cache dirty level did not reach 90% within 5 minutes " + f"(current dirty={dirty_pct}%). Aborting test." + ) + + with TestRun.step("Stop the I/O"): + kill_all_io() + + with TestRun.step( + "Observe cleaning: ensure monotonic decrease and stop at (threshold - inertia)" + ): + # Initial dirty after IO stop + last_dirty = cache.get_statistics().usage_stats.dirty + + not_decreasing = 0 + while last_dirty > target_cleaning_threshold: + time.sleep(5) + dirty_now = cache.get_statistics().usage_stats.dirty + + if dirty_now < last_dirty: + not_decreasing = 0 + else: + not_decreasing += 1 + if not_decreasing >= 20: + TestRun.fail( + f"Exception: Dirty amount not decreasing for 20 consecutive checks " + f"(stuck at {dirty_now}, target stop {target_cleaning_threshold})." + ) + + last_dirty = dirty_now + + with TestRun.step( + "Wait 30 seconds and confirm that the cleaning stopped after reaching the target threshold" + ): + time.sleep(30) + if last_dirty > target_cleaning_threshold: + TestRun.fail( + f"Cleaning did not stop at expected level " + f"(dirty={last_dirty}, expected <= {target_cleaning_threshold})." + )