couchbase 3.8.0 → 3.8.2

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/ext/cache/extconf_include.rb +4 -4
  4. data/ext/cache/mozilla-ca-bundle.crt +96 -675
  5. data/ext/cache/mozilla-ca-bundle.sha256 +1 -1
  6. data/ext/couchbase/CMakeLists.txt +3 -1
  7. data/ext/couchbase/core/error_context/base_error_context.hxx +32 -0
  8. data/ext/couchbase/core/error_context/key_value.cxx +1 -0
  9. data/ext/couchbase/core/error_context/key_value.hxx +23 -0
  10. data/ext/couchbase/core/error_context/key_value_error_context.hxx +35 -0
  11. data/ext/couchbase/core/error_context/subdocument_error_context.hxx +41 -0
  12. data/ext/couchbase/core/impl/binary_collection.cxx +123 -88
  13. data/ext/couchbase/core/impl/collection.cxx +416 -189
  14. data/ext/couchbase/core/impl/error.cxx +29 -4
  15. data/ext/couchbase/core/impl/invoke_with_node_id.hxx +44 -0
  16. data/ext/couchbase/core/impl/node_id.cxx +110 -0
  17. data/ext/couchbase/core/impl/node_id.hxx +40 -0
  18. data/ext/couchbase/core/impl/public_cluster.cxx +118 -5
  19. data/ext/couchbase/core/io/configuration_belongs_to_session.hxx +67 -0
  20. data/ext/couchbase/core/io/http_session_manager.hxx +97 -57
  21. data/ext/couchbase/core/io/mcbp_session.cxx +7 -16
  22. data/ext/couchbase/core/io/streams.cxx +143 -12
  23. data/ext/couchbase/core/io/streams.hxx +6 -0
  24. data/ext/couchbase/core/operations/document_get_all_replicas.hxx +14 -4
  25. data/ext/couchbase/core/operations/document_get_projected.cxx +3 -4
  26. data/ext/couchbase/core/operations/document_lookup_in_all_replicas.hxx +4 -0
  27. data/ext/couchbase/core/operations/document_query.cxx +12 -12
  28. data/ext/couchbase/core/operations/management/bucket_create.cxx +37 -38
  29. data/ext/couchbase/core/operations/management/bucket_update.cxx +26 -28
  30. data/ext/couchbase/core/operations/management/collection_create.cxx +11 -11
  31. data/ext/couchbase/core/operations/management/collection_drop.cxx +11 -11
  32. data/ext/couchbase/core/operations/management/collection_update.cxx +11 -11
  33. data/ext/couchbase/core/operations/management/error_utils.cxx +9 -6
  34. data/ext/couchbase/core/operations/management/query_index_create.cxx +5 -4
  35. data/ext/couchbase/core/operations/management/query_index_drop.cxx +7 -6
  36. data/ext/couchbase/core/operations/management/scope_create.cxx +12 -13
  37. data/ext/couchbase/core/operations/management/scope_drop.cxx +8 -9
  38. data/ext/couchbase/core/topology/configuration.cxx +21 -0
  39. data/ext/couchbase/core/topology/configuration.hxx +28 -0
  40. data/ext/couchbase/core/transactions/attempt_context_impl.cxx +254 -178
  41. data/ext/couchbase/core/transactions/attempt_context_impl.hxx +5 -1
  42. data/ext/couchbase/core/transactions/transactions_cleanup.cxx +42 -7
  43. data/ext/couchbase/core/transactions/waitable_op_list.hxx +13 -2
  44. data/ext/couchbase/core/utils/contains_string.cxx +61 -0
  45. data/ext/couchbase/core/utils/contains_string.hxx +26 -0
  46. data/ext/couchbase/couchbase/cluster.hxx +33 -0
  47. data/ext/couchbase/couchbase/collection.hxx +73 -0
  48. data/ext/couchbase/couchbase/error.hxx +16 -0
  49. data/ext/couchbase/couchbase/node_id.hxx +123 -0
  50. data/ext/couchbase/couchbase/node_id_for_options.hxx +61 -0
  51. data/ext/couchbase/couchbase/node_ids_options.hxx +62 -0
  52. data/ext/couchbase/couchbase/result.hxx +42 -0
  53. data/ext/rcb_crud.cxx +2 -0
  54. data/ext/rcb_logger.cxx +15 -17
  55. data/lib/couchbase/cluster.rb +16 -14
  56. data/lib/couchbase/datastructures/couchbase_list.rb +1 -0
  57. data/lib/couchbase/datastructures/couchbase_map.rb +1 -0
  58. data/lib/couchbase/datastructures/couchbase_queue.rb +1 -0
  59. data/lib/couchbase/datastructures/couchbase_set.rb +1 -0
  60. data/lib/couchbase/errors.rb +1 -1
  61. data/lib/couchbase/json_transcoder.rb +1 -1
  62. data/lib/couchbase/options.rb +9 -4
  63. data/lib/couchbase/protostellar/client.rb +0 -2
  64. data/lib/couchbase/protostellar/cluster.rb +4 -0
  65. data/lib/couchbase/protostellar/generated/admin/analytics/v1/analytics_pb.rb +54 -0
  66. data/lib/couchbase/protostellar/generated/admin/analytics/v1/analytics_services_pb.rb +51 -0
  67. data/lib/couchbase/protostellar/generated/admin/bucket/v1/bucket_pb.rb +5 -24
  68. data/lib/couchbase/protostellar/generated/admin/bucket/v1/bucket_services_pb.rb +16 -0
  69. data/lib/couchbase/protostellar/generated/admin/collection/v1/collection_pb.rb +5 -24
  70. data/lib/couchbase/protostellar/generated/admin/collection/v1/collection_services_pb.rb +16 -0
  71. data/lib/couchbase/protostellar/generated/admin/query/v1/query_pb.rb +5 -24
  72. data/lib/couchbase/protostellar/generated/admin/query/v1/query_services_pb.rb +18 -0
  73. data/lib/couchbase/protostellar/generated/admin/search/v1/search_pb.rb +2 -23
  74. data/lib/couchbase/protostellar/generated/admin/search/v1/search_services_pb.rb +23 -0
  75. data/lib/couchbase/protostellar/generated/analytics/v1/analytics_pb.rb +4 -25
  76. data/lib/couchbase/protostellar/generated/analytics/v1/analytics_services_pb.rb +10 -0
  77. data/lib/couchbase/protostellar/generated/internal/hooks/v1/hooks_pb.rb +6 -25
  78. data/lib/couchbase/protostellar/generated/internal/hooks/v1/hooks_services_pb.rb +18 -0
  79. data/lib/couchbase/protostellar/generated/internal/xdcr/v1/xdcr_pb.rb +46 -0
  80. data/lib/couchbase/protostellar/generated/internal/xdcr/v1/xdcr_services_pb.rb +56 -0
  81. data/lib/couchbase/protostellar/generated/kv/v1/kv_pb.rb +3 -26
  82. data/lib/couchbase/protostellar/generated/kv/v1/kv_services_pb.rb +47 -0
  83. data/lib/couchbase/protostellar/generated/query/v1/query_pb.rb +4 -26
  84. data/lib/couchbase/protostellar/generated/query/v1/query_services_pb.rb +10 -0
  85. data/lib/couchbase/protostellar/generated/routing/v2/routing_pb.rb +26 -0
  86. data/lib/couchbase/protostellar/generated/routing/v2/routing_services_pb.rb +43 -0
  87. data/lib/couchbase/protostellar/generated/search/v1/search_pb.rb +5 -26
  88. data/lib/couchbase/protostellar/generated/search/v1/search_services_pb.rb +11 -0
  89. data/lib/couchbase/protostellar/generated/transactions/v1/transactions_pb.rb +2 -23
  90. data/lib/couchbase/protostellar/generated/transactions/v1/transactions_services_pb.rb +30 -0
  91. data/lib/couchbase/protostellar/generated/view/v1/view_pb.rb +2 -23
  92. data/lib/couchbase/protostellar/generated/view/v1/view_services_pb.rb +9 -0
  93. data/lib/couchbase/protostellar/request_generator/admin/collection.rb +4 -2
  94. data/lib/couchbase/protostellar/request_generator/admin/query.rb +2 -0
  95. data/lib/couchbase/protostellar/request_generator/kv.rb +1 -1
  96. data/lib/couchbase/protostellar/scope.rb +4 -0
  97. data/lib/couchbase/utils/observability.rb +10 -4
  98. data/lib/couchbase/version.rb +1 -1
  99. metadata +20 -7
  100. data/lib/couchbase/protostellar/generated/routing/v1/routing_pb.rb +0 -52
  101. data/lib/couchbase/protostellar/generated/routing/v1/routing_services_pb.rb +0 -30
