Skip to content

Ft/v0.3.0#13

Merged
dev-davexoyinbo merged 41 commits into
mainfrom
ft/v0.3.0
Apr 13, 2026
Merged

Ft/v0.3.0#13
dev-davexoyinbo merged 41 commits into
mainfrom
ft/v0.3.0

Conversation

@dev-davexoyinbo
Copy link
Copy Markdown
Owner

Branch Changelog

Summary

This branch expands the counter APIs, improves the Redis key type, refreshes documentation, and fixes benchmark coverage to match the current public surface.

Added

  • Conditional counter writes using CounterComparator across plain and instance-aware counters:
    • inc_if
    • set_if
    • set_on_instance_if
    • set_all_if
    • set_all_on_instance_if
  • Batch increment APIs for both counter families:
    • inc_all
    • inc_all_if
  • Payload-based comparator variants:
    • CounterComparator::Eq(i64)
    • CounterComparator::Lt(i64)
    • CounterComparator::Gt(i64)
    • CounterComparator::Ne(i64)
    • CounterComparator::Nil
  • New DistkitRedisKey helpers:
    • default_prefix
    • new
    • new_or_panic
    • try_sanitize
    • sanitize_or_panic

Changed

  • Conditional write APIs now use the comparator payload enum instead of separate compare_against parameters.
  • Unconditional write methods delegate through the corresponding *_if implementations with CounterComparator::Nil.
  • Strict counter and strict instance-aware counter Lua write paths are unified so conditional and unconditional writes share the same scripts.
  • Public docs and examples now use DistkitRedisKey as the primary key type.
  • The crate still exposes a hidden RedisKey alias for compatibility while the new public name is DistkitRedisKey.

Documentation

  • Updated trait docs for CounterTrait and InstanceAwareCounterTrait with examples and assertions for the new conditional and batch APIs.
  • Refreshed crate docs and README examples to use DistkitRedisKey.
  • Added documentation for the new DistkitRedisKey helper methods.

Tests

  • Added comparator-driven coverage for conditional inc, set, and batch setters.
  • Added coverage for CounterComparator::Nil.
  • Added tests for:
    • inc_all
    • inc_all_if
    • duplicate-key sequential batch behavior
    • missing-key comparisons
    • stale-refresh behavior in lax counters
    • cumulative-vs-instance-slice comparisons in instance-aware counters

Benchmarks

  • Updated benchmark helpers and bench call sites to use DistkitRedisKey.
  • Replaced the strict instance-aware private batch benchmark path with the public inc_all API.
  • Renamed the strict instance-aware batch benchmark from inc_batch_10 to inc_all_10.

Verification

  • cargo test --doc --all-features
  • cargo bench --no-run

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

v0.3.0: Conditional writes, batch operations, and DistkitRedisKey refactor

✨ Enhancement 🧪 Tests 📝 Documentation 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• **Conditional write operations**: Added CounterComparator enum with variants (Eq, Lt, Gt,
  Ne, Nil) enabling payload-based conditional writes across all counter types via new inc_if and
  set_if methods
• **Batch operations**: Implemented batch APIs (get_all, inc_all, inc_all_if, set_all,
  set_all_if) for both plain and instance-aware counters with deduplication, chunking, and stale key
  refresh
• **Redis key type refresh**: Renamed RedisKey to DistkitRedisKey with new helper methods
  (default_prefix, new, new_or_panic, try_sanitize, sanitize_or_panic) and added
  backwards-compatible alias
• **Lua script unification**: Unified strict counter and strict instance-aware counter write paths
  so conditional and unconditional writes share the same scripts via compare_values helper function
• **Lax counter improvements**: Added pending_flushed tracking, extracted flush logic into
  separate method, and implemented stale key refresh via batch_refresh_stale helper
• **Robust batch execution**: Introduced execute_pipeline_with_script_retry utility for automatic
  script reloading on pipeline failures
• **Activity tracker fix**: Fixed signal ordering in ActivityTracker by adding atomic store before
  watch signal
• **Comprehensive test coverage**: Added tests for all conditional and batch operations, comparator
  variants, duplicate key handling, and stale refresh scenarios
• **Documentation refresh**: Updated all examples and docs to use DistkitRedisKey, added
  CounterComparator documentation, and included practical conditional write examples
