couchbase 3.8.1 → 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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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 +214 -155
  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 +325 -25
  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 +66 -0
  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 +316 -317
  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 +275 -14
  131. data/ext/couchbase/core/io/streams.hxx +53 -0
  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/bucket_create.cxx +37 -38
  183. data/ext/couchbase/core/operations/management/bucket_update.cxx +26 -28
  184. data/ext/couchbase/core/operations/management/change_password.cxx +2 -0
  185. data/ext/couchbase/core/operations/management/collection_drop.cxx +5 -1
  186. data/ext/couchbase/core/operations/management/error_utils.cxx +59 -5
  187. data/ext/couchbase/core/operations/management/error_utils.hxx +29 -1
  188. data/ext/couchbase/core/operations/management/eventing_deploy_function.cxx +9 -3
  189. data/ext/couchbase/core/operations/management/eventing_drop_function.cxx +8 -3
  190. data/ext/couchbase/core/operations/management/eventing_get_all_functions.cxx +4 -2
  191. data/ext/couchbase/core/operations/management/eventing_get_function.cxx +5 -3
  192. data/ext/couchbase/core/operations/management/eventing_get_status.cxx +4 -2
  193. data/ext/couchbase/core/operations/management/eventing_pause_function.cxx +9 -3
  194. data/ext/couchbase/core/operations/management/eventing_resume_function.cxx +9 -3
  195. data/ext/couchbase/core/operations/management/eventing_undeploy_function.cxx +9 -3
  196. data/ext/couchbase/core/operations/management/eventing_upsert_function.cxx +9 -3
  197. data/ext/couchbase/core/operations/management/group_drop.cxx +3 -1
  198. data/ext/couchbase/core/operations/management/group_get.cxx +3 -1
  199. data/ext/couchbase/core/operations/management/group_upsert.cxx +11 -8
  200. data/ext/couchbase/core/operations/management/query_index_build.cxx +5 -2
  201. data/ext/couchbase/core/operations/management/query_index_create.cxx +9 -3
  202. data/ext/couchbase/core/operations/management/query_index_drop.cxx +11 -4
  203. data/ext/couchbase/core/operations/management/query_index_get_all.cxx +1 -1
  204. data/ext/couchbase/core/operations/management/query_index_get_all_deferred.cxx +1 -1
  205. data/ext/couchbase/core/operations/management/scope_drop.cxx +5 -1
  206. data/ext/couchbase/core/operations/management/scope_get_all.cxx +6 -1
  207. data/ext/couchbase/core/operations/management/search_get_stats.cxx +10 -0
  208. data/ext/couchbase/core/operations/management/search_index_analyze_document.cxx +3 -2
  209. data/ext/couchbase/core/operations/management/search_index_control_ingest.cxx +4 -3
  210. data/ext/couchbase/core/operations/management/search_index_control_plan_freeze.cxx +4 -3
  211. data/ext/couchbase/core/operations/management/search_index_control_query.cxx +4 -3
  212. data/ext/couchbase/core/operations/management/search_index_drop.cxx +2 -2
  213. data/ext/couchbase/core/operations/management/search_index_get.cxx +2 -2
  214. data/ext/couchbase/core/operations/management/search_index_get_documents_count.cxx +3 -2
  215. data/ext/couchbase/core/operations/management/search_index_get_stats.cxx +3 -1
  216. data/ext/couchbase/core/operations/management/search_index_upsert.cxx +2 -2
  217. data/ext/couchbase/core/operations/management/user_drop.cxx +3 -1
  218. data/ext/couchbase/core/operations/management/user_get.cxx +3 -1
  219. data/ext/couchbase/core/operations/management/user_upsert.cxx +12 -9
  220. data/ext/couchbase/core/operations/management/view_index_drop.cxx +1 -1
  221. data/ext/couchbase/core/operations/management/view_index_get.cxx +1 -1
  222. data/ext/couchbase/core/operations/management/view_index_upsert.cxx +1 -1
  223. data/ext/couchbase/core/operations/operation_traits.hxx +18 -0
  224. data/ext/couchbase/core/operations/query_response_parsing.cxx +211 -0
  225. data/ext/couchbase/core/operations/query_response_parsing.hxx +60 -0
  226. data/ext/couchbase/core/operations_fwd.hxx +3 -3
  227. data/ext/couchbase/core/origin.cxx +54 -26
  228. data/ext/couchbase/core/origin.hxx +12 -0
  229. data/ext/couchbase/core/orphan_reporter.cxx +2 -1
  230. data/ext/couchbase/core/platform/random.cc +7 -7
  231. data/ext/couchbase/core/protocol/client_request.cxx +2 -2
  232. data/ext/couchbase/core/protocol/client_request.hxx +38 -21
  233. data/ext/couchbase/core/protocol/client_response.cxx +11 -3
  234. data/ext/couchbase/core/protocol/client_response.hxx +27 -6
  235. data/ext/couchbase/core/protocol/cmd_append.cxx +2 -2
  236. data/ext/couchbase/core/protocol/cmd_cluster_map_change_notification.cxx +2 -2
  237. data/ext/couchbase/core/protocol/cmd_decrement.cxx +6 -6
  238. data/ext/couchbase/core/protocol/cmd_get.cxx +1 -1
  239. data/ext/couchbase/core/protocol/cmd_get_and_lock.cxx +2 -2
  240. data/ext/couchbase/core/protocol/cmd_get_and_touch.cxx +2 -2
  241. data/ext/couchbase/core/protocol/cmd_get_cluster_config.cxx +6 -4
  242. data/ext/couchbase/core/protocol/cmd_get_collection_id.cxx +2 -2
  243. data/ext/couchbase/core/protocol/cmd_get_error_map.cxx +1 -1
  244. data/ext/couchbase/core/protocol/cmd_get_meta.cxx +4 -4
  245. data/ext/couchbase/core/protocol/cmd_get_replica.cxx +1 -1
  246. data/ext/couchbase/core/protocol/cmd_hello.cxx +1 -1
  247. data/ext/couchbase/core/protocol/cmd_increment.cxx +6 -6
  248. data/ext/couchbase/core/protocol/cmd_insert.cxx +4 -4
  249. data/ext/couchbase/core/protocol/cmd_lookup_in.cxx +4 -3
  250. data/ext/couchbase/core/protocol/cmd_lookup_in_replica.cxx +4 -3
  251. data/ext/couchbase/core/protocol/cmd_mutate_in.cxx +10 -9
  252. data/ext/couchbase/core/protocol/cmd_mutate_in.hxx +10 -10
  253. data/ext/couchbase/core/protocol/cmd_observe_seqno.cxx +7 -7
  254. data/ext/couchbase/core/protocol/cmd_prepend.cxx +2 -2
  255. data/ext/couchbase/core/protocol/cmd_remove.cxx +2 -2
  256. data/ext/couchbase/core/protocol/cmd_replace.cxx +4 -4
  257. data/ext/couchbase/core/protocol/cmd_touch.cxx +1 -1
  258. data/ext/couchbase/core/protocol/cmd_upsert.cxx +4 -4
  259. data/ext/couchbase/core/protocol/datatype.hxx +118 -1
  260. data/ext/couchbase/core/protocol/frame_info_utils.cxx +1 -1
  261. data/ext/couchbase/core/protocol/magic.hxx +4 -0
  262. data/ext/couchbase/core/protocol/magic_fmt.hxx +3 -0
  263. data/ext/couchbase/core/protocol/server_request.hxx +3 -1
  264. data/ext/couchbase/core/protocol/status.cxx +1 -1
  265. data/ext/couchbase/core/protostellar/analytics_converter.hxx +171 -0
  266. data/ext/couchbase/core/protostellar/bucket_admin_converter.hxx +301 -0
  267. data/ext/couchbase/core/protostellar/collection_admin_converter.hxx +92 -0
  268. data/ext/couchbase/core/protostellar/component.cxx +2445 -0
  269. data/ext/couchbase/core/protostellar/component.hxx +391 -0
  270. data/ext/couchbase/core/{columnar/bootstrap_notification_subscriber.hxx → protostellar/compression_settings.hxx} +11 -10
  271. data/ext/couchbase/core/protostellar/credentials.cxx +233 -0
  272. data/ext/couchbase/core/protostellar/credentials.hxx +88 -0
  273. data/ext/couchbase/core/protostellar/dispatcher.cxx +57 -0
  274. data/ext/couchbase/core/protostellar/dispatcher.hxx +482 -0
  275. data/ext/couchbase/core/protostellar/error_utils.cxx +293 -0
  276. data/ext/couchbase/core/protostellar/error_utils.hxx +97 -0
  277. data/ext/couchbase/core/protostellar/json_payload.hxx +49 -0
  278. data/ext/couchbase/core/protostellar/kv_converter.cxx +70 -0
  279. data/ext/couchbase/core/protostellar/kv_converter.hxx +600 -0
  280. data/ext/couchbase/core/protostellar/query_converter.hxx +265 -0
  281. data/ext/couchbase/core/protostellar/query_index_admin_converter.hxx +291 -0
  282. data/ext/couchbase/core/protostellar/query_proto.hxx +73 -0
  283. data/ext/couchbase/core/protostellar/search_converter.hxx +244 -0
  284. data/ext/couchbase/core/protostellar/search_index_admin_converter.hxx +156 -0
  285. data/ext/couchbase/core/protostellar/view_converter.hxx +158 -0
  286. data/ext/couchbase/core/query_stream.cxx +447 -0
  287. data/ext/couchbase/core/query_stream.hxx +141 -0
  288. data/ext/couchbase/core/query_stream_component.cxx +243 -0
  289. data/ext/couchbase/core/query_stream_component.hxx +68 -0
  290. data/ext/couchbase/core/range_scan_load_balancer.cxx +15 -8
  291. data/ext/couchbase/core/range_scan_orchestrator.cxx +15 -3
  292. data/ext/couchbase/core/response_handler.hxx +41 -4
  293. data/ext/couchbase/core/row_streamer.cxx +332 -46
  294. data/ext/couchbase/core/row_streamer.hxx +31 -1
  295. data/ext/couchbase/core/row_streamer_options.hxx +55 -0
  296. data/ext/couchbase/core/sasl/client.h +3 -0
  297. data/ext/couchbase/core/search_scoring.hxx +66 -0
  298. data/ext/couchbase/core/stream_error_details.hxx +69 -0
  299. data/ext/couchbase/core/timeout_defaults.hxx +0 -1
  300. data/ext/couchbase/core/topology/capabilities.hxx +6 -0
  301. data/ext/couchbase/core/topology/capabilities_fmt.hxx +3 -0
  302. data/ext/couchbase/core/topology/configuration.cxx +21 -6
  303. data/ext/couchbase/core/topology/configuration.hxx +29 -8
  304. data/ext/couchbase/core/topology/configuration_json.hxx +3 -0
  305. data/ext/couchbase/core/topology/error_map_json.hxx +3 -1
  306. data/ext/couchbase/core/tracing/threshold_logging_tracer.cxx +70 -4
  307. data/ext/couchbase/core/tracing/threshold_logging_tracer.hxx +10 -0
  308. data/ext/couchbase/core/transactions/active_transaction_record.cxx +0 -34
  309. data/ext/couchbase/core/transactions/attempt_context_impl.cxx +362 -241
  310. data/ext/couchbase/core/transactions/attempt_context_impl.hxx +24 -3
  311. data/ext/couchbase/core/transactions/error_list.hxx +2 -2
  312. data/ext/couchbase/core/transactions/get_multi_fetch.hxx +160 -0
  313. data/ext/couchbase/core/transactions/get_multi_orchestrator.cxx +326 -125
  314. data/ext/couchbase/core/transactions/get_multi_transaction_id.hxx +62 -0
  315. data/ext/couchbase/core/transactions/internal/atr_entry.hxx +22 -2
  316. data/ext/couchbase/core/transactions/internal/transaction_context.hxx +2 -2
  317. data/ext/couchbase/core/transactions/internal/transactions_cleanup.hxx +23 -1
  318. data/ext/couchbase/core/transactions/internal/utils.hxx +50 -14
  319. data/ext/couchbase/core/transactions/staged_mutation.cxx +23 -18
  320. data/ext/couchbase/core/transactions/staged_mutation.hxx +15 -4
  321. data/ext/couchbase/core/transactions/transaction_context.cxx +55 -40
  322. data/ext/couchbase/core/transactions/transaction_get_result.hxx +16 -0
  323. data/ext/couchbase/core/transactions/transactions.cxx +3 -55
  324. data/ext/couchbase/core/transactions/transactions_cleanup.cxx +194 -66
  325. data/ext/couchbase/core/transactions/transactions_config.cxx +0 -11
  326. data/ext/couchbase/core/transactions/utils.cxx +10 -0
  327. data/ext/couchbase/core/transactions/waitable_op_list.hxx +15 -4
  328. data/ext/couchbase/core/transactions.hxx +9 -9
  329. data/ext/couchbase/core/utils/byteswap.hxx +166 -9
  330. data/ext/couchbase/core/utils/connection_string.cxx +50 -85
  331. data/ext/couchbase/core/utils/connection_string.hxx +15 -0
  332. data/ext/couchbase/core/{columnar/backoff_calculator.hxx → utils/deadline.hxx} +13 -13
  333. data/ext/couchbase/core/utils/duration_parser.cxx +1 -1
  334. data/ext/couchbase/core/utils/json_streaming_lexer.cxx +48 -5
  335. data/ext/couchbase/core/utils/json_streaming_lexer.hxx +4 -1
  336. data/ext/couchbase/core/utils/movable_function.hxx +301 -52
  337. data/ext/couchbase/core/utils/name_codec.hxx +154 -11
  338. data/ext/couchbase/core/{columnar/retry_info.hxx → utils/process_id.hxx} +18 -13
  339. data/ext/couchbase/core/utils/url_codec.cxx +12 -51
  340. data/ext/couchbase/core/websocket_codec.cxx +3 -29
  341. data/ext/couchbase/couchbase/analytics_options.hxx +21 -10
  342. data/ext/couchbase/couchbase/analytics_row.hxx +105 -0
  343. data/ext/couchbase/couchbase/analytics_stream_result.hxx +266 -0
  344. data/ext/couchbase/couchbase/boolean_query.hxx +3 -3
  345. data/ext/couchbase/couchbase/bucket.hxx +57 -0
  346. data/ext/couchbase/couchbase/cluster.hxx +216 -5
  347. data/ext/couchbase/{core/columnar/security_options.hxx → couchbase/cluster_state.hxx} +18 -14
  348. data/ext/couchbase/couchbase/codec/lenient_json_transcoder.hxx +69 -0
  349. data/ext/couchbase/couchbase/collection.hxx +234 -14
  350. data/ext/couchbase/couchbase/common_durability_options.hxx +18 -3
  351. data/ext/couchbase/couchbase/conjunction_query.hxx +1 -1
  352. data/ext/couchbase/couchbase/create_dataset_analytics_options.hxx +6 -1
  353. data/ext/couchbase/couchbase/date_range_query.hxx +2 -2
  354. data/ext/couchbase/couchbase/decrement_options.hxx +1 -1
  355. data/ext/couchbase/couchbase/disjunction_query.hxx +1 -1
  356. data/ext/couchbase/couchbase/error.hxx +32 -0
  357. data/ext/couchbase/couchbase/error_codes.hxx +32 -0
  358. data/ext/couchbase/couchbase/geo_bounding_box_query.hxx +1 -1
  359. data/ext/couchbase/couchbase/geo_distance_query.hxx +1 -1
  360. data/ext/couchbase/couchbase/geo_polygon_query.hxx +1 -1
  361. data/ext/couchbase/couchbase/get_replica_options.hxx +68 -0
  362. data/ext/couchbase/couchbase/get_replica_result.hxx +5 -3
  363. data/ext/couchbase/couchbase/get_replica_strategy.hxx +161 -0
  364. data/ext/couchbase/couchbase/get_result.hxx +1 -0
  365. data/ext/couchbase/couchbase/increment_options.hxx +1 -1
  366. data/ext/couchbase/couchbase/insert_options.hxx +1 -1
  367. data/ext/couchbase/couchbase/match_phrase_query.hxx +1 -1
  368. data/ext/couchbase/couchbase/match_query.hxx +1 -1
  369. data/ext/couchbase/couchbase/mutate_in_options.hxx +3 -3
  370. data/ext/couchbase/couchbase/network_options.hxx +1 -1
  371. data/ext/couchbase/couchbase/node_id.hxx +42 -14
  372. data/ext/couchbase/couchbase/node_id_for_options.hxx +7 -1
  373. data/ext/couchbase/couchbase/node_ids_options.hxx +8 -2
  374. data/ext/couchbase/couchbase/numeric_range_query.hxx +1 -1
  375. data/ext/couchbase/couchbase/phrase_query.hxx +1 -1
  376. data/ext/couchbase/couchbase/prefix_query.hxx +1 -1
  377. data/ext/couchbase/couchbase/query_options.hxx +23 -12
  378. data/ext/couchbase/couchbase/query_row.hxx +111 -0
  379. data/ext/couchbase/couchbase/query_stream_result.hxx +302 -0
  380. data/ext/couchbase/couchbase/query_string_query.hxx +3 -3
  381. data/ext/couchbase/couchbase/regexp_query.hxx +1 -1
  382. data/ext/couchbase/couchbase/replace_options.hxx +3 -3
  383. data/ext/couchbase/couchbase/result.hxx +14 -3
  384. data/ext/couchbase/couchbase/scan_result_item.hxx +1 -0
  385. data/ext/couchbase/couchbase/scope.hxx +85 -2
  386. data/ext/couchbase/couchbase/search_options.hxx +58 -8
  387. data/ext/couchbase/couchbase/search_scoring.hxx +94 -0
  388. data/ext/couchbase/couchbase/search_scoring_none.hxx +51 -0
  389. data/ext/couchbase/couchbase/search_scoring_reciprocal_rank_fusion.hxx +88 -0
  390. data/ext/couchbase/couchbase/search_scoring_relative_score_fusion.hxx +70 -0
  391. data/ext/couchbase/couchbase/term_range_query.hxx +1 -1
  392. data/ext/couchbase/couchbase/tracing/request_span.hxx +46 -0
  393. data/ext/couchbase/couchbase/transactions/transaction_get_multi_replicas_from_preferred_server_group_result.hxx +23 -21
  394. data/ext/couchbase/couchbase/transactions/transaction_get_multi_result.hxx +24 -19
  395. data/ext/couchbase/couchbase/transactions/transaction_get_result.hxx +17 -3
  396. data/ext/couchbase/couchbase/transactions/transactions_config.hxx +4 -2
  397. data/ext/couchbase/couchbase/transactions.hxx +27 -18
  398. data/ext/couchbase/couchbase/upsert_options.hxx +3 -3
  399. data/ext/couchbase/couchbase/vector_query.hxx +1 -1
  400. data/ext/couchbase/couchbase/wait_until_ready_options.hxx +111 -0
  401. data/ext/couchbase/couchbase/wildcard_query.hxx +1 -1
  402. data/ext/extconf.rb +15 -10
  403. data/ext/rcb_analytics.cxx +82 -102
  404. data/ext/rcb_backend.cxx +42 -36
  405. data/ext/rcb_backend.hxx +2 -0
  406. data/ext/rcb_buckets.cxx +47 -59
  407. data/ext/rcb_collections.cxx +42 -52
  408. data/ext/rcb_crud.cxx +345 -224
  409. data/ext/rcb_diagnostics.cxx +117 -27
  410. data/ext/rcb_exceptions.cxx +152 -99
  411. data/ext/rcb_exceptions.hxx +25 -0
  412. data/ext/rcb_extras.cxx +18 -19
  413. data/ext/rcb_hdr_histogram.cxx +19 -4
  414. data/ext/rcb_logger.cxx +202 -27
  415. data/ext/rcb_logger.hxx +12 -0
  416. data/ext/rcb_multi.cxx +40 -40
  417. data/ext/rcb_observability.cxx +7 -5
  418. data/ext/rcb_query.cxx +99 -142
  419. data/ext/rcb_range_scan.cxx +41 -35
  420. data/ext/rcb_search.cxx +98 -99
  421. data/ext/rcb_users.cxx +36 -56
  422. data/ext/rcb_utils.cxx +100 -37
  423. data/ext/rcb_utils.hxx +287 -33
  424. data/ext/rcb_version.cxx +1 -1
  425. data/ext/rcb_views.cxx +55 -51
  426. data/lib/active_support/cache/couchbase_store.rb +77 -12
  427. data/lib/couchbase/bucket.rb +20 -0
  428. data/lib/couchbase/cluster.rb +33 -14
  429. data/lib/couchbase/collection.rb +36 -1
  430. data/lib/couchbase/errors.rb +18 -0
  431. data/lib/couchbase/fork_hooks.rb +16 -7
  432. data/lib/couchbase/get_replica_strategy.rb +89 -0
  433. data/lib/couchbase/logger.rb +1 -1
  434. data/lib/couchbase/management/analytics_index_manager.rb +15 -15
  435. data/lib/couchbase/management/bucket_manager.rb +6 -6
  436. data/lib/couchbase/management/collection_manager.rb +6 -6
  437. data/lib/couchbase/management/query_index_manager.rb +7 -7
  438. data/lib/couchbase/management/search_index_manager.rb +13 -13
  439. data/lib/couchbase/management/user_manager.rb +10 -10
  440. data/lib/couchbase/management/view_index_manager.rb +5 -5
  441. data/lib/couchbase/options.rb +181 -44
  442. data/lib/couchbase/protostellar/bucket.rb +4 -0
  443. data/lib/couchbase/protostellar/cluster.rb +4 -0
  444. data/lib/couchbase/protostellar/collection.rb +4 -0
  445. data/lib/couchbase/protostellar/error_handling.rb +2 -1
  446. data/lib/couchbase/protostellar/request.rb +1 -0
  447. data/lib/couchbase/protostellar/request_generator/kv.rb +16 -2
  448. data/lib/couchbase/protostellar/request_generator/search.rb +17 -1
  449. data/lib/couchbase/protostellar/retry/orchestrator.rb +3 -2
  450. data/lib/couchbase/protostellar/retry/strategies/fail_fast.rb +33 -0
  451. data/lib/couchbase/protostellar/retry/strategies.rb +1 -0
  452. data/lib/couchbase/search_options.rb +95 -6
  453. data/lib/couchbase/version.rb +1 -1
  454. metadata +92 -33
  455. data/ext/0001-fix-build-for-mingw-w64-ucrt-x86_64-toolchain.patch +0 -40
  456. data/ext/couchbase/core/columnar/agent.hxx +0 -71
  457. data/ext/couchbase/core/columnar/agent_config.hxx +0 -59
  458. data/ext/couchbase/core/columnar/background_bootstrap_listener.hxx +0 -42
  459. data/ext/couchbase/core/columnar/backoff_calculator.cxx +0 -62
  460. data/ext/couchbase/core/columnar/columnar_agent.cxx +0 -147
  461. data/ext/couchbase/core/columnar/columnar_agent_config.cxx +0 -48
  462. data/ext/couchbase/core/columnar/columnar_error.cxx +0 -51
  463. data/ext/couchbase/core/columnar/database_management_options.hxx +0 -56
  464. data/ext/couchbase/core/columnar/error.hxx +0 -51
  465. data/ext/couchbase/core/columnar/error_codes.cxx +0 -107
  466. data/ext/couchbase/core/columnar/error_codes.hxx +0 -75
  467. data/ext/couchbase/core/columnar/management_component.cxx +0 -300
  468. data/ext/couchbase/core/columnar/management_component.hxx +0 -56
  469. data/ext/couchbase/core/columnar/query_component.cxx +0 -523
  470. data/ext/couchbase/core/columnar/query_component.hxx +0 -57
  471. data/ext/couchbase/core/columnar/query_options.hxx +0 -59
  472. data/ext/couchbase/core/columnar/query_result.cxx +0 -130
  473. data/ext/couchbase/core/columnar/query_result.hxx +0 -78
  474. data/ext/couchbase/core/io/config_tracker.cxx +0 -687
  475. data/ext/couchbase/core/io/config_tracker.hxx +0 -123
