Explorar o código

Add WAL fallback for queries and performance metrics

When LRU eviction has moved documents to WAL, queries now:
1. Search in-memory documents first (fast path)
2. If collection has evicted docs, also search WAL (complete results)

New QueryResult metrics:
- usedWalFallback: true if WAL was scanned
- memoryMatchCount/walMatchCount: docs found in each source
- memorySearchMicros/walSearchMicros: timing breakdown

New client method findWithMetrics() returns full metrics.
peekEvictedDocument() loads from WAL without re-inserting to memory.
Fszontagh hai 5 meses
pai
achega
4c9d93e52b

+ 22 - 0
client/include/smartbotic/database/client.hpp

@@ -145,6 +145,22 @@ public:
         uint32_t offset = 0;
     };
 
+    /**
+     * Result of a find query with performance metrics.
+     */
+    struct FindResult {
+        std::vector<nlohmann::json> documents;
+        uint64_t totalCount = 0;
+        bool hasMore = false;
+
+        // WAL fallback metrics
+        bool usedWalFallback = false;       // True if WAL was scanned for evicted docs
+        uint32_t memoryMatchCount = 0;      // Documents matched from memory
+        uint32_t walMatchCount = 0;         // Documents matched from WAL
+        uint64_t memorySearchMicros = 0;    // Time spent searching memory (µs)
+        uint64_t walSearchMicros = 0;       // Time spent loading/searching WAL (µs)
+    };
+
     /**
      * Find documents matching a query.
      */
@@ -156,6 +172,12 @@ public:
      */
     [[nodiscard]] std::vector<nlohmann::json> find(const std::string& collection);
 
+    /**
+     * Find documents with full metrics (including WAL fallback info).
+     */
+    [[nodiscard]] FindResult findWithMetrics(const std::string& collection,
+                                              const QueryOptions& options);
+
     /**
      * Count documents matching filters.
      */

+ 67 - 0
client/src/client.cpp

@@ -296,6 +296,68 @@ public:
         return results;
     }
 