@@ -251,45 +251,45 @@ public:
251
251
  app_telemetry_meter_,
252
252
  options_.default_timeout_for(request.type));
253
253
  #endif
254
- cmd->start([start = std::chrono::steady_clock::now(),
255
- self = shared_from_this(),
256
- type,
257
- cmd,
258
- handler =
259
- collector->build_reporter()](operations::http_noop_response&& resp) {
260
- diag::ping_state state = diag::ping_state::ok;
261
- std::optional<std::string> error{};
262
- if (auto ec = resp.ctx.ec; ec) {
263
- if (ec == errc::common::unambiguous_timeout ||
264
- ec == errc::common::ambiguous_timeout) {
265
- state = diag::ping_state::timeout;
266
- } else {
267
- state = diag::ping_state::error;
254
+ cmd->start(
255
+ [start = std::chrono::steady_clock::now(),
256
+ self = shared_from_this(),
257
+ type,
258
+ cmd,
259
+ handler = collector->build_reporter()](operations::http_noop_response&& resp) {
260
+ diag::ping_state state = diag::ping_state::ok;
261
+ std::optional<std::string> error{};
262
+ if (auto ec = resp.ctx.ec; ec) {
263
+ if (ec == errc::common::unambiguous_timeout ||
264
+ ec == errc::common::ambiguous_timeout) {
265
+ state = diag::ping_state::timeout;
266
+ } else {
267
+ state = diag::ping_state::error;
268
+ }
269
+ error.emplace(fmt::format("code={}, message={}, http_code={}",
270
+ ec.value(),
271
+ ec.message(),
272
+ resp.ctx.http_status));
268
273
  }
269
- error.emplace(fmt::format("code={}, message={}, http_code={}",
270
- ec.value(),
271
- ec.message(),
272
- resp.ctx.http_status));
273
- }
274
- auto remote_address = cmd->session_->remote_address();
275
- // If not connected, the remote address will be empty. Better to
276
- // give the user some context on the "attempted" remote address.
277
- if (remote_address.empty()) {
278
- remote_address =
279
- fmt::format("{}:{}", cmd->session_->hostname(), cmd->session_->port());
280
- }
281
- handler->report(
282
- diag::endpoint_ping_info{ type,
283
- cmd->session_->id(),
284
- std::chrono::duration_cast<std::chrono::microseconds>(
285
- std::chrono::steady_clock::now() - start),
286
- remote_address,
287
- cmd->session_->local_address(),
288
- state,
289
- {},
290
- error });
291
- self->check_in(type, cmd->session_);
292
- });
274
+ auto remote_address = cmd->session_->remote_address();
275
+ // If not connected, the remote address will be empty. Better to
276
+ // give the user some context on the "attempted" remote address.
277
+ if (remote_address.empty()) {
278
+ remote_address =
279
+ fmt::format("{}:{}", cmd->session_->hostname(), cmd->session_->port());
280
+ }
281
+ handler->report(
282
+ diag::endpoint_ping_info{ type,
283
+ cmd->session_->id(),
284
+ std::chrono::duration_cast<std::chrono::microseconds>(
285
+ std::chrono::steady_clock::now() - start),
286
+ remote_address,
287
+ cmd->session_->local_address(),
288
+ state,
289
+ {},
290
+ error });
291
+ self->check_in(type, cmd->session_);
292
+ });
293
293
 
294
294
  cmd->set_command_session(session);
295
295
  if (!session->is_connected()) {
@@ -402,9 +402,20 @@ public:
402
402
  return;
403
403
  }
404
404
  if (!session->is_connected()) {
405
- CB_LOG_DEBUG("{} HTTP session never connected. Skipping check-in", session->log_prefix());
406
- return session.reset();
405
+ {
406
+ std::scoped_lock lock(sessions_mutex_);
407
+ if (auto pend_it = pending_sessions_.find(type); pend_it != pending_sessions_.end()) {
408
+ pend_it->second.remove_if([id = session->id()](const auto& s) {
409
+ return !s || s->id() == id;
410
+ });
411
+ }
412
+ }
413
+ CB_LOG_DEBUG("{} HTTP session never connected. Ensured session is not in pending.",
414
+ session->log_prefix());
415
+ return;
407
416
  }
417
+ bool should_stop = false;
418
+ std::chrono::milliseconds idle_timeout{};
408
419
  {
409
420
  std::scoped_lock lock(config_mutex_);
410
421
  if (!session->keep_alive() || !config_.has_node(options_.network,
@@ -412,22 +423,34 @@ public:
412
423
  options_.enable_tls,
413
424
  session->hostname(),
414
425
  session->port())) {
415
- return asio::post(session->get_executor(), [session]() {
416
- session->stop();
417
- });
426
+ should_stop = true;
427
+ } else {
428
+ idle_timeout = options_.idle_http_connection_timeout;
418
429
  }
419
430
  }
431
+ if (should_stop) {
432
+ return asio::post(session->get_executor(), [session]() {
433
+ session->stop();
434
+ });
435
+ }
420
436
  if (!session->is_stopped()) {
421
- session->set_idle(options_.idle_http_connection_timeout);
437
+ // set_idle() arms the idle timer via async_wait — it never calls stop() inline,
438
+ // so on_stop cannot re-enter sessions_mutex_ here. It must precede publication to
439
+ // idle_sessions_ so a concurrent check_out's reset_idle() finds a pending timer.
440
+ session->set_idle(idle_timeout);
422
441
  CB_LOG_DEBUG("{} put HTTP session back to idle connections", session->log_prefix());
423
442
  std::scoped_lock lock(sessions_mutex_);
424
443
  idle_sessions_[type].push_back(session);
425
- busy_sessions_[type].remove_if([id = session->id()](const auto& s) -> bool {
426
- return !s || s->id() == id;
427
- });
428
- pending_sessions_[type].remove_if([id = session->id()](const auto& s) -> bool {
429
- return !s || s->id() == id;
430
- });
444
+ if (auto busy_it = busy_sessions_.find(type); busy_it != busy_sessions_.end()) {
445
+ busy_it->second.remove_if([id = session->id()](const auto& s) -> bool {
446
+ return !s || s->id() == id;
447
+ });
448
+ }
449
+ if (auto pend_it = pending_sessions_.find(type); pend_it != pending_sessions_.end()) {
450
+ pend_it->second.remove_if([id = session->id()](const auto& s) -> bool {
451
+ return !s || s->id() == id;
452
+ });
453
+ }
431
454
  }
432
455
  }
