Skip to content

Conversation

@nicobuzeta
Copy link
Collaborator

@nicobuzeta nicobuzeta commented Nov 19, 2025

Summary by CodeRabbit

  • New Features

    • Partition-aware query evaluation: per-partition evaluators with caching and automatic routing.
  • Bug Fixes

    • Stricter out-of-order event detection with immediate error reporting in non-debug builds.
  • Improvements

    • Dynamic, bounded evaluator lifecycle with LRU-style eviction and proactive cleanup.
    • More consistent time-window handling and empty-state checks for evaluators.
  • Chores

    • CI workflows simplified by removing Artifactory upload/configuration steps.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.
📝 Walkthrough

Walkthrough

Adds evaluator state queries and unconditional out-of-order detection to Evaluator; reworks DynamicEvaluator storage to LRU-style weak/shared pointer management with on-demand creation and cleanup; implements partition-aware query evaluation with per-partition evaluator mapping and cached attribute-index lookups; removes multiple CI Artifactory steps.

Changes

Cohort / File(s) Change Summary
Evaluator Base Enhancements
src/core_server/internal/evaluation/evaluator.hpp
Added is_empty() and is_time_window_empty(uint64_t); moved last_tuple_time out of debug-only guard; made out-of-order detection unconditional (uses [[unlikely]]), logs critically and returns early in non-debug builds; debug builds retain sleep/assert behavior.
Dynamic Evaluator Lifecycle Management
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp
Replaced single-vector storage with std::vector<std::weak_ptr<...>> plus std::list<std::shared_ptr<...>> LRU backing; added max_evaluators, logger, get-or-create/save/cleanup helpers (get_or_create_evaluator, save_evaluator, clean_or_expand_evaluator_storage, etc.); process_event updated to acquire/create evaluators, apply LRU behavior, and reset on ANY policy matches.
Partition-Aware Query Evaluation
src/core_server/internal/interface/modules/query/query_types/partition_by_query.hpp
Introduced TupleValuesKey and ValueVectorHash; added partition_by_attrs_to_evaluator_idx and event_id_to_tuple_idx caches; extended create_query and process_event to compute tuple-indexes, build partition keys, find-or-create per-partition evaluator indices, and route events to DynamicEvaluator.
CI Workflow Cleanup
.github/workflows/benchmark.yaml, .github/workflows/checks.yaml, .github/workflows/test-checks.yaml, .github/workflows/upload-conan.yaml, .github/workflows/valgrind-checks.yaml
Removed steps that added/configured Artifactory Conan remote; removed Artifactory login/upload steps from upload workflow; left Conan default-remote update steps intact.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PartitionByQuery
    participant DynamicEvaluator
    participant Evaluator

    Client->>PartitionByQuery: process_event(event)
    rect rgba(200,220,255,0.18)
    Note over PartitionByQuery: resolve tuple indexes (cached)\nand build partition key
    PartitionByQuery->>PartitionByQuery: find_tuple_indexes / build key
    PartitionByQuery->>PartitionByQuery: find_or_create_evaluator_index(key)
    end

    PartitionByQuery->>DynamicEvaluator: process_event(event, evaluator_idx)
    rect rgba(220,255,220,0.18)
    Note over DynamicEvaluator: LRU get-or-create & cleanup
    DynamicEvaluator->>DynamicEvaluator: cleanup_evaluators_if_necessary(now)
    alt missing or expired evaluator
        DynamicEvaluator->>DynamicEvaluator: create_evaluator_*()
    end
    DynamicEvaluator->>Evaluator: next(event)
    end

    rect rgba(255,240,200,0.18)
    Note over Evaluator: evaluation & ordering checks
    Evaluator->>Evaluator: check out-of-order (unconditional)
    alt in-order
        Evaluator->>Evaluator: update last_tuple_time
        Evaluator->>Evaluator: evaluate event
    else out-of-order
        Evaluator-->>DynamicEvaluator: return empty / log critical
    end
    end

    Evaluator-->>DynamicEvaluator: result
    DynamicEvaluator-->>PartitionByQuery: result
    PartitionByQuery-->>Client: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Review weak_ptr/shared_ptr LRU semantics, iterator invalidation, and concurrency/race-safety in dynamic_evaluator.hpp.
  • Verify correctness and collision-resistance of TupleValuesKey equality/hash and cache invalidation in partition_by_query.hpp.
  • Confirm out-of-order behavior, logging level, and retained debug-only sleep/assert behavior in evaluator.hpp.

Possibly related PRs

  • Allow other types in partition by #159 — Modifies partition_by_query.hpp partition-key handling and evaluator lookup/creation; directly related to partition-key construction and evaluator mapping in this change.

Poem

🐰 I hopped through keys and hashes bright,

weak paws held evaluators tight,
No debug-guard now to make me stop —
partitions bloom and events all hop,
I tidy caches, then onward flight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Feat/partition by evaluator add only if triggered" relates to partition-aware evaluator management changes, matching the core PR functionality of implementing per-partition evaluators.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/core_server/internal/evaluation/evaluator.hpp (1)