@@ -17,6 +17,8 @@
17
17
 
18
18
  #include "bucket.hxx"
19
19
 
20
+ #include "bucket_unit_test_api.hxx"
21
+
20
22
  #include "collection_id_cache_entry.hxx"
21
23
  #include "core/app_telemetry_meter.hxx"
22
24
  #include "core/cluster_options.hxx"
@@ -26,6 +28,7 @@
26
28
  #include "core/error_context/key_value_status_code.hxx"
27
29
  #include "core/io/mcbp_message.hxx"
28
30
  #include "core/logger/logger.hxx"
31
+ #include "core/logger/redaction.hxx"
29
32
  #include "core/mcbp/codec.hxx"
30
33
  #include "core/metrics/meter_wrapper.hxx"
31
34
  #include "core/protocol/client_opcode.hxx"
@@ -33,6 +36,7 @@
33
36
  #include "core/protocol/hello_feature.hxx"
34
37
  #include "core/response_handler.hxx"
35
38
  #include "core/service_type.hxx"
39
+ #include "core/tracing/constants.hxx"
36
40
  #include "core/tracing/tracer_wrapper.hxx"
37
41
  #include "core/utils/movable_function.hxx"
38
42
  #include "dispatcher.hxx"
@@ -45,6 +49,7 @@
45
49
  #include "protocol/cmd_get_cluster_config.hxx"