433
456
 
@@ -741,13 +764,30 @@ private:
741
764
  }
742
765
 
743
766
  session->on_stop([type, id = session->id(), self = this->shared_from_this()]() {
744
- const std::scoped_lock inner_lock(self->sessions_mutex_);
745
- self->busy_sessions_[type].remove_if([&id](const auto& s) {
746
- return !s || s->id() == id;
747
- });
748
- self->idle_sessions_[type].remove_if([&id](const auto& s) {
749
- return !s || s->id() == id;
750
- });
767
+ // Declared outside the lock so destructors run after sessions_mutex_ is released.
768
+ // cppcheck-suppress variableScope
769
+ std::vector<std::shared_ptr<http_session>> dropped;
770
+ {
771
+ const std::scoped_lock inner_lock(self->sessions_mutex_);
772
+ for (auto* map :
773
+ { &self->busy_sessions_, &self->idle_sessions_, &self->pending_sessions_ }) {
774
+ auto map_it = map->find(type);
775
+ if (map_it == map->end()) {
776
+ continue;
777
+ }
778
+ auto& list = map_it->second;
779
+ for (auto it = list.begin(); it != list.end();) {
780
+ if (!*it || (*it)->id() == id) {
781
+ if (*it) {
782
+ dropped.push_back(std::move(*it));
783
+ }
784
+ it = list.erase(it);
785
+ } else {
786
+ ++it;
787
+ }
788
+ }
789
+ }
790
+ }
751
791
  });
