tarantool-client 0.2.0 → 0.3.1
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 +9 -0
- data/README.md +20 -1
- data/lib/tarantool/client/configuration.rb +9 -3
- 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/pool/member_role.rb +2 -2
- data/lib/tarantool/client/protocol/constants.rb +3 -0
- data/lib/tarantool/client/protocol/greeting.rb +4 -2
- 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/uuid.rb +4 -2
- data/lib/tarantool/client/version.rb +1 -1
- data/lib/tarantool/client.rb +2 -0
- data/sig/tarantool/client/pool.rbs +41 -0
- data/sig/tarantool/client/stream.rbs +26 -0
- data/sig/tarantool/client/values.rbs +179 -0
- data/sig/tarantool/client.rbs +73 -0
- data/sig/tarantool/errors.rbs +47 -0
- metadata +17 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 046c9d293325df27a2971c6c6257cd6691e3e6c2cb565ea41eb9cb1aa5a2ccf3
|
|
4
|
+
data.tar.gz: 2dc134d5da8d6f5a99fd8b73f28340ae022ae9243c62d750287e35e5d088d159
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8cb3cae463060a270127eea6427eacef15edd25ec159d6d377f97af2692d9fc7fade4fef6b871943cfe78387d740621c5061b583bfe2ae755ca06f8e8a7f282c
|
|
7
|
+
data.tar.gz: c456c5a8fa8a0c8de99804f29fd0c36831a35f692391c3cda7c582f1901fc7ee46cbf7fc709fed866bcba121b6fa4b5de6bbc4da6a5ea8ebe3e380f99a133a18
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
- Allows `bigdecimal` 4.x, the version Ruby 4 ships with.
|
|
6
|
+
|
|
7
|
+
## 0.3.0 (2026-10-17)
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
- 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`).
|
|
11
|
+
|
|
3
12
|
## 0.2.0 (2026-10-17)
|
|
4
13
|
|
|
5
14
|
- `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,9 +306,11 @@ 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
|
|
|
312
|
+
RBS signatures for this API ship with the gem in `sig/`.
|
|
313
|
+
|
|
295
314
|
Everything else, including anything tagged `@api private` in its source comment, is an implementation detail
|
|
296
315
|
and may change in any release.
|
|
297
316
|
|
|
@@ -24,15 +24,15 @@ module Tarantool
|
|
|
24
24
|
|
|
25
25
|
def self.parse_uri(uri)
|
|
26
26
|
return {} if uri.nil?
|
|
27
|
-
return parse_unix_uri(uri) if uri.match?(
|
|
27
|
+
return parse_unix_uri(uri) if uri.match?(Configuration::UNIX_URI)
|
|
28
28
|
|
|
29
29
|
parse_tcp_uri(uri)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def self.parse_unix_uri(uri)
|
|
33
|
-
match =
|
|
33
|
+
match = Configuration::UNIX_PATH.match(uri)
|
|
34
34
|
path = match && match[1]
|
|
35
|
-
raise ArgumentError, 'invalid Tarantool URI' if path.nil? || path.match?(
|
|
35
|
+
raise ArgumentError, 'invalid Tarantool URI' if path.nil? || path.match?(Configuration::EMPTY_PATH)
|
|
36
36
|
|
|
37
37
|
{ path: path }
|
|
38
38
|
end
|
|
@@ -84,5 +84,11 @@ module Tarantool
|
|
|
84
84
|
klass.name ? "#<#{klass.name}>" : klass.inspect
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
class Configuration
|
|
89
|
+
UNIX_URI = %r{\Aunix/?:}
|
|
90
|
+
UNIX_PATH = %r{\Aunix/?:(?://(?=/))?(.+)\z}
|
|
91
|
+
EMPTY_PATH = %r{\A/*\z}
|
|
92
|
+
end
|
|
87
93
|
end
|
|
88
94
|
end
|
|
@@ -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
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Tarantool
|
|
4
4
|
class Client
|
|
5
5
|
class Pool
|
|
6
|
-
# The role a member's instance reports through box.status. Includers set @lock, @changed, @read_only
|
|
7
|
-
# @instrumentation.
|
|
6
|
+
# The role a member's instance reports through box.status. Includers set @lock, @changed, @read_only
|
|
7
|
+
# and @instrumentation.
|
|
8
8
|
# @api private
|
|
9
9
|
module MemberRole
|
|
10
10
|
private
|
|
@@ -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
|
|
@@ -6,8 +6,6 @@ module Tarantool
|
|
|
6
6
|
# Greeting sent by the server on connect: a 64-byte banner and a 64-byte base64 salt line.
|
|
7
7
|
# @api private
|
|
8
8
|
Greeting = Data.define(:version, :salt) do
|
|
9
|
-
self::BANNER_VERSION = /\ATarantool (\S+)/
|
|
10
|
-
|
|
11
9
|
def self.parse(bytes)
|
|
12
10
|
banner = bytes.byteslice(0, 64)
|
|
13
11
|
version = banner[Greeting::BANNER_VERSION, 1]
|
|
@@ -16,6 +14,10 @@ module Tarantool
|
|
|
16
14
|
new(version:, salt: bytes.byteslice(64, 64).strip.unpack1('m'))
|
|
17
15
|
end
|
|
18
16
|
end
|
|
17
|
+
|
|
18
|
+
class Greeting
|
|
19
|
+
BANNER_VERSION = /\ATarantool (\S+)/
|
|
20
|
+
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -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) }
|
|
@@ -4,8 +4,6 @@ module Tarantool
|
|
|
4
4
|
class Client
|
|
5
5
|
# A Tarantool uuid value; only instances of this type are sent as MP_EXT uuid, never plain strings.
|
|
6
6
|
UUID = Data.define(:value) do
|
|
7
|
-
self::FORMAT = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
8
|
-
|
|
9
7
|
def initialize(value:)
|
|
10
8
|
raise ArgumentError, "invalid UUID: #{value.inspect}" unless value.is_a?(String) && value.match?(UUID::FORMAT)
|
|
11
9
|
|
|
@@ -16,5 +14,9 @@ module Tarantool
|
|
|
16
14
|
value
|
|
17
15
|
end
|
|
18
16
|
end
|
|
17
|
+
|
|
18
|
+
class UUID
|
|
19
|
+
FORMAT = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
20
|
+
end
|
|
19
21
|
end
|
|
20
22
|
end
|
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'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Tarantool
|
|
2
|
+
class Client
|
|
3
|
+
# Clients for the instances of one replica set, routed by the role each reports through box.status.
|
|
4
|
+
class Pool
|
|
5
|
+
type mode = :rw | :ro | :prefer_ro | :prefer_rw | :any
|
|
6
|
+
|
|
7
|
+
MODES: Array[mode]
|
|
8
|
+
|
|
9
|
+
def initialize: (Array[String] addresses, ?host: String, ?port: Integer, ?path: String?, ?user: String?,
|
|
10
|
+
?password: String?, ?connect_timeout: Numeric?, ?request_timeout: Numeric?, ?reconnect: bool,
|
|
11
|
+
?reconnect_attempts: Numeric, ?reconnect_delay: Numeric, ?reconnect_max_delay: Numeric,
|
|
12
|
+
?max_frame_size: Integer, ?instrumenter: _Instrumenter?, ?logger: _Logger?) -> void
|
|
13
|
+
|
|
14
|
+
def ping: (?mode: mode, ?timeout: Numeric?) -> true
|
|
15
|
+
def space_info: (space_ref space, ?mode: mode, ?timeout: Numeric?) -> SpaceInfo
|
|
16
|
+
def select: (space_ref space, ?key key, ?mode: mode, ?index: index_ref, ?limit: Integer, ?offset: Integer,
|
|
17
|
+
?iterator: iterator, ?after: cursor?, ?fetch_position: bool, ?as: shape,
|
|
18
|
+
?timeout: Numeric?) -> (Array[row] | Page)
|
|
19
|
+
def insert: (space_ref space, tuple_input tuple, ?mode: mode, ?as: shape, ?timeout: Numeric?) -> row
|
|
20
|
+
def replace: (space_ref space, tuple_input tuple, ?mode: mode, ?as: shape, ?timeout: Numeric?) -> row
|
|
21
|
+
def update: (space_ref space, key key, Array[operation] operations, ?mode: mode, ?index: index_ref, ?as: shape,
|
|
22
|
+
?timeout: Numeric?) -> row?
|
|
23
|
+
def upsert: (space_ref space, tuple_input tuple, Array[operation] operations, ?mode: mode,
|
|
24
|
+
?timeout: Numeric?) -> nil
|
|
25
|
+
def delete: (space_ref space, key key, ?mode: mode, ?index: index_ref, ?as: shape, ?timeout: Numeric?) -> row?
|
|
26
|
+
def delete_by: (space_ref space, key key, index: index_ref, ?mode: mode, ?iterator: iterator,
|
|
27
|
+
?batch_size: Integer, ?timeout: Numeric?) -> Integer
|
|
28
|
+
def call: (String function_name, ?Array[untyped] arguments, ?mode: mode, ?timeout: Numeric?,
|
|
29
|
+
?on_push: (^(untyped message) -> void)?) -> Array[untyped]
|
|
30
|
+
def eval: (String expression, ?Array[untyped] arguments, ?mode: mode, ?timeout: Numeric?) -> Array[untyped]
|
|
31
|
+
def execute: (String | Integer | PreparedStatement sql, ?Array[untyped] bindings, ?mode: mode,
|
|
32
|
+
?timeout: Numeric?) -> SqlResult
|
|
33
|
+
def prepare: (String sql, ?mode: mode, ?timeout: Numeric?) -> PreparedStatement
|
|
34
|
+
|
|
35
|
+
# Always on a writable instance.
|
|
36
|
+
def transaction: [T] (?isolation: isolation, ?timeout: Numeric?) { (Stream) -> T } -> T
|
|
37
|
+
def close: () -> void
|
|
38
|
+
def inspect: () -> String
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Tarantool
|
|
2
|
+
class Client
|
|
3
|
+
# The operations of one interactive transaction, yielded by Client#transaction and Pool#transaction.
|
|
4
|
+
class Stream
|
|
5
|
+
def ping: (?timeout: Numeric?) -> true
|
|
6
|
+
def space_info: (space_ref space, ?timeout: Numeric?) -> SpaceInfo
|
|
7
|
+
def select: (space_ref space, ?key key, ?index: index_ref, ?limit: Integer, ?offset: Integer,
|
|
8
|
+
?iterator: iterator, ?after: cursor?, ?fetch_position: bool, ?as: shape,
|
|
9
|
+
?timeout: Numeric?) -> (Array[row] | Page)
|
|
10
|
+
def insert: (space_ref space, tuple_input tuple, ?as: shape, ?timeout: Numeric?) -> row
|
|
11
|
+
def replace: (space_ref space, tuple_input tuple, ?as: shape, ?timeout: Numeric?) -> row
|
|
12
|
+
def update: (space_ref space, key key, Array[operation] operations, ?index: index_ref, ?as: shape,
|
|
13
|
+
?timeout: Numeric?) -> row?
|
|
14
|
+
def upsert: (space_ref space, tuple_input tuple, Array[operation] operations, ?timeout: Numeric?) -> nil
|
|
15
|
+
def delete: (space_ref space, key key, ?index: index_ref, ?as: shape, ?timeout: Numeric?) -> row?
|
|
16
|
+
def delete_by: (space_ref space, key key, index: index_ref, ?iterator: iterator, ?batch_size: Integer,
|
|
17
|
+
?timeout: Numeric?) -> Integer
|
|
18
|
+
def call: (String function_name, ?Array[untyped] arguments, ?timeout: Numeric?,
|
|
19
|
+
?on_push: (^(untyped message) -> void)?) -> Array[untyped]
|
|
20
|
+
def eval: (String expression, ?Array[untyped] arguments, ?timeout: Numeric?) -> Array[untyped]
|
|
21
|
+
def execute: (String | Integer | PreparedStatement sql, ?Array[untyped] bindings, ?timeout: Numeric?) -> SqlResult
|
|
22
|
+
# Raises TransactionError: prepare on the client, execute the statement here.
|
|
23
|
+
def prepare: (*untyped) -> bot
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
module Tarantool
|
|
2
|
+
class Client
|
|
3
|
+
# Subscription returned by Client#watch.
|
|
4
|
+
class Watcher
|
|
5
|
+
attr_reader key: String
|
|
6
|
+
|
|
7
|
+
def unwatch: () -> nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Server-side prepared SQL statement, bound to the session that prepared it.
|
|
11
|
+
class PreparedStatement
|
|
12
|
+
attr_reader id: Integer
|
|
13
|
+
|
|
14
|
+
def execute: (?Array[untyped] bindings, ?timeout: Numeric?) -> SqlResult
|
|
15
|
+
def close: (?timeout: Numeric?) -> nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Rows and column names of an SQL query, or the counters of a data-changing statement.
|
|
19
|
+
class SqlResult < Data
|
|
20
|
+
attr_reader rows: Array[Array[untyped]]
|
|
21
|
+
attr_reader columns: Array[String]
|
|
22
|
+
attr_reader row_count: Integer?
|
|
23
|
+
attr_reader autoincrement_ids: Array[Integer]
|
|
24
|
+
|
|
25
|
+
def self.new: (rows: Array[Array[untyped]], columns: Array[String], row_count: Integer?,
|
|
26
|
+
autoincrement_ids: Array[Integer]) -> SqlResult
|
|
27
|
+
def with: (?rows: Array[Array[untyped]], ?columns: Array[String], ?row_count: Integer?,
|
|
28
|
+
?autoincrement_ids: Array[Integer]) -> SqlResult
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A tuple as the server returned it, with its fields by name. Returned for `as: :row`.
|
|
32
|
+
class Row < Data
|
|
33
|
+
attr_reader tuple: tuple
|
|
34
|
+
attr_reader attributes: row_hash
|
|
35
|
+
|
|
36
|
+
def self.new: (tuple: tuple, attributes: row_hash) -> Row
|
|
37
|
+
def with: (?tuple: tuple, ?attributes: row_hash) -> Row
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One page of a select asked with `fetch_position: true`.
|
|
41
|
+
class Page < Data
|
|
42
|
+
include Enumerable[row]
|
|
43
|
+
|
|
44
|
+
attr_reader tuples: Array[row]
|
|
45
|
+
# Opaque, tied to the index and iterator that produced it; nil when the page is empty.
|
|
46
|
+
attr_reader position: String?
|
|
47
|
+
|
|
48
|
+
def self.new: (tuples: Array[row], position: String?) -> Page
|
|
49
|
+
def with: (?tuples: Array[row], ?position: String?) -> Page
|
|
50
|
+
def each: () { (row) -> void } -> Array[row]
|
|
51
|
+
| () -> Enumerator[row, Array[row]]
|
|
52
|
+
def empty?: () -> bool
|
|
53
|
+
def size: () -> Integer
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Format and indexes of a space from the loaded schema; `indexes` maps names to IndexInfo in id order.
|
|
57
|
+
class SpaceInfo < Data
|
|
58
|
+
attr_reader id: Integer
|
|
59
|
+
attr_reader name: String
|
|
60
|
+
attr_reader fields: Array[SpaceInfo::Field]
|
|
61
|
+
attr_reader indexes: Hash[String, IndexInfo]
|
|
62
|
+
|
|
63
|
+
def self.new: (id: Integer, name: String, fields: Array[SpaceInfo::Field], indexes: Hash[String, IndexInfo]) -> SpaceInfo
|
|
64
|
+
def with: (?id: Integer, ?name: String, ?fields: Array[SpaceInfo::Field], ?indexes: Hash[String, IndexInfo]) -> SpaceInfo
|
|
65
|
+
def primary_index: () -> IndexInfo?
|
|
66
|
+
def to_hash: (tuple tuple) -> row_hash
|
|
67
|
+
# An unknown name raises UnknownFieldError; missing fields go out as nil.
|
|
68
|
+
def to_tuple: (Hash[Symbol | String | Integer, untyped] hash) -> tuple
|
|
69
|
+
|
|
70
|
+
class Field < Data
|
|
71
|
+
attr_reader name: String
|
|
72
|
+
attr_reader type: String
|
|
73
|
+
attr_reader is_nullable: bool
|
|
74
|
+
|
|
75
|
+
def self.new: (name: String, type: String, is_nullable: bool) -> Field
|
|
76
|
+
def with: (?name: String, ?type: String, ?is_nullable: bool) -> Field
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# One index of a space.
|
|
81
|
+
class IndexInfo < Data
|
|
82
|
+
attr_reader id: Integer
|
|
83
|
+
attr_reader name: String
|
|
84
|
+
attr_reader type: String
|
|
85
|
+
attr_reader unique: bool
|
|
86
|
+
attr_reader parts: Array[IndexInfo::Part]
|
|
87
|
+
|
|
88
|
+
def self.new: (id: Integer, name: String, type: String, unique: bool, parts: Array[IndexInfo::Part]) -> IndexInfo
|
|
89
|
+
def with: (?id: Integer, ?name: String, ?type: String, ?unique: bool, ?parts: Array[IndexInfo::Part]) -> IndexInfo
|
|
90
|
+
|
|
91
|
+
# `fieldno` is 0-based; `name` is nil for a field beyond the space format.
|
|
92
|
+
class Part < Data
|
|
93
|
+
attr_reader fieldno: Integer
|
|
94
|
+
attr_reader name: String?
|
|
95
|
+
attr_reader type: String
|
|
96
|
+
attr_reader is_nullable: bool
|
|
97
|
+
attr_reader path: String?
|
|
98
|
+
|
|
99
|
+
def self.new: (fieldno: Integer, name: String?, type: String, is_nullable: bool, path: String?) -> Part
|
|
100
|
+
def with: (?fieldno: Integer, ?name: String?, ?type: String, ?is_nullable: bool, ?path: String?) -> Part
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A Tarantool uuid; only instances of this type are sent as MP_EXT uuid.
|
|
105
|
+
class UUID < Data
|
|
106
|
+
FORMAT: Regexp
|
|
107
|
+
|
|
108
|
+
attr_reader value: String
|
|
109
|
+
|
|
110
|
+
def self.new: (String value) -> UUID
|
|
111
|
+
| (value: String) -> UUID
|
|
112
|
+
def with: (?value: String) -> UUID
|
|
113
|
+
def to_s: () -> String
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A Tarantool datetime.interval; adjust is :none, :excess or :last.
|
|
117
|
+
class Interval < Data
|
|
118
|
+
type adjust = :none | :excess | :last
|
|
119
|
+
|
|
120
|
+
attr_reader year: Integer
|
|
121
|
+
attr_reader month: Integer
|
|
122
|
+
attr_reader week: Integer
|
|
123
|
+
attr_reader day: Integer
|
|
124
|
+
attr_reader hour: Integer
|
|
125
|
+
attr_reader min: Integer
|
|
126
|
+
attr_reader sec: Integer
|
|
127
|
+
attr_reader nsec: Integer
|
|
128
|
+
attr_reader adjust: adjust
|
|
129
|
+
|
|
130
|
+
def self.new: (?year: Integer, ?month: Integer, ?week: Integer, ?day: Integer, ?hour: Integer, ?min: Integer,
|
|
131
|
+
?sec: Integer, ?nsec: Integer, ?adjust: adjust) -> Interval
|
|
132
|
+
def with: (?year: Integer, ?month: Integer, ?week: Integer, ?day: Integer, ?hour: Integer, ?min: Integer,
|
|
133
|
+
?sec: Integer, ?nsec: Integer, ?adjust: adjust) -> Interval
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A box.error object returned as data (MP_ERROR), with its cause chain.
|
|
137
|
+
class BoxError < Data
|
|
138
|
+
attr_reader type: String?
|
|
139
|
+
attr_reader message: String?
|
|
140
|
+
attr_reader code: Integer?
|
|
141
|
+
attr_reader file: String?
|
|
142
|
+
attr_reader line: Integer?
|
|
143
|
+
attr_reader errno: Integer?
|
|
144
|
+
attr_reader fields: Hash[String, untyped]?
|
|
145
|
+
attr_reader cause: BoxError?
|
|
146
|
+
|
|
147
|
+
def self.new: (type: String?, message: String?, code: Integer?, file: String?, line: Integer?, errno: Integer?,
|
|
148
|
+
fields: Hash[String, untyped]?, cause: BoxError?) -> BoxError
|
|
149
|
+
def with: (?type: String?, ?message: String?, ?code: Integer?, ?file: String?, ?line: Integer?, ?errno: Integer?,
|
|
150
|
+
?fields: Hash[String, untyped]?, ?cause: BoxError?) -> BoxError
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Immutable connection settings; `inspect` and `to_s` hide the password, `to_h` keeps it.
|
|
154
|
+
class Configuration < Data
|
|
155
|
+
attr_reader host: String
|
|
156
|
+
attr_reader port: Integer
|
|
157
|
+
attr_reader path: String?
|
|
158
|
+
attr_reader user: String?
|
|
159
|
+
attr_reader password: String?
|
|
160
|
+
attr_reader connect_timeout: Numeric?
|
|
161
|
+
attr_reader request_timeout: Numeric?
|
|
162
|
+
attr_reader reconnect: bool
|
|
163
|
+
attr_reader reconnect_attempts: Numeric
|
|
164
|
+
attr_reader reconnect_delay: Numeric
|
|
165
|
+
attr_reader reconnect_max_delay: Numeric
|
|
166
|
+
attr_reader max_frame_size: Integer
|
|
167
|
+
attr_reader instrumenter: _Instrumenter?
|
|
168
|
+
attr_reader logger: _Logger?
|
|
169
|
+
|
|
170
|
+
def self.build: (?String? uri, ?host: String, ?port: Integer, ?path: String?, ?user: String?, ?password: String?,
|
|
171
|
+
?connect_timeout: Numeric?, ?request_timeout: Numeric?, ?reconnect: bool,
|
|
172
|
+
?reconnect_attempts: Numeric, ?reconnect_delay: Numeric, ?reconnect_max_delay: Numeric,
|
|
173
|
+
?max_frame_size: Integer, ?instrumenter: _Instrumenter?, ?logger: _Logger?) -> Configuration
|
|
174
|
+
def address: () -> String
|
|
175
|
+
def inspect: () -> String
|
|
176
|
+
def to_s: () -> String
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Tarantool
|
|
2
|
+
# Connection to one Tarantool instance.
|
|
3
|
+
class Client
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
# A space or index, by name or numeric id.
|
|
7
|
+
type space_ref = String | Symbol | Integer
|
|
8
|
+
type index_ref = String | Symbol | Integer
|
|
9
|
+
# A tuple as sent and received: an Array of msgpack values.
|
|
10
|
+
type tuple = Array[untyped]
|
|
11
|
+
# A tuple keyed by field name; fields beyond the format are keyed by their 0-based position.
|
|
12
|
+
type row_hash = Hash[Symbol | Integer, untyped]
|
|
13
|
+
# Tuples go out as Arrays or as Hashes keyed by field name (Symbol or String).
|
|
14
|
+
type tuple_input = tuple | Hash[Symbol | String, untyped]
|
|
15
|
+
# One msgpack value of a key part.
|
|
16
|
+
type scalar = Integer | Float | String | bool | nil | BigDecimal | Time | UUID | Interval
|
|
17
|
+
# An index key: the parts as an Array, or one scalar for a single-part key.
|
|
18
|
+
type key = Array[untyped] | scalar
|
|
19
|
+
type iterator = :eq | :req | :all | :lt | :le | :ge | :gt | :bits_all_set | :bits_any_set | :bits_all_not_set |
|
|
20
|
+
:overlaps | :neighbor | :np | :pp
|
|
21
|
+
type shape = :tuple | :hash | :row
|
|
22
|
+
# One update operation, e.g. ['=', 'name', value] or ['+', 2, 1]; splice operations have five elements.
|
|
23
|
+
type operation = Array[untyped]
|
|
24
|
+
# What select returns for one tuple, by `as:`.
|
|
25
|
+
type row = tuple | row_hash | Row
|
|
26
|
+
type isolation = :default | :read_committed | :read_confirmed | :best_effort | :linearizable
|
|
27
|
+
# A position from Page#position, or the last tuple of the previous page.
|
|
28
|
+
type cursor = String | tuple
|
|
29
|
+
|
|
30
|
+
interface _Instrumenter
|
|
31
|
+
def instrument: (String name, Hash[Symbol, untyped] payload) { () -> untyped } -> untyped
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
interface _Logger
|
|
35
|
+
def debug: (?untyped message) ?{ () -> untyped } -> untyped
|
|
36
|
+
def info: (?untyped message) ?{ () -> untyped } -> untyped
|
|
37
|
+
def warn: (?untyped message) ?{ () -> untyped } -> untyped
|
|
38
|
+
def error: (?untyped message) ?{ () -> untyped } -> untyped
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader config: Configuration
|
|
42
|
+
|
|
43
|
+
def initialize: (?String? uri, ?host: String, ?port: Integer, ?path: String?, ?user: String?, ?password: String?,
|
|
44
|
+
?connect_timeout: Numeric?, ?request_timeout: Numeric?, ?reconnect: bool,
|
|
45
|
+
?reconnect_attempts: Numeric, ?reconnect_delay: Numeric, ?reconnect_max_delay: Numeric,
|
|
46
|
+
?max_frame_size: Integer, ?instrumenter: _Instrumenter?, ?logger: _Logger?) -> void
|
|
47
|
+
|
|
48
|
+
def ping: (?timeout: Numeric?) -> true
|
|
49
|
+
def space_info: (space_ref space, ?timeout: Numeric?) -> SpaceInfo
|
|
50
|
+
def select: (space_ref space, ?key key, ?index: index_ref, ?limit: Integer, ?offset: Integer, ?iterator: iterator,
|
|
51
|
+
?after: cursor?, ?fetch_position: bool, ?as: shape, ?timeout: Numeric?) -> (Array[row] | Page)
|
|
52
|
+
def insert: (space_ref space, tuple_input tuple, ?as: shape, ?timeout: Numeric?) -> row
|
|
53
|
+
def replace: (space_ref space, tuple_input tuple, ?as: shape, ?timeout: Numeric?) -> row
|
|
54
|
+
def update: (space_ref space, key key, Array[operation] operations, ?index: index_ref, ?as: shape,
|
|
55
|
+
?timeout: Numeric?) -> row?
|
|
56
|
+
def upsert: (space_ref space, tuple_input tuple, Array[operation] operations, ?timeout: Numeric?) -> nil
|
|
57
|
+
def delete: (space_ref space, key key, ?index: index_ref, ?as: shape, ?timeout: Numeric?) -> row?
|
|
58
|
+
def delete_by: (space_ref space, key key, index: index_ref, ?iterator: iterator, ?batch_size: Integer,
|
|
59
|
+
?timeout: Numeric?) -> Integer
|
|
60
|
+
def call: (String function_name, ?Array[untyped] arguments, ?timeout: Numeric?,
|
|
61
|
+
?on_push: (^(untyped message) -> void)?) -> Array[untyped]
|
|
62
|
+
def eval: (String expression, ?Array[untyped] arguments, ?timeout: Numeric?) -> Array[untyped]
|
|
63
|
+
def execute: (String | Integer | PreparedStatement sql, ?Array[untyped] bindings, ?timeout: Numeric?) -> SqlResult
|
|
64
|
+
def prepare: (String sql, ?timeout: Numeric?) -> PreparedStatement
|
|
65
|
+
|
|
66
|
+
def server_version: () -> String
|
|
67
|
+
def connected?: () -> bool
|
|
68
|
+
def transaction: [T] (?isolation: isolation, ?timeout: Numeric?) { (Stream) -> T } -> T
|
|
69
|
+
def watch: (String key) { (String key, untyped value) -> void } -> Watcher
|
|
70
|
+
def close: () -> void
|
|
71
|
+
def inspect: () -> String
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Tarantool
|
|
2
|
+
# Base class for every error raised by the tarantool-* gems.
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
# Rows delete_by confirmed deleted before it failed; nil for any other operation.
|
|
5
|
+
attr_accessor deleted: Integer?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class ConnectionError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class ClientClosedError < ConnectionError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class TimeoutError < Error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class AuthenticationError < Error
|
|
18
|
+
attr_reader code: Integer?
|
|
19
|
+
|
|
20
|
+
def initialize: (?String? message, ?code: Integer?) -> void
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class ProtocolError < Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class SchemaError < Error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class UnknownFieldError < ArgumentError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class TransactionError < Error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class NoAvailableInstanceError < Error
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class ServerError < Error
|
|
39
|
+
attr_reader code: Integer
|
|
40
|
+
# The box.error stack as sent: one Hash per error (keys :type, :file, :line, :message, :errno, :code,
|
|
41
|
+
# :fields); the constructor takes whatever Array it is given.
|
|
42
|
+
attr_reader error_stack: Array[untyped]
|
|
43
|
+
attr_reader schema_version: Integer?
|
|
44
|
+
|
|
45
|
+
def initialize: (String? message, code: Integer, ?error_stack: Array[untyped], ?schema_version: Integer?) -> void
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
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.1
|
|
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-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '3.1'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
29
|
version: '3.1'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: msgpack
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -68,6 +74,7 @@ files:
|
|
|
68
74
|
- lib/tarantool/client/interval.rb
|
|
69
75
|
- lib/tarantool/client/named_requests.rb
|
|
70
76
|
- lib/tarantool/client/operations.rb
|
|
77
|
+
- lib/tarantool/client/page.rb
|
|
71
78
|
- lib/tarantool/client/pool.rb
|
|
72
79
|
- lib/tarantool/client/pool/member.rb
|
|
73
80
|
- lib/tarantool/client/pool/member_fork_safety.rb
|
|
@@ -117,6 +124,7 @@ files:
|
|
|
117
124
|
- lib/tarantool/client/transport/future.rb
|
|
118
125
|
- lib/tarantool/client/transport/handshake.rb
|
|
119
126
|
- lib/tarantool/client/transport/link.rb
|
|
127
|
+
- lib/tarantool/client/transport/link_draining.rb
|
|
120
128
|
- lib/tarantool/client/transport/link_events.rb
|
|
121
129
|
- lib/tarantool/client/transport/link_state.rb
|
|
122
130
|
- lib/tarantool/client/transport/pending_requests.rb
|
|
@@ -130,6 +138,11 @@ files:
|
|
|
130
138
|
- lib/tarantool/client/watcher.rb
|
|
131
139
|
- lib/tarantool/client/watchers.rb
|
|
132
140
|
- lib/tarantool/client/watchers/dispatcher.rb
|
|
141
|
+
- sig/tarantool/client.rbs
|
|
142
|
+
- sig/tarantool/client/pool.rbs
|
|
143
|
+
- sig/tarantool/client/stream.rbs
|
|
144
|
+
- sig/tarantool/client/values.rbs
|
|
145
|
+
- sig/tarantool/errors.rbs
|
|
133
146
|
homepage: https://github.com/Starlexxx/tarantool-ruby
|
|
134
147
|
licenses:
|
|
135
148
|
- MIT
|