Jelajahi Sumber

feat(events): emit MEMORY_PRESSURE_HIGH + MEMORY_EVICTION_BURST (v1.7.0 T10)

MEMORY_PRESSURE_HIGH - edge-triggered when pressure transitions INTO
Hard or Emergency from a lower level. Tracked via an atomic
lastObservedPressure_ so the event fires once per climb rather than
every eviction tick. Resets naturally when pressure drops to
Normal/Soft, so the next climb re-triggers.

MEMORY_EVICTION_BURST - fired from evictOneChunk() when a single tick
evicts >= eviction_burst_threshold docs (default 10000). Payload
includes doc count, bytes freed, and current pressure level. Lets
observability tooling (shadowman-cpp metrics rollup) annotate memory
graphs with "eviction storm" markers.

Both events are system-level: collection='' and documentId=''. The
JSON payload (DatabaseEvent::data) carries pressure level, percent,
estimated/max bytes, and (for bursts) doc + byte counts. Subscribers
with no collection/pattern filter match the all-events path and
receive them; filtered subscribers won't, which matches existing
match-by-collection semantics.

EventType enum extended with MEMORY_PRESSURE_HIGH=5 and
MEMORY_EVICTION_BURST=6; the existing +1 wire offset in
database_grpc_impl maps them to pb::EVENT_MEMORY_PRESSURE_HIGH (6)
and pb::EVENT_MEMORY_EVICTION_BURST (7), matching the proto added
in T2.

New config knob: memory.eviction_burst_threshold (uint32, default
10000 docs). Added to MemoryStore::Config, DatabaseService::Config,
JSON parsing, and packaging/deb/config/config.json default.
fszontagh 3 bulan lalu
induk
melakukan
0566e50050

+ 2 - 1
packaging/deb/config/config.json

@@ -16,7 +16,8 @@
       "hot_write_floor_ms": 30000,
       "memory_soft_percent": 70,
       "memory_hard_percent": 85,
-      "memory_emergency_percent": 95
+      "memory_emergency_percent": 95,
+      "eviction_burst_threshold": 10000
     },
     "persistence": {
       "wal_sync_interval_ms": 100,

+ 4 - 0
service/src/database_service.cpp

@@ -345,6 +345,9 @@ DatabaseService::Config DatabaseService::parseConfig(const nlohmann::json& json)
             config.memorySoftPercent = memory.value("memory_soft_percent", config.memorySoftPercent);
             config.memoryHardPercent = memory.value("memory_hard_percent", config.memoryHardPercent);
             config.memoryEmergencyPercent = memory.value("memory_emergency_percent", config.memoryEmergencyPercent);
+
+            // v1.7.0 T10 — eviction burst event threshold
+            config.evictionBurstThreshold = memory.value("eviction_burst_threshold", config.evictionBurstThreshold);
         }
 
         // Persistence settings
