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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 239078d6bcc2b8d55971852a3af66f3b1db2e40822bfec113f81442d7946bf08
4
- data.tar.gz: 8244a106fca7d641f7eaaf42377679de89ed2d1641d2c964f665d3bdff7953c9
3
+ metadata.gz: 046c9d293325df27a2971c6c6257cd6691e3e6c2cb565ea41eb9cb1aa5a2ccf3
4
+ data.tar.gz: 2dc134d5da8d6f5a99fd8b73f28340ae022ae9243c62d750287e35e5d088d159
5
5
  SHA512:
6
- metadata.gz: c3fb785f1d6362c9f3a6e94f951b1c761b73152ea7b76d7b1666726b1e130892aada79ea38c7f46be339557e4000dee41da8db0e547e0ee47e8f1c3df76a8daf
7
- data.tar.gz: 81fc563d05f48cc9cfa4d640fb00671d435951bea5cea85092394ab56914040967d99b89c0cc577bf6e37d71e0775c956060fe4fc5caecc4ec42941282e59495
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?(%r{\Aunix/?:})
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 = %r{\Aunix/?:(?://(?=/))?(.+)\z}.match(uri)
33
+ match = Configuration::UNIX_PATH.match(uri)
34
34
  path = match && match[1]
35
- raise ArgumentError, 'invalid Tarantool URI' if path.nil? || path.match?(%r{\A/*\z})
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
- response.data.map { |tuple| TupleShape.apply(as, tuple, space_info) }
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 and
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 (keyset pagination, 2.11+).
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 initialize(iterator:, after: nil, **)
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
- }.tap { |body| body[Constants::AFTER_TUPLE] = after unless after.nil? }
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
- # attempt; on_ready runs once the connection is current; on_lost runs when a reconnect starts,
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
- # #run_hook and #install.
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
- # Leaves ready only for the current connection, so a loss is handled once. The block gets the previous
56
- # reconnect thread and returns the new one; the state flips only once that thread exists.
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 == :ready && @connection.equal?(connection)
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 and the connection it is opening, for the caller to abort and join.
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 { @futures.delete(sync) }
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tarantool
4
4
  class Client
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.1'
6
6
  end
7
7
  end
@@ -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.2.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-17 00:00:00.000000000 Z
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