typesense 0.2.0 → 0.5.3

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.
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # These examples walk you through operations to manage API Keys
5
+
6
+ require_relative './client_initialization'
7
+
8
+ # Let's setup some test data for this example
9
+ schema = {
10
+ 'name' => 'users',
11
+ 'fields' => [
12
+ {
13
+ 'name' => 'company_id',
14
+ 'type' => 'int32',
15
+ 'facet' => false
16
+ },
17
+ {
18
+ 'name' => 'user_name',
19
+ 'type' => 'string',
20
+ 'facet' => false
21
+ },
22
+ {
23
+ 'name' => 'login_count',
24
+ 'type' => 'int32',
25
+ 'facet' => false
26
+ },
27
+ {
28
+ 'name' => 'country',
29
+ 'type' => 'string',
30
+ 'facet' => true
31
+ }
32
+ ],
33
+ 'default_sorting_field' => 'company_id'
34
+ }
35
+
36
+ # We have four users, belonging to two companies: 124 and 126
37
+ documents = [
38
+ {
39
+ 'company_id' => 124,
40
+ 'user_name' => 'Hilary Bradford',
41
+ 'login_count' => 10,
42
+ 'country' => 'USA'
43
+ },
44
+ {
45
+ 'company_id' => 124,
46
+ 'user_name' => 'Nile Carty',
47
+ 'login_count' => 100,
48
+ 'country' => 'USA'
49
+ },
50
+ {
51
+ 'company_id' => 126,
52
+ 'user_name' => 'Tahlia Maxwell',
53
+ 'login_count' => 1,
54
+ 'country' => 'France'
55
+ },
56
+ {
57
+ 'company_id' => 126,
58
+ 'user_name' => 'Karl Roy',
59
+ 'login_count' => 2,
60
+ 'country' => 'Germany'
61
+ }
62
+ ]
63
+
64
+ # Delete if the collection already exists from a previous example run
65
+ begin
66
+ @typesense.collections['users'].delete
67
+ rescue Typesense::Error::ObjectNotFound
68
+ end
69
+
70
+ # create a collection
71
+ @typesense.collections.create(schema)
72
+
73
+ # Index documents
74
+ documents.each do |document|
75
+ @typesense.collections['users'].documents.create(document)
76
+ end
77
+
78
+ # Generate an API key and restrict it to only allow searches
79
+ # You want to use this API Key in the browser instead of the master API Key
80
+ unscoped_search_only_api_key_response = @typesense.keys.create({
81
+ 'description' => 'Search-only key.',
82
+ 'actions' => ['documents:search'],
83
+ 'collections' => ['*']
84
+ })
85
+ ap unscoped_search_only_api_key_response
86
+
87
+ # Save the key returned, since this will be the only time the full API Key is returned, for security purposes
88
+ unscoped_search_only_api_key = unscoped_search_only_api_key_response['value']
89
+
90
+ # Side note: you can also retrieve metadata of API keys using the ID returned in the above response
91
+ unscoped_search_only_api_key_response = @typesense.keys[unscoped_search_only_api_key_response['id']].retrieve
92
+ ap unscoped_search_only_api_key_response
93
+
94
+ # We'll now use this search-only API key to generate a scoped search API key that can only access documents that have company_id:124
95
+ # This is useful when you store multi-tenant data in a single Typesense server, but you only want
96
+ # a particular tenant to access their own data. You'd generate one scoped search key per tenant.
97
+ # IMPORTANT: scoped search keys should only be generated *server-side*, so as to not leak the unscoped main search key to clients
98
+ scoped_search_only_api_key = @typesense.keys.generate_scoped_search_key(unscoped_search_only_api_key, { 'filter_by': 'company_id:124' })
99
+ ap "scoped_search_only_api_key: #{scoped_search_only_api_key}"
100
+
101
+ # Now let's search the data using the scoped API Key for company_id:124
102
+ # You can do searches with this scoped_search_only_api_key from the server-side or client-side
103
+ scoped_typesense_client = Typesense::Client.new({
104
+ 'nodes': [{
105
+ 'host': 'localhost',
106
+ 'port': '8108',
107
+ 'protocol': 'http'
108
+ }],
109
+ 'api_key': scoped_search_only_api_key
110
+ })
111
+
112
+ search_results = scoped_typesense_client.collections['users'].documents.search({
113
+ 'q' => 'Hilary',
114
+ 'query_by' => 'user_name'
115
+ })
116
+ ap search_results
117
+
118
+ # Search for a user that exists, but is outside the current key's scope
119
+ search_results = scoped_typesense_client.collections['users'].documents.search({
120
+ 'q': 'Maxwell',
121
+ 'query_by': 'user_name'
122
+ })
123
+ ap search_results # Will return empty result set
124
+
125
+ # Now let's delete the unscoped_search_only_api_key. You'd want to do this when you need to rotate keys for example.
126
+ results = @typesense.keys[unscoped_search_only_api_key_response['id']].delete
127
+ ap results
@@ -3,30 +3,9 @@
3
3
  ##
