tarantool-client 0.1.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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +222 -0
- data/lib/tarantool/client/backoff.rb +23 -0
- data/lib/tarantool/client/box_error.rb +15 -0
- data/lib/tarantool/client/callback_guard.rb +23 -0
- data/lib/tarantool/client/configuration.rb +71 -0
- data/lib/tarantool/client/errors.rb +56 -0
- data/lib/tarantool/client/fork_safety.rb +42 -0
- data/lib/tarantool/client/index_info.rb +27 -0
- data/lib/tarantool/client/interval.rb +25 -0
- data/lib/tarantool/client/operations.rb +89 -0
- data/lib/tarantool/client/pool/member.rb +141 -0
- data/lib/tarantool/client/pool/member_fork_safety.rb +34 -0
- data/lib/tarantool/client/pool.rb +127 -0
- data/lib/tarantool/client/prepared_statement.rb +25 -0
- data/lib/tarantool/client/protocol/auth/chap_sha1.rb +28 -0
- data/lib/tarantool/client/protocol/codec.rb +79 -0
- data/lib/tarantool/client/protocol/constants.rb +105 -0
- data/lib/tarantool/client/protocol/extensions/datetime.rb +39 -0
- data/lib/tarantool/client/protocol/extensions/decimal.rb +73 -0
- data/lib/tarantool/client/protocol/extensions/error.rb +33 -0
- data/lib/tarantool/client/protocol/extensions/interval.rb +59 -0
- data/lib/tarantool/client/protocol/extensions/uuid.rb +29 -0
- data/lib/tarantool/client/protocol/extensions.rb +27 -0
- data/lib/tarantool/client/protocol/greeting.rb +21 -0
- data/lib/tarantool/client/protocol/requests/auth.rb +23 -0
- data/lib/tarantool/client/protocol/requests/begin.rb +30 -0
- data/lib/tarantool/client/protocol/requests/call.rb +16 -0
- data/lib/tarantool/client/protocol/requests/commit.rb +16 -0
- data/lib/tarantool/client/protocol/requests/delete.rb +19 -0
- data/lib/tarantool/client/protocol/requests/eval.rb +16 -0
- data/lib/tarantool/client/protocol/requests/execute.rb +21 -0
- data/lib/tarantool/client/protocol/requests/id.rb +16 -0
- data/lib/tarantool/client/protocol/requests/insert.rb +16 -0
- data/lib/tarantool/client/protocol/requests/ping.rb +17 -0
- data/lib/tarantool/client/protocol/requests/prepare.rb +16 -0
- data/lib/tarantool/client/protocol/requests/replace.rb +16 -0
- data/lib/tarantool/client/protocol/requests/rollback.rb +16 -0
- data/lib/tarantool/client/protocol/requests/select.rb +32 -0
- data/lib/tarantool/client/protocol/requests/unwatch.rb +16 -0
- data/lib/tarantool/client/protocol/requests/update.rb +24 -0
- data/lib/tarantool/client/protocol/requests/upsert.rb +19 -0
- data/lib/tarantool/client/protocol/requests/watch.rb +16 -0
- data/lib/tarantool/client/protocol/response.rb +36 -0
- data/lib/tarantool/client/schema.rb +69 -0
- data/lib/tarantool/client/schema_cache.rb +54 -0
- data/lib/tarantool/client/space_info.rb +27 -0
- data/lib/tarantool/client/sql_result.rb +24 -0
- data/lib/tarantool/client/stream.rb +130 -0
- data/lib/tarantool/client/thread_join.rb +19 -0
- data/lib/tarantool/client/transaction_schema.rb +57 -0
- data/lib/tarantool/client/transaction_scope.rb +28 -0
- data/lib/tarantool/client/transport/connection.rb +141 -0
- data/lib/tarantool/client/transport/deadline.rb +17 -0
- data/lib/tarantool/client/transport/frame_writer.rb +41 -0
- data/lib/tarantool/client/transport/future.rb +60 -0
- data/lib/tarantool/client/transport/handshake.rb +36 -0
- data/lib/tarantool/client/transport/link.rb +150 -0
- data/lib/tarantool/client/transport/link_state.rb +115 -0
- data/lib/tarantool/client/transport/pending_requests.rb +55 -0
- data/lib/tarantool/client/transport/stream_ids.rb +20 -0
- data/lib/tarantool/client/transport/timed_lock.rb +53 -0
- data/lib/tarantool/client/transport/timed_read.rb +31 -0
- data/lib/tarantool/client/uuid.rb +20 -0
- data/lib/tarantool/client/version.rb +7 -0
- data/lib/tarantool/client/watcher.rb +37 -0
- data/lib/tarantool/client/watchers/dispatcher.rb +40 -0
- data/lib/tarantool/client/watchers.rb +137 -0
- data/lib/tarantool/client.rb +216 -0
- metadata +148 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Format and indexes of a space from the loaded schema. `indexes` maps names to IndexInfo in id order.
|
|
6
|
+
SpaceInfo = Data.define(:id, :name, :fields, :indexes) do
|
|
7
|
+
def self.from_tuples(space, index_tuples)
|
|
8
|
+
id, _owner, name, _engine, _field_count, _flags, format = space
|
|
9
|
+
fields = (format || []).map { |raw| SpaceInfo::Field.from_raw(raw) }
|
|
10
|
+
indexes = index_tuples.map { |tuple| IndexInfo.from_tuple(tuple, fields) }.sort_by(&:id)
|
|
11
|
+
|
|
12
|
+
new(id:, name:, fields:, indexes: indexes.to_h { |index| [index.name, index] })
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def primary_index
|
|
16
|
+
indexes.each_value.find { |index| index.id.zero? }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
SpaceInfo::Field = Data.define(:name, :type, :is_nullable) do
|
|
21
|
+
# A format entry without a type accepts any value.
|
|
22
|
+
def self.from_raw(raw)
|
|
23
|
+
new(name: raw['name'], type: raw.fetch('type', 'any'), is_nullable: raw.fetch('is_nullable', false))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Rows and column names of an SQL query, or the counters of a data-changing statement.
|
|
6
|
+
SqlResult = Data.define(:rows, :columns, :row_count, :autoincrement_ids) do
|
|
7
|
+
def self.from_response(response)
|
|
8
|
+
info = response.body.fetch(Protocol::Constants::SQL_INFO, {})
|
|
9
|
+
|
|
10
|
+
new(
|
|
11
|
+
rows: response.data || [],
|
|
12
|
+
columns: column_names(response.body),
|
|
13
|
+
row_count: info[Protocol::Constants::SQL_INFO_ROW_COUNT],
|
|
14
|
+
autoincrement_ids: info.fetch(Protocol::Constants::SQL_INFO_AUTOINCREMENT_IDS, [])
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.column_names(body)
|
|
19
|
+
body.fetch(Protocol::Constants::METADATA, []).map { |field| field.fetch(Protocol::Constants::FIELD_NAME) }
|
|
20
|
+
end
|
|
21
|
+
private_class_method :column_names
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Operations sent over one IPROTO stream inside an interactive transaction; the server runs a
|
|
6
|
+
# stream's requests in order. Created by Client#transaction.
|
|
7
|
+
class Stream
|
|
8
|
+
include Operations
|
|
9
|
+
|
|
10
|
+
LOST = 'connection lost during the transaction; the server rolled it back'
|
|
11
|
+
FINISHED = 'transaction is finished'
|
|
12
|
+
|
|
13
|
+
def initialize(connection:, schema_cache:, id:, request_timeout:)
|
|
14
|
+
@connection = connection
|
|
15
|
+
@schema_cache = schema_cache
|
|
16
|
+
@id = id
|
|
17
|
+
@request_timeout = request_timeout
|
|
18
|
+
@state = :idle
|
|
19
|
+
@pid = Process.pid
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def transaction(isolation:, timeout:, &)
|
|
23
|
+
raise TransactionError, TransactionScope::NESTED unless @state == :idle
|
|
24
|
+
|
|
25
|
+
start(isolation, timeout)
|
|
26
|
+
|
|
27
|
+
run(&)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def prepare(*)
|
|
31
|
+
raise TransactionError,
|
|
32
|
+
'prepare statements on the client; execute them in the transaction with tx.execute(statement, bindings)'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def start(isolation, timeout)
|
|
38
|
+
response = perform(Protocol::Requests::Begin.new(isolation:, timeout:))
|
|
39
|
+
@schema = TransactionSchema.new(@schema_cache, @connection, response.schema_version)
|
|
40
|
+
|
|
41
|
+
@state = :active
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Anything but normal completion of the block, including break and throw, rolls back.
|
|
45
|
+
def run
|
|
46
|
+
result = yield self
|
|
47
|
+
finish(Protocol::Requests::Commit.new)
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
ensure
|
|
51
|
+
abandon if @state == :active
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def perform(request, timeout: nil, schema_version: nil, on_push: nil)
|
|
55
|
+
check_active
|
|
56
|
+
|
|
57
|
+
send_request(request, timeout: timeout || @request_timeout, schema_version:, on_push:)
|
|
58
|
+
end
|
|
59
|
+
|
|
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
|
+
|
|
63
|
+
perform_by_name(space, index, timeout || @request_timeout, &)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Checks for a lost connection first: resolving would otherwise meet the next connection's schema
|
|
67
|
+
# and report a schema change instead.
|
|
68
|
+
def perform_by_name(space, index, timeout)
|
|
69
|
+
check_connection
|
|
70
|
+
deadline = Transport::Deadline.after(timeout)
|
|
71
|
+
request = yield(*@schema.resolve(space, index, deadline))
|
|
72
|
+
|
|
73
|
+
perform(request, timeout: remaining(deadline), schema_version: @schema.version)
|
|
74
|
+
rescue ServerError => e
|
|
75
|
+
raise unless e.code == Protocol::Constants::ER_WRONG_SCHEMA_VERSION
|
|
76
|
+
|
|
77
|
+
@schema.raise_changed(deadline)
|
|
78
|
+
rescue TimeoutError
|
|
79
|
+
raise TimeoutError, "no response within #{timeout}s"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def lookup_space(space, timeout)
|
|
83
|
+
check_active
|
|
84
|
+
check_connection
|
|
85
|
+
timeout ||= @request_timeout
|
|
86
|
+
|
|
87
|
+
@schema.space_info(space, Transport::Deadline.after(timeout))
|
|
88
|
+
rescue TimeoutError
|
|
89
|
+
raise TimeoutError, "no response within #{timeout}s"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def remaining(deadline)
|
|
93
|
+
Transport::Deadline.remaining(deadline)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# State flips to finished before the request so a failed COMMIT is not followed by ROLLBACK:
|
|
97
|
+
# its outcome is unknown (the server may have committed), and a rollback would add nothing.
|
|
98
|
+
def finish(request)
|
|
99
|
+
@state = :finished
|
|
100
|
+
|
|
101
|
+
send_request(request, timeout: @request_timeout)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A stream is bound to its connection's session, so a reconnect can't carry the transaction over.
|
|
105
|
+
def send_request(request, **)
|
|
106
|
+
check_connection
|
|
107
|
+
|
|
108
|
+
@connection.perform(request, stream_id: @id, **)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def check_connection
|
|
112
|
+
raise ConnectionError, 'stream was created before fork and cannot be used in a child process' \
|
|
113
|
+
if Process.pid != @pid
|
|
114
|
+
raise TransactionError, LOST if @state != :idle && @connection.closed?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def check_active
|
|
118
|
+
raise TransactionError, FINISHED if @state == :finished
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Runs while an error or a non-local exit is already propagating, which a failed rollback must
|
|
122
|
+
# not replace. A lost connection takes the transaction with it.
|
|
123
|
+
def abandon
|
|
124
|
+
finish(Protocol::Requests::Rollback.new)
|
|
125
|
+
rescue Error
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Joins an internal thread without re-raising the exception it died with: the failure has already
|
|
6
|
+
# reached whoever was waiting on the thread, and closing must not fail because of it. An exception
|
|
7
|
+
# raised in the joining thread itself while it waits still propagates.
|
|
8
|
+
# @api private
|
|
9
|
+
module ThreadJoin
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def join(thread)
|
|
13
|
+
thread&.join unless Thread.current == thread
|
|
14
|
+
rescue Exception
|
|
15
|
+
raise unless thread.status.nil?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Name lookup for one transaction, pinned to the schema version the server reported for BEGIN.
|
|
6
|
+
# Re-resolving names against a newer schema mid-transaction could send later statements to a
|
|
7
|
+
# different space than earlier ones, so a schema change fails the transaction instead.
|
|
8
|
+
# @api private
|
|
9
|
+
class TransactionSchema
|
|
10
|
+
CHANGED = 'schema changed during the transaction; retry the whole transaction'
|
|
11
|
+
|
|
12
|
+
attr_reader :version
|
|
13
|
+
|
|
14
|
+
def initialize(cache, connection, version)
|
|
15
|
+
@cache = cache
|
|
16
|
+
@connection = connection
|
|
17
|
+
@version = version
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resolve(space, index, deadline)
|
|
21
|
+
pinned(deadline).resolve(space, index)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def space_info(space, deadline)
|
|
25
|
+
pinned(deadline).space_info(space)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 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)
|
|
31
|
+
schema = @cache.schema
|
|
32
|
+
reload(schema, deadline) if schema.version == version
|
|
33
|
+
|
|
34
|
+
raise TransactionError, CHANGED
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# A cache older than BEGIN was stale before the transaction started and may catch up;
|
|
40
|
+
# anything newer means the schema changed after BEGIN. A schema of another connection means
|
|
41
|
+
# this one was lost, and the versions of two instances can't be compared.
|
|
42
|
+
def pinned(deadline)
|
|
43
|
+
schema = @cache.schema
|
|
44
|
+
raise TransactionError, Stream::LOST unless schema.connection.equal?(@connection)
|
|
45
|
+
|
|
46
|
+
schema = reload(schema, deadline) if schema.version < version
|
|
47
|
+
raise TransactionError, CHANGED unless schema.version == version
|
|
48
|
+
|
|
49
|
+
schema
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reload(schema, deadline)
|
|
53
|
+
@cache.reload(schema, timeout: Transport::Deadline.remaining(deadline))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Clients with a transaction open in the current fiber. Thread#[] is fiber-local and, unlike
|
|
6
|
+
# Fiber storage, not inherited, so a thread or fiber spawned inside a transaction block may
|
|
7
|
+
# open its own transaction.
|
|
8
|
+
# @api private
|
|
9
|
+
module TransactionScope
|
|
10
|
+
NESTED = 'nested transactions are not supported'
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def enter(client)
|
|
15
|
+
open = Thread.current[:tarantool_transactions] ||= {}
|
|
16
|
+
raise TransactionError, NESTED if open.key?(client)
|
|
17
|
+
|
|
18
|
+
open[client] = true
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
yield
|
|
22
|
+
ensure
|
|
23
|
+
open.delete(client)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
module Tarantool
|
|
6
|
+
class Client
|
|
7
|
+
module Transport
|
|
8
|
+
# One multiplexed IPROTO connection. Any number of threads or fibers may
|
|
9
|
+
# perform requests concurrently; a reader thread routes responses by sync.
|
|
10
|
+
# @api private
|
|
11
|
+
class Connection
|
|
12
|
+
attr_reader :greeting, :server_features
|
|
13
|
+
attr_writer :event_handler, :disconnect_handler
|
|
14
|
+
|
|
15
|
+
def initialize(config)
|
|
16
|
+
@config = config
|
|
17
|
+
@pending = PendingRequests.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def connect
|
|
21
|
+
establish
|
|
22
|
+
|
|
23
|
+
self
|
|
24
|
+
rescue StandardError
|
|
25
|
+
close
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A request sent with no time left could still run on the server while the caller gets TimeoutError.
|
|
30
|
+
def perform(request, timeout:, stream_id: nil, schema_version: nil, on_push: nil)
|
|
31
|
+
deadline = Deadline.after(timeout)
|
|
32
|
+
raise TimeoutError, "no response within #{timeout}s" if Deadline.remaining(deadline)&.zero?
|
|
33
|
+
|
|
34
|
+
sync, future = @pending.register
|
|
35
|
+
frame = Protocol::Codec.encode(request, sync:, stream_id:, schema_version:)
|
|
36
|
+
response = exchange(frame, future, deadline, timeout, &on_push)
|
|
37
|
+
raise ServerError.from_response(response) if response.error?
|
|
38
|
+
|
|
39
|
+
response
|
|
40
|
+
ensure
|
|
41
|
+
@pending.delete(sync) if sync
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# For requests the server never answers (WATCH, UNWATCH): there is no sync to wait on.
|
|
45
|
+
def notify(request, timeout:)
|
|
46
|
+
write(Protocol::Codec.encode(request, sync: 0), Deadline.after(timeout))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close
|
|
50
|
+
@pending.fail_all(ClientClosedError.new("connection to #{@config.address} closed"))
|
|
51
|
+
@socket&.close
|
|
52
|
+
ThreadJoin.join(@reader)
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def closed?
|
|
58
|
+
!@pending.failure.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def establish
|
|
64
|
+
deadline = Deadline.after(@config.connect_timeout)
|
|
65
|
+
@socket = open_socket(deadline)
|
|
66
|
+
@writer = FrameWriter.new(@socket)
|
|
67
|
+
@greeting = read_greeting(deadline)
|
|
68
|
+
start_reader
|
|
69
|
+
|
|
70
|
+
@server_features = Handshake.perform(self, @config, deadline)
|
|
71
|
+
rescue IOError, SystemCallError, SocketError, TimeoutError, ServerError => e
|
|
72
|
+
raise ConnectionError, "cannot connect to #{@config.address}: #{e.message}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def open_socket(deadline)
|
|
76
|
+
return UNIXSocket.new(@config.path) if @config.path
|
|
77
|
+
|
|
78
|
+
Socket.tcp(@config.host, @config.port, connect_timeout: Deadline.remaining(deadline)).tap do |socket|
|
|
79
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def read_greeting(deadline)
|
|
84
|
+
Protocol::Greeting.parse(TimedRead.read_exactly(@socket, Protocol::Constants::GREETING_SIZE, deadline))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def start_reader
|
|
88
|
+
@reader = Thread.new { read_loop }
|
|
89
|
+
|
|
90
|
+
@reader.name = "tarantool-reader #{@config.address}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read_loop
|
|
94
|
+
loop { route(Protocol::Codec.read_response(@socket, max_frame_size: @config.max_frame_size)) }
|
|
95
|
+
rescue IOError, SystemCallError, ProtocolError => e
|
|
96
|
+
reason = e.message
|
|
97
|
+
rescue StandardError, SystemStackError => e
|
|
98
|
+
reason = "#{e.class}: #{e.message}"
|
|
99
|
+
ensure
|
|
100
|
+
break_connection("connection to #{@config.address} lost: #{reason || 'reader stopped'}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Events carry the sync of whichever WATCH triggered them, so they are routed by type.
|
|
104
|
+
def route(response)
|
|
105
|
+
return @pending.push(response.sync, response.data.first) if response.push?
|
|
106
|
+
return @event_handler&.call(response.event_key, response.event_data) if response.event?
|
|
107
|
+
|
|
108
|
+
@pending.resolve(response.sync, response)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A write that runs out of time, waiting for the lock or for the socket, is reported like a missing reply.
|
|
112
|
+
def exchange(frame, future, deadline, timeout, &)
|
|
113
|
+
write(frame, deadline)
|
|
114
|
+
|
|
115
|
+
future.value(Deadline.remaining(deadline), &)
|
|
116
|
+
rescue TimeoutError
|
|
117
|
+
raise TimeoutError, "no response within #{timeout}s"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A partially written frame corrupts the stream, so any interruption after the
|
|
121
|
+
# first byte (deadline, IO error, Thread#raise, Timeout) kills the connection.
|
|
122
|
+
def write(frame, deadline)
|
|
123
|
+
@writer.write(frame, deadline) { break_connection("write to #{@config.address} did not complete") }
|
|
124
|
+
rescue IOError, SystemCallError => e
|
|
125
|
+
failure = @pending.failure
|
|
126
|
+
raise failure.exception(failure.message) if failure
|
|
127
|
+
|
|
128
|
+
raise ConnectionError, "write to #{@config.address} failed: #{e.message}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The handler hears only of a loss: close records ClientClosedError first, so it stays silent.
|
|
132
|
+
def break_connection(message)
|
|
133
|
+
lost = @pending.fail_all(ConnectionError.new(message))
|
|
134
|
+
@socket&.close
|
|
135
|
+
|
|
136
|
+
@disconnect_handler&.call if lost
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Transport
|
|
6
|
+
# Monotonic-clock deadlines. A nil timeout gives a nil deadline, which never expires.
|
|
7
|
+
# @api private
|
|
8
|
+
module Deadline
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def after(timeout) = timeout && (now + timeout)
|
|
12
|
+
def remaining(deadline) = deadline && [deadline - now, 0].max
|
|
13
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Transport
|
|
6
|
+
# Serializes whole-frame writes to a socket; both the wait for the lock and the write are bounded by the deadline.
|
|
7
|
+
# Yields when a write is interrupted after part of the frame went out, because
|
|
8
|
+
# the stream is corrupt from then on.
|
|
9
|
+
# @api private
|
|
10
|
+
class FrameWriter
|
|
11
|
+
def initialize(socket)
|
|
12
|
+
@socket = socket
|
|
13
|
+
@lock = TimedLock.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Async interrupts (Thread#raise, Timeout) are held until the blocking wait, so they
|
|
17
|
+
# never land between a write and the update of the unsent remainder.
|
|
18
|
+
def write(frame, deadline)
|
|
19
|
+
@lock.synchronize(deadline) do
|
|
20
|
+
rest = frame
|
|
21
|
+
Thread.handle_interrupt(Object => :on_blocking) do
|
|
22
|
+
rest = write_some(rest, deadline) until rest.empty?
|
|
23
|
+
end
|
|
24
|
+
ensure
|
|
25
|
+
yield if rest && !rest.empty? && rest.bytesize < frame.bytesize
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def write_some(frame, deadline)
|
|
32
|
+
written = @socket.write_nonblock(frame, exception: false)
|
|
33
|
+
return frame.byteslice(written..) unless written == :wait_writable
|
|
34
|
+
raise TimeoutError, 'socket write timed out' unless @socket.wait_writable(Deadline.remaining(deadline))
|
|
35
|
+
|
|
36
|
+
frame
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# @api private
|
|
6
|
+
module Transport
|
|
7
|
+
# Result slot for one in-flight request, also carrying its pushed messages. Thread::Queue#pop
|
|
8
|
+
# yields to the Fiber Scheduler when one is active, so waiting never blocks other fibers.
|
|
9
|
+
# @api private
|
|
10
|
+
class Future
|
|
11
|
+
Push = Data.define(:message)
|
|
12
|
+
private_constant :Push
|
|
13
|
+
|
|
14
|
+
MAX_UNREAD_PUSHES = 10_000
|
|
15
|
+
private_constant :MAX_UNREAD_PUSHES
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@queue = Thread::Queue.new
|
|
19
|
+
@overflow = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolve(value)
|
|
23
|
+
@queue.push(value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reject(error)
|
|
27
|
+
@queue.push(error)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def push(message)
|
|
31
|
+
return if @overflow
|
|
32
|
+
|
|
33
|
+
if @queue.size >= MAX_UNREAD_PUSHES
|
|
34
|
+
@overflow = true
|
|
35
|
+
@queue.push(ProtocolError.new('too many unread push messages'))
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@queue.push(Push.new(message))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Yields each pushed message, in the waiting thread, until the final value arrives.
|
|
43
|
+
def value(timeout)
|
|
44
|
+
deadline = Deadline.after(timeout)
|
|
45
|
+
|
|
46
|
+
loop do
|
|
47
|
+
result = @queue.pop(timeout: Deadline.remaining(deadline))
|
|
48
|
+
raise TimeoutError, "no response within #{timeout}s" if result.nil?
|
|
49
|
+
# One error object is shared by all failed futures; each waiter raises its own copy
|
|
50
|
+
# so concurrent raises don't race on it.
|
|
51
|
+
raise result.exception(result.message) if result.is_a?(Exception)
|
|
52
|
+
return result unless result.is_a?(Push)
|
|
53
|
+
|
|
54
|
+
yield result.message if block_given?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Transport
|
|
6
|
+
# IPROTO_ID feature exchange, then chap-sha1 AUTH for a named user. Returns the server features.
|
|
7
|
+
# @api private
|
|
8
|
+
module Handshake
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def perform(connection, config, deadline)
|
|
12
|
+
id = Protocol::Requests::Id.new(
|
|
13
|
+
version: Protocol::Constants::PROTOCOL_VERSION, features: Protocol::Constants::CLIENT_FEATURES
|
|
14
|
+
)
|
|
15
|
+
response = connection.perform(id, timeout: Deadline.remaining(deadline))
|
|
16
|
+
authenticate(connection, config, deadline) if config.user && config.user != 'guest'
|
|
17
|
+
|
|
18
|
+
response.body.fetch(Protocol::Constants::FEATURES, [])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def authenticate(connection, config, deadline)
|
|
22
|
+
scramble = Protocol::Auth::ChapSha1.scramble(config.password.to_s, connection.greeting.salt)
|
|
23
|
+
request = Protocol::Requests::Auth.new(
|
|
24
|
+
user: config.user, mechanism: Protocol::Auth::ChapSha1::MECHANISM, scramble:
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
connection.perform(request, timeout: Deadline.remaining(deadline))
|
|
28
|
+
rescue ServerError => e
|
|
29
|
+
raise AuthenticationError.new(e.message, code: e.code)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private_class_method :authenticate
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|