752
792
  return session;
753
793
  }
@@ -22,6 +22,7 @@
22
22
  #ifdef COUCHBASE_CXX_CLIENT_COLUMNAR
23
23
  #include "core/columnar/background_bootstrap_listener.hxx"
24
24
  #endif
25
+ #include "configuration_belongs_to_session.hxx"
25
26
  #include "core/config_listener.hxx"
26
27
  #include "core/diagnostics.hxx"
27
28
  #include "core/impl/bootstrap_error.hxx"
@@ -634,14 +635,9 @@ class mcbp_session_impl
634
635
  }
635
636
  }
636
637
  std::optional<topology::configuration> config = req.body().config();
637
- if (session_ && config.has_value()) {
638
- if ((!config->bucket.has_value() && req.body().bucket().empty()) ||
639
- (session_->bucket_name_.has_value() && !req.body().bucket().empty() &&
640
- // TODO(CXXCBC-549)
641
- // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
642
- session_->bucket_name_.value() == req.body().bucket())) {
643
- session_->update_configuration(std::move(config.value()));
644
- }
638
+ if (session_ && config.has_value() &&
639
+ configuration_belongs_to_session(config->bucket, session_->bucket_name_)) {
640
+ session_->update_configuration(std::move(config.value()));
645
641
  }
646
642
  } break;