• **Benchmark updates**: Renamed inc_batch_10 to inc_all_10 and migrated to public inc_all API
Diagram
flowchart LR
  A["CounterComparator<br/>Eq/Lt/Gt/Ne/Nil"] -->|"matches()"| B["Conditional<br/>Evaluation"]
  B -->|"inc_if/set_if"| C["Plain Counters<br/>Strict & Lax"]
  B -->|"set_on_instance_if"| D["Instance-Aware<br/>Counters"]
  E["Batch APIs<br/>get_all/inc_all"] -->|"dedup & chunk"| F["Pipeline<br/>Execution"]
  F -->|"execute_pipeline_with_script_retry"| G["Lua Scripts<br/>with compare_values"]
  H["DistkitRedisKey<br/>helpers"] -->|"sanitize/validate"| I["Redis Keys"]
  C -->|"uses"| G
  D -->|"uses"| G
Loading

Grey Divider

File Changes

1. src/icounter/strict_instance_aware_counter.rs ✨ Enhancement +668/-234

Conditional writes and batch operations for strict instance-aware counters

• Added compare_values Lua helper function to support conditional writes with CounterComparator
 variants (eq, lt, gt, ne, nil)
• Updated INC_LUA, SET_LUA, and SET_ON_INSTANCE_LUA scripts to accept comparator parameters
 and return matched flag (6-element tuples instead of 5)
• Implemented conditional single-key methods: inc_if, set_if, set_on_instance_if delegating
 through comparator-based Lua scripts
• Added batch operation methods: inc_if_batch, get_batch, set_batch, set_if_batch,
 set_on_instance_batch, set_on_instance_if_batch with deduplication and chunking
• Refactored recover_contributions_batched to use new execute_pipeline_with_script_retry helper
 for cleaner pipeline management
• Updated trait implementations to expose new conditional and batch APIs through
 InstanceAwareCounterTrait

src/icounter/strict_instance_aware_counter.rs


2. src/icounter/lax_instance_aware_counter.rs ✨ Enhancement +431/-97

Conditional writes and batch operations for lax instance-aware counters

• Added pending_flushed field to track pending increments across flush cycles
• Extracted flush logic into separate flush() method for better error handling and testability
• Implemented refresh_local_if_needed and batch_refresh_stale helpers to refresh stale keys from
 strict counter
• Added conditional methods: inc_if, set_if, set_on_instance_if with local comparator
 evaluation
• Implemented batch operations: get_all, get_all_on_instance, inc_all, inc_all_if,
 set_all, set_all_if, set_all_on_instance, set_all_on_instance_if
• Updated trait implementations to expose new conditional and batch APIs

src/icounter/lax_instance_aware_counter.rs


3. src/icounter/mod.rs ✨ Enhancement +311/-28

Trait definitions for conditional and batch counter operations

• Added inc_if, set_if, set_on_instance_if trait methods for conditional single-key operations
• Added batch operation trait methods: get_all, get_all_on_instance, inc_all, inc_all_if,
 set_all, set_all_if, set_all_on_instance, set_all_on_instance_if
• Updated all documentation examples to use DistkitRedisKey instead of RedisKey
• Added comprehensive docstrings with examples for all new conditional and batch APIs

src/icounter/mod.rs


View more (23)
4. src/comparator.rs ✨ Enhancement +74/-0

Counter comparator enum for conditional write operations

• New file defining CounterComparator enum with variants: Eq(i64), Lt(i64), Gt(i64),
 Ne(i64), Nil
• Implemented matches() method to evaluate comparator against current counter value
• Implemented as_lua_parts() helper to convert comparator to Lua string representation and operand
• Added comprehensive documentation and examples for comparator usage
• Included unit tests validating all comparator variants

src/comparator.rs


5. src/icounter/tests/lax_instance_aware_counter.rs 🧪 Tests +566/-5

Comprehensive test suite for lax instance-aware counter batch and conditional operations

• Added comprehensive test coverage for lax instance-aware counter operations including dec,
 get_all, get_all_on_instance, set_all_on_instance, set_all, and conditional variants
• Added tests for stale cumulative divergence scenarios demonstrating cache staleness re-fetch
 behavior
• Implemented tests for batch operations with duplicate key handling and sequential processing
• Updated imports to use DistkitRedisKey instead of RedisKey and added CounterComparator
 import

src/icounter/tests/lax_instance_aware_counter.rs


6. src/counter/lax_counter.rs ✨ Enhancement +265/-86

Add conditional and batch counter operations with stale refresh

• Implemented conditional increment/set operations (inc_if, set_if) using CounterComparator
 for payload-based comparisons
• Added batch operations (get_all, inc_all, inc_all_if, set_all, set_all_if) with stale
 key refresh capability
• Introduced batch_refresh_stale helper method to fetch missing/stale keys from Redis in a single
 pipeline
• Updated Lua scripts to return tuples with keys for batch processing and added
 execute_pipeline_with_script_retry helper usage

