emb 0.3.0 → 0.4.0.pre3

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.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'round_robin_pool'
4
+ require 'redis_client'
5
+
6
+ module Emb
7
+ # Owns a client's instance fan-out: one RoundRobinPool per configured url,
8
+ # instance-level round-robin on top of each pool's connection rotation, and
9
+ # the pre-send retry across instances.
10
+ #
11
+ # Retry is deliberately limited to PRESEND_CONNECTION_ERROR: a connection
12
+ # that was never established means the command was not written, so moving it
13
+ # to another instance is safe. Errors after a command may have been sent
14
+ # (timeouts, mid-flight connection loss) are never re-dispatched — retrying
15
+ # them would duplicate inference on the server.
16
+ class ConnectionRouter
17
+ # Captured at load so `rescue` keeps working when tests or apps replace the
18
+ # RedisClient constant (stub_const) — a runtime constant lookup in the
19
+ # rescue clause would otherwise resolve against the replacement.
20
+ PRESEND_CONNECTION_ERROR = RedisClient::CannotConnectError
21
+
22
+ attr_reader :pools
23
+
24
+ def initialize(size, urls, redis_options)
25
+ @pools = urls.map do |url|
26
+ RoundRobinPool.new(size) do
27
+ RedisClient.new(url: url, **redis_options)
28
+ end
29
+ end
30
+ @next_instance = 0
31
+ @instance_mutex = Mutex.new
32
+ end
33
+
34
+ def call(*args)
35
+ attempts = @pools.size
36
+ idx = pick_instance
37
+ last_error = nil
38
+ attempts.times do
39
+ return perform_command(@pools[idx], args)
40
+ rescue PRESEND_CONNECTION_ERROR => e
41
+ last_error = e
42
+ idx = (idx + 1) % @pools.size if @pools.size > 1
43
+ end
44
+ raise last_error
45
+ end
46
+
47
+ private
48
+
49
+ def pick_instance
50
+ return 0 if @pools.size == 1
51
+
52
+ @instance_mutex.synchronize do
53
+ idx = @next_instance % @pools.size
54
+ @next_instance += 1
55
+ idx
56
+ end
57
+ end
58
+
59
+ def perform_command(pool, args)
60
+ debug = Emb.debug?
61
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC) if debug
62
+ result = pool.with { |r| r.call(*args) }
63
+ log_command(args, started) if debug
64
+ result
65
+ end
66
+
67
+ def log_command(args, started)
68
+ elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000
69
+ $stdout.puts "[EMB] #{args.map(&:inspect).join(' ')} (#{format('%.2f', elapsed)}ms)"
70
+ end
71
+ end
72
+ end
data/lib/emb/errors.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emb
4
+ # Raised when an EMB.MULTI batch fails — after redis-client exhausted its
5
+ # configured re-sends (`reconnect_attempts`) on a transient error, or on the
6
+ # first attempt for an operation error (server error reply) or the default
7
+ # `reconnect_attempts: 0` configuration. The underlying redis error is
8
+ # preserved as `cause`; `attempts` counts the wire sends that were made.
9
+ class ServerError < StandardError
10
+ attr_reader :attempts
11
+
12
+ def initialize(message, attempts: 1)
13
+ super(message)
14
+ @attempts = attempts
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'batch_loader'
4
+ require_relative 'batch_scope'
5
+
6
+ module Emb
7
+ # Job middleware: clears the per-thread batch scope after each job execution,
8
+ # even when the job raises. Registered as a Sidekiq/Shoryuken server
9
+ # middleware (positional args differ per framework and are unused) and used by
10
+ # the ActiveJob perform callback.
11
+ class JobMiddleware
12
+ def call(*_args, &)
13
+ BatchScope.wrap(&)
14
+ end
15
+ end
16
+ end
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'batch_loader'
4
+ require_relative 'batch_scope'
4
5
 
5
6
  module Emb
6
- # Rack middleware that clears the per-thread batch scope at the end of each
7
- # request, bounding cache growth in long-lived request threads. The scope is
8
- # cleared even when the app raises; a fresh scope starts with the next request.
7
+ # Rack middleware: clears the per-thread batch scope after each request,
8
+ # even when the app raises.
9
9
  class Middleware