4
4
  # These examples walk you through operations specifically related to overrides
5
5
  # This is a Typesense Premium feature (see: https://typesense.org/premium)
6
+ # Be sure to add `--license-key=<>` as a parameter, when starting a Typesense Premium server
6
7
 
7
- require_relative '../lib/typesense'
8
- require 'awesome_print'
9
-
10
- AwesomePrint.defaults = {
11
- indent: -2
12
- }
13
-
14
- ##
15
- # Setup
16
- #
17
- # Start the master
18
- # $ docker run -p 8108:8108 -it -v/tmp/typesense-data-master/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=abcd --listen-port 8108 --license-key=<>
19
-
20
- ##
21
- # Create a client
22
- typesense = Typesense::Client.new(
23
- master_node: {
24
- host: 'localhost',
25
- port: 8108,
26
- protocol: 'http',
27
- api_key: 'abcd'
28
- }
29
- )
8
+ require_relative './client_initialization'
30
9
 
31
10
  ##
32
11
  # Create a collection
@@ -50,31 +29,31 @@ schema = {
50
29
  'default_sorting_field' => 'num_employees'
51
30
  }
52
31
 
53
- typesense.collections.create(schema)
32
+ @typesense.collections.create(schema)
54
33
 
55
34
  # Let's create a couple documents for us to use in our search examples
56
- typesense.collections['companies'].documents.create(
35
+ @typesense.collections['companies'].documents.create(
57
36
  'id' => '124',
58
37
  'company_name' => 'Stark Industries',
59
38
  'num_employees' => 5215,
60
39
  'country' => 'USA'
61
40
  )
62
41
 
63
- typesense.collections['companies'].documents.create(
42
+ @typesense.collections['companies'].documents.create(
64
43
  'id' => '127',
65
44
  'company_name' => 'Stark Corp',
66
45
  'num_employees' => 1031,
67
46
  'country' => 'USA'
68
47
  )
69
48
 
70
- typesense.collections['companies'].documents.create(
49
+ @typesense.collections['companies'].documents.create(
71
50
  'id' => '125',
72
51
  'company_name' => 'Acme Corp',
73
52
  'num_employees' => 1002,
74
53
  'country' => 'France'
75
54
  )
76
55
 
77
- typesense.collections['companies'].documents.create(
56
+ @typesense.collections['companies'].documents.create(
78
57
  'id' => '126',
79
58
  'company_name' => 'Doofenshmirtz Inc',
80
59
  'num_employees' => 2,
@@ -84,7 +63,7 @@ typesense.collections['companies'].documents.create(
84
63
  ##
85
64
  # Create overrides
86
65
 
87
- typesense.collections['companies'].overrides.create(
66
+ @typesense.collections['companies'].overrides.create(
88
67
  "id": 'promote-doofenshmirtz',
89
68
  "rule": {
90
69
  "query": 'doofen',
@@ -92,7 +71,7 @@ typesense.collections['companies'].overrides.create(
92
71
  },
93
72
  "includes": [{ 'id' => '126', 'position' => 1 }]
94
73
  )
95
- typesense.collections['companies'].overrides.create(
74
+ @typesense.collections['companies'].overrides.create(
96
75
  "id": 'promote-acme',
97
76
  "rule": {
98
77
  "query": 'stark',
@@ -103,19 +82,19 @@ typesense.collections['companies'].overrides.create(
103
82
 
104
83
  ##
105
84
  # Search for documents
106
- results = typesense.collections['companies'].documents.search(
85
+ results = @typesense.collections['companies'].documents.search(
107
86
  'q' => 'doofen',
108
87
  'query_by' => 'company_name'
109
88
  )
110
89
  ap results
111
90
 
112
- results = typesense.collections['companies'].documents.search(
91
+ results = @typesense.collections['companies'].documents.search(
113
92
  'q' => 'stark',
114
93
  'query_by' => 'company_name'
115
94
  )
116
95
  ap results
117
96
 
118
- results = typesense.collections['companies'].documents.search(
97
+ results = @typesense.collections['companies'].documents.search(
119
98
  'q' => 'Inc',
120
99
  'query_by' => 'company_name',
121
100
  'filter_by' => 'num_employees:<100',
@@ -126,4 +105,4 @@ ap results
126
105
  ##
127
106
  # Cleanup
128
107
  # Drop the collection
129
- typesense.collections['companies'].delete
108
+ @typesense.collections['companies'].delete
@@ -3,41 +3,7 @@
3
3
  ##
4
4
  # These examples walk you through operations specifically related to search
5
5
 
6
- require_relative '../lib/typesense'
7
- require 'awesome_print'
8
-
9
- AwesomePrint.defaults = {
10
- indent: -2
11
- }
12
-
13
- ##
14
- # Setup
15
- #
16
- # Start the master
17
- # $ docker run -p 8108:8108 -it -v/tmp/typesense-data-master/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=abcd --listen-port 8108
18
- #
19
- # Start the read replica
20
- # $ docker run -p 8109:8109 -it -v/tmp/typesense-data-read-replica-1/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=wxyz --listen-port 8109 --master http://localhost:8108
21
-
22
- ##
23
- # Create a client
24
- typesense = Typesense::Client.new(
25
- master_node: {
26
- host: 'localhost',
27
- port: 8108,
28
- protocol: 'http',
29
- api_key: 'abcd'
30
- },
31
- read_replica_nodes: [
32
- {
33
- host: 'localhost',
34
- port: 8109,
35
- protocol: 'http',
36
- api_key: 'wxyz'
37
- }
38
- ],
39
- timeout_seconds: 10
40
- )
6
+ require_relative './client_initialization'
41
7
 
42
8
  ##
43
9
  # Create a collection
@@ -61,31 +27,38 @@ schema = {
61
27
  'default_sorting_field' => 'num_employees'
62
28
  }
63
29
 
64
- typesense.collections.create(schema)
30
+ # Delete the collection if it already exists
31
+ begin
32
+ @typesense.collections['companies'].delete
33
+ rescue Typesense::Error::ObjectNotFound
34
+ end
35
+
36
+ # Now create the collection
37
+ @typesense.collections.create(schema)
65
38
 
66
39
  # Let's create a couple documents for us to use in our search examples
67
- typesense.collections['companies'].documents.create(
40
+ @typesense.collections['companies'].documents.create(
68
41
  'id' => '124',
69
42
  'company_name' => 'Stark Industries',
70
43
  'num_employees' => 5215,
71
44
  'country' => 'USA'
72
45
  )
73
46
 
74
- typesense.collections['companies'].documents.create(
47
+ @typesense.collections['companies'].documents.create(
75
48
  'id' => '127',
76
49
  'company_name' => 'Stark Corp',
77
50
  'num_employees' => 1031,
78
51
  'country' => 'USA'
79
52
  )
80
53
 
81
- typesense.collections['companies'].documents.create(
54
+ @typesense.collections['companies'].documents.create(
82
55
  'id' => '125',
83
56
  'company_name' => 'Acme Corp',
84
57
  'num_employees' => 1002,
85
58
  'country' => 'France'
86
59
  )
87
60
 
88
- typesense.collections['companies'].documents.create(
61
+ @typesense.collections['companies'].documents.create(
89
62
  'id' => '126',
90
63
  'company_name' => 'Doofenshmirtz Inc',
91
64
  'num_employees' => 2,
@@ -94,7 +67,7 @@ typesense.collections['companies'].documents.create(
94
67
 
95
68
  ##
96
69
  # Search for documents
97
- results = typesense.collections['companies'].documents.search(
70
+ results = @typesense.collections['companies'].documents.search(
98
71
  'q' => 'Stark',
99
72
  'query_by' => 'company_name'
100
73
  )
@@ -133,7 +106,7 @@ ap results
133
106
 
134
107
  ##
135
108
  # Search for more documents
136
- results = typesense.collections['companies'].documents.search(
109
+ results = @typesense.collections['companies'].documents.search(
137
110
  'q' => 'Inc',
138
111
  'query_by' => 'company_name',
139
112
  'filter_by' => 'num_employees:<100',
@@ -163,7 +136,7 @@ ap results
163
136
 
164
137
  ##
165
138
  # Search for more documents
166
- results = typesense.collections['companies'].documents.search(
139
+ results = @typesense.collections['companies'].documents.search(
167
140
  'q' => 'Non-existent',
168
141
  'query_by' => 'company_name'
169
142
  )
@@ -179,4 +152,4 @@ ap results
179
152
  ##
180
153
  # Cleanup
181
154
  # Drop the collection
182
- typesense.collections['companies'].delete
155
+ @typesense.collections['companies'].delete
@@ -15,5 +15,9 @@ require_relative 'typesense/overrides'
15
15
  require_relative 'typesense/override'
16
16
  require_relative 'typesense/aliases'
17
17
  require_relative 'typesense/alias'
18
+ require_relative 'typesense/keys'
19
+ require_relative 'typesense/key'
18
20
  require_relative 'typesense/debug'
21
+ require_relative 'typesense/health'
22
+ require_relative 'typesense/metrics'
19
23
  require_relative 'typesense/error'
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Typesense
4
4
  class Alias
5
- def initialize(configuration, name)
6
- @configuration = configuration
7
- @name = name
5
+ def initialize(name, api_call)
6
+ @name = name
7
+ @api_call = api_call
8
8
  end
9
9
 
10
10
  def retrieve
11
- ApiCall.new(@configuration).get(endpoint_path)
11
+ @api_call.get(endpoint_path)
12
12
  end
13
13
 
14
14
  def delete
15
- ApiCall.new(@configuration).delete(endpoint_path)
15
+ @api_call.delete(endpoint_path)
16
16
  end
17
17
 
18
18
  private
@@ -4,21 +4,21 @@ module Typesense
4
4
  class Aliases
5
5
  RESOURCE_PATH = '/aliases'
6
6
 
7
- def initialize(configuration)
8
- @configuration = configuration
7
+ def initialize(api_call)
8
+ @api_call = api_call
9
9
  @aliases = {}
10
10
  end
11
11
 
12
12
  def upsert(alias_name, mapping)
13
- ApiCall.new(@configuration).put(endpoint_path(alias_name), mapping)
13
+ @api_call.put(endpoint_path(alias_name), mapping)
14
14
  end
15
15
 
16
16
  def retrieve
17
- ApiCall.new(@configuration).get(RESOURCE_PATH)
17
+ @api_call.get(RESOURCE_PATH)
18
18
  end
19
19
 
20
20
  def [](alias_name)
21
- @aliases[alias_name] ||= Alias.new(@configuration, alias_name)
21
+ @aliases[alias_name] ||= Alias.new(alias_name, @api_call)
22
22
  end
23
23
 
24
24
  private
@@ -10,116 +10,224 @@ module Typesense
10
10
 
11
11
  def initialize(configuration)
12
12
  @configuration = configuration
13
+
14
+ @api_key = @configuration.api_key
15
+ @nodes = @configuration.nodes.dup # Make a copy, since we'll be adding additional metadata to the nodes
16
+ @nearest_node = @configuration.nearest_node.dup
17
+ @connection_timeout_seconds = @configuration.connection_timeout_seconds
18
+ @healthcheck_interval_seconds = @configuration.healthcheck_interval_seconds
19
+ @num_retries_per_request = @configuration.num_retries
20
+ @retry_interval_seconds = @configuration.retry_interval_seconds
21
+
22
+ @logger = @configuration.logger
23
+
24
+ initialize_metadata_for_nodes
25
+ @current_node_index = -1
13
26
  end
14
27
 
15
28
  def post(endpoint, parameters = {})
16
- perform_with_error_handling(:do_not_use_read_replicas) do
17
- self.class.post(uri_for(endpoint),
18
- default_options.merge(
19
- body: parameters.to_json,
20
- headers: default_headers.merge('Content-Type' => 'application/json')
21
- ))
22
- end.parsed_response
29
+ headers, body = extract_headers_and_body_from(parameters)
30
+
31
+ perform_request :post,
32
+ endpoint,
33
+ body: body,
34
+ headers: default_headers.merge(headers)
23
35
  end
24
36
 
25
37
  def put(endpoint, parameters = {})
26
- perform_with_error_handling(:do_not_use_read_replicas) do
27
- self.class.put(uri_for(endpoint),
28
- default_options.merge(
29
- body: parameters.to_json,
30
- headers: default_headers.merge('Content-Type' => 'application/json')
31
- ))
32
- end.parsed_response
38
+ headers, body = extract_headers_and_body_from(parameters)
39
+
40
+ perform_request :put,
41
+ endpoint,
42
+ body: body,
43
+ headers: default_headers.merge(headers)
33
44
  end
34
45
 
35
46
  def get(endpoint, parameters = {})
36
- get_unparsed_response(endpoint, parameters).parsed_response
37
- end
47
+ headers, query = extract_headers_and_query_from(parameters)
38
48
 
39
- def get_unparsed_response(endpoint, parameters = {})
40
- perform_with_error_handling(:use_read_replicas) do |node, node_index|
41
- self.class.get(uri_for(endpoint, node, node_index),
42
- default_options.merge(
43
- query: parameters,
44
- headers: default_headers
45
- ))
46
- end
49
+ perform_request :get,
50
+ endpoint,
51
+ query: query,
52
+ headers: default_headers.merge(headers)
47
53
  end
48
54
 
49
55
  def delete(endpoint, parameters = {})
50
- perform_with_error_handling(:do_not_use_read_replicas) do
51
- self.class.delete(uri_for(endpoint),
52
- default_options.merge(
53
- query: parameters,
54
- headers: default_headers
55
- ))
56
- end.parsed_response
56
+ headers, query = extract_headers_and_query_from(parameters)
57
+
58
+ perform_request :delete,
59
+ endpoint,
60
+ query: query,
61
+ headers: default_headers.merge(headers)
62
+ end
63
+
64
+ def perform_request(method, endpoint, options = {})
65
+ @configuration.validate!
66
+ last_exception = nil
67
+ @logger.debug "Performing #{method.to_s.upcase} request: #{endpoint}"
68
+ (1..(@num_retries_per_request + 1)).each do |num_tries|
69
+ node = next_node
70
+
71
+ @logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"
72
+
73
+ begin
74
+ response_object = self.class.send(method,
75
+ uri_for(endpoint, node),
76
+ default_options.merge(options))
77
+ response_code = response_object.response.code.to_i
78
+ set_node_healthcheck(node, is_healthy: true) if response_code >= 1 && response_code <= 499
79
+
80
+ @logger.debug "Request to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response_code}."
81
+
82
+ # If response is 2xx return the object, else raise the response as an exception
83
+ return response_object.parsed_response if response_object.response.code_type <= Net::HTTPSuccess # 2xx
84
+
85
+ exception_message = (response_object.parsed_response && response_object.parsed_response['message']) || 'Error'
86
+ raise custom_exception_klass_for(response_object.response), exception_message
87
+ rescue Net::ReadTimeout, Net::OpenTimeout,
88
+ EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
89
+ Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
90
+ Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
91
+ Timeout::Error, HTTParty::ResponseError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
92
+ # Rescue network layer exceptions and HTTP 5xx errors, so the loop can continue.
93
+ # Using loops for retries instead of rescue...retry to maintain consistency with client libraries in
94
+ # other languages that might not support the same construct.
95
+ set_node_healthcheck(node, is_healthy: false)
96
+ last_exception = e
97
+ @logger.warn "Request to Node #{node[:index]} failed due to \"#{e.class}: #{e.message}\""
98
+ @logger.warn "Sleeping for #{@retry_interval_seconds}s and then retrying request..."
99
+ sleep @retry_interval_seconds
100
+ end
101
+ end
102
+ @logger.debug "No retries left. Raising last error \"#{last_exception.class}: #{last_exception.message}\"..."
103
+ raise last_exception
57
104
  end
58
105
 
59
106
  private
60
107
 
61
- def uri_for(endpoint, node = :master, node_index = 0)
62
- if node == :read_replica
63
- "#{@configuration.read_replica_nodes[node_index][:protocol]}://#{@configuration.read_replica_nodes[node_index][:host]}:#{@configuration.read_replica_nodes[node_index][:port]}#{endpoint}"
108
+ def extract_headers_and_body_from(parameters)
109
+ if json_request?(parameters)
110
+ headers = { 'Content-Type' => 'application/json' }
111
+ body = sanitize_parameters(parameters).to_json
64
112
  else
65
- "#{@configuration.master_node[:protocol]}://#{@configuration.master_node[:host]}:#{@configuration.master_node[:port]}#{endpoint}"
113
+ headers = {}
114
+ body = parameters[:body]
66
115
  end
116
+ [headers, body]
67
117
  end
68
118
 
69
- def perform_with_error_handling(use_read_replicas = :do_not_use_read_replicas)
70
- @configuration.validate!
119
+ def extract_headers_and_query_from(parameters)
120
+ if json_request?(parameters)
121
+ headers = { 'Content-Type' => 'application/json' }
122
+ query = sanitize_parameters(parameters)
123
+ else
124
+ headers = {}
125
+ query = parameters[:query]
126
+ end
127
+ [headers, query]
128
+ end
129
+
130
+ def json_request?(parameters)
131
+ parameters[:as_json].nil? ? true : parameters[:as_json]
132
+ end
133
+
134
+ def sanitize_parameters(parameters)
135
+ sanitized_parameters = parameters.dup
136
+ sanitized_parameters.delete(:as_json)
137
+ sanitized_parameters.delete(:body)
138
+ sanitized_parameters.delete(:query)
71
139
 
72
- node = :master
73
- node_index = -1
74
-
75
- begin
76
- response_object = yield node, node_index
77
-
78
- return response_object if response_object.response.code_type <= Net::HTTPSuccess # 2xx
79
-
80
- error_klass = if response_object.response.code_type <= Net::HTTPBadRequest # 400
81
- Error::RequestMalformed
82
- elsif response_object.response.code_type <= Net::HTTPUnauthorized # 401
83
- Error::RequestUnauthorized
84
- elsif response_object.response.code_type <= Net::HTTPNotFound # 404
85
- Error::ObjectNotFound
86
- elsif response_object.response.code_type <= Net::HTTPConflict # 409
87
- Error::ObjectAlreadyExists
88
- elsif response_object.response.code_type <= Net::HTTPUnprocessableEntity # 422
89
- Error::ObjectUnprocessable
90
- elsif response_object.response.code_type <= Net::HTTPServerError # 5xx
91
- Error::ServerError
92
- else
93
- Error
94
- end
95
-
96
- raise error_klass, response_object.parsed_response['message']
97
- rescue Net::ReadTimeout, Net::OpenTimeout,
98
- EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
99
- Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
100
- Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
101
- Timeout::Error, Error::ServerError, HTTParty::ResponseError
102
- if ([:use_read_replicas, true].include? use_read_replicas) &&
103
- !@configuration.read_replica_nodes.nil?
104
- node = :read_replica
105
- node_index += 1
106
-
107
- retry unless @configuration.read_replica_nodes[node_index].nil?
140
+ sanitized_parameters
141
+ end
142
+
143
+ def uri_for(endpoint, node)
144
+ "#{node[:protocol]}://#{node[:host]}:#{node[:port]}#{endpoint}"
145
+ end
146
+
147
+ ## Attempts to find the next healthy node, looping through the list of nodes once.
148
+ # But if no healthy nodes are found, it will just return the next node, even if it's unhealthy
149
+ # so we can try the request for good measure, in case that node has become healthy since
150
+ def next_node
151
+ # Check if nearest_node is set and is healthy, if so return it
152
+ unless @nearest_node.nil?
153
+ @logger.debug "Nodes health: Node #{@nearest_node[:index]} is #{@nearest_node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}"
154
+ if @nearest_node[:is_healthy] == true || node_due_for_healthcheck?(@nearest_node)
155
+ @logger.debug "Updated current node to Node #{@nearest_node[:index]}"
156
+ return @nearest_node
157
+ end
158
+ @logger.debug 'Falling back to individual nodes'
159
+ end
160
+
161
+ # Fallback to nodes as usual
162
+ @logger.debug "Nodes health: #{@nodes.each_with_index.map { |node, i| "Node #{i} is #{node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}" }.join(' || ')}"
163
+ candidate_node = nil
164
+ (0..@nodes.length).each do |_i|
165
+ @current_node_index = (@current_node_index + 1) % @nodes.length
166
+ candidate_node = @nodes[@current_node_index]
167
+ if candidate_node[:is_healthy] == true || node_due_for_healthcheck?(candidate_node)
168
+ @logger.debug "Updated current node to Node #{candidate_node[:index]}"
169
+ return candidate_node
108
170
  end
171
+ end
172
+
173
+ # None of the nodes are marked healthy, but some of them could have become healthy since last health check.
174
+ # So we will just return the next node.
175
+ @logger.debug "No healthy nodes were found. Returning the next node, Node #{candidate_node[:index]}"
176
+ candidate_node
177
+ end
109
178
 
110
- raise
179
+ def node_due_for_healthcheck?(node)
180
+ is_due_for_check = Time.now.to_i - node[:last_access_timestamp] > @healthcheck_interval_seconds
181
+ @logger.debug "Node #{node[:index]} has exceeded healthcheck_interval_seconds of #{@healthcheck_interval_seconds}. Adding it back into rotation." if is_due_for_check
182
+ is_due_for_check
183
+ end
184
+
185
+ def initialize_metadata_for_nodes
186
+ unless @nearest_node.nil?
187
+ @nearest_node[:index] = 'nearest_node'
188
+ set_node_healthcheck(@nearest_node, is_healthy: true)
189
+ end
190
+ @nodes.each_with_index do |node, index|
191
+ node[:index] = index
192
+ set_node_healthcheck(node, is_healthy: true)
193
+ end
194
+ end
195
+
196
+ def set_node_healthcheck(node, is_healthy:)
197
+ node[:is_healthy] = is_healthy
198
+ node[:last_access_timestamp] = Time.now.to_i
199
+ end
200
+
201
+ def custom_exception_klass_for(response)
202
+ response_code_type = response.code_type
203
+ if response_code_type <= Net::HTTPBadRequest # 400
204
+ Typesense::Error::RequestMalformed
205
+ elsif response_code_type <= Net::HTTPUnauthorized # 401
206
+ Typesense::Error::RequestUnauthorized
207
+ elsif response_code_type <= Net::HTTPNotFound # 404
208
+ Typesense::Error::ObjectNotFound
209
+ elsif response_code_type <= Net::HTTPConflict # 409
210
+ Typesense::Error::ObjectAlreadyExists
211
+ elsif response_code_type <= Net::HTTPUnprocessableEntity # 422
212
+ Typesense::Error::ObjectUnprocessable
213
+ elsif response_code_type <= Net::HTTPServerError # 5xx
214
+ Typesense::Error::ServerError
215
+ elsif response.code.to_i.zero?
216
+ Typesense::Error::HTTPStatus0Error
217
+ else
218
+ Typesense::Error::HTTPError
111
219
  end
112
220
  end
113
221
 
114
222
  def default_options
115
223
  {
116
- timeout: @configuration.timeout_seconds
224
+ timeout: @connection_timeout_seconds
117
225
  }
118
226
  end
119
227
 
120
228
  def default_headers
121
229
  {
122
- API_KEY_HEADER_NAME.to_s => @configuration.master_node[:api_key]
230
+ API_KEY_HEADER_NAME.to_s => @api_key
123
231
  }
124
232
  end
125
233
  end