tarantool-client 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 239078d6bcc2b8d55971852a3af66f3b1db2e40822bfec113f81442d7946bf08
4
- data.tar.gz: 8244a106fca7d641f7eaaf42377679de89ed2d1641d2c964f665d3bdff7953c9
3
+ metadata.gz: 312e13dccce60b22bb3e74005e914cadcbf6dbbf096d378759a2697e6c0a1e8c
4
+ data.tar.gz: f0fd02d53f7bbb97ee347f7b81b1e9a9f83476a5af871042036b025c9cbb8060
5
5
  SHA512:
6
- metadata.gz: c3fb785f1d6362c9f3a6e94f951b1c761b73152ea7b76d7b1666726b1e130892aada79ea38c7f46be339557e4000dee41da8db0e547e0ee47e8f1c3df76a8daf
7
- data.tar.gz: 81fc563d05f48cc9cfa4d640fb00671d435951bea5cea85092394ab56914040967d99b89c0cc577bf6e37d71e0775c956060fe4fc5caecc4ec42941282e59495
6
+ metadata.gz: 81512742b3dfc97f726a1af22d2bbfd578cd083cc2dd75f3cbe3c4004bde4e1e73ee21ba03675581b7586d590c2624e6287bc8040ee281e3b3fe85ef8c093f4e
7
+ data.tar.gz: 8e4390ef152eab60197e2a5822a132266b12c4eb567801017a2dd0308426109823230b86df6700b1e220f9353f9de892cbe634acaa4d89578ff84c99167ead89
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-10-17)
4
+
5
+ - `select(fetch_position: true)` returns a `Page` with the server-side position of the last tuple; `after:` takes that position (String) as well as a tuple.
6
+ - Graceful shutdown: the client subscribes to `box.shutdown`; on the event it stops sending, a `Pool` member leaves the rotation, requests in flight finish, and the connection is closed by the client before the server stops (`shutdown.tarantool` event, `disconnect.tarantool` with the reason `server ... is shutting down`).
7
+
3
8
  ## 0.2.0 (2026-10-17)
4
9
 
5
10
  - `as: :hash` and `as: :row` on `select`, `insert`, `replace`, `update` and `delete` return tuples as Hashes by the space format (`Client::Row` keeps the raw tuple too); `SpaceInfo#to_hash` and `#to_tuple` do the conversion.
data/README.md CHANGED
@@ -72,6 +72,16 @@ page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50)
72
72
  next_page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50, after: page.last)
73
73
  ```
74
74
 
75
+ With `fetch_position: true` the server hands back a position instead, so the caller carries an opaque String rather than the last tuple (`IPROTO_FETCH_POSITION`, `IPROTO_POSITION`, `IPROTO_AFTER_POSITION`; 2.11+). `select` then returns a `Client::Page`: `tuples` (converted by `as:` as usual, also reachable through `each`), and `position`, nil once a page comes back empty. A position is tied to the index and iterator of the select that produced it; don't parse it or reuse it with another index. `after:` accepts a position (String) or a tuple (Array).
76
+
77
+ ```ruby
78
+ page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50, fetch_position: true)
79
+ until page.empty?
80
+ page.each { |user| ... }
81
+ page = client.select(:users, [18], index: :age, iterator: :ge, limit: 50, fetch_position: true, after: page.position)
82
+ end
83
+ ```
84
+
75
85
  `delete_by` deletes every tuple a select finds, including by a non-unique index, which IPROTO `DELETE` can't do. It selects `batch_size` tuples at a time (default 1000), continues past the last one with `after:`, deletes each by primary key, and returns the number deleted. TREE indexes only, like `after:`. Multikey and functional indexes are not supported either: tuple-based `after` raises `ServerError` there, before deleting anything.
76
86
 
77
87
  ```ruby
@@ -188,6 +198,12 @@ pool.close
188
198
 
189
199
  Roles come from `box.status` (`is_ro`), so a switchover is picked up as soon as the instances report it. Requests are spread round-robin among matching instances. If none matches, the call raises `NoAvailableInstanceError`. `Pool.new` raises it only if no address answers; addresses that are down keep being retried in the background. That retrying covers start-up only: once connected, a member relies on its client's reconnect, so with a finite `reconnect_attempts` a member that gives up stays out of rotation. `Pool#close` may wait for an in-progress background connect attempt, up to `connect_timeout` + `request_timeout`. Options are passed to each client. A prepared statement from `pool.prepare` stays bound to the instance that prepared it.
190
200
 
