From c5e92b2d91a8809c1ca47823e9724b207af13d68 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Thu, 15 Oct 2026 20:55:58 +0200 Subject: [PATCH 1/4] pyocf: Fix parameter ranges The ranges are defined inclusively, so using regular range() causes off-by-one bug. Introduce range_inclusive() to express inclusive ranges explicitly and avoid this class of bugs. Signed-off-by: Robert Baldyga --- tests/functional/pyocf/types/cache.py | 30 +++++++++++++-------------- tests/functional/pyocf/utils.py | 4 ++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/functional/pyocf/types/cache.py b/tests/functional/pyocf/types/cache.py index 2f4edbbd..9ba55d36 100644 --- a/tests/functional/pyocf/types/cache.py +++ b/tests/functional/pyocf/types/cache.py @@ -35,7 +35,7 @@ OcfCompletion, SeqCutOffPolicy, ) -from ..utils import Size, struct_to_dict +from ..utils import Size, struct_to_dict, range_inclusive from .core import Core from .queue import Queue from .stats.cache import CacheInfo @@ -134,24 +134,24 @@ class CacheStandbyActivateConfig(Structure): class ConfValidValues: - promotion_nhit_insertion_threshold_range = range(2, 1000) - promotion_nhit_trigger_threshold_range = range(0, 100) + promotion_nhit_insertion_threshold_range = range_inclusive(2, 1000) + promotion_nhit_trigger_threshold_range = range_inclusive(0, 100) - cleaning_alru_wake_up_time_range = range(0, 3600) - cleaning_alru_staleness_time_range = range(1, 3600) - cleaning_alru_flush_max_buffers_range = range(1, 10000) - cleaning_alru_activity_threshold_range = range(0, 1000000) + cleaning_alru_wake_up_time_range = range_inclusive(0, 3600) + cleaning_alru_staleness_time_range = range_inclusive(1, 3600) + cleaning_alru_flush_max_buffers_range = range_inclusive(1, 10000) + cleaning_alru_activity_threshold_range = range_inclusive(0, 1000000) - cleaning_acp_wake_up_time_range = range(0, 10000) - cleaning_acp_flush_max_buffers_range = range(1, 10000) + cleaning_acp_wake_up_time_range = range_inclusive(0, 10000) + cleaning_acp_flush_max_buffers_range = range_inclusive(1, 10000) - seq_cutoff_threshold_rage = range(1, 4294841344) - seq_detect_promotion_count_range = range(1, 65535) + seq_cutoff_threshold_rage = range_inclusive(1, 4294841344) + seq_detect_promotion_count_range = range_inclusive(1, 65535) - ioclass_id_range = range(0, 32) - ioclass_priority_range = range(-1, 255) - ioclass_min_size_range = range(0, 100) - ioclass_max_size_range = range(0, 100) + ioclass_id_range = range_inclusive(0, 33) + ioclass_priority_range = range_inclusive(-1, 255) + ioclass_min_size_range = range_inclusive(0, 100) + ioclass_max_size_range = range_inclusive(0, 100) CACHE_MODE_NONE = -1 diff --git a/tests/functional/pyocf/utils.py b/tests/functional/pyocf/utils.py index 1799120f..727a1f11 100644 --- a/tests/functional/pyocf/utils.py +++ b/tests/functional/pyocf/utils.py @@ -55,6 +55,10 @@ def print_buffer( print_fcn("<'0x{:02X}' until end>".format(ignore)) +def range_inclusive(start, stop): + return range(start, stop + 1) + + class Size: _KiB = 1024 _MiB = _KiB * 1024 From b3a3fec2a1695ba95adf0c87453046e6f66f7e0f Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Thu, 15 Oct 2026 20:59:37 +0200 Subject: [PATCH 2/4] pyocf: Add tests for dirty_ratio_inertia/dirty_ratio_threshold Signed-off-by: Robert Baldyga --- tests/functional/pyocf/types/cache.py | 4 + tests/functional/tests/cleaning/test_alru.py | 89 +++++++++++++++++++ .../tests/security/test_management_fuzzy.py | 4 + 3 files changed, 97 insertions(+) diff --git a/tests/functional/pyocf/types/cache.py b/tests/functional/pyocf/types/cache.py index 9ba55d36..c6a541c7 100644 --- a/tests/functional/pyocf/types/cache.py +++ b/tests/functional/pyocf/types/cache.py @@ -141,6 +141,8 @@ class ConfValidValues: cleaning_alru_staleness_time_range = range_inclusive(1, 3600) cleaning_alru_flush_max_buffers_range = range_inclusive(1, 10000) cleaning_alru_activity_threshold_range = range_inclusive(0, 1000000) + cleaning_alru_dirty_ratio_threshold_range = range_inclusive(0, 100) + cleaning_alru_dirty_ratio_inertia_range = range_inclusive(0, 2**32 - 1) cleaning_acp_wake_up_time_range = range_inclusive(0, 10000) cleaning_acp_flush_max_buffers_range = range_inclusive(1, 10000) @@ -225,6 +227,8 @@ class AlruParams(IntEnum): STALE_BUFFER_TIME = 1 FLUSH_MAX_BUFFERS = 2 ACTIVITY_THRESHOLD = 3 + DIRTY_RATIO_THRESHOLD = 4 + DIRTY_RATIO_INERTIA = 5 def __str__(self): return self.name diff --git a/tests/functional/tests/cleaning/test_alru.py b/tests/functional/tests/cleaning/test_alru.py index 5bad93ba..cb2c6b85 100644 --- a/tests/functional/tests/cleaning/test_alru.py +++ b/tests/functional/tests/cleaning/test_alru.py @@ -39,6 +39,10 @@ def cleaner_core_wr(stats): return stats["block"]["cleaner_core_wr"]["value"] +def cache_lines(stats): + return int(stats["conf"]["size"]) + + # -- cache setup helpers -- def setup_cache(pyocf_ctx, cls=CacheLineSize.DEFAULT): @@ -257,3 +261,88 @@ def test_alru_activity_threshold(pyocf_ctx, manual_cleaner, activity_threshold): manual_cleaner.run_until_idle(cache) assert dirty_blocks(cache.get_stats()) == 0 + + +def setup_dirty_ratio(cache, threshold, inertia, flush_max): + """Configure ALRU so that only the dirty ratio can trigger cleaning: + data never gets stale and I/O activity is always recent, as the test + does not advance time.""" + cache.set_cleaning_policy_param( + CleaningPolicy.ALRU, AlruParams.WAKE_UP_TIME, 1 + ) + cache.set_cleaning_policy_param( + CleaningPolicy.ALRU, AlruParams.STALE_BUFFER_TIME, 3600 + ) + cache.set_cleaning_policy_param( + CleaningPolicy.ALRU, AlruParams.ACTIVITY_THRESHOLD, 1000000 + ) + cache.set_cleaning_policy_param( + CleaningPolicy.ALRU, AlruParams.FLUSH_MAX_BUFFERS, flush_max + ) + cache.set_cleaning_policy_param( + CleaningPolicy.ALRU, AlruParams.DIRTY_RATIO_THRESHOLD, threshold + ) + cache.set_cleaning_policy_param( + CleaningPolicy.ALRU, AlruParams.DIRTY_RATIO_INERTIA, inertia + ) + cache.set_cleaning_policy(CleaningPolicy.ALRU) + + +@pytest.mark.parametrize("threshold", [10, 30, 50]) +def test_alru_dirty_ratio_threshold(pyocf_ctx, manual_cleaner, threshold): + """Verify cleaning is triggered by exceeding the dirty ratio threshold + regardless of staleness and I/O activity, and that it stops as soon as + the dirty ratio drops below the threshold.""" + cache, core, vol, queue = setup_cache(pyocf_ctx) + + flush_max = 100 + setup_dirty_ratio(cache, threshold, 0, flush_max) + + # Default cache line size is 4 KiB, so 4 KiB blocks == cache lines + threshold_cl = cache_lines(cache.get_stats()) // 100 * threshold + + # Stay below the threshold - nothing should be cleaned + below = threshold_cl - flush_max + fill_dirty(vol, queue, Size(0), STATS_BLOCK_SIZE * below) + + manual_cleaner.run(cache) + assert dirty_blocks(cache.get_stats()) == below, \ + "No data should be flushed below dirty ratio threshold" + + # Exceed the threshold + above = threshold_cl + 5 * flush_max + fill_dirty(vol, queue, STATS_BLOCK_SIZE * below, + STATS_BLOCK_SIZE * (above - below)) + assert dirty_blocks(cache.get_stats()) == above + + manual_cleaner.run_until_idle(cache) + + dirty = dirty_blocks(cache.get_stats()) + assert threshold_cl - flush_max <= dirty < threshold_cl, \ + f"Cleaning should stop right below threshold {threshold_cl}, " \ + f"dirty: {dirty}" + + +@pytest.mark.parametrize("inertia", [Size.from_MiB(1), Size.from_MiB(4)]) +def test_alru_dirty_ratio_inertia(pyocf_ctx, manual_cleaner, inertia): + """Verify cleaning triggered by dirty ratio continues until the dirty + ratio drops below (threshold - inertia).""" + cache, core, vol, queue = setup_cache(pyocf_ctx) + + flush_max = 100 + threshold = 30 + setup_dirty_ratio(cache, threshold, int(inertia), flush_max) + + threshold_cl = cache_lines(cache.get_stats()) // 100 * threshold + inertia_cl = inertia // STATS_BLOCK_SIZE + stop_cl = threshold_cl - inertia_cl + + above = threshold_cl + flush_max + fill_dirty(vol, queue, Size(0), STATS_BLOCK_SIZE * above) + + manual_cleaner.run_until_idle(cache) + + dirty = dirty_blocks(cache.get_stats()) + assert stop_cl - flush_max <= dirty < stop_cl, \ + f"Cleaning should stop right below {stop_cl} " \ + f"(threshold {threshold_cl} - inertia {inertia_cl}), dirty: {dirty}" diff --git a/tests/functional/tests/security/test_management_fuzzy.py b/tests/functional/tests/security/test_management_fuzzy.py index f3310562..2d08673e 100644 --- a/tests/functional/tests/security/test_management_fuzzy.py +++ b/tests/functional/tests/security/test_management_fuzzy.py @@ -334,6 +334,10 @@ def get_alru_param_valid_rage(param_id): return ConfValidValues.cleaning_alru_flush_max_buffers_range elif param_id == AlruParams.ACTIVITY_THRESHOLD: return ConfValidValues.cleaning_alru_activity_threshold_range + elif param_id == AlruParams.DIRTY_RATIO_THRESHOLD: + return ConfValidValues.cleaning_alru_dirty_ratio_threshold_range + elif param_id == AlruParams.DIRTY_RATIO_INERTIA: + return ConfValidValues.cleaning_alru_dirty_ratio_inertia_range @pytest.mark.parametrize("cm", CacheMode) From 23e45918030d7eab696c34f71a4cb4ce465f78b3 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Thu, 15 Oct 2026 21:00:36 +0200 Subject: [PATCH 3/4] cache: Setup policies on cache init This fixes standby, which did not call setup for the policies at all, and detach/attach path, which did not preserve policies parameters. It also simplifies setup handling, as now all the policies are set to default on cache creation, so on attach/standby_attach everything is already initialized, and on the load path, the params are just overwritten with metadata from the disk. Signed-off-by: Robert Baldyga --- src/mngt/ocf_mngt_cache.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/mngt/ocf_mngt_cache.c b/src/mngt/ocf_mngt_cache.c index 95585c2c..8a9b59ee 100644 --- a/src/mngt/ocf_mngt_cache.c +++ b/src/mngt/ocf_mngt_cache.c @@ -238,7 +238,6 @@ static void _init_parts_attached(ocf_pipeline_t pipeline, void *priv, static ocf_error_t __init_cleaning_policy(ocf_cache_t cache) { int result; - int i; OCF_ASSERT_PLUGGED(cache); @@ -247,9 +246,6 @@ static ocf_error_t __init_cleaning_policy(ocf_cache_t cache) if (result) return result; - for (i = 0; i < ocf_cleaning_max; i++) - ocf_cleaning_setup(cache, i); - result = ocf_cleaning_initialize(cache, cache->cleaner.policy); if (result) env_refcnt_deinit(&cache->cleaner.refcnt); @@ -263,6 +259,14 @@ static void __deinit_cleaning_policy(ocf_cache_t cache) env_refcnt_deinit(&cache->cleaner.refcnt); } +static void __setup_cleaning_policy(ocf_cache_t cache) +{ + int i; + + for (i = 0; i < ocf_cleaning_max; i++) + ocf_cleaning_setup(cache, i); +} + static void __setup_promotion_policy(ocf_cache_t cache) { int i; @@ -1512,8 +1516,6 @@ static void _ocf_mngt_attach_init_services(ocf_pipeline_t pipeline, ocf_cache_t cache = context->cache; ocf_error_t result; - __setup_promotion_policy(cache); - if (context->metadata.cleaner_disabled) __set_cleaning_policy(cache, ocf_cleaning_nop); @@ -1615,6 +1617,8 @@ static void _ocf_mngt_cache_init(ocf_cache_t cache, cache->conf_meta->promotion_policy_type = params->metadata.promotion_policy; cache->conf_meta->prefetch_mask = OCF_PF_MASK_DEFAULT; ocf_prefetch_setup(cache); + __setup_cleaning_policy(cache); + __setup_promotion_policy(cache); __set_cleaning_policy(cache, ocf_cleaning_default); /* Init Partitions */ From 5c1021f9d120994d34c6c95e12ac66f546e081f5 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Thu, 15 Oct 2026 21:02:16 +0200 Subject: [PATCH 4/4] pyocf: Add test for param preservation over detach/attach Check if cleaning/promotion/cleaning policy params are preserved after cache is detached and attached. Signed-off-by: Robert Baldyga --- tests/functional/pyocf/types/cache.py | 22 +++++++ .../tests/management/test_attach_cache.py | 59 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/tests/functional/pyocf/types/cache.py b/tests/functional/pyocf/types/cache.py index c6a541c7..0517a36a 100644 --- a/tests/functional/pyocf/types/cache.py +++ b/tests/functional/pyocf/types/cache.py @@ -429,6 +429,21 @@ def set_cleaning_policy_param(self, cleaning_policy: CleaningPolicy, param_id, p if status: raise OcfError("Error setting cleaning policy param", status) + def get_cleaning_policy_param(self, cleaning_policy: CleaningPolicy, param_id): + self.read_lock() + + param_value = c_uint32() + + status = self.owner.lib.ocf_mngt_cache_cleaning_get_param( + self.cache_handle, cleaning_policy, param_id, byref(param_value) + ) + + self.read_unlock() + if status: + raise OcfError("Error getting cleaning policy param", status) + + return param_value.value + def set_promotion_policy(self, promotion_policy: PromotionPolicy): self.write_lock() @@ -1286,6 +1301,13 @@ def get_by_name(cache_name, owner=None): c_uint32, ] lib.ocf_mngt_cache_cleaning_set_param.restype = c_int +lib.ocf_mngt_cache_cleaning_get_param.argtypes = [ + c_void_p, + c_uint32, + c_uint32, + POINTER(c_uint32), +] +lib.ocf_mngt_cache_cleaning_get_param.restype = c_int lib.ocf_mngt_cache_prefetch_set_policy.argtypes = [c_void_p, c_uint8] lib.ocf_mngt_cache_prefetch_set_policy.restype = c_int lib.ocf_mngt_cache_prefetch_get_policy.argtypes = [c_void_p, POINTER(c_uint8)] diff --git a/tests/functional/tests/management/test_attach_cache.py b/tests/functional/tests/management/test_attach_cache.py index 2264777e..46f0d7ec 100644 --- a/tests/functional/tests/management/test_attach_cache.py +++ b/tests/functional/tests/management/test_attach_cache.py @@ -1,6 +1,7 @@ # # Copyright(c) 2019-2022 Intel Corporation # Copyright(c) 2024-2025 Huawei Technologies +# Copyright(c) 2026 Unvertical # SPDX-License-Identifier: BSD-3-Clause # @@ -12,6 +13,12 @@ Cache, CacheMode, CleaningPolicy, + AlruParams, + AcpParams, + PromotionPolicy, + NhitParams, + Prefetcher, + ReadaheadParams, ) from pyocf.types.core import Core from pyocf.types.data import Data @@ -128,6 +135,58 @@ def test_detach_cache_zero_superblock(pyocf_ctx): cache.stop() +def test_reattach_preserves_policy_params(pyocf_ctx): + """Check if cleaning, promotion and prefetch policy parameters set before + detach are preserved after the cache device is attached again. + """ + cache_device = RamVolume(Size.from_MiB(50)) + cache = Cache.start_on_device(cache_device) + + # Non-default values of all the parameters + alru_params = { + AlruParams.WAKE_UP_TIME: 30, + AlruParams.STALE_BUFFER_TIME: 60, + AlruParams.FLUSH_MAX_BUFFERS: 50, + AlruParams.ACTIVITY_THRESHOLD: 5000, + AlruParams.DIRTY_RATIO_THRESHOLD: 50, + AlruParams.DIRTY_RATIO_INERTIA: 1024, + } + acp_params = { + AcpParams.WAKE_UP_TIME: 100, + AcpParams.FLUSH_MAX_BUFFERS: 64, + } + nhit_params = { + NhitParams.INSERTION_THRESHOLD: 5, + NhitParams.TRIGGER_THRESHOLD: 50, + } + readahead_params = { + ReadaheadParams.THRESHOLD: 128 * 1024, + } + + for param_id, value in alru_params.items(): + cache.set_cleaning_policy_param(CleaningPolicy.ALRU, param_id, value) + for param_id, value in acp_params.items(): + cache.set_cleaning_policy_param(CleaningPolicy.ACP, param_id, value) + for param_id, value in nhit_params.items(): + cache.set_promotion_policy_param(PromotionPolicy.NHIT, param_id, value) + for param_id, value in readahead_params.items(): + cache.set_prefetch_param(Prefetcher.READAHEAD, param_id, value) + + cache.detach_device() + cache.attach_device(cache_device, force=False) + + for param_id, value in alru_params.items(): + assert cache.get_cleaning_policy_param(CleaningPolicy.ALRU, param_id) == value + for param_id, value in acp_params.items(): + assert cache.get_cleaning_policy_param(CleaningPolicy.ACP, param_id) == value + for param_id, value in nhit_params.items(): + assert cache.get_promotion_policy_param(PromotionPolicy.NHIT, param_id).value == value + for param_id, value in readahead_params.items(): + assert cache.get_prefetch_param(Prefetcher.READAHEAD, param_id) == value + + cache.stop() + + @pytest.mark.parametrize("cls", CacheLineSize) @pytest.mark.parametrize("mode", [CacheMode.WB, CacheMode.WT, CacheMode.WO]) @pytest.mark.parametrize("new_cache_size", [80, 120])