647
643
  default:
@@ -824,14 +820,9 @@ class mcbp_session_impl
824
820
  }
825
821
  }
826
822
  std::optional<topology::configuration> config = req.body().config();
827
- if (session_ && config.has_value()) {
828
- if ((!config->bucket.has_value() && req.body().bucket().empty()) ||
829
- (session_->bucket_name_.has_value() && !req.body().bucket().empty() &&
830
- // TODO(CXXCBC-549)
831
- // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
832
- session_->bucket_name_.value() == req.body().bucket())) {
833
- session_->update_configuration(std::move(config.value()));
834
- }
823
+ if (session_ && config.has_value() &&
824
+ configuration_belongs_to_session(config->bucket, session_->bucket_name_)) {
825
+ session_->update_configuration(std::move(config.value()));
835
826
  }
836
827
  } break;
837
828
  default:
@@ -23,12 +23,128 @@
23
23
  #include <asio/error.hpp>
24
24
  #include <asio/ssl.hpp>
25
25
 
26
+ #include <cerrno>
27
+
28
+ #if defined(_WIN32)
29
+ #include <process.h>
30
+ #else
31
+ #include <unistd.h>
32
+ #endif
33
+
26
34
  namespace couchbase::core::io
27
35
  {
36
+ namespace
37
+ {
38
+ auto
39
+ current_process_id() -> long long
40
+ {
41
+ #if defined(_WIN32)
42
+ return static_cast<long long>(::_getpid());
43
+ #else
44
+ return static_cast<long long>(::getpid());
45
+ #endif
46
+ }
47
+
48
+ /**
49
+ * Release a socket this process inherited across fork(2), without disturbing the
50
+ * connection the parent is still using.
51
+ *
52
+ * Two things differ from the ordinary close path, and both matter:
53
+ *
54
+ * * No shutdown(2). It acts on the open file description, which fork(2) shares,
55
+ * so it would send FIN and break the parent's connection. close(2) drops only
56
+ * our own descriptor; the peer sees nothing.
57
+ * * release() before closing, rather than asio's close(). asio's close() skips
58
+ * EPOLL_CTL_DEL on the assumption that closing a descriptor removes its epoll
59
+ * registration. Per epoll(7) that happens only once *every* descriptor
60
+ * referring to the description is closed -- and the parent still holds one --
61
+ * so the registration would outlive the descriptor_state asio recycles, which
62
+ * is precisely the stale-pointer shape the reactor faults on. release() emits
63
+ * the EPOLL_CTL_DEL explicitly and hands back the raw descriptor to close.
64
+ *
65
+ * release() also completes any outstanding asynchronous operations on this socket
66
+ * with operation_aborted, which is what we want: those reads belong to the parent.
67
+ */
68
+ // Takes the basic_socket base rather than tcp::socket so the same code serves both
69
+ // the plain socket and a TLS stream's lowest_layer(), whose type is that base.
70
+ void
71
+ detach_inherited_socket(asio::basic_socket<asio::ip::tcp>& socket, asio::error_code& ec)
72
+ {
73
+ if (!socket.is_open()) {
74
+ ec = asio::error::bad_descriptor;
75
+ return;
76
+ }
77
+
78
+ const auto handle = socket.release(ec);
79
+ if (ec) {
80
+ // Report it. Do not reach for asio's close() here: that is the very call
81
+ // whose epoll behaviour makes it unsafe on an inherited descriptor, and the
82
+ // descriptor goes away when this process exits anyway.
83
+ return;
84
+ }
85
+
86
+ #if defined(_WIN32)
87
+ // Unreachable: without fork(2) no socket is ever inherited, so the owner pid
88
+ // always matches and close() never takes this path.
89
+ (void)handle;
90
+ ec = asio::error::operation_not_supported;
91
+ #else
92
+ if (::close(handle) != 0) {
93
+ ec.assign(errno, asio::error::get_system_category());
94
+ }
95
+ #endif
96
+ }
97
+ } // namespace
98
+
99
+ auto
100
+ configure_tls_handshake(asio::ssl::stream<asio::ip::tcp::socket>& stream,
101
+ const std::string& hostname) -> std::error_code
102
+ {
103
+ if (hostname.empty()) {
104
+ // Fail closed: a TLS client connection must have a hostname to verify the
105
+ // server's identity against. Returning success here would silently fall back
106
+ // to CA-only validation -- the CWE-297 problem this function exists to prevent.
107
+ return asio::error::make_error_code(asio::error::invalid_argument);
108
+ }
109
+
110
+ // Send SNI so that name-based / multi-tenant TLS endpoints present the correct
111
+ // certificate for this hostname.
112
+ {
113
+ // SSL_set_tlsext_host_name() is a macro that expands to an internal C-style
114
+ // cast to void*, which trips -Wold-style-cast; suppress it locally.
115
+ #if defined(__GNUC__) || defined(__clang__)
116
+ #pragma GCC diagnostic push
117
+ #pragma GCC diagnostic ignored "-Wold-style-cast"
118
+ #endif
119
+ ERR_clear_error();
120
+ const auto sni_result = SSL_set_tlsext_host_name(stream.native_handle(), hostname.c_str());
121
+ #if defined(__GNUC__) || defined(__clang__)
122
+ #pragma GCC diagnostic pop
123
+ #endif
124
+ if (sni_result != 1) {
125
+ // Prefer the underlying SSL reason (if any) so failures are diagnosable,
126
+ // and fall back to a generic error when the SSL error queue is empty.
127
+ if (const auto reason = ERR_get_error(); reason != 0) {
128
+ return { static_cast<int>(reason), asio::error::get_ssl_category() };
129
+ }
130
+ return asio::error::make_error_code(asio::error::invalid_argument);
131
+ }
132
+ }
133
+
134
+ // Verify that the presented certificate actually identifies `hostname`
135
+ // (SAN/CN). asio::ssl::verify_peer on its own only validates the certificate
136
+ // chain, not the name. In verify_none mode OpenSSL ignores the callback
137
+ // result, so this has no effect there.
138
+ std::error_code ec{};
139
+ stream.set_verify_callback(asio::ssl::host_name_verification(hostname), ec);
140
+ return ec;
141
+ }
142
+
28
143
  stream_impl::stream_impl(asio::io_context& ctx, bool is_tls)
29
144
  : strand_(asio::make_strand(ctx))
30
145
  , tls_(is_tls)
31
146
  , id_(uuid::to_string(uuid::random()))
147
+ , owner_pid_(current_process_id())
32
148
  {
33
149
  }
34
150
 
@@ -79,12 +195,20 @@ plain_stream_impl::close(utils::movable_function<void(std::error_code)>&& handle
79
195
  if (!stream_) {
80
196
  return handler(asio::error::bad_descriptor);
81
197
  }
82
- return asio::post(strand_, [stream = std::move(stream_), handler = std::move(handler)]() {
83
- asio::error_code ec{};
84
- stream->shutdown(asio::socket_base::shutdown_both, ec);
85
- stream->close(ec);
86
- handler(ec);
87
- });
198
+ return asio::post(
199
+ strand_, [stream = std::move(stream_), owner_pid = owner_pid_, handler = std::move(handler)]() {
200
+ asio::error_code ec{};
201
+ // Decide here rather than when this was posted: io_.stop() at fork_prepare
202
+ // does not drain the queue, so a close queued before the fork runs in the
203
+ // child once the io_context is restarted, and there the answer differs.
204
+ if (owner_pid != current_process_id()) {
205
+ detach_inherited_socket(*stream, ec);
206
+ } else {
207
+ stream->shutdown(asio::socket_base::shutdown_both, ec);
208
+ stream->close(ec);
209
+ }
210
+ handler(ec);
211
+ });
88
212
  }
89
213
 
90
214
  void
@@ -178,12 +302,19 @@ tls_stream_impl::close(utils::movable_function<void(std::error_code)>&& handler)
178
302
  if (!stream_) {
179
303
  return handler(asio::error::bad_descriptor);
180
304
  }
181
- return asio::post(strand_, [stream = std::move(stream_), handler = std::move(handler)]() {
182
- asio::error_code ec{};
183
- stream->lowest_layer().shutdown(asio::socket_base::shutdown_both, ec);
184
- stream->lowest_layer().close(ec);
185
- handler(ec);
186
- });
305
+ return asio::post(
306
+ strand_, [stream = std::move(stream_), owner_pid = owner_pid_, handler = std::move(handler)]() {
307
+ asio::error_code ec{};
308
+ // See plain_stream_impl::close(): the inheritance test belongs here, not at
309
+ // post time.
310
+ if (owner_pid != current_process_id()) {
311
+ detach_inherited_socket(stream->lowest_layer(), ec);
312
+ } else {
313
+ stream->lowest_layer().shutdown(asio::socket_base::shutdown_both, ec);
314
+ stream->lowest_layer().close(ec);
315
+ }
316
+ handler(ec);
317
+ });
187
318
  }
