couchbase 3.8.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (473) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/ext/CMakeLists.txt +16 -1
  4. data/ext/cache/cpm/{CPM_0.42.0.cmake → CPM_0.43.1.cmake} +18 -2
  5. data/ext/cache/extconf_include.rb +6 -6
  6. data/ext/cache/mozilla-ca-bundle.crt +121 -55
  7. data/ext/cache/mozilla-ca-bundle.sha256 +1 -1
  8. data/ext/couchbase/CMakeLists.txt +130 -21
  9. data/ext/couchbase/cmake/APKBUILD.in +25 -4
  10. data/ext/couchbase/cmake/Bundler.cmake +143 -34
  11. data/ext/couchbase/cmake/CPM.cmake +126 -12
  12. data/ext/couchbase/cmake/Cache.cmake +7 -1
  13. data/ext/couchbase/cmake/CompilerOptions.cmake +1 -1
  14. data/ext/couchbase/cmake/CompilerWarnings.cmake +17 -6
  15. data/ext/couchbase/cmake/GrpcProtobuf.cmake +481 -0
  16. data/ext/couchbase/cmake/OpenSSL.cmake +10 -3
  17. data/ext/couchbase/cmake/Packaging.cmake +519 -65
  18. data/ext/couchbase/cmake/Protostellar.cmake +250 -0
  19. data/ext/couchbase/cmake/Sanitizers.cmake +61 -50
  20. data/ext/couchbase/cmake/StaticAnalyzers.cmake +16 -5
  21. data/ext/couchbase/cmake/TestFramework.cmake +283 -0
  22. data/ext/couchbase/cmake/TestFrameworkAddTests.cmake +51 -0
  23. data/ext/couchbase/cmake/Testing.cmake +27 -9
  24. data/ext/couchbase/cmake/ThirdPartyDependencies.cmake +141 -8
  25. data/ext/couchbase/cmake/VersionInfo.cmake +50 -0
  26. data/ext/couchbase/cmake/build_config.hxx.in +2 -1
  27. data/ext/couchbase/cmake/couchbase-cxx-client.spec.in +101 -23
  28. data/ext/couchbase/cmake/couchbase_cxx_client_static-config.cmake.in +7 -0
  29. data/ext/couchbase/cmake/couchbase_cxx_client_static.pc.in +2 -0
  30. data/ext/couchbase/cmake/gRPC_Generate.cmake +58 -0
  31. data/ext/couchbase/cmake/protobuf_Generate.cmake +69 -0
  32. data/ext/couchbase/cmake/require-git-checkout.cmake +25 -0
  33. data/ext/couchbase/cmake/tarball_glob.txt +49 -0
  34. data/ext/couchbase/core/agent_group.cxx +0 -8
  35. data/ext/couchbase/core/agent_group.hxx +0 -4
  36. data/ext/couchbase/core/analytics_stream.cxx +314 -0
  37. data/ext/couchbase/core/analytics_stream.hxx +129 -0
  38. data/ext/couchbase/core/analytics_stream_component.cxx +245 -0
  39. data/ext/couchbase/core/analytics_stream_component.hxx +67 -0
  40. data/ext/couchbase/core/app_telemetry_meter.cxx +25 -3
  41. data/ext/couchbase/core/app_telemetry_meter.hxx +77 -0
  42. data/ext/couchbase/core/app_telemetry_reporter.cxx +7 -6
  43. data/ext/couchbase/core/bucket.cxx +381 -87
  44. data/ext/couchbase/core/bucket.hxx +105 -25
  45. data/ext/couchbase/core/bucket_unit_test_api.hxx +57 -0
  46. data/ext/couchbase/core/capella.hxx +91 -0
  47. data/ext/couchbase/core/cluster.cxx +1554 -398
  48. data/ext/couchbase/core/cluster.hxx +39 -8
  49. data/ext/couchbase/core/cluster_agent.cxx +0 -8
  50. data/ext/couchbase/core/cluster_agent.hxx +0 -4
  51. data/ext/couchbase/core/cluster_options.hxx +26 -3
  52. data/ext/couchbase/core/collections_component.cxx +39 -15
  53. data/ext/couchbase/core/config_profiles.cxx +1 -1
  54. data/ext/couchbase/core/crud_component.cxx +2 -2
  55. data/ext/couchbase/core/crud_options.hxx +34 -0
  56. data/ext/couchbase/core/crypto/cbcrypto.cc +18 -13
  57. data/ext/couchbase/core/document_id_redaction.hxx +81 -0
  58. data/ext/couchbase/core/error_context/base_error_context.hxx +7 -6
  59. data/ext/couchbase/core/error_context/key_value.cxx +25 -2
  60. data/ext/couchbase/core/error_context/key_value.hxx +60 -33
  61. data/ext/couchbase/core/error_context/key_value_error_context.hxx +16 -15
  62. data/ext/couchbase/core/error_context/search.hxx +5 -0
  63. data/ext/couchbase/core/error_context/search_json.hxx +3 -0
  64. data/ext/couchbase/core/error_context/subdocument_error_context.hxx +19 -18
  65. data/ext/couchbase/core/error_context/view.hxx +5 -0
  66. data/ext/couchbase/core/free_form_http_request.cxx +237 -4
  67. data/ext/couchbase/core/free_form_http_request.hxx +41 -8
  68. data/ext/couchbase/core/http_component.cxx +300 -284
  69. data/ext/couchbase/core/http_component.hxx +0 -4
  70. data/ext/couchbase/core/impl/analytics.cxx +38 -5
  71. data/ext/couchbase/core/impl/analytics.hxx +34 -0
  72. data/ext/couchbase/core/impl/bootstrap_error.hxx +0 -3
  73. data/ext/couchbase/core/impl/collection.cxx +215 -202
  74. data/ext/couchbase/core/impl/collection_manager.cxx +1 -1
  75. data/ext/couchbase/core/impl/dns_srv_tracker.cxx +45 -18
  76. data/ext/couchbase/core/impl/error.cxx +22 -2
  77. data/ext/couchbase/core/impl/internal_analytics_stream_result.hxx +111 -0
  78. data/ext/couchbase/core/impl/internal_query_stream_result.hxx +110 -0
  79. data/ext/couchbase/core/impl/invoke_with_node_id.hxx +15 -7
  80. data/ext/couchbase/core/impl/key_value_error_category.cxx +6 -0
  81. data/ext/couchbase/core/impl/lookup_in_replica.cxx +10 -10
  82. data/ext/couchbase/core/impl/lookup_in_replica.hxx +4 -4
  83. data/ext/couchbase/core/impl/node_id.cxx +49 -13
  84. data/ext/couchbase/core/impl/observability_recorder.cxx +12 -2
  85. data/ext/couchbase/core/impl/observe_poll.cxx +63 -23
  86. data/ext/couchbase/core/impl/public_analytics_stream_result.cxx +372 -0
  87. data/ext/couchbase/core/impl/public_bucket.cxx +48 -2
  88. data/ext/couchbase/core/impl/public_cluster.cxx +229 -42
  89. data/ext/couchbase/core/impl/public_query_stream_result.cxx +380 -0
  90. data/ext/couchbase/core/impl/public_transaction_get_multi_replicas_from_preferred_server_group_result.cxx +42 -0
  91. data/ext/couchbase/core/impl/public_transaction_get_multi_result.cxx +42 -0
  92. data/ext/couchbase/core/impl/query.cxx +76 -7
  93. data/ext/couchbase/core/impl/query.hxx +36 -0
  94. data/ext/couchbase/core/impl/query_index_manager.cxx +54 -1
  95. data/ext/couchbase/core/impl/replica_utils.cxx +101 -0
  96. data/ext/couchbase/core/impl/replica_utils.hxx +64 -0
  97. data/ext/couchbase/core/impl/resolve_node_id.hxx +125 -0
  98. data/ext/couchbase/core/impl/scope.cxx +70 -0
  99. data/ext/couchbase/core/impl/search.cxx +31 -1
  100. data/ext/couchbase/core/impl/transactions.cxx +130 -0
  101. data/ext/couchbase/core/impl/wait_until_ready.cxx +439 -0
  102. data/ext/couchbase/core/impl/wait_until_ready.hxx +110 -0
  103. data/ext/couchbase/core/impl/with_bucket_config_or_timeout.hxx +180 -0
  104. data/ext/couchbase/core/impl/with_cancellation.hxx +11 -3
  105. data/ext/couchbase/core/io/configuration_belongs_to_session.hxx +1 -2
  106. data/ext/couchbase/core/io/dns_client.cxx +122 -45
  107. data/ext/couchbase/core/io/dns_codec.hxx +21 -21
  108. data/ext/couchbase/core/io/dns_config.cxx +21 -6
  109. data/ext/couchbase/core/io/http_command.hxx +149 -121
  110. data/ext/couchbase/core/io/http_error.hxx +107 -0
  111. data/ext/couchbase/core/io/http_parser.cxx +4 -0
  112. data/ext/couchbase/core/io/http_session.cxx +606 -154
  113. data/ext/couchbase/core/io/http_session.hxx +147 -30
  114. data/ext/couchbase/core/io/http_session_manager.hxx +329 -382
  115. data/ext/couchbase/core/io/http_streaming_parser.cxx +4 -0
  116. data/ext/couchbase/core/io/http_streaming_response.cxx +328 -43
  117. data/ext/couchbase/core/io/http_streaming_response.hxx +37 -3
  118. data/ext/couchbase/core/io/mcbp_buffer_pool.hxx +122 -0
  119. data/ext/couchbase/core/io/mcbp_command.hxx +150 -51
  120. data/ext/couchbase/core/io/mcbp_command_id.hxx +45 -0
  121. data/ext/couchbase/core/io/mcbp_message.cxx +9 -1
  122. data/ext/couchbase/core/io/mcbp_message.hxx +19 -0
  123. data/ext/couchbase/core/io/mcbp_output_queue.hxx +150 -0
  124. data/ext/couchbase/core/io/mcbp_parser.cxx +24 -7
  125. data/ext/couchbase/core/io/mcbp_session.cxx +309 -301
  126. data/ext/couchbase/core/io/mcbp_session.hxx +27 -18
  127. data/ext/couchbase/core/io/opaque_ring_table.hxx +145 -0
  128. data/ext/couchbase/core/io/retry_orchestrator.hxx +6 -5
  129. data/ext/couchbase/core/io/stream_deadline.hxx +50 -0
  130. data/ext/couchbase/core/io/streams.cxx +151 -21
  131. data/ext/couchbase/core/io/streams.hxx +48 -1
  132. data/ext/couchbase/core/logger/logger.cxx +36 -0
  133. data/ext/couchbase/core/logger/logger.hxx +46 -6
  134. data/ext/couchbase/core/logger/redaction.hxx +503 -0
  135. data/ext/couchbase/core/management/eventing_status.hxx +10 -0
  136. data/ext/couchbase/core/management/eventing_status_json.hxx +4 -0
  137. data/ext/couchbase/core/mcbp/big_endian.cxx +1 -1
  138. data/ext/couchbase/core/mcbp/codec.cxx +27 -18
  139. data/ext/couchbase/core/mcbp/operation_queue.cxx +2 -2
  140. data/ext/couchbase/core/mcbp/packet.hxx +2 -2
  141. data/ext/couchbase/core/mcbp/queue_request.cxx +23 -6
  142. data/ext/couchbase/core/mcbp/queue_request.hxx +21 -0
  143. data/ext/couchbase/core/meta/features.hxx +13 -0
  144. data/ext/couchbase/core/meta/version.cxx +6 -3
  145. data/ext/couchbase/core/metrics/logging_meter.cxx +5 -10
  146. data/ext/couchbase/core/metrics/meter_wrapper.cxx +63 -33
  147. data/ext/couchbase/core/metrics/meter_wrapper.hxx +55 -3
  148. data/ext/couchbase/core/operations/analytics_response_parsing.cxx +177 -0
  149. data/ext/couchbase/core/operations/analytics_response_parsing.hxx +55 -0
  150. data/ext/couchbase/core/operations/document_analytics.cxx +48 -130
  151. data/ext/couchbase/core/operations/document_analytics.hxx +14 -2
  152. data/ext/couchbase/core/operations/document_get_all_replicas.hxx +15 -7
  153. data/ext/couchbase/core/operations/document_get_any_replica.hxx +19 -8
  154. data/ext/couchbase/core/operations/document_get_projected.cxx +1 -1
  155. data/ext/couchbase/core/{impl/get_replica.cxx → operations/document_get_replica.cxx} +21 -3
  156. data/ext/couchbase/core/{impl/get_replica.hxx → operations/document_get_replica.hxx} +31 -4
  157. data/ext/couchbase/core/operations/document_lookup_in.cxx +10 -10
  158. data/ext/couchbase/core/operations/document_lookup_in_all_replicas.hxx +228 -198
  159. data/ext/couchbase/core/operations/document_lookup_in_any_replica.hxx +215 -217
  160. data/ext/couchbase/core/operations/document_query.cxx +108 -221
  161. data/ext/couchbase/core/operations/document_query.hxx +18 -4
  162. data/ext/couchbase/core/operations/document_search.cxx +52 -8
  163. data/ext/couchbase/core/operations/document_search.hxx +3 -0
  164. data/ext/couchbase/core/operations/document_view.cxx +3 -3
  165. data/ext/couchbase/core/operations/management/analytics_dataset_create.cxx +15 -8
  166. data/ext/couchbase/core/operations/management/analytics_dataset_create.hxx +4 -0
  167. data/ext/couchbase/core/operations/management/analytics_dataset_drop.cxx +11 -6
  168. data/ext/couchbase/core/operations/management/analytics_dataset_get_all.cxx +3 -2
  169. data/ext/couchbase/core/operations/management/analytics_dataverse_create.cxx +10 -5
  170. data/ext/couchbase/core/operations/management/analytics_dataverse_drop.cxx +10 -5
  171. data/ext/couchbase/core/operations/management/analytics_get_pending_mutations.cxx +2 -1
  172. data/ext/couchbase/core/operations/management/analytics_index_create.cxx +33 -8
  173. data/ext/couchbase/core/operations/management/analytics_index_drop.cxx +12 -7
  174. data/ext/couchbase/core/operations/management/analytics_index_get_all.cxx +3 -2
  175. data/ext/couchbase/core/operations/management/analytics_link_connect.cxx +11 -6
  176. data/ext/couchbase/core/operations/management/analytics_link_create.cxx +3 -3
  177. data/ext/couchbase/core/operations/management/analytics_link_disconnect.cxx +10 -4
  178. data/ext/couchbase/core/operations/management/analytics_link_drop.cxx +6 -4
  179. data/ext/couchbase/core/operations/management/analytics_link_get_all.cxx +4 -3
  180. data/ext/couchbase/core/operations/management/analytics_link_replace.cxx +3 -3
  181. data/ext/couchbase/core/operations/management/analytics_link_utils.hxx +1 -1
  182. data/ext/couchbase/core/operations/management/change_password.cxx +2 -0
  183. data/ext/couchbase/core/operations/management/collection_drop.cxx +5 -1
  184. data/ext/couchbase/core/operations/management/error_utils.cxx +59 -5
  185. data/ext/couchbase/core/operations/management/error_utils.hxx +29 -1
  186. data/ext/couchbase/core/operations/management/eventing_deploy_function.cxx +9 -3
  187. data/ext/couchbase/core/operations/management/eventing_drop_function.cxx +8 -3
  188. data/ext/couchbase/core/operations/management/eventing_get_all_functions.cxx +4 -2
  189. data/ext/couchbase/core/operations/management/eventing_get_function.cxx +5 -3
  190. data/ext/couchbase/core/operations/management/eventing_get_status.cxx +4 -2
  191. data/ext/couchbase/core/operations/management/eventing_pause_function.cxx +9 -3
  192. data/ext/couchbase/core/operations/management/eventing_resume_function.cxx +9 -3
  193. data/ext/couchbase/core/operations/management/eventing_undeploy_function.cxx +9 -3
  194. data/ext/couchbase/core/operations/management/eventing_upsert_function.cxx +9 -3
  195. data/ext/couchbase/core/operations/management/group_drop.cxx +3 -1
  196. data/ext/couchbase/core/operations/management/group_get.cxx +3 -1
  197. data/ext/couchbase/core/operations/management/group_upsert.cxx +11 -8
  198. data/ext/couchbase/core/operations/management/query_index_build.cxx +5 -2
  199. data/ext/couchbase/core/operations/management/query_index_create.cxx +9 -3
  200. data/ext/couchbase/core/operations/management/query_index_drop.cxx +11 -4
  201. data/ext/couchbase/core/operations/management/query_index_get_all.cxx +1 -1
  202. data/ext/couchbase/core/operations/management/query_index_get_all_deferred.cxx +1 -1
  203. data/ext/couchbase/core/operations/management/scope_drop.cxx +5 -1
  204. data/ext/couchbase/core/operations/management/scope_get_all.cxx +6 -1
  205. data/ext/couchbase/core/operations/management/search_get_stats.cxx +10 -0
  206. data/ext/couchbase/core/operations/management/search_index_analyze_document.cxx +3 -2
  207. data/ext/couchbase/core/operations/management/search_index_control_ingest.cxx +4 -3
  208. data/ext/couchbase/core/operations/management/search_index_control_plan_freeze.cxx +4 -3
  209. data/ext/couchbase/core/operations/management/search_index_control_query.cxx +4 -3
  210. data/ext/couchbase/core/operations/management/search_index_drop.cxx +2 -2
  211. data/ext/couchbase/core/operations/management/search_index_get.cxx +2 -2
  212. data/ext/couchbase/core/operations/management/search_index_get_documents_count.cxx +3 -2
  213. data/ext/couchbase/core/operations/management/search_index_get_stats.cxx +3 -1
  214. data/ext/couchbase/core/operations/management/search_index_upsert.cxx +2 -2
  215. data/ext/couchbase/core/operations/management/user_drop.cxx +3 -1
  216. data/ext/couchbase/core/operations/management/user_get.cxx +3 -1
  217. data/ext/couchbase/core/operations/management/user_upsert.cxx +12 -9
  218. data/ext/couchbase/core/operations/management/view_index_drop.cxx +1 -1
  219. data/ext/couchbase/core/operations/management/view_index_get.cxx +1 -1
  220. data/ext/couchbase/core/operations/management/view_index_upsert.cxx +1 -1
  221. data/ext/couchbase/core/operations/operation_traits.hxx +18 -0
  222. data/ext/couchbase/core/operations/query_response_parsing.cxx +211 -0
  223. data/ext/couchbase/core/operations/query_response_parsing.hxx +60 -0
  224. data/ext/couchbase/core/operations_fwd.hxx +3 -3
  225. data/ext/couchbase/core/origin.cxx +54 -26
  226. data/ext/couchbase/core/origin.hxx +12 -0
  227. data/ext/couchbase/core/orphan_reporter.cxx +2 -1
  228. data/ext/couchbase/core/platform/random.cc +7 -7
  229. data/ext/couchbase/core/protocol/client_request.cxx +2 -2
  230. data/ext/couchbase/core/protocol/client_request.hxx +38 -21
  231. data/ext/couchbase/core/protocol/client_response.cxx +11 -3
  232. data/ext/couchbase/core/protocol/client_response.hxx +27 -6
  233. data/ext/couchbase/core/protocol/cmd_append.cxx +2 -2
  234. data/ext/couchbase/core/protocol/cmd_cluster_map_change_notification.cxx +2 -2
  235. data/ext/couchbase/core/protocol/cmd_decrement.cxx +6 -6
  236. data/ext/couchbase/core/protocol/cmd_get.cxx +1 -1
  237. data/ext/couchbase/core/protocol/cmd_get_and_lock.cxx +2 -2
  238. data/ext/couchbase/core/protocol/cmd_get_and_touch.cxx +2 -2
  239. data/ext/couchbase/core/protocol/cmd_get_cluster_config.cxx +6 -4
  240. data/ext/couchbase/core/protocol/cmd_get_collection_id.cxx +2 -2
  241. data/ext/couchbase/core/protocol/cmd_get_error_map.cxx +1 -1
  242. data/ext/couchbase/core/protocol/cmd_get_meta.cxx +4 -4
  243. data/ext/couchbase/core/protocol/cmd_get_replica.cxx +1 -1
  244. data/ext/couchbase/core/protocol/cmd_hello.cxx +1 -1
  245. data/ext/couchbase/core/protocol/cmd_increment.cxx +6 -6
  246. data/ext/couchbase/core/protocol/cmd_insert.cxx +4 -4
  247. data/ext/couchbase/core/protocol/cmd_lookup_in.cxx +4 -3
  248. data/ext/couchbase/core/protocol/cmd_lookup_in_replica.cxx +4 -3
  249. data/ext/couchbase/core/protocol/cmd_mutate_in.cxx +10 -9
  250. data/ext/couchbase/core/protocol/cmd_mutate_in.hxx +10 -10
  251. data/ext/couchbase/core/protocol/cmd_observe_seqno.cxx +7 -7
  252. data/ext/couchbase/core/protocol/cmd_prepend.cxx +2 -2
  253. data/ext/couchbase/core/protocol/cmd_remove.cxx +2 -2
  254. data/ext/couchbase/core/protocol/cmd_replace.cxx +4 -4
  255. data/ext/couchbase/core/protocol/cmd_touch.cxx +1 -1
  256. data/ext/couchbase/core/protocol/cmd_upsert.cxx +4 -4
  257. data/ext/couchbase/core/protocol/datatype.hxx +118 -1
  258. data/ext/couchbase/core/protocol/frame_info_utils.cxx +1 -1
  259. data/ext/couchbase/core/protocol/magic.hxx +4 -0
  260. data/ext/couchbase/core/protocol/magic_fmt.hxx +3 -0
  261. data/ext/couchbase/core/protocol/server_request.hxx +3 -1
  262. data/ext/couchbase/core/protocol/status.cxx +1 -1
  263. data/ext/couchbase/core/protostellar/analytics_converter.hxx +171 -0
  264. data/ext/couchbase/core/protostellar/bucket_admin_converter.hxx +301 -0
  265. data/ext/couchbase/core/protostellar/collection_admin_converter.hxx +92 -0
  266. data/ext/couchbase/core/protostellar/component.cxx +2445 -0
  267. data/ext/couchbase/core/protostellar/component.hxx +391 -0
  268. data/ext/couchbase/core/{columnar/bootstrap_notification_subscriber.hxx → protostellar/compression_settings.hxx} +11 -10
  269. data/ext/couchbase/core/protostellar/credentials.cxx +233 -0
  270. data/ext/couchbase/core/protostellar/credentials.hxx +88 -0
  271. data/ext/couchbase/core/protostellar/dispatcher.cxx +57 -0
  272. data/ext/couchbase/core/protostellar/dispatcher.hxx +482 -0
  273. data/ext/couchbase/core/protostellar/error_utils.cxx +293 -0
  274. data/ext/couchbase/core/protostellar/error_utils.hxx +97 -0
  275. data/ext/couchbase/core/protostellar/json_payload.hxx +49 -0
  276. data/ext/couchbase/core/protostellar/kv_converter.cxx +70 -0
  277. data/ext/couchbase/core/protostellar/kv_converter.hxx +600 -0
  278. data/ext/couchbase/core/protostellar/query_converter.hxx +265 -0
  279. data/ext/couchbase/core/protostellar/query_index_admin_converter.hxx +291 -0
  280. data/ext/couchbase/core/protostellar/query_proto.hxx +73 -0
  281. data/ext/couchbase/core/protostellar/search_converter.hxx +244 -0
  282. data/ext/couchbase/core/protostellar/search_index_admin_converter.hxx +156 -0
  283. data/ext/couchbase/core/protostellar/view_converter.hxx +158 -0
  284. data/ext/couchbase/core/query_stream.cxx +447 -0
  285. data/ext/couchbase/core/query_stream.hxx +141 -0
  286. data/ext/couchbase/core/query_stream_component.cxx +243 -0
  287. data/ext/couchbase/core/query_stream_component.hxx +68 -0
  288. data/ext/couchbase/core/range_scan_load_balancer.cxx +15 -8
  289. data/ext/couchbase/core/range_scan_orchestrator.cxx +15 -3
  290. data/ext/couchbase/core/response_handler.hxx +41 -4
  291. data/ext/couchbase/core/row_streamer.cxx +332 -46
  292. data/ext/couchbase/core/row_streamer.hxx +31 -1
  293. data/ext/couchbase/core/row_streamer_options.hxx +55 -0
  294. data/ext/couchbase/core/sasl/client.h +3 -0
  295. data/ext/couchbase/core/search_scoring.hxx +66 -0
  296. data/ext/couchbase/core/stream_error_details.hxx +69 -0
  297. data/ext/couchbase/core/timeout_defaults.hxx +0 -1
  298. data/ext/couchbase/core/topology/capabilities.hxx +6 -0
  299. data/ext/couchbase/core/topology/capabilities_fmt.hxx +3 -0
  300. data/ext/couchbase/core/topology/configuration.cxx +21 -6
  301. data/ext/couchbase/core/topology/configuration.hxx +29 -8
  302. data/ext/couchbase/core/topology/configuration_json.hxx +3 -0
  303. data/ext/couchbase/core/topology/error_map_json.hxx +3 -1
  304. data/ext/couchbase/core/tracing/threshold_logging_tracer.cxx +70 -4
  305. data/ext/couchbase/core/tracing/threshold_logging_tracer.hxx +10 -0
  306. data/ext/couchbase/core/transactions/active_transaction_record.cxx +0 -34
  307. data/ext/couchbase/core/transactions/attempt_context_impl.cxx +108 -63
  308. data/ext/couchbase/core/transactions/attempt_context_impl.hxx +19 -2
  309. data/ext/couchbase/core/transactions/error_list.hxx +2 -2
  310. data/ext/couchbase/core/transactions/get_multi_fetch.hxx +160 -0
  311. data/ext/couchbase/core/transactions/get_multi_orchestrator.cxx +326 -125
  312. data/ext/couchbase/core/transactions/get_multi_transaction_id.hxx +62 -0
  313. data/ext/couchbase/core/transactions/internal/atr_entry.hxx +22 -2
  314. data/ext/couchbase/core/transactions/internal/transaction_context.hxx +2 -2
  315. data/ext/couchbase/core/transactions/internal/transactions_cleanup.hxx +23 -1
  316. data/ext/couchbase/core/transactions/internal/utils.hxx +50 -14
  317. data/ext/couchbase/core/transactions/staged_mutation.cxx +23 -18
  318. data/ext/couchbase/core/transactions/staged_mutation.hxx +15 -4
  319. data/ext/couchbase/core/transactions/transaction_context.cxx +55 -40
  320. data/ext/couchbase/core/transactions/transaction_get_result.hxx +16 -0
  321. data/ext/couchbase/core/transactions/transactions.cxx +3 -55
  322. data/ext/couchbase/core/transactions/transactions_cleanup.cxx +165 -72
  323. data/ext/couchbase/core/transactions/transactions_config.cxx +0 -11
  324. data/ext/couchbase/core/transactions/utils.cxx +10 -0
  325. data/ext/couchbase/core/transactions/waitable_op_list.hxx +2 -2
  326. data/ext/couchbase/core/transactions.hxx +9 -9
  327. data/ext/couchbase/core/utils/byteswap.hxx +166 -9
  328. data/ext/couchbase/core/utils/connection_string.cxx +50 -85
  329. data/ext/couchbase/core/utils/connection_string.hxx +15 -0
  330. data/ext/couchbase/core/{columnar/backoff_calculator.hxx → utils/deadline.hxx} +13 -13
  331. data/ext/couchbase/core/utils/duration_parser.cxx +1 -1
  332. data/ext/couchbase/core/utils/json_streaming_lexer.cxx +48 -5
  333. data/ext/couchbase/core/utils/json_streaming_lexer.hxx +4 -1
  334. data/ext/couchbase/core/utils/movable_function.hxx +301 -52
  335. data/ext/couchbase/core/utils/name_codec.hxx +154 -11
  336. data/ext/couchbase/core/{columnar/retry_info.hxx → utils/process_id.hxx} +18 -13
  337. data/ext/couchbase/core/utils/url_codec.cxx +12 -51
  338. data/ext/couchbase/core/websocket_codec.cxx +3 -29
  339. data/ext/couchbase/couchbase/analytics_options.hxx +21 -10
  340. data/ext/couchbase/couchbase/analytics_row.hxx +105 -0
  341. data/ext/couchbase/couchbase/analytics_stream_result.hxx +266 -0
  342. data/ext/couchbase/couchbase/boolean_query.hxx +3 -3
  343. data/ext/couchbase/couchbase/bucket.hxx +57 -0
  344. data/ext/couchbase/couchbase/cluster.hxx +183 -5
  345. data/ext/couchbase/{core/columnar/security_options.hxx → couchbase/cluster_state.hxx} +18 -14
  346. data/ext/couchbase/couchbase/codec/lenient_json_transcoder.hxx +69 -0
  347. data/ext/couchbase/couchbase/collection.hxx +234 -14
  348. data/ext/couchbase/couchbase/common_durability_options.hxx +18 -3
  349. data/ext/couchbase/couchbase/conjunction_query.hxx +1 -1
  350. data/ext/couchbase/couchbase/create_dataset_analytics_options.hxx +6 -1
  351. data/ext/couchbase/couchbase/date_range_query.hxx +2 -2
  352. data/ext/couchbase/couchbase/decrement_options.hxx +1 -1
  353. data/ext/couchbase/couchbase/disjunction_query.hxx +1 -1
  354. data/ext/couchbase/couchbase/error.hxx +32 -0
  355. data/ext/couchbase/couchbase/error_codes.hxx +32 -0
  356. data/ext/couchbase/couchbase/geo_bounding_box_query.hxx +1 -1
  357. data/ext/couchbase/couchbase/geo_distance_query.hxx +1 -1
  358. data/ext/couchbase/couchbase/geo_polygon_query.hxx +1 -1
  359. data/ext/couchbase/couchbase/get_replica_options.hxx +68 -0
  360. data/ext/couchbase/couchbase/get_replica_result.hxx +5 -3
  361. data/ext/couchbase/couchbase/get_replica_strategy.hxx +161 -0
  362. data/ext/couchbase/couchbase/get_result.hxx +1 -0
  363. data/ext/couchbase/couchbase/increment_options.hxx +1 -1
  364. data/ext/couchbase/couchbase/insert_options.hxx +1 -1
  365. data/ext/couchbase/couchbase/match_phrase_query.hxx +1 -1
  366. data/ext/couchbase/couchbase/match_query.hxx +1 -1
  367. data/ext/couchbase/couchbase/mutate_in_options.hxx +3 -3
  368. data/ext/couchbase/couchbase/network_options.hxx +1 -1
  369. data/ext/couchbase/couchbase/node_id.hxx +42 -14
  370. data/ext/couchbase/couchbase/node_id_for_options.hxx +7 -1
  371. data/ext/couchbase/couchbase/node_ids_options.hxx +8 -2
  372. data/ext/couchbase/couchbase/numeric_range_query.hxx +1 -1
  373. data/ext/couchbase/couchbase/phrase_query.hxx +1 -1
  374. data/ext/couchbase/couchbase/prefix_query.hxx +1 -1
  375. data/ext/couchbase/couchbase/query_options.hxx +23 -12
  376. data/ext/couchbase/couchbase/query_row.hxx +111 -0
  377. data/ext/couchbase/couchbase/query_stream_result.hxx +302 -0
  378. data/ext/couchbase/couchbase/query_string_query.hxx +3 -3
  379. data/ext/couchbase/couchbase/regexp_query.hxx +1 -1
  380. data/ext/couchbase/couchbase/replace_options.hxx +3 -3
  381. data/ext/couchbase/couchbase/result.hxx +14 -3
  382. data/ext/couchbase/couchbase/scan_result_item.hxx +1 -0
  383. data/ext/couchbase/couchbase/scope.hxx +85 -2
  384. data/ext/couchbase/couchbase/search_options.hxx +58 -8
  385. data/ext/couchbase/couchbase/search_scoring.hxx +94 -0
  386. data/ext/couchbase/couchbase/search_scoring_none.hxx +51 -0
  387. data/ext/couchbase/couchbase/search_scoring_reciprocal_rank_fusion.hxx +88 -0
  388. data/ext/couchbase/couchbase/search_scoring_relative_score_fusion.hxx +70 -0
  389. data/ext/couchbase/couchbase/term_range_query.hxx +1 -1
  390. data/ext/couchbase/couchbase/tracing/request_span.hxx +46 -0
  391. data/ext/couchbase/couchbase/transactions/transaction_get_multi_replicas_from_preferred_server_group_result.hxx +23 -21
  392. data/ext/couchbase/couchbase/transactions/transaction_get_multi_result.hxx +24 -19
  393. data/ext/couchbase/couchbase/transactions/transaction_get_result.hxx +17 -3
  394. data/ext/couchbase/couchbase/transactions/transactions_config.hxx +4 -2
  395. data/ext/couchbase/couchbase/transactions.hxx +27 -18
  396. data/ext/couchbase/couchbase/upsert_options.hxx +3 -3
  397. data/ext/couchbase/couchbase/vector_query.hxx +1 -1
  398. data/ext/couchbase/couchbase/wait_until_ready_options.hxx +111 -0
  399. data/ext/couchbase/couchbase/wildcard_query.hxx +1 -1
  400. data/ext/extconf.rb +15 -10
  401. data/ext/rcb_analytics.cxx +82 -102
  402. data/ext/rcb_backend.cxx +42 -36
  403. data/ext/rcb_backend.hxx +2 -0
  404. data/ext/rcb_buckets.cxx +47 -59
  405. data/ext/rcb_collections.cxx +42 -52
  406. data/ext/rcb_crud.cxx +345 -224
  407. data/ext/rcb_diagnostics.cxx +117 -27
  408. data/ext/rcb_exceptions.cxx +152 -99
  409. data/ext/rcb_exceptions.hxx +25 -0
  410. data/ext/rcb_extras.cxx +18 -19
  411. data/ext/rcb_hdr_histogram.cxx +19 -4
  412. data/ext/rcb_logger.cxx +202 -27
  413. data/ext/rcb_logger.hxx +12 -0
  414. data/ext/rcb_multi.cxx +40 -40
  415. data/ext/rcb_observability.cxx +7 -5
  416. data/ext/rcb_query.cxx +99 -142
  417. data/ext/rcb_range_scan.cxx +41 -35
  418. data/ext/rcb_search.cxx +98 -99
  419. data/ext/rcb_users.cxx +36 -56
  420. data/ext/rcb_utils.cxx +100 -37
  421. data/ext/rcb_utils.hxx +287 -33
  422. data/ext/rcb_version.cxx +1 -1
  423. data/ext/rcb_views.cxx +55 -51
  424. data/lib/active_support/cache/couchbase_store.rb +77 -12
  425. data/lib/couchbase/bucket.rb +20 -0
  426. data/lib/couchbase/cluster.rb +17 -0
  427. data/lib/couchbase/collection.rb +36 -1
  428. data/lib/couchbase/errors.rb +18 -0
  429. data/lib/couchbase/fork_hooks.rb +16 -7
  430. data/lib/couchbase/get_replica_strategy.rb +89 -0
  431. data/lib/couchbase/logger.rb +1 -1
  432. data/lib/couchbase/management/analytics_index_manager.rb +15 -15
  433. data/lib/couchbase/management/bucket_manager.rb +6 -6
  434. data/lib/couchbase/management/collection_manager.rb +6 -6
  435. data/lib/couchbase/management/query_index_manager.rb +7 -7
  436. data/lib/couchbase/management/search_index_manager.rb +13 -13
  437. data/lib/couchbase/management/user_manager.rb +10 -10
  438. data/lib/couchbase/management/view_index_manager.rb +5 -5
  439. data/lib/couchbase/options.rb +174 -42
  440. data/lib/couchbase/protostellar/bucket.rb +4 -0
  441. data/lib/couchbase/protostellar/cluster.rb +4 -0
  442. data/lib/couchbase/protostellar/collection.rb +4 -0
  443. data/lib/couchbase/protostellar/error_handling.rb +2 -1
  444. data/lib/couchbase/protostellar/request.rb +1 -0
  445. data/lib/couchbase/protostellar/request_generator/kv.rb +16 -2
  446. data/lib/couchbase/protostellar/request_generator/search.rb +17 -1
  447. data/lib/couchbase/protostellar/retry/orchestrator.rb +3 -2
  448. data/lib/couchbase/protostellar/retry/strategies/fail_fast.rb +33 -0
  449. data/lib/couchbase/protostellar/retry/strategies.rb +1 -0
  450. data/lib/couchbase/search_options.rb +95 -6
  451. data/lib/couchbase/version.rb +1 -1
  452. metadata +91 -33
  453. data/ext/0001-fix-build-for-mingw-w64-ucrt-x86_64-toolchain.patch +0 -40
  454. data/ext/couchbase/core/columnar/agent.hxx +0 -71
  455. data/ext/couchbase/core/columnar/agent_config.hxx +0 -59
  456. data/ext/couchbase/core/columnar/background_bootstrap_listener.hxx +0 -42
  457. data/ext/couchbase/core/columnar/backoff_calculator.cxx +0 -62
  458. data/ext/couchbase/core/columnar/columnar_agent.cxx +0 -147
  459. data/ext/couchbase/core/columnar/columnar_agent_config.cxx +0 -48
  460. data/ext/couchbase/core/columnar/columnar_error.cxx +0 -51
  461. data/ext/couchbase/core/columnar/database_management_options.hxx +0 -56
  462. data/ext/couchbase/core/columnar/error.hxx +0 -51
  463. data/ext/couchbase/core/columnar/error_codes.cxx +0 -107
  464. data/ext/couchbase/core/columnar/error_codes.hxx +0 -75
  465. data/ext/couchbase/core/columnar/management_component.cxx +0 -300
  466. data/ext/couchbase/core/columnar/management_component.hxx +0 -56
  467. data/ext/couchbase/core/columnar/query_component.cxx +0 -523
  468. data/ext/couchbase/core/columnar/query_component.hxx +0 -57
  469. data/ext/couchbase/core/columnar/query_options.hxx +0 -59
  470. data/ext/couchbase/core/columnar/query_result.cxx +0 -130
  471. data/ext/couchbase/core/columnar/query_result.hxx +0 -78
  472. data/ext/couchbase/core/io/config_tracker.cxx +0 -687
  473. data/ext/couchbase/core/io/config_tracker.hxx +0 -123