201
+ ## Graceful shutdown
202
+
203
+ Tarantool 2.10+ announces `SIGTERM` through the `box.shutdown` event and waits, up to `box.ctl.set_on_shutdown_timeout` (3 s by default), for the connections subscribed to it to close. Every `Client` subscribes. When the event arrives the client stops sending: `connected?` turns false, a `Pool` member leaves the rotation at once, and new requests on a bare `Client` wait for the next connection as they do while reconnecting. Requests already in flight get their replies; once none is left, or after `request_timeout`, the client closes the connection itself with the reason `server <address> is shutting down` and the usual reconnect follows. A transaction open on that connection fails on its next request, as after any loss. Nothing is retried elsewhere.
204
+
205
+ Watching `box.shutdown` yourself works as before; the client's own subscription is never unwatched.
206
+
191
207
  ## Forking servers
192
208
 
193
209
  `Client` and `Pool` are fork-safe. The first request in a child process after `fork` reinitializes the connection transparently — a new socket is opened, background threads are restarted, and watchers are resubscribed. The parent's connection is unaffected.
@@ -227,6 +243,7 @@ Lifecycle events have the instance `address` too. Subscribers should be fast: `d
227
243
  | `reconnect_failed.tarantool` | `attempts` (when known), `exception`, `exception_object` | error |
228
244
  | `fork.tarantool` | | info |
229
245
  | `role_change.tarantool` | `read_only` (pool members) | info |
246
+ | `shutdown.tarantool` | `pending` (requests in flight when the server announced its shutdown) | info |
230
247
 
231
248
  `role_change.tarantool` fires only when the role actually changes, not on the first report after connecting.
232
249
 
@@ -289,7 +306,7 @@ Don't create multikey indexes on these containers: with `memtx_use_mvcc_engine =
289
306
  ## Public API
290
307
 
291
308
  `Tarantool::Client`, `Tarantool::Client::Pool`, `Tarantool::Client::Stream` (yielded by `transaction`),
292
- `Watcher` (returned by `watch`), `SqlResult`, `PreparedStatement`, `SpaceInfo`, `IndexInfo`, `Row`, `Configuration`
309
+ `Watcher` (returned by `watch`), `SqlResult`, `PreparedStatement`, `SpaceInfo`, `IndexInfo`, `Row`, `Page`, `Configuration`
293
310
  (returned by `client.config`), `Tarantool::Client::UUID`, `Interval`, `BoxError`, and the error classes above.
294
311
 
295
312
  Everything else, including anything tagged `@api private` in its source comment, is an implementation detail
@@ -27,16 +27,24 @@ module Tarantool
27
27
  nil
28
28
  end
29
29
 
30
- # Watcher registrations outlive a fork; on_ready resubscribes them on the new link.
30
+ # Watcher registrations outlive a fork; on_ready resubscribes them on the new link, the shutdown
31
+ # subscription among them.
31
32
  def open_link
32
33
  @link = Transport::Link.new(@config, instrumentation: @instrumentation, **link_hooks)
33
34
  @watchers&.restart_after_fork(@link)
35
+ first = @watchers.nil?
34
36
  @watchers ||= Watchers.new(@link, timeout: @config.request_timeout)
35
37
  @link.event_handler = @watchers.method(:dispatch)
36
38
  @schema_cache = SchemaCache.new
37
39
  @stream_ids = Transport::StreamIds.new
38
40
  @link.connect
39
41
  @pid = Process.pid
42
+ subscribe_to_shutdown if first
43
+ end
44
+
45
+ # box.shutdown is true once the server starts stopping; the value on subscribing is nil before that.
46
+ def subscribe_to_shutdown
47
+ @watchers.add('box.shutdown', ->(_key, value) { @link.drain if value })
40
48
  end
41
49
  end
42
50
  end
@@ -8,7 +8,8 @@ module Tarantool
8
8
  module LogLines
9
9
  LEVELS = {
10
10
  'request.tarantool' => :debug, 'disconnect.tarantool' => :warn, 'reconnect.tarantool' => :info,
11
- 'reconnect_failed.tarantool' => :error, 'fork.tarantool' => :info, 'role_change.tarantool' => :info
11
+ 'reconnect_failed.tarantool' => :error, 'fork.tarantool' => :info, 'role_change.tarantool' => :info,
12
+ 'shutdown.tarantool' => :info
12
13
  }.freeze
13
14
 