46
50
  #include "retry_orchestrator.hxx"
47
51
 
52
+ #include <couchbase/build_config.hxx>
48
53
  #include <couchbase/error_codes.hxx>
49
54
  #include <couchbase/retry_reason.hxx>
50
55
  #include <couchbase/retry_strategy.hxx>
@@ -94,7 +99,7 @@ public:
94
99
  tls_context_provider& tls)
95
100
  : client_id_{ std::move(client_id) }
96
101
  , name_{ std::move(name) }
97
- , log_prefix_{ fmt::format("[{}/{}]", client_id_, name_) }
102
+ , log_prefix_{ fmt::format("[{}/{}]", client_id_, logger::metadata(name_)) }
98
103
  , tracer_{ std::move(tracer) }
99
104
  , meter_{ std::move(meter) }
100
105
  , orphan_reporter_{ std::move(orphan_reporter) }
@@ -122,7 +127,11 @@ public:
122
127
  // TODO(SA): copy from mcbp_command, subject to refactor later
123
128
  #ifdef COUCHBASE_CXX_CLIENT_CREATE_OPERATION_SPAN_IN_CORE
124
129
  metrics::metric_attributes attrs{
125
- service_type::key_value, fmt::format("{}", req->command_), ec, name_, req->scope_name_,
130
+ tracing::service::key_value,
131
+ fmt::format("{}", req->command_),
132
+ metrics::standardized_error_type(ec),
133
+ name_,
134
+ req->scope_name_,
126
135
  req->collection_name_,
127
136
  };