10
10
  def initialize(app)
11
11
  @app = app
12
12
  end
13
13
 
14
14
  def call(env)
15
- @app.call(env)
16
- ensure
17
- BatchLoader::Executor.clear_current
15
+ BatchScope.wrap { @app.call(env) }
18
16
  end
19
17
  end
20
18
  end
data/lib/emb/proxy.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'values_reply'
4
+
3
5
  module Emb
4
6
  class Proxy
5
7
  attr_reader :name
@@ -9,8 +11,20 @@ module Emb
9
11
  @name = name
10
12
  end
11
13
 
12
- def [](text, *texts)
13
- return Emb.build_batch_loader(@client, @name, texts.empty? ? text : [text, *texts]) if @client.batch?
14
+ # Queries the model for one or more texts. format: :binary (default)
15
+ # returns the float32-unpacked vectors from the compact BLOB wire;
16
+ # format: :values sends the VALUES keyword and returns the decoded
17
+ # envelope — a Hash with dtype:/shape:/values: (Flats, flat for a single
18
+ # text, grouped per text when several are requested).
19
+ def [](text, *texts, format: :binary)
20
+ format = normalize_format(format)
21
+ if @client.lazy?
22
+ return Emb.build_batch_loader(@client, @name, texts.empty? ? text : [text, *texts], format: format)
23
+ end
24
+
25
+ if format == :values
26
+ return values_query(text, texts)
27
+ end
14
28
 
15
29
  set = Array(@client.send_command('EMB', @name.to_s, text, *texts))
16
30
  result = set.map { |entry| entry.unpack('e*') }
@@ -23,5 +37,23 @@ module Emb
23
37
  def inspect
24
38
  "#<Emb::Proxy #{@name}>"
25
39
  end
40
+
41
+ private
42
+
43
+ def normalize_format(format)
44
+ unless %i[binary values].include?(format)
45
+ raise ArgumentError, "unknown format #{format.inspect} (expected :binary or :values)"
46
+ end
47
+
48
+ format
49
+ end
50
+
51
+ def values_query(text, texts)
52
+ reply = @client.send_command('EMB', @name.to_s, 'VALUES', text, *texts)
53
+ envelope = Emb::ValuesReply.parse(reply)
54
+ return envelope if texts.empty?
55
+
56
+ envelope.merge(values: Emb::ValuesReply.rows(envelope[:shape], envelope[:values]))
57
+ end
26
58
  end
