tarantool-client 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -1
  3. data/README.md +96 -1
  4. data/lib/tarantool/client/batch_delete.rb +97 -0
  5. data/lib/tarantool/client/configuration.rb +20 -3
  6. data/lib/tarantool/client/diagnostics.rb +20 -0
  7. data/lib/tarantool/client/errors.rb +8 -1
  8. data/lib/tarantool/client/fork_safety.rb +11 -2
  9. data/lib/tarantool/client/instrumentation/log_instrumenter.rb +35 -0
  10. data/lib/tarantool/client/instrumentation/log_lines.rb +71 -0
  11. data/lib/tarantool/client/instrumentation/request_payload.rb +42 -0
  12. data/lib/tarantool/client/instrumentation.rb +51 -0
  13. data/lib/tarantool/client/instrumented.rb +42 -0
  14. data/lib/tarantool/client/named_requests.rb +67 -0
  15. data/lib/tarantool/client/operations.rb +58 -22
  16. data/lib/tarantool/client/page.rb +18 -0
  17. data/lib/tarantool/client/pool/member.rb +4 -9
  18. data/lib/tarantool/client/pool/member_fork_safety.rb +10 -5
  19. data/lib/tarantool/client/pool/member_role.rb +36 -0
  20. data/lib/tarantool/client/protocol/constants.rb +3 -0
  21. data/lib/tarantool/client/protocol/requests/select.rb +22 -5
  22. data/lib/tarantool/client/protocol/response.rb +1 -0
  23. data/lib/tarantool/client/row.rb +8 -0
  24. data/lib/tarantool/client/schema.rb +4 -3
  25. data/lib/tarantool/client/space_info.rb +2 -0
  26. data/lib/tarantool/client/stream.rb +20 -10
  27. data/lib/tarantool/client/transaction_schema.rb +12 -8
  28. data/lib/tarantool/client/transport/connection.rb +10 -0
  29. data/lib/tarantool/client/transport/link.rb +22 -27
  30. data/lib/tarantool/client/transport/link_draining.rb +35 -0
  31. data/lib/tarantool/client/transport/link_events.rb +48 -0
  32. data/lib/tarantool/client/transport/link_state.rb +39 -5
  33. data/lib/tarantool/client/transport/pending_requests.rb +28 -1
  34. data/lib/tarantool/client/tuple_mapping.rb +65 -0
  35. data/lib/tarantool/client/tuple_shape.rb +25 -0
  36. data/lib/tarantool/client/version.rb +1 -1
  37. data/lib/tarantool/client.rb +25 -48
  38. metadata +17 -2
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarantool
4
+ class Client
5
+ # Requests whose space and index resolve through the schema cache. Includers set @link, @schema_cache and
6
+ # @config, and provide ensure_fork_safe and mark_schema_reloaded.
7
+ # @api private
8
+ module NamedRequests
9
+ private
10
+
11
+ # Every schema fetch and attempt draws on one deadline, so the whole call stays within timeout.
12
+ # Waiting for a connection first means the cache already holds its schema or a newer one.
13
+ def perform_by_name(space, index, timeout, payload, &)
14
+ ensure_fork_safe
15
+ deadline = Transport::Deadline.after(timeout)
16
+ @link.connection(deadline)
17
+
18
+ perform_with_schema_check(space, index, deadline, payload, &)
19
+ rescue TimeoutError
20
+ raise TimeoutError, "no response within #{timeout}s"
21
+ end
22
+
23
+ def lookup_space(space, timeout)
24
+ ensure_fork_safe
25
+ timeout ||= @config.request_timeout
26
+ deadline = Transport::Deadline.after(timeout)
27
+ @link.connection(deadline)
28
+
29
+ @schema_cache.space_info(space, deadline)
30
+ rescue TimeoutError
31
+ raise TimeoutError, "no response within #{timeout}s"
32
+ end
33
+
34
+ def perform_with_schema_check(space, index, deadline, payload, &)
35
+ schema, ids = resolve_name(space, index, deadline, payload)
36
+
37
+ perform_built(schema, ids, deadline, &)
38
+ rescue ServerError, UnknownFieldError => e
39
+ raise if e.is_a?(ServerError) && e.code != Protocol::Constants::ER_WRONG_SCHEMA_VERSION
40
+
41
+ mark_schema_reloaded(payload)
42
+ fresh = @schema_cache.reload(schema, timeout: Transport::Deadline.remaining(deadline))
43
+ schema, ids = resolve_name(space, index, deadline, payload, fresh)
44
+ perform_built(schema, ids, deadline, &)
45
+ end
46
+
47
+ def perform_built(schema, ids, deadline)
48
+ perform_resolved(schema, yield(*ids, schema.space_info(ids.first)), deadline)
49
+ end
50
+
51
+ # The cache returns another schema only when resolving had to reload it.
52
+ def resolve_name(space, index, deadline, payload, schema = @schema_cache.schema)
53
+ @schema_cache.resolve(space, index, deadline, schema).tap do |resolved, _ids|
54
+ mark_schema_reloaded(payload) unless resolved.equal?(schema)
55
+ end
56
+ end
57
+
58
+ # Only the connection the schema came from: once it is lost, another instance may reuse the
59
+ # schema version with other ids, so the request fails with it rather than wait for a reconnect.
60
+ def perform_resolved(schema, request, deadline)
61
+ timeout = Transport::Deadline.remaining(deadline)
62
+
63
+ schema.connection.perform(request, timeout:, schema_version: schema.version)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -3,8 +3,10 @@
3
3
  module Tarantool