71-74: last_tuple_time used in non-debug code but only declared under CORE_DEBUG

is_time_window_empty (Lines 111-118) and next use last_tuple_time, but the member is declared only inside #ifdef CORE_DEBUG (Lines 71-74). In non-CORE_DEBUG builds this will not compile, and DynamicEvaluator::cleanup_out_of_time_window_evaluators relies on is_time_window_empty in all builds.

Given last_tuple_time now participates in functional logic (time-window cleanup), it should exist unconditionally, not just in debug.

Suggested fix (make last_tuple_time always available and update the comment):

-  // Only in debug, check tuples are being sent in ascending order.
-#ifdef CORE_DEBUG
-  uint64_t last_tuple_time = 0;
-#endif
+  // Tracks timestamp of the last processed tuple; used for ordering checks and
+  // time-window based cleanup.
+  uint64_t last_tuple_time = 0;

Also applies to: 107-118

src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (1)

32-45: EvaluatorArgs::limit is never initialized but used when creating evaluators

In EvaluatorArgs (Lines 32-45), the constructor takes a CEQL::Limit limit parameter but does not initialize the limit member. Later, create_evaluator (Lines 202-213) passes evaluator_args.limit into the Evaluation::Evaluator constructor.

This means per-partition evaluators will see a default-constructed CEQL::Limit instead of the query’s configured limit, changing result limiting behavior.

Suggested fix (initialize limit):

    EvaluatorArgs(Evaluation::PredicateEvaluator&& tuple_evaluator,
                  std::atomic<uint64_t>& event_time_of_expiration,
                  CEQL::ConsumeBy::ConsumptionPolicy consumption_policy,
                  CEQL::Limit limit)
-        : tuple_evaluator(std::move(tuple_evaluator)),
-          event_time_of_expiration(event_time_of_expiration),
-          consumption_policy(consumption_policy) {}
+        : tuple_evaluator(std::move(tuple_evaluator)),
+          event_time_of_expiration(event_time_of_expiration),
+          consumption_policy(consumption_policy),
+          limit(limit) {}

(Optional) If PredicateEvaluator is expensive to copy and is effectively stateless, you may also consider holding it by std::shared_ptr<const Evaluation::PredicateEvaluator> and passing shared ownership to each Evaluator to avoid repeated copies.

Also applies to: 202-213

🧹 Nitpick comments (2)
src/core_server/internal/evaluation/evaluator.hpp (1)

136-147: Out-of-order handling now active in release with 500ms sleep

The out-of-order check in next (Lines 136-147) is now compiled in all builds and still includes:

  • A blocking std::this_thread::sleep_for(std::chrono::nanoseconds(500000000)); (500ms).
  • An assert(false && ...) that disappears in release builds, leaving only log + sleep before proceeding.

If misordered events occur in production, this will stall the query thread for 500ms per event, which is a significant throughput and latency risk.

Consider keeping strong diagnostics but avoiding long sleeps in release:

