Przeglądaj źródła

feat(storage): shadow-read on Find handler against doc_store_

Third shadow-read in the v2.0 read-flip evidence loop. Same gates as
Exists/Get plus one extra: skip when result.usedWalFallback is true.
WAL fallback is a v1.x-only feature (returns docs that aren't in the
hot set by replaying the WAL); the LMDB substrate has no equivalent
and comparing would produce spurious divergence.

Probe compares shape (total_matched, has_more, doc count) first, then
walks per-doc id+version pairs capped at the first 32 documents. One
log line per divergent query is enough signal — repeating for every
doc in a 10k-result mismatch would drown the log.

Filter+sort+projection parity is guaranteed by 4eeb784's filter_eval
lift, so any Find divergence is now substrate-attributable.
fszontagh 2 miesięcy temu
rodzic
commit
7cb6ae9fb0
1 zmienionych plików z 44 dodań i 0 usunięć
  1. 44 0
      service/src/database_grpc_impl.cpp

+ 44 - 0
service/src/database_grpc_impl.cpp

@@ -738,6 +738,50 @@ grpc::Status DatabaseGrpcImpl::Find(
 
     QueryResult result = store_.find(targetCollection, query);
 
+    // v2.0 Stage 4 — shadow-read on Find. Same gates as Exists/Get. We
+    // also skip when the result came from the WAL-fallback path (v1.x
+    // only; LMDB substrate has no equivalent), because that signals
+    // MemoryStore returned data from outside its hot set — comparing
+    // would produce spurious divergence. Probe compares total_matched,
+    // has_more, document count, and per-doc id+version (capped at first
+    // 32 docs to keep shadow cost bounded for large pages).
+    if (!targetCollection.empty() && targetCollection[0] != '_'
+        && !result.usedWalFallback
+        && service_.mirrorHealthy() && service_.mirrorDriftCount() == 0) {
+        if (auto* ds = service_.docStore()) {
+            try {
+                auto lmdb_scan = ds->scan(targetCollection, query);
+                if (lmdb_scan.total_matched != result.totalCount
+                    || lmdb_scan.has_more != result.hasMore
+                    || lmdb_scan.documents.size() != result.documents.size()) {
+                    spdlog::warn("v2.0 shadow-read Find shape divergence coll={} "
+                                 "mem_total={} lmdb_total={} mem_count={} lmdb_count={} "
+                                 "mem_more={} lmdb_more={}",
+                                 targetCollection,
+                                 result.totalCount, lmdb_scan.total_matched,
+                                 result.documents.size(), lmdb_scan.documents.size(),
+                                 result.hasMore, lmdb_scan.has_more);
+                } else {
+                    const size_t cap = std::min<size_t>(result.documents.size(), 32);
+                    for (size_t i = 0; i < cap; ++i) {
+                        const auto& m = result.documents[i];
+                        const auto& l = lmdb_scan.documents[i];
+                        if (m.id != l.id || m.version != l.version) {
+                            spdlog::warn("v2.0 shadow-read Find doc divergence coll={} pos={} "
+                                         "mem_id={} mem_ver={} lmdb_id={} lmdb_ver={}",
+                                         targetCollection, i,
+                                         m.id, m.version, l.id, l.version);
+                            break;  // one log per query is enough signal
+                        }
+                    }
+                }
+            } catch (const std::exception& e) {
+                spdlog::warn("v2.0 shadow-read Find query failed coll={}: {}",
+                             targetCollection, e.what());
+            }
+        }
+    }
+
     response->set_total_count(result.totalCount);
     response->set_has_more(result.hasMore);