4
4
  class Client
5
5
  # Data operations, call, eval and SQL. Includers implement `perform(request, timeout:, on_push: nil)`,
6
- # `perform_on_space(space, index, timeout:) { |space_id, index_id| request }`, which returns the response to
7
- # the request built by the block, and `lookup_space(space, timeout)`, which returns its SpaceInfo.
6
+ # `perform_on_space(space, index, timeout:, format: false) { |space_id, index_id, space_info| request }`, which
7
+ # returns the response to the request built by the block, and `lookup_space(space, timeout)`, which returns its
8
+ # SpaceInfo. With `format: true` the block gets the SpaceInfo of the schema the request is resolved and
9
+ # version-checked against, even for numeric ids; otherwise the third argument may be absent.
8
10
  # @api private
9
11
  module Operations
10
12
  def ping(timeout: nil)
@@ -17,43 +19,61 @@ module Tarantool
17
19
  lookup_space(space, timeout)
18
20
  end
19
21
 
22
+ # `after:` takes a tuple or a position; `fetch_position: true` returns a Page carrying the position instead
23
+ # of a plain Array.
20
24
  def select(space, key = [], index: 0, limit: Protocol::Constants::MAX_LIMIT, offset: 0, iterator: :eq,
21
- after: nil, timeout: nil)
22
- perform_on_space(space, index, timeout:) do |space_id, index_id|
23
- Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after:)
24
- end.data
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:)
29
+ end
25
30
  end
26
31
 
27
- def insert(space, tuple, timeout: nil)
28
- perform_on_space(space, 0, timeout:) do |space_id|
29
- Protocol::Requests::Insert.new(space_id:, tuple:)
30
- end.data.first
32
+ def insert(space, tuple, as: :tuple, timeout: nil)
33
+ on_space(space, 0, as:, timeout:, format: tuple.is_a?(Hash)) do |space_id, _index_id, space_info|
34
+ Protocol::Requests::Insert.new(space_id:, tuple: tuple_for(tuple, space_info))
35
+ end.first
31
36
  end
32
37
 