27
59
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Rails::Railtie)
4
+ require_relative '../emb'
5
+
6
+ module Emb
7
+ # Rails integration, loaded by emb.rb when Rails is present.
8
+ #
9
+ # config.emb.middleware = false # skip Emb::Middleware in the stack
10
+ # config.emb.job_middleware = false # skip job-scope protection
11
+ class Railtie < Rails::Railtie
12
+ config.emb = ActiveSupport::OrderedOptions.new
13
+ config.emb.middleware = true
14
+ config.emb.job_middleware = true
15
+
16
+ initializer 'emb.middleware' do |app|
17
+ next if config.emb.middleware == false
18
+ next if app.middleware.include?(Emb::Middleware)
19
+
20
+ app.middleware.use Emb::Middleware
21
+ end
22
+
23
+ config.after_initialize do
24
+ next if config.emb.job_middleware == false
25
+
26
+ ActiveSupport.on_load(:active_job) do |base|
27
+ base.around_perform { |_job, block| Emb::BatchScope.wrap { block.call } }
28
+ end
29
+
30
+ if defined?(Sidekiq)
31
+ Sidekiq.configure_server do |sidekiq_config|
32
+ sidekiq_config.server_middleware { |chain| chain.add Emb::JobMiddleware }
33
+ end
34
+ end
35
+
36
+ if defined?(Sidekiq::Testing)
37
+ # Testing modes use their own middleware chain.
38
+ Sidekiq::Testing.server_middleware { |chain| chain.add Emb::JobMiddleware }
39
+ end
40
+
41
+ if defined?(Shoryuken)
42
+ Shoryuken.configure_server do |shoryuken_config|
43
+ shoryuken_config.server_middleware { |chain| chain.add Emb::JobMiddleware }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emb
4
+ # A thread-safe pool of N RedisClient connections with round-robin selection.
5
+ # Behind connection-level load balancers (AWS Service Connect, an NLB, or an
6
+ # Envoy TCP proxy) each keep-alive connection is pinned to one upstream
7
+ # instance, so rotating commands across the pool spreads traffic across every
8
+ # instance — even single-threaded at zero concurrency. Connections are created
9
+ # up front but connect lazily on first use.
10
+ #
11
+ # Two behaviors deliberately match the connection_pool gem it replaces: a
12
+ # nested `with` from the same thread re-enters the held connection, and after
13
+ # `fork` (Puma preload_app, unicorn, resque) the pool closes inherited sockets
14
+ # and rebuilds its mutexes in the child so parent and child never share a
15
+ # connection.
16
+ class RoundRobinPool
17
+ # Pools are tracked only to reset them in forked children. WeakMap so a
18
+ # pool is reclaimed together with its client.
19
+ INSTANCES = Process.respond_to?(:fork) ? ObjectSpace::WeakMap.new : nil
20
+ private_constant :INSTANCES
21
+
22
+ THREAD_KEY = :emb_round_robin_pool_held
23
+ private_constant :THREAD_KEY
24
+
25
+ attr_reader :size, :connections
26
+
27
+ def self.after_fork
28
+ INSTANCES&.each_value(&:reload_after_fork!)
29
+ end
30
+
31
+ def initialize(size, &)
32
+ raise ArgumentError, "pool size must be >= 1 (got #{size})" if size < 1
33
+
34
+ @size = size
35
+ @connections = Array.new(size, &)
36
+ @locks = Array.new(size) { Mutex.new }
37
+ @next = 0
38
+ @index_mutex = Mutex.new
39
+ INSTANCES&.[]=(self, self)
40
+ end
41
+
42
+ # Yields the next connection in rotation order. Safe from multiple threads:
43
+ # up to `size` commands run in parallel, each on its own connection. A
44
+ # nested `with` from the same thread re-enters the connection this pool
45
+ # already holds without re-locking; other pools are unaffected.
46
+ def with(&)
47
+ held = Thread.current[THREAD_KEY]
48
+ if held&.key?(self)
49
+ yield @connections[held[self]]
50
+ else
51
+ take(held, &)
52
+ end
53
+ end
54
+
55
+ # Child side of after_fork(): drop inherited sockets and sync state.
56
+ def reload_after_fork!
57
+ @connections.each { |conn| conn.close if conn.respond_to?(:close) }
58
+ @locks = Array.new(@size) { Mutex.new }
59
+ @next = 0
60
+ @index_mutex = Mutex.new
61
+ end
62
+
63
+ if Process.respond_to?(:fork)
64
+ # Hooks Process._fork (MRI 3.1+) so registered pools reset in the child.
65
+ module ForkTracker
66
+ def _fork
67
+ pid = super
68
+ RoundRobinPool.after_fork if pid.zero?
69
+ pid
70
+ end
71
+ end
72
+ Process.singleton_class.prepend(ForkTracker)
73
+ end
74
+
75
+ private
76
+
77
+ # Acquires the next connection, records it as held by this thread/pool, and
78
+ # releases both on exit — even when the block raises.
79
+ def take(held)
80
+ idx, connection = pick
81
+ held ||= {}
82
+ Thread.current[THREAD_KEY] = held
83
+ held[self] = idx
84
+ begin
85
+ @locks[idx].synchronize { yield connection }
86
+ ensure
87
+ held.delete(self)
88
+ Thread.current[THREAD_KEY] = nil if held.empty?
89
+ end
90
+ end
91
+
92
+ def pick
93
+ @index_mutex.synchronize do
94
+ idx = @next % @size
95
+ @next += 1
96
+ [idx, @connections[idx]]
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emb
4
+ # Opt-in float decoding for scripted replies — the decode: keyword on
5
+ # Emb::Commands#eval / #evalsha. Kept out of the Commands module so the
6
+ # command surface stays small. decode: is validated (normalize_decode) before
7
+ # any command is sent, then applied after reply parsing.
8
+ module ScriptReplyDecode
9
+ private
10
+
11
+ # Validates an (un-normalized) decode option and canonicalizes it. Raises
12
+ # ArgumentError for unknown modes so callers fail before any command is
13
+ # sent. nil and an empty hash both mean "no decoding".
14
+ def normalize_decode(decode)
15
+ case decode
16
+ when nil, :f32
17
+ decode
18
+ when Hash
19
+ schema = decode.each_with_object({}) do |(field, mode), acc|
20
+ unless mode == :f32
21
+ raise ArgumentError, "unsupported decode mode #{mode.inspect} for field #{field.inspect} (supported: :f32)"
22
+ end
23
+
24
+ acc[field.to_s] = :f32
25
+ end
26
+ schema.empty? ? nil : schema
27
+ else
28
+ raise ArgumentError, "unsupported decode mode #{decode.inspect} (supported: :f32 or {field => :f32})"
29
+ end
30
+ end
31
+
32
+ # Applies a normalized decode to the parsed (script_hash'ed) reply array.
33
+ def apply_decode(parsed, decode)
34
+ return parsed if decode.nil?
35
+
36
+ case decode
37
+ when :f32
38
+ parsed.each_with_index.map { |v, i| decode_f32(v, position(i)) }
39
+ when Hash
40
+ parsed.each_with_index.map { |v, i| decode_hash_fields(v, decode, position(i)) }
41
+ end
42
+ end
43
+
44
+ def position(index)
45
+ index.zero? ? 'the reply' : "element #{index + 1}"
46
+ end
47
+
48
+ # decode: :f32 — a float32-packed bulk unpacks to floats; a numeric array
49
+ # (the pre-float32_bytes reply style) passes through unchanged; anything
50
+ # else is a contract mismatch and raises.
51
+ def decode_f32(value, where)
52
+ case value
53
+ when String
54
+ unless (value.bytesize % 4).zero?
55
+ raise ArgumentError,
56
+ "decode :f32: #{where} is a #{value.bytesize}-byte bulk, not a multiple of 4 — not a float32 vector"
57
+ end
58
+
59
+ value.unpack('e*')
60
+ when Array
61
+ return value if value.all?(Numeric)
62
+
63
+ raise ArgumentError, "decode :f32: #{where} is an array of non-numbers, not a float vector"
64
+ else
65
+ raise ArgumentError,
66
+ "decode :f32: #{where} is a #{value.class}, expected a float32-packed bulk or a numeric array"
67
+ end
68
+ end
69
+
70
+ # decode: {field => :f32} — the reply (or each per-text reply) must be a
71
+ # hash; present fields decode as :f32, absent fields stay untouched.
72
+ def decode_hash_fields(value, schema, where)
73
+ unless value.is_a?(Hash)
74
+ raise ArgumentError, "decode #{schema.inspect}: #{where} is a #{value.class}, expected a hash reply"
75
+ end
76
+
77
+ schema.each_key do |field|
78
+ next unless value.key?(field)
79
+
80
+ value[field] = decode_f32(value[field], "field #{field.inspect} of #{where}")
81
+ end
82
+ value
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'values_reply'
4
+
5
+ module Emb
6
+ # VALUES-format batch resolution for the deferred path. Split out of
7
+ # BatchDispatch so both modules stay under the Metrics rubric.
8
+ module ValuesBatch
9
+ module_function
10
+
11
+ # Resolves a VALUES reply onto the slice's items. Single-model slices get
12
+ # ONE envelope reply carrying every text's flattened values (row-major),
13
+ # so the rows are re-sliced per item; mixed-model slices get per-pair
14
+ # envelopes and map one row each. Failures stay nil, mirroring the BLOB
15
+ # path's MGET semantics.
16
+ def resolve(loader, slice, results)
17
+ if single_model?(slice)
18
+ rows = envelope_rows(results)
19
+ expect_rows!(slice, rows.size)
20
+ assign_rows(loader, slice, rows)
21
+ else
22
+ resolve_entries(loader, slice, results)
23
+ end
24
+ end
25
+
26
+ def single_model?(slice)
27
+ slice.map { |item| item[1] }.uniq.size == 1
28
+ end
29
+
30
+ def envelope_rows(results)
31
+ envelope = Emb::ValuesReply.parse(rehydrate_envelope(results))
32
+ Emb::ValuesReply.rows(envelope[:shape], envelope[:values])
33
+ end
34
+
35
+ # Under RESP3, redis-client decodes the server's single-model VALUES map
36
+ # into a Ruby Hash, but dispatch_slice wraps every reply in Array(), which
37
+ # turns that Hash into an array of [key, value] pairs. Rehydrate those
38
+ # pairs back into a Hash so parse sees a real envelope; RESP2 flat pair
39
+ # arrays (top-level strings) pass through untouched.
40
+ def rehydrate_envelope(results)
41
+ return results.to_h if results.all? { |e| e.is_a?(Array) && e.size == 2 }
42
+
43
+ results
44
+ end
45
+
46
+ def expect_rows!(slice, got)
47
+ expected = slice.sum { |_, _, text, _| Array(text).size }
48
+ return if got >= expected
49
+
50
+ raise ShortReplyError, "expected #{expected} VALUES rows, got #{got}"
51
+ end
52
+
53
+ def assign_rows(loader, slice, rows)
54
+ offset = 0
55
+ slice.each do |item|
56
+ _, _, text, = item
57
+ texts = Array(text)
58
+ values = rows[offset, texts.size]
59
+ offset += texts.size
60
+ loader.call(item, values.size == 1 ? values.first : values)
61
+ end
62
+ end
63
+
64
+ def resolve_entries(loader, slice, results)
65
+ expect_entries!(slice, results.size)
66
+ offset = 0
67
+ slice.each do |item|
68
+ _, _, text, = item
69
+ texts = Array(text)
70
+ values = entry_values(results, offset, texts)
71
+ offset += texts.size
72
+ loader.call(item, values.size == 1 ? values.first : values)
73
+ end
74
+ end
75
+
76
+ # A short multi-model reply is a protocol violation: an explicit null slot
77
+ # is the only legal way to signal a missing pair (see the BLOB path).
78
+ def expect_entries!(slice, got)
79
+ expected = slice.sum { |_, _, text, _| Array(text).size }
80
+ return if got >= expected
81
+
82
+ raise ShortReplyError, "expected #{expected} VALUES reply entries, got #{got}"
83
+ end
84
+
85
+ def entry_values(results, offset, texts)
86
+ results[offset, texts.size].map { |entry| entry && row_of(entry) }
87
+ end
88
+
89
+ def row_of(entry)
90
+ envelope = Emb::ValuesReply.parse(entry)
91
+ Emb::ValuesReply.rows(envelope[:shape], envelope[:values]).first
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emb
4
+ # Decoding for EMB VALUES envelopes (the RedisAI META+VALUES shape). The
5
+ # server replies with flat alternating key/value pairs under RESP2 — dtype,
6
+ # shape, values — where values are decimal bulk strings carrying the float64
7
+ # widening of each float32 dimension.
8
+ module ValuesReply
9
+ module_function
10
+
11
+ # Parses one envelope into a Hash. RESP2 replies arrive as a flat 6-element
12
+ # pair array; under `protocol: 3` redis-client already decoded the server's
13
+ # RESP3 map into a Ruby Hash (keys frozen strings, values already Floats),
14
+ # which is used directly. values are converted to Ruby Floats; an
15
+ # unparsable entry raises ArgumentError naming the offending value.
16
+ def parse(reply)
17
+ pairs = reply.is_a?(Hash) ? reply : reply.each_slice(2).to_h
18
+ shape = pairs.fetch('shape')
19
+ values = Array(pairs.fetch('values')).map { |v| float_value(v) }
20
+ { dtype: pairs.fetch('dtype'), shape: shape, values: values }
21
+ end
22
+
23
+ # rows reshapes flat values into per-text rows using shape [m, dim].
24
+ def rows(shape, values)
25
+ dim = shape[1].to_i
26
+ values.each_slice(dim).to_a
27
+ end
28
+
29
+ def float_value(v)
30
+ Float(v)
31
+ rescue ArgumentError, TypeError
32
+ raise ArgumentError, "EMB VALUES: unparsable decimal value #{v.inspect} (expected a numeric bulk string)"
33
+ end
34
+ end
35
+ end
data/lib/emb.rb CHANGED
@@ -2,13 +2,17 @@
2
2
 