14
15
  LINES = {
@@ -27,6 +28,9 @@ module Tarantool
27
28
  end,
28
29
  'role_change.tarantool' => lambda do |payload, _duration|
29
30
  "tarantool #{payload[:address]} is now #{payload[:read_only] ? 'read-only' : 'writable'}"
31
+ end,
32
+ 'shutdown.tarantool' => lambda do |payload, _duration|
33
+ "tarantool #{payload[:address]} is shutting down, #{requests(payload[:pending])} in flight"
30
34
  end
31
35
  }.freeze
32
36
 
@@ -49,6 +53,7 @@ module Tarantool
49
53
  def request_name(payload) = payload[:request] ? "tarantool #{payload[:request]}" : 'tarantool'
50
54
 
51
55
  def attempts(count) = count == 1 ? '1 attempt' : "#{count} attempts"
56
+ def requests(count) = count == 1 ? '1 request' : "#{count} requests"
52
57
  def error(payload) = payload[:exception].join(': ')
53
58
 
54
59
  # The message may quote data (ER_TUPLE_FOUND repeats the tuple), so the request line keeps only the
@@ -19,10 +19,13 @@ module Tarantool
19
19
  lookup_space(space, timeout)
20
20
  end
21
21
 
22
+ # `after:` takes a tuple or a position; `fetch_position: true` returns a Page carrying the position instead
23
+ # of a plain Array.
22
24
  def select(space, key = [], index: 0, limit: Protocol::Constants::MAX_LIMIT, offset: 0, iterator: :eq,
23
- after: nil, as: :tuple, timeout: nil)
24
- on_space(space, index, as:, timeout:) do |space_id, index_id|
25
- Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after:)
25
+ after: nil, fetch_position: false, as: :tuple, timeout: nil)
26
+ on_space(space, index, as:, timeout:, page: fetch_position) do |space_id, index_id|
27
+ Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after:,
28
+ fetch_position:)
26
29
  end
27
30
  end
28
31
 
@@ -99,7 +102,7 @@ module Tarantool
99
102
  private
100
103
 
101
104
  # A retry after a schema change calls the block again, so the SpaceInfo kept is the one the response came from.
102
- def on_space(space, index, as:, timeout:, format: false)
105
+ def on_space(space, index, as:, timeout:, format: false, page: false)
103
106
  TupleShape.check(as)
104
107
  space_info = nil
105
108
  needs_format = format || as != :tuple
@@ -107,8 +110,9 @@ module Tarantool
107
110
  space_info = info
108
111
  yield space_id, index_id, info
109
112
  end
113
+ tuples = response.data.map { |tuple| TupleShape.apply(as, tuple, space_info) }
110
114
 
111
- 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
@@ -17,16 +17,19 @@ module Tarantool
17
17
  LIMIT = 0x12
18
18
  OFFSET = 0x13
19
19
  ITERATOR = 0x14
20
+ FETCH_POSITION = 0x1f
20
21
  KEY = 0x20
21
22
  TUPLE = 0x21
22
23
  FUNCTION_NAME = 0x22
23
24
  USER_NAME = 0x23
24
25
  EXPR = 0x27
25
26
  OPS = 0x28
27
+ AFTER_POSITION = 0x2e
26
28
  AFTER_TUPLE = 0x2f
27
29
  DATA = 0x30
28
30
  ERROR_24 = 0x31
29
31
  METADATA = 0x32
32
+ POSITION = 0x35
30
33
  SQL_TEXT = 0x40
31
34
  SQL_BIND = 0x41
32
35
  SQL_INFO = 0x42
@@ -4,11 +4,17 @@ module Tarantool
4
4
  class Client
5
5
  module Protocol
6
6
  module Requests
7
- # IPROTO_SELECT: index lookup or scan. `after` continues past a tuple (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) }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tarantool
4
4
  class Client
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
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'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tarantool-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Starlexxx
@@ -68,6 +68,7 @@ files:
68
68
  - lib/tarantool/client/interval.rb
69
69
  - lib/tarantool/client/named_requests.rb
70
70
  - lib/tarantool/client/operations.rb
71
+ - lib/tarantool/client/page.rb
71
72
  - lib/tarantool/client/pool.rb
72
73
  - lib/tarantool/client/pool/member.rb
73
74
  - lib/tarantool/client/pool/member_fork_safety.rb
@@ -117,6 +118,7 @@ files:
117
118
  - lib/tarantool/client/transport/future.rb
118
119
  - lib/tarantool/client/transport/handshake.rb
119
120
  - lib/tarantool/client/transport/link.rb
121
+ - lib/tarantool/client/transport/link_draining.rb
120
122
  - lib/tarantool/client/transport/link_events.rb
121
123
  - lib/tarantool/client/transport/link_state.rb
122
124
  - lib/tarantool/client/transport/pending_requests.rb