+    Client::FindResult findWithMetrics(const std::string& collection,
+                                        const Client::QueryOptions& options) {
+        smartbotic::databasepb::FindRequest request;
+        request.set_collection(collection);
+
+        // Set filters
+        for (const auto& [field, value] : options.filters) {
+            auto* filter = request.add_filters();
+            filter->set_field(field);
+            filter->set_value(value.dump());
+            if (field == "_search") {
+                filter->set_op(smartbotic::databasepb::FILTER_OP_SEARCH);
+            } else {
+                filter->set_op(smartbotic::databasepb::FILTER_OP_EQ);
+            }
+        }
+
+        // Set sorting
+        if (!options.sortField.empty()) {
+            auto* sort = request.mutable_sort();
+            sort->set_field(options.sortField);
+            sort->set_descending(options.sortDescending);
+        }
+
+        // Set pagination
+        request.set_limit(options.limit);
+        request.set_offset(options.offset);
+
+        smartbotic::databasepb::FindResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        auto status = stub_->Find(&context, request, &response);
+        if (!status.ok()) {
+            spdlog::error("Client::findWithMetrics failed: {}", status.error_message());
+            return {};
+        }
+
+        Client::FindResult result;
+        result.totalCount = response.total_count();
+        result.hasMore = response.has_more();
+        result.usedWalFallback = response.used_wal_fallback();
+        result.memoryMatchCount = response.memory_match_count();
+        result.walMatchCount = response.wal_match_count();
+        result.memorySearchMicros = response.memory_search_micros();
+        result.walSearchMicros = response.wal_search_micros();
+
+        result.documents.reserve(response.documents_size());
+        for (const auto& doc : response.documents()) {
+            auto json = nlohmann::json::parse(doc.data());
+            json["_id"] = doc.id();
+            json["_version"] = doc.version();
+            json["_created_at"] = doc.created_at();
+            json["_updated_at"] = doc.updated_at();
+            json["_created_by"] = doc.created_by();
+            json["_updated_by"] = doc.updated_by();
+            result.documents.push_back(json);
+        }
+
+        return result;
+    }
+
     uint64_t count(const std::string& collection,
                    const std::vector<std::pair<std::string, nlohmann::json>>& filters) {
         smartbotic::databasepb::CountRequest request;
@@ -880,6 +942,11 @@ std::vector<nlohmann::json> Client::find(const std::string& collection) {
     return impl_->find(collection, QueryOptions{});
 }
 
+Client::FindResult Client::findWithMetrics(const std::string& collection,
+                                            const QueryOptions& options) {
+    return impl_->findWithMetrics(collection, options);
+}
+
 uint64_t Client::count(const std::string& collection,
                                const std::vector<std::pair<std::string, nlohmann::json>>& filters) {
     return impl_->count(collection, filters);

+ 7 - 0
proto/database.proto

@@ -314,6 +314,13 @@ message FindResponse {
     repeated Document documents = 1;
     uint64 total_count = 2;
     bool has_more = 3;
+
+    // WAL fallback metrics (for queries that include evicted documents)
+    bool used_wal_fallback = 4;         // True if WAL was scanned for evicted docs
+    uint32 memory_match_count = 5;      // Documents matched from memory
+    uint32 wal_match_count = 6;         // Documents matched from WAL
+    uint64 memory_search_micros = 7;    // Time spent searching memory (µs)
+    uint64 wal_search_micros = 8;       // Time spent loading/searching WAL (µs)
 }
 
 message CountRequest {

+ 7 - 0
service/src/database_grpc_impl.cpp

@@ -389,6 +389,13 @@ grpc::Status DatabaseGrpcImpl::Find(
     response->set_total_count(result.totalCount);
     response->set_has_more(result.hasMore);
 
+    // WAL fallback metrics
+    response->set_used_wal_fallback(result.usedWalFallback);
+    response->set_memory_match_count(result.memoryMatchCount);
+    response->set_wal_match_count(result.walMatchCount);
+    response->set_memory_search_micros(result.memorySearchMicros);
+    response->set_wal_search_micros(result.walSearchMicros);
+
     for (auto& doc : result.documents) {
         encryption_.decryptSensitiveFields(doc);
         *response->add_documents() = toProto(doc);

+ 7 - 0
service/src/document.hpp

@@ -315,6 +315,13 @@ struct QueryResult {
     std::vector<Document> documents;
     uint64_t totalCount = 0;
     bool hasMore = false;
+
+    // WAL fallback metrics (for queries that include evicted documents)
+    bool usedWalFallback = false;       // True if WAL was scanned for evicted docs
+    uint32_t memoryMatchCount = 0;      // Documents matched from memory
+    uint32_t walMatchCount = 0;         // Documents matched from WAL
+    uint64_t memorySearchMicros = 0;    // Time spent searching memory
+    uint64_t walSearchMicros = 0;       // Time spent loading/searching WAL
 };
 
 /**

+ 94 - 1
service/src/memory_store.cpp

@@ -638,14 +638,48 @@ QueryResult MemoryStore::find(const std::string& collection, const Query& query)
         const_cast<MemoryStore*>(this)->stats_.queryCount++;
     }
 
-    // Collect matching documents
+    // Phase 1: Search in-memory documents
+    auto memoryStartTime = std::chrono::steady_clock::now();
     std::vector<Document> matches;
+    uint32_t memoryMatchCount = 0;
+
     for (const auto& [id, doc] : coll->documents) {
         if (doc.isExpired()) {
             continue;
         }
         if (matchesFilters(doc, query.filters)) {
             matches.push_back(doc);
+            memoryMatchCount++;
+        }
+    }
+
+    auto memoryElapsed = std::chrono::duration_cast<std::chrono::microseconds>(
+        std::chrono::steady_clock::now() - memoryStartTime).count();
+
+    // Phase 2: Search evicted documents from WAL (only if there are any)
+    uint32_t walMatchCount = 0;
+    uint64_t walElapsed = 0;
+    bool usedWalFallback = false;
+
+    if (hasEvictedDocuments(collection)) {
+        usedWalFallback = true;
+        auto walStartTime = std::chrono::steady_clock::now();
+
+        auto evictedIds = getEvictedDocumentIds(collection);
+        for (const auto& id : evictedIds) {
+            auto doc = peekEvictedDocument(collection, id);
+            if (doc && !doc->isExpired() && matchesFilters(*doc, query.filters)) {
+                matches.push_back(*doc);
+                walMatchCount++;
+            }
+        }
+
+        walElapsed = std::chrono::duration_cast<std::chrono::microseconds>(
+            std::chrono::steady_clock::now() - walStartTime).count();
+
+        if (walMatchCount > 0) {
+            spdlog::debug("WAL fallback for collection '{}': {} evicted docs checked, {} matched in {} µs",
+                         collection, evictedIds.size(), walMatchCount, walElapsed);
         }
     }
 
@@ -720,6 +754,13 @@ QueryResult MemoryStore::find(const std::string& collection, const Query& query)
 
     result.hasMore = end < matches.size();
 
+    // Record WAL fallback metrics
+    result.usedWalFallback = usedWalFallback;
+    result.memoryMatchCount = memoryMatchCount;
+    result.walMatchCount = walMatchCount;
+    result.memorySearchMicros = memoryElapsed;
+    result.walSearchMicros = walElapsed;
+
     // Record timing
     auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
         std::chrono::steady_clock::now() - startTime).count();
@@ -2059,4 +2100,56 @@ std::optional<MemoryStore::EvictedDocumentStub> MemoryStore::getEvictedStub(
     return stubIt->second;
 }
 
+bool MemoryStore::hasEvictedDocuments(const std::string& collection) const {
+    std::shared_lock<std::shared_mutex> lock(evictionMutex_);
+    auto collIt = evictedDocs_.find(collection);
+    if (collIt == evictedDocs_.end()) {
+        return false;
+    }
+    return !collIt->second.empty();
+}
+
+std::vector<std::string> MemoryStore::getEvictedDocumentIds(const std::string& collection) const {
+    std::shared_lock<std::shared_mutex> lock(evictionMutex_);
+    std::vector<std::string> ids;
+    auto collIt = evictedDocs_.find(collection);
+    if (collIt != evictedDocs_.end()) {
+        ids.reserve(collIt->second.size());
+        for (const auto& [id, stub] : collIt->second) {
+            ids.push_back(id);
+        }
+    }
+    return ids;
+}
+
+std::optional<Document> MemoryStore::peekEvictedDocument(
+    const std::string& collection, const std::string& id) const {
+    EvictedDocumentStub stub;
+
+    // Get stub
+    {
+        std::shared_lock<std::shared_mutex> lock(evictionMutex_);
+        auto collIt = evictedDocs_.find(collection);
+        if (collIt == evictedDocs_.end()) {
+            return std::nullopt;
+        }
+        auto stubIt = collIt->second.find(id);
+        if (stubIt == collIt->second.end()) {
+            return std::nullopt;
+        }
+        stub = stubIt->second;
+    }
+
+    // Load from persistence via callback (without re-inserting to memory)
+    std::optional<Document> doc;
+    {
+        std::lock_guard<std::mutex> lock(callbackMutex_);
+        if (documentLoadCallback_) {
+            doc = documentLoadCallback_(collection, id, stub.walSequence);
+        }
+    }
+
+    return doc;
+}
+
 } // namespace smartbotic::database

+ 21 - 1
service/src/memory_store.hpp

@@ -311,6 +311,26 @@ public:
     [[nodiscard]] std::optional<EvictedDocumentStub> getEvictedStub(
         const std::string& collection, const std::string& id) const;
 
+    /**
+     * Check if a collection has any evicted documents.
+     * Used to determine if WAL fallback is needed for queries.
+     */
+    [[nodiscard]] bool hasEvictedDocuments(const std::string& collection) const;
+
+    /**
+     * Get all evicted document IDs for a collection.
+     * Used for WAL-backed query fallback.
+     */
+    [[nodiscard]] std::vector<std::string> getEvictedDocumentIds(const std::string& collection) const;
+
+    /**
+     * Load an evicted document from WAL without re-inserting into memory.
+     * Used for query operations to avoid memory thrashing.
+     * @return Document data if found, nullopt otherwise
+     */
+    [[nodiscard]] std::optional<Document> peekEvictedDocument(
+        const std::string& collection, const std::string& id) const;
+
     // ===== Version History =====
 
     struct VersionHistoryResult {
@@ -460,7 +480,7 @@ private:
     // Event callbacks
     EventCallback eventCallback_;
     PersistCallback persistCallback_;
-    std::mutex callbackMutex_;
+    mutable std::mutex callbackMutex_;  // mutable for const peekEvictedDocument
 
     // Background expiration thread
     std::thread expirationThread_;