188
319
 
189
320
  void
@@ -65,6 +65,12 @@ protected:
65
65
  asio::strand<asio::io_context::executor_type> strand_;
66
66
  bool tls_;
67
67
  std::string id_{};
68
+ // Identifies the process that opened this socket. close() compares it against
69
+ // the process running at that moment to tell a socket inherited across fork(2)
70
+ // apart from one this process opened itself, because an inherited socket must not
71
+ // be shut down: shutdown(2) acts on the open file description, which fork(2)
72
+ // shares, so it would tear down a connection the parent is still using.
73
+ long long owner_pid_;
68
74
 
69
75
  public:
70
76
  stream_impl(asio::io_context& ctx, bool is_tls);
@@ -18,6 +18,7 @@
18
18
  #pragma once
19
19
 
20
20
  #include <couchbase/error_codes.hxx>
21
+ #include <couchbase/node_id.hxx>
21
22
 
22
23
  #include "core/error_context/key_value.hxx"
23
24
  #include "core/impl/get_replica.hxx"
@@ -40,6 +41,7 @@ struct get_all_replicas_response {
40
41
  couchbase::cas cas{};
41
42
  std::uint32_t flags{};
42
43
  bool replica{ true };
44
+ couchbase::node_id dispatched_to_node_id{};
43
45
  };
