34c2acc354
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 3m18s
CI / typecheck (pull_request) Successful in 4m3s
CI / security (pull_request) Successful in 4m13s
CI / build (pull_request) Successful in 21s
CI / quality (pull_request) Successful in 3m42s
CI / integration_tests (pull_request) Successful in 8m50s
CI / unit_tests (pull_request) Successful in 9m14s
CI / e2e_tests (pull_request) Successful in 9m28s
CI / docker (pull_request) Successful in 1m9s
CI / coverage (pull_request) Successful in 10m24s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-regression (pull_request) Successful in 48m36s
CI / build (push) Successful in 22s
CI / lint (push) Successful in 3m43s
CI / typecheck (push) Successful in 4m19s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m28s
CI / quality (push) Successful in 4m21s
CI / integration_tests (push) Successful in 9m12s
CI / e2e_tests (push) Successful in 9m29s
CI / unit_tests (push) Successful in 9m36s
CI / docker (push) Successful in 1m8s
CI / coverage (push) Successful in 10m33s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 25m40s
Implement the missing runtime logic for the ACMS context tier service. Previously only data models and manual promote()/demote()/evict_lru() methods existed. This commit adds: - Auto-promotion on access: get() now promotes fragments one tier up when access_count reaches the configurable promotion_threshold (default: 5 accesses). The access counter resets after each successful promotion so fragments must accumulate fresh accesses before the next tier transition. - Staleness enforcement: new enforce_staleness() method demotes hot fragments older than hot_ttl (default: 24h) to warm, and warm fragments older than warm_ttl to cold. A snapshot of existing warm-tier IDs prevents double-demotion in a single pass. - Budget enforcement on store and promote: store() and promote() now enforce TierBudget.max_tokens_hot by evicting LRU hot-tier fragments until the token budget is met. The eviction loop uses incremental token tracking to avoid recomputing the sum. - Event emission: Added TIER_PROMOTED, TIER_DEMOTED, TIER_EVICTED event types to EventType enum. All tier transitions emit DomainEvent instances through the optional EventBus. - Configuration: Added context_tier_promotion_threshold, context_tier_hot_ttl_hours, context_tier_warm_ttl_hours settings. The warm TTL setting also accepts the spec-defined env var CLEVERAGENTS_CTX_WARM_HOURS as an alias. - DI wiring: container.py now injects event_bus into context_tier_service. Review fixes applied (code review on PR #1150): - C1: Reset access_count to 0 after each auto-promotion to prevent chain promotion that bypassed the warm tier. - C2: Call _enforce_hot_budget() inside promote() warm-to-hot path so auto-promoted fragments respect the token budget. - H1: Corrected _enforce_hot_budget() docstring: actual complexity is O(n + n*k) not O(n), since min() scans remaining entries on each eviction. - M1: Added CLEVERAGENTS_CTX_WARM_HOURS as an additional env var alias for context_tier_warm_ttl_hours per specification line 30555. Review fixes applied (second code review on PR #1150): - B-CRIT-1: Fixed data loss in promote() warm-to-hot: emit TIER_PROMOTED before _enforce_hot_budget(), and if the promoted fragment is evicted by budget, restore it to the warm tier instead of silently losing it. - B-HIGH-1: Fixed self-eviction on store(): fragments whose token_count exceeds the entire hot-tier budget are now redirected to the warm tier with a warning log. - B-MED-1: Wrapped _emit_tier_event() in try/except so a failing event bus does not break tier operations (best-effort emission). - B-MED-2: Fixed event ordering so TIER_PROMOTED fires before any budget-triggered TIER_EVICTED events. - D-LOW-1: Fixed type hint in Robot helper (dict[str, Callable]). - D-LOW-2: Added __all__ export to context_tiers.py. - S-LOW-1: Added thread-safety docstring note to ContextTierService. Review fixes applied (third code review on PR #1150): - B-MED-1: Added TIER_DEMOTED event emission for oversized fragment redirect in store(), closing the observability gap where the only tier transition without event emission was the hot-to-warm redirect for fragments exceeding the entire hot-tier budget. - S-LOW-1: Added CLEVERAGENTS_CTX_HOT_HOURS as an additional env var alias for context_tier_hot_ttl_hours, for consistency with the warm-tier alias CLEVERAGENTS_CTX_WARM_HOURS. - S-LOW-2: Added docstring note to enforce_staleness() reconciling the hot-tier TTL with the specification statement that hot-tier retention is "Until resource removed" (TTL controls tier placement, not data retention). Review fixes applied (fourth code review on PR #1150): - B-HIGH-1: Reset access_count to 0 on demotion so that demoted fragments must accumulate fresh accesses before re-promotion. Without this reset, a previously popular fragment whose access_count already exceeded the promotion threshold would be re-promoted on the very next get() call, making staleness enforcement ineffective. Review fixes applied (freemo APPROVED review on PR #1150): - #1: Removed all # type: ignore annotations from test files. Fixed _EventCollector, _FailingBus, and _NullBus subscribe() signatures to use Callable[[DomainEvent], None] matching the EventBus protocol. Replaced dict-spread TieredFragment construction with explicit keyword arguments and post-construction assignment. - #2: Extracted runtime policy logic (enforce_staleness, _maybe_auto_promote, _re_fetch_after_promotion, _enforce_hot_budget, _emit_tier_event) into TierRuntimeMixin in tier_runtime.py to reduce context_tiers.py toward the 500-line guideline. - #3: Added fragment_id non-empty validation guard to promote() and demote() per CONTRIBUTING.md argument validation policy. - #8: Renamed _resolve to _re_fetch_after_promotion for clarity. Removed @tdd_expected_fail from TDD tests (Behave + Robot) as the bug is now fixed. All 3 TDD scenarios pass normally. Tests: 27 Behave scenarios (24 feature + 3 TDD), 4 Robot integration tests, 4 ASV benchmark suites. ISSUES CLOSED: #821
180 lines
9.3 KiB
Gherkin
180 lines
9.3 KiB
Gherkin
@mock_only
|
|
Feature: Context tier runtime promotion, demotion, and eviction logic
|
|
As an ACMS developer
|
|
I want the ContextTierService to automatically manage fragment lifecycle
|
|
So that frequently accessed fragments are promoted, stale fragments are
|
|
demoted, and budget limits are enforced without manual intervention
|
|
|
|
Background:
|
|
Given a context tier service with default settings
|
|
|
|
# -----------------------------------------------------------------
|
|
# Auto-promotion on access
|
|
# -----------------------------------------------------------------
|
|
|
|
Scenario: Fragment promoted from cold to warm after reaching access threshold
|
|
Given a fragment "frag-cold-01" stored in the cold tier with 50 tokens
|
|
When I access "frag-cold-01" via get 5 times
|
|
Then fragment "frag-cold-01" should be in the warm tier
|
|
|
|
Scenario: Fragment promoted from warm to hot after reaching access threshold
|
|
Given a fragment "frag-warm-01" stored in the warm tier with 50 tokens
|
|
When I access "frag-warm-01" via get 5 times
|
|
Then fragment "frag-warm-01" should be in the hot tier
|
|
|
|
Scenario: Fragment not promoted before reaching access threshold
|
|
Given a fragment "frag-cold-02" stored in the cold tier with 50 tokens
|
|
When I access "frag-cold-02" via get 3 times
|
|
Then fragment "frag-cold-02" should be in the cold tier
|
|
|
|
Scenario: Promoted fragment requires fresh accesses for next promotion
|
|
Given a fragment "frag-chain-01" stored in the cold tier with 50 tokens
|
|
When I access "frag-chain-01" via get 5 times
|
|
Then fragment "frag-chain-01" should be in the warm tier
|
|
When I access "frag-chain-01" via get 1 times
|
|
Then fragment "frag-chain-01" should be in the warm tier
|
|
When I access "frag-chain-01" via get 5 times
|
|
Then fragment "frag-chain-01" should be in the hot tier
|
|
|
|
# -----------------------------------------------------------------
|
|
# Staleness enforcement
|
|
# -----------------------------------------------------------------
|
|
|
|
Scenario: Stale hot fragment demoted to warm on staleness enforcement
|
|
Given a fragment "frag-stale-hot" stored in the hot tier with stale timestamp of 25 hours
|
|
When I invoke enforce_staleness on the service
|
|
Then fragment "frag-stale-hot" should be in the warm tier
|
|
|
|
Scenario: Fresh hot fragment not demoted on staleness enforcement
|
|
Given a fragment "frag-fresh-hot" stored in the hot tier with fresh timestamp
|
|
When I invoke enforce_staleness on the service
|
|
Then fragment "frag-fresh-hot" should be in the hot tier
|
|
|
|
Scenario: Stale warm fragment demoted to cold on staleness enforcement
|
|
Given a fragment "frag-stale-warm" stored in the warm tier with stale timestamp of 25 hours
|
|
When I invoke enforce_staleness on the service
|
|
Then fragment "frag-stale-warm" should be in the cold tier
|
|
|
|
Scenario: Staleness enforcement returns list of demoted fragment IDs
|
|
Given a fragment "frag-stale-a" stored in the hot tier with stale timestamp of 25 hours
|
|
And a fragment "frag-stale-b" stored in the hot tier with stale timestamp of 30 hours
|
|
When I invoke enforce_staleness on the service
|
|
Then the staleness result should contain 2 fragment IDs
|
|
|
|
# -----------------------------------------------------------------
|
|
# Budget enforcement on store
|
|
# -----------------------------------------------------------------
|
|
|
|
Scenario: Storing in hot tier when budget allows does not evict
|
|
Given a service with hot tier budget of 200 tokens
|
|
And a fragment "frag-a" stored in the hot tier with 50 tokens
|
|
When I store fragment "frag-b" in the hot tier with 50 tokens
|
|
Then fragment "frag-a" should be in the hot tier
|
|
And fragment "frag-b" should be in the hot tier
|
|
|
|
Scenario: Storing in hot tier over budget evicts oldest fragment
|
|
Given a service with hot tier budget of 100 tokens
|
|
And a fragment "frag-old" stored in the hot tier with 50 tokens and old timestamp
|
|
And a fragment "frag-new" stored in the hot tier with 50 tokens and recent timestamp
|
|
When I store fragment "frag-overflow" in the hot tier with 50 tokens
|
|
Then fragment "frag-old" should not be in any tier
|
|
And fragment "frag-new" should be in the hot tier
|
|
And fragment "frag-overflow" should be in the hot tier
|
|
|
|
Scenario: Oversized fragment redirected to warm tier instead of hot
|
|
Given a service with hot tier budget of 100 tokens
|
|
When I store fragment "frag-oversized" in the hot tier with 200 tokens
|
|
Then fragment "frag-oversized" should be in the warm tier
|
|
|
|
Scenario: Promotion to hot falls back to warm when budget evicts promoted fragment
|
|
Given a context tier service with an event bus and hot budget of 100 tokens
|
|
And a fragment "frag-resident" stored in the hot tier with 80 tokens and recent timestamp
|
|
And a fragment "frag-to-promote" stored in the warm tier with 80 tokens and old timestamp
|
|
When I promote fragment "frag-to-promote"
|
|
Then fragment "frag-to-promote" should be in the warm tier
|
|
And a TIER_PROMOTED event should have been emitted for "frag-to-promote"
|
|
|
|
# -----------------------------------------------------------------
|
|
# Event emission
|
|
# -----------------------------------------------------------------
|
|
|
|
Scenario: Promotion emits TIER_PROMOTED event with correct tier details
|
|
Given a context tier service with an event bus
|
|
And a fragment "frag-evt-01" stored in the cold tier with 50 tokens
|
|
When I promote fragment "frag-evt-01"
|
|
Then a TIER_PROMOTED event should have been emitted for "frag-evt-01"
|
|
And the last TIER_PROMOTED event should have from_tier "cold" and to_tier "warm"
|
|
|
|
Scenario: Demotion emits TIER_DEMOTED event with correct tier details
|
|
Given a context tier service with an event bus
|
|
And a fragment "frag-evt-02" stored in the hot tier with 50 tokens
|
|
When I demote fragment "frag-evt-02"
|
|
Then a TIER_DEMOTED event should have been emitted for "frag-evt-02"
|
|
And the last TIER_DEMOTED event should have from_tier "hot" and to_tier "warm"
|
|
|
|
Scenario: Budget eviction emits TIER_EVICTED event
|
|
Given a context tier service with an event bus and hot budget of 100 tokens
|
|
And a fragment "frag-evt-03" stored in the hot tier with 60 tokens and old timestamp
|
|
When I store fragment "frag-evt-04" in the hot tier with 60 tokens
|
|
Then a TIER_EVICTED event should have been emitted for "frag-evt-03"
|
|
|
|
Scenario: Explicit evict_lru emits TIER_EVICTED events
|
|
Given a context tier service with an event bus
|
|
And a fragment "frag-evict-01" stored in the hot tier with 50 tokens and old timestamp
|
|
And a fragment "frag-evict-02" stored in the hot tier with 50 tokens and recent timestamp
|
|
When I evict 1 LRU fragment from the hot tier
|
|
Then a TIER_EVICTED event should have been emitted for "frag-evict-01"
|
|
|
|
Scenario: Oversized fragment redirect emits TIER_DEMOTED event
|
|
Given a context tier service with an event bus and hot budget of 100 tokens
|
|
When I store fragment "frag-oversized-evt" in the hot tier with 200 tokens
|
|
Then fragment "frag-oversized-evt" should be in the warm tier
|
|
And a TIER_DEMOTED event should have been emitted for "frag-oversized-evt"
|
|
|
|
Scenario: Event emission failure does not break tier operation
|
|
Given a context tier service with a failing event bus
|
|
And a fragment "frag-evt-safe" stored in the cold tier with 50 tokens
|
|
When I promote fragment "frag-evt-safe"
|
|
Then fragment "frag-evt-safe" should be in the warm tier
|
|
|
|
# -----------------------------------------------------------------
|
|
# Edge cases
|
|
# -----------------------------------------------------------------
|
|
|
|
Scenario: Getting a nonexistent fragment returns nothing
|
|
When I access "no-such-fragment" via get 1 times
|
|
Then fragment "no-such-fragment" should not be in any tier
|
|
|
|
Scenario: Promoting a hot-tier fragment has no effect
|
|
Given a fragment "frag-already-hot" stored in the hot tier with 50 tokens
|
|
When I promote fragment "frag-already-hot"
|
|
Then fragment "frag-already-hot" should be in the hot tier
|
|
|
|
Scenario: Demoting a cold-tier fragment has no effect
|
|
Given a fragment "frag-already-cold" stored in the cold tier with 50 tokens
|
|
When I demote fragment "frag-already-cold"
|
|
Then fragment "frag-already-cold" should be in the cold tier
|
|
|
|
Scenario: Staleness enforcement with mixed stale hot and warm fragments
|
|
Given a context tier service with default settings
|
|
And a fragment "mixed-hot" stored in the hot tier with stale timestamp of 25 hours
|
|
And a fragment "mixed-warm" stored in the warm tier with stale timestamp of 25 hours
|
|
When I invoke enforce_staleness on the service
|
|
Then fragment "mixed-hot" should be in the warm tier
|
|
And fragment "mixed-warm" should be in the cold tier
|
|
And the staleness result should contain 2 fragment IDs
|
|
|
|
Scenario: Demoted fragment requires fresh accesses before re-promotion
|
|
Given a context tier service with default settings
|
|
And a fragment "frag-pop" stored in the hot tier with 50 tokens
|
|
When I access "frag-pop" via get 10 times
|
|
And I invoke enforce_staleness on the service with a hot TTL of 0 hours
|
|
And I access "frag-pop" via get 1 times
|
|
Then fragment "frag-pop" should be in the warm tier
|
|
|
|
Scenario: Promotion threshold of 1 promotes on first access without chain promotion
|
|
Given a context tier service with promotion threshold 1
|
|
And a fragment "frag-thresh" stored in the cold tier with 50 tokens
|
|
When I access "frag-thresh" via get 1 times
|
|
Then fragment "frag-thresh" should be in the warm tier
|