33
- def replace(space, tuple, timeout: nil)
34
- perform_on_space(space, 0, timeout:) do |space_id|
35
- Protocol::Requests::Replace.new(space_id:, tuple:)
36
- end.data.first
38
+ def replace(space, tuple, as: :tuple, timeout: nil)
39
+ on_space(space, 0, as:, timeout:, format: tuple.is_a?(Hash)) do |space_id, _index_id, space_info|
40
+ Protocol::Requests::Replace.new(space_id:, tuple: tuple_for(tuple, space_info))
41
+ end.first
37
42
  end
38
43
 
39
- def update(space, key, operations, index: 0, timeout: nil)
40
- perform_on_space(space, index, timeout:) do |space_id, index_id|
44
+ def update(space, key, operations, index: 0, as: :tuple, timeout: nil)
45
+ on_space(space, index, as:, timeout:) do |space_id, index_id|
41
46
  Protocol::Requests::Update.new(space_id:, index_id:, key: key_tuple(key), operations:)
42
- end.data.first
47
+ end.first
43
48
  end
44
49
 
45
50
  def upsert(space, tuple, operations, timeout: nil)
46
- perform_on_space(space, 0, timeout:) do |space_id|
47
- Protocol::Requests::Upsert.new(space_id:, tuple:, operations:)
51
+ perform_on_space(space, 0, timeout:, format: tuple.is_a?(Hash)) do |space_id, _index_id, space_info|
52
+ Protocol::Requests::Upsert.new(space_id:, tuple: tuple_for(tuple, space_info), operations:)
48
53
  end
49
54
 
50
55
  nil
51
56
  end
52
57
 
53
- def delete(space, key, index: 0, timeout: nil)
54
- perform_on_space(space, index, timeout:) do |space_id, index_id|
58
+ def delete(space, key, index: 0, as: :tuple, timeout: nil)
59
+ on_space(space, index, as:, timeout:) do |space_id, index_id|
55
60
  Protocol::Requests::Delete.new(space_id:, index_id:, key: key_tuple(key))
56
- end.data.first
61
+ end.first
62
+ end
63
+
64
+ # Deletes every tuple `select(space, key, index:, iterator:)` finds, which IPROTO DELETE can't do on a
65
+ # non-unique index: selects batch_size tuples at a time, continuing past the last one with `after:`, and
66
+ # deletes each by primary key. Returns the number of tuples deleted. TREE indexes only. Multikey and
67
+ # functional indexes are not supported either: tuple-based `after` raises ServerError there, before
68
+ # deleting anything.
69
+ #
70
+ # Outside a transaction it is not atomic: on failure the tuples deleted so far stay deleted, and the
71
+ # raised Tarantool::Error carries the number confirmed deleted in #deleted (a lower bound). Tuples
72
+ # inserted meanwhile behind the cursor are not deleted. Inside `transaction { |tx| tx.delete_by(...) }`
73
+ # it is atomic (memtx needs memtx_use_mvcc_engine). `timeout:` bounds the whole operation; nil gives
74
+ # each request request_timeout.
75
+ def delete_by(space, key, index:, iterator: :eq, batch_size: 1000, timeout: nil)
76
+ BatchDelete.new(self, space, key, index:, iterator:, batch_size:, timeout:).call
57
77
  end
58
78
 
59
79
  # on_push runs in the calling thread for each box.session.push message; an exception
@@ -81,6 +101,22 @@ module Tarantool
81
101
 
82
102
  private
83
103
 
104
+ # A retry after a schema change calls the block again, so the SpaceInfo kept is the one the response came from.
105
+ def on_space(space, index, as:, timeout:, format: false, page: false)
106
+ TupleShape.check(as)
107
+ space_info = nil
108
+ needs_format = format || as != :tuple
109
+ response = perform_on_space(space, index, timeout:, format: needs_format) do |space_id, index_id, info|
110
+ space_info = info
111
+ yield space_id, index_id, info
112
+ end
113
+ tuples = response.data.map { |tuple| TupleShape.apply(as, tuple, space_info) }
114
+
115
+ page ? Page.new(tuples:, position: response.position) : tuples
116
+ end
117
+
118
+ def tuple_for(tuple, space_info) = tuple.is_a?(Hash) ? space_info.to_tuple(tuple) : tuple
119
+
84
120
  def key_tuple(key)
