tarantool-client 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +79 -1
- data/lib/tarantool/client/batch_delete.rb +97 -0
- data/lib/tarantool/client/configuration.rb +20 -3
- data/lib/tarantool/client/diagnostics.rb +20 -0
- data/lib/tarantool/client/errors.rb +8 -1
- data/lib/tarantool/client/fork_safety.rb +2 -1
- data/lib/tarantool/client/instrumentation/log_instrumenter.rb +35 -0
- data/lib/tarantool/client/instrumentation/log_lines.rb +66 -0
- data/lib/tarantool/client/instrumentation/request_payload.rb +42 -0
- data/lib/tarantool/client/instrumentation.rb +51 -0
- data/lib/tarantool/client/instrumented.rb +42 -0
- data/lib/tarantool/client/named_requests.rb +67 -0
- data/lib/tarantool/client/operations.rb +53 -21
- data/lib/tarantool/client/pool/member.rb +4 -9
- data/lib/tarantool/client/pool/member_fork_safety.rb +10 -5
- data/lib/tarantool/client/pool/member_role.rb +36 -0
- data/lib/tarantool/client/row.rb +8 -0
- data/lib/tarantool/client/schema.rb +4 -3
- data/lib/tarantool/client/space_info.rb +2 -0
- data/lib/tarantool/client/stream.rb +20 -10
- data/lib/tarantool/client/transaction_schema.rb +12 -8
- data/lib/tarantool/client/transport/connection.rb +2 -0
- data/lib/tarantool/client/transport/link.rb +15 -15
- data/lib/tarantool/client/transport/link_events.rb +41 -0
- data/lib/tarantool/client/transport/link_state.rb +10 -0
- data/lib/tarantool/client/tuple_mapping.rb +65 -0
- data/lib/tarantool/client/tuple_shape.rb +25 -0
- data/lib/tarantool/client/version.rb +1 -1
- data/lib/tarantool/client.rb +23 -48
- metadata +15 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Requests whose space and index resolve through the schema cache. Includers set @link, @schema_cache and
|
|
6
|
+
# @config, and provide ensure_fork_safe and mark_schema_reloaded.
|
|
7
|
+
# @api private
|
|
8
|
+
module NamedRequests
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Every schema fetch and attempt draws on one deadline, so the whole call stays within timeout.
|
|
12
|
+
# Waiting for a connection first means the cache already holds its schema or a newer one.
|
|
13
|
+
def perform_by_name(space, index, timeout, payload, &)
|
|
14
|
+
ensure_fork_safe
|
|
15
|
+
deadline = Transport::Deadline.after(timeout)
|
|
16
|
+
@link.connection(deadline)
|
|
17
|
+
|
|
18
|
+
perform_with_schema_check(space, index, deadline, payload, &)
|
|
19
|
+
rescue TimeoutError
|
|
20
|
+
raise TimeoutError, "no response within #{timeout}s"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lookup_space(space, timeout)
|
|
24
|
+
ensure_fork_safe
|
|
25
|
+
timeout ||= @config.request_timeout
|
|
26
|
+
deadline = Transport::Deadline.after(timeout)
|
|
27
|
+
@link.connection(deadline)
|
|
28
|
+
|
|
29
|
+
@schema_cache.space_info(space, deadline)
|
|
30
|
+
rescue TimeoutError
|
|
31
|
+
raise TimeoutError, "no response within #{timeout}s"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def perform_with_schema_check(space, index, deadline, payload, &)
|
|
35
|
+
schema, ids = resolve_name(space, index, deadline, payload)
|
|
36
|
+
|
|
37
|
+
perform_built(schema, ids, deadline, &)
|
|
38
|
+
rescue ServerError, UnknownFieldError => e
|
|
39
|
+
raise if e.is_a?(ServerError) && e.code != Protocol::Constants::ER_WRONG_SCHEMA_VERSION
|
|
40
|
+
|
|
41
|
+
mark_schema_reloaded(payload)
|
|
42
|
+
fresh = @schema_cache.reload(schema, timeout: Transport::Deadline.remaining(deadline))
|
|
43
|
+
schema, ids = resolve_name(space, index, deadline, payload, fresh)
|
|
44
|
+
perform_built(schema, ids, deadline, &)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def perform_built(schema, ids, deadline)
|
|
48
|
+
perform_resolved(schema, yield(*ids, schema.space_info(ids.first)), deadline)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The cache returns another schema only when resolving had to reload it.
|
|
52
|
+
def resolve_name(space, index, deadline, payload, schema = @schema_cache.schema)
|
|
53
|
+
@schema_cache.resolve(space, index, deadline, schema).tap do |resolved, _ids|
|
|
54
|
+
mark_schema_reloaded(payload) unless resolved.equal?(schema)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Only the connection the schema came from: once it is lost, another instance may reuse the
|
|
59
|
+
# schema version with other ids, so the request fails with it rather than wait for a reconnect.
|
|
60
|
+
def perform_resolved(schema, request, deadline)
|
|
61
|
+
timeout = Transport::Deadline.remaining(deadline)
|
|
62
|
+
|
|
63
|
+
schema.connection.perform(request, timeout:, schema_version: schema.version)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
module Tarantool
|
|
4
4
|
class Client
|
|
5
5
|
# Data operations, call, eval and SQL. Includers implement `perform(request, timeout:, on_push: nil)`,
|
|
6
|
-
# `perform_on_space(space, index, timeout:) { |space_id, index_id| request }`, which
|
|
7
|
-
# the request built by the block, and `lookup_space(space, timeout)`, which returns its
|
|
6
|
+
# `perform_on_space(space, index, timeout:, format: false) { |space_id, index_id, space_info| request }`, which
|
|
7
|
+
# returns the response to the request built by the block, and `lookup_space(space, timeout)`, which returns its
|
|
8
|
+
# SpaceInfo. With `format: true` the block gets the SpaceInfo of the schema the request is resolved and
|
|
9
|
+
# version-checked against, even for numeric ids; otherwise the third argument may be absent.
|
|
8
10
|
# @api private
|
|
9
11
|
module Operations
|
|
10
12
|
def ping(timeout: nil)
|
|
@@ -18,42 +20,57 @@ module Tarantool
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def select(space, key = [], index: 0, limit: Protocol::Constants::MAX_LIMIT, offset: 0, iterator: :eq,
|
|
21
|
-
after: nil, timeout: nil)
|
|
22
|
-
|
|
23
|
+
after: nil, as: :tuple, timeout: nil)
|
|
24
|
+
on_space(space, index, as:, timeout:) do |space_id, index_id|
|
|
23
25
|
Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after:)
|
|
24
|
-
end
|
|
26
|
+
end
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def insert(space, tuple, timeout: nil)
|
|
28
|
-
|
|
29
|
-
Protocol::Requests::Insert.new(space_id:, tuple:)
|
|
30
|
-
end.
|
|
29
|
+
def insert(space, tuple, as: :tuple, timeout: nil)
|
|
30
|
+
on_space(space, 0, as:, timeout:, format: tuple.is_a?(Hash)) do |space_id, _index_id, space_info|
|
|
31
|
+
Protocol::Requests::Insert.new(space_id:, tuple: tuple_for(tuple, space_info))
|
|
32
|
+
end.first
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def replace(space, tuple, timeout: nil)
|
|
34
|
-
|
|
35
|
-
Protocol::Requests::Replace.new(space_id:, tuple:)
|
|
36
|
-
end.
|
|
35
|
+
def replace(space, tuple, as: :tuple, timeout: nil)
|
|
36
|
+
on_space(space, 0, as:, timeout:, format: tuple.is_a?(Hash)) do |space_id, _index_id, space_info|
|
|
37
|
+
Protocol::Requests::Replace.new(space_id:, tuple: tuple_for(tuple, space_info))
|
|
38
|
+
end.first
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
def update(space, key, operations, index: 0, timeout: nil)
|
|
40
|
-
|
|
41
|
+
def update(space, key, operations, index: 0, as: :tuple, timeout: nil)
|
|
42
|
+
on_space(space, index, as:, timeout:) do |space_id, index_id|
|
|
41
43
|
Protocol::Requests::Update.new(space_id:, index_id:, key: key_tuple(key), operations:)
|
|
42
|
-
end.
|
|
44
|
+
end.first
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def upsert(space, tuple, operations, timeout: nil)
|
|
46
|
-
perform_on_space(space, 0, timeout:) do |space_id|
|
|
47
|
-
Protocol::Requests::Upsert.new(space_id:, tuple
|
|
48
|
+
perform_on_space(space, 0, timeout:, format: tuple.is_a?(Hash)) do |space_id, _index_id, space_info|
|
|
49
|
+
Protocol::Requests::Upsert.new(space_id:, tuple: tuple_for(tuple, space_info), operations:)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
nil
|
|
51
53
|
end
|
|
52
54
|
|
|
53
|
-
def delete(space, key, index: 0, timeout: nil)
|
|
54
|
-
|
|
55
|
+
def delete(space, key, index: 0, as: :tuple, timeout: nil)
|
|
56
|
+
on_space(space, index, as:, timeout:) do |space_id, index_id|
|
|
55
57
|
Protocol::Requests::Delete.new(space_id:, index_id:, key: key_tuple(key))
|
|
56
|
-
end.
|
|
58
|
+
end.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Deletes every tuple `select(space, key, index:, iterator:)` finds, which IPROTO DELETE can't do on a
|
|
62
|
+
# non-unique index: selects batch_size tuples at a time, continuing past the last one with `after:`, and
|
|
63
|
+
# deletes each by primary key. Returns the number of tuples deleted. TREE indexes only. Multikey and
|
|
64
|
+
# functional indexes are not supported either: tuple-based `after` raises ServerError there, before
|
|
65
|
+
# deleting anything.
|
|
66
|
+
#
|
|
67
|
+
# Outside a transaction it is not atomic: on failure the tuples deleted so far stay deleted, and the
|
|
68
|
+
# raised Tarantool::Error carries the number confirmed deleted in #deleted (a lower bound). Tuples
|
|
69
|
+
# inserted meanwhile behind the cursor are not deleted. Inside `transaction { |tx| tx.delete_by(...) }`
|
|
70
|
+
# it is atomic (memtx needs memtx_use_mvcc_engine). `timeout:` bounds the whole operation; nil gives
|
|
71
|
+
# each request request_timeout.
|
|
72
|
+
def delete_by(space, key, index:, iterator: :eq, batch_size: 1000, timeout: nil)
|
|
73
|
+
BatchDelete.new(self, space, key, index:, iterator:, batch_size:, timeout:).call
|
|
57
74
|
end
|
|
58
75
|
|
|
59
76
|
# on_push runs in the calling thread for each box.session.push message; an exception
|
|
@@ -81,6 +98,21 @@ module Tarantool
|
|
|
81
98
|
|
|
82
99
|
private
|
|
83
100
|
|
|
101
|
+
# A retry after a schema change calls the block again, so the SpaceInfo kept is the one the response came from.
|
|
102
|
+
def on_space(space, index, as:, timeout:, format: false)
|
|
103
|
+
TupleShape.check(as)
|
|
104
|
+
space_info = nil
|
|
105
|
+
needs_format = format || as != :tuple
|
|
106
|
+
response = perform_on_space(space, index, timeout:, format: needs_format) do |space_id, index_id, info|
|
|
107
|
+
space_info = info
|
|
108
|
+
yield space_id, index_id, info
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
response.data.map { |tuple| TupleShape.apply(as, tuple, space_info) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def tuple_for(tuple, space_info) = tuple.is_a?(Hash) ? space_info.to_tuple(tuple) : tuple
|
|
115
|
+
|
|
84
116
|
def key_tuple(key)
|
|
85
117
|
key.is_a?(Array) ? key : [key]
|
|
86
118
|
end
|
|
@@ -8,11 +8,13 @@ module Tarantool
|
|
|
8
8
|
# @api private
|
|
9
9
|
class Member
|
|
10
10
|
include Pool::MemberForkSafety
|
|
11
|
+
include Pool::MemberRole
|
|
11
12
|
|
|
12
13
|
def initialize(uri, options)
|
|
13
14
|
@uri = uri
|
|
14
15
|
@options = options
|
|
15
16
|
@config = Configuration.build(uri, **options)
|
|
17
|
+
@instrumentation = Instrumentation.build(@config)
|
|
16
18
|
@lock = Mutex.new
|
|
17
19
|
@changed = ConditionVariable.new
|
|
18
20
|
@closed = false
|
|
@@ -75,13 +77,6 @@ module Tarantool
|
|
|
75
77
|
client.close if closed
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
def update(status)
|
|
79
|
-
@lock.synchronize do
|
|
80
|
-
@read_only = status['is_ro']
|
|
81
|
-
@changed.broadcast
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
80
|
def wait_for_status
|
|
86
81
|
deadline = Transport::Deadline.after(@config.connect_timeout)
|
|
87
82
|
|
|
@@ -115,7 +110,7 @@ module Tarantool
|
|
|
115
110
|
attempt += 1
|
|
116
111
|
end
|
|
117
112
|
rescue StandardError => e
|
|
118
|
-
warn "tarantool pool: gave up connecting to #{address}: #{e.class}: #{e.message}"
|
|
113
|
+
Diagnostics.warn(@config.logger, "tarantool pool: gave up connecting to #{address}: #{e.class}: #{e.message}")
|
|
119
114
|
end
|
|
120
115
|
|
|
121
116
|
def try_connect
|
|
@@ -125,7 +120,7 @@ module Tarantool
|
|
|
125
120
|
rescue Error
|
|
126
121
|
false
|
|
127
122
|
rescue StandardError => e
|
|
128
|
-
warn "tarantool pool: connecting to #{address} failed: #{e.class}: #{e.message}"
|
|
123
|
+
Diagnostics.warn(@config.logger, "tarantool pool: connecting to #{address} failed: #{e.class}: #{e.message}")
|
|
129
124
|
false
|
|
130
125
|
end
|
|
131
126
|
|
|
@@ -9,7 +9,16 @@ module Tarantool
|
|
|
9
9
|
# to the parent: closing the client in the child only drops its socket copy, dead threads join at once.
|
|
10
10
|
# Nothing inherited may stop the reset, or the child's pool would never start its members.
|
|
11
11
|
def reset_for_fork
|
|
12
|
-
old_retrier, old_client =
|
|
12
|
+
old_retrier, old_client = clear_fork_state
|
|
13
|
+
ThreadJoin.join(old_retrier)
|
|
14
|
+
close_inherited(old_client)
|
|
15
|
+
@instrumentation&.notify('fork.tarantool')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def clear_fork_state
|
|
21
|
+
@lock.synchronize do
|
|
13
22
|
retrier = @retrier
|
|
14
23
|
client = @client
|
|
15
24
|
@retrier = nil
|
|
@@ -17,12 +26,8 @@ module Tarantool
|
|
|
17
26
|
@read_only = nil
|
|
18
27
|
[retrier, client]
|
|
19
28
|
end
|
|
20
|
-
ThreadJoin.join(old_retrier)
|
|
21
|
-
close_inherited(old_client)
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
private
|
|
25
|
-
|
|
26
31
|
def close_inherited(client)
|
|
27
32
|
client&.close
|
|
28
33
|
rescue StandardError
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
class Pool
|
|
6
|
+
# The role a member's instance reports through box.status. Includers set @lock, @changed, @read_only and
|
|
7
|
+
# @instrumentation.
|
|
8
|
+
# @api private
|
|
9
|
+
module MemberRole
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def update(status)
|
|
13
|
+
read_only = status['is_ro']
|
|
14
|
+
changed = @lock.synchronize do
|
|
15
|
+
@read_only = read_only
|
|
16
|
+
@changed.broadcast
|
|
17
|
+
role_changed?(read_only)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@instrumentation&.notify('role_change.tarantool', read_only:) if changed
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Compared against the last known role rather than @read_only, which disconnect and fork reset:
|
|
24
|
+
# a reconnect that brings back the same role is not a change. A status without is_ro is routing
|
|
25
|
+
# information only, never a role to compare against.
|
|
26
|
+
def role_changed?(read_only)
|
|
27
|
+
return false if read_only.nil?
|
|
28
|
+
|
|
29
|
+
previous = @known_read_only
|
|
30
|
+
@known_read_only = read_only
|
|
31
|
+
!previous.nil? && previous != read_only
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -39,7 +39,7 @@ module Tarantool
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def space_info(space)
|
|
42
|
-
@space_infos.fetch(space_id(space))
|
|
42
|
+
@space_infos.fetch(space_id(space))
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
private
|
|
@@ -52,9 +52,10 @@ module Tarantool
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def space_id(space)
|
|
55
|
-
|
|
55
|
+
id = space.is_a?(Integer) ? space : @space_ids[space.to_s]
|
|
56
|
+
return id if @space_infos.key?(id)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
raise SchemaError, "space '#{space}' not found"
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def index_id(space_id, index)
|
|
@@ -4,6 +4,8 @@ module Tarantool
|
|
|
4
4
|
class Client
|
|
5
5
|
# Format and indexes of a space from the loaded schema. `indexes` maps names to IndexInfo in id order.
|
|
6
6
|
SpaceInfo = Data.define(:id, :name, :fields, :indexes) do
|
|
7
|
+
include TupleMapping
|
|
8
|
+
|
|
7
9
|
def self.from_tuples(space, index_tuples)
|
|
8
10
|
id, _owner, name, _engine, _field_count, _flags, format = space
|
|
9
11
|
fields = (format || []).map { |raw| SpaceInfo::Field.from_raw(raw) }
|
|
@@ -6,15 +6,17 @@ module Tarantool
|
|
|
6
6
|
# stream's requests in order. Created by Client#transaction.
|
|
7
7
|
class Stream
|
|
8
8
|
include Operations
|
|
9
|
+
include Instrumented
|
|
9
10
|
|
|
10
11
|
LOST = 'connection lost during the transaction; the server rolled it back'
|
|
11
12
|
FINISHED = 'transaction is finished'
|
|
12
13
|
|
|
13
|
-
def initialize(connection:, schema_cache:, id:, request_timeout:)
|
|
14
|
+
def initialize(connection:, schema_cache:, id:, request_timeout:, instrumentation: nil)
|
|
14
15
|
@connection = connection
|
|
15
16
|
@schema_cache = schema_cache
|
|
16
17
|
@id = id
|
|
17
18
|
@request_timeout = request_timeout
|
|
19
|
+
@instrumentation = instrumentation
|
|
18
20
|
@state = :idle
|
|
19
21
|
@pid = Process.pid
|
|
20
22
|
end
|
|
@@ -51,34 +53,42 @@ module Tarantool
|
|
|
51
53
|
abandon if @state == :active
|
|
52
54
|
end
|
|
53
55
|
|
|
54
|
-
def
|
|
56
|
+
def transmit(request, timeout: nil, schema_version: nil, on_push: nil)
|
|
55
57
|
check_active
|
|
56
58
|
|
|
57
59
|
send_request(request, timeout: timeout || @request_timeout, schema_version:, on_push:)
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
def
|
|
61
|
-
return
|
|
62
|
+
def transmit_on_space(space, index, timeout: nil, format: false, payload: nil, &)
|
|
63
|
+
return transmit(yield(space, index), timeout:) if !format && space.is_a?(Integer) && index.is_a?(Integer)
|
|
62
64
|
|
|
63
|
-
perform_by_name(space, index, timeout || @request_timeout, &)
|
|
65
|
+
perform_by_name(space, index, timeout || @request_timeout, payload, &)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
# Checks for a lost connection first: resolving would otherwise meet the next connection's schema
|
|
67
69
|
# and report a schema change instead.
|
|
68
|
-
def perform_by_name(space, index, timeout)
|
|
70
|
+
def perform_by_name(space, index, timeout, payload, &)
|
|
69
71
|
check_connection
|
|
70
72
|
deadline = Transport::Deadline.after(timeout)
|
|
71
|
-
request =
|
|
73
|
+
request = build_request(space, index, deadline, payload, &)
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
transmit(request, timeout: remaining(deadline), schema_version: @schema.version)
|
|
74
76
|
rescue ServerError => e
|
|
75
77
|
raise unless e.code == Protocol::Constants::ER_WRONG_SCHEMA_VERSION
|
|
76
78
|
|
|
77
|
-
@schema.
|
|
79
|
+
mark_schema_reloaded(payload) if @schema.refresh?(deadline)
|
|
80
|
+
raise TransactionError, TransactionSchema::CHANGED
|
|
78
81
|
rescue TimeoutError
|
|
79
82
|
raise TimeoutError, "no response within #{timeout}s"
|
|
80
83
|
end
|
|
81
84
|
|
|
85
|
+
def build_request(space, index, deadline, payload)
|
|
86
|
+
(space_id, index_id), reloaded = @schema.resolve(space, index, deadline)
|
|
87
|
+
mark_schema_reloaded(payload) if reloaded
|
|
88
|
+
|
|
89
|
+
yield(space_id, index_id, @schema.space_info(space_id, deadline))
|
|
90
|
+
end
|
|
91
|
+
|
|
82
92
|
def lookup_space(space, timeout)
|
|
83
93
|
check_active
|
|
84
94
|
check_connection
|
|
@@ -98,7 +108,7 @@ module Tarantool
|
|
|
98
108
|
def finish(request)
|
|
99
109
|
@state = :finished
|
|
100
110
|
|
|
101
|
-
send_request(request, timeout: @request_timeout)
|
|
111
|
+
instrumented(request) { send_request(request, timeout: @request_timeout) }
|
|
102
112
|
end
|
|
103
113
|
|
|
104
114
|
# A stream is bound to its connection's session, so a reconnect can't carry the transaction over.
|
|
@@ -17,21 +17,26 @@ module Tarantool
|
|
|
17
17
|
@version = version
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# The ids, and whether the cache had to catch up with the BEGIN version first.
|
|
20
21
|
def resolve(space, index, deadline)
|
|
21
|
-
|
|
22
|
+
cached = @cache.schema
|
|
23
|
+
schema = pinned(cached, deadline)
|
|
24
|
+
|
|
25
|
+
[schema.resolve(space, index), !schema.equal?(cached)]
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def space_info(space, deadline)
|
|
25
|
-
pinned(deadline).space_info(space)
|
|
29
|
+
pinned(@cache.schema, deadline).space_info(space)
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
# A request rejected for its schema version did not run, so the transaction still rolls back
|
|
29
|
-
# cleanly. The reload spares the retried transaction the same rejection.
|
|
30
|
-
def
|
|
33
|
+
# cleanly. The reload spares the retried transaction the same rejection. Returns whether it reloaded.
|
|
34
|
+
def refresh?(deadline)
|
|
31
35
|
schema = @cache.schema
|
|
32
|
-
|
|
36
|
+
return false unless schema.version == version
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
reload(schema, deadline)
|
|
39
|
+
true
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
private
|
|
@@ -39,8 +44,7 @@ module Tarantool
|
|
|
39
44
|
# A cache older than BEGIN was stale before the transaction started and may catch up;
|
|
40
45
|
# anything newer means the schema changed after BEGIN. A schema of another connection means
|
|
41
46
|
# this one was lost, and the versions of two instances can't be compared.
|
|
42
|
-
def pinned(deadline)
|
|
43
|
-
schema = @cache.schema
|
|
47
|
+
def pinned(schema, deadline)
|
|
44
48
|
raise TransactionError, Stream::LOST unless schema.connection.equal?(@connection)
|
|
45
49
|
|
|
46
50
|
schema = reload(schema, deadline) if schema.version < version
|
|
@@ -8,18 +8,21 @@ module Tarantool
|
|
|
8
8
|
# States: connecting -> ready -> reconnecting -> closed.
|
|
9
9
|
# @api private
|
|
10
10
|
class Link
|
|
11
|
+
include LinkEvents
|
|
12
|
+
|
|
11
13
|
attr_writer :event_handler
|
|
12
14
|
|
|
13
15
|
# on_connect runs on each new connection before requests can use it and may raise to fail the
|
|
14
16
|
# attempt; on_ready runs once the connection is current; on_lost runs when a reconnect starts,
|
|
15
17
|
# before its first attempt.
|
|
16
|
-
def initialize(config, on_connect:, on_ready:, on_lost: nil, backoff: nil)
|
|
18
|
+
def initialize(config, on_connect:, on_ready:, on_lost: nil, backoff: nil, instrumentation: nil)
|
|
17
19
|
@config = config
|
|
18
20
|
@on_connect = on_connect
|
|
19
21
|
@on_ready = on_ready
|
|
20
22
|
@on_lost = on_lost
|
|
21
23
|
@backoff = backoff || Backoff.new(base: config.reconnect_delay, max: config.reconnect_max_delay)
|
|
22
24
|
@state = LinkState.new(config.address, reconnect: config.reconnect)
|
|
25
|
+
@instrumentation = instrumentation
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def connect
|
|
@@ -93,25 +96,28 @@ module Tarantool
|
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
def lost(connection)
|
|
96
|
-
return unless @config.reconnect
|
|
99
|
+
return report_lost(connection) unless @config.reconnect
|
|
97
100
|
|
|
98
101
|
@state.start_reconnect(connection) do |previous|
|
|
99
|
-
Thread.new { reconnect(previous) }.tap
|
|
102
|
+
Thread.new { reconnect(previous, connection) }.tap do |thread|
|
|
103
|
+
thread.name = "tarantool-reconnect #{@config.address}"
|
|
104
|
+
end
|
|
100
105
|
end
|
|
101
106
|
rescue ThreadError => e
|
|
102
|
-
|
|
107
|
+
fail_reconnect(ConnectionError.new("cannot reconnect to #{@config.address}: #{e.message}"))
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
# Joining the previous thread first lets close join only the newest one. Nothing may escape: a dead
|
|
106
111
|
# thread would leave the link reconnecting forever, and its exception would resurface in every join.
|
|
107
|
-
def reconnect(previous)
|
|
112
|
+
def reconnect(previous, connection)
|
|
108
113
|
ThreadJoin.join(previous)
|
|
114
|
+
emit('disconnect.tarantool', reason: connection.failure&.message)
|
|
109
115
|
run_hook('on_lost') { @on_lost&.call }
|
|
110
116
|
|
|
111
117
|
reconnect_with_backoff
|
|
112
118
|
rescue StandardError => e
|
|
113
|
-
warn(e.full_message)
|
|
114
|
-
|
|
119
|
+
Diagnostics.warn(@config.logger, e.full_message)
|
|
120
|
+
fail_reconnect(ConnectionError.new("reconnect to #{@config.address} failed: #{e.class}: #{e.message}"))
|
|
115
121
|
end
|
|
116
122
|
|
|
117
123
|
def reconnect_with_backoff
|
|
@@ -119,7 +125,7 @@ module Tarantool
|
|
|
119
125
|
|
|
120
126
|
while @state.pause(@backoff.delay(attempt))
|
|
121
127
|
result = attempt_connect
|
|
122
|
-
return
|
|
128
|
+
return reconnected(result, attempt + 1) if result.is_a?(Connection)
|
|
123
129
|
|
|
124
130
|
attempt += 1
|
|
125
131
|
return give_up(attempt, result) if attempt >= @config.reconnect_attempts
|
|
@@ -136,13 +142,7 @@ module Tarantool
|
|
|
136
142
|
def run_hook(name)
|
|
137
143
|
yield
|
|
138
144
|
rescue StandardError => e
|
|
139
|
-
warn("#{name} failed for #{@config.address}: #{e.full_message}")
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def give_up(attempts, error)
|
|
143
|
-
@state.give_up(
|
|
144
|
-
ConnectionError.new("cannot reconnect to #{@config.address} after #{attempts} attempts: #{error.message}")
|
|
145
|
-
)
|
|
145
|
+
Diagnostics.warn(@config.logger, "#{name} failed for #{@config.address}: #{e.full_message}")
|
|
146
146
|
end
|
|
147
147
|
end
|
|
148
148
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Transport
|
|
6
|
+
# Lifecycle events of a Link. Includers set @config, @state and @instrumentation, and provide
|
|
7
|
+
# #run_hook and #install.
|
|
8
|
+
# @api private
|
|
9
|
+
module LinkEvents
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Without reconnect the link keeps the lost connection, so the loss of the current one is all to
|
|
13
|
+
# report. The reader thread's disconnect handler and install's own loss check can both reach this
|
|
14
|
+
# for the same connection, so LinkState claims the loss once.
|
|
15
|
+
def report_lost(connection)
|
|
16
|
+
emit('disconnect.tarantool', reason: connection.failure&.message) if @state.claim_loss(connection)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A connection the link was closed under, or that broke during on_ready, is no reconnect to report.
|
|
20
|
+
def reconnected(connection, attempts)
|
|
21
|
+
run_hook('on_ready') { install(connection) }
|
|
22
|
+
emit('reconnect.tarantool', attempts:) unless connection.closed?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def give_up(attempts, error)
|
|
26
|
+
message = "cannot reconnect to #{@config.address} after #{attempts} attempts: #{error.message}"
|
|
27
|
+
|
|
28
|
+
fail_reconnect(ConnectionError.new(message), attempts:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fail_reconnect(error, **payload)
|
|
32
|
+
@state.give_up(error)
|
|
33
|
+
emit('reconnect_failed.tarantool', **payload, **Instrumentation.exception_keys(error))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Link already has a public #notify for WATCH/UNWATCH requests, so events use a different name.
|
|
37
|
+
def emit(name, **payload) = @instrumentation&.notify(name, payload)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -20,6 +20,16 @@ module Tarantool
|
|
|
20
20
|
@lock.synchronize { @state == :ready && !@connection.closed? }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Without reconnect the link stays on the lost connection; the first caller reports its loss.
|
|
24
|
+
def claim_loss(connection)
|
|
25
|
+
@lock.synchronize do
|
|
26
|
+
next false unless @connection.equal?(connection) && !@lost.equal?(connection)
|
|
27
|
+
|
|
28
|
+
@lost = connection
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
# Records the connection being opened so that close can abort it; raises once the link is closed.
|
|
24
34
|
def attempting(connection)
|
|
25
35
|
@lock.synchronize do
|