3
3
  require_relative 'emb/version'
4
4
  require_relative 'emb/configuration'
5
+ require_relative 'emb/errors'
5
6
  require_relative 'emb/commands'
6
7
  require_relative 'emb/runtime_config'
7
8
  require_relative 'emb/client'
8
9
  require_relative 'emb/proxy'
10
+ require_relative 'emb/batch_scope'
11
+ require_relative 'emb/middleware'
12
+ require_relative 'emb/job_middleware'
9
13
  require_relative 'emb/multi'
10
14
  require_relative 'emb/batch'
11
- require_relative 'emb/middleware'
15
+ require_relative 'emb/railtie' if defined?(Rails::Railtie)
12
16
 
13
17
  module Emb
14
18
  class << self
@@ -28,19 +32,33 @@ module Emb
28
32
  end
29
33
 
30
34
  def [](name) = default_client[name]
31
- def batch = default_client.batch
32
35
  def models = default_client.models
33
36
  def info(name) = default_client.info(name)
34
37
  def stats = default_client.stats
35
38
  def server_info(*sections) = default_client.server_info(*sections)
39
+ def cache_flush(model = nil) = default_client.cache_flush(model)
40
+ def save_cache = default_client.save_cache
36
41
  def config = default_client.config
37
42
  def help = default_client.help