85
121
  key.is_a?(Array) ? key : [key]
86
122
  end
@@ -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
@@ -8,11 +8,13 @@ module Tarantool
8
8
  # @api private
9
9
  class Member
10
10
  include Pool::MemberForkSafety
11
+ include Pool::MemberRole
11
12
 
12
13
  def initialize(uri, options)
13
14
  @uri = uri
14
15
  @options = options
15
16
  @config = Configuration.build(uri, **options)
17
+ @instrumentation = Instrumentation.build(@config)
16
18
  @lock = Mutex.new
17
19
  @changed = ConditionVariable.new
18
20
  @closed = false
@@ -75,13 +77,6 @@ module Tarantool
75
77
  client.close if closed
76
78
  end
77
79
 
78
- def update(status)
79
- @lock.synchronize do
80
- @read_only = status['is_ro']
81
- @changed.broadcast
82
- end
83
- end
84
-
85
80
  def wait_for_status
86
81
  deadline = Transport::Deadline.after(@config.connect_timeout)
87
82
 
@@ -115,7 +110,7 @@ module Tarantool
115
110
  attempt += 1
116
111
  end
117
112
  rescue StandardError => e
118
- warn "tarantool pool: gave up connecting to #{address}: #{e.class}: #{e.message}"
113
+ Diagnostics.warn(@config.logger, "tarantool pool: gave up connecting to #{address}: #{e.class}: #{e.message}")
119
114
  end
120
115
 
121
116
  def try_connect
@@ -125,7 +120,7 @@ module Tarantool
125
120
  rescue Error
126
121
  false
127
122
  rescue StandardError => e
128
- warn "tarantool pool: connecting to #{address} failed: #{e.class}: #{e.message}"
123
+ Diagnostics.warn(@config.logger, "tarantool pool: connecting to #{address} failed: #{e.class}: #{e.message}")
129
124
  false
130
125
  end
131
126
 
@@ -9,7 +9,16 @@ module Tarantool
9
9
  # to the parent: closing the client in the child only drops its socket copy, dead threads join at once.
10
10
  # Nothing inherited may stop the reset, or the child's pool would never start its members.
11
11
  def reset_for_fork
12
- old_retrier, old_client = @lock.synchronize do
12
+ old_retrier, old_client = clear_fork_state
13
+ ThreadJoin.join(old_retrier)
14
+ close_inherited(old_client)
15
+ @instrumentation&.notify('fork.tarantool')
16
+ end
17
+
18
+ private
19
+
20
+ def clear_fork_state
21
+ @lock.synchronize do
13
22
  retrier = @retrier
14
23
  client = @client
15
24
  @retrier = nil
@@ -17,12 +26,8 @@ module Tarantool
17
26
  @read_only = nil
18
27
  [retrier, client]
19
28
  end
20
- ThreadJoin.join(old_retrier)
21
- close_inherited(old_client)
22
29
  end
23
30
 
24
- private
25
-
26
31
  def close_inherited(client)
27
32
  client&.close
28
33
  rescue StandardError
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarantool
4
+ class Client
5
+ class Pool
6
+ # The role a member's instance reports through box.status. Includers set @lock, @changed, @read_only and
7
+ # @instrumentation.
8
+ # @api private
9
+ module MemberRole
10
+ private
11
+
12
+ def update(status)
13
+ read_only = status['is_ro']
14
+ changed = @lock.synchronize do
15
+ @read_only = read_only
16
+ @changed.broadcast
17
+ role_changed?(read_only)
18
+ end
19
+
20
+ @instrumentation&.notify('role_change.tarantool', read_only:) if changed
21
+ end
22
+
23
+ # Compared against the last known role rather than @read_only, which disconnect and fork reset:
24
+ # a reconnect that brings back the same role is not a change. A status without is_ro is routing
25
+ # information only, never a role to compare against.
26
+ def role_changed?(read_only)
27
+ return false if read_only.nil?
28
+
29
+ previous = @known_read_only
30
+ @known_read_only = read_only
31
+ !previous.nil? && previous != read_only
32
+ end
33
+ end
34
+ end
35
+ end
36
+ 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarantool
4
+ class Client
5
+ # A tuple as the server returned it, with its fields by name (SpaceInfo#to_hash). Returned for `as: :row`.
6
+ Row = Data.define(:tuple, :attributes)
7
+ end
8
+ end
@@ -39,7 +39,7 @@ module Tarantool
39
39
  end