44
46
  key_value_error_context ctx{};
45
47
  std::vector<entry> entries{};
@@ -156,8 +158,12 @@ struct get_all_replicas_request {
156
158
  return;
157
159
  }
158
160
  } else {
159
- ctx->result_.emplace_back(get_all_replicas_response::entry{
160
- std::move(resp.value), resp.cas, resp.flags, true /* replica */ });
161
+ ctx->result_.emplace_back(
162
+ get_all_replicas_response::entry{ std::move(resp.value),
163
+ resp.cas,
164
+ resp.flags,
165
+ true /* replica */,
166
+ resp.ctx.last_dispatched_to_node_id() });
161
167
  }
162
168
  if (ctx->expected_responses_ == 0) {
163
169
  ctx->done_ = true;
@@ -203,8 +209,12 @@ struct get_all_replicas_request {
203
209
  return;
204
210
  }
205
211
  } else {
206
- ctx->result_.emplace_back(get_all_replicas_response::entry{
207
- std::move(resp.value), resp.cas, resp.flags, false /* active */ });
212
+ ctx->result_.emplace_back(
213
+ get_all_replicas_response::entry{ std::move(resp.value),
214
+ resp.cas,
215
+ resp.flags,
216
+ false /* active */,
217
+ resp.ctx.last_dispatched_to_node_id() });
208
218
  }
209
219
  if (ctx->expected_responses_ == 0) {
210
220
  ctx->done_ = true;
@@ -210,14 +210,13 @@ get_projected_request::make_response(key_value_error_context&& ctx,
210
210
  response.ctx.override_ec(errc::common::parsing_failure);
211
211
  return response;
212
212
  }
213
- tao::json::value new_doc;
213
+ tao::json::value new_doc = tao::json::empty_object;
214
214
  for (const auto& projection : projections) {
215
215
  if (auto value_to_apply = subdoc_lookup(full_doc, projection)) {
216
216
  subdoc_apply_projection(new_doc, projection, *value_to_apply, preserve_array_indexes);
217
- } else {
218
- response.ctx.override_ec(errc::key_value::path_not_found);
219
- return response;
220
217
  }
218
+ // We ignore paths that were not found, similar to how we ignore them in the subdoc
219
+ // multi-lookup below.
221
220
  }
222
221
  response.value = utils::json::generate_binary(new_doc);
223
222
  }
@@ -28,6 +28,7 @@
28
28
 
29
29
  #include <couchbase/codec/encoded_value.hxx>
30
30
  #include <couchbase/error_codes.hxx>
