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
@@ -26,9 +26,14 @@
26
26
  #include "utils/json_streaming_lexer.hxx"
27
27
  #include "utils/movable_function.hxx"
28
28
 
29
+ #include <asio/error.hpp>
29
30
  #include <asio/experimental/channel_error.hpp>
30
31
  #include <asio/experimental/concurrent_channel.hpp>
31
32
  #include <asio/io_context.hpp>
33
+ #include <asio/post.hpp>
34
+
35
+ #include <asio/steady_timer.hpp>
36
+ #include <chrono>
32
37
 
33
38
  #include <atomic>
34
39
  #include <memory>
@@ -48,50 +53,75 @@ struct row_stream_end_signal {
48
53
  class row_streamer_impl : public std::enable_shared_from_this<row_streamer_impl>
49
54
  {
50
55
  public:
51
- static constexpr std::size_t ROW_BUFFER_SIZE{ 100 };
52
- static constexpr std::size_t ROW_BUFFER_FEED_THRESHOLD{ ROW_BUFFER_SIZE * 3 / 4 };
53
- static constexpr std::uint32_t LEXER_DEPTH{ 4 };
54
-
55
56
  row_streamer_impl(asio::io_context& io,
56
57
  http_response_body body,
57
- const std::string& pointer_expression)
58
+ const std::string& pointer_expression,
59
+ row_streamer_options options)
58
60
  : io_{ io }
59
61
  , body_{ std::move(body) }
60
- , rows_{ io_, ROW_BUFFER_SIZE }
61
- , lexer_{ pointer_expression, LEXER_DEPTH }
62
+ // rows_ initializes before options_, so read the capacity from the ctor parameter.
63
+ , rows_{ io_, options.row_buffer_size }
64
+ , options_{ options }
65
+ , idle_timer_{ io_ }
66
+ , lexer_{ pointer_expression, options_.lexer_depth, options_.max_row_bytes }
62
67
  {
63
68
  }
64
69
 
65
70
  void start(utils::movable_function<void(std::string, std::error_code)>&& handler)
66
71
  {
72
+ metadata_header_handler_ = std::move(handler);
67
73
  lexer_.on_metadata_header_complete(
68
- [handler = std::move(handler)](auto ec, auto meta_header) mutable {
74
+ [weak = weak_from_this()](auto ec, auto meta_header) mutable {
75
+ auto self = weak.lock();
76
+ if (!self) {
77
+ return;
78
+ }
69
79
  // Trim any whitespace from the end
70
- meta_header.erase(meta_header.find_last_not_of(" \t\f\v\n\r") + 1);
80
+ auto last_non_ws = meta_header.find_last_not_of(" \t\f\v\n\r");
81
+ if (last_non_ws != std::string::npos) {
82
+ meta_header.erase(last_non_ws + 1);
83
+ }
71
84
 
72
85
  // The metadata header can end with an open `[` (opening bracket for pointer expression
73
86
  // array). If that's the case, close the array and the response object
74
- if (meta_header[meta_header.length() - 1] == '[') {
87
+ if (!meta_header.empty() && meta_header[meta_header.length() - 1] == '[') {
75
88
  meta_header.append("]}");
76
89
  }
77
- handler(meta_header, ec);
90
+ self->deliver_metadata_header(ec, std::move(meta_header));
78
91
  });
79
- lexer_.on_row([self = shared_from_this()](std::string&& row) -> utils::json::stream_control {
80
- self->rows_.async_send({}, std::move(row), [self](auto ec) {
81
- self->buffered_row_count_++;
92
+ lexer_.on_row([weak = weak_from_this()](std::string&& row) -> utils::json::stream_control {
93
+ auto self = weak.lock();
94
+ if (!self) {
95
+ return utils::json::stream_control::stop;
96
+ }
97
+ auto row_len = row.size();
98
+ // Account the row against the back-pressure budget *synchronously*, as it is handed to the
99
+ // channel. A concurrent_channel completes async_send immediately while it has spare capacity
100
+ // and otherwise queues the send until a receiver drains a slot; if we only counted bytes in
101
+ // the completion handler, a full channel would leave buffered_bytes_ pinned low and let
102
+ // maybe_feed_lexer() keep pulling from the socket, piling up unbounded pending sends (each
103
+ // owning a row). Counting here makes the high-water mark observe the true backlog.
104
+ self->buffered_bytes_.fetch_add(row_len, std::memory_order_relaxed);
105
+ self->rows_.async_send({}, std::move(row), [self, row_len](auto ec) {
82
106
  if (ec) {
83
107
  if (ec != asio::experimental::error::channel_closed &&
84
108
  ec != asio::experimental::error::channel_cancelled) {
85
109
  CB_LOG_WARNING(
86
110
  "unexpected error while sending to row channel: {} ({})", ec.value(), ec.message());
87
111
  }
88
- return;
112
+ // The send never reached a receiver (channel closed/cancelled), so release its budget.
113
+ self->release_buffered_bytes(row_len);
89
114
  }
90
115
  });
91
116
  return utils::json::stream_control::next_row;
92
117
  });
93
- lexer_.on_complete([self = shared_from_this()](
118
+ lexer_.on_complete([weak = weak_from_this()](
94
119
  std::error_code ec, std::size_t /*number_of_rows*/, std::string&& meta) {
120
+ auto self = weak.lock();
121
+ if (!self) {
122
+ return;
123
+ }
124
+ self->lexer_completed_ = true;
95
125
  row_stream_end_signal signal{ ec, std::move(meta) };
96
126
  self->rows_.async_send({}, std::move(signal), [self](auto ec) {
97
127
  if (ec) {
@@ -115,7 +145,6 @@ public:
115
145
  }
116
146
  rows_.async_receive(
117
147
  [self = shared_from_this(), handler = std::move(handler)](auto ec, auto row) mutable {
118
- self->buffered_row_count_--;
119
148
  if (ec) {
120
149
  if (ec == asio::experimental::error::channel_closed ||
121
150
  ec == asio::experimental::error::channel_cancelled) {
@@ -124,66 +153,288 @@ public:
124
153
  return handler({}, ec);
125
154
  }
126
155
  if (std::holds_alternative<row_stream_end_signal>(row)) {
127
- auto signal = std::get<row_stream_end_signal>(row);
156
+ auto& signal = std::get<row_stream_end_signal>(row);
128
157
  if (!signal.metadata.empty()) {
129
- std::lock_guard<std::mutex> const lock{ self->metadata_mutex_ };
158
+ const std::scoped_lock<std::mutex> lock{ self->metadata_mutex_ };
130
159
  self->metadata_ = std::move(signal.metadata);
131
160
  }
132
161
  return handler({}, signal.ec);
133
162
  }
134
- auto row_content = std::get<std::string>(row);
163
+ auto row_content = std::move(std::get<std::string>(row));
164
+ const auto row_bytes = row_content.size();
135
165
  handler(std::move(row_content), {});
136
166
 
137
- // After receiving a row check if more data needs to be fed into the lexer
138
- self->maybe_feed_lexer();
167
+ // Release the row's budget and resume feeding at or below the low-water mark.
168
+ self->release_buffered_bytes(row_bytes);
169
+ if (self->should_resume()) {
170
+ self->maybe_feed_lexer();
171
+ }
139
172
  return;
140
173
  });
141
174
  }
142
175
 
176
+ auto set_deadline(std::chrono::time_point<std::chrono::steady_clock> deadline_tp)
177
+ -> io::deadline_state
178
+ {
179
+ // Held across the check and the arm. Testing the flag and then arming as two steps leaves a
180
+ // window in which cancel() can run to completion in between, so this thread would install a
181
+ // deadline on a stream already cancelled and an expiry could report a timeout after it.
182
+ // cancel() takes the same lock around its own state change and its cancel_deadline(), so the
183
+ // two orderings are the only ones: arm-then-cancel, which the cancel supersedes, or
184
+ // cancel-then-arm, which this refuses.
185
+ const std::scoped_lock<std::mutex> lock{ cancel_mutex_ };
186
+ if (cancelled_) {
187
+ return io::deadline_state::body_already_ended;
188
+ }
189
+ // Arms on the calling thread: the body serialises arming, cancelling and the terminal
190
+ // transition under one lock and supersedes an expiry already queued. Not posted, which would
191
+ // leave the previous deadline installed until the post ran.
192
+ const auto on_expiry =
193
+ options_.is_read_only ? io::deadline_terminal::unambiguous : io::deadline_terminal::ambiguous;
194
+ return body_.set_deadline(deadline_tp, on_expiry);
195
+ }
196
+
143
197
  void cancel()
144
198
  {
145
- body_.cancel();
199
+ {
200
+ // The state change and the supersession are one step, against set_deadline() holding the
201
+ // same lock across its check and its arm. Recorded on this thread rather than in the posted
202
+ // teardown: the flag refuses later arms, and cancel_deadline() supersedes one already armed,
203
+ // which would otherwise stay live until the post ran and could take the body's terminal --
204
+ // reporting a timeout for a stream the caller had already cancelled.
205
+ const std::scoped_lock<std::mutex> lock{ cancel_mutex_ };
206
+ cancelled_ = true;
207
+ body_.cancel_deadline();
208
+ }
209
+ // An explicit cancel must win over a racing inter-read idle timer, so it is disarmed here
210
+ // rather than in the posted teardown: a timer completion running before that post would set
211
+ // timed_out_ and the read abort would be reported as a timeout. The completion holds this lock
212
+ // across its generation test and its store, so it either sees the bump and returns, or its
213
+ // store precedes the clear. idle_disarmed_ refuses a timer armed by a read completing before
214
+ // the teardown.
215
+ {
216
+ const std::scoped_lock<std::mutex> lock{ idle_timer_mutex_ };
217
+ idle_disarmed_ = true;
218
+ idle_timer_.cancel();
219
+ idle_generation_.fetch_add(1, std::memory_order_relaxed);
220
+ timed_out_ = false;
221
+ }
222
+ // Tear the body down from the io_context rather than the caller's thread, which may never run
223
+ // it. The channel is thread-safe, so cancel/close it synchronously to unblock a waiting
224
+ // consumer immediately. No post once the read-ahead has ended: the body is closed or drained,
225
+ // and its cancel() does nothing. After cluster close the io_context runs out of work and
226
+ // stops. A post queued then never runs, and its capture holds this streamer and the body until
227
+ // the io_context is destroyed.
228
+ if (!received_all_data_) {
229
+ asio::post(io_, [self = shared_from_this()]() {
230
+ self->body_.cancel();
231
+ });
232
+ }
146
233
  rows_.cancel();
147
234
  rows_.close();
148
235
  }
149
236
 
150
237
  auto metadata() -> std::optional<std::string>
151
238
  {
152
- std::lock_guard<std::mutex> const lock{ metadata_mutex_ };
239
+ const std::scoped_lock<std::mutex> lock{ metadata_mutex_ };
153
240
  return metadata_;
154
241
  }
155
242
 
243
+ [[nodiscard]] auto buffered_bytes() const -> std::size_t
244
+ {
245
+ return buffered_bytes_.load(std::memory_order_relaxed);
246
+ }
247
+
156
248
  private:
249
+ // Resolve start()'s preamble handler exactly once. The lexer delivers it on a well-formed
250
+ // response (rows-array open or clean root pop); the terminal paths in maybe_feed_lexer call this
251
+ // with an error so a response that never produces a metadata header (empty body, or a valid but
252
+ // non-object JSON root) still resolves the handler instead of hanging the caller forever.
253
+ void deliver_metadata_header(std::error_code ec, std::string&& meta_header)
254
+ {
255
+ if (metadata_header_delivered_.exchange(true)) {
256
+ return;
257
+ }
258
+ // Move the handler out before invoking so it is destroyed right after. The handler owns a
259
+ // strong reference to the owning query/analytics stream (which in turn owns this row_streamer),
260
+ // so keeping it in a member past delivery would form a reference cycle and leak the whole
261
+ // stream.
262
+ auto handler = std::move(metadata_header_handler_);
263
+ if (handler) {
264
+ handler(std::move(meta_header), ec);
265
+ }
266
+ }
267
+
268
+ // At or below the mark, not strictly below it: buffered_bytes_ is unsigned, so a strict
269
+ // comparison against a zero low-water mark can never hold and the consumer would never ask the
270
+ // producer to resume. Reading would stop for good at the first pause. An empty buffer resumes
271
+ // reading whatever the mark is set to.
272
+ [[nodiscard]] auto should_resume() const -> bool
273
+ {
274
+ return buffered_bytes_.load(std::memory_order_relaxed) <= options_.low_water_bytes;
275
+ }
276
+
277
+ // Release a row's byte budget, clamping at zero. Uses an atomic compare-exchange loop so the
278
+ // subtract is a single read-modify-write: even if the io_context is driven by more than one
279
+ // thread, a concurrent update to buffered_bytes_ cannot be lost (as it could with a separate
280
+ // load + store).
281
+ //
282
+ // Sequentially consistent, as is the gating load in maybe_feed_lexer(). The handshake needs
283
+ // visibility from consumer to producer, the opposite direction from the channel handoff, which
284
+ // therefore supplies nothing here. A consumer that drains below the low-water mark and then
285
+ // loses the feeding_ compare-exchange leaves the resume to the producer's gate; a gate reading a
286
+ // pre-drain value declines it too, and the stream stalls with no read outstanding and the
287
+ // consumer parked in next_row. Relaxed on both ends leaves that store-buffering shape open, and
288
+ // release/acquire does not forbid it either. Only a total order covering all four accesses --
289
+ // these two and the feeding_ gate's -- rules the stall out.
290
+ void release_buffered_bytes(std::size_t n)
291
+ {
292
+ auto current = buffered_bytes_.load(std::memory_order_relaxed);
293
+ while (!buffered_bytes_.compare_exchange_weak(current,
294
+ current >= n ? current - n : 0,
295
+ std::memory_order_seq_cst,
296
+ std::memory_order_relaxed)) {
297
+ }
298
+ }
299
+
300
+ // Arm the inter-read idle timer, if configured. It is armed only while a socket read is in
301
+ // flight (see maybe_feed_lexer) so a legitimately slow consumer — which produces no socket
302
+ // traffic — is never timed out; a fire means the server stalled mid-body.
303
+ void arm_idle_timer()
304
+ {
305
+ if (options_.idle_timeout.count() <= 0) {
306
+ return;
307
+ }
308
+ const std::scoped_lock<std::mutex> lock{ idle_timer_mutex_ };
309
+ if (idle_disarmed_) {
310
+ return;
311
+ }
312
+ const auto generation = idle_generation_.fetch_add(1, std::memory_order_relaxed) + 1;
313
+ idle_timer_.expires_after(options_.idle_timeout);
314
+ idle_timer_.async_wait([weak = weak_from_this(), generation](std::error_code ec) {
315
+ if (ec == asio::error::operation_aborted) {
316
+ return;
317
+ }
318
+ auto self = weak.lock();
319
+ if (!self) {
320
+ return;
321
+ }
322
+ // Ignore a completion from a timer that has already been superseded: when a read completes it
323
+ // cancels and bumps the generation, but a timer that fired at almost the same instant may
324
+ // already be queued. Acting on such a stale fire would abort the healthy next read and
325
+ // misreport it as a timeout.
326
+ {
327
+ // Under the lock cancel_idle_timer() bumps the generation in: on another thread, cancel()
328
+ // could otherwise bump and clear timed_out_ between this test and the store, and the
329
+ // cancelled read would be reported as a timeout.
330
+ const std::scoped_lock<std::mutex> lock{ self->idle_timer_mutex_ };
331
+ if (self->idle_generation_.load(std::memory_order_relaxed) != generation) {
332
+ return;
333
+ }
334
+ // Server stalled while a read was in flight: abort it; the read completion delivers the
335
+ // terminal with a timeout error (steered by timed_out_).
336
+ self->timed_out_ = true;
337
+ }
338
+ self->body_.cancel();
339
+ });
340
+ }
341
+
342
+ // Cancels the idle timer and supersedes its generation, so a completion already queued is
343
+ // ignored.
344
+ void cancel_idle_timer()
345
+ {
346
+ const std::scoped_lock<std::mutex> lock{ idle_timer_mutex_ };
347
+ idle_timer_.cancel();
348
+ idle_generation_.fetch_add(1, std::memory_order_relaxed);
349
+ }
350
+
157
351
  void maybe_feed_lexer()
158
352
  {
159
- if (feeding_ || received_all_data_ || buffered_row_count_ > ROW_BUFFER_FEED_THRESHOLD) {
353
+ // Sequentially consistent, as is the releasing read-modify-write in release_buffered_bytes().
354
+ // A consumer that has drained below the low-water mark and lost the gate below leaves the
355
+ // resume to this load, which has to observe its release.
356
+ if (received_all_data_ ||
357
+ buffered_bytes_.load(std::memory_order_seq_cst) > options_.high_water_bytes) {
160
358
  return;
161
359
  }
162
360
 
163
- feeding_ = true;
361
+ // Atomically claim the feeding gate. With a multi-threaded io_context two resume paths (a
362
+ // next_row completion draining a slot and the feed chain re-entering) can reach here at once;
363
+ // only one may hold an outstanding body_.next().
364
+ bool expected = false;
365
+ if (!feeding_.compare_exchange_strong(expected, true)) {
366
+ return;
367
+ }
368
+ arm_idle_timer();
164
369
 
165
- body_.next([self = shared_from_this()](const auto& data, auto ec) mutable {
370
+ body_.next([self = shared_from_this()](const auto& data, bool has_more, auto ec) mutable {
371
+ // A read completed (or was aborted); the idle timer only guards an in-flight read. Cancel it
372
+ // and bump the generation so a timer that fired at the same instant is treated as stale.
373
+ self->cancel_idle_timer();
166
374
  if (ec) {
167
375
  self->received_all_data_ = true;
168
- auto signal = row_stream_end_signal{ ec };
169
- return self->rows_.async_send({}, std::move(signal), [self](auto ec) {
170
- if (!ec || ec == asio::experimental::error::channel_cancelled ||
171
- ec == asio::experimental::error::channel_closed) {
172
- return;
173
- }
174
- CB_LOG_WARNING("unexpected error while sending row stream end signal: {} ({})",
175
- ec.value(),
176
- ec.message());
177
- });
178
- }
179
- if (data.empty()) {
180
- self->received_all_data_ = true;
376
+ // If the idle timer aborted the read, report a timeout rather than the raw cancel error.
377
+ // Classify it the way the buffered path (http_command) does: a read-only request is
378
+ // idempotent, so its timeout is unambiguous (definitely not applied); a mutating request
379
+ // must be ambiguous, because a retry layer that reads unambiguous would consider it safe to
380
+ // replay and could double-apply the mutation.
381
+ auto terminal_ec = ec;
382
+ if (self->timed_out_) {
383
+ terminal_ec = self->options_.is_read_only
384
+ ? std::error_code{ errc::common::unambiguous_timeout }
385
+ : std::error_code{ errc::common::ambiguous_timeout };
386
+ }
387
+ // A read error before any metadata header (e.g. the connection is reset right after the
388
+ // response headers) must still resolve start()'s handler rather than hang the caller.
389
+ self->deliver_metadata_header(terminal_ec, {});
390
+ // Only enqueue a terminal if the lexer has not already delivered one: on a keep-alive
391
+ // socket a read can error after the JSON document already completed, which would otherwise
392
+ // enqueue a second, never-consumed terminal.
393
+ if (!self->lexer_completed_) {
394
+ auto signal = row_stream_end_signal{ terminal_ec };
395
+ return self->rows_.async_send({}, std::move(signal), [self](auto ec) {
396
+ if (!ec || ec == asio::experimental::error::channel_cancelled ||
397
+ ec == asio::experimental::error::channel_closed) {
398
+ return;
399
+ }
400
+ CB_LOG_WARNING("unexpected error while sending row stream end signal: {} ({})",
401
+ ec.value(),
402
+ ec.message());
403
+ });
404
+ }
181
405
  return;
182
406
  }
183
- {
184
- const std::lock_guard<std::mutex> lock{ self->data_feed_mutex_ };
407
+ if (!data.empty()) {
408
+ // The chunk may be empty even mid-stream (a socket read consumed entirely by HTTP chunk
409
+ // framing); only feed the lexer when there are actual body bytes. End-of-stream is keyed
410
+ // off has_more, never off emptiness, so a framing-only read never looks like completion.
411
+ const std::scoped_lock<std::mutex> lock{ self->data_feed_mutex_ };
185
412
  self->lexer_.feed(data);
186
413
  }
414
+ if (!has_more) {
415
+ // Transport reached the end of the body. For a well-formed response the lexer's
416
+ // on_complete already delivered the terminal row_stream_end_signal; if the body ended
417
+ // before the JSON document completed (truncated/malformed response), no terminal would
418
+ // otherwise reach the consumer and it would block forever — surface a parsing failure.
419
+ self->received_all_data_ = true;
420
+ if (!self->lexer_completed_) {
421
+ // The body ended without the lexer completing the document: a truncated/malformed
422
+ // response, an empty body, or a valid-JSON-but-not-an-object root that the lexer rejects
423
+ // without ever emitting a metadata header. Resolve start()'s handler and enqueue a
424
+ // terminal so a consumer parked in start() or next_row() is not stuck forever.
425
+ self->deliver_metadata_header(errc::common::parsing_failure, {});
426
+ auto signal = row_stream_end_signal{ errc::common::parsing_failure };
427
+ self->rows_.async_send({}, std::move(signal), [self](auto ec) {
428
+ if (ec && ec != asio::experimental::error::channel_cancelled &&
429
+ ec != asio::experimental::error::channel_closed) {
430
+ CB_LOG_WARNING("unexpected error while sending row stream end signal: {} ({})",
431
+ ec.value(),
432
+ ec.message());
433
+ }
434
+ });
435
+ }
436
+ return;
437
+ }
187
438
 
188
439
  self->feeding_ = false;
189
440
 
@@ -196,10 +447,31 @@ private:
196
447
  asio::experimental::concurrent_channel<void(std::error_code,
197
448
  std::variant<std::string, row_stream_end_signal>)>
198
449
  rows_;
199
- std::atomic_size_t buffered_row_count_{ 0 };
450
+ row_streamer_options options_;
451
+ asio::steady_timer idle_timer_;
452
+ // Armed from the read path and cancelled from the read completion and from cancel() on its
453
+ // caller's thread, which differ once more than one thread runs the io_context or cancel() is
454
+ // called off it. Also held across the timer completion's generation test and its timed_out_
455
+ // store.
456
+ std::mutex idle_timer_mutex_{};
457
+ // Set by cancel(); guarded by idle_timer_mutex_.
458
+ bool idle_disarmed_{ false };
459
+ std::atomic_size_t buffered_bytes_{ 0 };
200
460
  std::atomic_bool received_all_data_{ false };
201
461
  std::atomic_bool feeding_{ false };
202
- std::optional<std::string> metadata_header_;
462
+ std::atomic_bool lexer_completed_{ false };
463
+ std::atomic_bool timed_out_{ false };
464
+ // Guards the pair below against set_deadline(): the check and the arm, and the state change and
465
+ // the supersession, each have to be one step or an arm can land on a cancelled stream.
466
+ std::mutex cancel_mutex_{};
467
+ bool cancelled_{ false };
468
+ // Guards the one-shot delivery of the preamble handler (see deliver_metadata_header): the stream
469
+ // must resolve start()'s handler on every terminal, even when the lexer never fires its metadata
470
+ // callback (empty body, or a valid-JSON-but-not-an-object root).
471
+ std::atomic_bool metadata_header_delivered_{ false };
472
+ utils::movable_function<void(std::string, std::error_code)> metadata_header_handler_{};
473
+ // Bumped on every arm/cancel of the idle timer so a superseded timer completion is ignored.
474
+ std::atomic_uint64_t idle_generation_{ 0 };
203
475
  std::optional<std::string> metadata_;
204
476
  utils::json::streaming_lexer lexer_;
205
477
  std::mutex data_feed_mutex_{};
@@ -208,8 +480,9 @@ private:
208
480
 
209
481
  row_streamer::row_streamer(asio::io_context& io,
210
482
  couchbase::core::http_response_body body,
211
- const std::string& pointer_expression)
212
- : impl_{ std::make_shared<row_streamer_impl>(io, std::move(body), pointer_expression) }
483
+ const std::string& pointer_expression,
484
+ row_streamer_options options)
485
+ : impl_{ std::make_shared<row_streamer_impl>(io, std::move(body), pointer_expression, options) }
213
486
  {
214
487
  }
215
488
 
@@ -225,6 +498,13 @@ row_streamer::next_row(utils::movable_function<void(std::string, std::error_code
225
498
  impl_->next_row(std::move(handler));
226
499
  }
227
500
 
501
+ auto
502
+ row_streamer::set_deadline(std::chrono::time_point<std::chrono::steady_clock> deadline_tp)
503
+ -> io::deadline_state
504
+ {
505
+ return impl_->set_deadline(deadline_tp);
506
+ }
507
+
228
508
  void
229
509
  row_streamer::cancel()
230
510
  {
@@ -236,4 +516,10 @@ row_streamer::metadata() -> std::optional<std::string>
236
516
  {
237
517
  return impl_->metadata();
238
518
  }
519
+
520
+ auto
521
+ row_streamer::buffered_bytes() const -> std::size_t
522
+ {
523
+ return impl_->buffered_bytes();
524
+ }
239
525
  } // namespace couchbase::core
@@ -17,8 +17,11 @@
17
17
 
18
18
  #pragma once
19
19
 
20
+ #include "core/io/stream_deadline.hxx"
21
+ #include "row_streamer_options.hxx"
20
22
  #include "utils/movable_function.hxx"
21
23
 
24
+ #include <chrono>
22
25
  #include <memory>
23
26
  #include <optional>
24
27
  #include <string>
@@ -39,7 +42,8 @@ class row_streamer
39
42
  public:
40
43
  row_streamer(asio::io_context& io,
41
44
  http_response_body body,
42
- const std::string& pointer_expression);
45
+ const std::string& pointer_expression,
46
+ row_streamer_options options = {});
43
47
 
44
48
  /**
45
49
  * Starts the row stream and returns all the metadata preceding the first row. This typically
@@ -52,6 +56,26 @@ public:
52
56
  */
53
57
  void next_row(utils::movable_function<void(std::string, std::error_code)>&& handler);
54
58
 
59
+ /**
60
+ * Closes the HTTP body at `deadline_tp`, whether or not a row is being pulled, releasing the
61
+ * connection where one is still held. A response received in full holds none: once its bytes
62
+ * have also been parsed the call arms nothing and reports body_already_ended, and while bytes
63
+ * remain unparsed it ends the body, dropping those and reporting the timeout. Rows already
64
+ * parsed stay consumable either way. The inter-read idle timer is armed only while a socket
65
+ * read is in flight, and a consumer stopped above the high-water mark has none.
66
+ *
67
+ * The terminal reaches the consumer through a pull. A pull outstanding at expiry completes
68
+ * with the timeout. A read that completes the response is the exception: the whole response
69
+ * arrived, so those bytes are delivered as a clean end. Otherwise the next pull reports the
70
+ * terminal, after any rows already buffered.
71
+ *
72
+ * Optional. The terminal is unambiguous_timeout for a read-only request, ambiguous_timeout
73
+ * otherwise. Re-arming before expiry replaces the deadline. Once the stream has ended a
74
+ * further call arms nothing and reports body_already_ended. Callable from any thread.
75
+ */
76
+ [[nodiscard]] auto set_deadline(std::chrono::time_point<std::chrono::steady_clock> deadline_tp)
77
+ -> io::deadline_state;
78
+
55
79
  /**
56
80
  * Cancels the row stream & closes the HTTP connection
57
81
  */
@@ -62,6 +86,12 @@ public:
62
86
  */
63
87
  auto metadata() -> std::optional<std::string>;
64
88
 
89
+ /**
90
+ * Number of row bytes currently buffered (delivered to the channel but not yet consumed).
91
+ * Exposed for back-pressure observability in tests and diagnostics.
92
+ */
93
+ [[nodiscard]] auto buffered_bytes() const -> std::size_t;
94
+
65
95
  private:
66
96
  std::shared_ptr<row_streamer_impl> impl_;
67
97
  };
@@ -0,0 +1,55 @@
1
+ /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2
+ /*
3
+ * Copyright 2026. Couchbase, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ #pragma once
19
+
20
+ #include <chrono>
21
+ #include <cstddef>
22
+ #include <cstdint>
23
+
24
+ namespace couchbase::core
25
+ {
26
+ /**
27
+ * Tuning knobs for the streaming row engine (row_streamer). These are internal to core — there is
28
+ * no public API to set them yet; sensible static defaults apply unless a core caller (e.g. via
29
+ * cluster_options::streaming) overrides them.
30
+ */
31
+ struct row_streamer_options {
32
+ // JSONPointer nesting depth the lexer descends before treating a value as a row.
33
+ std::uint32_t lexer_depth{ 4 };
34
+ // Buffered row bytes above which socket reads pause, and at or below which they resume
35
+ // (hysteresis). A zero low-water mark is honoured: reading resumes once the buffer is empty.
36
+ std::size_t high_water_bytes{ std::size_t{ 2 } * 1024 * 1024 };
37
+ std::size_t low_water_bytes{ std::size_t{ 512 } * 1024 };
38
+ // Hard ceiling on a single row (or the trailing metadata) before a synthetic error is raised.
39
+ std::size_t max_row_bytes{ std::size_t{ 64 } * 1024 * 1024 };
40
+ // Capacity of the row hand-off channel, in rows. A secondary bound; the byte watermarks above
41
+ // are the primary back-pressure limit.
42
+ std::size_t row_buffer_size{ 100 };
43
+ // Maximum time to wait for the server between socket reads while a read is in flight (an idle
44
+ // timer armed only while pulling, so a legitimately slow consumer is never timed out). Zero
45
+ // disables the timer.
46
+ std::chrono::milliseconds idle_timeout{ 0 };
47
+ // Whether the request being streamed is read-only (idempotent). Mirrors the buffered path's
48
+ // http_command branch. It classifies both timeout terminals, the inter-read idle timeout and
49
+ // the whole-stream deadline: unambiguous_timeout for a read-only request (definitely not
50
+ // applied, safe to retry) and ambiguous_timeout otherwise (a mutating query such as
51
+ // UPDATE ... RETURNING may have partially executed, so a retry layer must not treat it as safe
52
+ // to replay).
53
+ bool is_read_only{ false };
54
+ };
55
+ } // namespace couchbase::core
@@ -22,7 +22,10 @@
22
22
 
23
23
  #include <functional>
24
24
  #include <memory>
25
+ #include <string>
26
+ #include <string_view>
25
27
  #include <utility>
28
+ #include <vector>
26
29
 
27
30
  namespace couchbase::core::sasl
28
31
  {