couchbase 4.2.11-rc.1 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. package/deps/couchbase-cxx-cache/mozilla-ca-bundle.crt +49 -2
  2. package/deps/couchbase-cxx-cache/mozilla-ca-bundle.sha256 +1 -1
  3. package/deps/couchbase-cxx-client/CMakeLists.txt +1 -0
  4. package/deps/couchbase-cxx-client/cmake/ThirdPartyDependencies.cmake +2 -0
  5. package/deps/couchbase-cxx-client/core/bucket.cxx +2 -2
  6. package/deps/couchbase-cxx-client/core/impl/cluster.cxx +51 -5
  7. package/deps/couchbase-cxx-client/core/impl/collection.cxx +224 -209
  8. package/deps/couchbase-cxx-client/core/impl/query_error_context.cxx +2 -2
  9. package/deps/couchbase-cxx-client/core/impl/query_index_manager.cxx +1 -0
  10. package/deps/couchbase-cxx-client/core/io/dns_client.cxx +4 -0
  11. package/deps/couchbase-cxx-client/core/io/dns_config.cxx +15 -4
  12. package/deps/couchbase-cxx-client/core/io/dns_config.hxx +1 -1
  13. package/deps/couchbase-cxx-client/core/io/mcbp_session.cxx +95 -53
  14. package/deps/couchbase-cxx-client/core/io/mcbp_session.hxx +1 -0
  15. package/deps/couchbase-cxx-client/core/mcbp/operation_queue.cxx +1 -0
  16. package/deps/couchbase-cxx-client/core/meta/features.hxx +5 -0
  17. package/deps/couchbase-cxx-client/core/operations/document_lookup_in_all_replicas.hxx +116 -105
  18. package/deps/couchbase-cxx-client/core/operations/document_lookup_in_any_replica.hxx +116 -108
  19. package/deps/couchbase-cxx-client/core/operations/document_search.cxx +97 -81
  20. package/deps/couchbase-cxx-client/core/operations/document_search.hxx +5 -0
  21. package/deps/couchbase-cxx-client/core/range_scan_load_balancer.cxx +141 -0
  22. package/deps/couchbase-cxx-client/core/range_scan_load_balancer.hxx +64 -0
  23. package/deps/couchbase-cxx-client/core/range_scan_orchestrator.cxx +224 -336
  24. package/deps/couchbase-cxx-client/core/range_scan_orchestrator.hxx +5 -6
  25. package/deps/couchbase-cxx-client/core/range_scan_orchestrator_options.hxx +8 -5
  26. package/deps/couchbase-cxx-client/core/scan_result.hxx +1 -11
  27. package/deps/couchbase-cxx-client/core/transactions/atr_cleanup_entry.cxx +16 -7
  28. package/deps/couchbase-cxx-client/core/transactions/attempt_context_impl.cxx +578 -483
  29. package/deps/couchbase-cxx-client/core/transactions/attempt_context_testing_hooks.cxx +51 -50
  30. package/deps/couchbase-cxx-client/core/transactions/attempt_context_testing_hooks.hxx +4 -2
  31. package/deps/couchbase-cxx-client/core/transactions/cleanup_testing_hooks.cxx +6 -6
  32. package/deps/couchbase-cxx-client/core/transactions/cleanup_testing_hooks.hxx +3 -2
  33. package/deps/couchbase-cxx-client/core/transactions/internal/transactions_cleanup.hxx +2 -0
  34. package/deps/couchbase-cxx-client/core/transactions/internal/utils.hxx +5 -1
  35. package/deps/couchbase-cxx-client/core/transactions/staged_mutation.cxx +222 -179
  36. package/deps/couchbase-cxx-client/core/transactions/staged_mutation.hxx +23 -12
  37. package/deps/couchbase-cxx-client/core/transactions/transactions.cxx +61 -24
  38. package/deps/couchbase-cxx-client/core/transactions/transactions_cleanup.cxx +36 -16
  39. package/deps/couchbase-cxx-client/core/transactions/utils.cxx +9 -0
  40. package/deps/couchbase-cxx-client/core/transactions.hxx +40 -7
  41. package/deps/couchbase-cxx-client/couchbase/bucket.hxx +2 -2
  42. package/deps/couchbase-cxx-client/couchbase/cluster.hxx +20 -1
  43. package/deps/couchbase-cxx-client/couchbase/collection.hxx +1 -0
  44. package/deps/couchbase-cxx-client/couchbase/collection_query_index_manager.hxx +1 -1
  45. package/deps/couchbase-cxx-client/couchbase/error_context.hxx +1 -0
  46. package/deps/couchbase-cxx-client/couchbase/fork_event.hxx +39 -0
  47. package/deps/couchbase-cxx-client/couchbase/get_links_analytics_options.hxx +2 -2
  48. package/deps/couchbase-cxx-client/couchbase/scope.hxx +1 -1
  49. package/deps/couchbase-cxx-client/couchbase/search_options.hxx +2 -2
  50. package/deps/couchbase-cxx-client/couchbase/search_result.hxx +1 -1
  51. package/deps/couchbase-cxx-client/couchbase/subdocument_error_context.hxx +1 -0
  52. package/deps/couchbase-cxx-client/couchbase/transactions/transaction_options.hxx +1 -1
  53. package/deps/couchbase-cxx-client/couchbase-sdk-cxx-black-duck-manifest.yaml +1 -0
  54. package/dist/binding.d.ts +8 -0
  55. package/dist/bindingutilities.d.ts +6 -1
  56. package/dist/bindingutilities.js +15 -1
  57. package/dist/bucketmanager.d.ts +0 -12
  58. package/dist/cluster.d.ts +0 -2
  59. package/dist/cluster.js +0 -2
  60. package/dist/collection.d.ts +3 -3
  61. package/dist/collection.js +3 -1
  62. package/dist/querytypes.d.ts +0 -2
  63. package/dist/rangeScan.d.ts +0 -8
  64. package/dist/rangeScan.js +0 -8
  65. package/dist/scope.d.ts +0 -5
  66. package/dist/scope.js +0 -5
  67. package/dist/scopesearchindexmanager.d.ts +0 -2
  68. package/dist/scopesearchindexmanager.js +0 -2
  69. package/dist/searchexecutor.js +3 -1
  70. package/dist/searchtypes.d.ts +16 -6
  71. package/dist/searchtypes.js +2 -6
  72. package/dist/transactions.d.ts +23 -0
  73. package/dist/transactions.js +16 -10
  74. package/dist/vectorsearch.d.ts +8 -8
  75. package/dist/vectorsearch.js +7 -7
  76. package/package.json +7 -7
  77. package/src/instance.cpp +11 -1
  78. package/src/instance.hpp +1 -0
  79. package/src/jstocbpp_autogen.hpp +8 -1
  80. package/src/jstocbpp_transactions.hpp +40 -3
  81. package/src/transactions.cpp +12 -1
  82. package/tools/gen-bindings-json.py +0 -1
  83. package/deps/couchbase-cxx-client/core/scan_options.hxx +0 -44
