tarantool-client 0.2.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 +5 -0
- data/README.md +18 -1
- data/lib/tarantool/client/fork_safety.rb +9 -1
- data/lib/tarantool/client/instrumentation/log_lines.rb +6 -1
- data/lib/tarantool/client/operations.rb +9 -5
- data/lib/tarantool/client/page.rb +18 -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/transport/connection.rb +8 -0
- data/lib/tarantool/client/transport/link.rb +8 -13
- data/lib/tarantool/client/transport/link_draining.rb +35 -0
- data/lib/tarantool/client/transport/link_events.rb +9 -2
- data/lib/tarantool/client/transport/link_state.rb +29 -5
- data/lib/tarantool/client/transport/pending_requests.rb +28 -1
- data/lib/tarantool/client/version.rb +1 -1
- data/lib/tarantool/client.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 312e13dccce60b22bb3e74005e914cadcbf6dbbf096d378759a2697e6c0a1e8c
|
|
4
|
+
data.tar.gz: f0fd02d53f7bbb97ee347f7b81b1e9a9f83476a5af871042036b025c9cbb8060
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81512742b3dfc97f726a1af22d2bbfd578cd083cc2dd75f3cbe3c4004bde4e1e73ee21ba03675581b7586d590c2624e6287bc8040ee281e3b3fe85ef8c093f4e
|
|
7
|
+
data.tar.gz: 8e4390ef152eab60197e2a5822a132266b12c4eb567801017a2dd0308426109823230b86df6700b1e220f9353f9de892cbe634acaa4d89578ff84c99167ead89
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-10-17)
|
|
4
|
+
|
|
5
|
+
- `select(fetch_position: true)` returns a `Page` with the server-side position of the last tuple; `after:` takes that position (String) as well as a tuple.
|
|
6
|
+
- Graceful shutdown: the client subscribes to `box.shutdown`; on the event it stops sending, a `Pool` member leaves the rotation, requests in flight finish, and the connection is closed by the client before the server stops (`shutdown.tarantool` event, `disconnect.tarantool` with the reason `server ... is shutting down`).
|
|
7
|
+
|
|
3
8
|
## 0.2.0 (2026-10-17)
|
|
4
9
|
|
|
5
10
|
- `as: :hash` and `as: :row` on `select`, `insert`, `replace`, `update` and `delete` return tuples as Hashes by the space format (`Client::Row` keeps the raw tuple too); `SpaceInfo#to_hash` and `#to_tuple` do the conversion.
|
data/README.md
CHANGED
|
@@ -72,6 +72,16 @@ page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50)
|
|
|
72
72
|
next_page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50, after: page.last)
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
With `fetch_position: true` the server hands back a position instead, so the caller carries an opaque String rather than the last tuple (`IPROTO_FETCH_POSITION`, `IPROTO_POSITION`, `IPROTO_AFTER_POSITION`; 2.11+). `select` then returns a `Client::Page`: `tuples` (converted by `as:` as usual, also reachable through `each`), and `position`, nil once a page comes back empty. A position is tied to the index and iterator of the select that produced it; don't parse it or reuse it with another index. `after:` accepts a position (String) or a tuple (Array).
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50, fetch_position: true)
|
|
79
|
+
until page.empty?
|
|
80
|
+
page.each { |user| ... }
|
|
81
|
+
page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50, fetch_position: true, after: page.position)
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
75
85
|
`delete_by` deletes every tuple a select finds, including by a non-unique index, which IPROTO `DELETE` can't do. It selects `batch_size` tuples at a time (default 1000), continues past the last one with `after:`, deletes each by primary key, and returns the number deleted. TREE indexes only, like `after:`. Multikey and functional indexes are not supported either: tuple-based `after` raises `ServerError` there, before deleting anything.
|
|
76
86
|
|
|
77
87
|
```ruby
|
|
@@ -188,6 +198,12 @@ pool.close
|
|
|
188
198
|
|
|
189
199
|
Roles come from `box.status` (`is_ro`), so a switchover is picked up as soon as the instances report it. Requests are spread round-robin among matching instances. If none matches, the call raises `NoAvailableInstanceError`. `Pool.new` raises it only if no address answers; addresses that are down keep being retried in the background. That retrying covers start-up only: once connected, a member relies on its client's reconnect, so with a finite `reconnect_attempts` a member that gives up stays out of rotation. `Pool#close` may wait for an in-progress background connect attempt, up to `connect_timeout` + `request_timeout`. Options are passed to each client. A prepared statement from `pool.prepare` stays bound to the instance that prepared it.
|
|
190
200
|
|
|
201
|
+
## Graceful shutdown
|
|
202
|
+
|
|
203
|
+
Tarantool 2.10+ announces `SIGTERM` through the `box.shutdown` event and waits, up to `box.ctl.set_on_shutdown_timeout` (3 s by default), for the connections subscribed to it to close. Every `Client` subscribes. When the event arrives the client stops sending: `connected?` turns false, a `Pool` member leaves the rotation at once, and new requests on a bare `Client` wait for the next connection as they do while reconnecting. Requests already in flight get their replies; once none is left, or after `request_timeout`, the client closes the connection itself with the reason `server <address> is shutting down` and the usual reconnect follows. A transaction open on that connection fails on its next request, as after any loss. Nothing is retried elsewhere.
|
|
204
|
+
|
|
205
|
+
Watching `box.shutdown` yourself works as before; the client's own subscription is never unwatched.
|
|
206
|
+
|
|
191
207
|
## Forking servers
|
|
192
208
|
|
|
193
209
|
`Client` and `Pool` are fork-safe. The first request in a child process after `fork` reinitializes the connection transparently — a new socket is opened, background threads are restarted, and watchers are resubscribed. The parent's connection is unaffected.
|
|
@@ -227,6 +243,7 @@ Lifecycle events have the instance `address` too. Subscribers should be fast: `d
|
|
|
227
243
|
| `reconnect_failed.tarantool` | `attempts` (when known), `exception`, `exception_object` | error |
|
|
228
244
|
| `fork.tarantool` | | info |
|
|
229
245
|
| `role_change.tarantool` | `read_only` (pool members) | info |
|
|
246
|
+
| `shutdown.tarantool` | `pending` (requests in flight when the server announced its shutdown) | info |
|
|
230
247
|
|
|
231
248
|
`role_change.tarantool` fires only when the role actually changes, not on the first report after connecting.
|
|
232
249
|
|
|
@@ -289,7 +306,7 @@ Don't create multikey indexes on these containers: with `memtx_use_mvcc_engine =
|
|
|
289
306
|
## Public API
|
|
290
307
|
|
|
291
308
|
`Tarantool::Client`, `Tarantool::Client::Pool`, `Tarantool::Client::Stream` (yielded by `transaction`),
|
|
292
|
-
`Watcher` (returned by `watch`), `SqlResult`, `PreparedStatement`, `SpaceInfo`, `IndexInfo`, `Row`, `Configuration`
|
|
309
|
+
`Watcher` (returned by `watch`), `SqlResult`, `PreparedStatement`, `SpaceInfo`, `IndexInfo`, `Row`, `Page`, `Configuration`
|
|
293
310
|
(returned by `client.config`), `Tarantool::Client::UUID`, `Interval`, `BoxError`, and the error classes above.
|
|
294
311
|
|
|
295
312
|
Everything else, including anything tagged `@api private` in its source comment, is an implementation detail
|
|
@@ -27,16 +27,24 @@ module Tarantool
|
|
|
27
27
|
nil
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# Watcher registrations outlive a fork; on_ready resubscribes them on the new link
|
|
30
|
+
# Watcher registrations outlive a fork; on_ready resubscribes them on the new link, the shutdown
|
|
31
|
+
# subscription among them.
|
|
31
32
|
def open_link
|
|
32
33
|
@link = Transport::Link.new(@config, instrumentation: @instrumentation, **link_hooks)
|
|
33
34
|
@watchers&.restart_after_fork(@link)
|
|
35
|
+
first = @watchers.nil?
|
|
34
36
|
@watchers ||= Watchers.new(@link, timeout: @config.request_timeout)
|
|
35
37
|
@link.event_handler = @watchers.method(:dispatch)
|
|
36
38
|
@schema_cache = SchemaCache.new
|
|
37
39
|
@stream_ids = Transport::StreamIds.new
|
|
38
40
|
@link.connect
|
|
39
41
|
@pid = Process.pid
|
|
42
|
+
subscribe_to_shutdown if first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# box.shutdown is true once the server starts stopping; the value on subscribing is nil before that.
|
|
46
|
+
def subscribe_to_shutdown
|
|
47
|
+
@watchers.add('box.shutdown', ->(_key, value) { @link.drain if value })
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
@@ -8,7 +8,8 @@ module Tarantool
|
|
|
8
8
|
module LogLines
|
|
9
9
|
LEVELS = {
|
|
10
10
|
'request.tarantool' => :debug, 'disconnect.tarantool' => :warn, 'reconnect.tarantool' => :info,
|
|
11
|
-
'reconnect_failed.tarantool' => :error, 'fork.tarantool' => :info, 'role_change.tarantool' => :info
|
|
11
|
+
'reconnect_failed.tarantool' => :error, 'fork.tarantool' => :info, 'role_change.tarantool' => :info,
|
|
12
|
+
'shutdown.tarantool' => :info
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
14
15
|
LINES = {
|
|
@@ -27,6 +28,9 @@ module Tarantool
|
|
|
27
28
|
end,
|
|
28
29
|
'role_change.tarantool' => lambda do |payload, _duration|
|
|
29
30
|
"tarantool #{payload[:address]} is now #{payload[:read_only] ? 'read-only' : 'writable'}"
|
|
31
|
+
end,
|
|
32
|
+
'shutdown.tarantool' => lambda do |payload, _duration|
|
|
33
|
+
"tarantool #{payload[:address]} is shutting down, #{requests(payload[:pending])} in flight"
|
|
30
34
|
end
|
|
31
35
|
}.freeze
|
|
32
36
|
|
|
@@ -49,6 +53,7 @@ module Tarantool
|
|
|
49
53
|
def request_name(payload) = payload[:request] ? "tarantool #{payload[:request]}" : 'tarantool'
|
|
50
54
|
|
|
51
55
|
def attempts(count) = count == 1 ? '1 attempt' : "#{count} attempts"
|
|
56
|
+
def requests(count) = count == 1 ? '1 request' : "#{count} requests"
|
|
52
57
|
def error(payload) = payload[:exception].join(': ')
|
|
53
58
|
|
|
54
59
|
# The message may quote data (ER_TUPLE_FOUND repeats the tuple), so the request line keeps only the
|
|
@@ -19,10 +19,13 @@ module Tarantool
|
|
|
19
19
|
lookup_space(space, timeout)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# `after:` takes a tuple or a position; `fetch_position: true` returns a Page carrying the position instead
|
|
23
|
+
# of a plain Array.
|
|
22
24
|
def select(space, key = [], index: 0, limit: Protocol::Constants::MAX_LIMIT, offset: 0, iterator: :eq,
|
|
23
|
-
after: nil, as: :tuple, timeout: nil)
|
|
24
|
-
on_space(space, index, as:, timeout:) do |space_id, index_id|
|
|
25
|
-
Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after
|
|
25
|
+
after: nil, fetch_position: false, as: :tuple, timeout: nil)
|
|
26
|
+
on_space(space, index, as:, timeout:, page: fetch_position) do |space_id, index_id|
|
|
27
|
+
Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after:,
|
|
28
|
+
fetch_position:)
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
|
|
@@ -99,7 +102,7 @@ module Tarantool
|
|
|
99
102
|
private
|
|
100
103
|
|
|
101
104
|
# 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)
|
|
105
|
+
def on_space(space, index, as:, timeout:, format: false, page: false)
|
|
103
106
|
TupleShape.check(as)
|
|
104
107
|
space_info = nil
|
|
105
108
|
needs_format = format || as != :tuple
|
|
@@ -107,8 +110,9 @@ module Tarantool
|
|
|
107
110
|
space_info = info
|
|
108
111
|
yield space_id, index_id, info
|
|
109
112
|
end
|
|
113
|
+
tuples = response.data.map { |tuple| TupleShape.apply(as, tuple, space_info) }
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
page ? Page.new(tuples:, position: response.position) : tuples
|
|
112
116
|
end
|
|
113
117
|
|
|
114
118
|
def tuple_for(tuple, space_info) = tuple.is_a?(Hash) ? space_info.to_tuple(tuple) : tuple
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# One page of a select asked with `fetch_position: true`: the tuples (converted by `as:`) and the position to
|
|
6
|
+
# continue after with `after:`, nil when the page is empty. Opaque: tied to the index and iterator that
|
|
7
|
+
# produced it.
|
|
8
|
+
Page = Data.define(:tuples, :position) do
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
def each(&) = tuples.each(&)
|
|
12
|
+
|
|
13
|
+
def empty? = tuples.empty?
|
|
14
|
+
|
|
15
|
+
def size = tuples.size
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -17,16 +17,19 @@ module Tarantool
|
|
|
17
17
|
LIMIT = 0x12
|
|
18
18
|
OFFSET = 0x13
|
|
19
19
|
ITERATOR = 0x14
|
|
20
|
+
FETCH_POSITION = 0x1f
|
|
20
21
|
KEY = 0x20
|
|
21
22
|
TUPLE = 0x21
|
|
22
23
|
FUNCTION_NAME = 0x22
|
|
23
24
|
USER_NAME = 0x23
|
|
24
25
|
EXPR = 0x27
|
|
25
26
|
OPS = 0x28
|
|
27
|
+
AFTER_POSITION = 0x2e
|
|
26
28
|
AFTER_TUPLE = 0x2f
|
|
27
29
|
DATA = 0x30
|
|
28
30
|
ERROR_24 = 0x31
|
|
29
31
|
METADATA = 0x32
|
|
32
|
+
POSITION = 0x35
|
|
30
33
|
SQL_TEXT = 0x40
|
|
31
34
|
SQL_BIND = 0x41
|
|
32
35
|
SQL_INFO = 0x42
|
|
@@ -4,11 +4,17 @@ module Tarantool
|
|
|
4
4
|
class Client
|
|
5
5
|
module Protocol
|
|
6
6
|
module Requests
|
|
7
|
-
# IPROTO_SELECT: index lookup or scan. `after` continues past a tuple (
|
|
7
|
+
# IPROTO_SELECT: index lookup or scan. `after` continues past a tuple (Array) or a position (String, from
|
|
8
|
+
# a response asked with `fetch_position`); keyset pagination, 2.11+.
|
|
8
9
|
# @api private
|
|
9
|
-
Select = Data.define(:space_id, :index_id, :key, :limit, :offset, :iterator, :after) do
|
|
10
|
-
def
|
|
10
|
+
Select = Data.define(:space_id, :index_id, :key, :limit, :offset, :iterator, :after, :fetch_position) do
|
|
11
|
+
def self.cursor?(after) = after.nil? || after.is_a?(Array) || after.is_a?(String)
|
|
12
|
+
|
|
13
|
+
def initialize(iterator:, after: nil, fetch_position: false, **)
|
|
11
14
|
raise ArgumentError, "unknown iterator: #{iterator.inspect}" unless Constants::ITERATORS.key?(iterator)
|
|
15
|
+
unless Select.cursor?(after)
|
|
16
|
+
raise ArgumentError, "after: expected a tuple (Array) or a position (String), got #{after.inspect}"
|
|
17
|
+
end
|
|
12
18
|
|
|
13
19
|
super
|
|
14
20
|
end
|
|
@@ -22,8 +28,19 @@ module Tarantool
|
|
|
22
28
|
Constants::LIMIT => limit,
|
|
23
29
|
Constants::OFFSET => offset,
|
|
24
30
|
Constants::ITERATOR => Constants::ITERATORS.fetch(iterator),
|
|
25
|
-
Constants::KEY => key
|
|
26
|
-
|
|
31
|
+
Constants::KEY => key,
|
|
32
|
+
**pagination
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def pagination
|
|
39
|
+
keys = {}
|
|
40
|
+
keys[after.is_a?(String) ? Constants::AFTER_POSITION : Constants::AFTER_TUPLE] = after unless after.nil?
|
|
41
|
+
keys[Constants::FETCH_POSITION] = true if fetch_position
|
|
42
|
+
|
|
43
|
+
keys
|
|
27
44
|
end
|
|
28
45
|
end
|
|
29
46
|
end
|
|
@@ -17,6 +17,7 @@ module Tarantool
|
|
|
17
17
|
def sync = header[Constants::SYNC]
|
|
18
18
|
def schema_version = header[Constants::SCHEMA_VERSION]
|
|
19
19
|
def data = body[Constants::DATA]
|
|
20
|
+
def position = body[Constants::POSITION]
|
|
20
21
|
def error? = type.anybits?(Constants::TYPE_ERROR)
|
|
21
22
|
def push? = type == Constants::CHUNK
|
|
22
23
|
def event? = type == Constants::EVENT
|
|
@@ -58,6 +58,14 @@ module Tarantool
|
|
|
58
58
|
!@pending.failure.nil?
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def in_flight = @pending.size
|
|
62
|
+
|
|
63
|
+
# True once every request got its answer, false at the deadline.
|
|
64
|
+
def wait_idle(timeout) = @pending.wait_empty(timeout)
|
|
65
|
+
|
|
66
|
+
# Ends the connection on the client's initiative with a reason the requests and the disconnect handler see.
|
|
67
|
+
def shut_down(reason) = break_connection(reason)
|
|
68
|
+
|
|
61
69
|
def failure = @pending.failure
|
|
62
70
|
|
|
63
71
|
private
|
|
@@ -5,16 +5,16 @@ 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
11
|
include LinkEvents
|
|
12
|
+
include LinkDraining
|
|
12
13
|
|
|
13
14
|
attr_writer :event_handler
|
|
14
15
|
|
|
15
|
-
# on_connect runs on each new connection before requests can use it and may raise to fail the
|
|
16
|
-
#
|
|
17
|
-
# before its first attempt.
|
|
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
18
|
def initialize(config, on_connect:, on_ready:, on_lost: nil, backoff: nil, instrumentation: nil)
|
|
19
19
|
@config = config
|
|
20
20
|
@on_connect = on_connect
|
|
@@ -52,14 +52,16 @@ module Tarantool
|
|
|
52
52
|
def greeting = @state.connection.greeting
|
|
53
53
|
def connected? = @state.connected?
|
|
54
54
|
|
|
55
|
-
# 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.
|
|
56
57
|
def close
|
|
57
|
-
reconnector, attempt = @state.close
|
|
58
|
+
reconnector, attempt, drainer = @state.close
|
|
58
59
|
attempt&.close
|
|
59
60
|
begin
|
|
60
61
|
ThreadJoin.join(reconnector)
|
|
61
62
|
ensure
|
|
62
63
|
@state.connection&.close
|
|
64
|
+
ThreadJoin.join(drainer)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
nil
|
|
@@ -137,13 +139,6 @@ module Tarantool
|
|
|
137
139
|
rescue StandardError => e
|
|
138
140
|
e
|
|
139
141
|
end
|
|
140
|
-
|
|
141
|
-
# A failing hook is the caller's bug, not a reason to stop reconnecting or to drop the new connection.
|
|
142
|
-
def run_hook(name)
|
|
143
|
-
yield
|
|
144
|
-
rescue StandardError => e
|
|
145
|
-
Diagnostics.warn(@config.logger, "#{name} failed for #{@config.address}: #{e.full_message}")
|
|
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
|
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
module Tarantool
|
|
4
4
|
class Client
|
|
5
5
|
module Transport
|
|
6
|
-
# Lifecycle events of a Link. Includers set @config, @state and @instrumentation, and provide
|
|
7
|
-
# #
|
|
6
|
+
# Lifecycle events and hooks of a Link. Includers set @config, @state and @instrumentation, and provide
|
|
7
|
+
# #install.
|
|
8
8
|
# @api private
|
|
9
9
|
module LinkEvents
|
|
10
10
|
private
|
|
11
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
|
+
|
|
12
19
|
# Without reconnect the link keeps the lost connection, so the loss of the current one is all to
|
|
13
20
|
# report. The reader thread's disconnect handler and install's own loss check can both reach this
|
|
14
21
|
# for the same connection, so LinkState claims the loss once.
|
|
@@ -52,11 +52,34 @@ module Tarantool
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
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.
|
|
57
80
|
def start_reconnect(connection)
|
|
58
81
|
@lock.synchronize do
|
|
59
|
-
next unless @state
|
|
82
|
+
next unless %i[ready draining].include?(@state) && @connection.equal?(connection)
|
|
60
83
|
|
|
61
84
|
@reconnector = yield @reconnector
|
|
62
85
|
@state = :reconnecting
|
|
@@ -64,7 +87,8 @@ module Tarantool
|
|
|
64
87
|
end
|
|
65
88
|
end
|
|
66
89
|
|
|
67
|
-
# 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.
|
|
68
92
|
def close
|
|
69
93
|
@lock.synchronize do
|
|
70
94
|
@error = ClientClosedError.new("connection to #{@address} closed")
|
|
@@ -72,7 +96,7 @@ module Tarantool
|
|
|
72
96
|
@state = :closed
|
|
73
97
|
@changed.broadcast
|
|
74
98
|
end
|
|
75
|
-
[@reconnector, @attempt]
|
|
99
|
+
[@reconnector, @attempt, @drainer]
|
|
76
100
|
end
|
|
77
101
|
end
|
|
78
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) }
|
data/lib/tarantool/client.rb
CHANGED
|
@@ -58,6 +58,7 @@ require_relative 'client/transport/handshake'
|
|
|
58
58
|
require_relative 'client/transport/connection'
|
|
59
59
|
require_relative 'client/transport/link_state'
|
|
60
60
|
require_relative 'client/transport/link_events'
|
|
61
|
+
require_relative 'client/transport/link_draining'
|
|
61
62
|
require_relative 'client/transport/link'
|
|
62
63
|
require_relative 'client/index_info'
|
|
63
64
|
require_relative 'client/tuple_mapping'
|
|
@@ -69,6 +70,7 @@ require_relative 'client/prepared_statement'
|
|
|
69
70
|
require_relative 'client/batch_delete'
|
|
70
71
|
require_relative 'client/transaction_schema'
|
|
71
72
|
require_relative 'client/row'
|
|
73
|
+
require_relative 'client/page'
|
|
72
74
|
require_relative 'client/tuple_shape'
|
|
73
75
|
require_relative 'client/operations'
|
|
74
76
|
require_relative 'client/transaction_scope'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/tarantool/client/interval.rb
|
|
69
69
|
- lib/tarantool/client/named_requests.rb
|
|
70
70
|
- lib/tarantool/client/operations.rb
|
|
71
|
+
- lib/tarantool/client/page.rb
|
|
71
72
|
- lib/tarantool/client/pool.rb
|
|
72
73
|
- lib/tarantool/client/pool/member.rb
|
|
73
74
|
- lib/tarantool/client/pool/member_fork_safety.rb
|
|
@@ -117,6 +118,7 @@ files:
|
|
|
117
118
|
- lib/tarantool/client/transport/future.rb
|
|
118
119
|
- lib/tarantool/client/transport/handshake.rb
|
|
119
120
|
- lib/tarantool/client/transport/link.rb
|
|
121
|
+
- lib/tarantool/client/transport/link_draining.rb
|
|
120
122
|
- lib/tarantool/client/transport/link_events.rb
|
|
121
123
|
- lib/tarantool/client/transport/link_state.rb
|
|
122
124
|
- lib/tarantool/client/transport/pending_requests.rb
|