emb 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -4
- data/lib/emb/round_robin_pool.rb +45 -37
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70d880b22371c80bf58cde8ace64e25553d4105dfd029ae6fbfe6835f5c5ca1f
|
|
4
|
+
data.tar.gz: 19147276337343ef56c5b11cc4901f30db6839be1f23160941ba781634993b04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d9fdb749c21f8583f0e9fb6125b1dab3037b6eba1f2112f6839dcb5fa9740731a2a9fb4ef566fb56aee6435a4fd3d5e4015aec2384f784948169dfa80f378ca
|
|
7
|
+
data.tar.gz: 0a406f1e40b838ae8b39b3c5276c2ac471d5a7a33fd82cbee65a2e33c5b7095d74cc8c06833a4e12c2c84c259db96e8a97e42266830225564b9c8dc901beaa86
|
data/README.md
CHANGED
|
@@ -102,6 +102,11 @@ Emb.setup(url: "redis://localhost:6379", pool: 10)
|
|
|
102
102
|
The default pool size is **5**. The client keeps `pool` persistent connections per
|
|
103
103
|
emb **instance** and routes commands through them **in round-robin order** —
|
|
104
104
|
consecutive commands use different connections instead of reusing one hot connection.
|
|
105
|
+
Selection is **work-conserving**: a command takes the next *available* connection and
|
|
106
|
+
never waits behind a busy one while another connection is free, so an occasional slow
|
|
107
|
+
inference cannot stall commands that another connection could serve. A released
|
|
108
|
+
connection returns to the tail of the free queue, so sequential commands still rotate
|
|
109
|
+
in order.
|
|
105
110
|
|
|
106
111
|
`url` also accepts an **array of instances** (interchangeable replicas serving the same
|
|
107
112
|
model set); commands then round-robin across instances first, then across connections
|
|
@@ -126,7 +131,7 @@ and queue it there even while other instances sit idle. Round-robin spreading ac
|
|
|
126
131
|
**at least your expected emb instance count** (e.g. `pool: 10` for 10 instances).
|
|
127
132
|
|
|
128
133
|
Commands beyond the pool's parallelism do **not** time out: when `pool` commands are
|
|
129
|
-
already in flight, later commands wait
|
|
134
|
+
already in flight, later commands wait for the first connection to free
|
|
130
135
|
(there is no checkout timeout — unlike the previous pool's 5s
|
|
131
136
|
`ConnectionPool::TimeoutError`). Only the wait for a free pool connection is
|
|
132
137
|
unbounded — once a command runs, `RedisClient`'s `connect_timeout`,
|
|
@@ -138,9 +143,10 @@ pool size — every command reaches the same server. The pool size still control
|
|
|
138
143
|
how many persistent connections the client holds and how many commands run in
|
|
139
144
|
parallel. The pool is usually not the bottleneck for
|
|
140
145
|
inference-bound workloads (small pools are fine); it becomes a knob at high
|
|
141
|
-
concurrency on a multi-model box — see [Performance](#performance).
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
concurrency on a multi-model box — see [Performance](#performance). Up to `pool`
|
|
147
|
+
commands run in parallel; beyond that, commands wait for the first connection to
|
|
148
|
+
free rather than blocking on a specific busy one, so size `pool` to your expected
|
|
149
|
+
concurrency.
|
|
144
150
|
|
|
145
151
|
### Authentication
|
|
146
152
|
|
data/lib/emb/round_robin_pool.rb
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Emb
|
|
4
|
-
# A thread-safe pool of N RedisClient connections with
|
|
5
|
-
# Behind connection-level load balancers (AWS Service
|
|
6
|
-
# Envoy TCP proxy) each keep-alive connection is pinned
|
|
7
|
-
# instance, so rotating commands across the pool spreads
|
|
8
|
-
# instance — even single-threaded at zero concurrency.
|
|
9
|
-
# up front but connect lazily on first use.
|
|
4
|
+
# A thread-safe pool of N RedisClient connections with work-conserving
|
|
5
|
+
# round-robin selection. Behind connection-level load balancers (AWS Service
|
|
6
|
+
# Connect, an NLB, or an Envoy TCP proxy) each keep-alive connection is pinned
|
|
7
|
+
# to one upstream instance, so rotating commands across the pool spreads
|
|
8
|
+
# traffic across every instance — even single-threaded at zero concurrency.
|
|
9
|
+
# Connections are created up front but connect lazily on first use.
|
|
10
|
+
#
|
|
11
|
+
# Selection is work-conserving: a command takes the next *available*
|
|
12
|
+
# connection, never waiting behind a busy connection while another is free.
|
|
13
|
+
# A released connection returns to the tail of the free queue, so sequential
|
|
14
|
+
# commands still rotate in order.
|
|
10
15
|
#
|
|
11
16
|
# Two behaviors deliberately match the connection_pool gem it replaces: a
|
|
12
17
|
# nested `with` from the same thread re-enters the held connection, and after
|
|
13
18
|
# `fork` (Puma preload_app, unicorn, resque) the pool closes inherited sockets
|
|
14
|
-
# and rebuilds its
|
|
19
|
+
# and rebuilds its free queue in the child so parent and child never share a
|
|
15
20
|
# connection.
|
|
16
21
|
class RoundRobinPool
|
|
17
22
|
# Pools are tracked only to reset them in forked children. WeakMap so a
|
|
@@ -33,31 +38,30 @@ module Emb
|
|
|
33
38
|
|
|
34
39
|
@size = size
|
|
35
40
|
@connections = Array.new(size, &)
|
|
36
|
-
@
|
|
37
|
-
@next = 0
|
|
38
|
-
@index_mutex = Mutex.new
|
|
41
|
+
@free = free_queue
|
|
39
42
|
INSTANCES&.[]=(self, self)
|
|
40
43
|
end
|
|
41
44
|
|
|
42
|
-
# Yields
|
|
43
|
-
#
|
|
44
|
-
# nested `with` from the same
|
|
45
|
-
# already holds
|
|
45
|
+
# Yields an available connection. Safe from multiple threads: up to `size`
|
|
46
|
+
# commands run in parallel, each on its own connection; a command beyond that
|
|
47
|
+
# waits for the first connection to free. A nested `with` from the same
|
|
48
|
+
# thread re-enters the connection this pool already holds; other pools are
|
|
49
|
+
# unaffected.
|
|
46
50
|
def with(&)
|
|
47
51
|
held = Thread.current[THREAD_KEY]
|
|
48
52
|
if held&.key?(self)
|
|
49
53
|
yield @connections[held[self]]
|
|
50
54
|
else
|
|
51
|
-
|
|
55
|
+
acquire(held, &)
|
|
52
56
|
end
|
|
53
57
|
end
|
|
54
58
|
|
|
55
|
-
# Child side of after_fork(): drop inherited sockets and
|
|
59
|
+
# Child side of after_fork(): drop inherited sockets and rebuild the free
|
|
60
|
+
# queue, keeping the connection this thread still holds out of it (its
|
|
61
|
+
# holder re-adds it on release).
|
|
56
62
|
def reload_after_fork!
|
|
57
63
|
@connections.each { |conn| conn.close if conn.respond_to?(:close) }
|
|
58
|
-
@
|
|
59
|
-
@next = 0
|
|
60
|
-
@index_mutex = Mutex.new
|
|
64
|
+
@free = free_queue(exclude: Thread.current[THREAD_KEY]&.[](self))
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
if Process.respond_to?(:fork)
|
|
@@ -74,26 +78,30 @@ module Emb
|
|
|
74
78
|
|
|
75
79
|
private
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
|
+
def free_queue(exclude: nil)
|
|
82
|
+
free = Queue.new
|
|
83
|
+
@size.times { |idx| free << idx unless idx == exclude }
|
|
84
|
+
free
|
|
90
85
|
end
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
# Takes the next free connection, records it as held by this thread/pool,
|
|
88
|
+
# and returns it to the queue on exit — even when the block raises.
|
|
89
|
+
def acquire(held)
|
|
90
|
+
idx = nil
|
|
91
|
+
begin
|
|
92
|
+
idx = @free.pop
|
|
93
|
+
held ||= {}
|
|
94
|
+
Thread.current[THREAD_KEY] = held
|
|
95
|
+
held[self] = idx
|
|
96
|
+
yield @connections[idx]
|
|
97
|
+
ensure
|
|
98
|
+
# idx stays nil when no connection was taken; otherwise the taken index
|
|
99
|
+
# is returned even on async interrupt.
|
|
100
|
+
if idx
|
|
101
|
+
held&.delete(self)
|
|
102
|
+
Thread.current[THREAD_KEY] = nil if held&.empty?
|
|
103
|
+
@free << idx
|
|
104
|
+
end
|
|
97
105
|
end
|
|
98
106
|
end
|
|
99
107
|
end
|
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.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- elcuervo
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: batch-loader
|