128
137
  meter_->record_value(std::move(attrs), req->dispatched_time_);
@@ -141,8 +150,13 @@ public:
141
150
  // self->span_->add_tag(tracing::attributes::orphan, "canceled");
142
151
  return req->try_callback(resp, ec);
143
152
  }
144
- backoff_and_retry(
145
- req, reason == retry_reason::do_not_retry ? retry_reason::node_not_available : reason);
153
+ if (!backoff_and_retry(req,
154
+ reason == retry_reason::do_not_retry ? retry_reason::node_not_available
155
+ : reason)) {
156
+ // The retry was refused, so nothing else will carry this request: complete it here, as
157
+ // every other caller of backoff_and_retry() does.
158
+ req->try_callback(resp, ec);
159
+ }
146
160
  return;
147
161
  }
148
162
  key_value_status_code status{ key_value_status_code::unknown };
@@ -157,6 +171,58 @@ public:
157
171
  reason = retry_reason::key_value_error_map_retry_indicated;
158
172
  } else {
159
173
  switch (status) {
174
+ case key_value_status_code::unknown_collection:
175
+ /*
176
+ * SDK-RFC 0049 (SDK3 Retry Handling), section "Response Code Retry List":
177
+ * > Explicitly excluded here is the GetCollectionID KV command, since it returns 0x88 if
178
+ * > the collection is not found.
179
+ *
180
+ * The request completes with collection_not_found.
181
+ */
182
+ if (req->command_ == protocol::client_opcode::get_collection_id) {
183
+ reason = retry_reason::do_not_retry;
184
+ break;
185
+ }
186
+ /**
187
+ * The server does not recognise the collection id we sent. This covers two cases:
188
+ *
189
+ * 1. The collection was dropped and recreated, so its id changed. The cached id we sent
190
+ * is now stale and re-sending it would loop forever. We must invalidate the cache
191
+ * and re-resolve the id via GetCollectionID before retrying.
192
+ * 2. The node's collection manifest simply lags ns_server's view: the id is still valid
193
+ * but has not propagated to this particular KV node yet. Here re-sending the same id
194
+ * succeeds once the manifest catches up, bounded by the operation timeout.
195
+ *
196
+ * Prefer re-resolution: if the request was dispatched through the collection-id cache the
197
+ * hook invalidates the cached id and schedules a re-resolution retry (covering case 1,
198
+ * and case 2 too since GetCollectionID re-fetches). This matches the legacy mcbp_command
199
+ * path, which handled both via handle_unknown_collection(); when KV ops were migrated to
200
+ * this path that handling was dropped and the status surfaced directly as
201
+ * collection_not_found, which consumers such as the range scan orchestrator treat as
202
+ * fatal.
203
+ *
204
+ * If there is no hook (request bypassed the cid cache) or re-resolution gave up, fall
205
+ * back to a bounded collection-outdated retry with the same id, which still recovers
206
+ * case 2.
207
+ */
208
+ if (req->on_unknown_collection_ && req->on_unknown_collection_()) {
209
+ return;
210
+ }
211
+ reason = retry_reason::key_value_collection_outdated;
212
+ break;
213
+ case key_value_status_code::config_only:
214
+ /**
215
+ * The node has a configuration but is not serving data operations yet (e.g. during
216
+ * warmup or rebalance). Mirror the legacy path: fetch a fresh configuration and retry so
217
+ * the request is routed to a node that can serve it.
218
+ */
219
+ CB_LOG_DEBUG(
220
+ "{} server returned config_only status, meaning that the node does not serve "
221
+ "data operations, requesting new configuration and retrying",
222
+ log_prefix_);
223
+ fetch_config();
224
+ reason = retry_reason::service_response_code_indicated;
225
+ break;
160
226
  case key_value_status_code::locked:
161
227
  if (req->command_ != protocol::client_opcode::unlock) {
162
228
  /**
@@ -185,6 +251,7 @@ public:
185
251
  }
186
252
 
187
253
  void handle_response(std::shared_ptr<mcbp::queue_request> req,
254
+ std::optional<io::mcbp_session> /* session */,
188
255
  std::error_code error,
189
256
  retry_reason reason,
190
257
  io::mcbp_message msg,
@@ -213,21 +280,38 @@ public:
213
280
  if (ec == errc::common::request_canceled) {
214
281
  return req->cancel(ec);
215
282
  }
216
- self->direct_dispatch(req);
283
+ // This runs after direct_dispatch() returned, so its error code has nobody left to report
284
+ // to. Complete the request here rather than leave it to its deadline.
285
+ if (auto rc = self->direct_dispatch(req); rc) {
286
+ req->try_callback({}, rc);
287
+ }
217
288
  });
218
289
  }
219
290
 
220
291
  req->dispatched_time_ = std::chrono::steady_clock::now();
221
292
 
