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
@@ -42,6 +42,7 @@
42
42
  #include "internal/utils.hxx"
43
43
  #include "staged_mutation.hxx"
44
44
 
45
+ #include <cassert>
45
46
  #include <optional>
46
47
 
47
48
  namespace couchbase::core::transactions
@@ -200,11 +201,14 @@ attempt_context_impl::check_and_handle_blocking_transactions(
200
201
  if (err) {
201
202
  return cb(err);
202
203
  }
203
- return check_atr_entry_for_blocking_document(doc,
204
- exp_delay(std::chrono::milliseconds(50),
205
- std::chrono::milliseconds(500),
206
- std::chrono::seconds(1)),
207
- std::move(cb));
204
+ return check_atr_entry_for_blocking_document(
205
+ doc,
206
+ std::make_shared<async_timed_exp_delay>(
207
+ std::make_shared<asio::steady_timer>(cluster_ref().io_context()),
208
+ std::chrono::milliseconds(50),
209
+ std::chrono::milliseconds(500),
210
+ std::chrono::seconds(1)),
211
+ std::move(cb));
208
212
  }
209
213
  CB_ATTEMPT_CTX_LOG_DEBUG(this,
210
214
  "doc \"{}\" is in another transaction {}, but doesn't have enough "
@@ -233,10 +237,12 @@ attempt_context_impl::get(const core::document_id& id) -> transaction_get_result
233
237
  void
234
238
  attempt_context_impl::get(const core::document_id& id, Callback&& cb)
235
239
  {
236
- if (op_list_.get_mode().is_query()) {
237
- return get_with_query(id, false, std::move(cb));
238
- }
239
240
  cache_error_async(cb, [self = shared_from_this(), id, cb]() mutable {
241
+ // Mode is read after cache_error_async incremented the op count, closing the
242
+ // race with set_query_mode() -- see the comment in insert() for the rationale.
243
+ if (self->op_list_.get_mode().is_query()) {
244
+ return self->get_with_query(id, false, std::move(cb));
245
+ }
240
246
  self->check_if_done(cb);
241
247
  self->do_get(
242
248
  id,
@@ -323,10 +329,12 @@ void
323
329
  attempt_context_impl::get_optional(const core::document_id& id, Callback&& cb)
324
330
  {
325
331
 
326
- if (op_list_.get_mode().is_query()) {
327
- return get_with_query(id, true, std::move(cb));
328
- }
329
332
  cache_error_async(cb, [self = shared_from_this(), id, cb]() mutable {
333
+ // Mode is read after cache_error_async incremented the op count, closing the
334
+ // race with set_query_mode() -- see the comment in insert() for the rationale.
335
+ if (self->op_list_.get_mode().is_query()) {
336
+ return self->get_with_query(id, true, std::move(cb));
337
+ }
330
338
  self->ensure_open_bucket(
331
339
  id.bucket(), [self, id, cb = std::move(cb)](std::error_code ec) mutable {
332
340
  if (ec) {
@@ -886,11 +894,13 @@ attempt_context_impl::replace(const transaction_get_result& document,
886
894
  Callback&& cb)
887
895
  {
888
896
 
889
- if (op_list_.get_mode().is_query()) {
890
- return replace_raw_with_query(document, std::move(content), std::move(cb));
891
- }
892
897
  return cache_error_async(
893
898
  cb, [self = shared_from_this(), cb, document, content = std::move(content)]() mutable {
899
+ // Mode is read after cache_error_async incremented the op count, closing the
900
+ // race with set_query_mode() -- see the comment in insert() for the rationale.
901
+ if (self->op_list_.get_mode().is_query()) {
902
+ return self->replace_raw_with_query(document, std::move(content), std::move(cb));
903
+ }
894
904
  self->ensure_open_bucket(
895
905
  document.bucket(),
896
906
  [self, cb = std::move(cb), document, content = std::move(content)](
@@ -912,7 +922,7 @@ attempt_context_impl::replace(const transaction_get_result& document,
912
922
  CB_ATTEMPT_CTX_LOG_TRACE(
913
923
  self, "replacing {} with {}", document, utils::to_string(content.data));
914
924
  self->check_if_done(cb);
915
- staged_mutation* existing_sm = self->staged_mutations_->find_any(document.id());
925
+ const staged_mutation* existing_sm = self->staged_mutations_->find_any(document.id());
916
926
  if (existing_sm != nullptr && existing_sm->type() == staged_mutation_type::REMOVE) {
917
927
  CB_ATTEMPT_CTX_LOG_DEBUG(
918
928
  self, "found existing REMOVE of {} while replacing", document);
@@ -965,9 +975,7 @@ attempt_context_impl::replace(const transaction_get_result& document,
965
975
  self->create_staged_insert(document.id(),
966
976
  std::move(content),
967
977
  document.cas().value(),
968
- exp_delay(std::chrono::milliseconds(5),
969
- std::chrono::milliseconds(300),
970
- self->overall()->config().timeout),
978
+ self->staged_insert_delay(),
971
979
  op_id,
972
980
  std::move(cb));
973
981
  return;
@@ -1026,6 +1034,13 @@ attempt_context_impl::create_staged_replace(
1026
1034
  const std::optional<document_metadata>& document_metadata,
1027
1035
  Handler&& cb)
1028
1036
  {
1037
+ // ExtThreadSafety invariant -- see create_staged_insert() for the full rationale and
1038
+ // the spec reference (couchbase-transactions-specs/transactions-extensions.md,
1039
+ // "ExtThreadSafety"). replace() reads the mode after this op is counted, so reaching
1040
+ // this staging point in query mode means that ordering has regressed.
1041
+ assert(!op_list_.query_mode_entered() &&
1042
+ "ExtThreadSafety: KV replace staged after query mode entered -- would be missing "
1043
+ "from BEGIN WORK and lost at commit");
1029
1044
  operations::mutate_in_request req{ id };
1030
1045
  const bool binary =
1031
1046
  codec::codec_flags::has_common_flags(content.flags, codec::codec_flags::binary_common_flags);
@@ -1274,11 +1289,20 @@ attempt_context_impl::insert(const core::document_id& id,
1274
1289
  codec::encoded_value content,
1275
1290
  Callback&& cb)
1276
1291
  {
1277
- if (op_list_.get_mode().is_query()) {
1278
- return insert_raw_with_query(id, std::move(content), std::move(cb));
1279
- }
1280
1292
  return cache_error_async(
1281
1293
  cb, [self = shared_from_this(), id, cb, content = std::move(content)]() mutable {
1294
+ // The op count was incremented by cache_error_async before this lambda runs.
1295
+ // Reading the mode here -- after the increment -- closes a race with
1296
+ // set_query_mode(): a query switching to query mode first waits for all
1297
+ // in-flight KV ops to drain before issuing BEGIN WORK, so once this op is
1298
+ // counted, either we observe KV mode and our staged mutation is guaranteed to
1299
+ // be part of BEGIN WORK, or we observe query mode and route through query.
1300
+ // Checking the mode before the increment (as the code used to) left a window
1301
+ // where a query could flip the mode in the gap, after which this op would
1302
+ // stage a KV mutation that BEGIN WORK had already missed -- silently losing it.
1303
+ if (self->op_list_.get_mode().is_query()) {
1304
+ return self->insert_raw_with_query(id, std::move(content), std::move(cb));
1305
+ }
1282
1306
  self->ensure_open_bucket(
1283
1307
  id.bucket(),
1284
1308
  [self, id, content = std::move(content), cb = std::move(cb)](std::error_code ec) mutable {
@@ -1289,7 +1313,7 @@ attempt_context_impl::insert(const core::document_id& id,
1289
1313
  try {
1290
1314
  self->check_if_done(cb);
1291
1315
  auto op_id = uid_generator::next();
1292
- staged_mutation* existing_sm = self->staged_mutations_->find_any(id);
1316
+ const staged_mutation* existing_sm = self->staged_mutations_->find_any(id);
1293
1317
  if ((existing_sm != nullptr) &&
1294
1318
  (existing_sm->type() == staged_mutation_type::INSERT ||
1295
1319
  existing_sm->type() == staged_mutation_type::REPLACE)) {
@@ -1322,14 +1346,8 @@ attempt_context_impl::insert(const core::document_id& id,
1322
1346
  std::move(cb));
1323
1347
  }
1324
1348
  const std::uint64_t cas = 0;
1325
- self->create_staged_insert(id,
1326
- std::move(content),
1327
- cas,
1328
- exp_delay(std::chrono::milliseconds(5),
1329
- std::chrono::milliseconds(300),
1330
- self->overall()->config().timeout),
1331
- op_id,
1332
- std::move(cb));
1349
+ self->create_staged_insert(
1350
+ id, std::move(content), cas, self->staged_insert_delay(), op_id, std::move(cb));
1333
1351
  });
1334
1352
  } catch (const std::exception& e) {
1335
1353
  return self->op_completed_with_error(
@@ -1378,19 +1396,24 @@ attempt_context_impl::select_atr_if_needed_unlocked(
1378
1396
  CB_ATTEMPT_CTX_LOG_ERROR(this, "unexpected error \"{}\" during select atr if needed", e.what());
1379
1397
  }
1380
1398
  }
1381
- template<typename Handler, typename Delay>
1399
+ template<typename Handler>
1382
1400
  void
1383
- attempt_context_impl::check_atr_entry_for_blocking_document(const transaction_get_result& doc,
1384
- Delay delay,
1385
- Handler&& cb)
1401
+ attempt_context_impl::check_atr_entry_for_blocking_document(
1402
+ const transaction_get_result& doc,
1403
+ std::shared_ptr<async_timed_exp_delay> delay,
1404
+ Handler&& cb)
1386
1405
  {
1387
- try {
1388
- delay();
1389
- return hooks_.before_check_atr_entry_for_blocking_doc(
1390
- shared_from_this(),
1406
+ (*delay)([self = shared_from_this(), doc, delay, cb = std::forward<Handler>(cb)](
1407
+ const std::exception_ptr& delay_err) mutable {
1408
+ if (delay_err) {
1409
+ return cb(transaction_operation_failed(FAIL_WRITE_WRITE_CONFLICT,
1410
+ "document is in another transaction")
1411
+ .retry());
1412
+ }
1413
+ return self->hooks_.before_check_atr_entry_for_blocking_doc(
1414
+ self,
1391
1415
  doc.id().key(),
1392
- [self = shared_from_this(), delay = std::move(delay), cb = std::forward<Handler>(cb), doc](
1393
- auto ec) mutable {
1416
+ [self, delay = std::move(delay), cb = std::move(cb), doc](auto ec) mutable {
1394
1417
  if (ec) {
1395
1418
  return cb(transaction_operation_failed(FAIL_WRITE_WRITE_CONFLICT,
1396
1419
  "document is in another transaction")
@@ -1430,7 +1453,8 @@ attempt_context_impl::check_atr_entry_for_blocking_document(const transaction_ge
1430
1453
  "existing atr entry found in state {}, retrying",
1431
1454
  attempt_state_name(it->state()));
1432
1455
  }
1433
- return self->check_atr_entry_for_blocking_document(doc, delay, std::move(cb));
1456
+ return self->check_atr_entry_for_blocking_document(
1457
+ doc, std::move(delay), std::move(cb));
1434
1458
  }
1435
1459
  }
1436
1460
  CB_ATTEMPT_CTX_LOG_DEBUG(self, "no blocking atr entry");
@@ -1442,20 +1466,18 @@ attempt_context_impl::check_atr_entry_for_blocking_document(const transaction_ge
1442
1466
  .retry());
1443
1467
  });
1444
1468
  });
1445
- } catch (const retry_operation_timeout&) {
1446
- return cb(
1447
- transaction_operation_failed(FAIL_WRITE_WRITE_CONFLICT, "document is in another transaction")
1448
- .retry());
1449
- }
1469
+ });
1450
1470
  }
1451
1471
  void
1452
1472
  attempt_context_impl::remove(const transaction_get_result& document, VoidCallback&& cb)
1453
1473
  {
1454
1474
 
1455
- if (op_list_.get_mode().is_query()) {
1456
- return remove_with_query(document, std::move(cb));
1457
- }
1458
1475
  return cache_error_async(cb, [self = shared_from_this(), document, cb]() mutable {
1476
+ // Mode is read after cache_error_async incremented the op count, closing the
1477
+ // race with set_query_mode() -- see the comment in insert() for the rationale.
1478
+ if (self->op_list_.get_mode().is_query()) {
1479
+ return self->remove_with_query(document, std::move(cb));
1480
+ }
1459
1481
  self->check_if_done(cb);
1460
1482
  self->ensure_open_bucket(
1461
1483
  document.bucket(), [self, document, cb = std::move(cb)](std::error_code ec) mutable {
@@ -1539,6 +1561,15 @@ attempt_context_impl::remove(const transaction_get_result& document, VoidCallbac
1539
1561
  return error_handler(
1540
1562
  *ec, "before_staged_remove hook raised error", std::move(cb));
1541
1563
  }
1564
+ // ExtThreadSafety invariant -- see create_staged_insert() for the
1565
+ // full rationale and spec reference
1566
+ // (couchbase-transactions-specs/transactions-extensions.md,
1567
+ // "ExtThreadSafety"). remove() reads the mode after this op is
1568
+ // counted, so reaching this staging point in query mode means that
1569
+ // ordering has regressed.
1570
+ assert(!self->op_list_.query_mode_entered() &&
1571
+ "ExtThreadSafety: KV remove staged after query mode entered -- "
1572
+ "would be missing from BEGIN WORK and lost at commit");
1542
1573
  CB_ATTEMPT_CTX_LOG_TRACE(self,
1543
1574
  "about to remove doc {} with cas {}",
1544
1575
  document.id(),
@@ -1959,11 +1990,11 @@ dump_request(const core::operations::query_request& req) -> std::string
1959
1990
  std::string raw = "{";
1960
1991
  for (const auto& x : req.raw) {
1961
1992
  raw += x.first;
1962
- raw += ":";
1993
+ raw += ':';
1963
1994
  raw += x.second.str();
1964
- raw += ",";
1995
+ raw += ',';
1965
1996
  }
1966
- raw += "}";
1997
+ raw += '}';
1967
1998
  std::string params;
1968
1999
  for (const auto& x : req.positional_parameters) {
1969
2000
  params.append(x.str());
@@ -2015,6 +2046,7 @@ attempt_context_impl::wrap_query(
2015
2046
 
2016
2047
  if (check_expiry) {
2017
2048
  if (has_expired_client_side(hook_point, std::nullopt)) {
2049
+ query_check_expired_ = true;
2018
2050
  auto err = std::make_exception_ptr(
2019
2051
  transaction_operation_failed(FAIL_EXPIRY,
2020
2052
  fmt::format("{} expired in stage {}", statement, hook_point))
@@ -2203,59 +2235,69 @@ make_kv_txdata(std::optional<transaction_get_result> doc = std::nullopt) -> tao:
2203
2235
  void
2204
2236
  attempt_context_impl::get_with_query(const core::document_id& id, bool optional, Callback&& cb)
2205
2237
  {
2206
- cache_error_async(cb, [self = shared_from_this(), id, optional, cb]() mutable {
2207
- couchbase::transactions::transaction_query_options opts;
2208
- opts.readonly(true);
2209
- return self->wrap_query(
2210
- KV_GET,
2211
- opts,
2212
- make_params(id, {}),
2213
- make_kv_txdata(),
2214
- STAGE_QUERY_KV_GET,
2215
- true,
2216
- {},
2217
- [self, id, optional, cb = std::move(cb)](std::exception_ptr err,
2218
- core::operations::query_response resp) mutable {
2219
- if (resp.ctx.ec == couchbase::errc::key_value::document_not_found) {
2238
+ // Called only from get()/get_optional(), already within their cache_error_async()
2239
+ // (op counted, existing_error() checked); see insert_raw_with_query() for why we do
2240
+ // not re-wrap.
2241
+ auto self = shared_from_this();
2242
+ couchbase::transactions::transaction_query_options opts;
2243
+ opts.readonly(true);
2244
+ return self->wrap_query(
2245
+ KV_GET,
2246
+ opts,
2247
+ make_params(id, {}),
2248
+ make_kv_txdata(),
2249
+ STAGE_QUERY_KV_GET,
2250
+ true,
2251
+ {},
2252
+ [self, id, optional, cb = std::move(cb)](std::exception_ptr err,
2253
+ core::operations::query_response resp) mutable {
2254
+ if (resp.ctx.ec == couchbase::errc::key_value::document_not_found) {
2255
+ // A non-optional get() must fail rather than report an empty result: its callback contract
2256
+ // (and the synchronous get() wrapper) dereferences the returned optional, so err==nullptr
2257
+ // with res==nullopt would be a null dereference. Mirror the rows-empty handling below and
2258
+ // the KV get() path.
2259
+ if (optional) {
2220
2260
  return self->op_completed_with_callback(std::move(cb),
2221
2261
  std::optional<transaction_get_result>());
2222
2262
  }
2223
- if (!err) {
2224
- // make a transaction_get_result from the row...
2225
- try {
2226
- if (resp.rows.empty()) {
2227
- if (optional) {
2228
- return self->op_completed_with_callback(std::move(cb),
2229
- std::optional<transaction_get_result>());
2230
- }
2231
- return self->op_completed_with_error(
2232
- std::move(cb),
2233
- transaction_operation_failed(FAIL_DOC_NOT_FOUND, "document not found"));
2263
+ return self->op_completed_with_error(
2264
+ std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, "document not found"));
2265
+ }
2266
+ if (!err) {
2267
+ // make a transaction_get_result from the row...
2268
+ try {
2269
+ if (resp.rows.empty()) {
2270
+ if (optional) {
2271
+ return self->op_completed_with_callback(std::move(cb),
2272
+ std::optional<transaction_get_result>());
2234
2273
  }
2235
- CB_ATTEMPT_CTX_LOG_TRACE(self, "get_with_query got: {}", resp.rows.front());
2236
- transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
2237
- return self->op_completed_with_callback(std::move(cb),
2238
- std::optional<transaction_get_result>(doc));
2239
- } catch (const std::exception& e) {
2240
- // TODO(SA): unsure what to do here, but this is pretty fatal, so
2241
2274
  return self->op_completed_with_error(
2242
- std::move(cb), transaction_operation_failed(FAIL_OTHER, e.what()));
2275
+ std::move(cb),
2276
+ transaction_operation_failed(FAIL_DOC_NOT_FOUND, "document not found"));
2243
2277
  }
2278
+ CB_ATTEMPT_CTX_LOG_TRACE(self, "get_with_query got: {}", resp.rows.front());
2279
+ transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
2280
+ return self->op_completed_with_callback(std::move(cb),
2281
+ std::optional<transaction_get_result>(doc));
2282
+ } catch (const std::exception& e) {
2283
+ // Unexpected error here: fail with FAIL_OTHER so the transaction rolls back.
2284
+ return self->op_completed_with_error(std::move(cb),
2285
+ transaction_operation_failed(FAIL_OTHER, e.what()));
2244
2286
  }
2245
- // for get_optional. <sigh>
2246
- if (optional) {
2247
- try {
2248
- std::rethrow_exception(err);
2249
- } catch (const document_not_found&) {
2250
- return self->op_completed_with_callback(std::move(cb),
2251
- std::optional<transaction_get_result>());
2252
- } catch (...) {
2253
- return self->op_completed_with_error(std::move(cb), std::current_exception());
2254
- }
2287
+ }
2288
+ // for get_optional. <sigh>
2289
+ if (optional) {
2290
+ try {
2291
+ std::rethrow_exception(err);
2292
+ } catch (const document_not_found&) {
2293
+ return self->op_completed_with_callback(std::move(cb),
2294
+ std::optional<transaction_get_result>());
2295
+ } catch (...) {
2296
+ return self->op_completed_with_error(std::move(cb), std::current_exception());
2255
2297
  }
2256
- return self->op_completed_with_error(std::move(cb), std::move(err));
2257
- });
2258
- });
2298
+ }
2299
+ return self->op_completed_with_error(std::move(cb), std::move(err));
2300
+ });
2259
2301
  }
2260
2302
 
2261
2303
  void
@@ -2263,46 +2305,55 @@ attempt_context_impl::insert_raw_with_query(const core::document_id& id,
2263
2305
  codec::encoded_value content,
2264
2306
  Callback&& cb)
2265
2307
  {
2266
- cache_error_async(
2267
- cb, [self = shared_from_this(), id, content = std::move(content), cb]() mutable {
2268
- const couchbase::transactions::transaction_query_options opts;
2269
- return self->wrap_query(
2270
- KV_INSERT,
2271
- opts,
2272
- make_params(id, std::move(content)),
2273
- make_kv_txdata(),
2274
- STAGE_QUERY_KV_INSERT,
2275
- true,
2276
- {},
2277
- [self, id, cb = std::move(cb)](const std::exception_ptr& err,
2278
- core::operations::query_response resp) mutable {
2279
- if (err) {
2280
- try {
2281
- std::rethrow_exception(err);
2282
- } catch (const transaction_operation_failed& e) {
2283
- return self->op_completed_with_error(std::move(cb), e);
2284
- } catch (const op_exception& ex) {
2285
- return self->op_completed_with_error(std::move(cb), ex);
2286
- } catch (const std::exception& e) {
2287
- return self->op_completed_with_error(
2288
- std::move(cb), transaction_operation_failed(FAIL_OTHER, e.what()));
2289
- } catch (...) {
2290
- return self->op_completed_with_error(
2291
- std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected error"));
2292
- }
2293
- }
2294
- // make a transaction_get_result from the row...
2295
- try {
2296
- CB_ATTEMPT_CTX_LOG_TRACE(self, "insert_raw_with_query got: {}", resp.rows.front());
2297
- transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
2298
- return self->op_completed_with_callback(std::move(cb),
2299
- std::optional<transaction_get_result>(doc));
2300
- } catch (const std::exception& e) {
2301
- // TODO(SA): unsure what to do here, but this is pretty fatal, so
2302
- return self->op_completed_with_error(
2303
- std::move(cb), transaction_operation_failed(FAIL_OTHER, e.what()));
2304
- }
2305
- });
2308
+ // Called only from insert(), already within its cache_error_async(): the op has
2309
+ // been counted in the wait-group and existing_error() has been checked, so we must
2310
+ // not wrap again here -- double-counting would leave the op permanently in-flight
2311
+ // and deadlock the commit/rollback wait.
2312
+ auto self = shared_from_this();
2313
+ const couchbase::transactions::transaction_query_options opts;
2314
+ return self->wrap_query(
2315
+ KV_INSERT,
2316
+ opts,
2317
+ make_params(id, std::move(content)),
2318
+ make_kv_txdata(),
2319
+ STAGE_QUERY_KV_INSERT,
2320
+ true,
2321
+ {},
2322
+ [self, id, cb = std::move(cb)](const std::exception_ptr& err,
2323
+ core::operations::query_response resp) mutable {
2324
+ if (err) {
2325
+ try {
2326
+ std::rethrow_exception(err);
2327
+ } catch (const transaction_operation_failed& e) {
2328
+ return self->op_completed_with_error(std::move(cb), e);
2329
+ } catch (const op_exception& ex) {
2330
+ return self->op_completed_with_error(std::move(cb), ex);
2331
+ } catch (const std::exception& e) {
2332
+ return self->op_completed_with_error(std::move(cb),
2333
+ transaction_operation_failed(FAIL_OTHER, e.what()));
2334
+ } catch (...) {
2335
+ return self->op_completed_with_error(
2336
+ std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected error"));
2337
+ }
2338
+ }
2339
+ // make a transaction_get_result from the row...
2340
+ try {
2341
+ if (resp.rows.empty()) {
2342
+ // A successful insert query must return the staged document row; no rows is an
2343
+ // unexpected protocol condition. front() on an empty vector is UB, so guard explicitly.
2344
+ return self->op_completed_with_error(
2345
+ std::move(cb),
2346
+ transaction_operation_failed(FAIL_OTHER, "insert query returned no rows"));
2347
+ }
2348
+ CB_ATTEMPT_CTX_LOG_TRACE(self, "insert_raw_with_query got: {}", resp.rows.front());
2349
+ transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
2350
+ return self->op_completed_with_callback(std::move(cb),
2351
+ std::optional<transaction_get_result>(doc));
2352
+ } catch (const std::exception& e) {
2353
+ // Unexpected error here: fail with FAIL_OTHER so the transaction rolls back.
2354
+ return self->op_completed_with_error(std::move(cb),
2355
+ transaction_operation_failed(FAIL_OTHER, e.what()));
2356
+ }
2306
2357
  });
2307
2358
  }
2308
2359
 
@@ -2311,91 +2362,103 @@ attempt_context_impl::replace_raw_with_query(const transaction_get_result& docum
2311
2362
  codec::encoded_value content,
2312
2363
  Callback&& cb)
2313
2364
  {
2314
- cache_error_async(
2315
- cb, [self = shared_from_this(), document, content = std::move(content), cb]() mutable {
2316
- const couchbase::transactions::transaction_query_options opts;
2317
- return self->wrap_query(
2318
- KV_REPLACE,
2319
- opts,
2320
- make_params(document.id(), std::move(content)),
2321
- make_kv_txdata(document),
2322
- STAGE_QUERY_KV_REPLACE,
2323
- true,
2324
- {},
2325
- [self, id = document.id(), cb = std::move(cb)](
2326
- const std::exception_ptr& err, core::operations::query_response resp) mutable {
2327
- if (err) {
2328
- try {
2329
- std::rethrow_exception(err);
2330
- } catch (const query_cas_mismatch& e) {
2331
- return self->op_completed_with_error(
2332
- std::move(cb), transaction_operation_failed(FAIL_CAS_MISMATCH, e.what()).retry());
2333
- } catch (const document_not_found& e) {
2334
- return self->op_completed_with_error(
2335
- std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, e.what()).retry());
2336
- } catch (const transaction_operation_failed& e) {
2337
- return self->op_completed_with_error(std::move(cb), e);
2338
- } catch (std::exception& e) {
2339
- return self->op_completed_with_error(
2340
- std::move(cb), transaction_operation_failed(FAIL_OTHER, e.what()));
2341
- } catch (...) {
2342
- return self->op_completed_with_error(
2343
- std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected exception"));
2344
- }
2345
- }
2346
- // make a transaction_get_result from the row...
2347
- try {
2348
- CB_ATTEMPT_CTX_LOG_TRACE(self, "replace_raw_with_query got: {}", resp.rows.front());
2349
- transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
2350
- return self->op_completed_with_callback(std::move(cb),
2351
- std::optional<transaction_get_result>(doc));
2352
- } catch (const std::exception& e) {
2353
- // TODO(SA): unsure what to do here, but this is pretty fatal, so
2354
- return self->op_completed_with_error(
2355
- std::move(cb), transaction_operation_failed(FAIL_OTHER, e.what()));
2356
- }
2357
- });
2365
+ // Called only from replace(), already within its cache_error_async() (op counted,
2366
+ // existing_error() checked); see insert_raw_with_query() for why we do not re-wrap.
2367
+ auto self = shared_from_this();
2368
+ const couchbase::transactions::transaction_query_options opts;
2369
+ return self->wrap_query(
2370
+ KV_REPLACE,
2371
+ opts,
2372
+ make_params(document.id(), std::move(content)),
2373
+ make_kv_txdata(document),
2374
+ STAGE_QUERY_KV_REPLACE,
2375
+ true,
2376
+ {},
2377
+ [self, id = document.id(), cb = std::move(cb)](const std::exception_ptr& err,
2378
+ core::operations::query_response resp) mutable {
2379
+ if (err) {
2380
+ try {
2381
+ std::rethrow_exception(err);
2382
+ } catch (const query_cas_mismatch& e) {
2383
+ return self->op_completed_with_error(
2384
+ std::move(cb), transaction_operation_failed(FAIL_CAS_MISMATCH, e.what()).retry());
2385
+ } catch (const document_not_found& e) {
2386
+ return self->op_completed_with_error(
2387
+ std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, e.what()).retry());
2388
+ } catch (const transaction_operation_failed& e) {
2389
+ return self->op_completed_with_error(std::move(cb), e);
2390
+ } catch (const op_exception& ex) {
2391
+ return self->op_completed_with_error(std::move(cb), ex);
2392
+ } catch (std::exception& e) {
2393
+ return self->op_completed_with_error(std::move(cb),
2394
+ transaction_operation_failed(FAIL_OTHER, e.what()));
2395
+ } catch (...) {
2396
+ return self->op_completed_with_error(
2397
+ std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected exception"));
2398
+ }
2399
+ }
2400
+ // make a transaction_get_result from the row...
2401
+ try {
2402
+ if (resp.rows.empty()) {
2403
+ // A successful replace query must return the staged document row; no rows is an
2404
+ // unexpected protocol condition. front() on an empty vector is UB, so guard explicitly.
2405
+ return self->op_completed_with_error(
2406
+ std::move(cb),
2407
+ transaction_operation_failed(FAIL_OTHER, "replace query returned no rows"));
2408
+ }
2409
+ CB_ATTEMPT_CTX_LOG_TRACE(self, "replace_raw_with_query got: {}", resp.rows.front());
2410
+ transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
2411
+ return self->op_completed_with_callback(std::move(cb),
2412
+ std::optional<transaction_get_result>(doc));
2413
+ } catch (const std::exception& e) {
2414
+ // Unexpected error here: fail with FAIL_OTHER so the transaction rolls back.
2415
+ return self->op_completed_with_error(std::move(cb),
2416
+ transaction_operation_failed(FAIL_OTHER, e.what()));
2417
+ }
2358
2418
  });
2359
2419
  }
2360
2420
 
2361
2421
  void
2362
2422
  attempt_context_impl::remove_with_query(const transaction_get_result& document, VoidCallback&& cb)
2363
2423
  {
2364
- cache_error_async(cb, [self = shared_from_this(), document, cb]() {
2365
- const couchbase::transactions::transaction_query_options opts;
2366
- return self->wrap_query(
2367
- KV_REMOVE,
2368
- opts,
2369
- make_params(document.id(), {}),
2370
- make_kv_txdata(document),
2371
- STAGE_QUERY_KV_REMOVE,
2372
- true,
2373
- {},
2374
- [self, id = document.id(), cb](const std::exception_ptr& err,
2375
- const core::operations::query_response& /* resp */) mutable {
2376
- if (err) {
2377
- try {
2378
- std::rethrow_exception(err);
2379
- } catch (const transaction_operation_failed& e) {
2380
- return self->op_completed_with_error(std::move(cb), e);
2381
- } catch (const document_not_found& e) {
2382
- return self->op_completed_with_error(
2383
- std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, e.what()).retry());
2384
- } catch (const query_cas_mismatch& e) {
2385
- return self->op_completed_with_error(
2386
- std::move(cb), transaction_operation_failed(FAIL_CAS_MISMATCH, e.what()).retry());
2387
- } catch (const std::exception& e) {
2388
- return self->op_completed_with_error(
2389
- std::move(cb), transaction_operation_failed(FAIL_OTHER, e.what()));
2390
- } catch (...) {
2391
- return self->op_completed_with_error(
2392
- std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected exception"));
2393
- }
2424
+ // Called only from remove(), already within its cache_error_async() (op counted,
2425
+ // existing_error() checked); see insert_raw_with_query() for why we do not re-wrap.
2426
+ auto self = shared_from_this();
2427
+ const couchbase::transactions::transaction_query_options opts;
2428
+ return self->wrap_query(
2429
+ KV_REMOVE,
2430
+ opts,
2431
+ make_params(document.id(), {}),
2432
+ make_kv_txdata(document),
2433
+ STAGE_QUERY_KV_REMOVE,
2434
+ true,
2435
+ {},
2436
+ [self, id = document.id(), cb = std::move(cb)](
2437
+ const std::exception_ptr& err, const core::operations::query_response& /* resp */) mutable {
2438
+ if (err) {
2439
+ try {
2440
+ std::rethrow_exception(err);
2441
+ } catch (const transaction_operation_failed& e) {
2442
+ return self->op_completed_with_error(std::move(cb), e);
2443
+ } catch (const document_not_found& e) {
2444
+ return self->op_completed_with_error(
2445
+ std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, e.what()).retry());
2446
+ } catch (const query_cas_mismatch& e) {
2447
+ return self->op_completed_with_error(
2448
+ std::move(cb), transaction_operation_failed(FAIL_CAS_MISMATCH, e.what()).retry());
2449
+ } catch (const op_exception& ex) {
2450
+ return self->op_completed_with_error(std::move(cb), ex);
2451
+ } catch (const std::exception& e) {
2452
+ return self->op_completed_with_error(std::move(cb),
2453
+ transaction_operation_failed(FAIL_OTHER, e.what()));
2454
+ } catch (...) {
2455
+ return self->op_completed_with_error(
2456
+ std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected exception"));
2394
2457
  }
2395
- // make a transaction_get_result from the row...
2396
- return self->op_completed_with_callback(std::move(cb));
2397
- });
2398
- });
2458
+ }
2459
+ // make a transaction_get_result from the row...
2460
+ return self->op_completed_with_callback(std::move(cb));
2461
+ });
2399
2462
  }
2400
2463
 
2401
2464
  void
@@ -3176,10 +3239,10 @@ attempt_context_impl::set_atr_pending_locked(
3176
3239
  // This bounds the value to [0-timeout]. It should always be in this
3177
3240
  // range, this is just to protect against the application clock
3178
3241
  // changing.
3179
- const long remaining_bounded_nanos =
3242
+ const auto remaining_bounded_nanos =
3180
3243
  std::max(std::min(remaining.count(), self->overall()->config().timeout.count()),
3181
3244
  static_cast<std::chrono::nanoseconds::rep>(0));
3182
- const long remaining_bounded_msecs = remaining_bounded_nanos / 1'000'000;
3245
+ const auto remaining_bounded_msecs = remaining_bounded_nanos / 1'000'000;
3183
3246
 
3184
3247
  core::operations::mutate_in_request req{ self->atr_id_.value() };
3185
3248
 
@@ -3338,6 +3401,7 @@ attempt_context_impl::do_get(const core::document_id& id,
3338
3401
  return self->get_doc(
3339
3402
  id,
3340
3403
  allow_replica,
3404
+ std::nullopt,
3341
3405
  [self,
3342
3406
  id,
3343
3407
  allow_replica,
@@ -3513,6 +3577,7 @@ execute_lookup(attempt_context_impl* ctx, LookupInRequest& req, Callback&& cb)
3513
3577
  void
3514
3578
  attempt_context_impl::get_doc(const core::document_id& id,
3515
3579
  bool allow_replica,
3580
+ std::optional<std::chrono::milliseconds> timeout,
3516
3581
  std::function<void(std::optional<error_class>,
3517
3582
  std::optional<external_exception>,
3518
3583
  std::optional<std::string>,
@@ -3536,27 +3601,21 @@ attempt_context_impl::get_doc(const core::document_id& id,
3536
3601
 
3537
3602
  try {
3538
3603
  if (allow_replica) {
3539
- cluster_ref().with_bucket_configuration(
3540
- id.bucket(),
3541
- [this, id, specs, cb = std::move(cb)](
3542
- std::error_code ec, const std::shared_ptr<topology::configuration>& config) {
3543
- if (ec) {
3544
- cb(FAIL_OTHER,
3545
- std::nullopt,
3546
- "failed to check whether access_deleted is supported: " + ec.message(),
3547
- std::nullopt);
3548
- return;
3549
- }
3550
- core::operations::lookup_in_any_replica_request req{ id };
3551
- req.read_preference = couchbase::read_preference::selected_server_group_or_all_available;
3552
- req.specs = specs;
3553
- req.access_deleted = config->capabilities.supports_subdoc_access_deleted();
3554
- execute_lookup(this, req, cb);
3555
- });
3604
+ // Per the transactions spec ("Fetching an individual document") and MB-63241, a replica read
3605
+ // must not be combined with the accessDeleted flag; the reference SDKs omit it on the replica
3606
+ // path. We therefore always read replicas without accessDeleted (so tombstones are not
3607
+ // visible there) and need no bucket-config capability probe.
3608
+ core::operations::lookup_in_any_replica_request req{ id };
3609
+ req.read_preference = couchbase::read_preference::selected_server_group_or_all_available;
3610
+ req.specs = specs;
3611
+ req.access_deleted = false;
3612
+ req.timeout = timeout;
3613
+ execute_lookup(this, req, cb);
3556
3614
  } else {
3557
3615
  core::operations::lookup_in_request req{ id };
3558
3616
  req.access_deleted = true;
3559
3617
  req.specs = specs;
3618
+ req.timeout = timeout;
3560
3619
  execute_lookup(this, req, cb);
3561
3620
  }
3562
3621
  } catch (const std::exception& e) {
@@ -3593,8 +3652,8 @@ attempt_context_impl::create_staged_insert_error_handler(const core::document_id
3593
3652
  transaction_operation_failed(ec, "transient error in insert").retry());
3594
3653
  case FAIL_AMBIGUOUS:
3595
3654
  CB_ATTEMPT_CTX_LOG_DEBUG(this, "FAIL_AMBIGUOUS in create_staged_insert, retrying");
3596
- delay();
3597
- return create_staged_insert(id, content, cas, delay, op_id, std::forward<Handler>(cb));
3655
+ return retry_create_staged_insert(
3656
+ id, content, cas, std::forward<Delay>(delay), op_id, std::forward<Handler>(cb));
3598
3657
  case FAIL_OTHER:
3599
3658
  return op_completed_with_error(
3600
3659
  std::forward<Handler>(cb),
@@ -3655,6 +3714,7 @@ attempt_context_impl::create_staged_insert_error_handler(const core::document_id
3655
3714
  return self->get_doc(
3656
3715
  id,
3657
3716
  false,
3717
+ std::nullopt,
3658
3718
  [self, id, content, op_id, cb = std::forward<Handler>(cb), error_handler, delay](
3659
3719
  std::optional<error_class> ec3,
3660
3720
  std::optional<external_exception> /* cause */,
@@ -3682,8 +3742,7 @@ attempt_context_impl::create_staged_insert_error_handler(const core::document_id
3682
3742
  "create staged insert found existing deleted doc, "
3683
3743
  "retrying with cas {}",
3684
3744
  doc->cas().value());
3685
- delay();
3686
- return self->create_staged_insert(
3745
+ return self->retry_create_staged_insert(
3687
3746
  id, content, doc->cas().value(), delay, op_id, std::forward<Handler>(cb));
3688
3747
  }
3689
3748
  if (!doc->links().is_document_in_transaction()) {
@@ -3735,9 +3794,8 @@ attempt_context_impl::create_staged_insert_error_handler(const core::document_id
3735
3794
  "doc ok to overwrite, retrying create_staged_insert "
3736
3795
  "with cas {}",
3737
3796
  doc->cas().value());
3738
- delay();
3739
- return self->create_staged_insert(
3740
- id, content, doc->cas().value(), delay, op_id, std::forward<Handler>(cb));
3797
+ return self->retry_create_staged_insert(
3798
+ id, content, doc->cas().value(), delay, op_id, std::move(cb));
3741
3799
  });
3742
3800
  } else {
3743
3801
  // no doc now, just retry entire txn
@@ -3763,6 +3821,58 @@ attempt_context_impl::create_staged_insert_error_handler(const core::document_id
3763
3821
  }
3764
3822
  }
3765
3823
 
3824
+ auto
3825
+ attempt_context_impl::staged_insert_delay() const -> std::shared_ptr<async_timed_exp_delay>
3826
+ {
3827
+ auto delay = std::make_shared<async_timed_exp_delay>(
3828
+ std::make_shared<asio::steady_timer>(cluster_ref().io_context()),
3829
+ std::chrono::milliseconds(5),
3830
+ std::chrono::milliseconds(300),
3831
+ overall()->config().timeout);
3832
+ // The transaction expiry bounds the retries; a retry limit would fail a long transaction early.
3833
+ delay->max_retries = std::numeric_limits<std::size_t>::max();
3834
+ return delay;
3835
+ }
3836
+
3837
+ template<typename Handler, typename Delay>
3838
+ void
3839
+ attempt_context_impl::retry_create_staged_insert(const core::document_id& id,
3840
+ const codec::encoded_value& content,
3841
+ std::uint64_t cas,
3842
+ Delay&& delay,
3843
+ const std::string& op_id,
3844
+ Handler&& cb)
3845
+ {
3846
+ auto shared_delay = delay;
3847
+ (*shared_delay)([self = shared_from_this(),
3848
+ id,
3849
+ content = content,
3850
+ cas,
3851
+ delay = std::forward<Delay>(delay),
3852
+ op_id,
3853
+ cb = std::forward<Handler>(cb)](const std::exception_ptr& delay_err) mutable {
3854
+ if (delay_err) {
3855
+ try {
3856
+ std::rethrow_exception(delay_err);
3857
+ } catch (const retry_operation_timeout&) {
3858
+ return self->create_staged_insert_error_handler(id,
3859
+ content,
3860
+ cas,
3861
+ delay,
3862
+ op_id,
3863
+ std::move(cb),
3864
+ FAIL_EXPIRY,
3865
+ UNKNOWN,
3866
+ "create_staged_insert retries timed out");
3867
+ } catch (const std::exception& e) {
3868
+ return self->op_completed_with_error(std::move(cb),
3869
+ transaction_operation_failed(FAIL_OTHER, e.what()));
3870
+ }
3871
+ }
3872
+ return self->create_staged_insert(id, std::move(content), cas, delay, op_id, std::move(cb));
3873
+ });
3874
+ }
3875
+
3766
3876
  template<typename Handler, typename Delay>
3767
3877
  void
3768
3878
  attempt_context_impl::create_staged_insert(const core::document_id& id,
@@ -3798,6 +3908,17 @@ attempt_context_impl::create_staged_insert(const core::document_id& id,
3798
3908
  UNKNOWN,
3799
3909
  "before_staged_insert hook threw error");
3800
3910
  }
3911
+ // ExtThreadSafety invariant (couchbase-transactions-specs/transactions-extensions.md,
3912
+ // "ExtThreadSafety"): "no KV operations are staged but not sent in the BEGIN WORK" and
3913
+ // "no KV operations are performed regularly after queryMode is entered". insert()
3914
+ // reads the mode only after this op is counted in the wait-group, and set_query_mode()
3915
+ // drains all in-flight KV ops before issuing BEGIN WORK -- so reaching this staging
3916
+ // point with query mode already entered means that ordering has regressed and the
3917
+ // mutation would be silently lost at commit. Fires loudly in debug/FIT builds;
3918
+ // compiled out in release.
3919
+ assert(!op_list_.query_mode_entered() &&
3920
+ "ExtThreadSafety: KV insert staged after query mode entered -- would be missing "
3921
+ "from BEGIN WORK and lost at commit");
3801
3922
  CB_ATTEMPT_CTX_LOG_DEBUG(this, "about to insert staged doc {} with cas {}", id, cas);
3802
3923
  core::operations::mutate_in_request req{ id };
3803
3924
  const bool binary =