@@ -16,19 +16,31 @@
16
16
  #include "range_scan_orchestrator.hxx"
17
17
 
18
18
  #include "agent.hxx"
19
- #include "core/logger/logger.hxx"
19
+ #include "logger/logger.hxx"
20
+ #include "range_scan_load_balancer.hxx"
21
+ #include "range_scan_options.hxx"
22
+
20
23
  #include "couchbase/error_codes.hxx"
24
+ #include "utils/movable_function.hxx"
21
25
 
22
26
  #include <asio/bind_executor.hpp>
27
+ #include <asio/experimental/concurrent_channel.hpp>
23
28
  #include <asio/io_context.hpp>
24
29
  #include <asio/post.hpp>
25
30
 
26
- #include <asio/experimental/concurrent_channel.hpp>
27
-
28
- #include <gsl/narrow>
31
+ #include <gsl/util>
29
32
 
33
+ #include <atomic>
34
+ #include <chrono>
30
35
  #include <future>
31
- #include <random>
36
+ #include <limits>
37
+ #include <map>
38
+ #include <memory>
39
+ #include <mutex>
40
+ #include <optional>
41
+ #include <system_error>
42
+ #include <variant>
43
+ #include <vector>
32
44
 
33
45
  namespace couchbase::core
34
46
  {
@@ -50,24 +62,26 @@ mutation_state_to_snapshot_requirements(const std::optional<mutation_state>& sta
50
62
  return requirements;
51
63
  }
52
64
 
65
+ // Sent by the vbucket scan stream when it either completes or fails with a fatal error
66
+ struct scan_stream_end_signal {
67
+ std::uint16_t vbucket_id;
68
+ std::optional<std::error_code> error{};
69
+ };
70
+
53
71
  class range_scan_stream : public std::enable_shared_from_this<range_scan_stream>
54
72
  {
73
+ // The stream has failed and should not be retried
55
74
  struct failed {
56
75
  std::error_code ec;
57
76
  bool fatal{ true };
58
77
  };
59
78
 
60
- struct not_started {
61
- };
62
-
63
- struct awaiting_retry {
64
- std::error_code ec;
65
- };
66
-
79
+ // The stream is currently running
67
80
  struct running {
68
81
  std::vector<std::byte> uuid;
69
82
  };
70
83
 
84
+ // The stream has completed and the items have been retrieved
71
85
  struct completed {
72
86
  };
73
87
 
@@ -79,8 +93,8 @@ class range_scan_stream : public std::enable_shared_from_this<range_scan_stream>
79
93
  range_scan_create_options create_options,
80
94
  range_scan_continue_options continue_options,
81
95
  std::shared_ptr<scan_stream_manager> stream_manager)
82
- : items_{ io, continue_options.batch_item_limit }
83
- , agent_{ std::move(kv_provider) }
96
+ : agent_{ std::move(kv_provider) }
97
+ , io_{ io }
84
98
  , vbucket_id_{ vbucket_id }
85
99
  , node_id_{ node_id }
86
100
  , create_options_{ std::move(create_options) }
@@ -91,147 +105,119 @@ class range_scan_stream : public std::enable_shared_from_this<range_scan_stream>
91
105
 
92
106
  void start()
93
107
  {
94
- // Fail the stream if more time since the timeout has elapsed since the stream was first attempted (if this is a retry)
108
+ // Fail the stream if more time than the timeout has elapsed since the stream was first attempted (if this is a retry)
95
109
  if (first_attempt_timestamp_.has_value()) {
96
110
  if (std::chrono::steady_clock::now() - first_attempt_timestamp_.value() > create_options_.timeout) {
97
- CB_LOG_DEBUG("stream for vbucket_id {} cannot be retried any longer because it has exceeded the timeout", vbucket_id_);
111
+ CB_LOG_DEBUG("stream for vbucket_id {} cannot be retried because it has exceeded the timeout", vbucket_id_);
98
112
  state_ = failed{ errc::common::unambiguous_timeout, !is_sampling_scan() };
99
- stream_manager_->stream_start_failed(node_id_, error_is_fatal());
100
- drain_waiting_queue();
113
+ stream_manager_->stream_failed(node_id_, vbucket_id_, errc::common::unambiguous_timeout, error_is_fatal());
101
114
  return;
102
115
  }
103
116
  } else {
104
117
  first_attempt_timestamp_ = std::chrono::steady_clock::now();
105
118
  }
106
119
 
107
- CB_LOG_TRACE("starting stream {} in node {}", vbucket_id_, node_id_);
108
- state_ = std::monostate{};
120
+ CB_LOG_TRACE("starting stream for vbucket {} in node {}", vbucket_id_, node_id_);
121
+
109
122
  if (std::holds_alternative<range_scan>(create_options_.scan_type) && !last_seen_key_.empty()) {
110
123
  std::get<range_scan>(create_options_.scan_type).from = scan_term{ last_seen_key_ };
111
124
  }
112
125
 
113
- auto op = agent_.range_scan_create(vbucket_id_, create_options_, [self = shared_from_this()](auto res, auto ec) {
126
+ agent_.range_scan_create(vbucket_id_, create_options_, [self = shared_from_this()](auto res, auto ec) {
114
127
  if (ec) {
115
128
  if (ec == errc::key_value::document_not_found) {
116
129
  // Benign error
117
- CB_LOG_DEBUG("ignoring vbucket_id {} because no documents exist for it", self->vbucket_id_);
118
- CB_LOG_TRACE("setting state for stream {} to FAILED", self->vbucket_id_);
130
+ CB_LOG_TRACE("ignoring vbucket_id {} because no documents exist for it", self->vbucket_id_);
119
131
  self->state_ = failed{ ec, false };
120
- self->stream_manager_->stream_start_failed(self->node_id_, self->error_is_fatal());
132
+ self->stream_manager_->stream_failed(self->node_id_, self->vbucket_id_, ec, self->error_is_fatal());
121
133
  } else if (ec == errc::common::temporary_failure) {
122
- // Retryable error
123
- CB_LOG_DEBUG("received busy status from vbucket with ID {} - reducing concurrency & will retry", self->vbucket_id_);
124
- CB_LOG_TRACE("setting state for stream {} to AWAITING_RETRY", self->vbucket_id_);
125
- self->state_ = awaiting_retry{ ec };
134
+ // Retryable error - server is overwhelmed, retry after reducing concurrency
135
+ CB_LOG_DEBUG("received busy status during scan from vbucket with ID {} - reducing concurrency & retrying",
136
+ self->vbucket_id_);
137
+ self->state_ = std::monostate{};
126
138
  self->stream_manager_->stream_start_failed_awaiting_retry(self->node_id_, self->vbucket_id_);
127
139
  } else if (ec == errc::common::internal_server_failure || ec == errc::common::collection_not_found) {
128
140
  // Fatal errors
129
- CB_LOG_TRACE("setting state for stream {} to FAILED", self->vbucket_id_);
130
141
  self->state_ = failed{ ec, true };
131
- self->stream_manager_->stream_start_failed(self->node_id_, self->error_is_fatal());
142
+ self->stream_manager_->stream_failed(self->node_id_, self->vbucket_id_, ec, self->error_is_fatal());
132
143
  } else {
133
144
  // Unexpected errors
134
- CB_LOG_DEBUG(
135
- "received unexpected error {} from stream for vbucket {} ({})", ec.value(), self->vbucket_id_, ec.message());
136
- CB_LOG_TRACE("setting state for stream {} to FAILED", self->vbucket_id_);
145
+ CB_LOG_DEBUG("received unexpected error {} from stream for vbucket {} during range scan continue ({})",
146
+ ec.value(),
147
+ self->vbucket_id_,
148
+ ec.message());
137
149
  self->state_ = failed{ ec, true };
138
- self->stream_manager_->stream_start_failed(self->node_id_, self->error_is_fatal());
150
+ self->stream_manager_->stream_failed(self->node_id_, self->vbucket_id_, ec, self->error_is_fatal());
139
151
  }
140
- self->drain_waiting_queue();
141
152
  return;
142
153
  }
154
+
143
155
  self->state_ = running{ std::move(res.scan_uuid) };
144
- CB_LOG_TRACE("setting state for stream {} to RUNNING", self->vbucket_id_);
145
- self->drain_waiting_queue();
146
- self->resume();
156
+
157
+ return self->resume();
147
158
  });
148
159
  }
149
160
 
150
- void fail(std::error_code ec)
161
+ void should_cancel()
151
162
  {
152
- if (!is_failed()) {
153
- if (is_running()) {
154
- agent_.range_scan_cancel(uuid(), vbucket_id_, {}, [](auto /* res */, auto /* ec */) {});
155
- }
156
-
157
- items_.cancel();
158
- items_.close();
159
-
160
- bool fatal{};
161
- if (ec == errc::key_value::document_not_found || ec == errc::common::authentication_failure ||
162
- ec == errc::common::collection_not_found || ec == errc::common::request_canceled) {
163
- // Errors that are fatal unless this is a sampling scan
164
- fatal = !is_sampling_scan();
165
- } else if (ec == errc::common::feature_not_available || ec == errc::common::invalid_argument ||
166
- ec == errc::common::temporary_failure) {
167
- // Errors that are always fatal
168
- fatal = true;
169
- } else {
170
- // Unexpected error - always fatal
171
- CB_LOG_DEBUG("received unexpected error {} from stream for vbucket during range scan continue {} ({})",
172
- ec.value(),
173
- vbucket_id_,
174
- ec.message());
175
- fatal = true;
176
- }
177
-
178
- CB_LOG_TRACE("setting state for stream {} to FAILED after range scan continue", vbucket_id_);
179
- state_ = failed{ ec, fatal };
180
- stream_manager_->stream_continue_failed(node_id_, fatal);
181
- }
163
+ should_cancel_ = true;
182
164
  }
183
165
 
184
- void mark_not_started()
166
+ [[nodiscard]] auto node_id() const -> std::int16_t
185
167
  {
186
- state_ = not_started{};
168
+ return node_id_;
187
169
  }
188
170
 
189
- void complete()
171
+ private:
172
+ void fail(std::error_code ec)
190
173
  {
191
- if (!is_failed() && !is_completed()) {
192
- CB_LOG_TRACE("setting state for stream {} to COMPLETED", vbucket_id_);
193
-
194
- stream_manager_->stream_completed(node_id_);
195
- state_ = completed{};
196
- drain_waiting_queue();
174
+ if (is_failed()) {
175
+ return;
197
176
  }
198
- }
199
177
 
200
- void cancel()
201
- {
202
- if (!should_cancel_) {
203
- should_cancel_ = true;
204
- items_.cancel();
205
- items_.close();
178
+ bool fatal;
179
+ if (ec == errc::key_value::document_not_found || ec == errc::common::authentication_failure ||
180
+ ec == errc::common::collection_not_found || ec == errc::common::request_canceled) {
181
+ // Errors that are fatal unless this is a sampling scan
182
+ fatal = !is_sampling_scan();
183
+ } else if (ec == errc::common::feature_not_available || ec == errc::common::invalid_argument ||
184
+ ec == errc::common::temporary_failure) {
185
+ // Errors that are always fatal
186
+ fatal = true;
187
+ } else {
188
+ // Unexpected error - always fatal
189
+ CB_LOG_DEBUG("received unexpected error {} from stream for vbucket {} during range scan continue ({})",
190
+ ec.value(),
191
+ vbucket_id_,
192
+ ec.message());
193
+ fatal = true;
206
194
  }
207
- }
208
195
 
209
- template<typename Handler>
210
- void take(Handler&& handler)
211
- {
212
- do_when_ready([self = shared_from_this(), handler = std::forward<Handler>(handler)]() mutable {
213
- self->take_when_ready(std::forward<Handler>(handler));
214
- });
196
+ state_ = failed{ ec, fatal };
197
+ stream_manager_->stream_failed(node_id_, vbucket_id_, ec, fatal);
215
198
  }
216
199
 
217
- [[nodiscard]] auto node_id() const -> int16_t
200
+ void complete()
218
201
  {
219
- return node_id_;
220
- }
202
+ if (is_failed() || is_completed()) {
203
+ return;
204
+ }
221
205
 
222
- [[nodiscard]] auto is_ready() const -> bool
223
- {
224
- return !std::holds_alternative<std::monostate>(state_);
206
+ stream_manager_->stream_completed(node_id_, vbucket_id_);
207
+ state_ = completed{};
225
208
  }
226
209
 
227
- [[nodiscard]] auto is_not_started() const -> bool
210
+ void cancel()
228
211
  {
229
- return std::holds_alternative<not_started>(state_);
230
- }
212
+ auto scan_uuid = uuid();
213
+ if (scan_uuid.empty()) {
214
+ // The stream is not currently running
215
+ return;
216
+ }
231
217
 
232
- [[nodiscard]] auto is_awaiting_retry() const -> bool
233
- {
234
- return std::holds_alternative<awaiting_retry>(state_);
218
+ asio::post(asio::bind_executor(io_, [self = shared_from_this(), scan_uuid]() mutable {
219
+ self->agent_.range_scan_cancel(scan_uuid, self->vbucket_id_, {}, [](auto /* res */, auto /* ec */) {});
220
+ }));
235
221
  }
236
222
 
237
223
  [[nodiscard]] auto is_running() const -> bool
@@ -249,94 +235,50 @@ class range_scan_stream : public std::enable_shared_from_this<range_scan_stream>
249
235
  return std::holds_alternative<completed>(state_);
250
236
  }
251
237
 
252
- private:
253
- template<typename Handler>
254
- void take_when_ready(Handler&& handler)
255
- {
256
-
257
- if (is_failed()) {
258
- if (error_is_fatal()) {
259
- return handler(std::optional<range_scan_item>{}, false, std::optional<std::error_code>{ error() });
260
- } else {
261
- return handler(std::optional<range_scan_item>{}, false, std::optional<std::error_code>{});
262
- }
263
- }
264
- if (is_awaiting_retry() || is_not_started()) {
265
- return handler(std::optional<range_scan_item>{}, true, std::optional<std::error_code>{});
266
- }
267
- if (!items_.ready()) {
268
- return handler(std::optional<range_scan_item>{}, is_running(), std::optional<std::error_code>{});
269
- }
270
- items_.async_receive(
271
- [self = shared_from_this(), handler = std::forward<Handler>(handler)](std::error_code ec, range_scan_item item) mutable {
272
- if (ec) {
273
- return handler(std::optional<range_scan_item>{}, false, std::optional<std::error_code>{});
274
- }
275
- handler(std::optional<range_scan_item>{ std::move(item) }, true, std::optional<std::error_code>{});
276
- });
277
- }
278
-
279
- template<typename Handler>
280
- void do_when_ready(Handler&& handler)
281
- {
282
- if (is_ready()) {
283
- drain_waiting_queue();
284
- return handler();
285
- }
286
- waiting_queue_.emplace_back(std::forward<Handler>(handler));
287
- }
288
-
289
- void drain_waiting_queue()
290
- {
291
- auto queue = std::move(waiting_queue_);
292
- for (auto const& waiter : queue) {
293
- waiter();
294
- }
295
- }
296
-
297
238
  void resume()
298
239
  {
299
240
  if (!is_running()) {
300
241
  return;
301
242
  }
302
243
  if (should_cancel_) {
303
- agent_.range_scan_cancel(uuid(), vbucket_id_, {}, [](auto /* res */, auto /* ec */) {});
304
- items_.close();
305
- items_.cancel();
244
+ cancel();
306
245
  return;
307
246
  }
308
247
 
309
- agent_.range_scan_continue(
310
- uuid(),
311
- vbucket_id_,
312
- continue_options_,
313
- [self = shared_from_this()](auto item) {
314
- self->last_seen_key_ = item.key;
315
- self->items_.async_send({}, std::move(item), [self](std::error_code ec) {
248
+ asio::post(asio::bind_executor(io_, [self = shared_from_this()]() mutable {
249
+ self->agent_.range_scan_continue(
250
+ self->uuid(),
251
+ self->vbucket_id_,
252
+ self->continue_options_,
253
+ [self](auto item) {
254
+ // The scan has already been cancelled, no need to send items
255
+ if (self->should_cancel_) {
256
+ return;
257
+ }
258
+ self->last_seen_key_ = item.key;
259
+ self->stream_manager_->stream_received_item(std::move(item));
260
+ },
261
+ [self](auto res, auto ec) {
316
262
  if (ec) {
317
- self->fail(ec);
263
+ return self->fail(ec);
264
+ }
265
+ if (res.complete) {
266
+ return self->complete();
267
+ }
268
+ if (res.more) {
269
+ return self->resume();
318
270
  }
319
271
  });
320
- },
321
- [self = shared_from_this()](auto res, auto ec) {
322
- if (ec) {
323
- return self->fail(ec);
324
- }
325
- if (res.complete) {
326
- return self->complete();
327
- }
328
- if (res.more) {
329
- return self->resume();
330
- }
331
- });
272
+ }));
332
273
  }
333
274
 
334
275
  [[nodiscard]] auto uuid() const -> std::vector<std::byte>
335
276
  {
336
- if (is_running()) {
277
+ try {
337
278
  return std::get<running>(state_).uuid;
279
+ } catch (std::bad_variant_access&) {
280
+ return {};
338
281
  }
339
- return {};
340
282
  }
341
283
 
342
284
  [[nodiscard]] auto error() const -> std::error_code
@@ -360,18 +302,17 @@ class range_scan_stream : public std::enable_shared_from_this<range_scan_stream>
360
302
  return std::holds_alternative<sampling_scan>(create_options_.scan_type);
361
303
  }
362
304
 
363
- asio::experimental::concurrent_channel<void(std::error_code, range_scan_item)> items_;
364
305
  agent agent_;
306
+ asio::io_context& io_;
365
307
  std::uint16_t vbucket_id_;
366
308
  std::int16_t node_id_;
367
309
  range_scan_create_options create_options_;
368
310
  range_scan_continue_options continue_options_;
369
311
  std::shared_ptr<scan_stream_manager> stream_manager_;
370
312
  std::string last_seen_key_{};
371
- std::variant<std::monostate, not_started, failed, awaiting_retry, running, completed> state_{};
313
+ std::variant<std::monostate, failed, running, completed> state_{};
372
314
  bool should_cancel_{ false };
373
315
  std::optional<std::chrono::time_point<std::chrono::steady_clock>> first_attempt_timestamp_{};
374
- std::vector<utils::movable_function<void()>> waiting_queue_{};
375
316
  };
376
317
 
377
318
  class range_scan_orchestrator_impl
@@ -392,6 +333,8 @@ class range_scan_orchestrator_impl
392
333
  , vbucket_map_{ std::move(vbucket_map) }
393
334
  , scope_name_{ std::move(scope_name) }
394
335
  , collection_name_{ std::move(collection_name) }
336
+ , load_balancer_{ vbucket_map_ }
337
+ , items_{ io, 1024 }
395
338
  , scan_type_{ std::move(scan_type) }
396
339
  , options_{ std::move(options) }
397
340
  , vbucket_to_snapshot_requirements_{ mutation_state_to_snapshot_requirements(options_.consistent_with) }
@@ -399,16 +342,22 @@ class range_scan_orchestrator_impl
399
342
  {
400
343
 
401
344
  if (std::holds_alternative<sampling_scan>(scan_type_)) {
402
- item_limit_ = std::get<sampling_scan>(scan_type).limit;
345
+ auto s = std::get<sampling_scan>(scan_type);
346
+ item_limit_ = s.limit;
347
+
348
+ // Set the seed of the load balancer to ensure that if the sampling scan is run multiple times the vbuckets
349
+ // are scanned in the same order when concurrency is 1. This guarantees that the items returned will be the
350
+ // same. We cannot guarantee this when concurrency is greater than 1, as the order of the vbucket scans
351
+ // depends on how long each scan takes and what the load on a node is at any given time.
352
+ if (s.seed.has_value()) {
353
+ load_balancer_.seed(s.seed.value());
354
+ }
403
355
  }
404
356
  }
405
357
 
406
358
  auto scan() -> tl::expected<scan_result, std::error_code>
407
359
  {
408
- if (item_limit_ == 0) {
409
- return tl::unexpected(errc::common::invalid_argument);
410
- }
411
- if (concurrency_ <= 0) {
360
+ if (item_limit_ == 0 || concurrency_ <= 0) {
412
361
  return tl::unexpected(errc::common::invalid_argument);
413
362
  }
414
363
 
@@ -434,6 +383,7 @@ class range_scan_orchestrator_impl
434
383
  range_scan_continue_options const continue_options{
435
384
  options_.batch_item_limit, options_.batch_byte_limit, batch_time_limit, options_.timeout, options_.retry_strategy,
436
385
  };
386
+
437
387
  for (std::uint16_t vbucket = 0; vbucket < gsl::narrow_cast<std::uint16_t>(vbucket_map_.size()); ++vbucket) {
438
388
  const range_scan_create_options create_options{
439
389
  scope_name_, {},
@@ -453,10 +403,6 @@ class range_scan_orchestrator_impl
453
403
  continue_options,
454
404
  std::static_pointer_cast<scan_stream_manager>(shared_from_this()));
455
405
  streams_[vbucket] = stream;
456
- streams_[vbucket]->mark_not_started();
457
- if (stream_count_per_node_.count(node_id) == 0) {
458
- stream_count_per_node_[node_id] = 0;
459
- }
460
406
  }
461
407
  start_streams(concurrency_);
462
408
 
@@ -467,7 +413,7 @@ class range_scan_orchestrator_impl
467
413
  {
468
414
  cancelled_ = true;
469
415
  for (const auto& [vbucket_id, stream] : streams_) {
470
- stream->cancel();
416
+ stream->should_cancel();
471
417
  }
472
418
  }
473
419
 
@@ -479,212 +425,154 @@ class range_scan_orchestrator_impl
479
425
  auto next() -> std::future<tl::expected<range_scan_item, std::error_code>> override
480
426
  {
481
427
  auto barrier = std::make_shared<std::promise<tl::expected<range_scan_item, std::error_code>>>();
482
- if (item_limit_ == 0 || item_limit_-- == 0) {
483
- barrier->set_value(tl::unexpected{ errc::key_value::range_scan_completed });
484
- cancel();
485
- } else {
486
- next_item(streams_.begin(), [barrier](std::optional<range_scan_item> item, std::optional<std::error_code> ec) {
487
- if (item) {
488
- barrier->set_value(std::move(item.value()));
489
- } else if (ec) {
490
- barrier->set_value(tl::unexpected{ ec.value() });
491
- } else {
492
- barrier->set_value(tl::unexpected{ errc::key_value::range_scan_completed });
493
- }
494
- });
495
- }
428
+ next([barrier](range_scan_item item, std::error_code ec) mutable {
429
+ if (ec) {
430
+ barrier->set_value(tl::unexpected{ ec });
431
+ } else {
432
+ barrier->set_value(std::move(item));
433
+ }
434
+ });
496
435
  return barrier->get_future();
497
436
  }
498
437
 
499
438
  void next(utils::movable_function<void(range_scan_item, std::error_code)> callback) override
500
439
  {
501
- auto handler = [callback = std::move(callback)](std::optional<range_scan_item> item, std::optional<std::error_code> ec) mutable {
502
- if (item) {
503
- callback(std::move(item.value()), {});
504
- } else if (ec) {
505
- callback({}, ec.value());
506
- } else {
507
- callback({}, errc::key_value::range_scan_completed);
508
- }
509
- };
510
440
  if (item_limit_ == 0 || item_limit_-- == 0) {
511
- handler({}, {});
441
+ callback({}, errc::key_value::range_scan_completed);
512
442
  cancel();
513
443
  } else {
514
- next_item(streams_.begin(), std::move(handler));
444
+ next_item(std::move(callback));
515
445
  }
516
446
  }
517
447
 
518
- void start_streams(std::uint16_t stream_count)
448
+ template<typename Handler>
449
+ void next_item(Handler&& handler)
519
450
  {
520
- std::lock_guard<std::recursive_mutex> const lock(stream_start_mutex_);
451
+ if (streams_.empty() || cancelled_) {
452
+ items_.cancel();
453
+ items_.close();
454
+ return handler({}, errc::key_value::range_scan_completed);
455
+ }
456
+ items_.async_receive([self = shared_from_this(), handler = std::forward<Handler>(handler)](
457
+ std::error_code ec, std::variant<range_scan_item, scan_stream_end_signal> it) mutable {
458
+ if (ec) {
459
+ return handler({}, ec);
460
+ }
521
461
 
462
+ if (std::holds_alternative<range_scan_item>(it)) {
463
+ handler(std::get<range_scan_item>(it), {});
464
+ } else {
465
+ auto signal = std::get<scan_stream_end_signal>(it);
466
+ if (signal.error.has_value()) {
467
+ // Fatal error
468
+ handler({}, signal.error.value());
469
+ } else {
470
+ // Empty signal means that stream has completed
471
+ {
472
+ std::lock_guard<std::mutex> const lock{ self->stream_map_mutex_ };
473
+ self->streams_.erase(signal.vbucket_id);
474
+ }
475
+ return asio::post(asio::bind_executor(self->io_, [self, handler = std::forward<Handler>(handler)]() mutable {
476
+ self->next_item(std::forward<Handler>(handler));
477
+ }));
478
+ }
479
+ }
480
+ });
481
+ }
482
+
483
+ void start_streams(std::uint16_t stream_count)
484
+ {
522
485
  if (cancelled_) {
523
486
  CB_LOG_TRACE("scan has been cancelled, do not start another stream");
524
487
  return;
525
488
  }
526
489
 
527
- if (stream_count_per_node_.empty()) {
528
- CB_LOG_TRACE("no more vbuckets to scan");
529
- return;
530
- }
531
-
532
- std::uint16_t counter = 0;
490
+ std::uint16_t counter{ 0 };
533
491
  while (counter < stream_count) {
534
- // Find the node with the least number of active streams from those recorded in stream_count_per_node_
535
- int16_t least_busy_node{};
536
- {
537
- std::lock_guard<std::mutex> const stream_count_lock(stream_count_per_node_mutex_);
538
-
539
- // Pick a random node
540
- std::random_device rd;
541
- std::mt19937_64 gen(rd());
542
- std::uniform_int_distribution<std::size_t> dis(0, stream_count_per_node_.size() - 1);
543
- auto it = stream_count_per_node_.begin();
544
- std::advance(it, static_cast<decltype(stream_count_per_node_)::difference_type>(dis(gen)));
545
- least_busy_node = it->first;
546
-
547
- // If any other node has fewer streams running use that
548
- for (const auto& [node_id, count] : stream_count_per_node_) {
549
- if (count < stream_count_per_node_[least_busy_node]) {
550
- least_busy_node = node_id;
551
- }
552
- }
492
+ auto vbucket_id = load_balancer_.select_vbucket();
493
+ if (!vbucket_id.has_value()) {
494
+ CB_LOG_TRACE("no more scans, all vbuckets have been scanned");
495
+ return;
553
496
  }
554
497
 
498
+ auto v = vbucket_id.value();
555
499
  std::shared_ptr<range_scan_stream> stream{};
556
500
  {
557
- std::lock_guard<std::mutex> const stream_map_lock(stream_map_mutex_);
558
-
559
- for (const auto& [v, s] : streams_) {
560
- if ((s->is_not_started() || s->is_awaiting_retry()) && (s->node_id() == least_busy_node)) {
561
- CB_LOG_TRACE("selected vbucket {} to scan", v);
562
- stream = s;
563
- break;
564
- }
565
- }
566
- }
567
-
568
- if (stream == nullptr) {
569
- CB_LOG_TRACE("no vbuckets to scan for node {}", least_busy_node);
570
- {
571
- std::lock_guard<std::mutex> const stream_count_lock(stream_count_per_node_mutex_);
572
- stream_count_per_node_.erase(least_busy_node);
573
- }
574
- return start_streams(static_cast<std::uint16_t>(stream_count - counter));
501
+ std::lock_guard<std::mutex> const lock{ stream_map_mutex_ };
502
+ stream = streams_.at(v);
575
503
  }
576
-
577
- auto node_id = stream->node_id();
504
+ CB_LOG_TRACE("scanning vbucket {} at node {}", vbucket_id.value(), stream->node_id());
578
505
  active_stream_count_++;
579
- stream_count_per_node_[node_id]++;
580
- stream->start();
581
506
  counter++;
507
+ asio::post(asio::bind_executor(io_, [stream]() mutable { stream->start(); }));
582
508
  }
583
509
  }
584
510
 
585
- void stream_start_failed(std::int16_t node_id, bool fatal) override
586
- {
587
- stream_no_longer_running(node_id);
588
- if (fatal) {
589
- cancel();
590
- } else {
591
- start_streams(1);
592
- }
593
- }
594
-
595
- void stream_start_failed_awaiting_retry(std::int16_t node_id, std::uint16_t /* vbucket_id */) override
511
+ void stream_received_item(range_scan_item item) override
596
512
  {
597
- {
598
- std::lock_guard<std::mutex> const stream_count_lock(stream_count_per_node_mutex_);
599
- if (stream_count_per_node_.count(node_id) == 0) {
600
- stream_count_per_node_[node_id] = 1;
513
+ items_.async_send({}, std::move(item), [](std::error_code ec) {
514
+ if (ec && ec != asio::experimental::error::channel_closed && ec != asio::experimental::error::channel_cancelled) {
515
+ CB_LOG_WARNING("unexpected error while sending to scan item channel: {} ({})", ec.value(), ec.message());
601
516
  }
602
- }
603
- stream_no_longer_running(node_id);
604
- if (active_stream_count_ == 0) {
605
- start_streams(1);
606
- }
517
+ });
607
518
  }
608
519
 
609
- void stream_continue_failed(std::int16_t node_id, bool fatal) override
520
+ void stream_failed(std::int16_t node_id, std::uint16_t vbucket_id, std::error_code ec, bool fatal) override
610
521
  {
611
- stream_no_longer_running(node_id);
612
- if (fatal) {
613
- cancel();
614
- } else {
615
- start_streams(1);
522
+ if (!fatal) {
523
+ return stream_completed(node_id, vbucket_id);
616
524
  }
617
- }
618
525
 
619
- void stream_completed(std::int16_t node_id) override
620
- {
621
- stream_no_longer_running(node_id);
622
- start_streams(1);
526
+ load_balancer_.notify_stream_ended(node_id);
527
+ active_stream_count_--;
528
+ items_.async_send({}, scan_stream_end_signal{ vbucket_id, ec }, [](std::error_code ec) {
529
+ if (ec && ec != asio::experimental::error::channel_closed && ec != asio::experimental::error::channel_cancelled) {
530
+ CB_LOG_WARNING("unexpected error while sending to scan item channel: {} ({})", ec.value(), ec.message());
531
+ }
532
+ });
533
+ return cancel();
623
534
  }
624
535
 
625
- private:
626
- void stream_no_longer_running(std::int16_t node_id)
536
+ void stream_completed(std::int16_t node_id, std::uint16_t vbucket_id) override
627
537
  {
628
- {
629
- std::lock_guard<std::mutex> const stream_count_lock(stream_count_per_node_mutex_);
630
- if (stream_count_per_node_.count(node_id) > 0) {
631
- stream_count_per_node_[node_id]--;
632
- }
633
- }
538
+ load_balancer_.notify_stream_ended(node_id);
634
539
  active_stream_count_--;
540
+ items_.async_send({}, scan_stream_end_signal{ vbucket_id }, [](std::error_code ec) {
541
+ if (ec && ec != asio::experimental::error::channel_closed && ec != asio::experimental::error::channel_cancelled) {
542
+ CB_LOG_WARNING("unexpected error while sending to scan item channel: {} ({})", ec.value(), ec.message());
543
+ }
544
+ });
545
+ return start_streams(1);
635
546
  }
636
547
 
637
- template<typename Iterator, typename Handler>
638
- void next_item(Iterator it, Handler&& handler)
548
+ void stream_start_failed_awaiting_retry(std::int16_t node_id, std::uint16_t vbucket_id) override
639
549
  {
640
- if (streams_.empty() || cancelled_) {
641
- return handler({}, {});
550
+ load_balancer_.notify_stream_ended(node_id);
551
+ active_stream_count_--;
552
+
553
+ load_balancer_.enqueue_vbucket(node_id, vbucket_id);
554
+ if (active_stream_count_ == 0) {
555
+ return start_streams(1);
642
556
  }
643
- auto vbucket_id = it->first;
644
- auto stream = it->second;
645
- stream->take([it = std::next(it), vbucket_id, self = shared_from_this(), handler = std::forward<Handler>(handler)](
646
- auto item, bool has_more, auto ec) mutable {
647
- if (ec) {
648
- // Fatal error
649
- self->streams_.clear();
650
- return handler({}, ec);
651
- }
652
- if (!has_more) {
653
- std::lock_guard<std::mutex> const lock(self->stream_map_mutex_);
654
- self->streams_.erase(vbucket_id);
655
- }
656
- if (item) {
657
- return handler(std::move(item), {});
658
- }
659
- if (self->streams_.empty()) {
660
- return handler({}, {});
661
- }
662
- if (it == self->streams_.end()) {
663
- it = self->streams_.begin();
664
- }
665
- return asio::post(asio::bind_executor(self->io_, [it, self, handler = std::forward<Handler>(handler)]() mutable {
666
- self->next_item(it, std::forward<Handler>(handler));
667
- }));
668
- });
669
557
  }
670
558
 
559
+ private:
671
560
  asio::io_context& io_;
672
561
  agent agent_;
673
562
  topology::configuration::vbucket_map vbucket_map_;
674
563
  std::string scope_name_;
675
564
  std::string collection_name_;
565
+ range_scan_load_balancer load_balancer_;
566
+ asio::experimental::concurrent_channel<void(std::error_code, std::variant<range_scan_item, scan_stream_end_signal>)> items_;
676
567
  std::uint32_t collection_id_{ 0 };
677
568
  std::variant<std::monostate, range_scan, prefix_scan, sampling_scan> scan_type_;
678
569
  range_scan_orchestrator_options options_;
679
570
  std::map<std::size_t, std::optional<range_snapshot_requirements>> vbucket_to_snapshot_requirements_;
680
571
  std::map<std::uint16_t, std::shared_ptr<range_scan_stream>> streams_{};
681
- std::map<std::int16_t, std::atomic_uint16_t> stream_count_per_node_{};
682
- std::recursive_mutex stream_start_mutex_{};
683
572
  std::mutex stream_map_mutex_{};
684
- std::mutex stream_count_per_node_mutex_{};
685
- std::atomic_uint16_t active_stream_count_ = 0;
686
- std::uint16_t concurrency_ = 1;
687
- std::size_t item_limit_{ std::numeric_limits<size_t>::max() };
573
+ std::atomic_uint16_t active_stream_count_{ 0 };
574
+ std::uint16_t concurrency_{ 1 };
575
+ std::size_t item_limit_{ std::numeric_limits<std::size_t>::max() };
688
576
  bool cancelled_{ false };
689
577
  };
690
578