31
+ #include <couchbase/node_id.hxx>
31
32
 
32
33
  #include <functional>
33
34
  #include <memory>
@@ -50,6 +51,7 @@ struct lookup_in_all_replicas_response {
50
51
  couchbase::cas cas{};
51
52
  bool deleted{ false };
52
53
  bool is_replica{ true };
54
+ couchbase::node_id dispatched_to_node_id{};
53
55
  };
54
56
  subdocument_error_context ctx{};
55
57
  std::vector<entry> entries{};
@@ -203,6 +205,7 @@ struct lookup_in_all_replicas_request {
203
205
  top_entry.cas = resp.cas;
204
206
  top_entry.deleted = resp.deleted;
205
207
  top_entry.is_replica = true;
208
+ top_entry.dispatched_to_node_id = resp.ctx.last_dispatched_to_node_id();
206
209
  for (auto& field : resp.fields) {
207
210
  lookup_in_all_replicas_response::entry::lookup_in_entry lookup_in_entry{};
208
211
  lookup_in_entry.path = field.path;
@@ -267,6 +270,7 @@ struct lookup_in_all_replicas_request {
267
270
  top_entry.cas = resp.cas;
268
271
  top_entry.deleted = resp.deleted;
269
272
  top_entry.is_replica = false;
273
+ top_entry.dispatched_to_node_id = resp.ctx.last_dispatched_to_node_id();
270
274
  for (auto& field : resp.fields) {
271
275
  lookup_in_all_replicas_response::entry::lookup_in_entry lookup_in_entry{};
272
276
  lookup_in_entry.path = field.path;
@@ -20,6 +20,7 @@
20
20
  #include "core/cluster_options.hxx"
21
21
  #include "core/logger/logger.hxx"
22
22
  #include "core/operations/management/error_utils.hxx"
23
+ #include "core/utils/contains_string.hxx"
23
24
  #include "core/utils/duration_parser.hxx"
24
25
  #include "core/utils/json.hxx"
25
26
 
@@ -28,13 +29,11 @@
28
29
  #include <gsl/assert>
29
30
  #include <tao/json/value.hpp>
30
31
 
31
- #include <regex>
32
-
33
32
  namespace couchbase::core::operations
34
33
  {
35
34
  auto
36
- query_request::encode_to(query_request::encoded_request_type& encoded,
37
- http_context& context) -> std::error_code
35
+ query_request::encode_to(query_request::encoded_request_type& encoded, http_context& context)
36
+ -> std::error_code
38
37
  {
39
38
  ctx_.emplace(context);
40
39
  tao::json::value body{
@@ -209,8 +208,8 @@ query_request::encode_to(query_request::encoded_request_type& encoded,
209
208
  }
210
209
 
211
210
  auto
212
- query_request::make_response(error_context::query&& ctx,
213
- const encoded_response_type& encoded) -> query_response
211
+ query_request::make_response(error_context::query&& ctx, const encoded_response_type& encoded)
212
+ -> query_response
214
213
  {
215
214
  query_response response{ std::move(ctx) };
216
215
  response.ctx.statement = statement;
@@ -374,13 +373,14 @@ query_request::make_response(error_context::query&& ctx,
374
373
  response.ctx.ec = errc::common::index_exists;
375
374
  break;
376
375
  case 5000: /* IKey: "Internal Error" */
377
- if (std::regex_search(response.ctx.first_error_message,
378
- std::regex{ ".*[iI]ndex .*already exist.*" })) {
376
+ if (utils::contains_string(response.ctx.first_error_message, "index", true) &&
377
+ utils::contains_string(response.ctx.first_error_message, "already exist", true)) {
379
378
  response.ctx.ec = errc::common::index_exists;
380
- } else if (response.ctx.first_error_message.find("Index does not exist") !=
381
- std::string::npos ||
382
- std::regex_search(response.ctx.first_error_message,
383
- std::regex{ ".*[iI]ndex .*[nN]ot [fF]ound.*" })) {
379
+ } else if (utils::contains_string(response.ctx.first_error_message,
380
+ "Index does not exist") ||
381
+ (utils::contains_string(response.ctx.first_error_message, "index", true) &&
382
+ utils::contains_string(
383
+ response.ctx.first_error_message, "not found", true))) {
384
384
  response.ctx.ec = errc::common::index_not_found;
385
385
  } else if (response.ctx.first_error_message.find("Bucket Not Found") !=
386
386
  std::string::npos) {