Selaa lähdekoodia

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

First read-flip step on Stage 4. The Exists gRPC handler now consults
both MemoryStore (authoritative) and doc_store_ when mirror_healthy_
is true, drift_count is zero, and the collection isn't system
(_-prefixed). Divergence is logged at WARN with collection/id/answers.

MemoryStore stays authoritative; the goal is to generate evidence
that the mirror returns identical answers for the existence predicate
across realistic traffic before the next sub-task hard-flips Exists
onto doc_store_. Cheap probe (LMDB exists() does a zero-copy mdb_get
size sniff) so per-call cost is bounded.

Accessors on DatabaseService: docStore(), mirrorHealthy(),
mirrorDriftCount() — these are the contract the read-flip path uses
to decide when LMDB is trustworthy.
fszontagh 2 kuukautta sitten
vanhempi
sitoutus
0bf7b1af19
2 muutettua tiedostoa jossa 37 lisäystä ja 0 poistoa
  1. 24 0
      service/src/database_grpc_impl.cpp
  2. 13 0
      service/src/database_service.hpp

+ 24 - 0
service/src/database_grpc_impl.cpp

@@ -1,5 +1,6 @@
 #include "database_grpc_impl.hpp"
 #include "database_service.hpp"
+#include "storage/document_store.hpp"
 #include "json_parse.hpp"
 #include "persistence/wal.hpp"
 #include "views/projection.hpp"
@@ -451,6 +452,29 @@ grpc::Status DatabaseGrpcImpl::Exists(
     pb::ExistsResponse* response
 ) {
     bool exists = store_.exists(request->collection(), request->id());
+
+    // v2.0 Stage 4 — shadow-read: when the mirror is healthy and has zero
+    // drift, also ask doc_store_ and log any divergence. MemoryStore is
+    // still authoritative; this generates the evidence we need before the
+    // next sub-task hard-flips reads onto doc_store_. System collections
+    // are skipped because they aren't mirrored (Stage 5 owns their format).
+    if (!request->collection().empty() && request->collection()[0] != '_'
+        && service_.mirrorHealthy() && service_.mirrorDriftCount() == 0) {
+        if (auto* ds = service_.docStore()) {
+            try {
+                const bool lmdb_exists = ds->exists(request->collection(), request->id());
+                if (lmdb_exists != exists) {
+                    spdlog::warn("v2.0 shadow-read Exists divergence coll={} id={} "
+                                 "memstore={} lmdb={}",
+                                 request->collection(), request->id(), exists, lmdb_exists);
+                }
+            } catch (const std::exception& e) {
+                spdlog::warn("v2.0 shadow-read Exists query failed coll={} id={}: {}",
+                             request->collection(), request->id(), e.what());
+            }
+        }
+    }
+
     response->set_exists(exists);
     return grpc::Status::OK;
 }

+ 13 - 0
service/src/database_service.hpp

@@ -194,6 +194,19 @@ public:
      */
     static Config parseConfig(const nlohmann::json& json);
 
+    // v2.0 Stage 4 — accessors used by the gRPC layer's shadow-read /
+    // read-flip paths. `docStore()` may return nullptr when env open
+    // failed at boot (Stage 4 tolerates that). `mirrorHealthy()` plus
+    // `mirrorDriftCount()==0` together gate any handler that wants to
+    // trust doc_store_ as a read source.
+    smartbotic::db::storage::DocumentStore* docStore() noexcept { return doc_store_.get(); }
+    bool mirrorHealthy() const noexcept {
+        return mirror_healthy_.load(std::memory_order_acquire);
+    }
+    uint64_t mirrorDriftCount() const noexcept {
+        return mirror_drift_count_.load(std::memory_order_relaxed);
+    }
+
 private:
     void setupComponents();
     void startGrpcServer();