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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +96 -1
- data/lib/tarantool/client/batch_delete.rb +97 -0
- data/lib/tarantool/client/configuration.rb +20 -3
- data/lib/tarantool/client/diagnostics.rb +20 -0
- data/lib/tarantool/client/errors.rb +8 -1
- data/lib/tarantool/client/fork_safety.rb +11 -2
- data/lib/tarantool/client/instrumentation/log_instrumenter.rb +35 -0
- data/lib/tarantool/client/instrumentation/log_lines.rb +71 -0
- data/lib/tarantool/client/instrumentation/request_payload.rb +42 -0
- data/lib/tarantool/client/instrumentation.rb +51 -0
- data/lib/tarantool/client/instrumented.rb +42 -0
- data/lib/tarantool/client/named_requests.rb +67 -0
- data/lib/tarantool/client/operations.rb +58 -22
- data/lib/tarantool/client/page.rb +18 -0
- data/lib/tarantool/client/pool/member.rb +4 -9
- data/lib/tarantool/client/pool/member_fork_safety.rb +10 -5
- data/lib/tarantool/client/pool/member_role.rb +36 -0
- data/lib/tarantool/client/protocol/constants.rb +3 -0
- data/lib/tarantool/client/protocol/requests/select.rb +22 -5
- data/lib/tarantool/client/protocol/response.rb +1 -0
- data/lib/tarantool/client/row.rb +8 -0
- data/lib/tarantool/client/schema.rb +4 -3
- data/lib/tarantool/client/space_info.rb +2 -0
- data/lib/tarantool/client/stream.rb +20 -10
- data/lib/tarantool/client/transaction_schema.rb +12 -8
- data/lib/tarantool/client/transport/connection.rb +10 -0
- data/lib/tarantool/client/transport/link.rb +22 -27
- data/lib/tarantool/client/transport/link_draining.rb +35 -0
- data/lib/tarantool/client/transport/link_events.rb +48 -0
- data/lib/tarantool/client/transport/link_state.rb +39 -5
- data/lib/tarantool/client/transport/pending_requests.rb +28 -1
- data/lib/tarantool/client/tuple_mapping.rb +65 -0
- data/lib/tarantool/client/tuple_shape.rb +25 -0
- data/lib/tarantool/client/version.rb +1 -1
- data/lib/tarantool/client.rb +25 -48
- 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
|
|
7
|
-
# the request built by the block, and `lookup_space(space, timeout)`, which returns its
|
|
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
|
-
|
|
23
|
-
Protocol::Requests::Select.new(space_id:, index_id:, key: key_tuple(key), limit:, offset:, iterator:, after
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
Protocol::Requests::Insert.new(space_id:, tuple:)
|
|
30
|
-
end.
|
|
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
|
-
|
|
35
|
-
Protocol::Requests::Replace.new(space_id:, tuple:)
|
|
36
|
-
end.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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 (
|
|
7
|
+
# IPROTO_SELECT: index lookup or scan. `after` continues past a tuple (Array) or a position (String, from
|
|
8
|
+
# a response asked with `fetch_position`); keyset pagination, 2.11+.
|
|
8
9
|
# @api private
|
|
9
|
-
Select = Data.define(:space_id, :index_id, :key, :limit, :offset, :iterator, :after) do
|
|
10
|
-
def
|
|
10
|
+
Select = Data.define(:space_id, :index_id, :key, :limit, :offset, :iterator, :after, :fetch_position) do
|
|
11
|
+
def self.cursor?(after) = after.nil? || after.is_a?(Array) || after.is_a?(String)
|
|
12
|
+
|
|
13
|
+
def initialize(iterator:, after: nil, fetch_position: false, **)
|
|
11
14
|
raise ArgumentError, "unknown iterator: #{iterator.inspect}" unless Constants::ITERATORS.key?(iterator)
|
|
15
|
+
unless Select.cursor?(after)
|
|
16
|
+
raise ArgumentError, "after: expected a tuple (Array) or a position (String), got #{after.inspect}"
|
|
17
|
+
end
|
|
12
18
|
|
|
13
19
|
super
|
|
14
20
|
end
|
|
@@ -22,8 +28,19 @@ module Tarantool
|
|
|
22
28
|
Constants::LIMIT => limit,
|
|
23
29
|
Constants::OFFSET => offset,
|
|
24
30
|
Constants::ITERATOR => Constants::ITERATORS.fetch(iterator),
|
|
25
|
-
Constants::KEY => key
|
|
26
|
-
|
|
31
|
+
Constants::KEY => key,
|
|
32
|
+
**pagination
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def pagination
|
|
39
|
+
keys = {}
|
|
40
|
+
keys[after.is_a?(String) ? Constants::AFTER_POSITION : Constants::AFTER_TUPLE] = after unless after.nil?
|
|
41
|
+
keys[Constants::FETCH_POSITION] = true if fetch_position
|
|
42
|
+
|
|
43
|
+
keys
|
|
27
44
|
end
|
|
28
45
|
end
|
|
29
46
|
end
|
|
@@ -17,6 +17,7 @@ module Tarantool
|
|
|
17
17
|
def sync = header[Constants::SYNC]
|
|
18
18
|
def schema_version = header[Constants::SCHEMA_VERSION]
|
|
19
19
|
def data = body[Constants::DATA]
|
|
20
|
+
def position = body[Constants::POSITION]
|
|
20
21
|
def error? = type.anybits?(Constants::TYPE_ERROR)
|
|
21
22
|
def push? = type == Constants::CHUNK
|
|
22
23
|
def event? = type == Constants::EVENT
|
|
@@ -39,7 +39,7 @@ module Tarantool
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def space_info(space)
|
|
42
|
-
@space_infos.fetch(space_id(space))
|
|
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
|
-
|
|
55
|
+
id = space.is_a?(Integer) ? space : @space_ids[space.to_s]
|
|
56
|
+
return id if @space_infos.key?(id)
|
|
56
57
|
|
|
57
|
-
|
|
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
|
|
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
|
|
61
|
-
return
|
|
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 =
|
|
73
|
+
request = build_request(space, index, deadline, payload, &)
|
|
72
74
|
|
|
73
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
36
|
+
return false unless schema.version == version
|
|
33
37
|
|
|
34
|
-
|
|
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
|