|
@@ -6,11 +6,40 @@
|
|
|
#include <spdlog/spdlog.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
|
|
|
|
#include <atomic>
|
|
#include <atomic>
|
|
|
|
|
+#include <chrono>
|
|
|
|
|
+#include <cstdlib>
|
|
|
#include <mutex>
|
|
#include <mutex>
|
|
|
#include <thread>
|
|
#include <thread>
|
|
|
|
|
|
|
|
namespace smartbotic::database {
|
|
namespace smartbotic::database {
|
|
|
|
|
|
|
|
|
|
+namespace {
|
|
|
|
|
+// v1.7.0 T11 — transient transport errors that are safe to retry for idempotent
|
|
|
|
|
+// writes (insert with explicit ID, updateIfVersion, upsert, remove, patch).
|
|
|
|
|
+// All other codes (INVALID_ARGUMENT, FAILED_PRECONDITION, NOT_FOUND, ...) are
|
|
|
|
|
+// final — no retry.
|
|
|
|
|
+bool retryableStatus(const grpc::Status& s) {
|
|
|
|
|
+ switch (s.error_code()) {
|
|
|
|
|
+ case grpc::StatusCode::DEADLINE_EXCEEDED: // server queued behind eviction/WAL
|
|
|
|
|
+ case grpc::StatusCode::RESOURCE_EXHAUSTED: // admission control or gRPC concurrency cap
|
|
|
|
|
+ case grpc::StatusCode::UNAVAILABLE: // transient connection issue
|
|
|
|
|
+ return true;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Exponential backoff with jitter. attempt=0 => baseMs; attempt=1 => baseMs*2; ...
|
|
|
|
|
+uint32_t computeBackoffMs(uint32_t baseMs, uint32_t attempt, uint32_t capMs, double jitterPct) {
|
|
|
|
|
+ uint64_t exp = static_cast<uint64_t>(baseMs) << attempt;
|
|
|
|
|
+ if (exp > capMs) exp = capMs;
|
|
|
|
|
+ // ±jitterPct jitter; std::rand() is fine here — not a security sensitive RNG.
|
|
|
|
|
+ double jitter = 1.0 + ((double(std::rand()) / RAND_MAX) * 2.0 - 1.0) * jitterPct;
|
|
|
|
|
+ if (jitter < 0.1) jitter = 0.1;
|
|
|
|
|
+ return static_cast<uint32_t>(exp * jitter);
|
|
|
|
|
+}
|
|
|
|
|
+} // anonymous namespace
|
|
|
|
|
+
|
|
|
// ===== PIMPL Implementation =====
|
|
// ===== PIMPL Implementation =====
|
|
|
|
|
|
|
|
class Client::Impl {
|
|
class Client::Impl {
|
|
@@ -85,12 +114,27 @@ public:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
smartbotic::databasepb::InsertResponse response;
|
|
smartbotic::databasepb::InsertResponse response;
|
|
|
- grpc::ClientContext context;
|
|
|
|
|
- setDeadline(context);
|
|
|
|
|
-
|
|
|
|
|
- auto status = stub_->Insert(&context, request, &response);
|
|
|
|
|
|
|
+ grpc::Status status;
|
|
|
|
|
+ for (uint32_t attempt = 0; attempt <= config_.writeRetries; ++attempt) {
|
|
|
|
|
+ grpc::ClientContext context;
|
|
|
|
|
+ setDeadline(context);
|
|
|
|
|
+ response.Clear();
|
|
|
|
|
+ status = stub_->Insert(&context, request, &response);
|
|
|
|
|
+ if (status.ok() || !retryableStatus(status)) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attempt < config_.writeRetries) {
|
|
|
|
|
+ uint32_t backoffMs = computeBackoffMs(
|
|
|
|
|
+ config_.writeRetryBackoffMs, attempt,
|
|
|
|
|
+ config_.writeRetryMaxBackoffMs, config_.writeRetryJitter);
|
|
|
|
|
+ spdlog::warn("Client::insert {}; retrying in {}ms (attempt {}/{})",
|
|
|
|
|
+ status.error_message(), backoffMs,
|
|
|
|
|
+ attempt + 1, config_.writeRetries);
|
|
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (!status.ok()) {
|
|
if (!status.ok()) {
|
|
|
- spdlog::error("Client::insert failed: {}", status.error_message());
|
|
|
|
|
|
|
+ spdlog::error("Client::insert failed after retries: {}", status.error_message());
|
|
|
throw std::runtime_error(status.error_message());
|
|
throw std::runtime_error(status.error_message());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -180,12 +224,27 @@ public:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
smartbotic::databasepb::UpdateResponse response;
|
|
smartbotic::databasepb::UpdateResponse response;
|
|
|
- grpc::ClientContext context;
|
|
|
|
|
- setDeadline(context);
|
|
|
|
|
-
|
|
|
|
|
- auto status = stub_->Update(&context, request, &response);
|
|
|
|
|
|
|
+ grpc::Status status;
|
|
|
|
|
+ for (uint32_t attempt = 0; attempt <= config_.writeRetries; ++attempt) {
|
|
|
|
|
+ grpc::ClientContext context;
|
|
|
|
|
+ setDeadline(context);
|
|
|
|
|
+ response.Clear();
|
|
|
|
|
+ status = stub_->Update(&context, request, &response);
|
|
|
|
|
+ if (status.ok() || !retryableStatus(status)) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attempt < config_.writeRetries) {
|
|
|
|
|
+ uint32_t backoffMs = computeBackoffMs(
|
|
|
|
|
+ config_.writeRetryBackoffMs, attempt,
|
|
|
|
|
+ config_.writeRetryMaxBackoffMs, config_.writeRetryJitter);
|
|
|
|
|
+ spdlog::warn("Client::updateIfVersion {}; retrying in {}ms (attempt {}/{})",
|
|
|
|
|
+ status.error_message(), backoffMs,
|
|
|
|
|
+ attempt + 1, config_.writeRetries);
|
|
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (!status.ok()) {
|
|
if (!status.ok()) {
|
|
|
- spdlog::error("Client::updateIfVersion failed: {}", status.error_message());
|
|
|
|
|
|
|
+ spdlog::error("Client::updateIfVersion failed after retries: {}", status.error_message());
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -203,12 +262,27 @@ public:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
smartbotic::databasepb::PatchDocumentResponse response;
|
|
smartbotic::databasepb::PatchDocumentResponse response;
|
|
|
- grpc::ClientContext context;
|
|
|
|
|
- setDeadline(context);
|
|
|
|
|
-
|
|
|
|
|
- auto status = stub_->PatchDocument(&context, request, &response);
|
|
|
|
|
|
|
+ grpc::Status status;
|
|
|
|
|
+ for (uint32_t attempt = 0; attempt <= config_.writeRetries; ++attempt) {
|
|
|
|
|
+ grpc::ClientContext context;
|
|
|
|
|
+ setDeadline(context);
|
|
|
|
|
+ response.Clear();
|
|
|
|
|
+ status = stub_->PatchDocument(&context, request, &response);
|
|
|
|
|
+ if (status.ok() || !retryableStatus(status)) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attempt < config_.writeRetries) {
|
|
|
|
|
+ uint32_t backoffMs = computeBackoffMs(
|
|
|
|
|
+ config_.writeRetryBackoffMs, attempt,
|
|
|
|
|
+ config_.writeRetryMaxBackoffMs, config_.writeRetryJitter);
|
|
|
|
|
+ spdlog::warn("Client::patch {}; retrying in {}ms (attempt {}/{})",
|
|
|
|
|
+ status.error_message(), backoffMs,
|
|
|
|
|
+ attempt + 1, config_.writeRetries);
|
|
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (!status.ok()) {
|
|
if (!status.ok()) {
|
|
|
- spdlog::error("Client::patch failed: {}", status.error_message());
|
|
|
|
|
|
|
+ spdlog::error("Client::patch failed after retries: {}", status.error_message());
|
|
|
return 0;
|
|
return 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -237,12 +311,27 @@ public:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
smartbotic::databasepb::UpsertResponse response;
|
|
smartbotic::databasepb::UpsertResponse response;
|
|
|
- grpc::ClientContext context;
|
|
|
|
|
- setDeadline(context);
|
|
|
|
|
-
|
|
|
|
|
- auto status = stub_->Upsert(&context, request, &response);
|
|
|
|
|
|
|
+ grpc::Status status;
|
|
|
|
|
+ for (uint32_t attempt = 0; attempt <= config_.writeRetries; ++attempt) {
|
|
|
|
|
+ grpc::ClientContext context;
|
|
|
|
|
+ setDeadline(context);
|
|
|
|
|
+ response.Clear();
|
|
|
|
|
+ status = stub_->Upsert(&context, request, &response);
|
|
|
|
|
+ if (status.ok() || !retryableStatus(status)) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attempt < config_.writeRetries) {
|
|
|
|
|
+ uint32_t backoffMs = computeBackoffMs(
|
|
|
|
|
+ config_.writeRetryBackoffMs, attempt,
|
|
|
|
|
+ config_.writeRetryMaxBackoffMs, config_.writeRetryJitter);
|
|
|
|
|
+ spdlog::warn("Client::upsert {}; retrying in {}ms (attempt {}/{})",
|
|
|
|
|
+ status.error_message(), backoffMs,
|
|
|
|
|
+ attempt + 1, config_.writeRetries);
|
|
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (!status.ok()) {
|
|
if (!status.ok()) {
|
|
|
- spdlog::error("Client::upsert failed: {}", status.error_message());
|
|
|
|
|
|
|
+ spdlog::error("Client::upsert failed after retries: {}", status.error_message());
|
|
|
throw std::runtime_error(status.error_message());
|
|
throw std::runtime_error(status.error_message());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -255,12 +344,27 @@ public:
|
|
|
request.set_id(id);
|
|
request.set_id(id);
|
|
|
|
|
|
|
|
smartbotic::databasepb::DeleteResponse response;
|
|
smartbotic::databasepb::DeleteResponse response;
|
|
|
- grpc::ClientContext context;
|
|
|
|
|
- setDeadline(context);
|
|
|
|
|
-
|
|
|
|
|
- auto status = stub_->Delete(&context, request, &response);
|
|
|
|
|
|
|
+ grpc::Status status;
|
|
|
|
|
+ for (uint32_t attempt = 0; attempt <= config_.writeRetries; ++attempt) {
|
|
|
|
|
+ grpc::ClientContext context;
|
|
|
|
|
+ setDeadline(context);
|
|
|
|
|
+ response.Clear();
|
|
|
|
|
+ status = stub_->Delete(&context, request, &response);
|
|
|
|
|
+ if (status.ok() || !retryableStatus(status)) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attempt < config_.writeRetries) {
|
|
|
|
|
+ uint32_t backoffMs = computeBackoffMs(
|
|
|
|
|
+ config_.writeRetryBackoffMs, attempt,
|
|
|
|
|
+ config_.writeRetryMaxBackoffMs, config_.writeRetryJitter);
|
|
|
|
|
+ spdlog::warn("Client::remove {}; retrying in {}ms (attempt {}/{})",
|
|
|
|
|
+ status.error_message(), backoffMs,
|
|
|
|
|
+ attempt + 1, config_.writeRetries);
|
|
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (!status.ok()) {
|
|
if (!status.ok()) {
|
|
|
- spdlog::error("Client::remove failed: {}", status.error_message());
|
|
|
|
|
|
|
+ spdlog::error("Client::remove failed after retries: {}", status.error_message());
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|