src/counter/lax_counter.rs


7. src/counter/tests/lax_counter.rs 🧪 Tests +378/-4

Test coverage for conditional and batch lax counter operations

• Added tests for conditional operations (inc_if, set_if) with all comparator variants
• Added comprehensive batch operation tests (get_all, inc_all, inc_all_if, set_all,
 set_all_if) covering empty inputs, duplicates, and stale key refresh
• Added tests for flush behavior with set and dec operations
• Verified eventual consistency and visibility across fresh instances

src/counter/tests/lax_counter.rs


8. src/counter/counter_trait.rs ✨ Enhancement +205/-19

Expand CounterTrait with conditional and batch operations

• Added trait methods for conditional operations: inc_if and set_if with CounterComparator
 parameter
• Added batch operation trait methods: get_all, inc_all, inc_all_if, set_all, set_all_if
• Updated all documentation examples to use DistkitRedisKey instead of RedisKey
• Added comprehensive doc comments with examples for all new conditional and batch methods

src/counter/counter_trait.rs


9. src/counter/strict_counter.rs ✨ Enhancement +195/-25

Implement conditional and batch operations for strict counter

• Implemented conditional increment/set operations (inc_if, set_if) with Lua-based comparisons
 using CounterComparator
• Added batch operations (get_all, inc_all, inc_all_if, set_all, set_all_if) with pipeline
 execution and script retry logic
• Updated Lua scripts to include helper function compare_values and return tuples with keys for
 batch processing
• Integrated execute_pipeline_with_script_retry for robust batch execution with automatic script
 reloading

src/counter/strict_counter.rs


10. src/counter/tests/strict_counter.rs 🧪 Tests +117/-4

Test coverage for strict counter conditional and batch operations

• Added tests for conditional operations (inc_if, set_if) with all comparator variants
• Added tests for batch operations (inc_all, inc_all_if, set_all, set_all_if) covering
 partial success, missing keys, and duplicates
• Verified sequential processing of duplicate keys and proper comparator evaluation

src/counter/tests/strict_counter.rs


11. src/common/mod.rs ✨ Enhancement +155/-11

Rename RedisKey to DistkitRedisKey with enhanced utilities

• Renamed RedisKey to DistkitRedisKey with comprehensive documentation and validation examples
• Added helper methods: default_prefix, new, new_or_panic, try_sanitize, sanitize_or_panic
• Introduced execute_pipeline_with_script_retry utility function for robust batch script execution
 with automatic retry on script not found
• Added RedisKey type alias for backwards compatibility
• Added regex-based colon stripping functionality for key sanitization

src/common/mod.rs


12. src/icounter/tests/strict_instance_aware_counter.rs 🧪 Tests +158/-0

Test coverage for strict instance-aware conditional and batch operations

• Added tests for conditional instance-aware operations: inc_if, set_on_instance_if,
 set_all_if, set_all_on_instance_if
• Added tests for batch operations: inc_all, inc_all_if with comparator support
• Verified partial success scenarios, duplicate key handling, and sequential processing
• Tested comparisons against cumulative and instance slice values

src/icounter/tests/strict_instance_aware_counter.rs


13. src/icounter/tests/common.rs Miscellaneous +13/-10

Update instance-aware counter test helpers to use DistkitRedisKey

• Updated all test helper functions to use DistkitRedisKey instead of RedisKey
• Updated key helper function return type and all counter creation calls

src/icounter/tests/common.rs


14. src/__doctest_helpers.rs Miscellaneous +3/-3

Update doctest helpers to use DistkitRedisKey

• Updated doctest helper functions to use DistkitRedisKey instead of RedisKey
• Updated unique_prefix function return type

src/__doctest_helpers.rs


15. src/error.rs 📝 Documentation +2/-2

Update error documentation for DistkitRedisKey

• Updated error documentation to reference DistkitRedisKey instead of RedisKey

src/error.rs


16. benches/strict_instance_aware_counter.rs ✨ Enhancement +11/-11

Update benchmark to use public inc_all API

• Renamed benchmark function from inc_batch_10 to inc_all_10 to match the new public API
• Updated import from RedisKey to DistkitRedisKey and added InstanceAwareCounterTrait import
• Changed benchmark to use borrowed &DistkitRedisKey slices instead of owned `Vec<(RedisKey,
 i64)> with inc_batch`
• Updated to call public inc_all API instead of private inc_batch method

benches/strict_instance_aware_counter.rs


17. src/counter/tests/common.rs ✨ Enhancement +15/-7

Migrate counter test helpers to DistkitRedisKey