@@ -453,6 +456,7 @@ void DatabaseService::setupComponents() {
     storeConfig.memorySoftPercent = config_.memorySoftPercent;
     storeConfig.memoryHardPercent = config_.memoryHardPercent;
     storeConfig.memoryEmergencyPercent = config_.memoryEmergencyPercent;
+    storeConfig.evictionBurstThreshold = config_.evictionBurstThreshold;
     store_ = std::make_unique<MemoryStore>(storeConfig);
 
     // Create view manager (cache loaded in initialize() after persistence recovery)

+ 3 - 0
service/src/database_service.hpp

@@ -64,6 +64,9 @@ public:
         uint32_t memoryHardPercent = 85;
         uint32_t memoryEmergencyPercent = 95;
 
+        // v1.7.0 T10 — eviction burst event threshold (docs per tick).
+        uint32_t evictionBurstThreshold = 10000;
+
         // Persistence settings
         uint32_t walSyncIntervalMs = 100;
         uint32_t snapshotIntervalSec = 3600;

+ 15 - 5
service/src/document.hpp

@@ -360,13 +360,23 @@ struct QueryResult {
 
 /**
  * Storage event types.
+ *
+ * Integer ordering matters: database_grpc_impl.cpp converts to pb::EventType
+ * with a `+1` offset (pb::EVENT_UNKNOWN == 0 is the wire-level default).
+ * When adding entries, also add them to proto EventType in matching order.
  */
 enum class EventType {
-    INSERT,
-    UPDATE,
-    DELETE,
-    EXPIRE,
-    INVALIDATE
+    INSERT,                // pb::EVENT_INSERT
+    UPDATE,                // pb::EVENT_UPDATE
+    DELETE,                // pb::EVENT_DELETE
+    EXPIRE,                // pb::EVENT_EXPIRE
+    INVALIDATE,            // pb::EVENT_INVALIDATE
+
+    // v1.7.0 — memory-pressure observability events (T10).
+    // System-level: collection="" and documentId="". Payload in `data` carries
+    // pressure level, percent, and (for bursts) doc count and bytes freed.
+    MEMORY_PRESSURE_HIGH,  // pb::EVENT_MEMORY_PRESSURE_HIGH
+    MEMORY_EVICTION_BURST  // pb::EVENT_MEMORY_EVICTION_BURST
 };
 
 /**

+ 56 - 0
service/src/memory_store.cpp

@@ -2335,6 +2335,36 @@ void MemoryStore::emitEvent(EventType type, const std::string& collection, const
     }
 }
 
+void MemoryStore::emitPressureHigh(MemoryPressure level) {
+    nlohmann::json payload = {
+        {"pressure_level", memoryPressureToString(level)},
+        {"pressure_percent", pressurePercent()},
+        {"estimated_bytes", estimatedMemoryBytes_.load(std::memory_order_relaxed)},
+        {"max_bytes", config_.maxMemoryBytes}
+    };
+    // System-level event: empty collection + id. Subscribers receive it
+    // when subscribed with no collection/pattern filter (matches-all path).
+    emitEvent(EventType::MEMORY_PRESSURE_HIGH, "", "", payload);
+    spdlog::warn("Emitted MEMORY_PRESSURE_HIGH: {} at {}% ({} MB / {} MB)",
+                 memoryPressureToString(level),
+                 pressurePercent(),
+                 estimatedMemoryBytes_.load(std::memory_order_relaxed) / (1024 * 1024),
+                 config_.maxMemoryBytes / (1024 * 1024));
+}
+
+void MemoryStore::emitEvictionBurst(uint64_t docs, uint64_t bytesFreed) {
+    nlohmann::json payload = {
+        {"evicted_docs", docs},
+        {"bytes_freed", bytesFreed},
+        {"pressure_level", memoryPressureToString(pressure())},
+        {"pressure_percent", pressurePercent()}
+    };
+    emitEvent(EventType::MEMORY_EVICTION_BURST, "", "", payload);
+    spdlog::warn("Emitted MEMORY_EVICTION_BURST: {} docs evicted, {} MB freed (pressure={})",
+                 docs, bytesFreed / (1024 * 1024),
+                 memoryPressureToString(pressure()));
+}
+
 void MemoryStore::emitPersist(const std::string& collection, const std::string& id,
                               const std::optional<Document>& doc, EventType eventType) {
     std::lock_guard<std::mutex> lock(callbackMutex_);
@@ -2492,6 +2522,24 @@ void MemoryStore::evictionLoop() {
         }
 
         MemoryPressure p = pressure();
+
+        // v1.7.0 T10 — edge-triggered MEMORY_PRESSURE_HIGH event.
+        // Fire when we transition INTO Hard or Emergency from a lower level.
+        // Reset tracker only when we drop back to Normal/Soft so subscribers
+        // don't get spammed while we oscillate between Hard and Emergency.
+        MemoryPressure prevObserved =
+            lastObservedPressure_.load(std::memory_order_relaxed);
+        bool crossedIntoHigh =
+            (p == MemoryPressure::Hard || p == MemoryPressure::Emergency) &&
+            (prevObserved != MemoryPressure::Hard &&
+             prevObserved != MemoryPressure::Emergency);
+        if (crossedIntoHigh) {
+            emitPressureHigh(p);
+        }
+        // Always advance the tracker. "Reset" happens naturally when p drops
+        // into Normal/Soft, so the next Hard/Emergency climb re-triggers.
+        lastObservedPressure_.store(p, std::memory_order_relaxed);
+
         if (p == MemoryPressure::Normal) {
             continue;  // nothing to do
         }
@@ -2689,6 +2737,14 @@ uint64_t MemoryStore::evictOneChunk() {
     spdlog::info("Eviction chunk: {} docs evicted, {} bytes freed, pressure={}",
                  evictedCount, actuallyFreed, memoryPressureToString(pressure()));
 
+    // v1.7.0 T10 — MEMORY_EVICTION_BURST event.
+    // Large chunks are interesting because they indicate the eviction loop
+    // is doing heavy work, not trickling. Subscribers (shadowman-cpp metrics
+    // rollup) can annotate memory graphs with "burst" markers.
+    if (evictedCount >= config_.evictionBurstThreshold) {
+        emitEvictionBurst(evictedCount, actuallyFreed);
+    }
+
     return actuallyFreed;
 }
 

+ 12 - 0
service/src/memory_store.hpp

@@ -83,6 +83,11 @@ public:
         uint32_t memoryHardPercent = 85;             // Aggressive eviction + emit pressure event
         uint32_t memoryEmergencyPercent = 95;        // Refuse writes above this %
 
+        // v1.7.0 T10 — eviction burst threshold. If a single evictOneChunk()
+        // tick evicts >= this many docs, a MEMORY_EVICTION_BURST event is
+        // fired so observability tooling can annotate "eviction storms".
+        uint32_t evictionBurstThreshold = 10000;
+
         Config()
             : maxMemoryBytes(800ULL * 1024 * 1024)   // 800 MB default
             , expirationCheckIntervalMs(1000)         // Check TTL every second
@@ -647,6 +652,13 @@ private:
     void emitEvent(EventType type, const std::string& collection, const std::string& id,
                    const std::optional<nlohmann::json>& data = std::nullopt);
 
+    // v1.7.0 T10 — memory-pressure observability events.
+    // System-level events (collection="", documentId="") carrying a JSON
+    // payload via DatabaseEvent::data. Edge-triggered by evictionLoop /
+    // evictOneChunk so subscribers don't get spammed on sustained pressure.
+    void emitPressureHigh(MemoryPressure level);
+    void emitEvictionBurst(uint64_t docs, uint64_t bytesFreed);
+
     // Emit a persist callback
     void emitPersist(const std::string& collection, const std::string& id,
                      const std::optional<Document>& doc, EventType eventType);