skaidb 1.4.0 → 1.4.1
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/CHANGELOG.md +20 -0
- data/README.md +8 -5
- data/docs/pooling.md +12 -6
- data/lib/skaidb.rb +38 -16
- 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: 0514ffe373981266d475977fd65030cbf7bdc8a2ba568c77b80afe963904e242
|
|
4
|
+
data.tar.gz: a7a1ce25bf193955ae0f765df14245ab0cf1a4d6ff6902f712291666646b0061
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e60490e9a1e5ef0381199c96981bf18a19cfd915da2926ce6ac3395bb31cdb20be2d3c5135a1cc7b6172ab48dae15877d0479ea6aa86b1aa0da0219e062f70e9
|
|
7
|
+
data.tar.gz: 9a8a3422a05a1eb3294bd67425a6d59291924254d6b9acac4002923d14867b3f30205a21446b254c2957a51090a76508deb69df6abd1058b5bdef96383402d95
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to the skaidb Ruby driver. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.4.1] - 2026-10-31
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- One slow round trip at the start can no longer get a healthy member
|
|
11
|
+
ejected for latency. A member's first round trip set its average
|
|
12
|
+
outright, so a statement that stalled in the client — a CPU-bound
|
|
13
|
+
application holding the thread while it starts — put seconds into it;
|
|
14
|
+
ten round trips later the member was compared with the others, and the
|
|
15
|
+
average had barely decayed: ejected (`latency 4153.7 ms vs median
|
|
16
|
+
0.2 ms`) while it answered in milliseconds. The fastest of a member's
|
|
17
|
+
first three round trips now seeds its average (a round trip is only ever
|
|
18
|
+
lengthened), and later ones move it as before; until then the member
|
|
19
|
+
counts as not measured, and `pool.members` reports its `latency_ms` as
|
|
20
|
+
`nil`.
|
|
21
|
+
- A latency ejection's `logger` warning names the slowest of the member's
|
|
22
|
+
last 10 round trips alongside its average and the median, which tells a
|
|
23
|
+
slow member from one slow statement.
|
|
24
|
+
|
|
7
25
|
## [1.4.0] - 2026-10-29
|
|
8
26
|
|
|
9
27
|
### Fixed
|
|
@@ -206,6 +224,8 @@ batches, a 2 500-row stream, error handling, and the `drivers` table showing
|
|
|
206
224
|
because it is a bundled gem since Ruby 3.4.
|
|
207
225
|
- The example moved to `examples/basic.rb`.
|
|
208
226
|
|
|
227
|
+
[1.4.1]: https://github.com/porcupin26/skaidb-ruby/compare/v1.4.0...v1.4.1
|
|
228
|
+
[1.4.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.3.0...v1.4.0
|
|
209
229
|
[1.3.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.2.0...v1.3.0
|
|
210
230
|
[1.2.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.1.0...v1.2.0
|
|
211
231
|
[1.1.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.0.3...v1.1.0
|
data/README.md
CHANGED
|
@@ -55,13 +55,13 @@ Xcode command-line tools on macOS). `gem install skaidb` and vendoring reuse
|
|
|
55
55
|
the `bigdecimal` already installed.
|
|
56
56
|
|
|
57
57
|
Or vendor the single file: copy
|
|
58
|
-
[`lib/skaidb.rb`](https://github.com/porcupin26/skaidb-ruby/blob/v1.4.
|
|
58
|
+
[`lib/skaidb.rb`](https://github.com/porcupin26/skaidb-ruby/blob/v1.4.1/lib/skaidb.rb)
|
|
59
59
|
into your project and `require_relative` it. It has no dependencies beyond
|
|
60
60
|
Ruby's standard library.
|
|
61
61
|
|
|
62
62
|
Every release is also attached as a `.gem` to its
|
|
63
63
|
[GitHub release](https://github.com/porcupin26/skaidb-ruby/releases) for
|
|
64
|
-
installs without registry access: `gem install ./skaidb-1.4.
|
|
64
|
+
installs without registry access: `gem install ./skaidb-1.4.1.gem`.
|
|
65
65
|
|
|
66
66
|
## Quick start
|
|
67
67
|
|
|
@@ -359,8 +359,11 @@ pool.members # => [{ endpoint: "db1:7000", canonical: "10.0.0.1:7000", state: :r
|
|
|
359
359
|
|
|
360
360
|
A member with consecutive transport failures, or a round-trip average far
|
|
361
361
|
above the others', is ejected and re-admitted by a background probe (the last
|
|
362
|
-
usable member never is)
|
|
363
|
-
|
|
362
|
+
usable member never is). The fastest of a member's first three round trips
|
|
363
|
+
seeds its average, so a statement that stalls in your own process cannot get
|
|
364
|
+
a healthy member ejected; a latency ejection's `logger:` warning names the
|
|
365
|
+
average, the median and the slowest of the member's last 10 round trips.
|
|
366
|
+
`idle_order: :fifo` keeps every idle connection in use. Details: [docs/pooling.md](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/pooling.md).
|
|
364
367
|
|
|
365
368
|
## Streams (`CREATE STREAM`)
|
|
366
369
|
|
|
@@ -461,7 +464,7 @@ frame after every handshake. It shows up as `client_name = 'ruby'` /
|
|
|
461
464
|
|
|
462
465
|
```sql
|
|
463
466
|
SELECT client_name, client_version FROM drivers;
|
|
464
|
-
-- ruby | 1.4.
|
|
467
|
+
-- ruby | 1.4.1
|
|
465
468
|
```
|
|
466
469
|
|
|
467
470
|
The server records the row asynchronously, so a `SELECT` immediately after
|
data/docs/pooling.md
CHANGED
|
@@ -81,7 +81,7 @@ pool = Skaidb::Pool.new(
|
|
|
81
81
|
| `idle_order` | `:lifo` | `:lifo` hands out the most recently returned connection first, so a quiet pool keeps few sockets warm. `:fifo` hands out the longest-idle one first, so every idle connection keeps being used and a server-side idle timeout never finds one. |
|
|
82
82
|
| `discover` | `false` | Reads the members with `SHOW ENDPOINTS` (server ≥ 0.327; no privilege needed) when a member is first reached and every `refresh_interval` (60 s) after — every 5 s while a member is `resyncing` or `unreachable`. New members are added, members that left are dropped (seeds are kept, marked `unlisted`), and members the server reports `resyncing` or `unreachable` get no new connections until they are `ready` again, when they start with a fresh latency average. An older or standalone server leaves the seeds as they are, and the pool stops asking. |
|
|
83
83
|
| `eject_errors` | `3` | Consecutive transport failures (a failed dial, an I/O error mid-request, a capacity refusal) that eject a member. Statement errors and refused logins do not count. |
|
|
84
|
-
| `eject_latency_factor`, `eject_latency_floor` | `3.0`, `0.05` | A member whose round-trip average is above factor × the other usable members' median **and** more than floor seconds above it is ejected (once it and another member have 10 samples each). A factor of `0` disables it. |
|
|
84
|
+
| `eject_latency_factor`, `eject_latency_floor` | `3.0`, `0.05` | A member whose round-trip average is above factor × the other usable members' median **and** more than floor seconds above it is ejected (once it and another member have 10 samples each). The fastest of a member's first three round trips seeds its average, so a statement that stalls in your own process — a CPU-bound start-up — cannot get a healthy member ejected; the `logger` warning names the average, the median and the slowest of the member's last 10 round trips. A factor of `0` disables it. |
|
|
85
85
|
| `eject_backoff`, `eject_backoff_max` | `5.0`, `60.0` | How long an ejection lasts, doubling for each repeat up to the max; a member that stays healthy for `eject_backoff_max` after a re-admission starts over at the base. When the time is up a background thread dials the member and runs one statement: success re-admits it (with a fresh latency average), failure ejects it again. The last usable member is never ejected — a slow or flaky member beats none. |
|
|
86
86
|
| `read_your_writes` | `0.0` | Seconds after a write during which checkouts with the same `affinity` go to the member that took the write. |
|
|
87
87
|
| `logger` | `nil` | A `Logger` (or anything with `warn`) told of each ejection and re-admission. |
|
|
@@ -94,10 +94,15 @@ through to the other usable members, then to the unusable ones, so a
|
|
|
94
94
|
checkout never has nowhere to go while any member is configured.
|
|
95
95
|
|
|
96
96
|
**Latency.** Per member the pool keeps an exponentially weighted average of
|
|
97
|
-
round trips (request sent to the first response frame)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
round trips (request sent to the first response frame). The fastest of the
|
|
98
|
+
member's first three round trips seeds it: a round trip is only ever
|
|
99
|
+
lengthened — by the member, the network or your own process (a thread that
|
|
100
|
+
stalls while the application starts, say) — so one or two slow first
|
|
101
|
+
statements cannot set it. From then on each sample moves it 5 % of the way,
|
|
102
|
+
and no sample counts for more than 10 × the larger of the current average
|
|
103
|
+
and `eject_latency_floor`. One slow statement barely moves it; a member that
|
|
104
|
+
stays slow is noticed within a few dozen statements. Until its average is
|
|
105
|
+
seeded a member counts as not measured.
|
|
101
106
|
|
|
102
107
|
**Read-your-writes.** The member that coordinated a write reads it back at
|
|
103
108
|
any consistency, `ONE` included; another member may not have applied it yet.
|
|
@@ -139,7 +144,8 @@ endpoint:
|
|
|
139
144
|
`endpoint` is what the pool dials, `canonical` the name the member reports
|
|
140
145
|
for itself (once discovered), `state` one of `:ready`, `:ejected`,
|
|
141
146
|
`:resyncing`, `:unreachable`, `:unlisted`, and `latency_ms` is `nil` until
|
|
142
|
-
the member has been measured
|
|
147
|
+
the member has been measured (its first three round trips). The rules are
|
|
148
|
+
the same in every skaidb driver
|
|
143
149
|
([PROTOCOL.md §6.1](https://skaidb.org/docs/PROTOCOL.html#61-balancing-a-pool-across-members)).
|
|
144
150
|
|
|
145
151
|
## Behaviour
|
data/lib/skaidb.rb
CHANGED
|
@@ -35,7 +35,7 @@ require "bigdecimal"
|
|
|
35
35
|
module Skaidb
|
|
36
36
|
# The package version — the single source of truth. The gemspec reads it,
|
|
37
37
|
# and it is what the driver reports to the server in the Hello frame.
|
|
38
|
-
VERSION = "1.4.
|
|
38
|
+
VERSION = "1.4.1"
|
|
39
39
|
|
|
40
40
|
# Base class for every error raised by this driver.
|
|
41
41
|
class Error < StandardError; end
|
|
@@ -1568,13 +1568,18 @@ module Skaidb
|
|
|
1568
1568
|
class Pool
|
|
1569
1569
|
POLICIES = %i[random round_robin least_latency].freeze
|
|
1570
1570
|
IDLE_ORDERS = %i[lifo fifo].freeze
|
|
1571
|
-
# Latency EWMA:
|
|
1572
|
-
#
|
|
1573
|
-
#
|
|
1574
|
-
#
|
|
1575
|
-
#
|
|
1571
|
+
# Latency EWMA: the fastest of a member's first round trips seeds it (a
|
|
1572
|
+
# round trip is only ever lengthened — by the member, the network or this
|
|
1573
|
+
# process stalling — so a slow first statement cannot set it); then each
|
|
1574
|
+
# round trip moves it 5% toward it, and no sample counts for more than
|
|
1575
|
+
# 10x the larger of the current average and the ejection floor — one slow
|
|
1576
|
+
# query does not eject a healthy member, a member that stays slow is
|
|
1577
|
+
# ejected within a few dozen statements.
|
|
1578
|
+
EWMA_SEED_SAMPLES = 3
|
|
1576
1579
|
EWMA_WEIGHT = 0.05
|
|
1577
1580
|
EWMA_CLAMP = 10.0
|
|
1581
|
+
# Round trips a latency ejection's warning reports the slowest of.
|
|
1582
|
+
RECENT_SAMPLES = 10
|
|
1578
1583
|
# Round trips a member (and at least one other) needs before its average
|
|
1579
1584
|
# can eject it.
|
|
1580
1585
|
EWMA_MIN_SAMPLES = 10
|
|
@@ -1591,12 +1596,13 @@ module Skaidb
|
|
|
1591
1596
|
MAINTAIN_TICK = 0.25
|
|
1592
1597
|
# Read-your-writes keys kept before the expired ones are swept.
|
|
1593
1598
|
AFFINITY_SWEEP = 4096
|
|
1594
|
-
private_constant :
|
|
1595
|
-
:REFRESH_NOT_READY, :MAINTAIN_TICK,
|
|
1599
|
+
private_constant :EWMA_SEED_SAMPLES, :EWMA_WEIGHT, :EWMA_CLAMP, :RECENT_SAMPLES,
|
|
1600
|
+
:EWMA_MIN_SAMPLES, :STALE_SAMPLE, :REFRESH_NOT_READY, :MAINTAIN_TICK,
|
|
1601
|
+
:AFFINITY_SWEEP
|
|
1596
1602
|
|
|
1597
1603
|
# One member as the pool sees it.
|
|
1598
1604
|
class Member
|
|
1599
|
-
attr_reader :endpoint, :host, :port, :seed
|
|
1605
|
+
attr_reader :endpoint, :host, :port, :seed, :firsts, :recent
|
|
1600
1606
|
attr_accessor :canonical, :listed, :ewma, :samples, :last_sample, :errors,
|
|
1601
1607
|
:ejected_until, :ejections, :readmitted_at, :in_use, :probing
|
|
1602
1608
|
|
|
@@ -1608,6 +1614,8 @@ module Skaidb
|
|
|
1608
1614
|
@canonical = nil # its own SHOW ENDPOINTS name
|
|
1609
1615
|
@listed = :ready # :ready, :resyncing, :unreachable, :unlisted
|
|
1610
1616
|
@ewma = nil # seconds
|
|
1617
|
+
@firsts = [] # round trips before the average is seeded
|
|
1618
|
+
@recent = [] # the last RECENT_SAMPLES round trips
|
|
1611
1619
|
@samples = 0
|
|
1612
1620
|
@last_sample = 0.0
|
|
1613
1621
|
@errors = 0 # consecutive transport failures
|
|
@@ -1617,6 +1625,14 @@ module Skaidb
|
|
|
1617
1625
|
@in_use = 0
|
|
1618
1626
|
@probing = false
|
|
1619
1627
|
end
|
|
1628
|
+
|
|
1629
|
+
# Measure the member afresh (a re-admission, back to ready).
|
|
1630
|
+
def reset_latency
|
|
1631
|
+
@ewma = nil
|
|
1632
|
+
@firsts.clear
|
|
1633
|
+
@recent.clear
|
|
1634
|
+
@samples = 0
|
|
1635
|
+
end
|
|
1620
1636
|
end
|
|
1621
1637
|
private_constant :Member
|
|
1622
1638
|
|
|
@@ -1787,15 +1803,21 @@ module Skaidb
|
|
|
1787
1803
|
next if m.nil?
|
|
1788
1804
|
|
|
1789
1805
|
m.errors = 0
|
|
1806
|
+
m.recent.push(elapsed)
|
|
1807
|
+
m.recent.shift if m.recent.length > RECENT_SAMPLES
|
|
1790
1808
|
if m.ewma.nil?
|
|
1791
|
-
m.
|
|
1809
|
+
m.firsts.push(elapsed)
|
|
1810
|
+
if m.firsts.length >= EWMA_SEED_SAMPLES
|
|
1811
|
+
m.ewma = m.firsts.min
|
|
1812
|
+
m.firsts.clear
|
|
1813
|
+
end
|
|
1792
1814
|
else
|
|
1793
1815
|
cap = EWMA_CLAMP * [m.ewma, @eject_floor].max
|
|
1794
1816
|
m.ewma += EWMA_WEIGHT * ([elapsed, cap].min - m.ewma)
|
|
1795
1817
|
end
|
|
1796
1818
|
m.samples += 1
|
|
1797
1819
|
m.last_sample = now
|
|
1798
|
-
next unless m.samples >= EWMA_MIN_SAMPLES && @eject_factor.positive? && m.ejected_until.nil?
|
|
1820
|
+
next unless m.ewma && m.samples >= EWMA_MIN_SAMPLES && @eject_factor.positive? && m.ejected_until.nil?
|
|
1799
1821
|
|
|
1800
1822
|
others = @members.each_value.select do |o|
|
|
1801
1823
|
!o.equal?(m) && usable?(o) && o.ewma && o.samples >= EWMA_MIN_SAMPLES
|
|
@@ -1804,7 +1826,9 @@ module Skaidb
|
|
|
1804
1826
|
|
|
1805
1827
|
median = others[(others.length - 1) / 2]
|
|
1806
1828
|
if m.ewma > @eject_factor * median && m.ewma - median > @eject_floor
|
|
1807
|
-
eject(m, format("latency %.1f ms vs median %.1f ms"
|
|
1829
|
+
eject(m, format("latency %.1f ms vs median %.1f ms; " \
|
|
1830
|
+
"slowest of the last %d round trips %.1f ms",
|
|
1831
|
+
m.ewma * 1000, median * 1000, m.recent.length, m.recent.max * 1000))
|
|
1808
1832
|
end
|
|
1809
1833
|
end
|
|
1810
1834
|
end
|
|
@@ -2009,8 +2033,7 @@ module Skaidb
|
|
|
2009
2033
|
if ok && !@closed
|
|
2010
2034
|
m.ejected_until = nil
|
|
2011
2035
|
m.errors = 0
|
|
2012
|
-
m.
|
|
2013
|
-
m.samples = 0
|
|
2036
|
+
m.reset_latency
|
|
2014
2037
|
m.readmitted_at = now
|
|
2015
2038
|
log("re-admitted #{m.endpoint}")
|
|
2016
2039
|
if @idle.length < @maxsize
|
|
@@ -2112,8 +2135,7 @@ module Skaidb
|
|
|
2112
2135
|
state = %w[ready resyncing unreachable].include?(state) ? state.to_sym : :ready
|
|
2113
2136
|
if state == :ready && m.listed != :ready
|
|
2114
2137
|
# Back in rotation: measured afresh, like a re-admission.
|
|
2115
|
-
m.
|
|
2116
|
-
m.samples = 0
|
|
2138
|
+
m.reset_latency
|
|
2117
2139
|
m.errors = 0
|
|
2118
2140
|
end
|
|
2119
2141
|
m.listed = state
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skaidb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- skaidb contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|