database_client.cpp 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. #include "smartbotic/webserver/database_client.hpp"
  2. #include <spdlog/spdlog.h>
  3. #include <algorithm>
  4. #include <cmath>
  5. #include <cstdlib>
  6. #include <thread>
  7. namespace smartbotic::webserver {
  8. DatabaseClient::DatabaseClient(DatabaseClientConfig config) : config_(std::move(config)) {}
  9. DatabaseClient::DatabaseClient(const HttpServerConfig& server_config) {
  10. config_.address = server_config.database_address;
  11. }
  12. DatabaseClient::~DatabaseClient() {
  13. Disconnect();
  14. }
  15. auto DatabaseClient::Connect() -> bool {
  16. std::lock_guard<std::mutex> lock(mutex_);
  17. if (connected_.load()) {
  18. spdlog::debug("Database client already connected");
  19. return true;
  20. }
  21. spdlog::info("Connecting to database at {}", config_.address);
  22. try {
  23. CreateChannel();
  24. CreateStubs();
  25. // Wait for the channel to become ready
  26. if (!WaitForReady(config_.connect_timeout)) {
  27. spdlog::error("Failed to connect to database at {}: connection timeout", config_.address);
  28. return false;
  29. }
  30. connected_.store(true);
  31. spdlog::info("Successfully connected to database at {}", config_.address);
  32. return true;
  33. } catch (const std::exception& e) {
  34. spdlog::error("Failed to connect to database: {}", e.what());
  35. return false;
  36. }
  37. }
  38. void DatabaseClient::Disconnect() {
  39. std::lock_guard<std::mutex> lock(mutex_);
  40. if (!connected_.load()) {
  41. return;
  42. }
  43. spdlog::info("Disconnecting from database");
  44. // Clear stubs
  45. document_stub_.reset();
  46. collection_stub_.reset();
  47. query_stub_.reset();
  48. admin_stub_.reset();
  49. subscription_stub_.reset();
  50. // Release channel
  51. channel_.reset();
  52. connected_.store(false);
  53. spdlog::info("Disconnected from database");
  54. }
  55. auto DatabaseClient::IsConnected() const -> bool {
  56. if (!connected_.load()) {
  57. return false;
  58. }
  59. // Also verify channel state
  60. if (channel_) {
  61. auto state = channel_->GetState(false);
  62. return state == GRPC_CHANNEL_READY || state == GRPC_CHANNEL_IDLE;
  63. }
  64. return false;
  65. }
  66. auto DatabaseClient::HealthCheck() -> HealthCheckResult {
  67. HealthCheckResult result;
  68. if (!connected_.load() || !admin_stub_) {
  69. result.healthy = false;
  70. result.message = "Not connected to database";
  71. return result;
  72. }
  73. auto start = std::chrono::steady_clock::now();
  74. // Use GetDatabaseStats as a health check - it's lightweight
  75. grpc::ClientContext context;
  76. context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));
  77. ::smartbotic::database::GetDatabaseStatsRequest request;
  78. ::smartbotic::database::DatabaseStats response;
  79. auto status = admin_stub_->GetDatabaseStats(&context, request, &response);
  80. auto end = std::chrono::steady_clock::now();
  81. result.latency = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
  82. if (status.ok()) {
  83. result.healthy = true;
  84. result.message = "Database is healthy";
  85. } else {
  86. result.healthy = false;
  87. result.message = "Database health check failed: " + status.error_message();
  88. spdlog::warn("Database health check failed: {} (code: {})", status.error_message(),
  89. static_cast<int>(status.error_code()));
  90. }
  91. return result;
  92. }
  93. auto DatabaseClient::WaitForReady(std::chrono::milliseconds timeout) -> bool {
  94. if (!channel_) {
  95. return false;
  96. }
  97. auto deadline = std::chrono::system_clock::now() + timeout;
  98. // Try to establish connection
  99. auto state = channel_->GetState(true); // true = try to connect
  100. while (state != GRPC_CHANNEL_READY) {
  101. if (state == GRPC_CHANNEL_SHUTDOWN) {
  102. spdlog::error("Channel is shutdown");
  103. return false;
  104. }
  105. if (!channel_->WaitForStateChange(state, deadline)) {
  106. // Timeout
  107. spdlog::error("Timeout waiting for channel to become ready (current state: {})", GetChannelStateString());
  108. return false;
  109. }
  110. state = channel_->GetState(false);
  111. }
  112. return true;
  113. }
  114. auto DatabaseClient::GetChannelState() const -> grpc_connectivity_state {
  115. if (!channel_) {
  116. return GRPC_CHANNEL_SHUTDOWN;
  117. }
  118. return channel_->GetState(false);
  119. }
  120. auto DatabaseClient::GetChannelStateString() const -> std::string {
  121. switch (GetChannelState()) {
  122. case GRPC_CHANNEL_IDLE:
  123. return "IDLE";
  124. case GRPC_CHANNEL_CONNECTING:
  125. return "CONNECTING";
  126. case GRPC_CHANNEL_READY:
  127. return "READY";
  128. case GRPC_CHANNEL_TRANSIENT_FAILURE:
  129. return "TRANSIENT_FAILURE";
  130. case GRPC_CHANNEL_SHUTDOWN:
  131. return "SHUTDOWN";
  132. default:
  133. return "UNKNOWN";
  134. }
  135. }
  136. auto DatabaseClient::GetDocumentService() -> ::smartbotic::database::DocumentService::Stub* {
  137. return document_stub_.get();
  138. }
  139. auto DatabaseClient::GetCollectionService() -> ::smartbotic::database::CollectionService::Stub* {
  140. return collection_stub_.get();
  141. }
  142. auto DatabaseClient::GetQueryService() -> ::smartbotic::database::QueryService::Stub* {
  143. return query_stub_.get();
  144. }
  145. auto DatabaseClient::GetAdminService() -> ::smartbotic::database::AdminService::Stub* {
  146. return admin_stub_.get();
  147. }
  148. auto DatabaseClient::GetSubscriptionService() -> ::smartbotic::database::SubscriptionService::Stub* {
  149. return subscription_stub_.get();
  150. }
  151. void DatabaseClient::CreateChannel() {
  152. grpc::ChannelArguments args;
  153. // Set keepalive parameters for long-lived connections
  154. args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 30000); // 30 seconds
  155. args.SetInt(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 10000); // 10 seconds
  156. args.SetInt(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, 1); // Allow keepalive without active calls
  157. args.SetInt(GRPC_ARG_HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS, 5000);
  158. // Set maximum message sizes
  159. args.SetMaxReceiveMessageSize(64 * 1024 * 1024); // 64 MB
  160. args.SetMaxSendMessageSize(64 * 1024 * 1024); // 64 MB
  161. // Initial reconnect backoff
  162. args.SetInt(GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS, static_cast<int>(config_.initial_backoff.count()));
  163. args.SetInt(GRPC_ARG_MAX_RECONNECT_BACKOFF_MS, static_cast<int>(config_.max_backoff.count()));
  164. // Create channel with insecure credentials (for local development)
  165. // In production, this should use TLS credentials
  166. channel_ = grpc::CreateCustomChannel(config_.address, grpc::InsecureChannelCredentials(), args);
  167. spdlog::debug("Created gRPC channel to {}", config_.address);
  168. }
  169. void DatabaseClient::CreateStubs() {
  170. document_stub_ = ::smartbotic::database::DocumentService::NewStub(channel_);
  171. collection_stub_ = ::smartbotic::database::CollectionService::NewStub(channel_);
  172. query_stub_ = ::smartbotic::database::QueryService::NewStub(channel_);
  173. admin_stub_ = ::smartbotic::database::AdminService::NewStub(channel_);
  174. subscription_stub_ = ::smartbotic::database::SubscriptionService::NewStub(channel_);
  175. spdlog::debug("Created all gRPC service stubs");
  176. }
  177. auto DatabaseClient::CalculateBackoff(uint32_t attempt) const -> std::chrono::milliseconds {
  178. // Exponential backoff with jitter
  179. auto base_backoff = static_cast<double>(config_.initial_backoff.count());
  180. auto multiplier = std::pow(config_.backoff_multiplier, static_cast<double>(attempt));
  181. auto backoff_ms = static_cast<int64_t>(base_backoff * multiplier);
  182. // Cap at max backoff
  183. backoff_ms = std::min(backoff_ms, static_cast<int64_t>(config_.max_backoff.count()));
  184. // Add jitter (±10%)
  185. auto jitter = static_cast<int64_t>(backoff_ms * 0.1);
  186. backoff_ms += (rand() % (2 * jitter + 1)) - jitter; // NOLINT(concurrency-mt-unsafe)
  187. return std::chrono::milliseconds(backoff_ms);
  188. }
  189. auto DatabaseClient::IsRetryable(grpc::StatusCode code) -> bool {
  190. switch (code) {
  191. case grpc::StatusCode::UNAVAILABLE:
  192. case grpc::StatusCode::DEADLINE_EXCEEDED:
  193. case grpc::StatusCode::ABORTED:
  194. case grpc::StatusCode::RESOURCE_EXHAUSTED:
  195. return true;
  196. default:
  197. return false;
  198. }
  199. }
  200. } // namespace smartbotic::webserver