diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index dfff66c7..bd9e5b4c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [ main ] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: rubocop: runs-on: ubuntu-20.04 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b2a57db..98196441 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: source: runs-on: ubuntu-22.04 diff --git a/README.md b/README.md index 8ad32cdd..a998584b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The library has been tested with MRI 3.1, 3.2 and 3.3. Supported platforms are L Add this line to your application's Gemfile: ```ruby -gem "couchbase", "3.5.4" +gem "couchbase", "3.5.5" ``` And then execute: diff --git a/Rakefile b/Rakefile index bda52d9e..efc248d9 100644 --- a/Rakefile +++ b/Rakefile @@ -27,7 +27,7 @@ end desc "Compile binary extension" task :compile do require 'tempfile' - Dir.chdir(Dir.tmpdir) do + Dir.chdir(Dir.tmpdir) do # rubocop:disable ThreadSafety/DirChdir sh "ruby '#{File.join(__dir__, 'ext', 'extconf.rb')}'" end end @@ -52,9 +52,9 @@ end desc "Encode git revision into 'ext/extconf.rb' template (dependency of 'build' task)" task :render_git_revision do - library_revision = Dir.chdir(__dir__) { `git rev-parse HEAD`.strip } - core_revision = Dir.chdir(File.join(__dir__, 'ext', 'couchbase')) { `git rev-parse HEAD`.strip } - core_describe = Dir.chdir(File.join(__dir__, 'ext', 'couchbase')) do + library_revision = Dir.chdir(__dir__) { `git rev-parse HEAD`.strip } # rubocop:disable ThreadSafety/DirChdir + core_revision = Dir.chdir(File.join(__dir__, 'ext', 'couchbase')) { `git rev-parse HEAD`.strip } # rubocop:disable ThreadSafety/DirChdir + core_describe = Dir.chdir(File.join(__dir__, 'ext', 'couchbase')) do # rubocop:disable ThreadSafety/DirChdir `git fetch --tags >/dev/null 2>&1` `git describe --long --always HEAD`.strip end diff --git a/bin/jenkins/repackage-extension.rb b/bin/jenkins/repackage-extension.rb index 30ed3b83..95ab5ac9 100755 --- a/bin/jenkins/repackage-extension.rb +++ b/bin/jenkins/repackage-extension.rb @@ -51,7 +51,7 @@ def bundle(target_dir) end if first_gemspec - Dir.chdir(first_gemspec.full_gem_path) do + Dir.chdir(first_gemspec.full_gem_path) do # rubocop:disable ThreadSafety/DirChdir File.write("lib/couchbase/libcouchbase.rb", <<-RUBY) begin require_relative "\#{RUBY_VERSION[/(\\d+\\.\\d+)/]}/libcouchbase" @@ -101,7 +101,7 @@ def repackage(gemspec) # build new gem output_gem = nil - Dir.chdir gemspec.full_gem_path do + Dir.chdir gemspec.full_gem_path do # rubocop:disable ThreadSafety/DirChdir output_gem = Gem::Package.build(gemspec, true) end diff --git a/ext/CMakeLists.txt b/ext/CMakeLists.txt index efb35db2..b900e600 100644 --- a/ext/CMakeLists.txt +++ b/ext/CMakeLists.txt @@ -61,11 +61,14 @@ add_library( rcb_version.cxx rcb_views.cxx) target_include_directories(couchbase PRIVATE ${PROJECT_BINARY_DIR}/generated) -target_include_directories(couchbase PRIVATE SYSTEM ${RUBY_INCLUDE_DIR} - ${PROJECT_SOURCE_DIR}/couchbase - ${PROJECT_BINARY_DIR}/couchbase/generated - ${PROJECT_SOURCE_DIR}/couchbase/third_party/cxx_function - ${PROJECT_SOURCE_DIR}/couchbase/third_party/expected/include) +target_include_directories( + couchbase + PRIVATE SYSTEM + ${RUBY_INCLUDE_DIR} + ${PROJECT_SOURCE_DIR}/couchbase + ${PROJECT_BINARY_DIR}/couchbase/generated + ${PROJECT_SOURCE_DIR}/couchbase/third_party/cxx_function + ${PROJECT_SOURCE_DIR}/couchbase/third_party/expected/include) target_link_libraries( couchbase PRIVATE project_options @@ -74,7 +77,6 @@ target_link_libraries( Microsoft.GSL::GSL asio taocpp::json - fmt::fmt spdlog::spdlog snappy) if(RUBY_LIBRUBY) diff --git a/ext/couchbase b/ext/couchbase index b7086c05..24dca979 160000 --- a/ext/couchbase +++ b/ext/couchbase @@ -1 +1 @@ -Subproject commit b7086c059659d3f1e03a8d9bff266fad0c6c9b89 +Subproject commit 24dca979ec842ce200aaa1741f1271a4a61c837d diff --git a/ext/extconf.rb b/ext/extconf.rb index 4459e906..701fb2b0 100644 --- a/ext/extconf.rb +++ b/ext/extconf.rb @@ -104,6 +104,7 @@ def sys(*cmd) elsif ENV["CB_STATIC_OPENSSL"] cmake_flags << "-DCOUCHBASE_CXX_CLIENT_STATIC_OPENSSL=ON" end +cmake_flags << "-DCOUCHBASE_CXX_CLIENT_WRAPPER_UNIFIED_ID=ruby/#{Couchbase::VERSION[:sdk]}" unless cmake_flags.grep(/BORINGSSL/) case RbConfig::CONFIG["target_os"] diff --git a/ext/rcb_analytics.cxx b/ext/rcb_analytics.cxx index 26c6215a..a6f6bcc1 100644 --- a/ext/rcb_analytics.cxx +++ b/ext/rcb_analytics.cxx @@ -36,7 +36,7 @@ #include #include -#include +#include #include #include @@ -911,17 +911,17 @@ cb_Backend_analytics_link_get_all(VALUE self, VALUE options) cb_throw_error( resp.ctx, fmt::format(R"(unable to retrieve links type={}, dataverse="{}", name="{}")", - req.link_type, - req.link_name, - req.dataverse_name)); + req.link_type.value_or("-"), + req.link_name.value_or("-"), + req.dataverse_name.value_or("-"))); } else { const auto& first_error = resp.errors.front(); cb_throw_error( resp.ctx, fmt::format(R"(unable to retrieve links type={}, dataverse="{}", name="{}" ({}: {}))", - req.link_type, - req.link_name, - req.dataverse_name, + req.link_type.value_or("-"), + req.link_name.value_or("-"), + req.dataverse_name.value_or("-"), first_error.code, first_error.message)); } diff --git a/ext/rcb_backend.cxx b/ext/rcb_backend.cxx index 4c162ccb..693410c0 100644 --- a/ext/rcb_backend.cxx +++ b/ext/rcb_backend.cxx @@ -22,7 +22,7 @@ #include #include -#include +#include #include #include @@ -208,6 +208,11 @@ initialize_cluster_options(const core::utils::connection_string& connstr, cluster_options.network().preferred_network(param.value()); } + static const auto server_group = rb_id2sym(rb_intern("preferred_server_group")); + if (auto group = options::get_string(options, server_group); group) { + cluster_options.network().preferred_server_group(group.value()); + } + static const auto sym_use_ip_protocol = rb_id2sym(rb_intern("use_ip_protocol")); if (auto proto = options::get_symbol(options, sym_use_ip_protocol); proto) { static const auto sym_any = rb_id2sym(rb_intern("any")); diff --git a/ext/rcb_buckets.cxx b/ext/rcb_buckets.cxx index cd908179..f81be58d 100644 --- a/ext/rcb_buckets.cxx +++ b/ext/rcb_buckets.cxx @@ -24,7 +24,7 @@ #include #include -#include +#include #include #include diff --git a/ext/rcb_collections.cxx b/ext/rcb_collections.cxx index dd494921..d18bff0b 100644 --- a/ext/rcb_collections.cxx +++ b/ext/rcb_collections.cxx @@ -23,7 +23,7 @@ #include #include -#include +#include #include #include diff --git a/ext/rcb_crud.cxx b/ext/rcb_crud.cxx index cfbd7760..397cca0a 100644 --- a/ext/rcb_crud.cxx +++ b/ext/rcb_crud.cxx @@ -128,7 +128,7 @@ cb_Backend_document_get_any_replica(VALUE self, VALUE id, VALUE options) { - auto cluster = cb_backend_to_public_api_cluster(self); + auto cluster = cb_backend_to_core_api_cluster(self); Check_Type(bucket, T_STRING); Check_Type(scope, T_STRING); @@ -136,23 +136,32 @@ cb_Backend_document_get_any_replica(VALUE self, Check_Type(id, T_STRING); try { - couchbase::get_any_replica_options opts; - set_timeout(opts, options); + core::document_id doc_id{ + cb_string_new(bucket), + cb_string_new(scope), + cb_string_new(collection), + cb_string_new(id), + }; - auto f = cluster.bucket(cb_string_new(bucket)) - .scope(cb_string_new(scope)) - .collection(cb_string_new(collection)) - .get_any_replica(cb_string_new(id), opts); - auto [ctx, resp] = cb_wait_for_future(f); - if (ctx.ec()) { - cb_throw_error(ctx, "unable to get replica of the document"); + core::operations::get_any_replica_request req{ doc_id }; + cb_extract_timeout(req, options); + cb_extract_read_preference(req, options); + + std::promise promise; + auto f = promise.get_future(); + cluster.execute(req, [promise = std::move(promise)](auto&& resp) mutable { + promise.set_value(std::forward(resp)); + }); + auto resp = cb_wait_for_future(f); + if (resp.ctx.ec()) { + cb_throw_error(resp.ctx, "unable to get replica of the document"); } - auto value = resp.content_as(); VALUE res = rb_hash_new(); - rb_hash_aset(res, rb_id2sym(rb_intern("content")), cb_str_new(value.data)); - rb_hash_aset(res, rb_id2sym(rb_intern("cas")), cb_cas_to_num(resp.cas())); - rb_hash_aset(res, rb_id2sym(rb_intern("flags")), UINT2NUM(value.flags)); + rb_hash_aset(res, rb_id2sym(rb_intern("content")), cb_str_new(resp.value)); + rb_hash_aset(res, rb_id2sym(rb_intern("cas")), cb_cas_to_num(resp.cas)); + rb_hash_aset(res, rb_id2sym(rb_intern("flags")), UINT2NUM(resp.flags)); + rb_hash_aset(res, rb_id2sym(rb_intern("replica")), resp.replica ? Qtrue : Qfalse); return res; } catch (const std::system_error& se) { rb_exc_raise(cb_map_error_code( @@ -171,7 +180,7 @@ cb_Backend_document_get_all_replicas(VALUE self, VALUE id, VALUE options) { - auto cluster = cb_backend_to_public_api_cluster(self); + auto cluster = cb_backend_to_core_api_cluster(self); Check_Type(bucket, T_STRING); Check_Type(scope, T_STRING); @@ -179,25 +188,35 @@ cb_Backend_document_get_all_replicas(VALUE self, Check_Type(id, T_STRING); try { - couchbase::get_all_replicas_options opts; - set_timeout(opts, options); + core::document_id doc_id{ + cb_string_new(bucket), + cb_string_new(scope), + cb_string_new(collection), + cb_string_new(id), + }; - auto f = cluster.bucket(cb_string_new(bucket)) - .scope(cb_string_new(scope)) - .collection(cb_string_new(collection)) - .get_all_replicas(cb_string_new(id), opts); - auto [ctx, resp] = cb_wait_for_future(f); - if (ctx.ec()) { - cb_throw_error(ctx, "unable to get all replicas for the document"); + core::operations::get_all_replicas_request req{ doc_id }; + cb_extract_timeout(req, options); + cb_extract_read_preference(req, options); + + std::promise promise; + auto f = promise.get_future(); + cluster.execute(req, [promise = std::move(promise)](auto&& resp) mutable { + promise.set_value(std::forward(resp)); + }); + auto resp = cb_wait_for_future(f); + if (resp.ctx.ec()) { + cb_throw_error(resp.ctx, "unable to get all replicas for the document"); } - VALUE res = rb_ary_new_capa(static_cast(resp.size())); - for (const auto& entry : resp) { + VALUE res = rb_ary_new_capa(static_cast(resp.entries.size())); + + for (const auto& entry : resp.entries) { VALUE response = rb_hash_new(); - auto value = entry.content_as(); - rb_hash_aset(response, rb_id2sym(rb_intern("content")), cb_str_new(value.data)); - rb_hash_aset(response, rb_id2sym(rb_intern("cas")), cb_cas_to_num(entry.cas())); - rb_hash_aset(response, rb_id2sym(rb_intern("flags")), UINT2NUM(value.flags)); + rb_hash_aset(response, rb_id2sym(rb_intern("content")), cb_str_new(entry.value)); + rb_hash_aset(response, rb_id2sym(rb_intern("cas")), cb_cas_to_num(entry.cas)); + rb_hash_aset(response, rb_id2sym(rb_intern("flags")), UINT2NUM(entry.flags)); + rb_hash_aset(response, rb_id2sym(rb_intern("replica")), entry.replica ? Qtrue : Qfalse); rb_ary_push(res, response); } return res; @@ -1107,6 +1126,7 @@ cb_Backend_document_lookup_in_any_replica(VALUE self, core::operations::lookup_in_any_replica_request req{ doc_id }; cb_extract_timeout(req, options); + cb_extract_read_preference(req, options); static VALUE xattr_property = rb_id2sym(rb_intern("xattr")); static VALUE path_property = rb_id2sym(rb_intern("path")); @@ -1234,6 +1254,7 @@ cb_Backend_document_lookup_in_all_replicas(VALUE self, core::operations::lookup_in_all_replicas_request req{ doc_id }; cb_extract_timeout(req, options); + cb_extract_read_preference(req, options); static VALUE xattr_property = rb_id2sym(rb_intern("xattr")); static VALUE path_property = rb_id2sym(rb_intern("path")); diff --git a/ext/rcb_diagnostics.cxx b/ext/rcb_diagnostics.cxx index 11845ccf..afec2b7c 100644 --- a/ext/rcb_diagnostics.cxx +++ b/ext/rcb_diagnostics.cxx @@ -17,7 +17,7 @@ #include -#include +#include #include diff --git a/ext/rcb_exceptions.cxx b/ext/rcb_exceptions.cxx index 8cce91d8..107c4770 100644 --- a/ext/rcb_exceptions.cxx +++ b/ext/rcb_exceptions.cxx @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -27,7 +26,9 @@ #include #include -#include +#include + +#include #include "rcb_exceptions.hxx" #include "rcb_utils.hxx" diff --git a/ext/rcb_extras.cxx b/ext/rcb_extras.cxx index 7000e73e..731e4a66 100644 --- a/ext/rcb_extras.cxx +++ b/ext/rcb_extras.cxx @@ -26,8 +26,8 @@ #include #include -#include #include +#include #include #include diff --git a/ext/rcb_range_scan.cxx b/ext/rcb_range_scan.cxx index b5ce40de..54127e24 100644 --- a/ext/rcb_range_scan.cxx +++ b/ext/rcb_range_scan.cxx @@ -24,7 +24,7 @@ #include #include -#include +#include #include diff --git a/ext/rcb_search.cxx b/ext/rcb_search.cxx index 3a277f0b..03c76a76 100644 --- a/ext/rcb_search.cxx +++ b/ext/rcb_search.cxx @@ -30,8 +30,8 @@ #include #include -#include #include +#include #include #include diff --git a/ext/rcb_users.cxx b/ext/rcb_users.cxx index fccabc7a..c5b3ae59 100644 --- a/ext/rcb_users.cxx +++ b/ext/rcb_users.cxx @@ -28,8 +28,8 @@ #include #include -#include -#include +#include +#include #include #include diff --git a/ext/rcb_utils.hxx b/ext/rcb_utils.hxx index ba16da80..9ea7b571 100644 --- a/ext/rcb_utils.hxx +++ b/ext/rcb_utils.hxx @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -128,6 +129,37 @@ cb_extract_timeout(Request& req, VALUE options) } } +template +inline void +cb_extract_read_preference(Request& req, VALUE options) +{ + static VALUE property_name = rb_id2sym(rb_intern("read_preference")); + if (!NIL_P(options)) { + if (TYPE(options) != T_HASH) { + throw ruby_exception(rb_eArgError, + rb_sprintf("expected options to be Hash, given %+" PRIsVALUE, options)); + } + + VALUE val = rb_hash_aref(options, property_name); + if (NIL_P(val)) { + return; + } + if (TYPE(val) != T_SYMBOL) { + throw ruby_exception( + rb_eArgError, rb_sprintf("read_preference must be a Symbol, but given %+" PRIsVALUE, val)); + } + + if (ID mode = rb_sym2id(val); mode == rb_intern("no_preference")) { + req.read_preference = couchbase::read_preference::no_preference; + } else if (mode == rb_intern("selected_server_group")) { + req.read_preference = couchbase::read_preference::selected_server_group; + } else { + throw ruby_exception(rb_eArgError, + rb_sprintf("unexpected read_preference, given %+" PRIsVALUE, val)); + } + } +} + template inline void cb_extract_duration(Field& field, VALUE options, const char* name) diff --git a/lib/couchbase/logger.rb b/lib/couchbase/logger.rb index 9a3d6c16..a5b1ca05 100644 --- a/lib/couchbase/logger.rb +++ b/lib/couchbase/logger.rb @@ -46,7 +46,7 @@ def self.log_level # Return logger associated with the library def self.logger - @logger # rubocop:disable ThreadSafety/InstanceVariableInClassMethod + @logger # rubocop:disable ThreadSafety/ClassInstanceVariable end # Associate logger with the library @@ -67,8 +67,8 @@ def self.logger # # @since 3.4.0 def self.set_logger(logger, adapter_class: nil, verbose: false, level: :info) - @logger = logger # rubocop:disable ThreadSafety/InstanceVariableInClassMethod - if @logger.nil? # rubocop:disable ThreadSafety/InstanceVariableInClassMethod + @logger = logger # rubocop:disable ThreadSafety/ClassInstanceVariable + if @logger.nil? # rubocop:disable ThreadSafety/ClassInstanceVariable Backend.install_logger_shim(nil) return end diff --git a/lib/couchbase/options.rb b/lib/couchbase/options.rb index a87c2ea6..8c788463 100644 --- a/lib/couchbase/options.rb +++ b/lib/couchbase/options.rb @@ -237,10 +237,14 @@ def to_backend # Options for {Collection#get_all_replicas} class GetAllReplicas < Base attr_accessor :transcoder # @return [JsonTranscoder, #decode(String, Integer)] + attr_accessor :read_preference # @return [Symbol] # Creates an instance of options for {Collection#get_all_replicas} # # @param [JsonTranscoder, #decode(String, Integer)] transcoder used for decoding + # @param [Symbol] read_preference decides how the replica nodes will be selected. + # +:no_preference+:: no preference and will select any available replica. This is the default + # +:selected_server_group+:: restrict to nodes in {Cluster#preferred_server_group} # # @param [Integer, #in_milliseconds, nil] timeout # @param [Proc, nil] retry_strategy the custom retry strategy, if set @@ -249,12 +253,14 @@ class GetAllReplicas < Base # # @yieldparam [GetAllReplicas] self def initialize(transcoder: JsonTranscoder.new, + read_preference: :no_preference, timeout: nil, retry_strategy: nil, client_context: nil, parent_span: nil) super(timeout: timeout, retry_strategy: retry_strategy, client_context: client_context, parent_span: parent_span) @transcoder = transcoder + @read_preference = read_preference yield self if block_given? end @@ -262,6 +268,7 @@ def initialize(transcoder: JsonTranscoder.new, def to_backend { timeout: Utils::Time.extract_duration(@timeout), + read_preference: @read_preference, } end @@ -272,10 +279,14 @@ def to_backend # Options for {Collection#get_any_replica} class GetAnyReplica < Base attr_accessor :transcoder # @return [JsonTranscoder, #decode(String, Integer)] + attr_accessor :read_preference # @return [Symbol] # Creates an instance of options for {Collection#get_any_replica} # # @param [JsonTranscoder, #decode(String, Integer)] transcoder used for decoding + # @param [Symbol] read_preference decides how the replica nodes will be selected. + # +:no_preference+:: no preference and will select any available replica. This is the default + # +:selected_server_group+:: restrict to nodes in {Cluster#preferred_server_group} # # @param [Integer, #in_milliseconds, nil] timeout # @param [Proc, nil] retry_strategy the custom retry strategy, if set @@ -284,12 +295,14 @@ class GetAnyReplica < Base # # @yieldparam [GetAnyReplica] self def initialize(transcoder: JsonTranscoder.new, + read_preference: :no_preference, timeout: nil, retry_strategy: nil, client_context: nil, parent_span: nil) super(timeout: timeout, retry_strategy: retry_strategy, client_context: client_context, parent_span: parent_span) @transcoder = transcoder + @read_preference = read_preference yield self if block_given? end @@ -297,6 +310,7 @@ def initialize(transcoder: JsonTranscoder.new, def to_backend { timeout: Utils::Time.extract_duration(@timeout), + read_preference: @read_preference, } end @@ -1032,10 +1046,14 @@ def to_backend # Options for {Collection#lookup_in_any_replica} class LookupInAnyReplica < Base attr_accessor :transcoder # @return [JsonTranscoder, #decode(String)] + attr_accessor :read_preference # @return [Symbol] # Creates an instance of options for {Collection#lookup_in_any_replica} # # @param [JsonTranscoder, #decode(String)] transcoder used for encoding + # @param [Symbol] read_preference decides how the replica nodes will be selected. + # +:no_preference+:: no preference and will select any available replica. This is the default + # +:selected_server_group+:: restrict to nodes in {Cluster#preferred_server_group} # # @param [Integer, #in_milliseconds, nil] timeout # @param [Proc, nil] retry_strategy the custom retry strategy, if set @@ -1044,12 +1062,14 @@ class LookupInAnyReplica < Base # # @yieldparam [LookupIn] self def initialize(transcoder: JsonTranscoder.new, + read_preference: :no_preference, timeout: nil, retry_strategy: nil, client_context: nil, parent_span: nil) super(timeout: timeout, retry_strategy: retry_strategy, client_context: client_context, parent_span: parent_span) @transcoder = transcoder + @read_preference = read_preference yield self if block_given? end @@ -1057,6 +1077,7 @@ def initialize(transcoder: JsonTranscoder.new, def to_backend { timeout: Utils::Time.extract_duration(@timeout), + read_preference: @read_preference, } end @@ -1071,10 +1092,14 @@ def to_backend # Options for {Collection#lookup_in_all_replicas} class LookupInAllReplicas < Base attr_accessor :transcoder # @return [JsonTranscoder, #decode(String)] + attr_accessor :read_preference # @return [Symbol] # Creates an instance of options for {Collection#lookup_in_all_replicas} # # @param [JsonTranscoder, #decode(String)] transcoder used for encoding + # @param [Symbol] read_preference decides how the replica nodes will be selected. + # +:no_preference+:: no preference and will select any available replica. This is the default + # +:selected_server_group+:: restrict to nodes in {Cluster#preferred_server_group} # # @param [Integer, #in_milliseconds, nil] timeout # @param [Proc, nil] retry_strategy the custom retry strategy, if set @@ -1083,12 +1108,14 @@ class LookupInAllReplicas < Base # # @yieldparam [LookupInAllReplicas] self def initialize(transcoder: JsonTranscoder.new, + read_preference: :no_preference, timeout: nil, retry_strategy: nil, client_context: nil, parent_span: nil) super(timeout: timeout, retry_strategy: retry_strategy, client_context: client_context, parent_span: parent_span) @transcoder = transcoder + @read_preference = read_preference yield self if block_given? end @@ -1096,6 +1123,7 @@ def initialize(transcoder: JsonTranscoder.new, def to_backend { timeout: Utils::Time.extract_duration(@timeout), + read_preference: @read_preference, } end @@ -1646,6 +1674,8 @@ def initialize(get_options: Get.new, class Cluster attr_accessor :authenticator # @return [PasswordAuthenticator, CertificateAuthenticator] + attr_accessor :preferred_server_group # @return [String] + attr_accessor :enable_metrics # @return [Boolean] attr_accessor :metrics_emit_interval # @return [nil, Integer, #in_milliseconds] attr_accessor :enable_tracing # @return [Boolean] @@ -1679,6 +1709,7 @@ class Cluster # Creates an instance of options for {Couchbase::Cluster.connect} # # @param [PasswordAuthenticator, CertificateAuthenticator] authenticator + # @param [String] preferred_server_group the server group to use for replica APIs e.g. {Collection#get_all_replicas} # @param [nil, Integer, #in_milliseconds] key_value_timeout default timeout for Key/Value operations, e.g. {Collection#get} # @param [nil, Integer, #in_milliseconds] view_timeout default timeout for View query # @param [nil, Integer, #in_milliseconds] query_timeout default timeout for N1QL query @@ -1690,6 +1721,7 @@ class Cluster # # @yieldparam [Cluster] self def initialize(authenticator: nil, + preferred_server_group: nil, enable_metrics: nil, metrics_emit_interval: nil, enable_tracing: nil, @@ -1719,6 +1751,7 @@ def initialize(authenticator: nil, config_idle_redial_timeout: nil, idle_http_connection_timeout: nil) @authenticator = authenticator + @preferred_server_group = preferred_server_group @enable_metrics = enable_metrics @metrics_emit_interval = metrics_emit_interval @enable_tracing = enable_tracing @@ -1765,6 +1798,7 @@ def apply_profile(profile_name) def to_backend { enable_metrics: @enable_metrics, + preferred_server_group: @preferred_server_group, metrics_emit_interval: Utils::Time.extract_duration(@metrics_emit_interval), enable_tracing: @enable_tracing, orphaned_emit_interval: Utils::Time.extract_duration(@orphaned_emit_interval), @@ -2841,6 +2875,20 @@ def Scan(**args) Scan.new(**args) end + # Construct {LookupInAnyReplica} options for {Collection#lookup_in_any_replica} + # + # @return [LookupInAnyReplica] + def LookupInAnyReplica(**args) + LookupInAnyReplica.new(**args) + end + + # Construct {LookupInAllReplics} options for {Collection#lookup_in_all_replicas} + # + # @return [LookupInAllReplicas] + def LookupInAllReplicas(**args) + LookupInAllReplicas.new(**args) + end + # rubocop:enable Naming/MethodName end end diff --git a/lib/couchbase/version.rb b/lib/couchbase/version.rb index be73e732..0cb8a262 100644 --- a/lib/couchbase/version.rb +++ b/lib/couchbase/version.rb @@ -21,5 +21,5 @@ module Couchbase # $ ruby -rcouchbase -e 'pp Couchbase::VERSION' # {:sdk=>"3.4.0", :ruby_abi=>"3.1.0", :revision=>"416fe68e6029ec8a4c40611cf6e6b30d3b90d20f"} VERSION = {} unless defined?(VERSION) # rubocop:disable Style/MutableConstant - VERSION.update(:sdk => "3.5.4") + VERSION.update(:sdk => "3.5.5") end diff --git a/test/crud_test.rb b/test/crud_test.rb index 42672ae8..96823342 100644 --- a/test/crud_test.rb +++ b/test/crud_test.rb @@ -94,6 +94,28 @@ def test_reads_from_replica_does_not_exist end end + def test_replica_reads_from_preferred_server_group + skip("#{name}: CAVES does not server group replica reads") if use_caves? + skip("#{name}: Server does not support server group replica reads") unless env.server_version.supports_server_group_replica_reads? + skip("#{name}: The #{Couchbase::Protostellar::NAME} protocol does not support subdoc read from replica") if env.protostellar? + + doc_id = uniq_id(:foo) + document = {"value" => 42} + + @collection.upsert(doc_id, document) + + options = Options::GetAllReplicas(read_preference: :selected_server_group) + # The cluster here does not have the selected server group configured + assert_raises(Couchbase::Error::DocumentIrretrievable, "Document \"#{doc_id}\" should not be retrievable") do + @collection.get_all_replicas(doc_id, options) + end + + options = Options::GetAnyReplica(read_preference: :selected_server_group) + assert_raises(Couchbase::Error::DocumentIrretrievable, "Document \"#{doc_id}\" should not be retrievable") do + @collection.get_any_replica(doc_id, options) + end + end + def test_touch_sets_expiration document = {"value" => 42} doc_id = uniq_id(:foo) diff --git a/test/subdoc_test.rb b/test/subdoc_test.rb index 8b6b5e4f..0a13e1af 100644 --- a/test/subdoc_test.rb +++ b/test/subdoc_test.rb @@ -1491,6 +1491,31 @@ def test_lookup_in_all_replicas_bad_key end end + def test_lookup_in_replica_reads_from_preferred_server_group + skip("#{name}: CAVES does not support subdoc read from replica") if use_caves? + skip("#{name}: Server does not support server group replica reads") unless env.server_version.supports_server_group_replica_reads? + skip("#{name}: The #{Couchbase::Protostellar::NAME} protocol does not support subdoc read from replica") if env.protostellar? + + doc_id = uniq_id(:foo) + document = {"answer" => 42} + + @collection.upsert(doc_id, document) + + options = Options::LookupInAllReplicas(read_preference: :selected_server_group) + assert_raises(Error::DocumentIrretrievable) do + @collection.lookup_in_all_replicas(doc_id, [ + LookupInSpec.get("answer"), + ], options) + end + + options = Options::LookupInAnyReplica(read_preference: :selected_server_group) + assert_raises(Error::DocumentIrretrievable) do + @collection.lookup_in_any_replica(doc_id, [ + LookupInSpec.get("answer"), + ], options) + end + end + def test_lookup_in_path_invalid doc_id = uniq_id(:foo) document = {"value" => 42} diff --git a/test/test_helper.rb b/test/test_helper.rb index eae5c41c..c94c56a7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -136,6 +136,10 @@ def supports_vector_search? def supports_multiple_xattr_keys_mutation? trinity? end + + def supports_server_group_replica_reads? + @version >= Gem::Version.create("7.6.2") + end end require "couchbase"