@@ -17,6 +17,16 @@
17
17
 
18
18
  #include <couchbase/build_config.hxx>
19
19
 
20
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
21
+ #include "core/protostellar/component.hxx"
22
+ #include "core/protostellar/credentials.hxx"
23
+ #include "core/protostellar/error_utils.hxx"
24
+
25
+ #include <couchbase/retry_strategy.hxx>
26
+
27
+ #include <asio/steady_timer.hpp>
28
+ #endif
29
+
20
30
  #define COUCHBASE_CXX_CLIENT_IGNORE_CORE_DEPRECATIONS
21
31
  #include "cluster.hxx"
22
32
 
@@ -24,23 +34,25 @@
24
34
  #undef COUCHBASE_CXX_CLIENT_IGNORE_CORE_DEPRECATIONS
25
35
 
26
36
  #include "bucket.hxx"
37
+ #include "capella.hxx"
27
38
  #include "capella_ca.hxx"
39
+ #include "cluster_label_listener.hxx"
40
+ #include "core/analytics_stream_component.hxx"
28
41
  #include "core/app_telemetry_meter.hxx"
29
42
  #include "core/app_telemetry_reporter.hxx"
43
+ #include "core/core_sdk_shim.hxx"
30
44
  #include "core/diagnostics.hxx"