-    if (current_time < last_tuple_time) [[unlikely]] {
+    if (current_time < last_tuple_time) [[unlikely]] {
       std::string attributes = event.get_event_reference().to_string();
       LOG_CRITICAL(logger,
                    "Received tuple with timestamp {} in Evaluator::next, "
                    "but the last tuple time was {}. Attributes: {}",
                    current_time,
                    last_tuple_time,
                    attributes);
-      std::this_thread::sleep_for(std::chrono::nanoseconds(500000000));
-      assert(false && "Received tuple out of order in Evaluator::next");
+  #ifdef CORE_DEBUG
+      std::this_thread::sleep_for(std::chrono::nanoseconds(500000000));
+      assert(false && "Received tuple out of order in Evaluator::next");
+  #endif
     }

This preserves strict behavior in debug while avoiding long sleeps in optimized builds.

src/core_server/internal/interface/modules/query/query_types/partition_by_query.hpp (1)

48-78: Partition key hashing and evaluator index mapping look correct; consider long‑term map growth

The TupleValuesKey + ValueVectorHash implementation (Lines 48-78) and the mapping in
find_or_create_evaluator_index_from_tuple_indexes (Lines 199-229) correctly:

  • Compare full attribute value vectors (using Types::Value::operator==).
  • Hash both type and textual representation to distinguish different value types and contents.
  • Produce dense evaluator indices (0, 1, 2, …) used by DynamicEvaluator.

The overall logic and caching are sound.

One trade‑off is that partition_by_attrs_to_evaluator_idx (Lines 88-89) only ever grows: even when a partition’s evaluator is later garbage‑collected by DynamicEvaluator, the key → index entry remains. For workloads with extremely high partition cardinality and churn, this map could become a noticeable memory sink.

If that becomes an issue in practice, you might later:

  • Track last‑access time per partition index and periodically evict “cold” keys whose evaluators are gone and haven’t been referenced for a long time, or
  • Store a more compact representation of the partition key (e.g., pre-hashed string or lightweight value variant) instead of full Value clones.

For now, the current design is reasonable and consistent with the rest of the pipeline.

Also applies to: 88-92, 199-229

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 479d0fe and c8adc93.

📒 Files selected for processing (3)
  • src/core_server/internal/evaluation/evaluator.hpp (2 hunks)
  • src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (3 hunks)
  • src/core_server/internal/interface/modules/query/query_types/partition_by_query.hpp (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/core_server/internal/interface/modules/query/query_types/partition_by_query.hpp (4)
src/shared/datatypes/value.hpp (1)
  • val (145-145)
src/core_server/internal/interface/modules/query/query_types/simple_query.hpp (3)
  • query (43-70)
  • query (43-43)
  • event (72-74)
src/core_server/internal/evaluation/predicate_evaluator.hpp (1)
  • PredicateEvaluator (21-27)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (11)
  • it (128-139)
  • event (71-102)
  • event (72-72)
  • evaluator_idx (157-164)
  • evaluator_idx (157-157)
  • evaluator_idx (169-180)
  • evaluator_idx (169-169)
  • evaluator_idx (185-197)
  • evaluator_idx (185-185)
  • evaluator_idx (202-213)
  • evaluator_idx (202-202)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (2)
src/core_server/internal/interface/modules/query/query_types/partition_by_query.hpp (6)
  • event (144-164)
  • event (144-144)
  • event (169-197)
  • event (169-169)
  • event (202-229)
  • event (203-204)
src/core_server/internal/evaluation/evaluator.hpp (7)
  • event (120-225)
  • event (121-121)
  • event (257-316)
  • event (257-261)
  • ZoneScopedN (319-346)
  • current_time (111-118)
  • current_time (111-111)
src/core_server/internal/evaluation/evaluator.hpp (1)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (4)
  • current_time (104-123)
  • current_time (104-104)
  • current_time (144-155)
  • current_time (144-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (16)
  • GitHub Check: upload-conan (x86_64-linux-gcc-libstdc)
  • GitHub Check: upload-conan (x86_64-linux-clang-libstdc)
  • GitHub Check: run-benchmarks-release
  • GitHub Check: run-docker-build-check
  • GitHub Check: run-tests-release (x86_64-linux-gcc-libstdc)
  • GitHub Check: run-thread-sanitizer-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-address-sanitizer-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-basic-tests-debug (x86_64-linux-gcc-libstdc)
  • GitHub Check: run-basic-tests-debug (x86_64-linux-clang-libstdc)
  • GitHub Check: run-sanitizer-tests-debug (x86_64-linux-clang-libstdc)
  • GitHub Check: check-clang-tidy
  • GitHub Check: check-ceql-query-grammar-up-to-date
  • GitHub Check: check-code-format
  • GitHub Check: run-valgrind-debug (x86_64-linux-gcc-libstdc)
  • GitHub Check: run-valgrind-debug (x86_64-linux-clang-libstdc)

@nicobuzeta
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nicobuzeta
Copy link
Collaborator Author

@coderabbitai ignore

@nicobuzeta
Copy link
Collaborator Author

https://github.com/coderabbitai review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (1)

32-45: EvaluatorArgs::limit parameter is ignored and left at its default value

In EvaluatorArgs, the constructor takes a CEQL::Limit limit parameter but does not initialize the corresponding member:

struct EvaluatorArgs {
  ...
  CEQL::Limit limit;

  EvaluatorArgs(..., CEQL::Limit limit)
      : tuple_evaluator(std::move(tuple_evaluator)),
        event_time_of_expiration(event_time_of_expiration),
        consumption_policy(consumption_policy) {}
};

As written, evaluator_args.limit is default-constructed and the passed-in limit is never used, so all created Evaluation::Evaluator instances effectively ignore the configured limit.

Recommend wiring this up:

   EvaluatorArgs(Evaluation::PredicateEvaluator&& tuple_evaluator,
                 std::atomic<uint64_t>& event_time_of_expiration,
                 CEQL::ConsumeBy::ConsumptionPolicy consumption_policy,
                 CEQL::Limit limit)
       : tuple_evaluator(std::move(tuple_evaluator)),
         event_time_of_expiration(event_time_of_expiration),
-        consumption_policy(consumption_policy) {}
+        consumption_policy(consumption_policy),
+        limit(std::move(limit)) {}

(or copy limit if move semantics are not desired).

♻️ Duplicate comments (1)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (1)

119-141: Evaluator cleanup invariant and max_evaluators growth logic still fragile

The logic here still has the issue from the prior review:

  • You compute current_size, optionally run cleanup, then recompute current_size.
  • If current_size > max_evaluators, you log and double max_evaluators once.
  • You then assert evaluators_storage.size() <= max_evaluators.

If cleanup removes little or nothing, you can end up with evaluators_storage.size() still far above the (once-doubled) max_evaluators, tripping the assert as before (e.g., 5000 evaluators vs. max_evaluators grown from 1000 → 2000).

Consider the tighter version previously suggested:

  void cleanup_evaluators_if_necessary(uint64_t current_time) {
    ZoneScopedN("Interface::DynamicEvaluator::cleanup_evaluators_if_necessary");
    std::size_t current_size = evaluators_storage.size();
    if (current_size > max_evaluators) {
      cleanup_empty_evaluators();
      cleanup_out_of_time_window_evaluators(current_time);
      current_size = evaluators_storage.size();
    }

-    if (current_size > max_evaluators) {
+    while (current_size > max_evaluators) {
      LOG_WARNING(this->logger,
                  "Number of evaluators ({}) exceeded maximum allowed ({}). "
                  "Doubling current size to {}",
                  current_size,
                  max_evaluators,
                  max_evaluators * 2);
      max_evaluators *= 2;
+     // optionally recompute current_size here if you expect concurrent changes
     }
-    assert(evaluators_storage.size() <= max_evaluators
+    assert(current_size <= max_evaluators
            && "Number of evaluators should be within the maximum allowed after cleanup");
  }

This guarantees the invariant in debug builds even when cleanup cannot shrink far enough.

🧹 Nitpick comments (4)
src/core_server/internal/evaluation/evaluator.hpp (1)

71-72: Clarify and enforce caller contract for time-window and emptiness checks

is_empty() and is_time_window_empty() look reasonable, but they implicitly assume:

  • current_time is monotonic per evaluator (assert(current_time >= last_tuple_time)).
  • An evaluator that has ever processed an event will likely never report is_empty() == true, because historic_union_list_map is never cleared in reset().

If this is intentional (i.e., “empty” really only means “never used” and cleanup is expected to rely mainly on is_time_window_empty), consider documenting these expectations in comments, or relaxing the assert in is_time_window_empty to fall back to “not empty” instead of aborting when the precondition is violated. Otherwise, any upstream bug that feeds an older timestamp into cleanup will assert before the out-of-order path in next() can run.

Also applies to: 105-116

src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (3)

47-69: Logger initialization pattern is fine, but the null-check is redundant

logger is a member initialized to nullptr and then immediately checked in the constructor before being set from quill::Frontend::get_logger("root"). Since nothing else can assign logger before this constructor body runs, the if (logger == nullptr) is effectively always true.

This is harmless, but you could simplify to a direct assignment in the constructor body or member initializer if you want to reduce noise.


146-157: Cleanup helpers assume single-threaded access and rely on strong invariants

Both cleanup_empty_evaluators and cleanup_out_of_time_window_evaluators:

  • Assert it->get() != nullptr.
  • Assert it->use_count() == 1 (no other shared owners).
  • Rely on Evaluator::is_empty() and Evaluator::is_time_window_empty(current_time).

This is fine if DynamicEvaluator is strictly single-threaded and the only extra shared_ptr copies are the short-lived ones in process_event. If there is any possibility of:

  • Concurrent calls to process_event/cleanup on the same instance, or
  • External code holding shared_ptrs to evaluators,

these asserts will become fragile and may start firing in debug. If multi-threaded use is in scope, you’ll need either synchronization around evaluators_storage or to relax the use_count() assertions.

Also, note that cleanup_out_of_time_window_evaluators will hit the assert(current_time >= last_tuple_time) inside Evaluator::is_time_window_empty if a non-monotonic timestamp is ever passed in for that evaluator, so the same monotonicity contract applies here.

Also applies to: 162-173


178-201: Evaluator creation and ownership wiring are sound; minor style tweaks possible

The creation helpers:

  • Distinguish between expired existing indices and genuinely new indices (create_evaluator_expired_or_new).
  • Use create_evaluator(evaluator_idx) to build a std::unique_ptr<Evaluation::Evaluator>.
  • Move that into evaluators_storage (which holds std::shared_ptr<...>), relying on the converting constructor from unique_ptr to shared_ptr.
  • Update evaluator_idx_to_evaluator with a weak_ptr view of the list’s shared_ptr.

Ownership and mapping invariants look correct, and the assertions on index bounds help. Two minor suggestions:

  • You could use evaluators_storage.emplace_back(std::move(evaluator)); to make the unique→shared conversion more explicit.
  • Consider adding a brief comment explaining that evaluator_idx_to_evaluator can hold expired weak_ptrs and will be refreshed lazily on demand, just to clarify why there’s no explicit cleanup of the index vector.

Functionally, this part looks solid.

Also applies to: 206-218, 223-234

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8adc93 and 27cf68e.

📒 Files selected for processing (7)
  • .github/workflows/benchmark.yaml (0 hunks)
  • .github/workflows/checks.yaml (0 hunks)
  • .github/workflows/test-checks.yaml (0 hunks)
  • .github/workflows/upload-conan.yaml (0 hunks)
  • .github/workflows/valgrind-checks.yaml (0 hunks)
  • src/core_server/internal/evaluation/evaluator.hpp (3 hunks)
  • src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (3 hunks)
💤 Files with no reviewable changes (5)
  • .github/workflows/valgrind-checks.yaml
  • .github/workflows/checks.yaml
  • .github/workflows/test-checks.yaml
  • .github/workflows/upload-conan.yaml
  • .github/workflows/benchmark.yaml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: run-valgrind-debug (x86_64-linux-gcc-libstdc)
  • GitHub Check: run-valgrind-debug (x86_64-linux-clang-libstdc)
  • GitHub Check: upload-conan (x86_64-linux-gcc-libstdc)
  • GitHub Check: upload-conan (x86_64-linux-clang-libstdc)
  • GitHub Check: check-ceql-query-grammar-up-to-date
  • GitHub Check: check-clang-tidy
  • GitHub Check: run-sanitizer-tests-debug (x86_64-linux-clang-libstdc)
  • GitHub Check: run-address-sanitizer-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-tests-release (x86_64-linux-gcc-libstdc)
  • GitHub Check: run-basic-tests-debug (x86_64-linux-clang-libstdc)
  • GitHub Check: run-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-thread-sanitizer-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-basic-tests-debug (x86_64-linux-gcc-libstdc)
  • GitHub Check: run-benchmarks-release
  • GitHub Check: run-docker-build-check
🔇 Additional comments (2)
src/core_server/internal/evaluation/evaluator.hpp (1)

134-148: Out-of-order handling now unconditionally drops events in release builds

The new out-of-order check (if (current_time < last_tuple_time) [[unlikely]]) with a CRITICAL log and an empty return makes behavior in non-debug builds explicit: out-of-order tuples are always ignored, and only in CORE_DEBUG do you sleep and assert.

If this is the intended contract (best-effort processing with strict ordering requirement), this change looks good and is much clearer than relying on debug-only checks. Just make sure callers and tests are updated to expect that out-of-order events are silently dropped (aside from logging) in release builds.

src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (1)

83-114: Process flow and ANY-consumption reset look consistent with new storage model

process_event:

  • Runs cleanup_evaluators_if_necessary(time) first.
  • Lazily creates or recreates the evaluator at evaluator_idx via the weak_ptr mapping.
  • Locks a shared_ptr, asserts non-null, calls evaluator->next(...).
  • Under ANY policy, sets should_reset on all evaluators in evaluators_storage when a match is produced.

This sequencing is coherent with the new lifetime model and matches the intended “per-index evaluator” semantics. Just ensure that:

  • event_time(event) is monotonic per evaluator index.
  • DynamicEvaluator is not used concurrently from multiple threads, since none of the containers are synchronized and later cleanup relies on use_count() == 1.

Otherwise this looks good.

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

✅ Actions performed

Reviews paused.

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

Stocks:

query time
other-q1_any.txt 1.40666666666666666666
other-q1_none.txt 2.85666666666666666666
other-q2_any.txt .76000000000000000000
other-q2_none.txt .96000000000000000000
other-q3_any.txt .79333333333333333333
other-q3_none.txt .79666666666666666666
other-q4_any.txt 1.45333333333333333333
other-q4_none.txt 28.13666666666666666666
other-q5_any.txt .80666666666666666666
other-q5_none.txt 3.36333333333333333333
other-q6_any.txt .81000000000000000000
other-q6_none.txt .82666666666666666666
other-q6_partition.txt .79666666666666666666
q1_any.txt .75333333333333333333
q1_none.txt 1.67000000000000000000
q2_any.txt 3.02666666666666666666
q2_none.txt 10.24666666666666666666
q3_any.txt 1.66666666666666666666
q3_none.txt 19.29666666666666666666
q4_any.txt .86666666666666666666
q4_none.txt .88000000000000000000
q5_any.txt 1.02000000000000000000
q5_none.txt 1.00000000000000000000
q6_any.txt .78333333333333333333
q6_none.txt .78333333333333333333
q7_any.txt .81000000000000000000
q7_none.txt .83333333333333333333
q8_any.txt .84666666666666666666
q8_none.txt .85666666666666666666
q9_any.txt .90666666666666666666
q9_none.txt .90333333333333333333
q10_any.txt 1.01000000000000000000
q10_none.txt 1.02333333333333333333
q11_any.txt .78000000000000000000
q11_none.txt .77000000000000000000
q12_any.txt .77333333333333333333
q12_none.txt .75666666666666666666

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

Unordered Stocks:

query time
other-q1_any.txt 1.34000000000000000000
other-q1_none.txt 3.06333333333333333333
other-q2_any.txt .96000000000000000000
other-q2_none.txt 1.17666666666666666666
other-q3_any.txt .97666666666666666666
other-q3_none.txt .97666666666666666666
other-q4_any.txt 1.66000000000000000000
other-q4_none.txt 27.75000000000000000000
other-q5_any.txt .97333333333333333333
other-q5_none.txt 3.67666666666666666666
other-q6_any.txt 1.00333333333333333333
other-q6_none.txt 1.02666666666666666666
other-q6_partition.txt 1.00333333333333333333
q1_any.txt .93333333333333333333
q1_none.txt 1.83333333333333333333
q2_any.txt 3.22000000000000000000
q2_none.txt 10.37666666666666666666
q3_any.txt 1.85333333333333333333
q3_none.txt 19.37666666666666666666
q4_any.txt 1.06333333333333333333
q4_none.txt 1.06666666666666666666
q5_any.txt 1.20333333333333333333
q5_none.txt 1.22000000000000000000
q6_any.txt .97000000000000000000
q6_none.txt .97333333333333333333
q7_any.txt 1.00333333333333333333
q7_none.txt .98666666666666666666
q8_any.txt 1.03000000000000000000
q8_none.txt 1.04666666666666666666
q9_any.txt 1.07666666666666666666
q9_none.txt 1.07333333333333333333
q10_any.txt 1.19333333333333333333
q10_none.txt 1.20000000000000000000
q11_any.txt .96333333333333333333
q11_none.txt .96333333333333333333
q12_any.txt .97333333333333333333
q12_none.txt .96333333333333333333

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

Smart Homes:

query time
q1_any.txt 3.70000000000000000000
q1_none.txt 3.88000000000000000000
q2_any.txt 3.77666666666666666666
q2_none.txt 44.71333333333333333333
q3_any.txt 7.63666666666666666666
q3_none.txt 134.96000000000000000000
q4_any.txt 13.85666666666666666666
q4_none.txt 113.15666666666666666666
q5_any.txt 13.67000000000000000000
q5_none.txt 45.68333333333333333333
q6_any.txt 3.01666666666666666666
q6_none.txt 3.00666666666666666666
q7_any.txt 3.42000000000000000000
q7_none.txt 3.34666666666666666666
q8_any.txt 3.85666666666666666666
q8_none.txt 3.80333333333333333333
q9_any.txt 4.38000000000000000000
q9_none.txt 4.29000000000000000000
q10_any.txt 6.04666666666666666666
q10_none.txt 5.92000000000000000000
q11_any.txt 2.95000000000000000000
q11_none.txt 2.90333333333333333333
q12_any.txt 2.96000000000000000000
q12_none.txt 2.99000000000000000000

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

Taxis:

query time
q1_any.txt 4.26666666666666666666
q1_none.txt 4.49666666666666666666
q2_any.txt 4.81333333333333333333
q2_none.txt 5.49666666666666666666
q3_any.txt 4.99333333333333333333
q3_none.txt 8.76000000000000000000
q4_any.txt 5.20000000000000000000
q4_none.txt 9.25000000000000000000
q5_any.txt 5.70666666666666666666
q5_none.txt 6.01666666666666666666
q6_any.txt 4.52666666666666666666
q6_none.txt 4.44333333333333333333
q7_any.txt 4.80333333333333333333
q7_none.txt 4.80666666666666666666
q8_any.txt 4.92666666666666666666
q8_none.txt 4.90333333333333333333
q9_any.txt 5.00000000000000000000
q9_none.txt 5.09666666666666666666
q10_any.txt 5.74666666666666666666
q10_none.txt 5.64666666666666666666
q11_any.txt 4.43333333333333333333
q11_none.txt 4.53333333333333333333
q12_any.txt 4.49333333333333333333
q12_none.txt 4.43000000000000000000

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

Ordered Bluesky:

query time
q1.txt .22333333333333333333

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

Unordered Bluesky:

query time
q1.txt 58.51000000000000000000
q2.txt 58.51000000000000000000
q3.txt 58.51000000000000000000
q4.txt 58.51000000000000000000

@nicobuzeta nicobuzeta force-pushed the feat/partition-by-evaluator-add-only-if-triggered branch 3 times, most recently from 667aeb1 to 22acee4 Compare November 26, 2025 04:20
@nicobuzeta nicobuzeta force-pushed the feat/partition-by-evaluator-add-only-if-triggered branch from 22acee4 to 9b8903b Compare November 26, 2025 04:41
@nicobuzeta
Copy link
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Nov 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (1)

186-203: clean_or_expand_evaluator_storage can leave evaluators_storage.size() > max_evaluators after partial cleanup

The LRU traversal and erase logic look correct, but there’s a logic hole around max_evaluators enforcement:

size_t initial_size = evaluators_storage.size();
// ...
// potentially erase some evaluators
// ...
if (initial_size != evaluators_storage.size()) {
  // log cleanup
} else {
  // no cleanup; double max_evaluators
  max_evaluators *= 2;
}
assert(evaluators_storage.size() <= max_evaluators);

If you start above the limit and remove some evaluators but not enough, you get:

  • initial_size != evaluators_storage.size() → skip doubling,
  • evaluators_storage.size() may still be > max_evaluators,
  • the final assert can fail.

Example: max_evaluators = 1000, initial_size = 1500, cleanup removes 100 → evaluators_storage.size() = 1400 (> 1000), but initial_size != current_size, so max_evaluators stays 1000 and the assert trips.

This is essentially the same issue that existed in the earlier cleanup_evaluators_if_necessary variant noted in the past review; the shape has changed but the underlying invariant problem remains.

One way to fix it while keeping your new function:

void clean_or_expand_evaluator_storage(uint64_t current_time) {
  ZoneScopedN("Interface::DynamicEvaluator::clean_or_expand_evaluator_storage");
  const auto before_cleanup = evaluators_storage.size();

  // Try to clean from LRU end
  for (auto it = evaluators_storage.rbegin(); it != evaluators_storage.rend();) {
    assert(it->evaluator != nullptr && "Evaluator pointer should not be null");
    assert(it->evaluator.use_count() == 1 && "Shared pointer use count should be 1.");

    if (it->evaluator->is_time_window_empty(current_time)) {
      value_to_evaluator_list_pos.erase(it->evaluator_idx);
      auto to_erase = std::next(it).base();
      it = std::make_reverse_iterator(evaluators_storage.erase(to_erase));
    } else {
      ++it;
    }
  }

  auto current_size = evaluators_storage.size();
  if (before_cleanup != current_size) {
    LOG_TRACE_L1(this->logger,
                 "Cleaned up {} evaluators that were out of time window.",
                 before_cleanup - current_size);
  }

  while (current_size > max_evaluators) {
    LOG_WARNING(this->logger,
                "Number of evaluators ({}) exceeded maximum allowed ({}). "
                "Doubling current size to {}",
                current_size,
                max_evaluators,
                max_evaluators * 2);
    max_evaluators *= 2;
  }

  assert(current_size <= max_evaluators
         && "Number of evaluators should be within the maximum allowed after cleanup");
}

This preserves your logging but ensures the invariant size <= max_evaluators is always restored, even when cleanup only partially succeeds.

Also applies to: 207-243

🧹 Nitpick comments (5)
src/core_server/internal/evaluation/evaluator.hpp (2)

111-118: Simplify is_time_window_empty and validate the monotonic-time assumption

The logic is correct and guards against underflow, but you can make it more compact and slightly clearer:

bool is_time_window_empty(uint64_t current_time) const {
  assert(current_time >= last_tuple_time);
  const uint64_t expiration_time =
    current_time < time_window ? 0 : current_time - time_window;
  return last_tuple_time < expiration_time;
}

Also, assert(current_time >= last_tuple_time) assumes the caller always passes a globally non-decreasing current_time. That matches how DynamicEvaluator::clean_or_expand_evaluator_storage currently uses it, but it’s worth keeping in mind that if upstream ever relaxes global ordering this assert will start to trip in debug builds.


136-150: Out-of-order handling in next() is now production-visible; behavior looks intentional

The unconditional check

if (current_time < last_tuple_time) [[unlikely]] { ... return {}; }

with a CRITICAL log and early return, plus keeping last_tuple_time unchanged, is a sensible way to reject late events without corrupting internal state. The debug-only sleep/assert under CORE_DEBUG also keeps the production path lean.

If you need more observability later, consider adding a dedicated metric/counter for dropped out-of-order events, but the current behavior is coherent as-is.

src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (3)

32-58: Make EvaluatorStorageWrapper constructors explicit and simplify initialization

The wrapper design and get_evaluator() logic look fine, but you can tighten the API and address the static analysis feedback:

  • Mark the single-argument constructors as explicit to avoid unintended implicit conversions.
  • Drop the redundant evaluator() initializer in the iterator constructor; the member will be default-initialized anyway.

For example:

struct EvaluatorStorageWrapper {
  std::optional<EvaluatorIndexWrapper> evaluator;
  std::optional<std::list<EvaluatorIndexWrapper>::iterator> list_position;

  explicit EvaluatorStorageWrapper(EvaluatorIndexWrapper&& evaluator)
      : evaluator(std::move(evaluator)), list_position(std::nullopt) {}

  explicit EvaluatorStorageWrapper(std::list<EvaluatorIndexWrapper>::iterator list_it)
      : list_position(list_it) {}

  EvaluatorIndexWrapper& get_evaluator() {
    if (evaluator.has_value()) {
      return *evaluator;
    }
    assert(list_position.has_value()
           && "Either evaluator or list position must be set");
    return *list_position.value();
  }
};

96-110: process_event: LRU usage is correct; fix return std::move and consider aligning comment with behavior

The overall flow looks good:

  • You obtain or create the evaluator, call next, then only insert new evaluators into the LRU via save_evaluator, which enforces max_evaluators.
  • For ANY consumption policy you correctly propagate should_reset = true to all stored evaluators after a successful match.

Two small points:

  1. The comment
// Only if not empty and new, we save it

doesn’t match the code: you only check “new” (evaluator_wrapper.evaluator.has_value()), not “not empty” (no is_empty() / enumerator.has_value() check). Either update the comment or add the missing condition, depending on your intent.

  1. return std::move(enumerator); at the end prevents copy elision and triggers the Sonar warning. Prefer just:
return enumerator;

which will move under the usual return semantics without forcing it.

Also applies to: 124-152


157-180: Remove unused current_time parameter from get_or_create_evaluator (or use it)

get_or_create_evaluator takes uint64_t current_time but doesn’t use it anywhere. With common warning flags this will show up as an unused-parameter warning.

If there’s no near-term plan to use current_time here (e.g., for eager cleanup decisions), simplify the API:

EvaluatorStorageWrapper
get_or_create_evaluator(size_t evaluator_idx) {
  ZoneScopedN("Interface::DynamicEvaluator::get_or_create_evaluator");
  auto it = value_to_evaluator_list_pos.find(evaluator_idx);
  if (it != value_to_evaluator_list_pos.end()) {
    auto list_it = it->second;
    evaluators_storage.splice(evaluators_storage.begin(), evaluators_storage, list_it);
    it->second = evaluators_storage.begin();
    return EvaluatorStorageWrapper(evaluators_storage.begin());
  }

  auto evaluator = std::make_shared<Evaluation::Evaluator>(
      this->cea,
      evaluator_args.tuple_evaluator,
      time_window.duration,
      evaluator_args.event_time_of_expiration,
      evaluator_args.consumption_policy,
      evaluator_args.limit);

  return EvaluatorStorageWrapper(EvaluatorIndexWrapper{evaluator_idx, std::move(evaluator)});
}

and adjust the call in process_event.

If you do intend to use current_time later, consider adding a [[maybe_unused]] attribute for now to keep builds clean.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27cf68e and 9b8903b.

📒 Files selected for processing (2)
  • src/core_server/internal/evaluation/evaluator.hpp (3 hunks)
  • src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/core_server/internal/evaluation/evaluator.hpp (1)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (4)
  • current_time (185-202)
  • current_time (186-186)
  • current_time (207-243)
  • current_time (207-207)
src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp (1)
src/core_server/internal/evaluation/evaluator.hpp (8)
  • assert (105-109)
  • event (120-228)
  • event (121-121)
  • event (260-319)
  • event (260-264)
  • ZoneScopedN (322-349)
  • current_time (111-118)
  • current_time (111-111)
🪛 GitHub Check: SonarCloud Code Analysis
src/core_server/internal/evaluation/evaluator.hpp

[warning] 114-114: Use the init-statement to declare "expiration_time" inside the if statement.

See more on https://sonarcloud.io/project/issues?id=CORE-cer_CORE&issues=AZqdEalJ30eZV0KTqvBe&open=AZqdEalJ30eZV0KTqvBe&pullRequest=165

src/core_server/internal/interface/modules/query/evaluators/dynamic_evaluator.hpp

[failure] 44-44: Add the "explicit" keyword to this constructor.

See more on https://sonarcloud.io/project/issues?id=CORE-cer_CORE&issues=AZq-V7iYE_u7do73tn_L&open=AZq-V7iYE_u7do73tn_L&pullRequest=165


[failure] 47-47: Add the "explicit" keyword to this constructor.

See more on https://sonarcloud.io/project/issues?id=CORE-cer_CORE&issues=AZq-V7iYE_u7do73tn_M&open=AZq-V7iYE_u7do73tn_M&pullRequest=165


[warning] 48-48: Remove this use of the constructor's initializer list for data member "evaluator". It is redundant with default initialization behavior.

See more on https://sonarcloud.io/project/issues?id=CORE-cer_CORE&issues=AZq-V7iYE_u7do73tn_K&open=AZq-V7iYE_u7do73tn_K&pullRequest=165


[warning] 151-151: moving a local object in a return statement prevents copy elision

See more on https://sonarcloud.io/project/issues?id=CORE-cer_CORE&issues=AZq-V7iYE_u7do73tn_J&open=AZq-V7iYE_u7do73tn_J&pullRequest=165

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: check-clang-tidy
  • GitHub Check: run-address-sanitizer-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-thread-sanitizer-tests-release (x86_64-linux-clang-libstdc)
  • GitHub Check: run-benchmarks-release
🔇 Additional comments (1)
src/core_server/internal/evaluation/evaluator.hpp (1)

105-109: is_empty() assertion and semantics look sound

The consistency assert between historic_ordered_keys and historic_union_list_map plus using historic_ordered_keys.empty() as the emptiness check is a reasonable invariant for this type. No issues from my side here.

@sonarqubecloud
Copy link

@nicobuzeta nicobuzeta merged commit c8acd83 into main Dec 31, 2025
19 checks passed
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.

2 participants