40
40
 
41
41
  def space_info(space)
42
- @space_infos.fetch(space_id(space)) { raise SchemaError, "space '#{space}' not found" }
42
+ @space_infos.fetch(space_id(space))
43
43
  end
44
44
 
45
45
  private
@@ -52,9 +52,10 @@ module Tarantool
52
52
  end
53
53
 
54
54
  def space_id(space)
55
- return space if space.is_a?(Integer)
55
+ id = space.is_a?(Integer) ? space : @space_ids[space.to_s]
56
+ return id if @space_infos.key?(id)
56
57
 
57
- @space_ids.fetch(space.to_s) { raise SchemaError, "space '#{space}' not found" }
58
+ raise SchemaError, "space '#{space}' not found"
58
59
  end
59
60
 
60
61
  def index_id(space_id, index)
@@ -4,6 +4,8 @@ module Tarantool
4
4
  class Client
5
5
  # Format and indexes of a space from the loaded schema. `indexes` maps names to IndexInfo in id order.
6
6
  SpaceInfo = Data.define(:id, :name, :fields, :indexes) do
7
+ include TupleMapping
8
+
7
9
  def self.from_tuples(space, index_tuples)
8
10
  id, _owner, name, _engine, _field_count, _flags, format = space
9
11
  fields = (format || []).map { |raw| SpaceInfo::Field.from_raw(raw) }
@@ -6,15 +6,17 @@ module Tarantool
6
6
  # stream's requests in order. Created by Client#transaction.
7
7
  class Stream
8
8
  include Operations
9
+ include Instrumented
9
10
 
10
11
  LOST = 'connection lost during the transaction; the server rolled it back'
11
12
  FINISHED = 'transaction is finished'
12
13
 
13
- def initialize(connection:, schema_cache:, id:, request_timeout:)
14
+ def initialize(connection:, schema_cache:, id:, request_timeout:, instrumentation: nil)
14
15
  @connection = connection
15
16
  @schema_cache = schema_cache
16
17
  @id = id
17
18
  @request_timeout = request_timeout
19
+ @instrumentation = instrumentation
18
20
  @state = :idle
19
21
  @pid = Process.pid
20
22
  end
@@ -51,34 +53,42 @@ module Tarantool
51
53
  abandon if @state == :active
52
54
  end
53
55
 
54
- def perform(request, timeout: nil, schema_version: nil, on_push: nil)
56
+ def transmit(request, timeout: nil, schema_version: nil, on_push: nil)
55
57
  check_active
56
58
 
57
59
  send_request(request, timeout: timeout || @request_timeout, schema_version:, on_push:)
58
60
  end
59
61
 
60
- def perform_on_space(space, index, timeout: nil, &)
61
- return perform(yield(space, index), timeout:) if space.is_a?(Integer) && index.is_a?(Integer)
62
+ def transmit_on_space(space, index, timeout: nil, format: false, payload: nil, &)
63
+ return transmit(yield(space, index), timeout:) if !format && space.is_a?(Integer) && index.is_a?(Integer)
62
64
 
63
- perform_by_name(space, index, timeout || @request_timeout, &)
65
+ perform_by_name(space, index, timeout || @request_timeout, payload, &)
64
66
  end
65
67
 
