7866e2c1f5
CI / push-validation (pull_request) Successful in 27s
CI / lint (pull_request) Successful in 42s
CI / quality (pull_request) Successful in 53s
CI / build (pull_request) Successful in 1m5s
CI / helm (pull_request) Successful in 1m17s
CI / typecheck (pull_request) Successful in 1m24s
CI / security (pull_request) Successful in 1m25s
CI / unit_tests (pull_request) Successful in 5m49s
CI / docker (pull_request) Successful in 1m36s
CI / integration_tests (pull_request) Successful in 10m8s
CI / coverage (pull_request) Successful in 12m59s
CI / status-check (pull_request) Successful in 4s
The new step `the hot tier size_bytes should be {n:d}` in
features/steps/acms_hot_storage_tier_steps.py shared the matched
pattern of the existing
`the hot tier size_bytes should be {expected:d}` step in
features/steps/acms_context_analysis_engine_steps.py — Behave's
step registry strips parameter names when computing the pattern,
so both compile to the same regex. Every scenario hitting the
step raised AmbiguousStep at run-time, which Behave reports as
"errored" (not "failed"); that produced the 6 errored scenarios
on `features/acms_hot_storage_tier.feature` (lines 9, 96, 101,
149, 202, 210) seen in CI unit_tests.
Rename the new step and its `at most` companion to
`the hot storage tier size_bytes should be ...` (mirroring the
HotStorageTier class name) so the patterns no longer collide
with the analysis-engine TierDistribution step. Update the 8
feature-file references in `acms_hot_storage_tier.feature` to
match. The other-metric steps (entry_count, hit_count, miss_count,
max_entries, max_bytes) keep their `the hot tier` prefix because
they have no analogous collision — the analysis-engine file uses
`count` (not `entry_count`), so they are already unambiguous.
ISSUES CLOSED: #9972
243 lines
10 KiB
Gherkin
243 lines
10 KiB
Gherkin
Feature: ACMS Hot Storage Tier (in-memory LRU cache)
|
|
As an ACMS developer
|
|
I want a hot storage tier backed by an in-memory LRU cache
|
|
So that recently and frequently accessed context entries are served
|
|
with minimal latency while capacity limits are enforced automatically
|
|
|
|
# ---- Construction ----
|
|
|
|
Scenario: Create HotStorageTier with no limits
|
|
Given a HotStorageTier with no capacity limits
|
|
Then the hot tier entry_count should be 0
|
|
And the hot storage tier size_bytes should be 0
|
|
And the hot tier hit_count should be 0
|
|
And the hot tier miss_count should be 0
|
|
|
|
Scenario: Create HotStorageTier with max_entries limit
|
|
Given a HotStorageTier with max_entries 10
|
|
Then the hot tier max_entries should be 10
|
|
And the hot tier max_bytes should be None
|
|
|
|
Scenario: Create HotStorageTier with max_bytes limit
|
|
Given a HotStorageTier with max_bytes 1024
|
|
Then the hot tier max_bytes should be 1024
|
|
And the hot tier max_entries should be None
|
|
|
|
Scenario: Create HotStorageTier with both limits
|
|
Given a HotStorageTier with max_entries 5 and max_bytes 512
|
|
Then the hot tier max_entries should be 5
|
|
And the hot tier max_bytes should be 512
|
|
|
|
Scenario: HotStorageTier rejects max_entries of zero
|
|
Then creating a HotStorageTier with max_entries 0 should raise ValueError
|
|
|
|
Scenario: HotStorageTier rejects negative max_entries
|
|
Then creating a HotStorageTier with max_entries -1 should raise ValueError
|
|
|
|
Scenario: HotStorageTier rejects max_bytes of zero
|
|
Then creating a HotStorageTier with max_bytes 0 should raise ValueError
|
|
|
|
Scenario: HotStorageTier rejects negative max_bytes
|
|
Then creating a HotStorageTier with max_bytes -1 should raise ValueError
|
|
|
|
# ---- Basic put/get ----
|
|
|
|
Scenario: Put and get a single entry
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "key1" with content "hello world"
|
|
Then getting "key1" from the hot tier should return "hello world"
|
|
|
|
Scenario: Get returns None for missing entry
|
|
Given a HotStorageTier with no capacity limits
|
|
Then getting "missing" from the hot tier should return None
|
|
|
|
Scenario: Put updates existing entry
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "key1" with content "original"
|
|
And I put entry "key1" with content "updated"
|
|
Then getting "key1" from the hot tier should return "updated"
|
|
And the hot tier entry_count should be 1
|
|
|
|
Scenario: Put rejects empty entry_id
|
|
Given a HotStorageTier with no capacity limits
|
|
Then putting an entry with empty id should raise ValueError
|
|
|
|
# ---- Metrics ----
|
|
|
|
Scenario: Hit count increments on successful get
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "k1" with content "data"
|
|
And I get "k1" from the hot tier
|
|
Then the hot tier hit_count should be 1
|
|
And the hot tier miss_count should be 0
|
|
|
|
Scenario: Miss count increments on failed get
|
|
Given a HotStorageTier with no capacity limits
|
|
When I get "nonexistent" from the hot tier
|
|
Then the hot tier hit_count should be 0
|
|
And the hot tier miss_count should be 1
|
|
|
|
Scenario: Hit and miss counts accumulate independently
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "k1" with content "data"
|
|
And I get "k1" from the hot tier
|
|
And I get "k1" from the hot tier
|
|
And I get "missing" from the hot tier
|
|
Then the hot tier hit_count should be 2
|
|
And the hot tier miss_count should be 1
|
|
|
|
Scenario: entry_count reflects current cache size
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "a" with content "aaa"
|
|
And I put entry "b" with content "bbb"
|
|
And I put entry "c" with content "ccc"
|
|
Then the hot tier entry_count should be 3
|
|
|
|
Scenario: size_bytes reflects UTF-8 encoded content size
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "k1" with content "hello"
|
|
Then the hot storage tier size_bytes should be 5
|
|
|
|
Scenario: size_bytes updates when entry is replaced
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "k1" with content "hi"
|
|
And I put entry "k1" with content "hello world"
|
|
Then the hot storage tier size_bytes should be 11
|
|
|
|
# ---- LRU eviction: max_entries ----
|
|
|
|
Scenario: LRU eviction triggers when max_entries is exceeded
|
|
Given a HotStorageTier with max_entries 3
|
|
When I put entry "e1" with content "first"
|
|
And I put entry "e2" with content "second"
|
|
And I put entry "e3" with content "third"
|
|
And I put entry "e4" with content "fourth"
|
|
Then the hot tier entry_count should be 3
|
|
And getting "e1" from the hot tier should return None
|
|
And getting "e4" from the hot tier should return "fourth"
|
|
|
|
Scenario: LRU order is updated on get
|
|
Given a HotStorageTier with max_entries 2
|
|
When I put entry "a" with content "aaa"
|
|
And I put entry "b" with content "bbb"
|
|
And I get "a" from the hot tier
|
|
And I put entry "c" with content "ccc"
|
|
Then getting "a" from the hot tier should return "aaa"
|
|
And getting "b" from the hot tier should return None
|
|
And getting "c" from the hot tier should return "ccc"
|
|
|
|
Scenario: LRU order is updated on put (update existing)
|
|
Given a HotStorageTier with max_entries 2
|
|
When I put entry "a" with content "aaa"
|
|
And I put entry "b" with content "bbb"
|
|
And I put entry "a" with content "aaa-updated"
|
|
And I put entry "c" with content "ccc"
|
|
Then getting "a" from the hot tier should return "aaa-updated"
|
|
And getting "b" from the hot tier should return None
|
|
And getting "c" from the hot tier should return "ccc"
|
|
|
|
# ---- LRU eviction: max_bytes ----
|
|
|
|
Scenario: LRU eviction triggers when max_bytes is exceeded
|
|
Given a HotStorageTier with max_bytes 10
|
|
When I put entry "k1" with content "12345"
|
|
And I put entry "k2" with content "67890"
|
|
And I put entry "k3" with content "abcde"
|
|
Then the hot storage tier size_bytes should be at most 10
|
|
And getting "k1" from the hot tier should return None
|
|
|
|
Scenario: Single entry larger than max_bytes is evicted immediately
|
|
Given a HotStorageTier with max_bytes 3
|
|
When I put entry "big" with content "hello"
|
|
Then the hot tier entry_count should be 0
|
|
And the hot storage tier size_bytes should be 0
|
|
|
|
# ---- Eviction callback (warm-tier demotion) ----
|
|
|
|
Scenario: on_evict callback is called with evicted entry id
|
|
Given a HotStorageTier with max_entries 2 and an eviction recorder
|
|
When I put entry "x1" with content "content1"
|
|
And I put entry "x2" with content "content2"
|
|
And I put entry "x3" with content "content3"
|
|
Then the eviction recorder should have recorded entry id "x1"
|
|
|
|
Scenario: on_evict callback receives correct content for evicted entry
|
|
Given a HotStorageTier with max_entries 1 and an eviction recorder
|
|
When I put entry "first" with content "first-content"
|
|
And I put entry "second" with content "second-content"
|
|
Then the eviction recorder should have recorded entry "first" with content "first-content"
|
|
|
|
Scenario: on_evict callback error does not interrupt cache operation
|
|
Given a HotStorageTier with max_entries 1 and a failing eviction callback
|
|
When I put entry "a" with content "aaa"
|
|
And I put entry "b" with content "bbb"
|
|
Then the hot tier entry_count should be 1
|
|
And getting "b" from the hot tier should return "bbb"
|
|
|
|
Scenario: on_evict is not called when no eviction occurs
|
|
Given a HotStorageTier with max_entries 5 and an eviction recorder
|
|
When I put entry "only" with content "data"
|
|
Then the eviction recorder should have recorded 0 evictions
|
|
|
|
# ---- Remove ----
|
|
|
|
Scenario: Remove an existing entry
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "r1" with content "remove-me"
|
|
And I remove "r1" from the hot tier
|
|
Then getting "r1" from the hot tier should return None
|
|
And the hot tier entry_count should be 0
|
|
|
|
Scenario: Remove returns the content of the removed entry
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "r2" with content "my-content"
|
|
And I remove "r2" from the hot tier
|
|
Then removing "r2" from the hot tier should return "my-content"
|
|
|
|
Scenario: Remove returns None for missing entry
|
|
Given a HotStorageTier with no capacity limits
|
|
When I remove "nonexistent" from the hot tier
|
|
Then removing "nonexistent" from the hot tier should return None
|
|
|
|
Scenario: Remove updates size_bytes correctly
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "s1" with content "hello"
|
|
And I remove "s1" from the hot tier
|
|
Then the hot storage tier size_bytes should be 0
|
|
|
|
# ---- Clear ----
|
|
|
|
Scenario: Clear removes all entries
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "c1" with content "aaa"
|
|
And I put entry "c2" with content "bbb"
|
|
And I clear the hot tier
|
|
Then the hot tier entry_count should be 0
|
|
And the hot storage tier size_bytes should be 0
|
|
|
|
Scenario: Clear does not reset hit/miss counters
|
|
Given a HotStorageTier with no capacity limits
|
|
When I put entry "c1" with content "aaa"
|
|
And I get "c1" from the hot tier
|
|
And I clear the hot tier
|
|
Then the hot tier hit_count should be 1
|
|
|
|
# ---- Thread safety ----
|
|
|
|
Scenario: Concurrent puts from multiple threads do not corrupt the cache
|
|
Given a HotStorageTier with max_entries 50
|
|
When 10 threads concurrently put 5 entries each into the hot tier
|
|
Then no exception should have been raised during concurrent puts
|
|
And the hot tier entry_count should be at most 50
|
|
|
|
Scenario: Concurrent gets and puts do not raise RuntimeError
|
|
Given a HotStorageTier with no capacity limits
|
|
When 5 threads concurrently put entries and 5 threads concurrently get entries
|
|
Then no exception should have been raised during concurrent gets and puts
|
|
|
|
Scenario: Concurrent evictions do not corrupt size_bytes
|
|
Given a HotStorageTier with max_bytes 100
|
|
When 8 threads concurrently put large entries into the hot tier
|
|
Then no exception should have been raised during concurrent evictions
|
|
And the hot storage tier size_bytes should be at most 100
|