• Updated all imports and type references from RedisKey to DistkitRedisKey
• Reformatted multi-line function calls for better readability
• Updated helper functions make_strict_counter, make_lax_counter, and key to use
 DistkitRedisKey

src/counter/tests/common.rs


18. benches/common.rs ✨ Enhancement +7/-8

Update benchmark common helpers to DistkitRedisKey

• Replaced RedisKey import with DistkitRedisKey in benchmark helpers
• Updated key() function to return DistkitRedisKey and use try_from constructor
• Updated bench_prefix() internal helper to use DistkitRedisKey
• Reformatted long error message strings for consistency

benches/common.rs


19. src/counter/mod.rs ✨ Enhancement +5/-5

Update CounterOptions to use DistkitRedisKey

• Changed CounterOptions struct field prefix type from RedisKey to DistkitRedisKey
• Updated new() constructor signature to accept DistkitRedisKey parameter
• Updated documentation examples to use DistkitRedisKey instead of RedisKey

src/counter/mod.rs


20. src/common/activity_tracker.rs 🐞 Bug fix +1/-0

Fix activity tracker signal ordering

• Added atomic store operation self.is_active.store(true, Ordering::Release) in the signal()
 method
• This ensures the active flag is set before sending the watch signal

src/common/activity_tracker.rs


21. src/lib.rs ✨ Enhancement +2/-0

Export comparator module to public API

• Added new module exports for comparator module
• Exports CounterComparator and related types to the public API

src/lib.rs


22. src/icounter/tests/mod.rs Formatting +1/-1

Reorder instance-aware counter test modules

• Reordered test module declarations to alphabetical order (lax before strict)

src/icounter/tests/mod.rs


23. docs/lib.md 📝 Documentation +68/-20

Refresh documentation with DistkitRedisKey and conditional writes

• Replaced all RedisKey references with DistkitRedisKey throughout documentation
• Added new section documenting CounterComparator with examples of conditional writes (inc_if,
 set_if)
• Added examples demonstrating batch conditional writes with inc_all_if
• Added section on conditional instance-aware writes explaining comparison semantics
• Updated error handling section to reference DistkitRedisKey::try_from
• Enhanced documentation with practical examples of conditional and batch operations

docs/lib.md


24. README.md 📝 Documentation +43/-14

Update README with DistkitRedisKey and conditional write examples

• Updated all code examples to use DistkitRedisKey instead of RedisKey
• Added new section demonstrating conditional writes using CounterComparator::Eq and
 CounterComparator::Gt
• Added examples of batch increment operations with inc_all_if
• Added documentation on conditional instance-aware writes and comparison semantics
• Updated instance-aware counter examples to use DistkitRedisKey

README.md


25. CLAUDE.md 📝 Documentation +102/-0

Add Claude Code guidance and architecture documentation

• New file providing guidance for Claude Code when working with the repository
• Documents make commands for testing and benchmarking
• Provides architecture overview of counter families and consistency modes
• Explains instance-aware counter design with UUID tracking and dead instance cleanup
• Documents Lua script organization and the execute_pipeline_with_script_retry helper
• Describes ActivityTracker implementation for lax flush task management
• Lists feature flags and provides module layout reference

CLAUDE.md


26. Cargo.toml Dependencies +2/-1

Bump version and add regex dependency

• Bumped version from 0.2.3 to 0.3.0
• Added regex = "1.12.3" to dependencies (likely for DistkitRedisKey sanitization helpers)

Cargo.toml


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 13, 2026

Code Review by Qodo

🐞 Bugs (3)   📘 Rule violations (2)   📎 Requirement gaps (0)   🖥 UI issues (0)   🎨 UX Issues (0)
🐞\ ≡ Correctness (3)
📘\ ≡ Correctness (1) ☼ Reliability (1)

Grey Divider


Action required

1. cargo test --doc missing REDIS_URL 📘
Description
CLAUDE.md instructs running cargo test --doc --all-features without setting REDIS_URL, despite
stating tests require a live Redis and REDIS_URL. This can cause contributors/automation to run
tests in an invalid environment, violating the required test invocation guidance.
Code

CLAUDE.md[R14-19]

+To run tests manually (Redis must be running):
+```bash
+REDIS_URL="redis://127.0.0.1:16379/" cargo test --all-features
+REDIS_URL="redis://127.0.0.1:16379/" cargo test --all-features <test_name>  # single test
+cargo test --doc --all-features                                              # doctests only
+```
Evidence
The compliance rule requires using make test for the full suite, and any direct cargo test
instructions must set REDIS_URL and assume a live Redis. The added documentation includes a bare
cargo test --doc --all-features command without REDIS_URL while the surrounding text states
Redis/REDIS_URL are required.