66
68
  # Checks for a lost connection first: resolving would otherwise meet the next connection's schema
67
69
  # and report a schema change instead.
68
- def perform_by_name(space, index, timeout)
70
+ def perform_by_name(space, index, timeout, payload, &)
69
71
  check_connection
70
72
  deadline = Transport::Deadline.after(timeout)
71
- request = yield(*@schema.resolve(space, index, deadline))
73
+ request = build_request(space, index, deadline, payload, &)
72
74
 
73
- perform(request, timeout: remaining(deadline), schema_version: @schema.version)
75
+ transmit(request, timeout: remaining(deadline), schema_version: @schema.version)
74
76
  rescue ServerError => e
75
77
  raise unless e.code == Protocol::Constants::ER_WRONG_SCHEMA_VERSION
76
78
 
77
- @schema.raise_changed(deadline)
79
+ mark_schema_reloaded(payload) if @schema.refresh?(deadline)
80
+ raise TransactionError, TransactionSchema::CHANGED
78
81
  rescue TimeoutError
79
82
  raise TimeoutError, "no response within #{timeout}s"
80
83
  end
81
84
 
85
+ def build_request(space, index, deadline, payload)
86
+ (space_id, index_id), reloaded = @schema.resolve(space, index, deadline)
87
+ mark_schema_reloaded(payload) if reloaded
88
+
89
+ yield(space_id, index_id, @schema.space_info(space_id, deadline))
90
+ end
91
+
82
92
  def lookup_space(space, timeout)
83
93
  check_active
84
94
  check_connection
@@ -98,7 +108,7 @@ module Tarantool
98
108
  def finish(request)
99
109
  @state = :finished
100
110
 
101
- send_request(request, timeout: @request_timeout)
111
+ instrumented(request) { send_request(request, timeout: @request_timeout) }
102
112
  end
103
113
 
104
114
  # A stream is bound to its connection's session, so a reconnect can't carry the transaction over.
@@ -17,21 +17,26 @@ module Tarantool
17
17
  @version = version
18
18
  end
19
19
 
20
+ # The ids, and whether the cache had to catch up with the BEGIN version first.
20
21
  def resolve(space, index, deadline)
21
- pinned(deadline).resolve(space, index)
22
+ cached = @cache.schema
23
+ schema = pinned(cached, deadline)
24
+
25
+ [schema.resolve(space, index), !schema.equal?(cached)]
22
26
  end
23
27
 
24
28
  def space_info(space, deadline)
25
- pinned(deadline).space_info(space)
29
+ pinned(@cache.schema, deadline).space_info(space)
26
30
  end
27
31
 
28
32
  # A request rejected for its schema version did not run, so the transaction still rolls back
29
- # cleanly. The reload spares the retried transaction the same rejection.
30
- def raise_changed(deadline)
33
+ # cleanly. The reload spares the retried transaction the same rejection. Returns whether it reloaded.
34
+ def refresh?(deadline)
31
35
  schema = @cache.schema
32
- reload(schema, deadline) if schema.version == version
36
+ return false unless schema.version == version
33
37
 
34
- raise TransactionError, CHANGED
38
+ reload(schema, deadline)
39
+ true
35
40
  end
36
41
 
37
42
  private
@@ -39,8 +44,7 @@ module Tarantool
39
44
  # A cache older than BEGIN was stale before the transaction started and may catch up;
40
45
  # anything newer means the schema changed after BEGIN. A schema of another connection means
41
46
  # this one was lost, and the versions of two instances can't be compared.
42
- def pinned(deadline)
43
- schema = @cache.schema
47
+ def pinned(schema, deadline)
44
48
  raise TransactionError, Stream::LOST unless schema.connection.equal?(@connection)
45
49
 
46
50
  schema = reload(schema, deadline) if schema.version < version
@@ -58,6 +58,16 @@ 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
+
69
+ def failure = @pending.failure
70
+
61
71
  private
62
72
 
63
73
  def establish