222
- auto session = route_request(req);
223
- if (!session || !session->has_config()) {
224
- return defer_command([self = shared_from_this(), req](std::error_code ec) mutable {
293
+ auto routed = route_request(req);
294
+ if (!routed.session || !routed.session->has_config()) {
295
+ if (!routed.connect_pending) {
296
+ // No bootstrap was started for this node, and only a bootstrap
297
+ // completion or a bucket close drains the deferred queue. Deferring
298
+ // here would leave the request waiting for its deadline, so retry
299
+ // instead: that re-routes against the current configuration.
300
+ if (backoff_and_retry(req, retry_reason::node_not_available)) {
301
+ return {};
302
+ }
303
+ return errc::common::service_not_available;
304
+ }
305
+ return defer_command([self = shared_from_this(), req](std::error_code ec) {
225
306
  if (ec == errc::common::request_canceled) {
226
307
  return req->cancel(ec);
227
308
  }
228
- self->direct_dispatch(std::move(req));
309
+ if (auto rc = self->direct_dispatch(req); rc) {
310
+ req->try_callback({}, rc);
311
+ }
229
312
  });
230
313
  }
314
+ auto session = routed.session;
231
315
  if (session->is_stopped()) {
232
316
  if (backoff_and_retry(req, retry_reason::node_not_available)) {
233
317
  return {};
@@ -244,7 +328,7 @@ public:
244
328
  {
245
329
  auto handle_error = [is_retry, req](std::error_code ec) {
246
330
  // We only want to log an error on retries if the error isn't cancelled.
247
- if (!is_retry || (is_retry && ec != errc::common::request_canceled)) {
331
+ if (!is_retry || ec != errc::common::request_canceled) {
248
332
  CB_LOG_ERROR("reschedule failed, failing request ({})", ec.message());
249
333
  }
250
334
 
@@ -253,15 +337,29 @@ public:
253
337
 
254
338
  CB_LOG_DEBUG("request being re-queued. opaque={}, opcode={}", req->opaque_, req->command_);
255
339
 
256
- auto session = route_request(req);
257
- if (!session || !session->has_config()) {
258
- return defer_command([self = shared_from_this(), req](std::error_code ec) {
340
+ auto routed = route_request(req);
341
+ if (!routed.session || !routed.session->has_config()) {
342
+ if (!routed.connect_pending) {
343
+ // As in direct_dispatch(): no bootstrap is pending for this node, so a
344
+ // deferred request would wait for its deadline instead of being sent.
345
+ if (backoff_and_retry(req, retry_reason::node_not_available)) {
346
+ return {};
347
+ }
348
+ // A re-queue is driven by a backoff timer that ignores what this
349
+ // returns, so the request has to be completed here.
350
+ handle_error(errc::common::service_not_available);
351
+ return errc::common::service_not_available;
352
+ }
353
+ return defer_command([self = shared_from_this(), req, handle_error](std::error_code ec) {
259
354
  if (ec == errc::common::request_canceled) {
260
355
  return req->cancel(ec);
261
356
  }
262
- self->direct_dispatch(req);
357
+ if (auto rc = self->direct_dispatch(req); rc) {
358
+ handle_error(rc);
359
+ }
263
360
  });
264
361
  }
362
+ auto session = routed.session;
265
363
  if (session->is_stopped()) {
266
364
  if (backoff_and_retry(req, retry_reason::node_not_available)) {
267
365
  return {};
@@ -302,23 +400,55 @@ public:
302
400
  -> bool
303
401
  {
304
402
  auto action = retry_orchestrator::should_retry(request, reason);
305
- auto retried = action.need_to_retry();
306
- if (retried) {
307
- auto timer = std::make_shared<asio::steady_timer>(ctx_);
308
- timer->expires_after(action.duration());
309
- timer->async_wait([self = shared_from_this(), request](auto error) {
310
- if (error == asio::error::operation_aborted) {
311
- return;
312
- }
313
- self->direct_re_queue(request, true);
314
- });
315
- request->set_retry_backoff(timer);
403
+ if (!action.need_to_retry()) {
404
+ return false;
405
+ }
406
+
407
+ // Park the request where close() can reach it. The timer below is the only
408
+ // thing that would re-dispatch it, and asio abandons a pending wait when the
409
+ // io_context goes away rather than completing it, so a request reachable only
410
+ // from that handler is neither completed nor released. A queue also lets
411
+ // cancel() take it back out, since a request de-queues from whichever queue
412
+ // holds it.
413
+ if (auto ec = retry_backoff_queue_.push(request, 0); ec) {
414
+ // Nothing will retry it: the bucket has closed, or it is already cancelled.
415
+ request->try_callback({}, errc::common::request_canceled);
416
+ return true;
316
417
  }
317
- return retried;
418
+
419
+ auto timer = std::make_shared<asio::steady_timer>(ctx_);
420
+ timer->expires_after(action.duration());
421
+ timer->async_wait([self = shared_from_this(), request](auto error) {
422
+ if (!self->retry_backoff_queue_.remove(request)) {
423
+ // close() completed it, or it was cancelled: either way it is done.
424
+ return;
425
+ }
426
+ if (error == asio::error::operation_aborted) {
427
+ request->try_callback({}, errc::common::request_canceled);
428
+ return;
429
+ }
430
+ self->direct_re_queue(request, true);
431
+ });
432
+ request->set_retry_backoff(timer);
433
+ return true;
318
434
  }
319
435
 
320
- auto route_request(const std::shared_ptr<mcbp::queue_request>& req)
321
- -> std::optional<io::mcbp_session>
436
+ struct routed_session {
437
+ std::optional<io::mcbp_session> session{};
438
+
439
+ /**
440
+ * False when no session exists for the node and no bootstrap was started
441
+ * for it, so nothing will drain the deferred queue. The caller must retry
442
+ * rather than defer; see connect_session().
443
+ *
444
+ * The default is false. route_request() returns a default-constructed
445
+ * value when the configuration names no server for the vbucket, and a
446
+ * request deferred on that has nothing to wait for.
447
+ */
448
+ bool connect_pending{ false };
449
+ };
450
+
451
+ auto route_request(const std::shared_ptr<mcbp::queue_request>& req) -> routed_session
322
452
  {
323
453
  if (req->key_.empty()) {
324
454
  if (auto server = server_by_vbucket(req->vbucket_, req->replica_index_); server) {
@@ -328,7 +458,7 @@ public:
328
458
  req->vbucket_ = partition;
329
459
  return find_or_connect_session_by_index(server.value());
330
460
  }
331
- return std::nullopt;
461
+ return {};
332
462
  }
333
463
 
334
464
  [[nodiscard]] auto server_by_vbucket(std::uint16_t vbucket, std::size_t node_index)
@@ -351,6 +481,12 @@ public:
351
481
  return { 0, std::nullopt };
352
482
  }
353
483
 
484
+ [[nodiscard]] auto config_snapshot() const -> std::shared_ptr<topology::configuration>
485
+ {
486
+ const std::scoped_lock lock(config_mutex_);
487
+ return config_;
488
+ }
489
+
354
490
  auto config_rev() const -> std::string
355
491
  {
356
492
  const std::scoped_lock lock(config_mutex_);
@@ -370,11 +506,33 @@ public:
370
506
  return { 0, std::nullopt };
371
507
  }
372
508
 
373
- void connect_session(std::size_t index)
509
+ /**
510
+ * A command deferred for this index is only ever dispatched or completed by
511
+ * one of two events: a bootstrap finishing, or the bucket closing.
512
+ *
513
+ * @return true when one of them is coming. Either a bootstrap is now pending
514
+ * for this index, or the bucket is closed and defer_command() completes the
515
+ * command with a cancellation. False when neither is, in which case the
516
+ * caller must retry: a command deferred now would wait for its deadline.
517
+ */
518
+ [[nodiscard]] auto connect_session(std::size_t index) -> bool
374
519
  {
375
520
  const std::scoped_lock lock(config_mutex_, sessions_mutex_);
376
521
  if (!config_) {
377
- return;
522
+ return false;
523
+ }
524
+ if (closed_) {
525
+ // Do not connect a session into a bucket that is being torn down: close() has already stopped
526
+ // sessions_ and will not run again, so a session added here would never be stopped and would
527
+ // strand the whole bucket/session graph as a pure cycle (see the closed_ guard in bootstrap()
528
+ // and update_config()).
529
+ return true;
530
+ }
531
+
532
+ if (index >= config_->nodes.size()) {
533
+ // The index was resolved against an earlier configuration and the node it
534
+ // named is no longer in the topology, so there is nothing to connect to.
535
+ return false;
378
536
  }
379
537
 
380
538
  const auto& node = config_->nodes[index];
@@ -383,7 +541,9 @@ public:
383
541
  auto port = node.port_or(
384
542
  origin_.options().network, service_type::key_value, origin_.options().enable_tls, 0);
385
543
  if (port == 0) {
386
- return;
544
+ // The node is in the topology but advertises no key-value port, so there
545
+ // is no session to open for it either.
546
+ return false;
387
547
  }
388
548
 
389
549
  const couchbase::core::origin origin(origin_.credentials(), hostname, port, origin_.options());
@@ -393,13 +553,12 @@ public:
393
553
  client_id_, node.node_uuid, ctx_, tls_, origin, state_listener_, name_, known_features_)
394
554
  : io::mcbp_session(
395
555
  client_id_, node.node_uuid, ctx_, origin, state_listener_, name_, known_features_);
396
- CB_LOG_DEBUG(R"({} rev={}, connect idx={}, session="{}", address="{}:{}")",
556
+ CB_LOG_DEBUG(R"({} rev={}, connect idx={}, session="{}", address="{}")",
397
557
  log_prefix_,
398
558
  config_->rev_str(),
399
559
  node.index,
400
560
  session.id(),
401
- hostname,
402
- port);
561
+ logger::system_data(fmt::format("{}:{}", hostname, port)));
403
562
  session.bootstrap(
404
563
  [self = shared_from_this(), session](std::error_code err,
405
564
  topology::configuration cfg) mutable -> void {
@@ -415,6 +574,7 @@ public:
415
574
  },
416
575
  true);
417
576
  sessions_.insert_or_assign(index, std::move(session));
577
+ return true;
418
578
  }
419
579
 
420
580
  void restart_sessions()
@@ -423,6 +583,13 @@ public:
423
583
  if (!config_) {
424
584
  return;
425
585
  }
586
+ if (closed_) {
587
+ // remove_session() posts restart_sessions() whenever a session drops. If that post is
588
+ // serviced after close() has torn the bucket down, recreating sessions here would strand them
589
+ // (nothing stops a session added after close()), leaking the whole bucket/session graph as a
590
+ // pure cycle. close() sets closed_ before it swaps and stops sessions_, so bail out.
591
+ return;
592
+ }
426
593
 
427
594
  std::size_t kv_node_index{ 0 };
428
595
  for (std::size_t index = 0; index < config_->nodes.size(); ++index) {
@@ -445,12 +612,11 @@ public:
445
612
  if (auto found_kv_node_index = ptr->first; found_kv_node_index != kv_node_index) {
446
613
  if (auto current = sessions_.find(kv_node_index); current == sessions_.end()) {
447
614
  CB_LOG_WARNING(
448
- R"({} KV node index mismatch: config rev={} states that address="{}:{}" should be at idx={}, )"
615
+ R"({} KV node index mismatch: config rev={} states that address="{}" should be at idx={}, )"
449
616
  R"(but it is at idx={} ("{}"). Moving session to idx={}.)",
450
617
  log_prefix_,
451
618
  config_->rev_str(),
452
- hostname,
453
- port,
619
+ logger::system_data(fmt::format("{}:{}", hostname, port)),
454
620
  kv_node_index,
455
621
  found_kv_node_index,
456
622
  ptr->second.id(),
@@ -459,17 +625,16 @@ public:
459
625
  sessions_.erase(ptr);
460
626
  } else {
461
627
  CB_LOG_WARNING(
462
- R"({} KV node index mismatch: config rev={} states that address="{}:{}" should be at idx={}, )"
628
+ R"({} KV node index mismatch: config rev={} states that address="{}" should be at idx={}, )"
463
629
  R"(but it is at idx={} ("{}"). Slot with idx={} is holds session with address="{}" ("{}"), swapping them.)",
464
630
  log_prefix_,
465
631
  config_->rev_str(),
466
- hostname,
467
- port,
632
+ logger::system_data(fmt::format("{}:{}", hostname, port)),
468
633
  kv_node_index,
469
634
  found_kv_node_index,
470
635
  ptr->second.id(),
471
636
  kv_node_index,
472
- current->second.bootstrap_address(),
637
+ logger::system_data(current->second.bootstrap_address()),
473
638
  current->second.id());
474
639
  std::swap(current->second, ptr->second);
475
640
  }
@@ -491,13 +656,12 @@ public:
491
656
  known_features_)
492
657
  : io::mcbp_session(
493
658
  client_id_, node.node_uuid, ctx_, origin, state_listener_, name_, known_features_);
494
- CB_LOG_DEBUG(R"({} rev={}, restart idx={}, session="{}", address="{}:{}")",
659
+ CB_LOG_DEBUG(R"({} rev={}, restart idx={}, session="{}", address="{}")",
495
660
  log_prefix_,
496
661
  config_->rev_str(),
497
662
  node.index,
498
663
  session.id(),
499
- hostname,
500
- port);
664
+ logger::system_data(fmt::format("{}:{}", hostname, port)));
501
665
  session.bootstrap(
502
666
  [self = shared_from_this(), session](std::error_code err,
503
667
  topology::configuration cfg) mutable {
@@ -523,12 +687,11 @@ public:
523
687
  const std::scoped_lock lock(sessions_mutex_);
524
688
  for (auto ptr = sessions_.cbegin(); ptr != sessions_.cend();) {
525
689
  if (ptr->second.id() == id) {
526
- CB_LOG_DEBUG(R"({} removed session id="{}", address="{}", bootstrap_address="{}:{}")",
690
+ CB_LOG_DEBUG(R"({} removed session id="{}", address="{}", bootstrap_address="{}")",
527
691
  log_prefix_,
528
692
  ptr->second.id(),
529
- ptr->second.remote_address(),
530
- ptr->second.bootstrap_hostname(),
531
- ptr->second.bootstrap_port());
693
+ logger::system_data(ptr->second.remote_address()),
694
+ logger::system_data(ptr->second.bootstrap_address()));
532
695
  ptr = sessions_.erase(ptr);
533
696
  found = true;
534
697
  } else {
@@ -545,6 +708,17 @@ public:
545
708
 
546
709
  void bootstrap(utils::movable_function<void(std::error_code, topology::configuration)>&& handler)
547
710
  {
711
+ if (closed_) {
712
+ // The bucket was closed before this bootstrap even started -- e.g. the cluster was torn down
713
+ // in the window between cluster_impl::open_bucket() inserting the bucket into buckets_ and
714
+ // calling bootstrap() (the buckets_ lock is released in between). close() is one-shot and
715
+ // will not run again, so registering a config listener or starting a session here would never
716
+ // be undone: the session, its self-capturing bootstrap continuation, and this bucket would be
717
+ // stranded as a pure cycle that LeakSanitizer/Valgrind report as leaked, and the continuation
718
+ // (which a scan/ping/etc. may be parked behind) would never fire. Bail before touching any
719
+ // shared state.
720
+ return handler(errc::common::request_canceled, {});
721
+ }
548
722
  if (state_listener_) {
549
723
  state_listener_->register_config_listener(shared_from_this());
550
724
  }
@@ -553,28 +727,68 @@ public:
553
727
  ? io::mcbp_session(
554
728
  client_id_, {}, ctx_, tls_, origin_, state_listener_, name_, known_features_)
555
729
  : io::mcbp_session(client_id_, {}, ctx_, origin_, state_listener_, name_, known_features_);
730
+ {
731
+ // Publish the in-flight session so close() can stop it if the cluster is torn down before
732
+ // this bootstrap completes; otherwise the continuation below (which captures new_session)
733
+ // would keep it alive forever. See bootstrapping_session_.
734
+ const std::scoped_lock lock(sessions_mutex_);
735
+ if (closed_) {
736
+ // close() ran concurrently after the entry check above (it sets closed_ before it swaps and
737
+ // stops sessions_ under sessions_mutex_). Do not publish a session close() has already
738
+ // swept past -- nothing would ever stop it. The bootstrap continuation is not registered
739
+ // yet, so returning here strands nothing.
740
+ return handler(errc::common::request_canceled, {});
741
+ }
742
+ bootstrapping_session_ = new_session;
743
+ }
556
744
  new_session.bootstrap([self = shared_from_this(), new_session, h = std::move(handler)](
557
745
  std::error_code ec, topology::configuration cfg) mutable {
558
746
  if (ec) {
559
747
  CB_LOG_WARNING(R"({} failed to bootstrap session ec={}, bucket="{}")",
560
748
  new_session.log_prefix(),
561
749
  ec.message(),
562
- self->name_);
750
+ logger::metadata(self->name_));
563
751
  self->remove_session(new_session.id());
564
- } else {
752
+ {
753
+ const std::scoped_lock lock(self->sessions_mutex_);
754
+ self->bootstrapping_session_.reset();
755
+ }
756
+ // Deliver the failure inline rather than through the asio::post below: on cluster teardown
757
+ // the io_context is stopped and any queued post is discarded, so a posted completion would
758
+ // be dropped -- a std::future caller then observes broken_promise and a callback caller
759
+ // hangs. On the error path the handler only reports the failure to the operation (it does
760
+ // not re-enter the bucket under a held lock here), so completing it synchronously is safe.
761
+ return h(ec, cfg);
762
+ }
763
+ {
565
764
  const std::size_t this_index = new_session.index();
566
- new_session.on_configuration_update(self);
567
- new_session.on_stop([id = new_session.id(), self]() {
568
- self->remove_session(id);
569
- });
570
-
765
+ bool established{ false };
571
766
  {
767
+ // Establish the session and stop tracking it as bootstrapping in one critical section, so
768
+ // a concurrent close() finds it in exactly one of the two containers (never neither).
572
769
  const std::scoped_lock lock(self->sessions_mutex_);
573
- self->sessions_.insert_or_assign(this_index, std::move(new_session));
770
+ self->bootstrapping_session_.reset();
771
+ if (self->closed_) {
772
+ // The bucket was closed while this session was still bootstrapping (close() sets
773
+ // closed_ before it swaps the session maps). Do not register callbacks or add it to
774
+ // sessions_: a session established after close() would be stranded, and its on_stop
775
+ // handler could post restart_sessions() during shutdown. Stop it here instead -- no
776
+ // on_stop is registered, so stopping under the lock cannot re-enter remove_session().
777
+ new_session.stop(retry_reason::do_not_retry);
778
+ } else {
779
+ new_session.on_configuration_update(self);
780
+ new_session.on_stop([id = new_session.id(), self]() {
781
+ self->remove_session(id);
782
+ });
783
+ self->sessions_.insert_or_assign(this_index, std::move(new_session));
784
+ established = true;
785
+ }
786
+ }
787
+ if (established) {
788
+ self->update_config(cfg);
789
+ self->drain_deferred_queue({});
790
+ self->poll_config({});
574
791
  }
575
- self->update_config(cfg);
576
- self->drain_deferred_queue({});
577
- self->poll_config({});
578
792
  }
579
793
  asio::post(
580
794
  asio::bind_executor(self->ctx_, [h = std::move(h), ec, cfg = std::move(cfg)]() mutable {
@@ -601,8 +815,12 @@ public:
601
815
  }
602
816
  return handler(errc::network::configuration_not_available, nullptr);
603
817
  }
604
- const std::scoped_lock lock(deferred_commands_mutex_);
605
- deferred_commands_.emplace(
818
+ // Park the continuation until the bucket is configured. defer_command re-checks closed_ under
819
+ // deferred_commands_mutex_, so a continuation that races close() is either drained or completed
820
+ // with request_canceled instead of being stranded. Stranding would leak the whole cluster
821
+ // graph: the continuation strongly captures the cluster_impl (core), which owns this bucket,
822
+ // which owns the deferred queue that would hold the orphaned continuation (CXXCBC-846).
823
+ defer_command(
606
824
  [self = shared_from_this(), handler = std::move(handler)](std::error_code ec) mutable {
607
825
  if (ec == errc::common::request_canceled || self->closed_ || !self->configured_) {
608
826
  return handler(errc::network::configuration_not_available, nullptr);
@@ -704,6 +922,14 @@ public:
704
922
 
705
923
  drain_deferred_queue(errc::common::request_canceled);
706
924
 
925
+ // A request waiting for a retry backoff is only ever re-dispatched by its
926
+ // timer, which a closed bucket never fires. Complete them here, for the same
927
+ // reason the deferred queue is drained above.
928
+ retry_backoff_queue_.close();
929
+ retry_backoff_queue_.drain([](const auto& request) {
930
+ request->try_callback({}, errc::common::request_canceled);
931
+ });
932
+
707
933
  if (state_listener_ != nullptr) {
708
934
  state_listener_->unregister_config_listener(shared_from_this());
709
935
  }
@@ -714,13 +940,20 @@ public:
714
940
  }
715
941
 
716
942
  std::map<size_t, io::mcbp_session> old_sessions;
943
+ std::optional<io::mcbp_session> bootstrapping_session;
717
944
  {
718
945
  const std::scoped_lock lock(sessions_mutex_);
719
946
  std::swap(old_sessions, sessions_);
947
+ std::swap(bootstrapping_session, bootstrapping_session_);
720
948
  }
721
949
  for (auto& [index, session] : old_sessions) {
722
950
  session.stop(retry_reason::do_not_retry);
723
951
  }
952
+ // Stop the session that is still bootstrapping (if any): this completes its bootstrap with an
953
+ // error, releasing the continuation that would otherwise strand the session and this bucket.
954
+ if (bootstrapping_session) {
955
+ bootstrapping_session->stop(retry_reason::do_not_retry);
956
+ }
724
957
  }
725
958
 
726
959
  /**
@@ -831,8 +1064,19 @@ public:
831
1064
  }
832
1065
  }
833
1066
  }
1067
+ std::vector<io::mcbp_session> dropped_sessions{};
834
1068
  if (!added.empty() || !removed.empty() || sequence_changed) {
835
1069
  const std::scoped_lock lock(sessions_mutex_);
1070
+ if (closed_) {
1071
+ // The bucket was closed concurrently (close() sets closed_ before it swaps and stops
1072
+ // sessions_ under sessions_mutex_). Do not rebuild the session list: any session created
1073
+ // here would be established after close() has already torn everything down. Because each
1074
+ // new session's bootstrap continuation strongly captures the session and this bucket (via
1075
+ // on_configuration_update()/on_stop()), and nothing stops a session added after close(),
1076
+ // it would strand the whole bucket/session graph as a pure cycle that LeakSanitizer reports
1077
+ // as leaked. bootstrap() guards the initial session the same way (see closed_ there).
1078
+ return;
1079
+ }
836
1080
  std::map<size_t, io::mcbp_session> new_sessions{};
837
1081
 
838
1082
  std::size_t next_index{ 0 };
@@ -848,12 +1092,11 @@ public:
848
1092
  for (auto it = sessions_.begin(); it != sessions_.end(); ++it) {
849
1093
  if (it->second.bootstrap_hostname() == hostname &&
850
1094
  it->second.bootstrap_port_number() == port) {
851
- CB_LOG_DEBUG(R"({} rev={}, preserve session="{}", address="{}:{}", index={}->{})",
1095
+ CB_LOG_DEBUG(R"({} rev={}, preserve session="{}", address="{}", index={}->{})",
852
1096
  log_prefix_,
853
1097
  config.rev_str(),
854
1098
  it->second.id(),
855
- it->second.bootstrap_hostname(),
856
- it->second.bootstrap_port(),
1099
+ logger::system_data(it->second.bootstrap_address()),
857
1100
  it->first,
858
1101
  next_index);
859
1102
  new_sessions.insert_or_assign(next_index, std::move(it->second));
@@ -885,23 +1128,21 @@ public:
885
1128
  known_features_)
886
1129
  : io::mcbp_session(
887
1130
  client_id_, node.node_uuid, ctx_, origin, state_listener_, name_, known_features_);
888
- CB_LOG_DEBUG(R"({} rev={}, add session="{}", address="{}:{}", index={})",
1131
+ CB_LOG_DEBUG(R"({} rev={}, add session="{}", address="{}", index={})",
889
1132
  log_prefix_,
890
1133
  config.rev_str(),
891
1134
  session.id(),
892
- hostname,
893
- port,
1135
+ logger::system_data(fmt::format("{}:{}", hostname, port)),
894
1136
  node.index);
895
1137
  session.bootstrap(
896
1138
  [self = shared_from_this(), session, idx = next_index](
897
1139
  std::error_code err, topology::configuration cfg) mutable {
898
1140
  if (err) {
899
1141
  CB_LOG_WARNING(
900
- R"({} failed to bootstrap session="{}", address="{}:{}", index={}, ec={})",
1142
+ R"({} failed to bootstrap session="{}", address="{}", index={}, ec={})",
901
1143
  session.log_prefix(),
902
1144
  session.id(),
903
- session.bootstrap_hostname(),
904
- session.bootstrap_port(),
1145
+ logger::system_data(session.bootstrap_address()),
905
1146
  idx,
906
1147
  err.message());
907
1148
  return self->remove_session(session.id());
@@ -920,18 +1161,24 @@ public:
920
1161
  std::swap(sessions_, new_sessions);
921
1162
 
922
1163
  for (auto it = new_sessions.begin(); it != new_sessions.end(); ++it) {
923
- CB_LOG_DEBUG(R"({} rev={}, drop session="{}", address="{}:{}", index={})",
1164
+ CB_LOG_DEBUG(R"({} rev={}, drop session="{}", address="{}", index={})",
924
1165
  log_prefix_,
925
1166
  config.rev_str(),
926
1167
  it->second.id(),
927
- it->second.bootstrap_hostname(),
928
- it->second.bootstrap_port(),
1168
+ logger::system_data(it->second.bootstrap_address()),
929
1169
  it->first);
930
- asio::post(asio::bind_executor(ctx_, [session = std::move(it->second)]() mutable {
931
- return session.stop(retry_reason::do_not_retry);
932
- }));
1170
+ dropped_sessions.push_back(std::move(it->second));
933
1171
  }
934
1172
  }
1173
+ // Stop dropped sessions synchronously, after releasing sessions_mutex_ (stop() fires on_stop ->
1174
+ // remove_session(), which re-acquires the mutex). Stopping synchronously -- rather than via
1175
+ // asio::post -- clears each session's bucket-capturing handlers (config_listeners_,
1176
+ // on_stop_handler_) right now. A posted stop can be abandoned by io_context shutdown before it
1177
+ // runs, leaving those handlers set and stranding the whole bucket/session graph as a pure cycle
1178
+ // (LeakSanitizer reports it as all-indirect leaks).
1179
+ for (auto& session : dropped_sessions) {
1180
+ session.stop(retry_reason::do_not_retry);
1181
+ }
935
1182
  }
936
1183
 
937
1184
  [[nodiscard]] auto find_session_by_index(std::size_t index) const
@@ -944,14 +1191,15 @@ public:
944
1191
  return {};
945
1192
  }
946
1193
 
947
- [[nodiscard]] auto find_or_connect_session_by_index(std::size_t index)
948
- -> std::optional<io::mcbp_session>
1194
+ [[nodiscard]] auto find_or_connect_session_by_index(std::size_t index) -> routed_session
949
1195
  {
950
1196
  if (auto session = find_session_by_index(index); session) {
951
- return session;
1197
+ // A session exists, so it is safe to defer behind it. That holds even
1198
+ // if it is still bootstrapping: the completion that finishes it is what
1199
+ // drains the deferred queue.
1200
+ return { session, true };
952
1201
  }
953
- connect_session(index);
954
- return {};
1202
+ return { {}, connect_session(index) };
955
1203
  }
956
1204
 
957
1205
  [[nodiscard]] auto next_session_index() -> std::size_t
@@ -1048,9 +1296,18 @@ public:
1048
1296
 
1049
1297
  auto defer_command(utils::movable_function<void(std::error_code)> command) -> std::error_code
1050
1298
  {
1051
- const std::scoped_lock lock_for_deferred_commands(deferred_commands_mutex_);
1052
- deferred_commands_.emplace(std::move(command));
1053
- return {};
1299
+ {
1300
+ const std::scoped_lock lock_for_deferred_commands(deferred_commands_mutex_);
1301
+ if (!closed_) {
1302
+ deferred_commands_.emplace(std::move(command));
1303
+ return {};
1304
+ }
1305
+ }
1306
+ // The bucket was closed (and its deferred queue already drained) before this command could be
1307
+ // parked. Enqueuing now would strand the command forever, leaking whatever its continuation
1308
+ // captures. Complete it with a cancellation instead so its state is released (CXXCBC-846).
1309
+ command(errc::common::request_canceled);
1310
+ return errc::common::request_canceled;
1054
1311
  }
1055
1312
 
1056
1313
  void for_each_session(utils::movable_function<void(io::mcbp_session&)> handler)
@@ -1097,7 +1354,17 @@ private:
1097
1354
  std::queue<utils::movable_function<void(std::error_code)>> deferred_commands_{};
1098
1355
  std::mutex deferred_commands_mutex_{};
1099
1356
 
1357
+ // Requests waiting for a retry backoff, so that close() can complete them.
1358
+ mcbp::operation_queue retry_backoff_queue_{};
1359
+
1100
1360
  std::map<size_t, io::mcbp_session> sessions_{};
1361
+ // The very first session is only moved into sessions_ once it finishes bootstrapping; until then
1362
+ // it lives solely inside its own bootstrap continuation (which captures a copy of the session and
1363
+ // this bucket). If the cluster is closed while that bootstrap is in flight (e.g. a bucket that
1364
+ // never opens), close() would otherwise never see the session, so its self-capture would strand
1365
+ // the session, the bucket, and the whole cluster graph. Track it here so close() can stop it and
1366
+ // let the continuation fire. Guarded by sessions_mutex_.
1367
+ std::optional<io::mcbp_session> bootstrapping_session_{};
1101
1368
  mutable std::mutex sessions_mutex_{};
1102
1369
  std::atomic_size_t round_robin_next_{ 0 };
1103
1370
  };
@@ -1287,6 +1554,33 @@ bucket::config_rev() const -> std::string
1287
1554
  return impl_->config_rev();
1288
1555
  }
1289
1556
 
1557
+ auto
1558
+ bucket::config_snapshot() const -> std::shared_ptr<topology::configuration>
1559
+ {
1560
+ return impl_->config_snapshot();
1561
+ }
1562
+
1563
+ bucket_unit_test_api::bucket_unit_test_api(std::shared_ptr<bucket_impl> impl)
1564
+ : impl_{ std::move(impl) }
1565
+ {
1566
+ }
1567
+
1568
+ void
1569
+ bucket_unit_test_api::resolve_response(const std::shared_ptr<mcbp::queue_request>& request,
1570
+ const std::shared_ptr<mcbp::queue_response>& response,
1571
+ std::error_code ec,
1572
+ retry_reason reason,
1573
+ std::optional<key_value_error_map_info> error_info)
1574
+ {
1575
+ impl_->resolve_response(request, response, ec, reason, std::move(error_info));
1576
+ }
1577
+
1578
+ auto
1579
+ bucket::unit_test_api() -> bucket_unit_test_api
1580
+ {
1581
+ return bucket_unit_test_api(impl_);
1582
+ }
1583
+
1290
1584
  auto
1291
1585
  bucket::direct_dispatch(std::shared_ptr<mcbp::queue_request> req) -> std::error_code
1292
1586
  {
@@ -1300,8 +1594,8 @@ bucket::direct_re_queue(const std::shared_ptr<mcbp::queue_request>& req, bool is
1300
1594
  return impl_->direct_re_queue(req, is_retry);
1301
1595
  }
1302
1596
 
1303
- void
1304
- bucket::connect_session(std::size_t index)
1597
+ auto
1598
+ bucket::connect_session(std::size_t index) -> bool
1305
1599
  {
1306
1600
  return impl_->connect_session(index);
1307
1601
  }