31
45
  #include "core/error.hxx"
32
- #include "core/impl/get_replica.hxx"
46
+ #include "core/http_component.hxx"
33
47
  #include "core/impl/lookup_in_replica.hxx"
34
48
  #include "core/impl/observe_seqno.hxx"
35
49
  #include "core/io/http_command.hxx"
36
50
  #include "core/io/http_message.hxx"
37
51
  #include "core/io/http_session_manager.hxx"
38
52
  #include "core/io/mcbp_session.hxx"
39
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
40
- #include "core/io/config_tracker.hxx"
41
- #endif
42
- #include "cluster_label_listener.hxx"
53
+ #include "core/io/mcbp_traits.hxx"
43
54
  #include "core/logger/logger.hxx"
55
+ #include "core/logger/redaction.hxx"
44
56
  #include "core/management/analytics_link_azure_blob_external.hxx"
45
57
  #include "core/management/analytics_link_couchbase_remote.hxx"
46
58
  #include "core/management/analytics_link_s3_external.hxx"
@@ -59,6 +71,7 @@
59
71
  #include "core/operations/document_get_and_touch.hxx"
60
72
  #include "core/operations/document_get_any_replica.hxx"
61
73
  #include "core/operations/document_get_projected.hxx"
74
+ #include "core/operations/document_get_replica.hxx"
62
75
  #include "core/operations/document_increment.hxx"
63
76
  #include "core/operations/document_insert.hxx"
64
77
  #include "core/operations/document_lookup_in.hxx"
@@ -149,13 +162,14 @@
149
162
  #include "core/orphan_reporter.hxx"
150
163
  #include "core/platform/uuid.h"
151
164
  #include "core/protocol/hello_feature.hxx"
165
+ #include "core/query_stream_component.hxx"
152
166
  #include "core/service_type.hxx"
153
167
  #include "core/tls_verify_mode.hxx"
154
168
  #include "core/topology/capabilities.hxx"
155
169
  #include "core/tracing/noop_tracer.hxx"
156
170
  #include "core/tracing/threshold_logging_tracer.hxx"
157
171
  #include "core/tracing/tracer_wrapper.hxx"
158
- #include "core/utils/join_strings.hxx"
172
+ #include "core/utils/deadline.hxx"
159
173
  #include "core/utils/movable_function.hxx"
160
174
  #include "crud_component.hxx"
161
175
  #include "dispatcher.hxx"
@@ -174,9 +188,11 @@
174
188
  #include <asio/post.hpp>
175
189
  #include <asio/ssl/verify_mode.hpp>
176
190
 
191
+ #include <algorithm>
177
192
  #include <atomic>
178
193
  #include <chrono>
179
194
  #include <cstring>
195
+ #include <list>
180
196
  #include <map>
181
197
  #include <memory>
182
198
  #include <mutex>
@@ -202,6 +218,32 @@ class ping_collector_impl
202
218
  std::mutex mutex_{};
203
219
 
204
220
  public:
221
+ /**
222
+ * Counted like a reporter, so the ping cannot complete while pings are still being dispatched.
223
+ * This prevents erroneously returning early when the in-flight pings momentarily drop to 0.
224
+ */
225
+ class dispatch_scope
226
+ {
227
+ public:
228
+ explicit dispatch_scope(std::shared_ptr<ping_collector_impl> collector)
229
+ : collector_{ std::move(collector) }
230
+ {
231
+ }
232
+
233
+ dispatch_scope(const dispatch_scope&) = delete;
234
+ dispatch_scope(dispatch_scope&&) = delete;
235
+ auto operator=(const dispatch_scope&) -> dispatch_scope& = delete;
236
+ auto operator=(dispatch_scope&&) -> dispatch_scope& = delete;
237
+
238
+ ~dispatch_scope()
239
+ {
240
+ collector_->release();
241
+ }
242
+
243
+ private:
244
+ std::shared_ptr<ping_collector_impl> collector_;
245
+ };
246
+
205
247
  ping_collector_impl(std::string report_id,
206
248
  utils::movable_function<void(diag::ping_result)>&& handler)
207
249
  : res_{ std::move(report_id), meta::sdk_id() }
@@ -226,11 +268,11 @@ public:
226
268
 
227
269
  void report(diag::endpoint_ping_info&& info) override
228
270
  {
229
- const std::scoped_lock lock(mutex_);
230
- res_.services[info.type].emplace_back(std::move(info));
231
- if (--expected_ == 0) {
232
- invoke_handler();
271
+ {
272
+ const std::scoped_lock lock(mutex_);
273
+ res_.services[info.type].emplace_back(std::move(info));
233
274
  }
275
+ release();
234
276
  }
235
277
 
236
278
  auto build_reporter() -> std::shared_ptr<diag::ping_reporter> override
@@ -239,17 +281,115 @@ public:
239
281
  return shared_from_this();
240
282
  }
241
283
 
284
+ [[nodiscard]] auto begin_dispatch() -> std::shared_ptr<dispatch_scope>
285
+ {
286
+ ++expected_;
287
+ return std::make_shared<dispatch_scope>(shared_from_this());
288
+ }
289
+
290
+ private:
291
+ void release()
292
+ {
293
+ if (--expected_ == 0) {
294
+ invoke_handler();
295
+ }
296
+ }
297
+
242
298
  void invoke_handler()
243
299
  {
244
- if (handler_ != nullptr) {
245
- handler_(std::move(res_));
300
+ utils::movable_function<void(diag::ping_result)> handler{};
301
+ diag::ping_result res{};
302
+ {
303
+ const std::scoped_lock lock(mutex_);
304
+ if (handler_ == nullptr) {
305
+ return;
306
+ }
307
+ handler = std::move(handler_);
246
308
  handler_ = nullptr;
309
+ res = std::move(res_);
247
310
  }
311
+ handler(std::move(res));
248
312
  }
249
313
  };
250
314
 
251
315
  namespace