CLAUDE.md
CLAUDE.md[14-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CLAUDE.md` includes a bare `cargo test --doc --all-features` command without `REDIS_URL`, which conflicts with the requirement that tests run with a live Redis and `REDIS_URL` set.

## Issue Context
The compliance checklist requires that documentation/instructions use `make test` for the full test suite, and that any direct `cargo test` invocation sets `REDIS_URL` and assumes Redis is running.

## Fix Focus Areas
- CLAUDE.md[14-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. inc_if_batch uses .zip() 📘
Description
StrictInstanceAwareCounter::inc_if_batch zips chunk/local_epochs with chunk_results,
aligning by position and ignoring the key string returned in each result tuple. This violates the
requirement to associate results with keys using the echoed key, not positional ordering.
Code

src/icounter/strict_instance_aware_counter.rs[R868-875]

+            for (
+                ((key, _, _), local_epoch),
+                (_, cumulative, inst_count, redis_epoch, _, matched_raw),
+            ) in chunk
+                .iter()
+                .zip(local_epochs.iter())
+                .zip(chunk_results.into_iter())
+            {
Evidence
The compliance rule requires building a keyed map from the returned key string instead of positional
alignment. The added code uses .zip(local_epochs.iter()).zip(chunk_results.into_iter()),
destructuring and discarding the returned key string (_) from each result tuple.

CLAUDE.md
src/icounter/strict_instance_aware_counter.rs[868-875]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`StrictInstanceAwareCounter::inc_if_batch` aligns pipeline results to inputs with `.zip()`, discarding the echoed key in the Lua result tuple and relying on positional ordering.

## Issue Context
The Lua scripts already echo keys; batch callers should build a `HashMap` keyed by that returned key string and then update local state/output by looking up the returned key.

## Fix Focus Areas
- src/icounter/strict_instance_aware_counter.rs[839-881]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Global set batch drops duplicates 🐞
Description
StrictInstanceAwareCounter::set_if_batch stores results in a HashMap keyed by DistkitRedisKey and
then rebuilds the output by key lookup, so duplicate keys return the last result for every
occurrence. This produces incorrect (cumulative, instance_count) pairs for earlier duplicate entries
and breaks the trait’s per-item ordered batch semantics.
Code

src/icounter/strict_instance_aware_counter.rs[R981-1040]

+        let mut conn = self.connection_manager.clone();
+        let mut map: HashMap<DistkitRedisKey, (i64, i64)> = HashMap::with_capacity(updates.len());
+        let mut processed = 0;

-            for (key_str, cumulative, inst_count, redis_epoch, _) in chunk_results {
-                if let Ok(key) = RedisKey::try_from(key_str.clone()) {
+        while processed < updates.len() {
+            let end = (processed + MAX_BATCH_SIZE).min(updates.len());
+            let chunk = &updates[processed..end];
+            let script = &self.set_script;
+            let local_epochs: HashMap<DistkitRedisKey, u64> = chunk
+                .iter()
+                .map(|(key, _, _)| ((*key).clone(), self.get_local_epoch(key)))
+                .collect();
+
+            let chunk_results: Vec<(String, i64, i64, u64, i64, i64)> =
+                execute_pipeline_with_script_retry(&mut conn, script, chunk, |update| {
+                    let (key, comparator, count) = update;
+                    let (lua_comparator, compare_against) = comparator.as_lua_parts();
+                    let mut inv = script.key(self.epoch_key());
+                    inv.key(self.instances_key());
+                    inv.key(self.cumulative_key());
+                    inv.key(self.keys_key());
+                    inv.key(self.inst_count_key());
+                    inv.arg(key.as_str());
+                    inv.arg(lua_comparator);
+                    inv.arg(compare_against);
+                    inv.arg(*count);
+                    inv.arg(self.get_local_epoch(key));
+                    inv.arg(self.get_local_count(key));
+                    inv.arg(self.dead_instance_threshold_ms);
+                    inv.arg(self.prefix_str());
+                    inv.arg(&self.instance_id);
+                    inv.arg(self.max_epoch);
+                    inv
+                })
+                .await?;
+
+            for (key, cumulative, inst_count, redis_epoch, _, matched_raw) in chunk_results {
+                let Ok(key) = DistkitRedisKey::try_from(key.clone()) else {
+                    continue;
+                };
+
+                let local_epoch = local_epochs.get(&key).copied().unwrap_or(0);
+                if matched_raw != 0 || local_epoch == redis_epoch {
                    self.update_local_store(&key, redis_epoch, cumulative, inst_count);
                }
-                output.push((key_str, cumulative, inst_count));
+
+                map.insert(key, (cumulative, inst_count));
            }

            processed = end;
        }

-        // All chunks succeeded — drain entire input.
-        increments.drain(..processed);
+        Ok(updates
+            .iter()
+            .map(|(k, _, _)| {
+                let (cum, inst) = map.get(k).copied().unwrap_or((0, 0));
+                (*k, cum, inst)
+            })
+            .collect())
+    }
Evidence
InstanceAwareCounterTrait::set_all_if documents per-item evaluation with results preserving input
order, but set_if_batch collapses multiple results for the same key into a single HashMap entry,
losing earlier per-item results. The public trait method set_all_if delegates directly to this
batch function, so the bug is exposed via the public API.

src/icounter/mod.rs[467-472]
src/icounter/strict_instance_aware_counter.rs[971-1040]
src/icounter/strict_instance_aware_counter.rs[1768-1773]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`StrictInstanceAwareCounter::set_if_batch` uses a `HashMap<DistkitRedisKey, (i64, i64)>` and later reconstructs the output by key lookup. Duplicate keys in the batch overwrite earlier entries, so the returned Vec is wrong for earlier duplicates.

## Issue Context
Redis pipelines return replies in the same order as invocations. The safest approach is to accumulate an output Vec in the same iteration order (e.g., zip `chunk` with `chunk_results`) and avoid de-duplicating results.

## Fix Focus Areas
- src/icounter/strict_instance_aware_counter.rs[971-1040]
- src/icounter/strict_instance_aware_counter.rs[1768-1773]
- src/icounter/mod.rs[467-472]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. get_all_on_instance does Redis I/O 🐞
Description
InstanceAwareCounterTrait::get_all_on_instance is documented as pure-local with no Redis round-trip,
but both lax and strict implementations call Redis refresh/get batch paths. Callers relying on
local-only behavior will incur unexpected network latency and staleness checks.
Code

src/icounter/mod.rs[R389-395]

+    /// Returns `(key, instance_count)` for each key in `keys`, in the same
+    /// order. Pure-local: no Redis round-trip, no staleness check. A key
+    /// with no local contribution returns `(key, 0)`.
+    async fn get_all_on_instance<'k>(
+        &self,
+        keys: &[&'k DistkitRedisKey],
+    ) -> Result<Vec<(&'k DistkitRedisKey, i64)>, DistkitError>;
Evidence
The public trait explicitly promises “Pure-local: no Redis round-trip,” but
LaxInstanceAwareCounter::get_all_on_instance calls batch_refresh_stale (Redis pipeline path) and
StrictInstanceAwareCounter::get_all_on_instance calls get_batch (Redis script pipeline path).

src/icounter/mod.rs[389-395]
src/icounter/lax_instance_aware_counter.rs[852-857]
src/icounter/strict_instance_aware_counter.rs[1734-1739]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`InstanceAwareCounterTrait::get_all_on_instance` is documented as pure-local (no Redis I/O), but current implementations perform Redis calls.

## Issue Context
Either (a) update implementations to compute instance counts strictly from local state (no refresh / no Redis scripts), accepting potentially stale results as documented, or (b) change the trait docs to match the actual behavior.

## Fix Focus Areas
- src/icounter/mod.rs[389-395]
- src/icounter/lax_instance_aware_counter.rs[852-871]
- src/icounter/strict_instance_aware_counter.rs[1734-1740]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Conditional no-op leaves stale cache 🐞
Description
StrictInstanceAwareCounter::inc_if (and related conditional setters) updates local_store only when
the comparator matched or when local_epoch equals redis_epoch, so a comparator failure during an
epoch mismatch leaves local epoch/local_count stale. This can cause subsequent operations and
heartbeat recovery to use outdated local state.
Code

src/icounter/strict_instance_aware_counter.rs[R1257-1259]

+        if matched_raw != 0 || local_epoch == redis_epoch {
+            self.update_local_store(key, redis_epoch, cumulative, inst_count);
        }
Evidence
The conditional methods explicitly skip update_local_store when matched_raw == 0 and
local_epoch != redis_epoch, even though the Redis scripts return the latest redis_epoch and
counts. local_store is the source of truth for get_local_epoch/get_local_count and is used to
decide recovery work in mark_alive, so leaving it stale after a read/no-op causes downstream
incorrect local behavior and unnecessary recovery attempts.

src/icounter/strict_instance_aware_counter.rs[1227-1259]
src/icounter/strict_instance_aware_counter.rs[658-695]
src/icounter/strict_instance_aware_counter.rs[1143-1162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Conditional instance-aware operations skip updating `local_store` when the comparator fails during an epoch mismatch (`local_epoch != redis_epoch`). This leaves cached epoch/local_count stale even though Redis returned fresh state.

## Issue Context
A common scenario is: another instance bumps the epoch (via set/del), this instance has a stale `local_epoch`, and then a conditional write fails its comparator. The method returns fresh `(cumulative, inst_count)` but does not update cache, so future calls keep using stale epoch/local_count.

## Fix Focus Areas
- src/icounter/strict_instance_aware_counter.rs[1219-1262]
- src/icounter/strict_instance_aware_counter.rs[1291-1340]
- src/icounter/strict_instance_aware_counter.rs[1373-1421]
- src/icounter/strict_instance_aware_counter.rs[658-695]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@dev-davexoyinbo dev-davexoyinbo merged commit c5dbf26 into main Apr 13, 2026
1 check passed
@dev-davexoyinbo dev-davexoyinbo deleted the ft/v0.3.0 branch April 13, 2026 00:37
Comment thread CLAUDE.md
Comment on lines +14 to +19
To run tests manually (Redis must be running):
```bash
REDIS_URL="redis://127.0.0.1:16379/" cargo test --all-features
REDIS_URL="redis://127.0.0.1:16379/" cargo test --all-features <test_name> # single test
cargo test --doc --all-features # doctests only
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. cargo test --doc missing redis_url 📘 Rule violation ☼ Reliability

CLAUDE.md instructs running cargo test --doc --all-features without setting REDIS_URL, despite
stating tests require a live Redis and REDIS_URL. This can cause contributors/automation to run
tests in an invalid environment, violating the required test invocation guidance.
Agent Prompt
## Issue description
`CLAUDE.md` includes a bare `cargo test --doc --all-features` command without `REDIS_URL`, which conflicts with the requirement that tests run with a live Redis and `REDIS_URL` set.

## Issue Context
The compliance checklist requires that documentation/instructions use `make test` for the full test suite, and that any direct `cargo test` invocation sets `REDIS_URL` and assumes Redis is running.

## Fix Focus Areas
- CLAUDE.md[14-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +868 to +875
for (
((key, _, _), local_epoch),
(_, cumulative, inst_count, redis_epoch, _, matched_raw),
) in chunk
.iter()
.zip(local_epochs.iter())
.zip(chunk_results.into_iter())
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. inc_if_batch uses .zip() 📘 Rule violation ≡ Correctness

StrictInstanceAwareCounter::inc_if_batch zips chunk/local_epochs with chunk_results,
aligning by position and ignoring the key string returned in each result tuple. This violates the
requirement to associate results with keys using the echoed key, not positional ordering.
Agent Prompt
## Issue description
`StrictInstanceAwareCounter::inc_if_batch` aligns pipeline results to inputs with `.zip()`, discarding the echoed key in the Lua result tuple and relying on positional ordering.

## Issue Context
The Lua scripts already echo keys; batch callers should build a `HashMap` keyed by that returned key string and then update local state/output by looking up the returned key.

## Fix Focus Areas
- src/icounter/strict_instance_aware_counter.rs[839-881]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +981 to +1040
let mut conn = self.connection_manager.clone();
let mut map: HashMap<DistkitRedisKey, (i64, i64)> = HashMap::with_capacity(updates.len());
let mut processed = 0;

for (key_str, cumulative, inst_count, redis_epoch, _) in chunk_results {
if let Ok(key) = RedisKey::try_from(key_str.clone()) {
while processed < updates.len() {
let end = (processed + MAX_BATCH_SIZE).min(updates.len());
let chunk = &updates[processed..end];
let script = &self.set_script;
let local_epochs: HashMap<DistkitRedisKey, u64> = chunk
.iter()
.map(|(key, _, _)| ((*key).clone(), self.get_local_epoch(key)))
.collect();

let chunk_results: Vec<(String, i64, i64, u64, i64, i64)> =
execute_pipeline_with_script_retry(&mut conn, script, chunk, |update| {
let (key, comparator, count) = update;
let (lua_comparator, compare_against) = comparator.as_lua_parts();
let mut inv = script.key(self.epoch_key());
inv.key(self.instances_key());
inv.key(self.cumulative_key());
inv.key(self.keys_key());
inv.key(self.inst_count_key());
inv.arg(key.as_str());
inv.arg(lua_comparator);
inv.arg(compare_against);
inv.arg(*count);
inv.arg(self.get_local_epoch(key));
inv.arg(self.get_local_count(key));
inv.arg(self.dead_instance_threshold_ms);
inv.arg(self.prefix_str());
inv.arg(&self.instance_id);
inv.arg(self.max_epoch);
inv
})
.await?;

for (key, cumulative, inst_count, redis_epoch, _, matched_raw) in chunk_results {
let Ok(key) = DistkitRedisKey::try_from(key.clone()) else {
continue;
};

let local_epoch = local_epochs.get(&key).copied().unwrap_or(0);
if matched_raw != 0 || local_epoch == redis_epoch {
self.update_local_store(&key, redis_epoch, cumulative, inst_count);
}
output.push((key_str, cumulative, inst_count));

map.insert(key, (cumulative, inst_count));
}

processed = end;
}

// All chunks succeeded — drain entire input.
increments.drain(..processed);
Ok(updates
.iter()
.map(|(k, _, _)| {
let (cum, inst) = map.get(k).copied().unwrap_or((0, 0));
(*k, cum, inst)
})
.collect())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Global set batch drops duplicates 🐞 Bug ≡ Correctness

StrictInstanceAwareCounter::set_if_batch stores results in a HashMap keyed by DistkitRedisKey and
then rebuilds the output by key lookup, so duplicate keys return the last result for every
occurrence. This produces incorrect (cumulative, instance_count) pairs for earlier duplicate entries
and breaks the trait’s per-item ordered batch semantics.
Agent Prompt
## Issue description
`StrictInstanceAwareCounter::set_if_batch` uses a `HashMap<DistkitRedisKey, (i64, i64)>` and later reconstructs the output by key lookup. Duplicate keys in the batch overwrite earlier entries, so the returned Vec is wrong for earlier duplicates.

## Issue Context
Redis pipelines return replies in the same order as invocations. The safest approach is to accumulate an output Vec in the same iteration order (e.g., zip `chunk` with `chunk_results`) and avoid de-duplicating results.

## Fix Focus Areas
- src/icounter/strict_instance_aware_counter.rs[971-1040]
- src/icounter/strict_instance_aware_counter.rs[1768-1773]
- src/icounter/mod.rs[467-472]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/icounter/mod.rs
Comment on lines +389 to +395
/// Returns `(key, instance_count)` for each key in `keys`, in the same
/// order. Pure-local: no Redis round-trip, no staleness check. A key
/// with no local contribution returns `(key, 0)`.
async fn get_all_on_instance<'k>(
&self,
keys: &[&'k DistkitRedisKey],
) -> Result<Vec<(&'k DistkitRedisKey, i64)>, DistkitError>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Get_all_on_instance does redis i/o 🐞 Bug ≡ Correctness

InstanceAwareCounterTrait::get_all_on_instance is documented as pure-local with no Redis round-trip,
but both lax and strict implementations call Redis refresh/get batch paths. Callers relying on
local-only behavior will incur unexpected network latency and staleness checks.
Agent Prompt
## Issue description
`InstanceAwareCounterTrait::get_all_on_instance` is documented as pure-local (no Redis I/O), but current implementations perform Redis calls.

## Issue Context
Either (a) update implementations to compute instance counts strictly from local state (no refresh / no Redis scripts), accepting potentially stale results as documented, or (b) change the trait docs to match the actual behavior.

## Fix Focus Areas
- src/icounter/mod.rs[389-395]
- src/icounter/lax_instance_aware_counter.rs[852-871]
- src/icounter/strict_instance_aware_counter.rs[1734-1740]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1257 to 1259
if matched_raw != 0 || local_epoch == redis_epoch {
self.update_local_store(key, redis_epoch, cumulative, inst_count);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Conditional no-op leaves stale cache 🐞 Bug ≡ Correctness

StrictInstanceAwareCounter::inc_if (and related conditional setters) updates local_store only when
the comparator matched or when local_epoch equals redis_epoch, so a comparator failure during an
epoch mismatch leaves local epoch/local_count stale. This can cause subsequent operations and
heartbeat recovery to use outdated local state.
Agent Prompt
## Issue description
Conditional instance-aware operations skip updating `local_store` when the comparator fails during an epoch mismatch (`local_epoch != redis_epoch`). This leaves cached epoch/local_count stale even though Redis returned fresh state.

## Issue Context
A common scenario is: another instance bumps the epoch (via set/del), this instance has a stale `local_epoch`, and then a conditional write fails its comparator. The method returns fresh `(cumulative, inst_count)` but does not update cache, so future calls keep using stale epoch/local_count.

## Fix Focus Areas
- src/icounter/strict_instance_aware_counter.rs[1219-1262]
- src/icounter/strict_instance_aware_counter.rs[1291-1340]
- src/icounter/strict_instance_aware_counter.rs[1373-1421]
- src/icounter/strict_instance_aware_counter.rs[658-695]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant