tarantool-client 0.1.0 → 0.3.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 +17 -1
- data/README.md +96 -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 +11 -2
- data/lib/tarantool/client/instrumentation/log_instrumenter.rb +35 -0
- data/lib/tarantool/client/instrumentation/log_lines.rb +71 -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 +58 -22
- data/lib/tarantool/client/page.rb +18 -0
- 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/protocol/constants.rb +3 -0
- data/lib/tarantool/client/protocol/requests/select.rb +22 -5
- data/lib/tarantool/client/protocol/response.rb +1 -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 +10 -0
- data/lib/tarantool/client/transport/link.rb +22 -27
- data/lib/tarantool/client/transport/link_draining.rb +35 -0
- data/lib/tarantool/client/transport/link_events.rb +48 -0
- data/lib/tarantool/client/transport/link_state.rb +39 -5
- data/lib/tarantool/client/transport/pending_requests.rb +28 -1
- 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 +25 -48
- metadata +17 -2
|
@@ -5,21 +5,24 @@ module Tarantool
|
|
|
5
5
|
module Transport
|
|
6
6
|
# Keeps one instance connected. Owns the current Connection; once it is lost, a background thread
|
|
7
7
|
# opens new ones with backoff while requests wait for one to become ready.
|
|
8
|
-
# States: connecting -> ready -> reconnecting -> closed.
|
|
8
|
+
# States: connecting -> ready -> draining | reconnecting -> closed.
|
|
9
9
|
# @api private
|
|
10
10
|
class Link
|
|
11
|
+
include LinkEvents
|
|
12
|
+
include LinkDraining
|
|
13
|
+
|
|
11
14
|
attr_writer :event_handler
|
|
12
15
|
|
|
13
|
-
# on_connect runs on each new connection before requests can use it and may raise to fail the
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
def initialize(config, on_connect:, on_ready:, on_lost: nil, backoff: nil)
|
|
16
|
+
# on_connect runs on each new connection before requests can use it and may raise to fail the attempt;
|
|
17
|
+
# on_ready runs once the connection is current; on_lost runs when a reconnect starts, before its first attempt.
|
|
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
|
|
@@ -49,14 +52,16 @@ module Tarantool
|
|
|
49
52
|
def greeting = @state.connection.greeting
|
|
50
53
|
def connected? = @state.connected?
|
|
51
54
|
|
|
52
|
-
# Closing the attempt first fails a handshake or on_connect blocked on it, so the join is short.
|
|
55
|
+
# Closing the attempt first fails a handshake or on_connect blocked on it, so the join is short. Closing
|
|
56
|
+
# the connection wakes the drain thread, so it is joined last.
|
|
53
57
|
def close
|
|
54
|
-
reconnector, attempt = @state.close
|
|
58
|
+
reconnector, attempt, drainer = @state.close
|
|
55
59
|
attempt&.close
|
|
56
60
|
begin
|
|
57
61
|
ThreadJoin.join(reconnector)
|
|
58
62
|
ensure
|
|
59
63
|
@state.connection&.close
|
|
64
|
+
ThreadJoin.join(drainer)
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
nil
|
|
@@ -93,25 +98,28 @@ module Tarantool
|
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
def lost(connection)
|
|
96
|
-
return unless @config.reconnect
|
|
101
|
+
return report_lost(connection) unless @config.reconnect
|
|
97
102
|
|
|
98
103
|
@state.start_reconnect(connection) do |previous|
|
|
99
|
-
Thread.new { reconnect(previous) }.tap
|
|
104
|
+
Thread.new { reconnect(previous, connection) }.tap do |thread|
|
|
105
|
+
thread.name = "tarantool-reconnect #{@config.address}"
|
|
106
|
+
end
|
|
100
107
|
end
|
|
101
108
|
rescue ThreadError => e
|
|
102
|
-
|
|
109
|
+
fail_reconnect(ConnectionError.new("cannot reconnect to #{@config.address}: #{e.message}"))
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
# Joining the previous thread first lets close join only the newest one. Nothing may escape: a dead
|
|
106
113
|
# thread would leave the link reconnecting forever, and its exception would resurface in every join.
|
|
107
|
-
def reconnect(previous)
|
|
114
|
+
def reconnect(previous, connection)
|
|
108
115
|
ThreadJoin.join(previous)
|
|
116
|
+
emit('disconnect.tarantool', reason: connection.failure&.message)
|
|
109
117
|
run_hook('on_lost') { @on_lost&.call }
|
|
110
118
|
|
|
111
119
|
reconnect_with_backoff
|
|
112
120
|
rescue StandardError => e
|
|
113
|
-
warn(e.full_message)
|
|
114
|
-
|
|
121
|
+
Diagnostics.warn(@config.logger, e.full_message)
|
|
122
|
+
fail_reconnect(ConnectionError.new("reconnect to #{@config.address} failed: #{e.class}: #{e.message}"))
|
|
115
123
|
end
|
|
116
124
|
|
|
117
125
|
def reconnect_with_backoff
|
|
@@ -119,7 +127,7 @@ module Tarantool
|
|
|
119
127
|
|
|
120
128
|
while @state.pause(@backoff.delay(attempt))
|
|
121
129
|
result = attempt_connect
|
|
122
|
-
return
|
|
130
|
+
return reconnected(result, attempt + 1) if result.is_a?(Connection)
|
|
123
131
|
|
|
124
132
|
attempt += 1
|
|
125
133
|
return give_up(attempt, result) if attempt >= @config.reconnect_attempts
|
|
@@ -131,19 +139,6 @@ module Tarantool
|
|
|
131
139
|
rescue StandardError => e
|
|
132
140
|
e
|
|
133
141
|
end
|
|
134
|
-
|
|
135
|
-
# A failing hook is the caller's bug, not a reason to stop reconnecting or to drop the new connection.
|
|
136
|
-
def run_hook(name)
|
|
137
|
-
yield
|
|
138
|
-
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
|
-
)
|
|
146
|
-
end
|
|
147
142
|
end
|
|
148
143
|
end
|
|
149
144
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Transport
|
|
6
|
+
# Graceful shutdown of a Link's connection. Includers set @config and @state, and provide #emit.
|
|
7
|
+
# @api private
|
|
8
|
+
module LinkDraining
|
|
9
|
+
# The server announced its shutdown and waits for this connection to close: new requests wait for the
|
|
10
|
+
# next connection, in-flight ones finish, then the connection is closed and its loss handled as usual.
|
|
11
|
+
def drain
|
|
12
|
+
connection = @state.connection
|
|
13
|
+
|
|
14
|
+
@state.start_draining(connection) do
|
|
15
|
+
Thread.new { drain_connection(connection) }.tap do |thread|
|
|
16
|
+
thread.name = "tarantool-drain #{@config.address}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Without reconnect the drained connection becomes current again so requests fail with its reason.
|
|
24
|
+
def drain_connection(connection)
|
|
25
|
+
emit('shutdown.tarantool', pending: connection.in_flight)
|
|
26
|
+
connection.wait_idle(@config.request_timeout)
|
|
27
|
+
@state.finish_draining(connection) unless @config.reconnect
|
|
28
|
+
connection.shut_down("server #{@config.address} is shutting down")
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
Diagnostics.warn(@config.logger, "drain failed for #{@config.address}: #{e.full_message}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Transport
|
|
6
|
+
# Lifecycle events and hooks of a Link. Includers set @config, @state and @instrumentation, and provide
|
|
7
|
+
# #install.
|
|
8
|
+
# @api private
|
|
9
|
+
module LinkEvents
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# A failing hook is the caller's bug, not a reason to stop reconnecting or to drop the new connection.
|
|
13
|
+
def run_hook(name)
|
|
14
|
+
yield
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
Diagnostics.warn(@config.logger, "#{name} failed for #{@config.address}: #{e.full_message}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Without reconnect the link keeps the lost connection, so the loss of the current one is all to
|
|
20
|
+
# report. The reader thread's disconnect handler and install's own loss check can both reach this
|
|
21
|
+
# for the same connection, so LinkState claims the loss once.
|
|
22
|
+
def report_lost(connection)
|
|
23
|
+
emit('disconnect.tarantool', reason: connection.failure&.message) if @state.claim_loss(connection)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A connection the link was closed under, or that broke during on_ready, is no reconnect to report.
|
|
27
|
+
def reconnected(connection, attempts)
|
|
28
|
+
run_hook('on_ready') { install(connection) }
|
|
29
|
+
emit('reconnect.tarantool', attempts:) unless connection.closed?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def give_up(attempts, error)
|
|
33
|
+
message = "cannot reconnect to #{@config.address} after #{attempts} attempts: #{error.message}"
|
|
34
|
+
|
|
35
|
+
fail_reconnect(ConnectionError.new(message), attempts:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fail_reconnect(error, **payload)
|
|
39
|
+
@state.give_up(error)
|
|
40
|
+
emit('reconnect_failed.tarantool', **payload, **Instrumentation.exception_keys(error))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Link already has a public #notify for WATCH/UNWATCH requests, so events use a different name.
|
|
44
|
+
def emit(name, **payload) = @instrumentation&.notify(name, payload)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
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
|
|
@@ -42,11 +52,34 @@ module Tarantool
|
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
54
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
55
|
+
# The server announced its shutdown: requests wait as while reconnecting, in-flight ones finish on the
|
|
56
|
+
# connection. The block returns the drain thread; only the current connection drains, once.
|
|
57
|
+
def start_draining(connection)
|
|
58
|
+
@lock.synchronize do
|
|
59
|
+
next false unless @state == :ready && @connection.equal?(connection)
|
|
60
|
+
|
|
61
|
+
@drainer = yield
|
|
62
|
+
@state = :draining
|
|
63
|
+
@changed.broadcast
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Without reconnect the drained connection stays current, failing requests with the shutdown reason.
|
|
69
|
+
def finish_draining(connection)
|
|
70
|
+
@lock.synchronize do
|
|
71
|
+
next unless @state == :draining && @connection.equal?(connection)
|
|
72
|
+
|
|
73
|
+
@state = :ready
|
|
74
|
+
@changed.broadcast
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Leaves ready or draining only for the current connection, so a loss is handled once. The block gets
|
|
79
|
+
# the previous reconnect thread and returns the new one; the state flips only once that thread exists.
|
|
47
80
|
def start_reconnect(connection)
|
|
48
81
|
@lock.synchronize do
|
|
49
|
-
next unless @state
|
|
82
|
+
next unless %i[ready draining].include?(@state) && @connection.equal?(connection)
|
|
50
83
|
|
|
51
84
|
@reconnector = yield @reconnector
|
|
52
85
|
@state = :reconnecting
|
|
@@ -54,7 +87,8 @@ module Tarantool
|
|
|
54
87
|
end
|
|
55
88
|
end
|
|
56
89
|
|
|
57
|
-
# Returns the reconnect thread
|
|
90
|
+
# Returns the reconnect thread, the connection it is opening and the drain thread, for the caller to
|
|
91
|
+
# abort and join.
|
|
58
92
|
def close
|
|
59
93
|
@lock.synchronize do
|
|
60
94
|
@error = ClientClosedError.new("connection to #{@address} closed")
|
|
@@ -62,7 +96,7 @@ module Tarantool
|
|
|
62
96
|
@state = :closed
|
|
63
97
|
@changed.broadcast
|
|
64
98
|
end
|
|
65
|
-
[@reconnector, @attempt]
|
|
99
|
+
[@reconnector, @attempt, @drainer]
|
|
66
100
|
end
|
|
67
101
|
end
|
|
68
102
|
|
|
@@ -8,6 +8,7 @@ module Tarantool
|
|
|
8
8
|
class PendingRequests
|
|
9
9
|
def initialize
|
|
10
10
|
@lock = Mutex.new
|
|
11
|
+
@emptied = ConditionVariable.new
|
|
11
12
|
@futures = {}
|
|
12
13
|
@last_sync = 0
|
|
13
14
|
@failure = nil
|
|
@@ -31,7 +32,32 @@ module Tarantool
|
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def delete(sync)
|
|
34
|
-
@lock.synchronize
|
|
35
|
+
@lock.synchronize do
|
|
36
|
+
future = @futures.delete(sync)
|
|
37
|
+
@emptied.broadcast if @futures.empty?
|
|
38
|
+
|
|
39
|
+
future
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def size
|
|
44
|
+
@lock.synchronize { @futures.size }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# True once no request is in flight, false at the deadline.
|
|
48
|
+
def wait_empty(timeout)
|
|
49
|
+
deadline = Deadline.after(timeout)
|
|
50
|
+
|
|
51
|
+
@lock.synchronize do
|
|
52
|
+
until @futures.empty?
|
|
53
|
+
remaining = Deadline.remaining(deadline)
|
|
54
|
+
return false if remaining&.zero?
|
|
55
|
+
|
|
56
|
+
@emptied.wait(@lock, remaining)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
true
|
|
60
|
+
end
|
|
35
61
|
end
|
|
36
62
|
|
|
37
63
|
def failure
|
|
@@ -43,6 +69,7 @@ module Tarantool
|
|
|
43
69
|
first, failure, futures = @lock.synchronize do
|
|
44
70
|
first = @failure.nil?
|
|
45
71
|
@failure ||= error
|
|
72
|
+
@emptied.broadcast
|
|
46
73
|
[first, @failure, @futures.values.tap { @futures.clear }]
|
|
47
74
|
end
|
|
48
75
|
futures.each { |future| future.reject(failure) }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Tuple <-> Hash by the space format, for SpaceInfo. Fields beyond the format are keyed by their 0-based position.
|
|
6
|
+
# @api private
|
|
7
|
+
module TupleMapping
|
|
8
|
+
# A position this far past the format would otherwise let a huge Integer key allocate a giant Array.
|
|
9
|
+
MAX_POSITIONS_BEYOND_FORMAT = 1024
|
|
10
|
+
|
|
11
|
+
def to_hash(tuple)
|
|
12
|
+
hash = fields.each_with_index.to_h { |field, position| [field.name.to_sym, tuple[position]] }
|
|
13
|
+
(fields.size...tuple.size).each { |position| hash[position] = tuple[position] }
|
|
14
|
+
|
|
15
|
+
hash
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# A missing field goes out as nil; on 3.x the server applies the field default to nil as well.
|
|
19
|
+
def to_tuple(hash)
|
|
20
|
+
positions = fields.each_with_index.to_h { |field, position| [field.name, position] }
|
|
21
|
+
named_positions = {}
|
|
22
|
+
|
|
23
|
+
hash.each_with_object(Array.new(fields.size)) do |(key, value), tuple|
|
|
24
|
+
position = position_of(key, positions)
|
|
25
|
+
check_not_duplicate(key, position, named_positions)
|
|
26
|
+
tuple[position] = value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def position_of(key, positions)
|
|
33
|
+
case key
|
|
34
|
+
when Symbol, String
|
|
35
|
+
positions.fetch(key.to_s) { raise UnknownFieldError, "space #{name} has no field '#{key}'" }
|
|
36
|
+
when Integer then position_beyond_format(key)
|
|
37
|
+
else raise ArgumentError, "unsupported key #{key.inspect} for space #{name}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `:a` and 'a' name the same field; the later one would otherwise win silently.
|
|
42
|
+
def check_not_duplicate(key, position, named_positions)
|
|
43
|
+
return unless key.is_a?(Symbol) || key.is_a?(String)
|
|
44
|
+
raise ArgumentError, "space #{name} field '#{fields[position].name}' given twice" if named_positions[position]
|
|
45
|
+
|
|
46
|
+
named_positions[position] = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def position_beyond_format(position)
|
|
50
|
+
raise ArgumentError, "position #{position} is negative for space #{name}" if position.negative?
|
|
51
|
+
|
|
52
|
+
if position < fields.size
|
|
53
|
+
raise ArgumentError, "position #{position} is inside the format of space #{name}; use the field name"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if position >= fields.size + MAX_POSITIONS_BEYOND_FORMAT
|
|
57
|
+
raise ArgumentError, "position #{position} is more than #{MAX_POSITIONS_BEYOND_FORMAT} past the format " \
|
|
58
|
+
"of space #{name}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
position
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# What data operations return for each tuple, chosen with `as:`.
|
|
6
|
+
# @api private
|
|
7
|
+
module TupleShape
|
|
8
|
+
SHAPES = %i[tuple hash row].freeze
|
|
9
|
+
|
|
10
|
+
def self.check(shape)
|
|
11
|
+
return if SHAPES.include?(shape)
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, "unknown as: #{shape.inspect}, expected one of #{SHAPES.join(', ')}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.apply(shape, tuple, space_info)
|
|
17
|
+
case shape
|
|
18
|
+
when :hash then space_info.to_hash(tuple)
|
|
19
|
+
when :row then Row.new(tuple:, attributes: space_info.to_hash(tuple))
|
|
20
|
+
else tuple
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/tarantool/client.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative 'client/errors'
|
|
|
8
8
|
require_relative 'client/configuration'
|
|
9
9
|
require_relative 'client/backoff'
|
|
10
10
|
require_relative 'client/thread_join'
|
|
11
|
+
require_relative 'client/diagnostics'
|
|
11
12
|
require_relative 'client/protocol/constants'
|
|
12
13
|
require_relative 'client/box_error'
|
|
13
14
|
require_relative 'client/uuid'
|
|
@@ -40,6 +41,12 @@ require_relative 'client/protocol/requests/auth'
|
|
|
40
41
|
require_relative 'client/protocol/requests/id'
|
|
41
42
|
require_relative 'client/protocol/requests/watch'
|
|
42
43
|
require_relative 'client/protocol/requests/unwatch'
|
|
44
|
+
require_relative 'client/instrumentation'
|
|
45
|
+
require_relative 'client/instrumentation/request_payload'
|
|
46
|
+
require_relative 'client/instrumentation/log_lines'
|
|
47
|
+
require_relative 'client/instrumentation/log_instrumenter'
|
|
48
|
+
require_relative 'client/instrumented'
|
|
49
|
+
require_relative 'client/named_requests'
|
|
43
50
|
require_relative 'client/transport/future'
|
|
44
51
|
require_relative 'client/transport/pending_requests'
|
|
45
52
|
require_relative 'client/transport/stream_ids'
|
|
@@ -50,14 +57,21 @@ require_relative 'client/transport/frame_writer'
|
|
|
50
57
|
require_relative 'client/transport/handshake'
|
|
51
58
|
require_relative 'client/transport/connection'
|
|
52
59
|
require_relative 'client/transport/link_state'
|
|
60
|
+
require_relative 'client/transport/link_events'
|
|
61
|
+
require_relative 'client/transport/link_draining'
|
|
53
62
|
require_relative 'client/transport/link'
|
|
54
63
|
require_relative 'client/index_info'
|
|
64
|
+
require_relative 'client/tuple_mapping'
|
|
55
65
|
require_relative 'client/space_info'
|
|
56
66
|
require_relative 'client/schema'
|
|
57
67
|
require_relative 'client/schema_cache'
|
|
58
68
|
require_relative 'client/sql_result'
|
|
59
69
|
require_relative 'client/prepared_statement'
|
|
70
|
+
require_relative 'client/batch_delete'
|
|
60
71
|
require_relative 'client/transaction_schema'
|
|
72
|
+
require_relative 'client/row'
|
|
73
|
+
require_relative 'client/page'
|
|
74
|
+
require_relative 'client/tuple_shape'
|
|
61
75
|
require_relative 'client/operations'
|
|
62
76
|
require_relative 'client/transaction_scope'
|
|
63
77
|
require_relative 'client/stream'
|
|
@@ -68,6 +82,7 @@ require_relative 'client/watchers/dispatcher'
|
|
|
68
82
|
require_relative 'client/fork_safety'
|
|
69
83
|
require_relative 'client/pool'
|
|
70
84
|
require_relative 'client/pool/member_fork_safety'
|
|
85
|
+
require_relative 'client/pool/member_role'
|
|
71
86
|
require_relative 'client/pool/member'
|
|
72
87
|
|
|
73
88
|
module Tarantool
|
|
@@ -75,6 +90,8 @@ module Tarantool
|
|
|
75
90
|
class Client
|
|
76
91
|
include Operations
|
|
77
92
|
include ForkSafety
|
|
93
|
+
include Instrumented
|
|
94
|
+
include NamedRequests
|
|
78
95
|
|
|
79
96
|
attr_reader :config
|
|
80
97
|
|
|
@@ -85,6 +102,7 @@ module Tarantool
|
|
|
85
102
|
|
|
86
103
|
def initialize(uri = nil, **)
|
|
87
104
|
@config = Configuration.build(uri, **)
|
|
105
|
+
@instrumentation = Instrumentation.build(@config)
|
|
88
106
|
@pid = Process.pid
|
|
89
107
|
@reinit_lock = Mutex.new
|
|
90
108
|
open_link
|
|
@@ -107,7 +125,7 @@ module Tarantool
|
|
|
107
125
|
TransactionScope.enter(self) do
|
|
108
126
|
stream = Stream.new(
|
|
109
127
|
connection: @link.connection, schema_cache: @schema_cache, id: @stream_ids.next,
|
|
110
|
-
request_timeout: @config.request_timeout
|
|
128
|
+
request_timeout: @config.request_timeout, instrumentation: @instrumentation
|
|
111
129
|
)
|
|
112
130
|
stream.transaction(isolation:, timeout:, &block)
|
|
113
131
|
end
|
|
@@ -139,7 +157,7 @@ module Tarantool
|
|
|
139
157
|
|
|
140
158
|
def link_hooks = { on_connect: method(:load_schema), on_ready: method(:resubscribe), on_lost: method(:lost) }
|
|
141
159
|
|
|
142
|
-
def
|
|
160
|
+
def transmit(request, timeout: nil, schema_version: nil, on_push: nil)
|
|
143
161
|
ensure_fork_safe
|
|
144
162
|
@link.perform(request, timeout: timeout || @config.request_timeout, schema_version:, on_push:)
|
|
145
163
|
end
|
|
@@ -164,53 +182,12 @@ module Tarantool
|
|
|
164
182
|
|
|
165
183
|
# Name-resolved requests carry the schema version they were resolved against, so the
|
|
166
184
|
# server rejects them (ER_WRONG_SCHEMA_VERSION) instead of running them on a stale id.
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
# A request that needs the space format is resolved the same way even by ids, so the
|
|
186
|
+
# format and the version it carries come from one schema.
|
|
187
|
+
def transmit_on_space(space, index, timeout: nil, format: false, payload: nil, &)
|
|
188
|
+
return transmit(yield(space, index), timeout:) if !format && space.is_a?(Integer) && index.is_a?(Integer)
|
|
169
189
|
|
|
170
|
-
perform_by_name(space, index, timeout || @config.request_timeout, &)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Every schema fetch and attempt draws on one deadline, so the whole call stays within timeout.
|
|
174
|
-
# Waiting for a connection first means the cache already holds its schema or a newer one.
|
|
175
|
-
def perform_by_name(space, index, timeout, &)
|
|
176
|
-
ensure_fork_safe
|
|
177
|
-
deadline = Transport::Deadline.after(timeout)
|
|
178
|
-
@link.connection(deadline)
|
|
179
|
-
|
|
180
|
-
perform_with_schema_check(space, index, deadline, &)
|
|
181
|
-
rescue TimeoutError
|
|
182
|
-
raise TimeoutError, "no response within #{timeout}s"
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def lookup_space(space, timeout)
|
|
186
|
-
ensure_fork_safe
|
|
187
|
-
timeout ||= @config.request_timeout
|
|
188
|
-
deadline = Transport::Deadline.after(timeout)
|
|
189
|
-
@link.connection(deadline)
|
|
190
|
-
|
|
191
|
-
@schema_cache.space_info(space, deadline)
|
|
192
|
-
rescue TimeoutError
|
|
193
|
-
raise TimeoutError, "no response within #{timeout}s"
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def perform_with_schema_check(space, index, deadline)
|
|
197
|
-
schema, ids = @schema_cache.resolve(space, index, deadline)
|
|
198
|
-
|
|
199
|
-
perform_resolved(schema, yield(*ids), deadline)
|
|
200
|
-
rescue ServerError => e
|
|
201
|
-
raise unless e.code == Protocol::Constants::ER_WRONG_SCHEMA_VERSION
|
|
202
|
-
|
|
203
|
-
fresh = @schema_cache.reload(schema, timeout: Transport::Deadline.remaining(deadline))
|
|
204
|
-
schema, ids = @schema_cache.resolve(space, index, deadline, fresh)
|
|
205
|
-
perform_resolved(schema, yield(*ids), deadline)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Only the connection the schema came from: once it is lost, another instance may reuse the
|
|
209
|
-
# schema version with other ids, so the request fails with it rather than wait for a reconnect.
|
|
210
|
-
def perform_resolved(schema, request, deadline)
|
|
211
|
-
timeout = Transport::Deadline.remaining(deadline)
|
|
212
|
-
|
|
213
|
-
schema.connection.perform(request, timeout:, schema_version: schema.version)
|
|
190
|
+
perform_by_name(space, index, timeout || @config.request_timeout, payload, &)
|
|
214
191
|
end
|
|
215
192
|
end
|
|
216
193
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tarantool-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Starlexxx
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -52,17 +52,27 @@ files:
|
|
|
52
52
|
- README.md
|
|
53
53
|
- lib/tarantool/client.rb
|
|
54
54
|
- lib/tarantool/client/backoff.rb
|
|
55
|
+
- lib/tarantool/client/batch_delete.rb
|
|
55
56
|
- lib/tarantool/client/box_error.rb
|
|
56
57
|
- lib/tarantool/client/callback_guard.rb
|
|
57
58
|
- lib/tarantool/client/configuration.rb
|
|
59
|
+
- lib/tarantool/client/diagnostics.rb
|
|
58
60
|
- lib/tarantool/client/errors.rb
|
|
59
61
|
- lib/tarantool/client/fork_safety.rb
|
|
60
62
|
- lib/tarantool/client/index_info.rb
|
|
63
|
+
- lib/tarantool/client/instrumentation.rb
|
|
64
|
+
- lib/tarantool/client/instrumentation/log_instrumenter.rb
|
|
65
|
+
- lib/tarantool/client/instrumentation/log_lines.rb
|
|
66
|
+
- lib/tarantool/client/instrumentation/request_payload.rb
|
|
67
|
+
- lib/tarantool/client/instrumented.rb
|
|
61
68
|
- lib/tarantool/client/interval.rb
|
|
69
|
+
- lib/tarantool/client/named_requests.rb
|
|
62
70
|
- lib/tarantool/client/operations.rb
|
|
71
|
+
- lib/tarantool/client/page.rb
|
|
63
72
|
- lib/tarantool/client/pool.rb
|
|
64
73
|
- lib/tarantool/client/pool/member.rb
|
|
65
74
|
- lib/tarantool/client/pool/member_fork_safety.rb
|
|
75
|
+
- lib/tarantool/client/pool/member_role.rb
|
|
66
76
|
- lib/tarantool/client/prepared_statement.rb
|
|
67
77
|
- lib/tarantool/client/protocol/auth/chap_sha1.rb
|
|
68
78
|
- lib/tarantool/client/protocol/codec.rb
|
|
@@ -93,6 +103,7 @@ files:
|
|
|
93
103
|
- lib/tarantool/client/protocol/requests/upsert.rb
|
|
94
104
|
- lib/tarantool/client/protocol/requests/watch.rb
|
|
95
105
|
- lib/tarantool/client/protocol/response.rb
|
|
106
|
+
- lib/tarantool/client/row.rb
|
|
96
107
|
- lib/tarantool/client/schema.rb
|
|
97
108
|
- lib/tarantool/client/schema_cache.rb
|
|
98
109
|
- lib/tarantool/client/space_info.rb
|
|
@@ -107,11 +118,15 @@ files:
|
|
|
107
118
|
- lib/tarantool/client/transport/future.rb
|
|
108
119
|
- lib/tarantool/client/transport/handshake.rb
|
|
109
120
|
- lib/tarantool/client/transport/link.rb
|
|
121
|
+
- lib/tarantool/client/transport/link_draining.rb
|
|
122
|
+
- lib/tarantool/client/transport/link_events.rb
|
|
110
123
|
- lib/tarantool/client/transport/link_state.rb
|
|
111
124
|
- lib/tarantool/client/transport/pending_requests.rb
|
|
112
125
|
- lib/tarantool/client/transport/stream_ids.rb
|
|
113
126
|
- lib/tarantool/client/transport/timed_lock.rb
|
|
114
127
|
- lib/tarantool/client/transport/timed_read.rb
|
|
128
|
+
- lib/tarantool/client/tuple_mapping.rb
|
|
129
|
+
- lib/tarantool/client/tuple_shape.rb
|
|
115
130
|
- lib/tarantool/client/uuid.rb
|
|
116
131
|
- lib/tarantool/client/version.rb
|
|
117
132
|
- lib/tarantool/client/watcher.rb
|