252
316
  {
317
+ // A wait with an optional deadline, held by cluster_impl::defer_until_open() or
318
+ // cluster_impl::wait_until(). complete() runs on the IO thread; the destructor runs `resume` on
319
+ // whichever thread drops the last reference.
320
+ struct deferred_request {
321
+ utils::movable_function<void(std::error_code)> resume{};
322
+ std::unique_ptr<asio::steady_timer> deadline{};
323
+ std::atomic_bool completed{ false };
324
+ // Where the entry sits in cluster_impl::deferred_until_open_, while it is there. Guarded by
325
+ // deferred_until_open_mutex_.
326
+ std::optional<std::list<std::shared_ptr<deferred_request>>::iterator> queued{};
327
+
328
+ void complete(std::error_code ec)
329
+ {
330
+ if (completed.exchange(true)) {
331
+ return;
332
+ }
333
+ if (deadline) {
334
+ // An armed timer keeps the entry and the io_context's work alive until it expires.
335
+ deadline->cancel();
336
+ // The deadline can pass while the IO thread is busy, before the timer callback runs. A
337
+ // request resumed after it would be dispatched with remaining_until()'s floor.
338
+ if (!ec && deadline->expiry() <= std::chrono::steady_clock::now()) {
339
+ ec = errc::common::unambiguous_timeout;
340
+ }
341
+ }
342
+ auto handler = std::move(resume);
343
+ handler(ec);
344
+ }
345
+
346
+ // complete() for a thread other than the IO thread: the timer is not touched, and is left to
347
+ // expire or to be destroyed with its io_context.
348
+ void complete_off_io_thread(std::error_code ec)
349
+ {
350
+ if (completed.exchange(true)) {
351
+ return;
352
+ }
353
+ auto handler = std::move(resume);
354
+ if (handler) {
355
+ handler(ec);
356
+ }
357
+ }
358
+
359
+ deferred_request() = default;
360
+ deferred_request(const deferred_request&) = delete;
361
+ deferred_request(deferred_request&&) = delete;
362
+ auto operator=(const deferred_request&) -> deferred_request& = delete;
363
+ auto operator=(deferred_request&&) -> deferred_request& = delete;
364
+
365
+ // A stopped io_context drops the timer wait and any completion posted to it. The last reference
366
+ // then goes here, and the request completes with cluster_closed rather than never. The timer is
367
+ // not touched: its io_context may be shutting down.
368
+ ~deferred_request()
369
+ {
370
+ try {
371
+ complete_off_io_thread(errc::network::cluster_closed);
372
+ } catch (...) { // NOLINT(bugprone-empty-catch)
373
+ }
374
+ }
375
+ };
376
+
377
+ using utils::remaining_until;
378
+
379
+ // A durable mutation gets at least this long, counted from when it is issued.
380
+ constexpr std::chrono::milliseconds durability_timeout_floor{ 1'500 };
381
+
382
+ // Whether T has a client_context_id member: a request that can carry one, or a context that reports
383
+ // it.
384
+ template<typename T, typename = void>
385
+ struct has_client_context_id : std::false_type {
386
+ };
387
+
388
+ template<typename T>
389
+ struct has_client_context_id<T, std::void_t<decltype(std::declval<T&>().client_context_id)>>
390
+ : std::true_type {
391
+ };
392
+
253
393
  template<typename Request>
254
394
  auto
255
395
  is_feature_supported(const Request& /* request */,
@@ -270,6 +410,10 @@ is_feature_supported(const operations::search_request& request,
270
410
  if (request.vector_search && !capabilities.supports_vector_search()) {
271
411
  return false;
272
412
  }
413
+ // "none" is not behind the score fusion capability
414
+ if (is_score_fusion(request.scoring) && !capabilities.supports_score_fusion()) {
415
+ return false;
416
+ }
273
417
 
274
418
  return true;
275
419
  }
@@ -279,6 +423,7 @@ is_feature_supported(const operations::analytics_request& /*request*/,
279
423
  const configuration_capabilities& capabilities,
280
424
  const cluster_options& options) -> bool
281
425
  {
426
+ // cppcheck-suppress knownConditionTrueFalse
282
427
  if (operations::analytics_request::allow_enterprise_analytics) {
283
428
  return true;
284
429
  }
@@ -292,27 +437,75 @@ is_feature_supported(const operations::management::search_index_upsert_request&
292
437
  {
293
438
  return !request.index.is_vector_index() || capabilities.supports_vector_search();
294
439
  }
440
+
441
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
442
+ // Extract the error_code from a response's error context. KV responses carry a
443
+ // key_value_error_context (ec() accessor); the query/analytics/search/view/management responses
444
+ // carry a plain error_context::* struct with an `ec` field.
445
+ inline auto
446
+ protostellar_response_ec(const key_value_error_context& ctx) -> std::error_code
447
+ {
448
+ return ctx.ec();
449
+ }
450
+ template<typename Context>
451
+ inline auto
452
+ protostellar_response_ec(const Context& ctx) -> std::error_code
453
+ {
454
+ return ctx.ec;
455
+ }
456
+
457
+ // The cluster's configured timeouts, in the form the component and the retry loop both read them
458
+ // from. Assigned per field rather than brace-initialised positionally: every member has the same
459
+ // type, so a mis-ordering is not a compile error, and an omitted one silently takes the struct's
460
+ // own default instead of the value the caller configured.
461
+ inline auto
462
+ make_component_timeouts(const cluster_options& options) -> protostellar::component_timeouts
463
+ {
464
+ protostellar::component_timeouts timeouts{};
465
+ timeouts.key_value = options.key_value_timeout;
466
+ timeouts.key_value_durable = options.key_value_durable_timeout;
467
+ timeouts.query = options.query_timeout;
468
+ timeouts.analytics = options.analytics_timeout;
469
+ timeouts.search = options.search_timeout;
470
+ timeouts.view = options.view_timeout;
471
+ timeouts.management = options.management_timeout;
472
+ return timeouts;
473
+ }
474
+ #endif
295
475
  } // namespace
296
476
 
477
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
478
+ // Whether the couchbase2 component can carry this request, answered by asking whether it has an
479
+ // execute() overload for it.
480
+ //
481
+ // The alternative -- a hand-maintained list of request types -- has to agree with the overload set
482
+ // and nothing checks that it does. A type in the list with no overload fails to compile, so that
483
+ // half is safe; an overload with no entry in the list is silently unreachable, and that is how
484
+ // query_index_build_deferred_request came to be wired to a gateway RPC that no caller could
485
+ // reach. Detecting the overload makes the list *be* the overload set.
486
+ template<typename Request, typename = void>
487
+ struct component_routes : std::false_type {
488
+ };
489
+
490
+ template<typename Request>
491
+ struct component_routes<
492
+ Request,
493
+ std::void_t<decltype(std::declval<protostellar::component&>().execute(
494
+ std::declval<Request>(),
495
+ std::declval<utils::movable_function<void(typename Request::response_type)>>()))>>
496
+ : std::true_type {
497
+ };
498
+ #endif
499
+
297
500
  class cluster_impl : public std::enable_shared_from_this<cluster_impl>
298
501
  {
299
502
  public:
300
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
301
- explicit cluster_impl(asio::io_context& ctx)
302
- : ctx_(ctx)
303
- , work_(asio::make_work_guard(ctx_))
304
- , session_manager_(std::make_shared<io::http_session_manager>(id_, ctx_, tls_, origin_))
305
- , retry_backoff_(ctx_)
306
- {
307
- }
308
- #else
309
503
  explicit cluster_impl(asio::io_context& ctx)
310
504
  : ctx_(ctx)
311
505
  , work_(asio::make_work_guard(ctx_))
312
506
  , session_manager_(std::make_shared<io::http_session_manager>(id_, ctx_, tls_, origin_))
313
507
  {
314
508
  }
315
- #endif
316
509
 
317
510
  auto io_context() -> asio::io_context&
318
511
  {
@@ -334,7 +527,21 @@ public:
334
527
  tls_options |= asio::ssl::context::no_tlsv1_2; // published: 2008, still in use
335
528
  }
336
529
  ctx->set_options(tls_options);
337
- switch (origin_.options().tls_verify) {
530
+ const auto requested_verify = origin_.options().tls_verify;
531
+ const auto effective_verify = effective_tls_verify_mode(requested_verify, has_capella_host);
532
+ if (requested_verify == tls_verify_mode::none && effective_verify == tls_verify_mode::peer) {
533
+ CB_LOG_WARNING(
534
+ "[{}]: TLS certificate verification cannot be disabled when connecting to Couchbase "
535
+ "Capella; enforcing peer verification (ignoring tls_verify=none).",
536
+ id_);
537
+ } else if (effective_verify == tls_verify_mode::none) {
538
+ CB_LOG_WARNING(
539
+ "[{}]: TLS certificate verification is DISABLED (tls_verify=none). The server's "
540
+ "identity will NOT be checked and the connection is vulnerable to interception. This "
541
+ "is insecure and must not be used in production.",
542
+ id_);
543
+ }
544
+ switch (effective_verify) {
338
545
  case tls_verify_mode::none:
339
546
  ctx->set_verify_mode(asio::ssl::verify_none);
340
547
  break;
@@ -465,27 +672,58 @@ public:
465
672
  return handler(errc::common::invalid_argument);
466
673
  }
467
674
 
468
- origin_ = std::move(origin);
675
+ {
676
+ const std::scoped_lock lock(origin_mutex_);
677
+ origin_ = std::move(origin);
678
+ }
679
+ apply_log_redaction();
469
680
  CB_LOG_DEBUG(R"(open cluster, id: "{}", core version: "{}", connection string: {}, {})",
470
681
  id_,
471
682
  couchbase::core::meta::sdk_semver(),
472
- origin_.connection_string(),
473
- origin_.to_json());
683
+ logger::user_data(origin_.connection_string()),
684
+ logger::user_data(origin_.to_json()));
685
+ // The scheme is recognised in every build; only the ability to serve it is conditional. The
686
+ // parser stays identical either way, so no API or ABI depends on the build mode -- a build
687
+ // without couchbase2 support answers with a named error instead of attempting an MCBP handshake
688
+ // against the gateway's gRPC port, which surfaced as an unrelated bootstrap timeout.
689
+ if (origin_.uses_protostellar()) {
690
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
691
+ return open_protostellar(std::move(handler));
692
+ #else
693
+ CB_LOG_ERROR(
694
+ R"([{}]: cannot open "{}": this build has no couchbase2 support )"
695
+ "(COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2 is off), so the couchbase2:// scheme cannot be "
696
+ "served.",
697
+ id_,
698
+ logger::user_data(origin_.connection_string()));
699
+ stopped_ = true;
700
+ work_.reset();
701
+ return handler(errc::common::feature_not_available);
702
+ #endif
703
+ }
704
+ {
705
+ const std::scoped_lock lock(deferred_until_open_mutex_);
706
+ opening_ = true;
707
+ }
474
708
  setup_observability();
475
709
  if (origin_.options().enable_dns_srv) {
476
- std::string hostname;
477
- std::string port;
478
- std::tie(hostname, port) = origin_.next_address();
710
+ auto [hostname, port] = origin_.next_address();
479
711
  dns_srv_tracker_ = std::make_shared<impl::dns_srv_tracker>(
480
712
  ctx_, hostname, origin_.options().dns_config, origin_.options().enable_tls);
713
+ {
714
+ // close() completes the open itself rather than waiting out the lookup.
715
+ const std::scoped_lock lock(dns_srv_open_handler_mutex_);
716
+ dns_srv_open_handler_ = std::move(handler);
717
+ }
481
718
  return asio::post(asio::bind_executor(
482
- ctx_,
483
- [self = shared_from_this(),
484
- hostname = std::move(hostname),
485
- handler = std::move(handler)]() mutable {
719
+ ctx_, [self = shared_from_this(), hostname = std::move(hostname)]() mutable {
486
720
  return self->dns_srv_tracker_->get_srv_nodes(
487
- [self, hostname = std::move(hostname), handler = std::move(handler)](
488
- origin::node_list nodes, std::error_code ec) mutable {
721
+ [self, hostname = std::move(hostname)](origin::node_list nodes,
722
+ std::error_code ec) mutable {
723
+ auto handler = self->take_dns_srv_open_handler();
724
+ if (!handler) {
725
+ return;
726
+ }
489
727
  if (ec) {
490
728
  #ifdef __clang_analyzer__
491
729
  // TODO(CXXCBC-549): clang-tidy-19 reports potential memory leak here
@@ -496,11 +734,13 @@ public:
496
734
  });
497
735
  }
498
736
  if (!nodes.empty()) {
737
+ const std::scoped_lock lock(self->origin_mutex_);
499
738
  self->origin_.set_nodes(std::move(nodes));
500
739
  CB_LOG_INFO(
501
740
  "replace list of bootstrap nodes with addresses from DNS SRV of \"{}\": [{}]",
502
- hostname,
503
- utils::join_strings(self->origin_.get_nodes(), ", "));
741
+ logger::system_data(hostname),
742
+ logger::system_data_list(self->origin_.get_node_addresses(),
743
+ logger::list_entries::quoted));
504
744
  }
505
745
  return self->do_open(std::move(handler));
506
746
  });
@@ -509,40 +749,63 @@ public:
509
749
  do_open(std::move(handler));
510
750
  }
511
751
 
512
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
513
- void open_in_background(couchbase::core::origin origin,
514
- utils::movable_function<void(std::error_code)>&& handler)
752
+ // Runs `resume` once: with the error that `start` reports through the completion it is given, or
753
+ // with unambiguous_timeout when `deadline` passes first. A success reported after `deadline` is a
754
+ // timeout too. `resume` runs on the IO thread while the cluster is open. After close() the IO
755
+ // thread may never run again, so a stopped cluster completes at once with cluster_closed, and a
756
+ // wait that reports after close() runs `resume` on the thread that reports.
757
+ template<typename Start>
758
+ void wait_until(std::chrono::steady_clock::time_point deadline,
759
+ utils::movable_function<void(std::error_code)>&& resume,
760
+ Start&& start)
515
761
  {
516
762
  if (stopped_) {
517
- return handler(errc::network::cluster_closed);
518
- }
519
- if (background_open_started_) {
520
- CB_LOG_DEBUG("Background open already started for cluster, id: \"{}\"", id_);
521
- return handler({});
522
- }
523
- if (origin.get_nodes().empty()) {
524
- stopped_ = true;
525
- work_.reset();
526
- return handler(errc::common::invalid_argument);
763
+ return resume(errc::network::cluster_closed);
527
764
  }
765
+ auto entry = std::make_shared<deferred_request>();
766
+ entry->resume = std::move(resume);
767
+ entry->deadline = std::make_unique<asio::steady_timer>(ctx_, deadline);
768
+ entry->deadline->async_wait([entry](std::error_code ec) {
769
+ if (ec != asio::error::operation_aborted) {
770
+ entry->complete(errc::common::unambiguous_timeout);
771
+ }
772
+ });
773
+ // Held weakly: `start` may keep this completion after the wait ends, as a bucket's queue of
774
+ // configuration waiters does. The pending timer keeps the entry alive until it completes.
775
+ std::forward<Start>(start)(
776
+ [self = shared_from_this(),
777
+ weak_entry = std::weak_ptr<deferred_request>(entry)](std::error_code ec) {
778
+ auto entry = weak_entry.lock();
779
+ if (!entry) {
780
+ return;
781
+ }
782
+ if (self->ctx_.get_executor().running_in_this_thread()) {
783
+ return entry->complete(ec);
784
+ }
785
+ // A wait that started just before close() can report after the IO thread has stopped, and
786
+ // a stopped io_context keeps what is posted to it without running it.
787
+ if (self->stopped_) {
788
+ entry->complete_off_io_thread(ec);
789
+ // The armed timer would keep the io_context busy until the deadline; dropped if it
790
+ // stopped.
791
+ return asio::post(self->ctx_, [entry] {
792
+ entry->deadline->cancel();
793
+ });
794
+ }
795
+ asio::post(self->ctx_, [entry, ec] {
796
+ entry->complete(ec);
797
+ });
798
+ });
799
+ }
528
800
 
529
- origin_ = std::move(origin);
530
- CB_LOG_DEBUG(R"(open cluster in background, id: "{}", core version: "{}", {})",
531
- id_,
532
- couchbase::core::meta::sdk_semver(),
533
- origin_.to_json());
534
- setup_observability();
535
- session_manager_->set_dispatch_timeout(origin_.options().dispatch_timeout);
536
- // at this point we will infinitely try to connect
537
- if (origin_.options().enable_dns_srv) {
538
- do_background_dns_srv_open();
539
- } else {
540
- do_background_open();
541
- }
542
- background_open_started_ = true;
543
- return handler({});
801
+ void open_bucket_until(const std::string& bucket_name,
802
+ std::chrono::steady_clock::time_point deadline,
803
+ utils::movable_function<void(std::error_code)>&& handler)
804
+ {
805
+ wait_until(deadline, std::move(handler), [&](auto&& done) {
806
+ open_bucket(bucket_name, std::forward<decltype(done)>(done));
807
+ });
544
808
  }
545
- #endif
546
809
 
547
810
  void open_bucket(const std::string& bucket_name,
548
811
  utils::movable_function<void(std::error_code)>&& handler)
@@ -550,6 +813,19 @@ public:
550
813
  if (stopped_) {
551
814
  return handler(errc::network::cluster_closed);
552
815
  }
816
+ if (is_protostellar()) {
817
+ // couchbase2 has no bucket bootstrap. Opening one would dial the gateway over MCBP, which it
818
+ // does not serve, and fail with unambiguous_timeout only at bootstrap_timeout.
819
+ return handler(errc::common::feature_not_available);
820
+ }
821
+ if (defer_until_open([&] {
822
+ return [self = shared_from_this(), bucket_name, handler = std::move(handler)](
823
+ std::error_code /* no deadline */) mutable {
824
+ self->open_bucket(bucket_name, std::move(handler));
825
+ };
826
+ })) {
827
+ return;
828
+ }
553
829
  std::shared_ptr<bucket> b{};
554
830
  {
555
831
  const std::scoped_lock lock(buckets_mutex_);
@@ -623,6 +899,8 @@ public:
623
899
 
624
900
  auto update_credentials(const cluster_credentials& auth) -> core::error
625
901
  {
902
+ // origin_mutex_ as well: while open() bootstraps, the IO thread writes origin_.
903
+ const std::scoped_lock lock(update_credentials_mutex_, origin_mutex_);
626
904
  if (stopped_) {
627
905
  return { errc::network::cluster_closed, {} };
628
906
  }
@@ -645,8 +923,7 @@ public:
645
923
  });
646
924
 
647
925
  if (auth.requires_tls()) {
648
- // Recreate and atomically swap the TLS context, existing sessions should continue to use the
649
- // old one.
926
+ // Recreate and atomically swap the TLS context. KV sessions keep the one they connected with.
650
927
  const auto new_ctx = std::make_shared<asio::ssl::context>(asio::ssl::context::tls_client);
651
928
  auto ec = configure_tls_context(new_ctx);
652
929
  if (ec) {
@@ -655,6 +932,11 @@ public:
655
932
  return { ec, {} };
656
933
  }
657
934
  tls_.set_ctx(new_ctx);
935
+ // A pooled HTTP connection presents the certificate it was opened with. A password or JWT is
936
+ // sent with each request.
937
+ if (auth.uses_certificate()) {
938
+ session_manager_->retire_sessions();
939
+ }
658
940
  }
659
941
 
660
942
  if (auth.uses_jwt()) {
@@ -665,14 +947,6 @@ public:
665
947
  return {};
666
948
  }
667
949
 
668
- auto origin() const -> std::pair<std::error_code, couchbase::core::origin>
669
- {
670
- if (stopped_) {
671
- return { errc::network::cluster_closed, {} };
672
- }
673
- return { {}, origin_ };
674
- }
675
-
676
950
  template<class Request,
677
951
  class Handler,
678
952
  typename std::enable_if_t<
@@ -685,7 +959,65 @@ public:
685
959
  return handler(request.make_response(
686
960
  make_key_value_error_context(errc::network::cluster_closed, request.id), response_type{}));
687
961
  }
962
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
963
+ if (auto component = protostellar_component(); component) {
964
+ if constexpr (protostellar::component_supports_v<Request>) {
965
+ execute_protostellar(std::move(request), std::forward<Handler>(handler));
966
+ return;
967
+ } else {
968
+ return handler(request.make_response(
969
+ make_key_value_error_context(errc::common::feature_not_available, request.id),
970
+ response_type{}));
971
+ }
972
+ }
973
+ #endif
974
+ auto timeout = request.timeout.value_or(default_timeout_for(service_type::key_value));
975
+ if constexpr (io::mcbp_traits::supports_durability_v<Request>) {
976
+ if (request.durability_level != durability_level::none &&
977
+ timeout < durability_timeout_floor) {
978
+ CB_LOG_DEBUG(
979
+ R"(Timeout is too low for operation with durability, increasing to sensible value. key="{}", timeout={}ms, floor={}ms)",
980
+ logger::document(request.id),
981
+ timeout.count(),
982
+ durability_timeout_floor.count());
983
+ timeout = durability_timeout_floor;
984
+ }
985
+ }
986
+ execute_until(std::move(request),
987
+ std::chrono::steady_clock::now() + timeout,
988
+ std::forward<Handler>(handler));
989
+ }
990
+
991
+ // A key-value request's timeout counts from when it was issued: waiting for the bootstrap or for
992
+ // the bucket to open spends it, and the command gets only what is left. Past `deadline` the
993
+ // request gets unambiguous_timeout and is not dispatched.
994
+ template<class Request, class Handler>
995
+ void execute_until(Request request,
996
+ std::chrono::steady_clock::time_point deadline,
997
+ Handler&& handler)
998
+ {
999
+ using response_type = typename Request::encoded_response_type;
1000
+ if (stopped_) {
1001
+ return handler(request.make_response(
1002
+ make_key_value_error_context(errc::network::cluster_closed, request.id), response_type{}));
1003
+ }
1004
+ auto resume = [&] {
1005
+ return [self = shared_from_this(),
1006
+ deadline,
1007
+ request = std::move(request),
1008
+ handler = std::forward<Handler>(handler)](std::error_code ec) mutable {
1009
+ if (ec) {
1010
+ return handler(
1011
+ request.make_response(make_key_value_error_context(ec, request.id), response_type{}));
1012
+ }
1013
+ self->execute_until(std::move(request), deadline, std::move(handler));
1014
+ };
1015
+ };
1016
+ if (defer_until_open(resume, deadline)) {
1017
+ return;
1018
+ }
688
1019
  if (auto bucket = find_bucket_by_name(request.id.bucket()); bucket != nullptr) {
1020
+ request.timeout = remaining_until(deadline);
689
1021
  return bucket->execute(std::move(request), std::forward<Handler>(handler));
690
1022
  }
691
1023
  if (request.id.bucket().empty()) {
@@ -693,16 +1025,7 @@ public:
693
1025
  make_key_value_error_context(errc::common::bucket_not_found, request.id), response_type{}));
694
1026
  }
695
1027
  auto bucket_name = request.id.bucket();
696
- return open_bucket(bucket_name,
697
- [self = shared_from_this(),
698
- request = std::move(request),
699
- handler = std::forward<Handler>(handler)](std::error_code ec) mutable {
700
- if (ec) {
701
- return handler(request.make_response(
702
- make_key_value_error_context(ec, request.id), response_type{}));
703
- }
704
- return self->execute(std::move(request), std::forward<Handler>(handler));
705
- });
1028
+ open_bucket_until(bucket_name, deadline, resume());
706
1029
  }
707
1030
 
708
1031
  template<class Request,
@@ -716,6 +1039,50 @@ public:
716
1039
  if (stopped_) {
717
1040
  return handler(request.make_response({ errc::network::cluster_closed }, response_type{}));
718
1041
  }
1042
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
1043
+ if (auto component = protostellar_component(); component) {
1044
+ if constexpr (component_routes<Request>::value) {
1045
+ // Query/analytics/search/view/management requests carry no retry_context on the request, so
1046
+ // execute_protostellar_http supplies the retry state and re-dispatches a retryable
1047
+ // (service_not_available) response under the same overall-deadline bound as the KV path.
1048
+ execute_protostellar_http(std::move(request), std::forward<Handler>(handler));
1049
+ return;
1050
+ } else {
1051
+ // Remaining management ops over couchbase2 are not wired yet. Reject cleanly rather than
1052
+ // falling through to the MCBP session manager, which is never bootstrapped on this path.
1053
+ return handler(
1054
+ request.make_response({ errc::common::feature_not_available }, response_type{}));
1055
+ }
1056
+ }
1057
+ #endif
1058
+ // The request's timeout counts from now, not from the end of the bootstrap.
1059
+ if (const auto deadline = std::chrono::steady_clock::now() +
1060
+ request.timeout.value_or(default_timeout_for(request.type));
1061
+ defer_until_open(
1062
+ [&] {
1063
+ return [self = shared_from_this(),
1064
+ deadline,
1065
+ request = std::move(request),
1066
+ handler = std::forward<Handler>(handler)](std::error_code ec) mutable {
1067
+ if (ec) {
1068
+ auto response = request.make_response({ ec }, response_type{});
1069
+ if constexpr (has_client_context_id<Request>::value &&
1070
+ has_client_context_id<decltype(response.ctx)>::value) {
1071
+ // The caller's id, or a generated one, as a dispatched request carries.
1072
+ response.ctx.client_context_id = request.client_context_id.has_value()
1073
+ ? *request.client_context_id
1074
+ : uuid::to_string(uuid::random());
1075
+ }
1076
+ return handler(std::move(response));
1077
+ }
1078
+ request.timeout = remaining_until(deadline);
1079
+ self->execute(std::move(request), std::move(handler));
1080
+ };
1081
+ },
1082
+ deadline)) {
1083
+ return;
1084
+ }
1085
+ // cppcheck-suppress knownConditionTrueFalse
719
1086
  if (!is_feature_supported(
720
1087
  request, session_manager_->configuration_capabilities(), origin_.options())) {
721
1088
  return handler(
@@ -737,32 +1104,59 @@ public:
737
1104
  bucket_capability cap,
738
1105
  Handler&& handler)
739
1106
  {
1107
+ // There is no bucket capability to read over couchbase2, and no MCBP connection to read it
1108
+ // over: open_bucket() below would bootstrap a session against a port the gateway does not
1109
+ // serve, so a request that only wants a capability checked would fail on the check rather
1110
+ // than on what it asked for. The gateway still validates the capability the request depends
1111
+ // on -- history retention needs a magma bucket wherever the request arrives from -- so
1112
+ // routing straight through refuses the same requests, one hop later and in the server's own
1113
+ // words.
1114
+ if (is_protostellar()) {
1115
+ return execute(std::move(request), std::forward<Handler>(handler));
1116
+ }
1117
+ // The waits for the bootstrap and for the bucket's configuration spend the request's timeout;
1118
+ // the request gets what is left. The timed hold comes first: open_bucket() would hold the
1119
+ // request without a deadline, and it would stay queued after timing out.
1120
+ const auto deadline = std::chrono::steady_clock::now() +
1121
+ request.timeout.value_or(default_timeout_for(request.type));
1122
+ if (defer_until_open(
1123
+ [&] {
1124
+ return [self = shared_from_this(),
1125
+ deadline,
1126
+ cap,
1127
+ request = std::move(request),
1128
+ handler = std::forward<Handler>(handler)](std::error_code ec) mutable {
1129
+ if (ec) {
1130
+ return handler(request.make_response({ ec }, {}));
1131
+ }
1132
+ request.timeout = remaining_until(deadline);
1133
+ self->execute_with_bucket_capability_check(
1134
+ std::move(request), cap, std::move(handler));
1135
+ };
1136
+ },
1137
+ deadline)) {
1138
+ return;
1139
+ }
740
1140
  auto bucket_name = request.bucket_name;
741
- return open_bucket(
1141
+ return with_bucket_configuration(
742
1142
  bucket_name,
1143
+ deadline,
743
1144
  [self = shared_from_this(),
744
- bucket_name,
1145
+ deadline,
745
1146
  cap,
746
1147
  request = std::move(request),
747
- handler = std::forward<Handler>(handler)](std::error_code ec) mutable {
1148
+ handler = std::forward<Handler>(handler)](
1149
+ std::error_code ec, const std::shared_ptr<topology::configuration>& config) mutable {
748
1150
  if (ec) {
749
1151
  handler(request.make_response({ ec }, {}));
750
1152
  return;
751
1153
  }
752
- return self->with_bucket_configuration(
753
- bucket_name,
754
- [self, cap, request = std::move(request), handler = std::forward<Handler>(handler)](
755
- std::error_code ec, const std::shared_ptr<topology::configuration>& config) mutable {
756
- if (ec) {
757
- handler(request.make_response({ ec }, {}));
758
- return;
759
- }
760
- if (!config->capabilities.has_bucket_capability(cap)) {
761
- handler(request.make_response({ errc::common::feature_not_available }, {}));
762
- return;
763
- }
764
- return self->execute(std::move(request), std::forward<Handler>(handler));
765
- });
1154
+ if (!config->capabilities.has_bucket_capability(cap)) {
1155
+ handler(request.make_response({ errc::common::feature_not_available }, {}));
1156
+ return;
1157
+ }
1158
+ request.timeout = remaining_until(deadline);
1159
+ return self->execute(std::move(request), std::forward<Handler>(handler));
766
1160
  });
767
1161
  }
768
1162
 
@@ -805,8 +1199,413 @@ public:
805
1199
  }
806
1200
  }
807
1201
 
1202
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
1203
+ // couchbase2:// open path: the gateway does the routing, so there is no MCBP config bootstrap.
1204
+ // Build a single gRPC channel + KV component to the first endpoint and report success.
1205
+ void open_protostellar(utils::movable_function<void(std::error_code)>&& handler)
1206
+ {
1207
+ // next_address() dereferences the node iterator; a couchbase2:// string that parsed with no
1208
+ // bootstrap node would make it UB, so fail cleanly instead.
1209
+ if (origin_.get_nodes().empty()) {
1210
+ return handler(errc::common::invalid_argument);
1211
+ }
1212
+ auto [hostname, port] = origin_.next_address();
1213
+ auto endpoint = hostname + ":" + port;
1214
+ // Several nodes reach here from an origin assembled programmatically (set_nodes(), or the
1215
+ // hostname/port constructors), and from a connection string naming more than one host:
1216
+ // parse_connection_string() records that as an error on the connection_string, but no caller
1217
+ // reads that field, so the origin is built with every host regardless. Kept as a log rather
1218
+ // than a rejection: one endpoint is as good as another here, and since the node list is
1219
+ // shuffled unless preserve_bootstrap_nodes_order is set, the message is what says which one
1220
+ // this process picked.
1221
+ if (origin_.get_nodes().size() > 1) {
1222
+ CB_LOG_INFO("[{}]: couchbase2 uses a single gateway endpoint (\"{}\"); the remaining {} "
1223
+ "node(s) on this origin are ignored (the gateway fronts the cluster).",
1224
+ id_,
1225
+ logger::system_data(endpoint),
1226
+ origin_.get_nodes().size() - 1);
1227
+ }
1228
+
1229
+ // Apply the same Capella safety clamp as the MCBP path (configure_tls_options): force peer
1230
+ // verification against the managed service even if tls_verify=none was requested, and surface
1231
+ // the same warnings. Without this the couchbase2 path would silently allow MITM on Capella.
1232
+ auto options = origin_.options();
1233
+ const auto requested_verify = options.tls_verify;
1234
+ options.tls_verify = effective_tls_verify_mode(requested_verify, is_capella_host(hostname));
1235
+ if (requested_verify == tls_verify_mode::none && options.tls_verify == tls_verify_mode::peer) {
1236
+ CB_LOG_WARNING(R"([{}]: tls_verify=none was requested but "{}" is a Capella endpoint; )"
1237
+ "enforcing peer verification.",
1238
+ id_,
1239
+ logger::system_data(hostname));
1240
+ }
1241
+ // The "verification is disabled" warning is not repeated here: make_channel_credentials() is
1242
+ // the one point every couchbase2 channel passes through and warns there, so it also covers
1243
+ // callers that build a component directly. Only the Capella clamp is specific to this path.
1244
+
1245
+ // Build the channel before starting observability. The two sibling failure paths in open()
1246
+ // (an origin with no nodes) latch stopped_ and release the work guard while nothing has been
1247
+ // started yet; doing the same after setup_observability() would leave its tracer, meter and
1248
+ // reporters running with close() short-circuiting on stopped_ and never stopping them.
1249
+ std::shared_ptr<grpc::Channel> channel;
1250
+ try {
1251
+ channel = protostellar::make_channel(endpoint, options, origin_.credentials());
1252
+ } catch (const std::exception& e) {
1253
+ CB_LOG_ERROR(R"([{}]: failed to establish couchbase2 channel: {})", id_, e.what());
1254
+ stopped_ = true;
1255
+ work_.reset();
1256
+ return handler(errc::common::invalid_argument);
1257
+ }
1258
+ setup_observability();
1259
+ {
1260
+ // Written under the lock for symmetry with close(): the assignment itself happens before any
1261
+ // caller can reach execute() (the open handler has not fired yet), but leaving one access
1262
+ // unguarded is how the invariant gets lost later.
1263
+ const std::scoped_lock lock(protostellar_mutex_);
1264
+ protostellar_ = std::make_shared<protostellar::component>(
1265
+ ctx_,
1266
+ protostellar::component_config{ std::move(channel),
1267
+ origin_.credentials(),
1268
+ make_component_timeouts(options),
1269
+ make_compression_settings(options) });
1270
+ }
1271
+ CB_LOG_INFO(
1272
+ R"(open couchbase2 cluster, id: "{}", endpoint: "{}")", id_, logger::system_data(endpoint));
1273
+ return handler({});
1274
+ }
1275
+ #endif
1276
+
1277
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
1278
+ // The KV component, or nullptr when this cluster is not a couchbase2 one. Returns a copy under
1279
+ // the lock, in the same shape as find_bucket_by_name(): execute() runs on the caller's thread
1280
+ // while close() resets the member on the io thread, and reading a shared_ptr that another thread
1281
+ // is writing is a data race whatever the refcount does. Holding a copy also keeps the component
1282
+ // alive for the duration of the call that is about to use it.
1283
+ [[nodiscard]] auto protostellar_component() const -> std::shared_ptr<protostellar::component>
1284
+ {
1285
+ const std::scoped_lock lock(protostellar_mutex_);
1286
+ return protostellar_;
1287
+ }
1288
+
1289
+ // Whether the couchbase2 channel is READY. An idle channel is asked to connect, so polling this
1290
+ // also establishes the connection. cluster_closed once the cluster is closed.
1291
+ [[nodiscard]] auto protostellar_channel_ready() const -> std::pair<std::error_code, bool>
1292
+ {
1293
+ const auto component = protostellar_component();
1294
+ if (stopped_ || component == nullptr) {
1295
+ return { errc::network::cluster_closed, false };
1296
+ }
1297
+ const auto& channel = component->channel();
1298
+ auto state = channel->GetState(/* try_to_connect */ true);
1299
+ if (state != GRPC_CHANNEL_READY && state != GRPC_CHANNEL_SHUTDOWN) {
1300
+ // On gRPC builds that progress a connection only under polling, an attempt stalls while no
1301
+ // call is in flight. A wait whose deadline has already passed polls once without blocking.
1302
+ channel->WaitForStateChange(state, std::chrono::system_clock::now());
1303
+ state = channel->GetState(/* try_to_connect */ false);
1304
+ }
1305
+ switch (state) {
1306
+ case GRPC_CHANNEL_READY:
1307
+ return { {}, true };
1308
+ case GRPC_CHANNEL_SHUTDOWN:
1309
+ return { errc::network::cluster_closed, false };
1310
+ default:
1311
+ return { {}, false };
1312
+ }
1313
+ }
1314
+ #endif
1315
+
1316
+ // True when the cluster was configured with a couchbase2:// connection string -- a property of
1317
+ // the origin, so it holds from construction and stays true after close(). Compound /
1318
+ // self-dispatching operations (replica reads, legacy durability) check this to reject cleanly
1319
+ // instead of falling through to the MCBP bootstrap path, which does not exist on couchbase2.
1320
+ //
1321
+ // It answers "which transport was configured", never "is the transport usable". Most callers want
1322
+ // exactly the former and reject with feature_not_available, which is a capability statement and
1323
+ // is correct whether the cluster is open or closed. The callers that do need liveness test
1324
+ // stopped_ themselves, or route into a dispatch that re-reads the component under the lock.
1325
+ [[nodiscard]] auto is_protostellar() const -> bool
1326
+ {
1327
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
1328
+ return origin_.uses_protostellar();
1329
+ #else
1330
+ return false;
1331
+ #endif
1332
+ }
1333
+
1334
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
1335
+ // Execute a couchbase2 operation through the component with transport-level retry. Retryable
1336
+ // errors (see protostellar::retry_reason_for) consult the request's retry strategy (falling back
1337
+ // to the origin default), and on a positive decision the request is re-issued after a backoff.
1338
+ // Idempotency is enforced by the strategy, so ambiguous mutations are not replayed.
1339
+ //
1340
+ // The deadline is resolved here, before the first attempt, and every attempt is issued against
1341
+ // it. Falling back to the cluster's KV default when the request carries no timeout is what bounds
1342
+ // the loop at all: the public API leaves the per-operation timeout unset unless the caller asks
1343
+ // for one, and the strategy never refuses a retry for service_not_available, so a request issued
1344
+ // with default options against an unavailable gateway would otherwise retry until the process
1345
+ // ended -- never completing, and never reporting an error either.
1346
+ //
1347
+ // Which default applies is resolve_kv_timeout's rule, not a second copy of it: a durable mutation
1348
+ // is bounded by KvDurableTimeout and everything else by the standard KV timeout. It has to be
1349
+ // applied here, because dispatch_protostellar narrows request->timeout on every attempt including
1350
+ // the first -- so the component is always handed an engaged timeout and its own fallback never
1351
+ // selects for a request that arrives through this loop. Resolving with the standard default here
1352
+ // would cap a durable mutation at the budget the durable default exists to replace.
1353
+ template<class Request, class Handler>
1354
+ void execute_protostellar(Request request, Handler&& handler)
1355
+ {
1356
+ // The fallback below is a KV default, so routing anything else here would bound it with a
1357
+ // budget belonging to another service. Non-KV requests carry no retry_context and have their
1358
+ // own path.
1359
+ static_assert(protostellar::component_supports_v<Request>,
1360
+ "execute_protostellar resolves the key-value timeout");
1361
+ const auto deadline =
1362
+ std::chrono::steady_clock::now() +
1363
+ protostellar::resolve_kv_timeout(request.timeout,
1364
+ protostellar::requested_durability(request),
1365
+ make_component_timeouts(origin_.options()));
1366
+ // Held by shared_ptr and re-issued from there. The request owns the document body for a
1367
+ // mutation, so copying it to keep a re-issue possible charged every operation on the data plane
1368
+ // for a retry that in normal operation never happens.
1369
+ dispatch_protostellar(
1370
+ std::make_shared<Request>(std::move(request)), std::forward<Handler>(handler), deadline);
1371
+ }
1372
+
1373
+ // One attempt of the above, re-entered from the backoff timer for each retry.
1374
+ template<class Request, class Handler>
1375
+ void dispatch_protostellar(std::shared_ptr<Request> request,
1376
+ Handler&& handler,
1377
+ std::chrono::steady_clock::time_point deadline)
1378
+ {
1379
+ // Each attempt gets what is LEFT of the operation's budget rather than a fresh full-length one.
1380
+ // Without this an attempt started just inside the deadline would run for the whole timeout
1381
+ // again, so a retried operation could take roughly twice its nominal budget. Narrowing keeps
1382
+ // the request carrying a duration -- which is all the component accepts -- while making that
1383
+ // duration mean "until the operation's deadline". A non-positive remainder is not
1384
+ // special-cased, and is in fact how the loop ends: the component rejects an exhausted budget
1385
+ // as a timeout instead of dispatching a call with no deadline.
1386
+ request->timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
1387
+ deadline - std::chrono::steady_clock::now());
1388
+ // Re-read through the locked accessor on every attempt: a retry re-enters this
1389
+ // function from a timer, long after the caller checked.
1390
+ auto component = protostellar_component();
1391
+ if (!component) {
1392
+ return handler(
1393
+ request->make_response(make_key_value_error_context(errc::network::cluster_closed,
1394
+ request->id,
1395
+ request->retries.retry_attempts(),
1396
+ request->retries.retry_reasons()),
1397
+ typename Request::encoded_response_type{}));
1398
+ }
1399
+ component->execute(
1400
+ *request,
1401
+ [self = shared_from_this(), request, handler = std::forward<Handler>(handler), deadline](
1402
+ auto response) mutable {
1403
+ if (auto reason = protostellar::retry_reason_for(protostellar_response_ec(response.ctx));
1404
+ reason.has_value()) {
1405
+ auto strategy = request->retries.strategy()
1406
+ ? request->retries.strategy()
1407
+ : self->origin_.options().default_retry_strategy_;
1408
+ if (strategy) {
1409
+ const auto action = strategy->retry_after(request->retries, reason.value());
1410
+ // The wait is capped at the deadline rather than the retry being refused for landing
1411
+ // past it. Refusing would end the operation early on the last attempt's transport
1412
+ // error -- temporary_failure for an unreachable gateway -- while the caller's budget
1413
+ // was still running, so the same condition that times out over the classic transport
1414
+ // would report a different code here. Capping lets the operation use its whole budget
1415
+ // and end on the deadline: the attempt scheduled at it gets a non-positive remainder
1416
+ // above, which the component answers as a timeout carrying the accumulated attempts
1417
+ // and reasons. That is terminal, so the loop cannot spin on it -- retry_reason_for
1418
+ // does not classify a timeout as retryable. Both other implementations of this loop
1419
+ // cap the same way (io/retry_orchestrator.hxx cap_duration, and Java's
1420
+ // RetryOrchestratorProtostellar.capDuration).
1421
+ const auto when =
1422
+ std::min(std::chrono::steady_clock::now() + action.duration(), deadline);
1423
+ if (action.need_to_retry()) {
1424
+ if (self->stopped_) {
1425
+ // A retry was due and will not happen: close() latches stopped_ before it clears
1426
+ // the component, so this completion arrived after the close. The transport error
1427
+ // the next attempt would have replaced is not the caller's answer -- reporting it
1428
+ // says "try again" about a cluster that is gone -- so report the close, as the
1429
+ // re-entry below does once the component is cleared. Gating on a due retry keeps a
1430
+ // terminal response out of this: request_canceled in particular carries the
1431
+ // CXXCBC-909 rule that the statement must not be replayed.
1432
+ return handler(request->make_response(
1433
+ make_key_value_error_context(errc::network::cluster_closed,
1434
+ request->id,
1435
+ request->retries.retry_attempts(),
1436
+ request->retries.retry_reasons()),
1437
+ typename Request::encoded_response_type{}));
1438
+ }
1439
+ // Recorded before the re-issue, so the next attempt carries the accumulated count
1440
+ // and the error context it builds reports the retries that preceded it.
1441
+ request->retries.record_retry_attempt(reason.value());
1442
+ auto timer = std::make_shared<asio::steady_timer>(self->ctx_);
1443
+ timer->expires_at(when);
1444
+ timer->async_wait([self,
1445
+ request,
1446
+ handler = std::move(handler),
1447
+ deadline,
1448
+ response = std::move(response),
1449
+ timer](const std::error_code& timer_ec) mutable {
1450
+ if (timer_ec) {
1451
+ // Timer cancelled: deliver the last retryable response rather than dropping
1452
+ // the caller's completion, which would hang it.
1453
+ handler(std::move(response));
1454
+ return;
1455
+ }
1456
+ // A cluster closed during the backoff is answered by dispatch_protostellar,
1457
+ // which re-reads the component under the lock and reports cluster_closed. The
1458
+ // last retryable response would instead report a temporary failure, which reads
1459
+ // as "retry later" for a cluster that is gone.
1460
+ self->dispatch_protostellar(std::move(request), std::move(handler), deadline);
1461
+ });
1462
+ return;
1463
+ }
1464
+ }
1465
+ }
1466
+ // A declined retry reports the transport error even when the cluster is closing: the
1467
+ // strategy, not the close, is what ended the operation, and the caller gets the answer a
1468
+ // close-free run would have given. The close is reported only where it is the more specific
1469
+ // answer -- above, where a retry was due and will not happen, and at the top of this
1470
+ // function, where no attempt was made and there is no transport error to report.
1471
+ handler(std::move(response));
1472
+ });
1473
+ }
1474
+
1475
+ template<typename Request>
1476
+ static auto timeout_for(const protostellar::component_timeouts& timeouts)
1477
+ -> std::chrono::milliseconds
1478
+ {
1479
+ if constexpr (std::is_same_v<Request, operations::query_request>) {
1480
+ return timeouts.query;
1481
+ } else if constexpr (std::is_same_v<Request, operations::analytics_request>) {
1482
+ return timeouts.analytics;
1483
+ } else if constexpr (std::is_same_v<Request, operations::search_request>) {
1484
+ return timeouts.search;
1485
+ } else if constexpr (std::is_same_v<Request, operations::document_view_request>) {
1486
+ return timeouts.view;
1487
+ } else {
1488
+ return timeouts.management;
1489
+ }
1490
+ }
1491
+
1492
+ // Retry for non-KV couchbase2 operations (query/analytics/search/view/management). These
1493
+ // HTTP-shaped requests carry no retry_context, so the retry state lives here: a heap
1494
+ // retry_context threaded across re-dispatches, mutated in place so the strategy sees the
1495
+ // accumulated attempts. In practice only service_not_available (a transport UNAVAILABLE -- the
1496
+ // call never reached a server) arises on this path: retry_reason_for is shared with the KV path
1497
+ // and can also report key_value_locked, which no query/analytics/search/view/management response
1498
+ // produces. The strategy permits service_not_available for non-idempotent ops too, so the fixed
1499
+ // retry_context<false> idempotency is not load-bearing. The strategy is the origin
1500
+ // default (these requests have no per-request override). Like the KV path, retries are bounded by
1501
+ // the strategy and by an overall deadline stamped before the first attempt: the request's own
1502
+ // timeout when it carries one, the per-service default otherwise. Every attempt is therefore
1503
+ // bounded, including the first.
1504
+ //
1505
+ // This overload stamps that deadline; the one below carries it. Taking a plain time_point rather
1506
+ // than an optional keeps "no deadline yet" unrepresentable once the loop has started.
1507
+ template<class Request, class Handler>
1508
+ void execute_protostellar_http(Request request, Handler&& handler)
1509
+ {
1510
+ const auto default_timeout = timeout_for<Request>(make_component_timeouts(origin_.options()));
1511
+ const auto deadline =
1512
+ std::chrono::steady_clock::now() + request.timeout.value_or(default_timeout);
1513
+ execute_protostellar_http(
1514
+ std::move(request),
1515
+ std::forward<Handler>(handler),
1516
+ std::make_shared<io::retry_context<false>>(origin_.options().default_retry_strategy_),
1517
+ deadline);
1518
+ }
1519
+
1520
+ template<class Request, class Handler>
1521
+ void execute_protostellar_http(Request request,
1522
+ Handler&& handler,
1523
+ std::shared_ptr<io::retry_context<false>> retries,
1524
+ std::chrono::steady_clock::time_point deadline)
1525
+ {
1526
+ // Re-read through the locked accessor on every attempt: a retry re-enters this
1527
+ // function from a timer, long after the caller checked.
1528
+ auto component = protostellar_component();
1529
+ if (!component) {
1530
+ auto response = request.make_response({ errc::network::cluster_closed },
1531
+ typename Request::encoded_response_type{});
1532
+ // A cluster closed mid-backoff reaches here with attempts already behind it, and dropping
1533
+ // them would report the operation as never retried.
1534
+ response.ctx.retry_attempts = retries->retry_attempts();
1535
+ response.ctx.retry_reasons = retries->retry_reasons();
1536
+ return handler(std::move(response));
1537
+ }
1538
+
1539
+ auto request_ptr = std::make_shared<Request>(std::move(request));
1540
+ component->execute(
1541
+ *request_ptr,
1542
+ [self = shared_from_this(),
1543
+ request_ptr,
1544
+ handler = std::forward<Handler>(handler),
1545
+ retries,
1546
+ deadline](auto response) mutable {
1547
+ if (auto reason = protostellar::retry_reason_for(protostellar_response_ec(response.ctx));
1548
+ reason.has_value()) {
1549
+ if (retries->strategy()) {
1550
+ const auto action = retries->strategy()->retry_after(*retries, reason.value());
1551
+ const auto when =
1552
+ std::min(std::chrono::steady_clock::now() + action.duration(), deadline);
1553
+ if (action.need_to_retry()) {
1554
+ if (self->stopped_) {
1555
+ // See dispatch_protostellar(): a retry the close will not let happen reports the
1556
+ // close, not the error the next attempt would have replaced.
1557
+ auto closed = request_ptr->make_response({ errc::network::cluster_closed },
1558
+ typename Request::encoded_response_type{});
1559
+ closed.ctx.retry_attempts = retries->retry_attempts();
1560
+ closed.ctx.retry_reasons = retries->retry_reasons();
1561
+ return handler(std::move(closed));
1562
+ }
1563
+ retries->record_retry_attempt(reason.value());
1564
+ auto timer = std::make_shared<asio::steady_timer>(self->ctx_);
1565
+ timer->expires_at(when);
1566
+ timer->async_wait([self,
1567
+ request_ptr = std::move(request_ptr),
1568
+ handler = std::move(handler),
1569
+ retries,
1570
+ deadline,
1571
+ response = std::move(response),
1572
+ timer](const std::error_code& timer_ec) mutable {
1573
+ if (timer_ec) {
1574
+ // Timer cancelled: deliver the last response rather than dropping the caller's
1575
+ // completion, which would hang it.
1576
+ response.ctx.retry_attempts = retries->retry_attempts();
1577
+ response.ctx.retry_reasons = retries->retry_reasons();
1578
+ handler(std::move(response));
1579
+ return;
1580
+ }
1581
+ // A cluster closed during the backoff is answered by the re-entry below, which
1582
+ // re-reads the component under the lock and reports cluster_closed -- the same
1583
+ // answer a caller gets when the cluster is already closed on arrival.
1584
+ auto next_request = *request_ptr;
1585
+ next_request.timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
1586
+ deadline - std::chrono::steady_clock::now());
1587
+ self->execute_protostellar_http(
1588
+ std::move(next_request), std::move(handler), retries, deadline);
1589
+ });
1590
+ return;
1591
+ }
1592
+ }
1593
+ }
1594
+ // As in dispatch_protostellar(): a declined retry reports the transport error, because the
1595
+ // strategy rather than the close is what ended the operation.
1596
+ response.ctx.retry_attempts = retries->retry_attempts();
1597
+ response.ctx.retry_reasons = retries->retry_reasons();
1598
+ handler(std::move(response));
1599
+ });
1600
+ }
1601
+ #endif
1602
+
808
1603
  void do_open(utils::movable_function<void(std::error_code)> handler)
809
1604
  {
1605
+ // After a DNS SRV lookup, close() may already have run; a bootstrap now would outlive it.
1606
+ if (stopped_) {
1607
+ return handler(errc::network::cluster_closed);
1608
+ }
810
1609
  // Warn users if idle_http_connection_timeout is too close to server idle timeouts
811
1610
  if (origin_.options().idle_http_connection_timeout > std::chrono::milliseconds(4'500)) {
812
1611
  CB_LOG_INFO("[{}]: The SDK may produce trivial warnings due to the idle HTTP connection "
@@ -841,7 +1640,16 @@ public:
841
1640
 
842
1641
  if (origin_.options().enable_tls) {
843
1642
  const auto ctx = tls_.get_ctx();
844
- auto ec = configure_tls_context(ctx);
1643
+ std::error_code ec{};
1644
+ {
1645
+ // configure_tls_context() reads the credentials, which a forked child's
1646
+ // set_authenticator() can rewrite while its reopen runs.
1647
+ const std::scoped_lock lock(update_credentials_mutex_);
1648
+ ec = configure_tls_context(ctx);
1649
+ if (!ec) {
1650
+ session_ = io::mcbp_session(id_, {}, ctx_, tls_, origin_, dns_srv_tracker_);
1651
+ }
1652
+ }
845
1653
  if (ec) {
846
1654
  #ifdef __clang_analyzer__
847
1655
  // TODO(CXXCBC-549): clang-tidy-19 reports potential memory leak here
@@ -851,13 +1659,14 @@ public:
851
1659
  return handler(ec);
852
1660
  });
853
1661
  }
854
- session_ = io::mcbp_session(id_, {}, ctx_, tls_, origin_, dns_srv_tracker_);
855
1662
  } else {
1663
+ const std::scoped_lock lock(update_credentials_mutex_);
856
1664
  session_ = io::mcbp_session(id_, {}, ctx_, origin_, dns_srv_tracker_);
857
1665
  }
858
1666
  session_->bootstrap([self = shared_from_this(), handler = std::move(handler)](
859
1667
  std::error_code ec, const topology::configuration& config) mutable {
860
1668
  if (!ec) {
1669
+ std::unique_lock origin_lock(self->origin_mutex_);
861
1670
  if (self->origin_.options().network == "auto") {
862
1671
  self->origin_.options().network =
863
1672
  config.select_network(self->session_->bootstrap_hostname());
@@ -871,208 +1680,44 @@ public:
871
1680
  self->origin_.options().network);
872
1681
  }
873
1682
  }
874
- if (self->origin_.options().network != "default") {
875
- self->origin_.set_nodes_from_config(config);
876
- CB_LOG_INFO(
877
- "replace list of bootstrap nodes with addresses of alternative network \"{}\": [{}]",
878
- self->origin_.options().network,
879
- utils::join_strings(self->origin_.get_nodes(), ","));
880
- }
881
- // FIXME(SA): fix the session manager to receive initial configuration and cluster-wide
882
- // session to poll for updates like the bucket does. Or just subscribe before the bootstrap.
883
- self->session_manager_->set_configuration(config, self->origin_.options());
884
- self->session_->on_configuration_update(self->session_manager_);
885
- self->session_->on_configuration_update(self->app_telemetry_reporter_);
886
- self->session_->on_configuration_update(self->cluster_label_listener_);
887
- self->app_telemetry_reporter_->update_config(config);
888
- self->cluster_label_listener_->update_config(config);
889
- self->session_->on_stop([self]() {
890
- if (self->session_) {
891
- self->session_.reset();
892
- }
893
- });
894
- }
895
- if (ec) {
896
- #ifdef __clang_analyzer__
897
- // TODO(CXXCBC-549): clang-tidy-19 reports potential memory leak here
898
- [[clang::suppress]]
899
- #endif
900
- return self->close([ec, handler = std::move(handler)]() mutable {
901
- handler(ec);
902
- });
903
- }
904
- handler(ec);
905
- });
906
- }
907
-
908
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
909
- void do_background_open()
910
- {
911
- // TODO(JC): retries on more failures? Right now only retry if load_verify_file() fails...
912
-
913
- // Disables TLS v1.2 which should be okay cloud/columnar default.
914
- auto ctx = tls_.get_ctx();
915
- configure_tls_options(true, ctx);
916
- if (origin_.options().security_options.trust_only_capella) {
917
- std::error_code ec{};
918
- CB_LOG_DEBUG(R"([{}]: use Capella CA for TLS verify)", id_);
919
- ctx->add_certificate_authority(
920
- asio::const_buffer(couchbase::core::default_ca::capellaCaCert,
921
- strlen(couchbase::core::default_ca::capellaCaCert)),
922
- ec);
923
- if (ec) {
924
- CB_LOG_WARNING("[{}]: unable to load Capella CAs: {}", id_, ec.message());
925
- // we don't consider this fatal and try to continue without it
926
- }
927
- } else if (origin_.options().security_options.trust_only_pem_file ||
928
- origin_.options().security_options.trust_only_pem_string) {
929
- if (origin_.options().trust_certificate.empty() /* No CA certificate (or other SDK-specific
930
- trust source) is specified */
931
- && origin_.options()
932
- .trust_certificate_value.empty() /* and certificate value has not been specified */
933
- && origin_.options().tls_verify !=
934
- tls_verify_mode::none /* The user did not disable all TLS verification */) {
935
- CB_LOG_WARNING("[{}] When TLS is enabled, the cluster options must specify certificate(s) "
936
- "to trust or ensure that they are "
937
- "available in system CA store.",
938
- id_);
939
- }
940
- std::error_code ec{};
941
- // load only the explicit certificate
942
- // system and default capella certificates are not loaded
943
- if (!origin_.options().trust_certificate_value.empty()) {
944
- CB_LOG_DEBUG(R"([{}]: use TLS certificate passed through via options object)", id_);
945
- ctx->add_certificate_authority(
946
- asio::const_buffer(origin_.options().trust_certificate_value.data(),
947
- origin_.options().trust_certificate_value.size()),
948
- ec);
949
- if (ec) {
950
- CB_LOG_WARNING(
951
- "[{}]: unable to load CA passed via options object: {}", id_, ec.message());
952
- }
953
- }
954
- if (!origin_.options().trust_certificate.empty()) {
955
- CB_LOG_DEBUG(
956
- R"([{}]: use TLS verify file: "{}")", id_, origin_.options().trust_certificate);
957
- ctx->load_verify_file(origin_.options().trust_certificate, ec);
958
- if (ec) {
959
- CB_LOG_ERROR("[{}]: unable to load verify file \"{}\": {}",
960
- id_,
961
- origin_.options().trust_certificate,
962
- ec.message());
963
- auto backoff = std::chrono::milliseconds(500);
964
- CB_LOG_DEBUG(
965
- "[{}] waiting for {}ms before retrying TLS verify file.", id_, backoff.count());
966
- backoff_then_retry(backoff, [self = shared_from_this()]() {
967
- self->do_background_open();
968
- });
969
- }
1683
+ if (self->origin_.options().network != "default") {
1684
+ self->origin_.set_nodes_from_config(config);
1685
+ CB_LOG_INFO(
1686
+ "replace list of bootstrap nodes with addresses of alternative network \"{}\": [{}]",
1687
+ self->origin_.options().network,
1688
+ logger::system_data_list(
1689
+ self->origin_.get_node_addresses(), logger::list_entries::quoted, ","));
1690
+ }
1691
+ origin_lock.unlock();
1692
+ // FIXME(SA): fix the session manager to receive initial configuration and cluster-wide
1693
+ // session to poll for updates like the bucket does. Or just subscribe before the bootstrap.
1694
+ self->session_manager_->set_configuration(config, self->origin_.options());
1695
+ self->session_->on_configuration_update(self->session_manager_);
1696
+ self->session_->on_configuration_update(self->app_telemetry_reporter_);
1697
+ self->session_->on_configuration_update(self->cluster_label_listener_);
1698
+ self->app_telemetry_reporter_->update_config(config);
1699
+ self->cluster_label_listener_->update_config(config);
1700
+ self->session_->on_stop([self]() {
1701
+ if (self->session_) {
1702
+ self->session_.reset();
1703
+ }
1704
+ });
970
1705
  }
971
- } else if (origin_.options().security_options.trust_only_platform) {
972
- // TODO(CXXCBC-548): security_options updates (use Mozilla certs?)
973
- CB_LOG_DEBUG(R"([{}]: use default CA for TLS verify)", id_);
974
- std::error_code ec{};
975
- // load system certificates
976
- ctx->set_default_verify_paths(ec);
977
1706
  if (ec) {
978
- CB_LOG_WARNING(R"([{}]: failed to load system CAs: {})", id_, ec.message());
979
- }
980
- } else if (!origin_.options().security_options.trust_only_certificates.empty()) {
981
- std::error_code ec{};
982
- CB_LOG_DEBUG("[{}]: loading {} user provided CA certificates.",
983
- id_,
984
- origin_.options().security_options.trust_only_certificates.size());
985
- for (const auto& cert : origin_.options().security_options.trust_only_certificates) {
986
- ctx->add_certificate_authority(asio::const_buffer(cert.data(), cert.size()), ec);
987
- if (ec) {
988
- CB_LOG_WARNING("[{}]: unable to load CA: {}", id_, ec.message());
989
- }
1707
+ #ifdef __clang_analyzer__
1708
+ // TODO(CXXCBC-549): clang-tidy-19 reports potential memory leak here
1709
+ [[clang::suppress]]
1710
+ #endif
1711
+ return self->close([ec, handler = std::move(handler)]() mutable {
1712
+ handler(ec);
1713
+ });
990
1714
  }
991
- }
992
- // TODO(CXXCBC-548): security_options updates (support cipher suites)
993
- // if (!origin_.options().security_options.cipher_suites.empty()) {
994
- // }
995
- config_tracker_ = std::make_shared<couchbase::core::io::cluster_config_tracker>(
996
- id_, origin_, ctx_, tls_, dns_srv_tracker_);
997
- config_tracker_->register_bootstrap_notification_subscriber(session_manager_);
998
- create_cluster_sessions();
999
- }
1000
-
1001
- void backoff_then_retry(std::chrono::milliseconds backoff,
1002
- utils::movable_function<void()> callback)
1003
- {
1004
- retry_backoff_.expires_after(backoff);
1005
- retry_backoff_.async_wait(
1006
- [self = shared_from_this(), cb = std::move(callback)](std::error_code ec) {
1007
- if (ec == asio::error::operation_aborted || self->stopped_) {
1008
- return;
1009
- }
1010
- if (ec) {
1011
- CB_LOG_WARNING("[{}] Retry callback received error ec={}.", self->id_, ec.message());
1012
- }
1013
- cb();
1014
- });
1015
- }
1016
-
1017
- void do_background_dns_srv_open()
1018
- {
1019
- std::string hostname;
1020
- std::string port;
1021
- std::tie(hostname, port) = origin_.next_address();
1022
- dns_srv_tracker_ = std::make_shared<impl::dns_srv_tracker>(
1023
- ctx_, hostname, origin_.options().dns_config, origin_.options().enable_tls);
1024
- return asio::post(asio::bind_executor(
1025
- ctx_, [self = shared_from_this(), hostname = std::move(hostname)]() mutable {
1026
- return self->dns_srv_tracker_->get_srv_nodes(
1027
- [self, hostname = std::move(hostname)](origin::node_list nodes,
1028
- std::error_code ec) mutable {
1029
- if (ec) {
1030
- auto backoff = std::chrono::milliseconds(500);
1031
- self->session_manager_->notify_bootstrap_error({ ec, ec.message(), hostname, {} });
1032
- CB_LOG_DEBUG(
1033
- "[{}] waiting for {}ms before retrying DNS query.", self->id_, backoff.count());
1034
- self->backoff_then_retry(backoff, [self]() {
1035
- self->do_background_dns_srv_open();
1036
- });
1037
- return;
1038
- }
1039
- if (!nodes.empty()) {
1040
- self->origin_.set_nodes(std::move(nodes));
1041
- CB_LOG_INFO(
1042
- "[{}] Replace list of bootstrap nodes with addresses from DNS SRV of \"{}\": [{}]",
1043
- self->id_,
1044
- hostname,
1045
- utils::join_strings(self->origin_.get_nodes(), ", "));
1046
- }
1047
- return self->do_background_open();
1048
- });
1049
- }));
1050
- }
1051
-
1052
- void create_cluster_sessions()
1053
- {
1054
- config_tracker_->create_sessions(
1055
- [self = shared_from_this()](std::error_code ec,
1056
- const topology::configuration& cfg,
1057
- const cluster_options& options) mutable {
1058
- if (ec) {
1059
- auto backoff = std::chrono::milliseconds(500);
1060
- CB_LOG_DEBUG("[{}] Waiting for {}ms before retrying to create cluster sessions.",
1061
- self->id_,
1062
- backoff.count());
1063
- self->backoff_then_retry(backoff, [self]() {
1064
- self->create_cluster_sessions();
1065
- });
1066
- } else {
1067
- self->session_manager_->set_configuration(cfg, options);
1068
- self->config_tracker_->on_configuration_update(self->session_manager_);
1069
- self->config_tracker_->on_configuration_update(self->app_telemetry_reporter_);
1070
- self->app_telemetry_reporter_->update_config(cfg);
1071
- self->config_tracker_->register_state_listener();
1072
- }
1073
- });
1715
+ // After the open handler, so a held request sees what the handler set up inline, such as the
1716
+ // transactions connect() creates when no metadata collection is configured.
1717
+ handler(ec);
1718
+ self->run_deferred_until_open();
1719
+ });
1074
1720
  }
