Pārlūkot izejas kodu

feat: optimistic locking in Client::update() (v1.3.0)

update() now uses version-checked writes transparently:
1. Reads current document version via get()
2. Calls updateIfVersion() with that version
3. On version conflict (concurrent writer), retries up to maxRetries

This prevents silent data loss when two clients update the same
document simultaneously. Each write is properly version-sequenced
and recorded in history. Callers don't need to change their code.

Metadata fields (_version, _id, _created_at, etc.) are automatically
stripped from the data before writing, so callers can pass the JSON
from get() directly after modifying fields.
fszontagh 3 mēneši atpakaļ
vecāks
revīzija
697cd8d9c3
2 mainītis faili ar 34 papildinājumiem un 16 dzēšanām
  1. 1 1
      VERSION
  2. 33 15
      client/src/client.cpp

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.2.0
+1.3.0

+ 33 - 15
client/src/client.cpp

@@ -128,25 +128,43 @@ public:
 
     bool update(const std::string& collection, const std::string& id, const nlohmann::json& data,
                 const std::string& actor) {
-        smartbotic::databasepb::UpdateRequest request;
-        request.set_collection(collection);
-        request.set_id(id);
-        request.set_data(data.dump());
-        if (!actor.empty()) {
-            request.set_actor(actor);
-        }
+        // Strip metadata fields from data — callers pass the JSON from get() which
+        // includes _version, _id, etc.  We use _version for optimistic locking.
+        auto cleanData = data;
+        cleanData.erase("_id");
+        cleanData.erase("_version");
+        cleanData.erase("_created_at");
+        cleanData.erase("_updated_at");
+        cleanData.erase("_created_by");
+        cleanData.erase("_updated_by");
+
+        // Optimistic locking with automatic retry:
+        // 1. Read current version
+        // 2. Call updateIfVersion
+        // 3. On version conflict (another writer), re-read and retry
+        for (uint32_t attempt = 0; attempt <= config_.maxRetries; ++attempt) {
+            // Get current version
+            auto current = get(collection, id);
+            if (!current) {
+                return false;  // document doesn't exist
+            }
+            uint64_t version = (*current)["_version"].get<uint64_t>();
 
-        smartbotic::databasepb::UpdateResponse response;
-        grpc::ClientContext context;
-        setDeadline(context);
+            // Attempt version-checked update
+            if (updateIfVersion(collection, id, cleanData, version, actor)) {
+                return true;
+            }
 
-        auto status = stub_->Update(&context, request, &response);
-        if (!status.ok()) {
-            spdlog::error("Client::update failed: {}", status.error_message());
-            return false;
+            // Version conflict — another client wrote between our get() and update
+            if (attempt < config_.maxRetries) {
+                spdlog::debug("Client::update version conflict on {}/{}, retry {}/{}",
+                              collection, id, attempt + 1, config_.maxRetries);
+            }
         }
 
-        return response.success();
+        spdlog::warn("Client::update failed after {} retries due to version conflicts on {}/{}",
+                     config_.maxRetries, collection, id);
+        return false;
     }
 
     bool updateIfVersion(const std::string& collection, const std::string& id,