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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Extensions
|
|
7
|
+
# MP_EXT interval: uint8 count, then (uint8 field id, msgpack int) pairs for non-zero values.
|
|
8
|
+
# @api private
|
|
9
|
+
module Interval
|
|
10
|
+
FIELD_NAMES = %i[year month week day hour min sec nsec adjust].freeze
|
|
11
|
+
FIELDS = FIELD_NAMES.each_with_index.to_h { |name, id| [id, name] }.freeze
|
|
12
|
+
ADJUSTS = { excess: 0, none: 1, last: 2 }.freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def pack(interval)
|
|
17
|
+
pairs = codes(interval).each_with_index.reject { |code, _id| code.zero? }
|
|
18
|
+
|
|
19
|
+
pairs.reduce([pairs.size].pack('C')) do |payload, (code, id)|
|
|
20
|
+
payload + [id].pack('C') + MessagePack.pack(code)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Count and field ids are uint8 below 0x80, which are byte-identical to positive fixints,
|
|
25
|
+
# so the whole payload reads as a msgpack stream.
|
|
26
|
+
def unpack(payload)
|
|
27
|
+
count, *pairs = MessagePack::Unpacker.new.feed(payload).each.to_a
|
|
28
|
+
raise ArgumentError, 'malformed interval: field count mismatch' unless pairs.size == count * 2
|
|
29
|
+
|
|
30
|
+
attributes = pairs.each_slice(2).to_h { |id, value| [FIELDS.fetch(id), value] }
|
|
31
|
+
attributes[:adjust] = decode_adjust(attributes.fetch(:adjust, 0))
|
|
32
|
+
|
|
33
|
+
Client::Interval.new(**attributes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def codes(interval)
|
|
37
|
+
values = interval.to_h.merge(adjust: encode_adjust(interval.adjust))
|
|
38
|
+
|
|
39
|
+
FIELD_NAMES.map { |name| values.fetch(name) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def encode_adjust(adjust)
|
|
43
|
+
ADJUSTS.fetch(adjust) { raise ArgumentError, "unknown interval adjust: #{adjust.inspect}" }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def decode_adjust(code)
|
|
47
|
+
ADJUSTS.key(code) || raise(ArgumentError, "malformed interval: adjust #{code}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private_class_method :codes, :encode_adjust, :decode_adjust
|
|
51
|
+
|
|
52
|
+
Extensions.register(
|
|
53
|
+
Constants::MP_INTERVAL, Client::Interval, packer: method(:pack), unpacker: method(:unpack)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Extensions
|
|
7
|
+
# MP_EXT uuid: 16 raw bytes in RFC 4122 order.
|
|
8
|
+
# @api private
|
|
9
|
+
module Uuid
|
|
10
|
+
SIZE = 16
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def pack(uuid)
|
|
15
|
+
[uuid.value.delete('-')].pack('H*')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unpack(payload)
|
|
19
|
+
raise ArgumentError, "malformed uuid: #{payload.bytesize} bytes" unless payload.bytesize == SIZE
|
|
20
|
+
|
|
21
|
+
Client::UUID.new(payload.unpack('H8H4H4H4H12').join('-'))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Extensions.register(Constants::MP_UUID, Client::UUID, packer: method(:pack), unpacker: method(:unpack))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
# MP_EXT types the codec knows; every file under extensions/ registers itself here.
|
|
7
|
+
# @api private
|
|
8
|
+
module Extensions
|
|
9
|
+
Entry = Data.define(:type, :klass, :options)
|
|
10
|
+
|
|
11
|
+
@entries = []
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
def register(type, klass, **options)
|
|
17
|
+
@entries << Entry.new(type:, klass:, options:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each(&)
|
|
21
|
+
@entries.each(&)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
# Greeting sent by the server on connect: a 64-byte banner and a 64-byte base64 salt line.
|
|
7
|
+
# @api private
|
|
8
|
+
Greeting = Data.define(:version, :salt) do
|
|
9
|
+
self::BANNER_VERSION = /\ATarantool (\S+)/
|
|
10
|
+
|
|
11
|
+
def self.parse(bytes)
|
|
12
|
+
banner = bytes.byteslice(0, 64)
|
|
13
|
+
version = banner[Greeting::BANNER_VERSION, 1]
|
|
14
|
+
raise ProtocolError, "unexpected greeting: #{banner.strip.inspect}" unless version
|
|
15
|
+
|
|
16
|
+
new(version:, salt: bytes.byteslice(64, 64).strip.unpack1('m'))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_AUTH: authenticate the session.
|
|
8
|
+
# @api private
|
|
9
|
+
Auth = Data.define(:user, :mechanism, :scramble) do
|
|
10
|
+
def type = Constants::AUTH
|
|
11
|
+
|
|
12
|
+
# Tarantool expects the scramble as MP_STR; binary Ruby strings would be packed as MP_BIN.
|
|
13
|
+
def body
|
|
14
|
+
{
|
|
15
|
+
Constants::USER_NAME => user,
|
|
16
|
+
Constants::TUPLE => [mechanism, scramble.dup.force_encoding(Encoding::UTF_8)]
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_BEGIN: open a transaction in a stream. The server rolls it back once the timeout (seconds) expires.
|
|
8
|
+
# @api private
|
|
9
|
+
Begin = Data.define(:isolation, :timeout) do
|
|
10
|
+
def initialize(isolation:, **)
|
|
11
|
+
unless Constants::ISOLATION_LEVELS.key?(isolation)
|
|
12
|
+
raise ArgumentError, "unknown isolation level: #{isolation.inspect}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type = Constants::BEGIN_TRANSACTION
|
|
19
|
+
|
|
20
|
+
# The server accepts only a MessagePack double for the timeout and rejects an integer.
|
|
21
|
+
def body
|
|
22
|
+
level = Constants::ISOLATION_LEVELS.fetch(isolation)
|
|
23
|
+
|
|
24
|
+
{ Constants::TXN_ISOLATION => level, Constants::TIMEOUT => timeout&.to_f }.compact
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_CALL: invoke a stored or global Lua function.
|
|
8
|
+
# @api private
|
|
9
|
+
Call = Data.define(:function_name, :arguments) do
|
|
10
|
+
def type = Constants::CALL
|
|
11
|
+
def body = { Constants::FUNCTION_NAME => function_name, Constants::TUPLE => arguments }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_COMMIT: commit the stream's transaction.
|
|
8
|
+
# @api private
|
|
9
|
+
Commit = Data.define do
|
|
10
|
+
def type = Constants::COMMIT
|
|
11
|
+
def body = {}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_DELETE: remove the tuple found by key.
|
|
8
|
+
# @api private
|
|
9
|
+
Delete = Data.define(:space_id, :index_id, :key) do
|
|
10
|
+
def type = Constants::DELETE
|
|
11
|
+
|
|
12
|
+
def body
|
|
13
|
+
{ Constants::SPACE_ID => space_id, Constants::INDEX_ID => index_id, Constants::KEY => key }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_EVAL: run a Lua chunk; arguments are available as `...`.
|
|
8
|
+
# @api private
|
|
9
|
+
Eval = Data.define(:expression, :arguments) do
|
|
10
|
+
def type = Constants::EVAL
|
|
11
|
+
def body = { Constants::EXPR => expression, Constants::TUPLE => arguments }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_EXECUTE: run SQL given as text or as a prepared statement id.
|
|
8
|
+
# @api private
|
|
9
|
+
Execute = Data.define(:statement, :bindings) do
|
|
10
|
+
def type = Constants::EXECUTE
|
|
11
|
+
|
|
12
|
+
def body
|
|
13
|
+
key = statement.is_a?(Integer) ? Constants::STMT_ID : Constants::SQL_TEXT
|
|
14
|
+
|
|
15
|
+
{ key => statement, Constants::SQL_BIND => bindings }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_ID: exchange protocol version and feature lists.
|
|
8
|
+
# @api private
|
|
9
|
+
Id = Data.define(:version, :features) do
|
|
10
|
+
def type = Constants::ID
|
|
11
|
+
def body = { Constants::VERSION => version, Constants::FEATURES => features }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_INSERT: fails on duplicate primary key.
|
|
8
|
+
# @api private
|
|
9
|
+
Insert = Data.define(:space_id, :tuple) do
|
|
10
|
+
def type = Constants::INSERT
|
|
11
|
+
def body = { Constants::SPACE_ID => space_id, Constants::TUPLE => tuple }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
# @api private
|
|
7
|
+
module Requests
|
|
8
|
+
# IPROTO_PING: liveness check.
|
|
9
|
+
# @api private
|
|
10
|
+
Ping = Data.define do
|
|
11
|
+
def type = Constants::PING
|
|
12
|
+
def body = {}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_PREPARE: prepare SQL text, or deallocate a prepared statement given its id.
|
|
8
|
+
# @api private
|
|
9
|
+
Prepare = Data.define(:statement) do
|
|
10
|
+
def type = Constants::PREPARE
|
|
11
|
+
def body = { (statement.is_a?(Integer) ? Constants::STMT_ID : Constants::SQL_TEXT) => statement }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_REPLACE: insert or overwrite by primary key.
|
|
8
|
+
# @api private
|
|
9
|
+
Replace = Data.define(:space_id, :tuple) do
|
|
10
|
+
def type = Constants::REPLACE
|
|
11
|
+
def body = { Constants::SPACE_ID => space_id, Constants::TUPLE => tuple }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_ROLLBACK: roll back the stream's transaction.
|
|
8
|
+
# @api private
|
|
9
|
+
Rollback = Data.define do
|
|
10
|
+
def type = Constants::ROLLBACK
|
|
11
|
+
def body = {}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_SELECT: index lookup or scan. `after` continues past a tuple (keyset pagination, 2.11+).
|
|
8
|
+
# @api private
|
|
9
|
+
Select = Data.define(:space_id, :index_id, :key, :limit, :offset, :iterator, :after) do
|
|
10
|
+
def initialize(iterator:, after: nil, **)
|
|
11
|
+
raise ArgumentError, "unknown iterator: #{iterator.inspect}" unless Constants::ITERATORS.key?(iterator)
|
|
12
|
+
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def type = Constants::SELECT
|
|
17
|
+
|
|
18
|
+
def body
|
|
19
|
+
{
|
|
20
|
+
Constants::SPACE_ID => space_id,
|
|
21
|
+
Constants::INDEX_ID => index_id,
|
|
22
|
+
Constants::LIMIT => limit,
|
|
23
|
+
Constants::OFFSET => offset,
|
|
24
|
+
Constants::ITERATOR => Constants::ITERATORS.fetch(iterator),
|
|
25
|
+
Constants::KEY => key
|
|
26
|
+
}.tap { |body| body[Constants::AFTER_TUPLE] = after unless after.nil? }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_UNWATCH: drop the subscription to a key. No reply.
|
|
8
|
+
# @api private
|
|
9
|
+
Unwatch = Data.define(:key) do
|
|
10
|
+
def type = Constants::UNWATCH
|
|
11
|
+
def body = { Constants::EVENT_KEY => key }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_UPDATE: field operations on the tuple found by key.
|
|
8
|
+
# @api private
|
|
9
|
+
Update = Data.define(:space_id, :index_id, :key, :operations) do
|
|
10
|
+
def type = Constants::UPDATE
|
|
11
|
+
|
|
12
|
+
def body
|
|
13
|
+
{
|
|
14
|
+
Constants::SPACE_ID => space_id,
|
|
15
|
+
Constants::INDEX_ID => index_id,
|
|
16
|
+
Constants::KEY => key,
|
|
17
|
+
Constants::TUPLE => operations
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_UPSERT: insert the tuple or apply operations to the existing one.
|
|
8
|
+
# @api private
|
|
9
|
+
Upsert = Data.define(:space_id, :tuple, :operations) do
|
|
10
|
+
def type = Constants::UPSERT
|
|
11
|
+
|
|
12
|
+
def body
|
|
13
|
+
{ Constants::SPACE_ID => space_id, Constants::TUPLE => tuple, Constants::OPS => operations }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
module Requests
|
|
7
|
+
# IPROTO_WATCH: subscribe to a key, or acknowledge the last event for it. No reply.
|
|
8
|
+
# @api private
|
|
9
|
+
Watch = Data.define(:key) do
|
|
10
|
+
def type = Constants::WATCH
|
|
11
|
+
def body = { Constants::EVENT_KEY => key }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
module Protocol
|
|
6
|
+
# Decoded IPROTO response frame.
|
|
7
|
+
# @api private
|
|
8
|
+
class Response
|
|
9
|
+
attr_reader :header, :body
|
|
10
|
+
|
|
11
|
+
def initialize(header, body)
|
|
12
|
+
@header = header
|
|
13
|
+
@body = body
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def type = header.fetch(Constants::REQUEST_TYPE)
|
|
17
|
+
def sync = header[Constants::SYNC]
|
|
18
|
+
def schema_version = header[Constants::SCHEMA_VERSION]
|
|
19
|
+
def data = body[Constants::DATA]
|
|
20
|
+
def error? = type.anybits?(Constants::TYPE_ERROR)
|
|
21
|
+
def push? = type == Constants::CHUNK
|
|
22
|
+
def event? = type == Constants::EVENT
|
|
23
|
+
def event_key = body[Constants::EVENT_KEY]
|
|
24
|
+
def event_data = body[Constants::EVENT_DATA]
|
|
25
|
+
def error_code = type & ~Constants::TYPE_ERROR
|
|
26
|
+
def error_message = body[Constants::ERROR_24]
|
|
27
|
+
|
|
28
|
+
def error_stack
|
|
29
|
+
body.fetch(Constants::ERROR, {}).fetch(Constants::ERROR_STACK, []).map do |entry|
|
|
30
|
+
entry.transform_keys(Constants::ERROR_FIELDS)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Space and index name -> id lookup and space formats loaded from the _vspace and _vindex system views over one
|
|
6
|
+
# connection. Its ids are valid only for the instance behind that connection.
|
|
7
|
+
# @api private
|
|
8
|
+
class Schema
|
|
9
|
+
attr_reader :version, :connection
|
|
10
|
+
|
|
11
|
+
def self.fetch(connection, timeout:)
|
|
12
|
+
deadline = Transport::Deadline.after(timeout)
|
|
13
|
+
spaces, indexes = [Protocol::Constants::VSPACE_ID, Protocol::Constants::VINDEX_ID].map do |space_id|
|
|
14
|
+
connection.perform(select_all(space_id), timeout: Transport::Deadline.remaining(deadline))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
new(version: spaces.schema_version, spaces: spaces.data, indexes: indexes.data, connection:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.select_all(space_id)
|
|
21
|
+
Protocol::Requests::Select.new(
|
|
22
|
+
space_id:, index_id: 0, key: [], limit: Protocol::Constants::MAX_LIMIT, offset: 0, iterator: :all
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
private_class_method :select_all
|
|
26
|
+
|
|
27
|
+
def initialize(version:, spaces:, indexes:, connection: nil)
|
|
28
|
+
@version = version
|
|
29
|
+
@connection = connection
|
|
30
|
+
@space_ids = spaces.to_h { |space| [space[2], space[0]] }
|
|
31
|
+
@index_ids = indexes.to_h { |index| [[index[0], index[2]], index[1]] }
|
|
32
|
+
@space_infos = build_space_infos(spaces, indexes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve(space, index)
|
|
36
|
+
space_id = space_id(space)
|
|
37
|
+
|
|
38
|
+
[space_id, index_id(space_id, index)]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def space_info(space)
|
|
42
|
+
@space_infos.fetch(space_id(space)) { raise SchemaError, "space '#{space}' not found" }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_space_infos(spaces, indexes)
|
|
48
|
+
indexes_by_space = indexes.group_by(&:first)
|
|
49
|
+
infos = spaces.map { |space| SpaceInfo.from_tuples(space, indexes_by_space.fetch(space.first, [])) }
|
|
50
|
+
|
|
51
|
+
infos.to_h { |info| [info.id, info] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def space_id(space)
|
|
55
|
+
return space if space.is_a?(Integer)
|
|
56
|
+
|
|
57
|
+
@space_ids.fetch(space.to_s) { raise SchemaError, "space '#{space}' not found" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def index_id(space_id, index)
|
|
61
|
+
return index if index.is_a?(Integer)
|
|
62
|
+
|
|
63
|
+
@index_ids.fetch([space_id, index.to_s]) do
|
|
64
|
+
raise SchemaError, "index '#{index}' not found in space #{space_id}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
class Client
|
|
5
|
+
# Latest known schema, installed by #replace for each new connection. Concurrent reloads of the same
|
|
6
|
+
# stale schema collapse into one fetch.
|
|
7
|
+
# @api private
|
|
8
|
+
class SchemaCache
|
|
9
|
+
attr_reader :schema
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@lock = Transport::TimedLock.new
|
|
13
|
+
@assign = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Fetches over the stale schema's own connection. A reconnect may replace the schema meanwhile, and
|
|
17
|
+
# its schema wins: the fetched one is kept only while the stale one is still current.
|
|
18
|
+
def reload(stale, timeout:)
|
|
19
|
+
deadline = Transport::Deadline.after(timeout)
|
|
20
|
+
|
|
21
|
+
@lock.synchronize(deadline) do
|
|
22
|
+
return @schema unless @schema.equal?(stale)
|
|
23
|
+
|
|
24
|
+
fresh = Schema.fetch(stale.connection, timeout: Transport::Deadline.remaining(deadline))
|
|
25
|
+
@assign.synchronize { @schema = fresh if @schema.equal?(stale) }
|
|
26
|
+
@schema
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Skips the TimedLock: a reload holding it may hang on a lost connection until its timeout, and a new
|
|
31
|
+
# connection's schema must not wait for that.
|
|
32
|
+
def replace(schema)
|
|
33
|
+
@assign.synchronize { @schema = schema }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# An unknown name may have been created after the last load, so it costs one reload.
|
|
37
|
+
def resolve(space, index, deadline, schema = @schema)
|
|
38
|
+
with_reload(schema, deadline) { |current| [current, current.resolve(space, index)] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def space_info(space, deadline)
|
|
42
|
+
with_reload(@schema, deadline) { |current| current.space_info(space) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def with_reload(schema, deadline)
|
|
48
|
+
yield schema
|
|
49
|
+
rescue SchemaError
|
|
50
|
+
yield reload(schema, timeout: Transport::Deadline.remaining(deadline))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|