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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE +21 -0
  4. data/README.md +222 -0
  5. data/lib/tarantool/client/backoff.rb +23 -0
  6. data/lib/tarantool/client/box_error.rb +15 -0
  7. data/lib/tarantool/client/callback_guard.rb +23 -0
  8. data/lib/tarantool/client/configuration.rb +71 -0
  9. data/lib/tarantool/client/errors.rb +56 -0
  10. data/lib/tarantool/client/fork_safety.rb +42 -0
  11. data/lib/tarantool/client/index_info.rb +27 -0
  12. data/lib/tarantool/client/interval.rb +25 -0
  13. data/lib/tarantool/client/operations.rb +89 -0
  14. data/lib/tarantool/client/pool/member.rb +141 -0
  15. data/lib/tarantool/client/pool/member_fork_safety.rb +34 -0
  16. data/lib/tarantool/client/pool.rb +127 -0
  17. data/lib/tarantool/client/prepared_statement.rb +25 -0
  18. data/lib/tarantool/client/protocol/auth/chap_sha1.rb +28 -0
  19. data/lib/tarantool/client/protocol/codec.rb +79 -0
  20. data/lib/tarantool/client/protocol/constants.rb +105 -0
  21. data/lib/tarantool/client/protocol/extensions/datetime.rb +39 -0
  22. data/lib/tarantool/client/protocol/extensions/decimal.rb +73 -0
  23. data/lib/tarantool/client/protocol/extensions/error.rb +33 -0
  24. data/lib/tarantool/client/protocol/extensions/interval.rb +59 -0
  25. data/lib/tarantool/client/protocol/extensions/uuid.rb +29 -0
  26. data/lib/tarantool/client/protocol/extensions.rb +27 -0
  27. data/lib/tarantool/client/protocol/greeting.rb +21 -0
  28. data/lib/tarantool/client/protocol/requests/auth.rb +23 -0
  29. data/lib/tarantool/client/protocol/requests/begin.rb +30 -0
  30. data/lib/tarantool/client/protocol/requests/call.rb +16 -0
  31. data/lib/tarantool/client/protocol/requests/commit.rb +16 -0
  32. data/lib/tarantool/client/protocol/requests/delete.rb +19 -0
  33. data/lib/tarantool/client/protocol/requests/eval.rb +16 -0
  34. data/lib/tarantool/client/protocol/requests/execute.rb +21 -0
  35. data/lib/tarantool/client/protocol/requests/id.rb +16 -0
  36. data/lib/tarantool/client/protocol/requests/insert.rb +16 -0
  37. data/lib/tarantool/client/protocol/requests/ping.rb +17 -0
  38. data/lib/tarantool/client/protocol/requests/prepare.rb +16 -0
  39. data/lib/tarantool/client/protocol/requests/replace.rb +16 -0
  40. data/lib/tarantool/client/protocol/requests/rollback.rb +16 -0
  41. data/lib/tarantool/client/protocol/requests/select.rb +32 -0
  42. data/lib/tarantool/client/protocol/requests/unwatch.rb +16 -0
  43. data/lib/tarantool/client/protocol/requests/update.rb +24 -0
  44. data/lib/tarantool/client/protocol/requests/upsert.rb +19 -0
  45. data/lib/tarantool/client/protocol/requests/watch.rb +16 -0
  46. data/lib/tarantool/client/protocol/response.rb +36 -0
  47. data/lib/tarantool/client/schema.rb +69 -0
  48. data/lib/tarantool/client/schema_cache.rb +54 -0
  49. data/lib/tarantool/client/space_info.rb +27 -0
  50. data/lib/tarantool/client/sql_result.rb +24 -0
  51. data/lib/tarantool/client/stream.rb +130 -0
  52. data/lib/tarantool/client/thread_join.rb +19 -0
  53. data/lib/tarantool/client/transaction_schema.rb +57 -0
  54. data/lib/tarantool/client/transaction_scope.rb +28 -0
  55. data/lib/tarantool/client/transport/connection.rb +141 -0
  56. data/lib/tarantool/client/transport/deadline.rb +17 -0
  57. data/lib/tarantool/client/transport/frame_writer.rb +41 -0
  58. data/lib/tarantool/client/transport/future.rb +60 -0
  59. data/lib/tarantool/client/transport/handshake.rb +36 -0
  60. data/lib/tarantool/client/transport/link.rb +150 -0
  61. data/lib/tarantool/client/transport/link_state.rb +115 -0
  62. data/lib/tarantool/client/transport/pending_requests.rb +55 -0
  63. data/lib/tarantool/client/transport/stream_ids.rb +20 -0
  64. data/lib/tarantool/client/transport/timed_lock.rb +53 -0
  65. data/lib/tarantool/client/transport/timed_read.rb +31 -0
  66. data/lib/tarantool/client/uuid.rb +20 -0
  67. data/lib/tarantool/client/version.rb +7 -0
  68. data/lib/tarantool/client/watcher.rb +37 -0
  69. data/lib/tarantool/client/watchers/dispatcher.rb +40 -0
  70. data/lib/tarantool/client/watchers.rb +137 -0
  71. data/lib/tarantool/client.rb +216 -0
  72. 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