38
43
  def ping = default_client.ping
39
44
  def ready = default_client.ready
40
45
  def ready? = default_client.ready?
41
46
  def multi(&) = default_client.multi(&)
47
+
48
+ def eval(model, script, texts, args = [], decode: nil)
49
+ default_client.eval(model, script, texts, args, decode: decode)
50
+ end
51
+
52
+ def evalsha(model, sha, texts, args = [], decode: nil)
53
+ default_client.evalsha(model, sha, texts, args, decode: decode)
54
+ end
55
+
56
+ def script = default_client.script
57
+
42
58
  def reset_registry! = default_client.reset_registry!
59
+
43
60
  def debug? = @debug
61
+
44
62
  def send_command(*) = default_client.send_command(*)
45
63
 
46
64
  def debug!
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0.pre3
5
5
  platform: ruby
6
6
  authors:
7
7
  - elcuervo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-01 00:00:00.000000000 Z
11
+ date: 2026-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: batch-loader
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
- - !ruby/object:Gem::Dependency
28
- name: connection_pool
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.5'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.5'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: redis-client
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -63,13 +49,23 @@ files:
63
49
  - README.md
64
50
  - lib/emb.rb
65
51
  - lib/emb/batch.rb
52
+ - lib/emb/batch_dispatch.rb
53
+ - lib/emb/batch_scope.rb
66
54
  - lib/emb/client.rb
67
55
  - lib/emb/commands.rb
68
56
  - lib/emb/configuration.rb
57
+ - lib/emb/connection_router.rb
58
+ - lib/emb/errors.rb
59
+ - lib/emb/job_middleware.rb
69
60
  - lib/emb/middleware.rb
70
61
  - lib/emb/multi.rb
71
62
  - lib/emb/proxy.rb
63
+ - lib/emb/railtie.rb
64
+ - lib/emb/round_robin_pool.rb
72
65
  - lib/emb/runtime_config.rb
66
+ - lib/emb/script_reply_decode.rb
67
+ - lib/emb/values_batch.rb
68
+ - lib/emb/values_reply.rb
73
69
  - lib/emb/version.rb
74
70
  homepage: https://github.com/elcuervo/emb
75
71
  licenses: