couchbase 3.5.7 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/ext/cache/extconf_include.rb +3 -3
  4. data/ext/cache/mozilla-ca-bundle.crt +3 -165
  5. data/ext/cache/mozilla-ca-bundle.sha256 +1 -1
  6. data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/CMakeLists.txt +14 -10
  7. data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy.cc +7 -4
  8. data/ext/couchbase/CMakeLists.txt +12 -1
  9. data/ext/couchbase/cmake/Profiler.cmake +15 -0
  10. data/ext/couchbase/cmake/ThirdPartyDependencies.cmake +2 -2
  11. data/ext/couchbase/cmake/couchbase_cxx_client.pc.in +1 -1
  12. data/ext/couchbase/core/app_telemetry_address.cxx +55 -0
  13. data/ext/couchbase/core/app_telemetry_address.hxx +39 -0
  14. data/ext/couchbase/core/app_telemetry_meter.cxx +753 -0
  15. data/ext/couchbase/core/app_telemetry_meter.hxx +198 -0
  16. data/ext/couchbase/core/app_telemetry_reporter.cxx +895 -0
  17. data/ext/couchbase/core/app_telemetry_reporter.hxx +59 -0
  18. data/ext/couchbase/core/bucket.cxx +77 -35
  19. data/ext/couchbase/core/bucket.hxx +17 -10
  20. data/ext/couchbase/core/cluster.cxx +54 -16
  21. data/ext/couchbase/core/cluster_credentials.cxx +27 -0
  22. data/ext/couchbase/core/cluster_credentials.hxx +36 -0
  23. data/ext/couchbase/core/cluster_options.hxx +12 -0
  24. data/ext/couchbase/core/collections_component.cxx +7 -5
  25. data/ext/couchbase/core/http_component.cxx +6 -0
  26. data/ext/couchbase/core/impl/binary_collection.cxx +4 -0
  27. data/ext/couchbase/core/impl/bucket_manager.cxx +2 -0
  28. data/ext/couchbase/core/impl/cluster.cxx +9 -0
  29. data/ext/couchbase/core/impl/collection.cxx +2 -0
  30. data/ext/couchbase/core/impl/error.cxx +1 -0
  31. data/ext/couchbase/core/impl/logger.cxx +51 -0
  32. data/ext/couchbase/core/impl/replica_utils.cxx +1 -1
  33. data/ext/couchbase/core/impl/transaction_get_multi_replicas_from_preferred_server_group_spec.cxx +32 -0
  34. data/ext/couchbase/core/impl/transaction_get_multi_spec.cxx +30 -0
  35. data/ext/couchbase/core/impl/transaction_op_error_category.cxx +2 -0
  36. data/ext/couchbase/core/io/config_tracker.cxx +6 -6
  37. data/ext/couchbase/core/io/http_command.hxx +35 -11
  38. data/ext/couchbase/core/io/http_session.cxx +10 -0
  39. data/ext/couchbase/core/io/http_session.hxx +4 -0
  40. data/ext/couchbase/core/io/http_session_manager.hxx +83 -34
  41. data/ext/couchbase/core/io/mcbp_command.hxx +41 -2
  42. data/ext/couchbase/core/io/mcbp_session.cxx +52 -19
  43. data/ext/couchbase/core/io/mcbp_session.hxx +3 -0
  44. data/ext/couchbase/core/logger/logger.cxx +46 -0
  45. data/ext/couchbase/core/logger/logger.hxx +41 -1
  46. data/ext/couchbase/core/management/bucket_settings.hxx +1 -0
  47. data/ext/couchbase/core/management/bucket_settings_json.hxx +4 -0
  48. data/ext/couchbase/core/meta/features.hxx +32 -0
  49. data/ext/couchbase/core/operations/document_analytics.cxx +9 -9
  50. data/ext/couchbase/core/operations/document_append.cxx +1 -0
  51. data/ext/couchbase/core/operations/document_append.hxx +1 -0
  52. data/ext/couchbase/core/operations/document_get_all_replicas.hxx +10 -2
  53. data/ext/couchbase/core/operations/document_lookup_in.cxx +4 -0
  54. data/ext/couchbase/core/operations/document_lookup_in_all_replicas.hxx +14 -2
  55. data/ext/couchbase/core/operations/document_lookup_in_any_replica.hxx +4 -0
  56. data/ext/couchbase/core/operations/document_mutate_in.cxx +4 -0
  57. data/ext/couchbase/core/operations/document_mutate_in.hxx +1 -0
  58. data/ext/couchbase/core/operations/document_prepend.cxx +1 -0
  59. data/ext/couchbase/core/operations/document_prepend.hxx +1 -0
  60. data/ext/couchbase/core/operations/document_query.cxx +12 -10
  61. data/ext/couchbase/core/operations/http_noop.cxx +1 -0
  62. data/ext/couchbase/core/operations/management/bucket_create.cxx +3 -0
  63. data/ext/couchbase/core/operations/management/bucket_update.cxx +3 -0
  64. data/ext/couchbase/core/origin.cxx +0 -5
  65. data/ext/couchbase/core/origin.hxx +2 -11
  66. data/ext/couchbase/core/platform/random.cc +6 -3
  67. data/ext/couchbase/core/platform/random.h +2 -2
  68. data/ext/couchbase/core/protocol/cmd_mutate_in.hxx +9 -0
  69. data/ext/couchbase/core/timeout_defaults.hxx +4 -0
  70. data/ext/couchbase/core/topology/configuration.cxx +10 -13
  71. data/ext/couchbase/core/topology/configuration.hxx +14 -15
  72. data/ext/couchbase/core/topology/configuration_json.hxx +6 -0
  73. data/ext/couchbase/core/transactions/async_attempt_context.hxx +22 -2
  74. data/ext/couchbase/core/transactions/attempt_context.hxx +25 -7
  75. data/ext/couchbase/core/transactions/attempt_context_impl.cxx +688 -238
  76. data/ext/couchbase/core/transactions/attempt_context_impl.hxx +91 -12
  77. data/ext/couchbase/core/transactions/exceptions.cxx +5 -0
  78. data/ext/couchbase/core/transactions/exceptions.hxx +20 -0
  79. data/ext/couchbase/core/transactions/exceptions_fmt.hxx +3 -0
  80. data/ext/couchbase/core/transactions/forward_compat.cxx +71 -6
  81. data/ext/couchbase/core/transactions/forward_compat.hxx +45 -59
  82. data/ext/couchbase/core/transactions/get_multi_orchestrator.cxx +616 -0
  83. data/ext/couchbase/core/transactions/get_multi_orchestrator.hxx +61 -0
  84. data/ext/couchbase/core/transactions/internal/doc_record.cxx +8 -0
  85. data/ext/couchbase/core/transactions/internal/doc_record.hxx +16 -5
  86. data/ext/couchbase/core/transactions/internal/exceptions_internal.hxx +12 -0
  87. data/ext/couchbase/core/transactions/internal/transaction_context.hxx +13 -0
  88. data/ext/couchbase/core/transactions/internal/transaction_fields.hxx +1 -0
  89. data/ext/couchbase/core/transactions/staged_mutation.cxx +277 -96
  90. data/ext/couchbase/core/transactions/staged_mutation.hxx +28 -76
  91. data/ext/couchbase/core/transactions/transaction_context.cxx +33 -0
  92. data/ext/couchbase/core/transactions/transaction_get_multi_mode.hxx +28 -0
  93. data/ext/couchbase/core/transactions/transaction_get_multi_replicas_from_preferred_server_group_mode.hxx +27 -0
  94. data/ext/couchbase/core/transactions/transaction_get_multi_replicas_from_preferred_server_group_result.hxx +71 -0
  95. data/ext/couchbase/core/transactions/transaction_get_multi_result.hxx +66 -0
  96. data/ext/couchbase/core/transactions/transaction_links.hxx +10 -0
  97. data/ext/couchbase/core/transactions/transactions.cxx +8 -3
  98. data/ext/couchbase/core/utils/connection_string.cxx +4 -0
  99. data/ext/couchbase/core/utils/url_codec.cxx +26 -0
  100. data/ext/couchbase/core/utils/url_codec.hxx +11 -0
  101. data/ext/couchbase/core/websocket_codec.cxx +647 -0
  102. data/ext/couchbase/core/websocket_codec.hxx +77 -0
  103. data/ext/couchbase/couchbase/analytics_options.hxx +70 -6
  104. data/ext/couchbase/couchbase/application_telemetry_options.hxx +124 -0
  105. data/ext/couchbase/couchbase/cluster_options.hxx +17 -0
  106. data/ext/couchbase/couchbase/error_codes.hxx +1 -0
  107. data/ext/couchbase/couchbase/logger.hxx +16 -0
  108. data/ext/couchbase/couchbase/management/bucket_settings.hxx +1 -0
  109. data/ext/couchbase/couchbase/query_options.hxx +70 -6
  110. data/ext/couchbase/couchbase/transactions/async_attempt_context.hxx +29 -5
  111. data/ext/couchbase/couchbase/transactions/attempt_context.hxx +24 -7
  112. data/ext/couchbase/couchbase/transactions/transaction_get_multi_mode.hxx +47 -0
  113. data/ext/couchbase/couchbase/transactions/transaction_get_multi_options.hxx +44 -0
  114. data/ext/couchbase/couchbase/transactions/transaction_get_multi_replicas_from_preferred_server_group_mode.hxx +46 -0
  115. data/ext/couchbase/couchbase/transactions/transaction_get_multi_replicas_from_preferred_server_group_options.hxx +48 -0
  116. data/ext/couchbase/couchbase/transactions/transaction_get_multi_replicas_from_preferred_server_group_result.hxx +109 -0
  117. data/ext/couchbase/couchbase/transactions/transaction_get_multi_replicas_from_preferred_server_group_spec.hxx +47 -0
  118. data/ext/couchbase/couchbase/transactions/transaction_get_multi_result.hxx +102 -0
  119. data/ext/couchbase/couchbase/transactions/transaction_get_multi_spec.hxx +45 -0
  120. data/ext/rcb_buckets.cxx +26 -0
  121. data/lib/active_support/cache/couchbase_store.rb +1 -1
  122. data/lib/couchbase/cluster.rb +1 -1
  123. data/lib/couchbase/collection.rb +1 -1
  124. data/lib/couchbase/collection_options.rb +2 -2
  125. data/lib/couchbase/management/analytics_index_manager.rb +4 -4
  126. data/lib/couchbase/management/bucket_manager.rb +8 -2
  127. data/lib/couchbase/protostellar/cluster.rb +2 -2
  128. data/lib/couchbase/protostellar/collection.rb +1 -1
  129. data/lib/couchbase/protostellar/management/collection_query_index_manager.rb +1 -1
  130. data/lib/couchbase/protostellar/request_generator/admin/bucket.rb +4 -4
  131. data/lib/couchbase/protostellar/request_generator/admin/collection.rb +6 -6
  132. data/lib/couchbase/protostellar/request_generator/admin/query.rb +13 -13
  133. data/lib/couchbase/protostellar/request_generator/kv.rb +25 -25
  134. data/lib/couchbase/protostellar/request_generator/query.rb +4 -4
  135. data/lib/couchbase/protostellar/request_generator/search.rb +25 -25
  136. data/lib/couchbase/protostellar/response_converter/search.rb +1 -1
  137. data/lib/couchbase/protostellar/retry/reason.rb +1 -1
  138. data/lib/couchbase/protostellar/timeouts.rb +1 -1
  139. data/lib/couchbase/scope.rb +1 -1
  140. data/lib/couchbase/transcoder_flags.rb +1 -1
  141. data/lib/couchbase/utils/stdlib_logger_adapter.rb +1 -1
  142. data/lib/couchbase/version.rb +1 -1
  143. metadata +47 -19
  144. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/COPYING +0 -0
  145. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/cmake/SnappyConfig.cmake.in +0 -0
  146. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/cmake/config.h.in +0 -0
  147. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-c.cc +0 -0
  148. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-c.h +0 -0
  149. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-internal.h +0 -0
  150. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-sinksource.cc +0 -0
  151. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-sinksource.h +0 -0
  152. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-stubs-internal.cc +0 -0
  153. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-stubs-internal.h +0 -0
  154. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy-stubs-public.h.in +0 -0
  155. /data/ext/cache/snappy/{585305c8dbb8f762f2c2e17f937f1cf3ac6cbc9c → 3cde171792b3607f75c14e5011eaf69da4857bd8}/snappy/snappy.h +0 -0
@@ -0,0 +1,895 @@
1
+ /* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2
+ /*
3
+ * Copyright 2024-Present Couchbase, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
6
+ * except in compliance with the License. You may obtain a copy of the License at
7
+ *
8
+ * https://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software distributed under
11
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12
+ * ANY KIND, either express or implied. See the License for the specific language governing
13
+ * permissions and limitations under the License.
14
+ */
15
+
16
+ #include "app_telemetry_reporter.hxx"
17
+
18
+ #include "app_telemetry_address.hxx"
19
+ #include "app_telemetry_meter.hxx"
20
+ #include "cluster_credentials.hxx"
21
+ #include "cluster_options.hxx"
22
+ #include "io/streams.hxx"
23
+ #include "logger/logger.hxx"
24
+ #include "platform/base64.h"
25
+ #include "utils/url_codec.hxx"
26
+ #include "websocket_codec.hxx"
27
+
28
+ #include <couchbase/error_codes.hxx>
29
+
30
+ #include <asio/io_context.hpp>
31
+ #include <asio/ip/tcp.hpp>
32
+ #include <asio/ssl/context.hpp>
33
+ #include <asio/steady_timer.hpp>
34
+ #include <llhttp.h>
35
+ #include <spdlog/fmt/bin_to_hex.h>
36
+ #include <spdlog/fmt/bundled/chrono.h>
37
+ #include <spdlog/fmt/bundled/core.h>
38
+ #include <tao/json/to_string.hpp>
39
+ #include <tao/json/value.hpp>
40
+
41
+ #include <chrono>
42
+ #include <cstdint>
43
+ #include <memory>
44
+ #include <mutex>
45
+ #include <queue>
46
+ #include <random>
47
+ #include <utility>
48
+
49
+ namespace couchbase::core
50
+ {
51
+
52
+ namespace
53
+ {
54
+ enum class connection_state : std::uint8_t {
55
+ disconnected,
56
+ connecting,
57
+ connected,
58
+ stopped,
59
+ };
60
+
61
+ class connection_state_listener
62
+ {
63
+ public:
64
+ connection_state_listener() = default;
65
+ connection_state_listener(const connection_state_listener&) = default;
66
+ connection_state_listener(connection_state_listener&&) = default;
67
+ auto operator=(connection_state_listener&&) -> connection_state_listener& = default;
68
+ auto operator=(const connection_state_listener&) -> connection_state_listener& = default;
69
+ virtual ~connection_state_listener() = default;
70
+
71
+ virtual void on_connection_pending(const app_telemetry_address& address) = 0;
72
+ virtual void on_connected(const app_telemetry_address& address,
73
+ std::unique_ptr<io::stream_impl>&& stream) = 0;
74
+ virtual void on_websocket_ready() = 0;
75
+ virtual void on_error(const app_telemetry_address& address,
76
+ std::error_code ec,
77
+ const std::string& message) = 0;
78
+ };
79
+
80
+ class telemetry_dialer : public std::enable_shared_from_this<telemetry_dialer>
81
+ {
82
+ public:
83
+ static auto dial(app_telemetry_address address,
84
+ cluster_options options,
85
+ asio::io_context& ctx,
86
+ asio::ssl::context& tls,
87
+ std::shared_ptr<connection_state_listener>&& handler)
88
+ -> std::shared_ptr<telemetry_dialer>
89
+ {
90
+ auto dialer = std::make_shared<telemetry_dialer>(
91
+ std::move(address), std::move(options), ctx, tls, std::move(handler));
92
+ dialer->resolve_address();
93
+ return dialer;
94
+ }
95
+
96
+ telemetry_dialer(app_telemetry_address address,
97
+ cluster_options options,
98
+ asio::io_context& ctx,
99
+ asio::ssl::context& tls,
100
+ std::shared_ptr<connection_state_listener>&& handler)
101
+ : address_{ std::move(address) }
102
+ , options_{ std::move(options) }
103
+ , ctx_(ctx)
104
+ , tls_(tls)
105
+ , resolve_deadline_(ctx_)
106
+ , connect_deadline_(ctx_)
107
+ , resolver_(ctx_)
108
+ , handler_{ std::move(handler) }
109
+ {
110
+ }
111
+
112
+ void stop()
113
+ {
114
+ resolver_.cancel();
115
+ connect_deadline_.cancel();
116
+ resolve_deadline_.cancel();
117
+ if (stream_) {
118
+ stream_->close([](auto /* ec */) {
119
+ });
120
+ }
121
+ complete_with_error(asio::error::operation_aborted, "stop dialer");
122
+ }
123
+
124
+ private:
125
+ void complete_with_error(std::error_code ec, const std::string& message)
126
+ {
127
+ connect_deadline_.cancel();
128
+ resolve_deadline_.cancel();
129
+ if (auto handler = std::move(handler_); handler) {
130
+ handler->on_error(address_, ec, message);
131
+ }
132
+ }
133
+
134
+ void complete_with_success()
135
+ {
136
+ connect_deadline_.cancel();
137
+ resolve_deadline_.cancel();
138
+ if (auto handler = std::move(handler_); handler) {
139
+ handler->on_connected(address_, std::move(stream_));
140
+ }
141
+ }
142
+
143
+ void reconnect_socket(std::error_code reconnect_reason, const std::string& message)
144
+ {
145
+ last_error_ = reconnect_reason;
146
+ stream_->close([self = shared_from_this(), message, reconnect_reason](std::error_code ec) {
147
+ if (ec) {
148
+ CB_LOG_WARNING(
149
+ "unable to close app telemetry socket, but continue reconnecting anyway. {}",
150
+ tao::json::to_string(tao::json::value{
151
+ { "message", message },
152
+ { "reconnect_reason",
153
+ fmt::format("{}, {}", reconnect_reason.value(), reconnect_reason.message()) },
154
+ { "ec", fmt::format("{}, {}", ec.value(), ec.message()) },
155
+ }));
156
+ }
157
+ self->connect_socket();
158
+ });
159
+ }
160
+
161
+ void connect_socket()
162
+ {
163
+ if (next_endpoint_ == endpoints_.end()) {
164
+ if (!last_error_) {
165
+ last_error_ = errc::network::no_endpoints_left;
166
+ }
167
+ return complete_with_error(last_error_, "no more endpoints to connect");
168
+ }
169
+ auto endpoint = next_endpoint_++;
170
+
171
+ connect_deadline_.expires_after(options_.connect_timeout);
172
+ connect_deadline_.async_wait([self = shared_from_this()](auto ec) {
173
+ if (ec == asio::error::operation_aborted) {
174
+ return;
175
+ }
176
+ return self->reconnect_socket(ec, "connect deadline");
177
+ });
178
+
179
+ if (options_.enable_tls) {
180
+ stream_ = std::make_unique<io::tls_stream_impl>(ctx_, tls_);
181
+ } else {
182
+ stream_ = std::make_unique<io::plain_stream_impl>(ctx_);
183
+ }
184
+ stream_->async_connect(endpoint->endpoint(), [self = shared_from_this()](auto ec) {
185
+ if (ec) {
186
+ return self->reconnect_socket(ec, "connection failure");
187
+ }
188
+ self->complete_with_success();
189
+ });
190
+ }
191
+
192
+ void resolve_address()
193
+ {
194
+ resolve_deadline_.expires_after(options_.resolve_timeout);
195
+ resolve_deadline_.async_wait([self = shared_from_this()](auto ec) {
196
+ if (ec == asio::error::operation_aborted) {
197
+ return;
198
+ }
199
+ return self->complete_with_error(errc::common::unambiguous_timeout, "timeout on resolve");
200
+ });
201
+ io::async_resolve(options_.use_ip_protocol,
202
+ resolver_,
203
+ address_.hostname,
204
+ address_.service,
205
+ [self = shared_from_this()](auto ec, const auto& endpoints) {
206
+ self->resolve_deadline_.cancel();
207
+ if (ec) {
208
+ CB_LOG_DEBUG("failed to resolve address for app telemetry socket. {}",
209
+ tao::json::to_string(tao::json::value{
210
+ { "message", ec.message() },
211
+ { "hostname", self->address_.hostname },
212
+ }));
213
+ return self->complete_with_error(
214
+ ec, "failed to resolve app telemetry socket");
215
+ }
216
+
217
+ self->endpoints_ = endpoints;
218
+ self->next_endpoint_ = self->endpoints_.begin();
219
+ self->connect_socket();
220
+ });
221
+ }
222
+
223
+ app_telemetry_address address_;
224
+ cluster_options options_;
225
+ asio::io_context& ctx_;
226
+ asio::ssl::context& tls_;
227
+ asio::steady_timer resolve_deadline_;
228
+ asio::steady_timer connect_deadline_;
229
+ asio::ip::tcp::resolver resolver_;
230
+ std::shared_ptr<connection_state_listener> handler_;
231
+ std::error_code last_error_{};
232
+ std::unique_ptr<io::stream_impl> stream_{};
233
+ asio::ip::tcp::resolver::results_type endpoints_{};
234
+ asio::ip::tcp::resolver::results_type::iterator next_endpoint_{};
235
+ };
236
+
237
+ enum class app_telemetry_opcode : std::uint8_t {
238
+ GET_TELEMETRY = 0x00,
239
+ };
240
+
241
+ constexpr auto
242
+ is_valid_app_telemetry_opcode(std::byte opcode) -> bool
243
+ {
244
+ return opcode == static_cast<std::byte>(app_telemetry_opcode::GET_TELEMETRY);
245
+ }
246
+
247
+ enum class app_telemetry_status : std::uint8_t {
248
+ SUCCESS = 0x00,
249
+ UNKNOWN_COMMAND = 0x01,
250
+ };
251
+
252
+ class websocket_session
253
+ : public websocket_callbacks
254
+ , public std::enable_shared_from_this<websocket_session>
255
+ {
256
+ public:
257
+ static auto start(asio::io_context& ctx,
258
+ app_telemetry_address address,
259
+ cluster_credentials credentials,
260
+ std::unique_ptr<io::stream_impl>&& stream,
261
+ std::shared_ptr<app_telemetry_meter> meter,
262
+ std::shared_ptr<connection_state_listener> reporter,
263
+ std::chrono::milliseconds ping_interval,
264
+ std::chrono::milliseconds ping_timeout) -> std::shared_ptr<websocket_session>
265
+ {
266
+ auto handler = std::make_shared<websocket_session>(ctx,
267
+ std::move(address),
268
+ std::move(credentials),
269
+ std::move(stream),
270
+ std::move(meter),
271
+ std::move(reporter),
272
+ ping_interval,
273
+ ping_timeout);
274
+ handler->start();
275
+ return handler;
276
+ }
277
+
278
+ websocket_session(asio::io_context& ctx,
279
+ app_telemetry_address address,
280
+ cluster_credentials credentials,
281
+ std::unique_ptr<io::stream_impl>&& stream,
282
+ std::shared_ptr<app_telemetry_meter> meter,
283
+ std::shared_ptr<connection_state_listener> reporter,
284
+ std::chrono::milliseconds ping_interval,
285
+ std::chrono::milliseconds ping_timeout)
286
+ : ctx_{ ctx }
287
+ , address_{ std::move(address) }
288
+ , credentials_{ std::move(credentials) }
289
+ , stream_{ std::move(stream) }
290
+ , meter_{ std::move(meter) }
291
+ , reporter_{ std::move(reporter) }
292
+ , codec_{ this }
293
+ , ping_interval_timer_{ ctx_ }
294
+ , ping_timeout_timer_{ ctx_ }
295
+ , ping_interval_{ ping_interval }
296
+ , ping_timeout_{ ping_timeout }
297
+ {
298
+ }
299
+
300
+ void stop()
301
+ {
302
+ is_running_ = false;
303
+ ping_interval_timer_.cancel();
304
+ ping_timeout_timer_.cancel();
305
+ stream_->close([](auto /* ec */) {
306
+ });
307
+ }
308
+
309
+ void stop_and_error(std::error_code ec, const std::string& message)
310
+ {
311
+ stop();
312
+ if (auto reporter = std::move(reporter_); reporter) {
313
+ reporter->on_error(address_, ec, message);
314
+ }
315
+ }
316
+
317
+ void send_ping(const websocket_codec& ws)
318
+ {
319
+ write_buffer(ws.ping());
320
+ start_write();
321
+
322
+ ping_timeout_timer_.expires_after(ping_timeout_);
323
+ ping_timeout_timer_.async_wait([self = shared_from_this()](auto ec) {
324
+ if (ec == asio::error::operation_aborted) {
325
+ return;
326
+ }
327
+ CB_LOG_DEBUG("app telemetry websocket did not respond in time for ping request. {}",
328
+ tao::json::to_string(tao::json::value{
329
+ { "ping_interval", fmt::format("{}", self->ping_interval_) },
330
+ { "ping_timeout", fmt::format("{}", self->ping_timeout_) },
331
+ { "hostname", self->address_.hostname },
332
+ }));
333
+ self->stop_and_error(errc::common::unambiguous_timeout, "server did not respond in time");
334
+ });
335
+
336
+ ping_interval_timer_.expires_after(ping_interval_);
337
+ ping_interval_timer_.async_wait([self = shared_from_this(), &ws](auto ec) {
338
+ if (ec == asio::error::operation_aborted) {
339
+ return;
340
+ }
341
+ self->send_ping(ws);
342
+ });
343
+ }
344
+
345
+ void on_ready(const websocket_codec& ws) override
346
+ {
347
+ reporter_->on_websocket_ready();
348
+ send_ping(ws);
349
+ }
350
+
351
+ void on_text(const websocket_codec& /* ws */, gsl::span<std::byte> payload) override
352
+ {
353
+ CB_LOG_WARNING("text messages are not supported. {}",
354
+ tao::json::to_string(tao::json::value{
355
+ { "payload", base64::encode(payload) },
356
+ { "hostname", address_.hostname },
357
+ }));
358
+ return stop_and_error(errc::network::protocol_error, "unsupported frame: text");
359
+ }
360
+
361
+ void on_binary(const websocket_codec& ws, gsl::span<std::byte> payload) override
362
+ {
363
+ if (payload.empty()) {
364
+ CB_LOG_WARNING("binary message have to be at least a byte. {}",
365
+ tao::json::to_string(tao::json::value{
366
+ { "payload", base64::encode(payload) },
367
+ { "hostname", address_.hostname },
368
+ }));
369
+ return stop_and_error(errc::network::protocol_error, "the paload is too small");
370
+ }
371
+ if (!is_valid_app_telemetry_opcode(payload[0])) {
372
+ CB_LOG_WARNING("binary message has unknown opcode. {}",
373
+ tao::json::to_string(tao::json::value{
374
+ { "payload", base64::encode(payload) },
375
+ { "hostname", address_.hostname },
376
+ }));
377
+ return stop_and_error(errc::network::protocol_error,
378
+ fmt::format("invalid opcode: {}", payload[0]));
379
+ }
380
+
381
+ auto opcode = static_cast<app_telemetry_opcode>(payload[0]);
382
+ switch (opcode) {
383
+ case app_telemetry_opcode::GET_TELEMETRY: {
384
+ std::vector<std::byte> response{};
385
+ response.emplace_back(static_cast<std::byte>(app_telemetry_status::SUCCESS));
386
+ meter_->generate_report(response);
387
+ write_buffer(ws.binary({ response.data(), response.size() }));
388
+ start_write();
389
+ } break;
390
+ }
391
+ }
392
+
393
+ void on_ping(const websocket_codec& ws, gsl::span<std::byte> payload) override
394
+ {
395
+ write_buffer(ws.pong(payload));
396
+ start_write();
397
+ }
398
+
399
+ void on_pong(const websocket_codec& /* ws */, gsl::span<std::byte> /* payload */) override
400
+ {
401
+ ping_timeout_timer_.cancel();
402
+ }
403
+
404
+ void on_close(const websocket_codec& /* ws */, gsl::span<std::byte> payload) override
405
+ {
406
+ CB_LOG_DEBUG("remote peer closed WebSocket. {}",
407
+ tao::json::to_string(tao::json::value{
408
+ { "payload", base64::encode(payload) },
409
+ { "hostname", address_.hostname },
410
+ }));
411
+ return stop_and_error({}, "server sent close message");
412
+ }
413
+
414
+ void on_error(const websocket_codec& /* ws */, const std::string& message) override
415
+ {
416
+ CB_LOG_WARNING("error from WebSocket codec. {}",
417
+ tao::json::to_string(tao::json::value{
418
+ { "message", message },
419
+ { "hostname", address_.hostname },
420
+ }));
421
+
422
+ return stop_and_error(errc::network::protocol_error,
423
+ fmt::format("websocket error: {}", message));
424
+ }
425
+
426
+ private:
427
+ auto build_handshake_message() -> std::vector<std::byte>
428
+ {
429
+ auto credentials = fmt::format("{}:{}", credentials_.username, credentials_.password);
430
+ auto message = fmt::format("GET {} HTTP/1.1\r\n"
431
+ "Authorization: Basic {}\r\n"
432
+ "Upgrade: websocket\r\n"
433
+ "Connection: Upgrade\r\n"
434
+ "Host: {}:{}\r\n"
435
+ "Sec-WebSocket-Version: 13\r\n"
436
+ "Sec-WebSocket-Key: {}\r\n"
437
+ "\r\n",
438
+ address_.path,
439
+ base64::encode(gsl::as_bytes(gsl::span{
440
+ credentials.data(),
441
+ credentials.size(),
442
+ })),
443
+ address_.hostname,
444
+ address_.service,
445
+ codec_.session_key());
446
+ return {
447
+ reinterpret_cast<const std::byte*>(message.data()),
448
+ reinterpret_cast<const std::byte*>(message.data()) + message.size(),
449
+ };
450
+ }
451
+
452
+ void write_buffer(std::vector<std::byte>&& buffer)
453
+ {
454
+ const std::scoped_lock lock(mutex_);
455
+ buffers_.emplace(std::move(buffer));
456
+ }
457
+
458
+ void start()
459
+ {
460
+ is_running_ = true;
461
+ write_buffer(build_handshake_message());
462
+ start_write();
463
+ }
464
+
465
+ void start_write()
466
+ {
467
+ if (!is_running_) {
468
+ return;
469
+ }
470
+
471
+ if (!is_writing_) {
472
+ asio::post(stream_->get_executor(), [self = shared_from_this()]() {
473
+ self->do_write();
474
+ });
475
+ is_writing_ = true;
476
+ }
477
+ }
478
+
479
+ void do_write()
480
+ {
481
+ std::vector<asio::const_buffer> buffers;
482
+ std::vector<std::vector<std::byte>> active_buffers;
483
+
484
+ {
485
+ const std::scoped_lock lock(mutex_);
486
+
487
+ while (!buffers_.empty()) {
488
+ active_buffers.emplace_back(std::move(buffers_.front()));
489
+ buffers_.pop();
490
+ buffers.emplace_back(asio::buffer(active_buffers.back()));
491
+ }
492
+ }
493
+
494
+ if (!buffers.empty()) {
495
+ stream_->async_write(buffers,
496
+ [self = shared_from_this(), active_buffers = std::move(active_buffers)](
497
+ auto ec, auto bytes_transferred) mutable {
498
+ if (ec == asio::error::operation_aborted) {
499
+ return;
500
+ }
501
+ self->handle_write(ec, bytes_transferred);
502
+ });
503
+ start_read();
504
+ } else {
505
+ is_writing_ = false;
506
+ }
507
+ }
508
+
509
+ void handle_write(std::error_code ec, std::size_t /* bytes_transferred */)
510
+ {
511
+ if (!is_running_) {
512
+ return;
513
+ }
514
+ if (ec) {
515
+ is_writing_ = false;
516
+ CB_LOG_DEBUG("unable to write to app telemetry socket. {}",
517
+ tao::json::to_string(tao::json::value{
518
+ { "message", ec.message() },
519
+ { "hostname", address_.hostname },
520
+ }));
521
+ return stop_and_error(ec, "failed to write to app telemetry socket");
522
+ }
523
+
524
+ if (const std::scoped_lock lock(mutex_); !buffers_.empty()) {
525
+ start_write();
526
+ } else {
527
+ is_writing_ = false;
528
+ }
529
+ }
530
+
531
+ void start_read()
532
+ {
533
+ if (!is_running_) {
534
+ return;
535
+ }
536
+
537
+ if (!is_reading_) {
538
+ is_reading_ = true;
539
+ do_read();
540
+ }
541
+ }
542
+
543
+ void do_read()
544
+ {
545
+ stream_->async_read_some(asio::buffer(read_buffer_),
546
+ [self = shared_from_this()](auto ec, auto bytes_transferred) {
547
+ if (ec == asio::error::operation_aborted) {
548
+ return;
549
+ }
550
+ self->handle_read(ec, bytes_transferred);
551
+ });
552
+ }
553
+
554
+ void handle_read(std::error_code ec, std::size_t bytes_transferred)
555
+ {
556
+ if (!is_running_) {
557
+ return;
558
+ }
559
+ if (ec) {
560
+ is_reading_ = false;
561
+ CB_LOG_DEBUG("unable to read from app telemetry socket. {}",
562
+ tao::json::to_string(tao::json::value{
563
+ { "message", ec.message() },
564
+ { "hostname", address_.hostname },
565
+ }));
566
+ return stop_and_error(ec, "unable to read from the app telemetry socket");
567
+ }
568
+ codec_.feed(gsl::span(read_buffer_.data(), bytes_transferred));
569
+ do_read();
570
+ }
571
+
572
+ asio::io_context& ctx_;
573
+ app_telemetry_address address_;
574
+ cluster_credentials credentials_;
575
+ std::unique_ptr<io::stream_impl> stream_;
576
+ std::shared_ptr<app_telemetry_meter> meter_;
577
+ std::shared_ptr<connection_state_listener> reporter_;
578
+ websocket_codec codec_;
579
+
580
+ asio::steady_timer ping_interval_timer_;
581
+ asio::steady_timer ping_timeout_timer_;
582
+ std::chrono::milliseconds ping_interval_;
583
+ std::chrono::milliseconds ping_timeout_;
584
+
585
+ std::atomic<bool> is_running_{ false };
586
+ std::queue<std::vector<std::byte>> buffers_;
587
+ std::mutex mutex_;
588
+ std::atomic<bool> is_writing_{ false };
589
+ std::array<std::byte, 1024> read_buffer_{};
590
+ std::atomic<bool> is_reading_{ false };
591
+ };
592
+
593
+ class backoff_calculator
594
+ {
595
+ public:
596
+ backoff_calculator() = default;
597
+ backoff_calculator(const backoff_calculator&) = default;
598
+ backoff_calculator(backoff_calculator&&) = default;
599
+ auto operator=(backoff_calculator&&) -> backoff_calculator& = default;
600
+ auto operator=(const backoff_calculator&) -> backoff_calculator& = default;
601
+ virtual ~backoff_calculator() = default;
602
+
603
+ [[nodiscard]] virtual auto retry_after(std::size_t retry_attempts) const
604
+ -> std::chrono::milliseconds = 0;
605
+ };
606
+
607
+ class no_backoff : public backoff_calculator
608
+ {
609
+ public:
610
+ [[nodiscard]] auto retry_after(std::size_t /* retry_attempts */) const
611
+ -> std::chrono::milliseconds override
612
+ {
613
+ return std::chrono::milliseconds::zero();
614
+ }
615
+ };
616
+
617
+ class exponential_backoff_with_jitter : public backoff_calculator
618
+ {
619
+ public:
620
+ exponential_backoff_with_jitter(std::chrono::milliseconds min,
621
+ std::chrono::milliseconds max,
622
+ double factor,
623
+ double jitter_factor)
624
+ : min_{ static_cast<double>(min.count()) }
625
+ , max_{ static_cast<double>(max.count()) }
626
+ , factor_{ factor }
627
+ , jitter_factor_{ jitter_factor }
628
+ {
629
+ }
630
+
631
+ [[nodiscard]] auto retry_after(std::size_t retry_attempts) const
632
+ -> std::chrono::milliseconds override
633
+ {
634
+ double backoff = min_ * std::pow(factor_, static_cast<double>(retry_attempts));
635
+ backoff = std::max(std::min(backoff, max_), min_);
636
+
637
+ const double jitter = calculate_jitter(backoff);
638
+ backoff += jitter;
639
+
640
+ return std::chrono::milliseconds(static_cast<std::uint64_t>(backoff));
641
+ }
642
+
643
+ private:
644
+ [[nodiscard]] auto calculate_jitter(double backoff) const -> double
645
+ {
646
+ if (backoff == 0) {
647
+ return 0;
648
+ }
649
+
650
+ static thread_local std::default_random_engine gen{ std::random_device{}() };
651
+
652
+ const double jitter_offset = (backoff * 100.0 * jitter_factor_) / 100.0;
653
+ auto low_bound = static_cast<std::int64_t>(std::max(min_ - backoff, -jitter_offset));
654
+ auto high_bound = static_cast<std::int64_t>(std::min(max_ - backoff, jitter_offset));
655
+
656
+ std::uniform_int_distribution<std::int64_t> dis(low_bound, high_bound);
657
+ return static_cast<double>(dis(gen));
658
+ }
659
+
660
+ double min_;
661
+ double max_;
662
+ double factor_;
663
+ double jitter_factor_;
664
+ };
665
+
666
+ } // namespace
667
+
668
+ class app_telemetry_reporter_impl
669
+ : public std::enable_shared_from_this<app_telemetry_reporter_impl>
670
+ , public connection_state_listener
671
+ {
672
+ public:
673
+ app_telemetry_reporter_impl(std::shared_ptr<app_telemetry_meter> meter,
674
+ cluster_options options,
675
+ cluster_credentials credentials,
676
+ asio::io_context& ctx,
677
+ asio::ssl::context& tls)
678
+ : meter_{ std::move(meter) }
679
+ , options_{ std::move(options) }
680
+ , credentials_{ std::move(credentials) }
681
+ , ctx_{ ctx }
682
+ , tls_{ tls }
683
+ , backoff_{ ctx }
684
+ , exponential_backoff_calculator_{
685
+ std::chrono::milliseconds{ 100 },
686
+ options_.app_telemetry_backoff_interval,
687
+ 2 /* backoff factor */,
688
+ 0.5 /* jitter factor */,
689
+ }
690
+ {
691
+ if (options_.enable_app_telemetry) {
692
+ if (!options_.app_telemetry_endpoint.empty()) {
693
+ auto url = couchbase::core::utils::string_codec::url_parse(options_.app_telemetry_endpoint);
694
+ if (url.host.empty() || url.scheme != "ws") {
695
+ CB_LOG_WARNING(
696
+ "unable to use \"{}\" as a app telemetry endpoint (expected ws:// and hostname)",
697
+ options_.app_telemetry_endpoint);
698
+ return;
699
+ }
700
+ addresses_.push_back({
701
+ url.host,
702
+ std::to_string(url.port),
703
+ url.path,
704
+ {},
705
+ });
706
+ }
707
+ } else {
708
+ meter_->disable();
709
+ }
710
+ }
711
+
712
+ void stop()
713
+ {
714
+ websocket_state_ = connection_state::stopped;
715
+
716
+ meter_->disable();
717
+ if (auto dialer = std::move(dialer_); dialer) {
718
+ dialer->stop();
719
+ }
720
+ backoff_.cancel();
721
+ if (auto session = std::move(websocket_session_); session) {
722
+ session->stop();
723
+ }
724
+ }
725
+
726
+ void on_connection_pending(const app_telemetry_address& address) override
727
+ {
728
+ websocket_state_ = connection_state::connecting;
729
+ CB_LOG_WARNING("connecting app telemetry WebSocket. {}",
730
+ tao::json::to_string(tao::json::value{
731
+ { "hostname", address.hostname },
732
+ }));
733
+ }
734
+
735
+ void on_connected(const app_telemetry_address& address,
736
+ std::unique_ptr<io::stream_impl>&& stream) override
737
+ {
738
+ dialer_ = nullptr;
739
+ backoff_.cancel();
740
+
741
+ if (websocket_state_ == connection_state::stopped) {
742
+ return;
743
+ }
744
+
745
+ websocket_state_ = connection_state::connected;
746
+ CB_LOG_WARNING("connected app telemetry endpoint. {}",
747
+ tao::json::to_string(tao::json::value{
748
+ { "stream", stream->id() },
749
+ { "hostname", address.hostname },
750
+ }));
751
+ websocket_session_ = websocket_session::start(ctx_,
752
+ address,
753
+ credentials_,
754
+ std::move(stream),
755
+ meter_,
756
+ shared_from_this(),
757
+ options_.app_telemetry_ping_interval,
758
+ options_.app_telemetry_ping_timeout);
759
+ retry_backoff_calculator_ = &no_backoff_calculator_;
760
+ ++next_address_index_;
761
+ }
762
+
763
+ void on_websocket_ready() override
764
+ {
765
+ connection_attempt_ = 0;
766
+ }
767
+
768
+ void on_error(const app_telemetry_address& address,
769
+ std::error_code ec,
770
+ const std::string& message) override
771
+ {
772
+ if (ec == asio::error::operation_aborted || websocket_state_ == connection_state::stopped) {
773
+ return;
774
+ }
775
+
776
+ websocket_state_ = connection_state::disconnected;
777
+ websocket_session_ = nullptr;
778
+
779
+ if (addresses_.empty()) {
780
+ CB_LOG_WARNING("do not reconnect WebSocket for Application Telemetry, none of the nodes "
781
+ "exposes the collector endpoint. {}",
782
+ tao::json::to_string(tao::json::value{
783
+ { "message", ec.message() },
784
+ { "ec", ec.value() },
785
+ { "hostname", address.hostname },
786
+ }));
787
+ return;
788
+ }
789
+
790
+ ++connection_attempt_;
791
+ ++next_address_index_;
792
+ if (next_address_index_ >= addresses_.size()) {
793
+ static thread_local std::default_random_engine gen{ std::random_device{}() };
794
+ std::shuffle(addresses_.begin(), addresses_.end(), gen);
795
+ next_address_index_ = 0;
796
+ retry_backoff_calculator_ = &exponential_backoff_calculator_;
797
+ }
798
+ auto next_address = addresses_[next_address_index_];
799
+ auto backoff = retry_backoff_calculator_->retry_after(connection_attempt_);
800
+ CB_LOG_WARNING("error from app telemetry endpoint, reconnecting in {}. {}",
801
+ backoff,
802
+ tao::json::to_string(tao::json::value{
803
+ { "message", message },
804
+ { "ec", fmt::format("{}, {}", ec.value(), ec.message()) },
805
+ { "connection_attempt", connection_attempt_ },
806
+ { "hostname", address.hostname },
807
+ { "next_hostname", next_address.hostname },
808
+ }));
809
+ if (backoff > std::chrono::milliseconds::zero()) {
810
+ backoff_.expires_after(backoff);
811
+ backoff_.async_wait([self = shared_from_this(), next_address](auto ec) {
812
+ if (ec == asio::error::operation_aborted) {
813
+ return;
814
+ }
815
+ if (self->websocket_state_ == connection_state::disconnected) {
816
+ self->dialer_ =
817
+ telemetry_dialer::dial(next_address, self->options_, self->ctx_, self->tls_, self);
818
+ }
819
+ });
820
+ return;
821
+ }
822
+ dialer_ = telemetry_dialer::dial(next_address, options_, ctx_, tls_, shared_from_this());
823
+ }
824
+
825
+ void update_config(topology::configuration&& config)
826
+ {
827
+ if (!options_.enable_app_telemetry) {
828
+ meter_->disable();
829
+ return;
830
+ }
831
+ meter_->update_config(config);
832
+
833
+ if (options_.app_telemetry_endpoint.empty()) {
834
+ addresses_ = get_app_telemetry_addresses(config, options_.enable_tls, options_.network);
835
+ next_address_index_ = 0;
836
+ }
837
+
838
+ if (addresses_.empty()) {
839
+ meter_->disable();
840
+ } else {
841
+ meter_->enable();
842
+ if (websocket_state_ == connection_state::disconnected) {
843
+ dialer_ = telemetry_dialer::dial(
844
+ addresses_[next_address_index_], options_, ctx_, tls_, shared_from_this());
845
+ }
846
+ }
847
+ }
848
+
849
+ private:
850
+ std::shared_ptr<app_telemetry_meter> meter_;
851
+ cluster_options options_;
852
+ cluster_credentials credentials_;
853
+ asio::io_context& ctx_;
854
+ asio::ssl::context& tls_;
855
+ asio::steady_timer backoff_;
856
+ const exponential_backoff_with_jitter exponential_backoff_calculator_;
857
+
858
+ std::shared_ptr<telemetry_dialer> dialer_{ nullptr };
859
+
860
+ connection_state websocket_state_{ connection_state::disconnected };
861
+ std::shared_ptr<websocket_session> websocket_session_{};
862
+ std::vector<app_telemetry_address> addresses_{};
863
+ std::size_t next_address_index_{ 0 };
864
+
865
+ const no_backoff no_backoff_calculator_{};
866
+ const backoff_calculator* retry_backoff_calculator_{ &no_backoff_calculator_ };
867
+ std::size_t connection_attempt_{ 0 };
868
+ };
869
+
870
+ app_telemetry_reporter::app_telemetry_reporter(std::shared_ptr<app_telemetry_meter> meter,
871
+ const cluster_options& options,
872
+ const cluster_credentials& credentials,
873
+ asio::io_context& ctx,
874
+ asio::ssl::context& tls)
875
+ : impl_{
876
+ std::make_shared<app_telemetry_reporter_impl>(std::move(meter), options, credentials, ctx, tls)
877
+ }
878
+ {
879
+ }
880
+
881
+ app_telemetry_reporter::~app_telemetry_reporter() = default;
882
+
883
+ void
884
+ app_telemetry_reporter::update_config(topology::configuration config)
885
+ {
886
+ return impl_->update_config(std::move(config));
887
+ }
888
+
889
+ void
890
+ app_telemetry_reporter::stop()
891
+ {
892
+ return impl_->stop();
893
+ }
894
+
895
+ } // namespace couchbase::core