1075
- #endif
1076
1721
 
1077
1722
  void with_bucket_configuration(
1078
1723
  const std::string& bucket_name,
@@ -1089,6 +1734,13 @@ public:
1089
1734
  bucket_name,
1090
1735
  [self = shared_from_this(), bucket_name, handler = std::move(handler)](auto ec) mutable {
1091
1736
  if (ec) {
1737
+ // If the cluster was torn down while this open_bucket was parked in the bootstrap window,
1738
+ // the bucket bootstrap is aborted with request_canceled. Surface that to the caller as
1739
+ // cluster_closed so an in-flight operation parked here (e.g. a scan) observes the
1740
+ // cluster-close contract rather than a bare cancellation.
1741
+ if (self->stopped_) {
1742
+ return handler(errc::network::cluster_closed, nullptr);
1743
+ }
1092
1744
  return handler(ec, nullptr);
1093
1745
  }
1094
1746
 
@@ -1099,6 +1751,32 @@ public:
1099
1751
  });
1100
1752
  }
1101
1753
 
1754
+ // with_bucket_configuration(), or unambiguous_timeout with no configuration once `deadline`
1755
+ // passes.
1756
+ void with_bucket_configuration(
1757
+ const std::string& bucket_name,
1758
+ std::chrono::steady_clock::time_point deadline,
1759
+ utils::movable_function<void(std::error_code, std::shared_ptr<topology::configuration>)>&&
1760
+ handler)
1761
+ {
1762
+ // Written before the completion is dispatched, and read only after it ran.
1763
+ auto config = std::make_shared<std::shared_ptr<topology::configuration>>();
1764
+ wait_until(
1765
+ deadline,
1766
+ [config, handler = std::move(handler)](std::error_code ec) mutable {
1767
+ handler(ec, ec ? nullptr : std::move(*config));
1768
+ },
1769
+ [&](auto&& done) {
1770
+ with_bucket_configuration(
1771
+ bucket_name,
1772
+ [config, done = std::forward<decltype(done)>(done)](
1773
+ std::error_code ec, std::shared_ptr<topology::configuration> c) mutable {
1774
+ *config = std::move(c);
1775
+ done(ec);
1776
+ });
1777
+ });
1778
+ }
1779
+
1102
1780
  void ping(std::optional<std::string> report_id,
1103
1781
  std::optional<std::string> bucket_name,
1104
1782
  std::set<service_type> services,
@@ -1108,7 +1786,7 @@ public:
1108
1786
  if (!report_id) {
1109
1787
  report_id = std::make_optional(uuid::to_string(uuid::random()));
1110
1788
  }
1111
- if (stopped_) {
1789
+ if (stopped_ || is_protostellar()) {
1112
1790
  return handler({ report_id.value(), meta::sdk_id() });
1113
1791
  }
1114
1792
  if (services.empty()) {
@@ -1128,20 +1806,44 @@ public:
1128
1806
  handler = std::move(handler)]() mutable {
1129
1807
  auto collector =
1130
1808
  std::make_shared<ping_collector_impl>(report_id.value(), std::move(handler));
1809
+ // Held until every endpoint this ping covers has taken a reporter. This prevents returning
1810
+ // early when the number of in-flight pings momentarily drops to 0.
1811
+ auto dispatch = collector->begin_dispatch();
1131
1812
  if (bucket_name) {
1132
1813
  if (services.find(service_type::key_value) != services.end()) {
1133
1814
  if (auto bucket = cluster->find_bucket_by_name(bucket_name.value()); bucket) {
1134
- return bucket->ping(collector, timeout);
1135
- }
1136
- cluster->open_bucket(
1137
- bucket_name.value(), [collector, cluster, bucket_name, timeout](std::error_code ec) {
1138
- if (!ec) {
1139
- if (auto bucket = cluster->find_bucket_by_name(bucket_name.value()); bucket) {
1140
- return bucket->ping(collector, timeout);
1815
+ bucket->ping(collector, timeout);
1816
+ } else {
1817
+ // Capture a weak_ptr, not a strong cluster: open_bucket parks this callback in the
1818
+ // bucket-bootstrap queue, which close() does not drain. A strong self-capture would
1819
+ // form a cluster<->parked-callback cycle that pins the ping collector past cluster
1820
+ // close, so its destructor -- the backstop that reports the ping result -- would
1821
+ // never run and the caller would hang. With a weak capture the callback is released
1822
+ // when the cluster is torn down, the collector is destroyed, and its destructor
1823
+ // delivers the report.
1824
+ //
1825
+ // The dispatch scope rides along so that the KV sessions this callback adds are
1826
+ // still covered by it.
1827
+ cluster->open_bucket(
1828
+ bucket_name.value(),
1829
+ [collector, dispatch, weak = std::weak_ptr(cluster), bucket_name, timeout](
1830
+ std::error_code ec) {
1831
+ if (!ec) {
1832
+ if (auto cluster = weak.lock()) {
1833
+ if (auto bucket = cluster->find_bucket_by_name(bucket_name.value()); bucket) {
1834
+ return bucket->ping(collector, timeout);
1835
+ }
1836
+ }
1141
1837
  }
1142
- }
1143
- });
1838
+ });
1839
+ }
1144
1840
  }
1841
+ // Query, search, analytics, management, eventing and views are cluster-level services,
1842
+ // not bucket-scoped. Ping them through the session manager just like the no-bucket path
1843
+ // so a bucket-scoped ping -- and therefore bucket::wait_until_ready() -- reports the
1844
+ // non-KV services too, rather than only key/value. The session manager pings only the
1845
+ // HTTP services it owns and ignores key_value in the requested set.
1846
+ cluster->session_manager_->ping(services, timeout, collector);
1145
1847
  } else {
1146
1848
  if (services.find(service_type::key_value) != services.end()) {
1147
1849
  if (cluster->session_) {
@@ -1162,7 +1864,7 @@ public:
1162
1864
  if (!report_id) {
1163
1865
  report_id = std::make_optional(uuid::to_string(uuid::random()));
1164
1866
  }
1165
- if (stopped_) {
1867
+ if (stopped_ || is_protostellar()) {
1166
1868
  return handler({ report_id.value(), couchbase::core::meta::sdk_id() });
1167
1869
  }
1168
1870
  asio::post(asio::bind_executor(
@@ -1179,6 +1881,27 @@ public:
1179
1881
  }));
1180
1882
  }
1181
1883
 
1884
+ // IMPORTANT — do not std::move observability members out of *self* below.
1885
+ //
1886
+ // The public-API cluster::notify_fork(fork_event::child) in
1887
+ // core/impl/public_cluster.cxx replaces the outer cluster_impl and drops
1888
+ // the old one; the old impl's destructor calls do_close() which lands
1889
+ // here. But user code that acquired a bucket/collection *before* the
1890
+ // fork holds its own copy of core::cluster — and core::cluster is a
1891
+ // shared_ptr<cluster_impl> wrapper — so the inner cluster_impl's
1892
+ // refcount stays > 0 and this object survives close().
1893
+ //
1894
+ // Anything moved out of `self->X_` becomes silently empty to those
1895
+ // still-live references on the next operation. tracer() and meter()
1896
+ // accessors return the member directly without checking stopped_, so an
1897
+ // empty shared_ptr there gets converted to an empty weak_ptr in
1898
+ // observability_recorder and then dereferenced as `this=nullptr` inside
1899
+ // tracer_wrapper::create_span. Same shape for the rest of the
1900
+ // observability/telemetry/orphan set.
1901
+ //
1902
+ // Members whose accessor already gates on stopped_ (e.g. session_manager_
1903
+ // via http_session_manager()) are safe to move out and intentionally
1904
+ // remain that way — callers there get a clean errc::network::cluster_closed.
1182
1905
  void close(utils::movable_function<void()>&& handler)
1183
1906
  {
1184
1907
  if (stopped_) {
@@ -1187,17 +1910,22 @@ public:
1187
1910
  stopped_ = true;
1188
1911
  asio::post(asio::bind_executor(
1189
1912
  ctx_, [self = shared_from_this(), handler = std::move(handler)]() mutable {
1190
- if (auto session = std::move(self->session_); session) {
1191
- session->stop(retry_reason::do_not_retry);
1913
+ // Re-issued after stopped_ is set, so each deferred request completes with cluster_closed.
1914
+ self->run_deferred_until_open();
1915
+ if (auto open_handler = self->take_dns_srv_open_handler(); open_handler) {
1916
+ open_handler(errc::network::cluster_closed);
1192
1917
  }
1193
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
1194
- if (self->config_tracker_) {
1195
- self->config_tracker_->close();
1196
- self->config_tracker_->unregister_bootstrap_notification_subscriber(
1197
- self->session_manager_);
1918
+ std::optional<io::mcbp_session> session{};
1919
+ std::shared_ptr<io::http_session_manager> session_manager{};
1920
+ {
1921
+ // update_credentials() uses both from the caller's thread.
1922
+ const std::scoped_lock lock(self->update_credentials_mutex_);
1923
+ session = std::move(self->session_);
1924
+ session_manager = std::move(self->session_manager_);
1925
+ }
1926
+ if (session) {
1927
+ session->stop(retry_reason::do_not_retry);
1198
1928
  }
1199
- self->retry_backoff_.cancel();
1200
- #endif
1201
1929
 
1202
1930
  std::map<std::string, std::shared_ptr<bucket>> buckets{};
1203
1931
  {
@@ -1207,29 +1935,86 @@ public:
1207
1935
  for (const auto& [_, bucket] : buckets) {
1208
1936
  bucket->close();
1209
1937
  }
1210
- if (const auto session_manager = std::move(self->session_manager_); session_manager) {
1211
- session_manager->close();
1212
- }
1213
- self->work_.reset();
1214
- if (const auto tracer = std::move(self->tracer_); tracer) {
1215
- tracer->stop();
1216
- }
1217
- if (const auto meter = std::move(self->meter_); meter) {
1218
- meter->stop();
1219
- }
1220
- if (const auto meter = std::move(self->app_telemetry_meter_); meter) {
1221
- meter->disable();
1222
- }
1223
- if (const auto reporter = std::move(self->app_telemetry_reporter_); reporter) {
1224
- reporter->stop();
1225
- }
1226
- if (const auto orphan_reporter = std::move(self->orphan_reporter_); orphan_reporter) {
1227
- orphan_reporter->stop();
1938
+ // The rest of the teardown waits until every HTTP session is stopped and holds no
1939
+ // read_some() call. The caller may stop the io_context as soon as `handler` runs, and an
1940
+ // HTTP session whose stop is still queued then is never torn down.
1941
+ if (session_manager) {
1942
+ return session_manager->close([self, handler = std::move(handler)]() mutable {
1943
+ self->finish_close(std::move(handler));
1944
+ });
1228
1945
  }
1229
- handler();
1946
+ self->finish_close(std::move(handler));
1230
1947
  }));
1231
1948
  }
1232
1949
 
1950
+ // The part of close() that runs once every HTTP session is stopped and holds no read_some() call.
1951
+ void finish_close(utils::movable_function<void()>&& handler)
1952
+ {
1953
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
1954
+ {
1955
+ // Drop the reference under the lock, but destroy the component outside it: ~component
1956
+ // drains in-flight gRPC calls, and holding protostellar_mutex_ across that would block
1957
+ // every concurrent execute() on the io thread for the duration of the drain.
1958
+ std::shared_ptr<protostellar::component> component;
1959
+ {
1960
+ const std::scoped_lock lock(protostellar_mutex_);
1961
+ component = std::move(protostellar_);
1962
+ protostellar_.reset();
1963
+ }
1964
+ }
1965
+ #endif
1966
+ // Hooks run before the handler: the caller may stop the io_context as soon as it runs, and an
1967
+ // operation waiting on a timer would then never complete.
1968
+ decltype(close_hooks_) hooks{};
1969
+ {
1970
+ const std::scoped_lock lock(close_hooks_mutex_);
1971
+ hooks = std::move(close_hooks_);
1972
+ close_hooks_.clear();
1973
+ }
1974
+ for (auto& [owner, hook] : hooks) {
1975
+ if (!owner.expired()) {
1976
+ hook();
1977
+ }
1978
+ }
1979
+ work_.reset();
1980
+ // Observability members: stop in place, do NOT std::move. See the
1981
+ // comment above close() for why this matters across fork(child).
1982
+ if (tracer_) {
1983
+ tracer_->stop();
1984
+ }
1985
+ if (meter_) {
1986
+ meter_->stop();
1987
+ }
1988
+ if (app_telemetry_meter_) {
1989
+ app_telemetry_meter_->disable();
1990
+ }
1991
+ if (app_telemetry_reporter_) {
1992
+ app_telemetry_reporter_->stop();
1993
+ }
1994
+ if (orphan_reporter_) {
1995
+ orphan_reporter_->stop();
1996
+ }
1997
+ handler();
1998
+ }
1999
+
2000
+ auto add_close_hook(std::weak_ptr<void> owner, utils::movable_function<void()>&& hook) -> bool
2001
+ {
2002
+ const std::scoped_lock lock(close_hooks_mutex_);
2003
+ // close() sets stopped_ before it posts the teardown, and finish_close() takes the hooks under
2004
+ // this mutex, so a hook that passes this check is called.
2005
+ if (stopped_) {
2006
+ return false;
2007
+ }
2008
+ close_hooks_.erase(std::remove_if(close_hooks_.begin(),
2009
+ close_hooks_.end(),
2010
+ [](const auto& entry) {
2011
+ return entry.first.expired();
2012
+ }),
2013
+ close_hooks_.end());
2014
+ close_hooks_.emplace_back(std::move(owner), std::move(hook));
2015
+ return true;
2016
+ }
2017
+
1233
2018
  auto direct_dispatch(const std::string& bucket_name,
1234
2019
  std::shared_ptr<couchbase::core::mcbp::queue_request> req) -> std::error_code
1235
2020
  {
@@ -1249,7 +2034,11 @@ public:
1249
2034
  if (ec) {
1250
2035
  return req->cancel(ec);
1251
2036
  }
1252
- self->direct_dispatch(bucket_name, std::move(req));
2037
+ // open_bucket()'s caller has already returned, so nothing reads this
2038
+ // error code and nothing else completes the request.
2039
+ if (const auto rc = self->direct_dispatch(bucket_name, req); rc) {
2040
+ req->try_callback({}, rc);
2041
+ }
1253
2042
  });
1254
2043
  return {};
1255
2044
  }
@@ -1274,11 +2063,99 @@ public:
1274
2063
  if (ec) {
1275
2064
  return req->cancel(ec);
1276
2065
  }
1277
- self->direct_re_queue(bucket_name, std::move(req), is_retry);
2066
+ // As in direct_dispatch(): this error code has no other
2067
+ // reader. Every error the bucket-level re-queue returns has
2068
+ // already completed the request, so this is a backstop for a
2069
+ // future path that does not -- try_callback() is idempotent.
2070
+ if (const auto rc = self->direct_re_queue(bucket_name, req, is_retry); rc) {
2071
+ req->try_callback({}, rc);
2072
+ }
1278
2073
  });
1279
2074
  return {};
1280
2075
  }
1281
2076
 
2077
+ // A request issued while open() bootstraps would read origin_ and session_, which the IO thread
2078
+ // writes until the bootstrap ends. While that window is open, the request made by `make_resume`
2079
+ // is held and this returns true. `make_resume` is called only then, so a request that is not
2080
+ // held is not moved from.
2081
+ //
2082
+ // The held request resumes exactly once: with no error when run_deferred_until_open() closes the
2083
+ // window, or with unambiguous_timeout when `deadline` passes first.
2084
+ template<typename MakeResume>
2085
+ auto defer_until_open(MakeResume&& make_resume,
2086
+ std::optional<std::chrono::steady_clock::time_point> deadline = {}) -> bool
2087
+ {
2088
+ const std::scoped_lock lock(deferred_until_open_mutex_);
2089
+ if (!opening_) {
2090
+ return false;
2091
+ }
2092
+ auto entry = std::make_shared<deferred_request>();
2093
+ entry->resume = std::forward<MakeResume>(make_resume)();
2094
+ if (deadline) {
2095
+ entry->deadline = std::make_unique<asio::steady_timer>(ctx_, deadline.value());
2096
+ entry->deadline->async_wait([weak_self = weak_from_this(), entry](std::error_code ec) {
2097
+ if (ec == asio::error::operation_aborted) {
2098
+ return;
2099
+ }
2100
+ // Removed now, or each request that times out during a slow bootstrap stays queued
2101
+ // until the bootstrap ends.
2102
+ if (auto self = weak_self.lock()) {
2103
+ const std::scoped_lock lock(self->deferred_until_open_mutex_);
2104
+ if (entry->queued) {
2105
+ self->deferred_until_open_.erase(*entry->queued);
2106
+ entry->queued.reset();
2107
+ }
2108
+ }
2109
+ entry->complete(errc::common::unambiguous_timeout);
2110
+ });
2111
+ }
2112
+ entry->queued = deferred_until_open_.insert(deferred_until_open_.end(), entry);
2113
+ return true;
2114
+ }
2115
+
2116
+ // Closes the bootstrap window and resumes what it held. Runs on the IO thread, at the end of a
2117
+ // successful bootstrap or at the start of close(); a failed bootstrap reaches close() first.
2118
+ void run_deferred_until_open()
2119
+ {
2120
+ std::list<std::shared_ptr<deferred_request>> deferred{};
2121
+ {
2122
+ const std::scoped_lock lock(deferred_until_open_mutex_);
2123
+ opening_ = false;
2124
+ std::swap(deferred, deferred_until_open_);
2125
+ for (const auto& entry : deferred) {
2126
+ entry->queued.reset();
2127
+ }
2128
+ }
2129
+ for (const auto& entry : deferred) {
2130
+ entry->complete({});
2131
+ }
2132
+ }
2133
+
2134
+ // A DNS SRV open's handler, taken by whichever of the lookup and close() finishes first.
2135
+ auto take_dns_srv_open_handler() -> utils::movable_function<void(std::error_code)>
2136
+ {
2137
+ const std::scoped_lock lock(dns_srv_open_handler_mutex_);
2138
+ auto handler = std::move(dns_srv_open_handler_);
2139
+ dns_srv_open_handler_ = nullptr;
2140
+ return handler;
2141
+ }
2142
+
2143
+ // The timeouts are not among the fields the bootstrap rewrites, so no lock is needed.
2144
+ [[nodiscard]] auto default_timeout_for(service_type type) const -> std::chrono::milliseconds
2145
+ {
2146
+ return origin_.options().default_timeout_for(type);
2147
+ }
2148
+
2149
+ // origin_ copied under origin_mutex_: the IO thread rewrites it while open() bootstraps.
2150
+ auto origin() const -> std::pair<std::error_code, couchbase::core::origin>
2151
+ {
2152
+ if (stopped_) {
2153
+ return { errc::network::cluster_closed, {} };
2154
+ }
2155
+ const std::scoped_lock lock(origin_mutex_);
2156
+ return { {}, origin_ };
2157
+ }
2158
+
1282
2159
  auto http_session_manager()
1283
2160
  -> std::pair<std::error_code, std::shared_ptr<io::http_session_manager>>
1284
2161
  {
@@ -1304,6 +2181,29 @@ public:
1304
2181
  }
1305
2182
 
1306
2183
  private:
2184
+ // Must be called from every open path, before the connection string is logged and before any
2185
+ // session or bucket is constructed, so that connection-context prefixes built later carry their
2186
+ // tags.
2187
+ //
2188
+ // Redaction state is process-wide while it is configured per cluster, so opening a cluster may
2189
+ // only turn it on, never off: one cluster must not silently disable redaction for another, or
2190
+ // for an application that enabled it directly.
2191
+ void apply_log_redaction() const
2192
+ {
2193
+ if (origin_.options().log_redaction) {
2194
+ logger::set_log_redaction(true);
2195
+ }
2196
+ // The warning has to test the state that is in effect rather than what this cluster asked for.
2197
+ // Redaction is process-wide, so another cluster or a direct set_log_redaction() call may have
2198
+ // turned it on, and a cluster opened with only dump_configuration would then write
2199
+ // unannotated dumps into a redacted log without saying so.
2200
+ if (origin_.options().dump_configuration && logger::is_log_redaction_enabled()) {
2201
+ CB_LOG_WARNING("log redaction is enabled, but so is dump_configuration: configuration dumps "
2202
+ "are not annotated for redaction and may contain values that are annotated "
2203
+ "elsewhere in the log");
2204
+ }
2205
+ }
2206
+
1307
2207
  void setup_observability()
1308
2208
  {
1309
2209
  // ignore the enable_tracing flag if a tracer was passed in
@@ -1352,15 +2252,10 @@ private:
1352
2252
 
1353
2253
  auto has_capella_host() const -> bool
1354
2254
  {
1355
- bool has_capella_host = false;
1356
- static const std::string suffix = "cloud.couchbase.com";
1357
- for (const auto& node : origin_.get_hostnames()) {
1358
- if (auto pos = node.find(suffix);
1359
- pos != std::string::npos && pos + suffix.size() == node.size()) {
1360
- has_capella_host = true;
1361
- }
1362
- }
1363
- return has_capella_host;
2255
+ auto hostnames = origin_.get_hostnames();
2256
+ return std::any_of(hostnames.begin(), hostnames.end(), [](const std::string& host) {
2257
+ return is_capella_host(host);
2258
+ });
1364
2259
  }
1365
2260
 
1366
2261
  std::string id_{ uuid::to_string(uuid::random()) };
@@ -1372,8 +2267,19 @@ private:
1372
2267
  std::optional<io::mcbp_session> session_{};
1373
2268
  std::shared_ptr<impl::dns_srv_tracker> dns_srv_tracker_{};
1374
2269
  std::mutex buckets_mutex_{};
2270
+ // Orders update_credentials() against do_open() constructing session_, and against close() moving
2271
+ // session_ and session_manager_ out.
2272
+ std::mutex update_credentials_mutex_{};
1375
2273
  std::map<std::string, std::shared_ptr<bucket>> buckets_{};
1376
2274
  couchbase::core::origin origin_{};
2275
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
2276
+ // Set when the cluster was opened with a couchbase2:// connection string; KV ops route here
2277
+ // instead of to the MCBP session manager. Guarded by protostellar_mutex_: execute() reads it on
2278
+ // the caller's thread while close() resets it on the io thread. Always reach it through
2279
+ // protostellar_component(). Mutable so the const observers can lock.
2280
+ mutable std::mutex protostellar_mutex_{};
2281
+ std::shared_ptr<protostellar::component> protostellar_{};
2282
+ #endif
1377
2283
  std::shared_ptr<class cluster_label_listener> cluster_label_listener_{
1378
2284
  std::make_shared<class cluster_label_listener>()
1379
2285
  };
@@ -1381,22 +2287,77 @@ private:
1381
2287
  std::shared_ptr<metrics::meter_wrapper> meter_{ nullptr };
1382
2288
  std::shared_ptr<orphan_reporter> orphan_reporter_{ nullptr };
1383
2289
  std::atomic_bool stopped_{ false };
2290
+ std::mutex close_hooks_mutex_{};
2291
+ std::vector<std::pair<std::weak_ptr<void>, utils::movable_function<void()>>> close_hooks_{};
2292
+ std::mutex deferred_until_open_mutex_{};
2293
+ bool opening_{ false };
2294
+ // Each entry's resume holds this cluster until it runs, as session_'s bootstrap callback does.
2295
+ // close() runs them, so the io_context must outlive the cluster and still be running when
2296
+ // close() is called.
2297
+ std::list<std::shared_ptr<deferred_request>> deferred_until_open_{};
2298
+ std::mutex dns_srv_open_handler_mutex_{};
2299
+ utils::movable_function<void(std::error_code)> dns_srv_open_handler_{};
2300
+ // Taken by origin(), update_credentials() and the writes to origin_ that can overlap them: the
2301
+ // assignment in open(), and the DNS SRV and bootstrap callbacks.
2302
+ mutable std::mutex origin_mutex_{};
1384
2303
  std::shared_ptr<core::app_telemetry_meter> app_telemetry_meter_{
1385
2304
  std::make_shared<core::app_telemetry_meter>()
1386
2305
  };
1387
-
1388
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
1389
- std::shared_ptr<couchbase::core::io::cluster_config_tracker> config_tracker_{};
1390
- asio::steady_timer retry_backoff_;
1391
- std::atomic_bool background_open_started_{ false };
1392
- #endif
1393
2306
  };
1394
2307
 
2308
+ namespace
2309
+ {
2310
+ // Holds a replica read issued while open() bootstraps, as cluster_impl::execute() holds a
2311
+ // key-value request. These reach the bucket through open_bucket(), which carries no deadline.
2312
+ // `make_error` builds the response for a request whose timeout passes first.
2313
+ template<typename Request, typename Handler, typename MakeError>
2314
+ auto
2315
+ defer_replica_read(const cluster& self,
2316
+ const std::shared_ptr<cluster_impl>& impl,
2317
+ Request& request,
2318
+ Handler& handler,
2319
+ MakeError make_error) -> bool
2320
+ {
2321
+ const auto deadline =
2322
+ std::chrono::steady_clock::now() +
2323
+ request.timeout.value_or(impl->default_timeout_for(service_type::key_value));
2324
+ return impl->defer_until_open(
2325
+ [&] {
2326
+ return [self,
2327
+ deadline,
2328
+ request = std::move(request),
2329
+ handler = std::move(handler),
2330
+ make_error = std::move(make_error)](std::error_code ec) mutable {
2331
+ if (ec) {
2332
+ return handler(make_error(request, ec));
2333
+ }
2334
+ request.timeout = remaining_until(deadline);
2335
+ self.execute(std::move(request), std::move(handler));
2336
+ };
2337
+ },
2338
+ deadline);
2339
+ }
2340
+
2341
+ // The response for a subdocument replica read that did not run.
2342
+ template<typename Response, typename Request>
2343
+ auto
2344
+ subdocument_replica_error(const Request& request, std::error_code ec) -> Response
2345
+ {
2346
+ return Response{ make_subdocument_error_context(
2347
+ make_key_value_error_context(ec, request.id), ec, {}, {}, false) };
2348
+ }
2349
+ } // namespace
2350
+
1395
2351
  cluster::cluster(asio::io_context& ctx)
1396
2352
  : impl_{ std::make_shared<cluster_impl>(ctx) }
1397
2353
  {
1398
2354
  }
1399
2355
 
2356
+ cluster::cluster(std::shared_ptr<cluster_impl> impl)
2357
+ : impl_{ std::move(impl) }
2358
+ {
2359
+ }
2360
+
1400
2361
  auto
1401
2362
  cluster::direct_dispatch(const std::string& bucket_name,
1402
2363
  std::shared_ptr<couchbase::core::mcbp::queue_request> req) const
@@ -1445,20 +2406,6 @@ cluster::open(couchbase::core::origin origin,
1445
2406
  }
1446
2407
  }
1447
2408
 
1448
- void
1449
- cluster::open_in_background(
1450
- [[maybe_unused]] const couchbase::core::origin& origin,
1451
- [[maybe_unused]] utils::movable_function<void(std::error_code)>&& handler) const
1452
- {
1453
- #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
1454
- if (impl_) {
1455
- impl_->open_in_background(origin, std::move(handler));
1456
- }
1457
- #else
1458
- CB_LOG_ERROR("Background open only available for Columnar builds.");
1459
- #endif
1460
- }
1461
-
1462
2409
  void
1463
2410
  cluster::diagnostics(std::optional<std::string> report_id,
1464
2411
  utils::movable_function<void(diag::diagnostics_result)>&& handler) const
@@ -1561,16 +2508,16 @@ cluster::execute(
1561
2508
  operations::get_all_replicas_request request,
1562
2509
  utils::movable_function<void(operations::get_all_replicas_response)>&& handler) const
1563
2510
  {
1564
- auto bucket_name = request.id.bucket();
1565
- return open_bucket(
1566
- bucket_name,
1567
- [impl = impl_, request = std::move(request), handler = std::move(handler)](auto ec) mutable {
1568
- if (ec) {
1569
- return handler(
1570
- operations::get_all_replicas_response{ make_key_value_error_context(ec, request.id) });
1571
- }
1572
- return request.execute(impl, std::move(handler));
1573
- });
2511
+ if (impl_->is_protostellar()) {
2512
+ return handler(operations::get_all_replicas_response{
2513
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
2514
+ }
2515
+ if (defer_replica_read(*this, impl_, request, handler, [](const auto& r, std::error_code ec) {
2516
+ return operations::get_all_replicas_response{ make_key_value_error_context(ec, r.id) };
2517
+ })) {
2518
+ return;
2519
+ }
2520
+ return request.execute(impl_, std::move(handler));
1574
2521
  }
1575
2522
 
1576
2523
  void
@@ -1592,16 +2539,16 @@ cluster::execute(
1592
2539
  operations::get_any_replica_request request,
1593
2540
  utils::movable_function<void(operations::get_any_replica_response)>&& handler) const
1594
2541
  {
1595
- auto bucket_name = request.id.bucket();
1596
- return open_bucket(
1597
- bucket_name,
1598
- [impl = impl_, request = std::move(request), handler = std::move(handler)](auto ec) mutable {
1599
- if (ec) {
1600
- return handler(
1601
- operations::get_any_replica_response{ make_key_value_error_context(ec, request.id) });
1602
- }
1603
- return request.execute(impl, std::move(handler));
1604
- });
2542
+ if (impl_->is_protostellar()) {
2543
+ return handler(operations::get_any_replica_response{
2544
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
2545
+ }
2546
+ if (defer_replica_read(*this, impl_, request, handler, [](const auto& r, std::error_code ec) {
2547
+ return operations::get_any_replica_response{ make_key_value_error_context(ec, r.id) };
2548
+ })) {
2549
+ return;
2550
+ }
2551
+ return request.execute(impl_, std::move(handler));
1605
2552
  }
1606
2553
 
1607
2554
  void
@@ -1637,6 +2584,19 @@ cluster::execute(
1637
2584
  operations::lookup_in_any_replica_request request,
1638
2585
  utils::movable_function<void(operations::lookup_in_any_replica_response)>&& handler) const
1639
2586
  {
2587
+ if (impl_->is_protostellar()) {
2588
+ return handler(operations::lookup_in_any_replica_response{ make_subdocument_error_context(
2589
+ make_key_value_error_context(errc::common::feature_not_available, request.id),
2590
+ errc::common::feature_not_available,
2591
+ {},
2592
+ {},
2593
+ false) });
2594
+ }
2595
+ if (defer_replica_read(*this, impl_, request, handler, [](const auto& r, std::error_code ec) {
2596
+ return subdocument_replica_error<operations::lookup_in_any_replica_response>(r, ec);
2597
+ })) {
2598
+ return;
2599
+ }
1640
2600
  return request.execute(impl_, std::move(handler));
1641
2601
  }
1642
2602
 
@@ -1645,6 +2605,19 @@ cluster::execute(
1645
2605
  operations::lookup_in_all_replicas_request request,
1646
2606
  utils::movable_function<void(operations::lookup_in_all_replicas_response)>&& handler) const
1647
2607
  {
2608
+ if (impl_->is_protostellar()) {
2609
+ return handler(operations::lookup_in_all_replicas_response{ make_subdocument_error_context(
2610
+ make_key_value_error_context(errc::common::feature_not_available, request.id),
2611
+ errc::common::feature_not_available,
2612
+ {},
2613
+ {},
2614
+ false) });
2615
+ }
2616
+ if (defer_replica_read(*this, impl_, request, handler, [](const auto& r, std::error_code ec) {
2617
+ return subdocument_replica_error<operations::lookup_in_all_replicas_response>(r, ec);
2618
+ })) {
2619
+ return;
2620
+ }
1648
2621
  return request.execute(impl_, std::move(handler));
1649
2622
  }
1650
2623
 
@@ -1669,6 +2642,111 @@ cluster::execute(operations::query_request request,
1669
2642
  return impl_->execute(std::move(request), std::move(handler));
1670
2643
  }
1671
2644
 
2645
+ void
2646
+ cluster::query_stream(operations::query_request request,
2647
+ utils::movable_function<void(couchbase::core::query_stream,
2648
+ error_context::query)>&& handler) const
2649
+ {
2650
+ if (const auto deadline =
2651
+ std::chrono::steady_clock::now() +
2652
+ request.timeout.value_or(impl_->default_timeout_for(service_type::query));
2653
+ impl_->defer_until_open(
2654
+ [&] {
2655
+ return
2656
+ [self = *this, deadline, request = std::move(request), handler = std::move(handler)](
2657
+ std::error_code ec) mutable {
2658
+ if (ec) {
2659
+ error_context::query ctx{};
2660
+ ctx.ec = ec;
2661
+ ctx.statement = request.statement;
2662
+ ctx.client_context_id = request.client_context_id.has_value()
2663
+ ? *request.client_context_id
2664
+ : uuid::to_string(uuid::random());
2665
+ return handler({}, std::move(ctx));
2666
+ }
2667
+ request.timeout = remaining_until(deadline);
2668
+ self.query_stream(std::move(request), std::move(handler));
2669
+ };
2670
+ },
2671
+ deadline)) {
2672
+ return;
2673
+ }
2674
+ // Mirror operations::query_request::encode_to: use_replica is only honored when the cluster
2675
+ // advertises read-from-replica support, otherwise the request is rejected up front. Rejected here
2676
+ // means nothing was dispatched, so the context can only describe the request itself.
2677
+ if (request.use_replica.has_value()) {
2678
+ auto reject = [&request](std::error_code ec) {
2679
+ error_context::query ctx{};
2680
+ ctx.ec = ec;
2681
+ ctx.statement = request.statement;
2682
+ // Generate an id when the caller supplied none, the way every dispatched path does, so even
2683
+ // a request rejected before dispatch has a context that can be correlated with the logs.
2684
+ // Not value_or(): its argument is evaluated whether or not the optional is engaged, which
2685
+ // would generate and format a UUID only to discard it whenever the caller supplied an id.
2686
+ ctx.client_context_id = request.client_context_id.has_value()
2687
+ ? *request.client_context_id
2688
+ : uuid::to_string(uuid::random());
2689
+ ctx.method = "POST";
2690
+ ctx.path = "/query/service";
2691
+ return ctx;
2692
+ };
2693
+ auto [ec, manager] = impl_->http_session_manager();
2694
+ if (ec) {
2695
+ return handler({}, reject(ec));
2696
+ }
2697
+ if (!manager->configuration_capabilities().supports_read_from_replica()) {
2698
+ return handler({}, reject(errc::common::feature_not_available));
2699
+ }
2700
+ }
2701
+
2702
+ auto cluster_opts = impl_->origin().second.options();
2703
+ auto default_timeout = cluster_opts.default_timeout_for(service_type::query);
2704
+ http_component http{ impl_->io_context(), core_sdk_shim{ *this } };
2705
+ const query_stream_component component{
2706
+ impl_->io_context(), std::move(http), default_timeout, cluster_opts.streaming
2707
+ };
2708
+ component.execute(std::move(request), std::move(handler));
2709
+ }
2710
+
2711
+ void
2712
+ cluster::analytics_query_stream(
2713
+ operations::analytics_request request,
2714
+ utils::movable_function<void(couchbase::core::analytics_stream, error_context::analytics)>&&
2715
+ handler) const
2716
+ {
2717
+ if (const auto deadline =
2718
+ std::chrono::steady_clock::now() +
2719
+ request.timeout.value_or(impl_->default_timeout_for(service_type::analytics));
2720
+ impl_->defer_until_open(
2721
+ [&] {
2722
+ return
2723
+ [self = *this, deadline, request = std::move(request), handler = std::move(handler)](
2724
+ std::error_code ec) mutable {
2725
+ if (ec) {
2726
+ error_context::analytics ctx{};
2727
+ ctx.ec = ec;
2728
+ ctx.statement = request.statement;
2729
+ ctx.client_context_id = request.client_context_id.has_value()
2730
+ ? *request.client_context_id
2731
+ : uuid::to_string(uuid::random());
2732
+ return handler({}, std::move(ctx));
2733
+ }
2734
+ request.timeout = remaining_until(deadline);
2735
+ self.analytics_query_stream(std::move(request), std::move(handler));
2736
+ };
2737
+ },
2738
+ deadline)) {
2739
+ return;
2740
+ }
2741
+ auto cluster_opts = impl_->origin().second.options();
2742
+ auto default_timeout = cluster_opts.default_timeout_for(service_type::analytics);
2743
+ http_component http{ impl_->io_context(), core_sdk_shim{ *this } };
2744
+ const analytics_stream_component component{
2745
+ impl_->io_context(), std::move(http), default_timeout, cluster_opts.streaming
2746
+ };
2747
+ component.execute(std::move(request), std::move(handler));
2748
+ }
2749
+
1672
2750
  void
1673
2751
  cluster::execute(operations::remove_request request,
1674
2752
  utils::movable_function<void(operations::remove_response)>&& handler) const
@@ -2331,8 +3409,8 @@ cluster::execute(
2331
3409
  }
2332
3410
 
2333
3411
  void
2334
- cluster::execute(impl::get_replica_request request,
2335
- utils::movable_function<void(impl::get_replica_response)>&& handler) const
3412
+ cluster::execute(operations::get_replica_request request,
3413
+ utils::movable_function<void(operations::get_replica_response)>&& handler) const
2336
3414
  {
2337
3415
  return impl_->execute(std::move(request), std::move(handler));
2338
3416
  }
@@ -2415,6 +3493,10 @@ void
2415
3493
  cluster::execute(operations::upsert_request_with_legacy_durability request,
2416
3494
  utils::movable_function<void(operations::upsert_response)>&& handler) const
2417
3495
  {
3496
+ if (impl_->is_protostellar()) {
3497
+ return handler(operations::upsert_response{
3498
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3499
+ }
2418
3500
  return request.execute(*this, std::move(handler));
2419
3501
  }
2420
3502
 
@@ -2422,6 +3504,10 @@ void
2422
3504
  cluster::execute(operations::insert_request_with_legacy_durability request,
2423
3505
  utils::movable_function<void(operations::insert_response)>&& handler) const
2424
3506
  {
3507
+ if (impl_->is_protostellar()) {
3508
+ return handler(operations::insert_response{
3509
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3510
+ }
2425
3511
  return request.execute(*this, std::move(handler));
2426
3512
  }
2427
3513
 
@@ -2429,6 +3515,10 @@ void
2429
3515
  cluster::execute(operations::append_request_with_legacy_durability request,
2430
3516
  utils::movable_function<void(operations::append_response)>&& handler) const
2431
3517
  {
3518
+ if (impl_->is_protostellar()) {
3519
+ return handler(operations::append_response{
3520
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3521
+ }
2432
3522
  return request.execute(*this, std::move(handler));
2433
3523
  }
2434
3524
 
@@ -2436,6 +3526,10 @@ void
2436
3526
  cluster::execute(operations::prepend_request_with_legacy_durability request,
2437
3527
  utils::movable_function<void(operations::prepend_response)>&& handler) const
2438
3528
  {
3529
+ if (impl_->is_protostellar()) {
3530
+ return handler(operations::prepend_response{
3531
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3532
+ }
2439
3533
  return request.execute(*this, std::move(handler));
2440
3534
  }
2441
3535
 
@@ -2443,6 +3537,10 @@ void
2443
3537
  cluster::execute(operations::replace_request_with_legacy_durability request,
2444
3538
  utils::movable_function<void(operations::replace_response)>&& handler) const
2445
3539
  {
3540
+ if (impl_->is_protostellar()) {
3541
+ return handler(operations::replace_response{
3542
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3543
+ }
2446
3544
  return request.execute(*this, std::move(handler));
2447
3545
  }
2448
3546
 
@@ -2450,6 +3548,14 @@ void
2450
3548
  cluster::execute(operations::mutate_in_request_with_legacy_durability request,
2451
3549
  utils::movable_function<void(operations::mutate_in_response)>&& handler) const
2452
3550
  {
3551
+ if (impl_->is_protostellar()) {
3552
+ return handler(operations::mutate_in_response{ make_subdocument_error_context(
3553
+ make_key_value_error_context(errc::common::feature_not_available, request.id),
3554
+ errc::common::feature_not_available,
3555
+ {},
3556
+ {},
3557
+ false) });
3558
+ }
2453
3559
  return request.execute(*this, std::move(handler));
2454
3560
  }
2455
3561
 
@@ -2457,6 +3563,10 @@ void
2457
3563
  cluster::execute(operations::remove_request_with_legacy_durability request,
2458
3564
  utils::movable_function<void(operations::remove_response)>&& handler) const
2459
3565
  {
3566
+ if (impl_->is_protostellar()) {
3567
+ return handler(operations::remove_response{
3568
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3569
+ }
2460
3570
  return request.execute(*this, std::move(handler));
2461
3571
  }
2462
3572
 
@@ -2464,6 +3574,10 @@ void
2464
3574
  cluster::execute(operations::increment_request_with_legacy_durability request,
2465
3575
  utils::movable_function<void(operations::increment_response)>&& handler) const
2466
3576
  {
3577
+ if (impl_->is_protostellar()) {
3578
+ return handler(operations::increment_response{
3579
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3580
+ }
2467
3581
  return request.execute(*this, std::move(handler));
2468
3582
  }
2469
3583
 
@@ -2471,6 +3585,10 @@ void
2471
3585
  cluster::execute(operations::decrement_request_with_legacy_durability request,
2472
3586
  utils::movable_function<void(operations::decrement_response)>&& handler) const
2473
3587
  {
3588
+ if (impl_->is_protostellar()) {
3589
+ return handler(operations::decrement_response{
3590
+ make_key_value_error_context(errc::common::feature_not_available, request.id) });
3591
+ }
2474
3592
  return request.execute(*this, std::move(handler));
2475
3593
  }
2476
3594
 
@@ -2497,7 +3615,7 @@ cluster::execute(operations::lookup_in_request_with_cancellation request,
2497
3615
 
2498
3616
  void
2499
3617
  cluster::execute(operations::get_replica_request_with_cancellation request,
2500
- utils::movable_function<void(impl::get_replica_response)>&& handler) const
3618
+ utils::movable_function<void(operations::get_replica_response)>&& handler) const
2501
3619
  {
2502
3620
  return impl_->execute(std::move(request), std::move(handler));
2503
3621
  }
@@ -2526,13 +3644,13 @@ cluster::to_string() const -> std::string
2526
3644
  }
2527
3645
 
2528
3646
  auto
2529
- cluster::tracer() const -> const std::shared_ptr<tracing::tracer_wrapper>&
3647
+ cluster::tracer() const -> std::shared_ptr<tracing::tracer_wrapper>
2530
3648
  {
2531
3649
  return impl_->tracer();
2532
3650
  }
2533
3651
 
2534
3652
  auto
2535
- cluster::meter() const -> const std::shared_ptr<metrics::meter_wrapper>&
3653
+ cluster::meter() const -> std::shared_ptr<metrics::meter_wrapper>
2536
3654
  {
2537
3655
  return impl_->meter();
2538
3656
  }
@@ -2543,4 +3661,42 @@ cluster::cluster_label_listener() const -> const std::shared_ptr<core::cluster_l
2543
3661
  return impl_->cluster_label_listener();
2544
3662
  }
2545
3663
 
3664
+ auto
3665
+ cluster::find_bucket_by_name(const std::string& name) const -> std::shared_ptr<bucket>
3666
+ {
3667
+ return impl_->find_bucket_by_name(name);
3668
+ }
3669
+
3670
+ auto
3671
+ cluster::is_protostellar() const -> bool
3672
+ {
3673
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
3674
+ if (impl_) {
3675
+ return impl_->is_protostellar();
3676
+ }
3677
+ #endif
3678
+ return false;
3679
+ }
3680
+
3681
+ auto
3682
+ cluster::add_close_hook(std::weak_ptr<void> owner, utils::movable_function<void()>&& hook) const
3683
+ -> bool
3684
+ {
3685
+ if (!impl_) {
3686
+ return false;
3687
+ }
3688
+ return impl_->add_close_hook(std::move(owner), std::move(hook));
3689
+ }
3690
+
3691
+ auto
3692
+ cluster::protostellar_channel_ready() const -> std::pair<std::error_code, bool>
3693
+ {
3694
+ #ifdef COUCHBASE_CXX_CLIENT_BUILD_COUCHBASE2
3695
+ if (impl_ && impl_->is_protostellar()) {
3696
+ return impl_->protostellar_channel_ready();
3697
+ }
3698
+ #endif
3699
+ return { errc::common::feature_not_available, false };
3700
+ }
3701
+
2546
3702
  } // namespace couchbase::core