couchbase 4.2.11-rc.1 → 4.2.11

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 (26) hide show
  1. package/deps/couchbase-cxx-client/CMakeLists.txt +1 -0
  2. package/deps/couchbase-cxx-client/cmake/ThirdPartyDependencies.cmake +2 -0
  3. package/deps/couchbase-cxx-client/core/bucket.cxx +2 -2
  4. package/deps/couchbase-cxx-client/core/io/mcbp_session.cxx +11 -0
  5. package/deps/couchbase-cxx-client/core/io/mcbp_session.hxx +1 -0
  6. package/deps/couchbase-cxx-client/core/range_scan_load_balancer.cxx +141 -0
  7. package/deps/couchbase-cxx-client/core/range_scan_load_balancer.hxx +64 -0
  8. package/deps/couchbase-cxx-client/core/range_scan_orchestrator.cxx +224 -336
  9. package/deps/couchbase-cxx-client/core/range_scan_orchestrator.hxx +5 -6
  10. package/deps/couchbase-cxx-client/core/range_scan_orchestrator_options.hxx +6 -4
  11. package/deps/couchbase-cxx-client/core/scan_result.hxx +1 -11
  12. package/deps/couchbase-cxx-client/couchbase/bucket.hxx +2 -2
  13. package/deps/couchbase-cxx-client/couchbase/cluster.hxx +1 -1
  14. package/deps/couchbase-cxx-client/couchbase/collection.hxx +1 -0
  15. package/deps/couchbase-cxx-client/couchbase/collection_query_index_manager.hxx +1 -1
  16. package/deps/couchbase-cxx-client/couchbase/error_context.hxx +1 -0
  17. package/deps/couchbase-cxx-client/couchbase/get_links_analytics_options.hxx +2 -2
  18. package/deps/couchbase-cxx-client/couchbase/scope.hxx +1 -1
  19. package/deps/couchbase-cxx-client/couchbase/search_options.hxx +2 -2
  20. package/deps/couchbase-cxx-client/couchbase/search_result.hxx +1 -1
  21. package/deps/couchbase-cxx-client/couchbase/subdocument_error_context.hxx +1 -0
  22. package/deps/couchbase-cxx-client/couchbase/transactions/transaction_options.hxx +1 -1
  23. package/deps/couchbase-cxx-client/couchbase-sdk-cxx-black-duck-manifest.yaml +1 -0
  24. package/package.json +7 -7
  25. package/src/jstocbpp_autogen.hpp +0 -1
  26. 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