skaidb 1.3.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 +34 -0
- data/README.md +12 -6
- data/docs/api.md +4 -1
- data/docs/pooling.md +12 -6
- data/docs/streaming.md +6 -0
- data/lib/skaidb.rb +82 -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: 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,38 @@ 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
|
+
|
|
25
|
+
## [1.4.0] - 2026-10-29
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- A streamed `CALL` whose procedure body `EMIT`s result sets returns them.
|
|
29
|
+
The server answers such a call with every set in one `ResultSets` frame
|
|
30
|
+
(tag 8) even to a stream request, and `conn.stream`, with or without
|
|
31
|
+
parameters, raised `QueryError` (`unexpected response tag 8 to stream
|
|
32
|
+
request`) and lost the sets. The frame is now decoded by the code `exec`
|
|
33
|
+
uses: the block receives the rows `exec`'s `Result` would yield (the
|
|
34
|
+
call's final result, the LAST set) and `stream` returns that same `:rows`
|
|
35
|
+
`Result`, every set in order under `result_sets`. Nothing is left in
|
|
36
|
+
flight, so the connection stays usable — the block may even run
|
|
37
|
+
statements on it.
|
|
38
|
+
|
|
7
39
|
## [1.3.0] - 2026-10-29
|
|
8
40
|
|
|
9
41
|
### Added
|
|
@@ -192,6 +224,8 @@ batches, a 2 500-row stream, error handling, and the `drivers` table showing
|
|
|
192
224
|
because it is a bundled gem since Ruby 3.4.
|
|
193
225
|
- The example moved to `examples/basic.rb`.
|
|
194
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
|
|
195
229
|
[1.3.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.2.0...v1.3.0
|
|
196
230
|
[1.2.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.1.0...v1.2.0
|
|
197
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.
|
|
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.
|
|
64
|
+
installs without registry access: `gem install ./skaidb-1.4.1.gem`.
|
|
65
65
|
|
|
66
66
|
## Quick start
|
|
67
67
|
|
|
@@ -306,7 +306,10 @@ unordered or in storage-key order), and no scan budget applies to a streamed
|
|
|
306
306
|
page (server ≥ 0.330). With a block `stream` returns a `Result` whose `kind`
|
|
307
307
|
is `:stream` and whose `fields` are the column names (the rows went to the
|
|
308
308
|
block); a non-row statement returns its `:mutation` / `:ddl` Result without
|
|
309
|
-
yielding.
|
|
309
|
+
yielding. A `CALL` whose body `EMIT`s result sets is answered with all of
|
|
310
|
+
them in one frame even when streamed: the block receives the rows `exec`
|
|
311
|
+
would return (the last set), and `stream` returns that same `Result`, every
|
|
312
|
+
set under `result_sets`.
|
|
310
313
|
|
|
311
314
|
**The abandon/drain rule.** The connection is busy for the whole stream:
|
|
312
315
|
a statement from another thread waits for it. Leaving the block early —
|
|
@@ -356,8 +359,11 @@ pool.members # => [{ endpoint: "db1:7000", canonical: "10.0.0.1:7000", state: :r
|
|
|
356
359
|
|
|
357
360
|
A member with consecutive transport failures, or a round-trip average far
|
|
358
361
|
above the others', is ejected and re-admitted by a background probe (the last
|
|
359
|
-
usable member never is)
|
|
360
|
-
|
|
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).
|
|
361
367
|
|
|
362
368
|
## Streams (`CREATE STREAM`)
|
|
363
369
|
|
|
@@ -458,7 +464,7 @@ frame after every handshake. It shows up as `client_name = 'ruby'` /
|
|
|
458
464
|
|
|
459
465
|
```sql
|
|
460
466
|
SELECT client_name, client_version FROM drivers;
|
|
461
|
-
-- ruby | 1.
|
|
467
|
+
-- ruby | 1.4.1
|
|
462
468
|
```
|
|
463
469
|
|
|
464
470
|
The server records the row asynchronously, so a `SELECT` immediately after
|
data/docs/api.md
CHANGED
|
@@ -67,7 +67,10 @@ Raises `Skaidb::Error` before dialing for an unknown `auth_mechanism`, a
|
|
|
67
67
|
`ArgumentError`. For a row stream the returned Result has
|
|
68
68
|
`kind == :stream`, the column names in `fields` and no `rows` (they went to
|
|
69
69
|
the block); a statement without rows returns its `:mutation` / `:ddl`
|
|
70
|
-
Result without yielding
|
|
70
|
+
Result without yielding; a `CALL` whose body `EMIT`s result sets (answered
|
|
71
|
+
in one frame even when streamed) yields the rows `exec` would return (the
|
|
72
|
+
last set) and returns that same `:rows` Result, every set under
|
|
73
|
+
`result_sets`. Without a block returns an Enumerator.
|
|
71
74
|
- `subscribe(stream_name, after: nil, poll: 0.5) { |event| … }` — yields a
|
|
72
75
|
`CREATE STREAM` log's events forever, each a Hash with `"id"`, `"op"`,
|
|
73
76
|
`"k"`, `"ts"`, `"doc"`. Polls `_stream_<name>` 500 rows at a time with a
|
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/docs/streaming.md
CHANGED
|
@@ -28,6 +28,12 @@ end
|
|
|
28
28
|
- A non-row statement (an `INSERT`, DDL, `USE`) answers with its ordinary
|
|
29
29
|
result over the same opcode; `stream` returns that Result (`kind`
|
|
30
30
|
`:mutation` with `cmd_tuples`, or `:ddl`) without yielding.
|
|
31
|
+
- A `CALL` whose procedure body `EMIT`s result sets answers with all of them
|
|
32
|
+
in one frame, as it does to `exec`: the block receives the rows `exec`
|
|
33
|
+
would return (the call's final result, the last set) and `stream` returns
|
|
34
|
+
that same `:rows` Result, every set in order under `result_sets`. Nothing
|
|
35
|
+
is left in flight, so the connection is free again before the first row
|
|
36
|
+
reaches the block.
|
|
31
37
|
- A server without the streaming opcode raises `Skaidb::QueryError`
|
|
32
38
|
(`server does not support streaming: …`).
|
|
33
39
|
|
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.
|
|
38
|
+
VERSION = "1.4.1"
|
|
39
39
|
|
|
40
40
|
# Base class for every error raised by this driver.
|
|
41
41
|
class Error < StandardError; end
|
|
@@ -973,7 +973,12 @@ module Skaidb
|
|
|
973
973
|
# +kind+ is +:stream+ and +fields+ holds the column names (known even when
|
|
974
974
|
# no row arrived) while +rows+ stays empty — the rows went to the block;
|
|
975
975
|
# a statement that produces no rows returns its ordinary +:mutation+ or
|
|
976
|
-
# +:ddl+ Result without yielding.
|
|
976
|
+
# +:ddl+ Result without yielding. A CALL whose body EMITs result sets is
|
|
977
|
+
# answered with all of them in one frame, streamed or not: the block
|
|
978
|
+
# receives the rows exec's Result would yield (the call's final result,
|
|
979
|
+
# the LAST set) and the return value is that same +:rows+ Result, every
|
|
980
|
+
# set under +result_sets+ — with nothing left in flight, so the block may
|
|
981
|
+
# use the connection.
|
|
977
982
|
def stream(sql, params = [], consistency: nil)
|
|
978
983
|
return enum_for(:stream, sql, params, consistency: consistency) unless block_given?
|
|
979
984
|
raise ConnectionError, "connection is closed" if @closed
|
|
@@ -991,6 +996,7 @@ module Skaidb
|
|
|
991
996
|
|
|
992
997
|
live = false
|
|
993
998
|
summary = nil
|
|
999
|
+
sets = nil
|
|
994
1000
|
@mutex.synchronize do
|
|
995
1001
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
996
1002
|
begin
|
|
@@ -1015,6 +1021,12 @@ module Skaidb
|
|
|
1015
1021
|
return Result.new(fields: [], rows: [], cmd_tuples: r.u64, kind: :mutation) # not row-producing
|
|
1016
1022
|
when 2
|
|
1017
1023
|
return Result.new(fields: [], rows: [], cmd_tuples: 0, kind: :ddl)
|
|
1024
|
+
when 8
|
|
1025
|
+
# ResultSets: a CALL whose body EMITted is answered with every set
|
|
1026
|
+
# in ONE frame, streamed or not — the frame exec receives, decoded
|
|
1027
|
+
# by the same code. Nothing follows it, so the rows are yielded
|
|
1028
|
+
# below, with the connection free.
|
|
1029
|
+
sets = parse_result_sets(r)
|
|
1018
1030
|
when 5
|
|
1019
1031
|
# The server has already committed to a row stream, so from here
|
|
1020
1032
|
# the socket carries frames this call owns — mark it BEFORE
|
|
@@ -1065,6 +1077,12 @@ module Skaidb
|
|
|
1065
1077
|
raise QueryError, "unexpected response tag #{tag} to stream request"
|
|
1066
1078
|
end
|
|
1067
1079
|
end
|
|
1080
|
+
if sets
|
|
1081
|
+
# What exec's Result yields (the call's final result, the LAST set),
|
|
1082
|
+
# and that same Result back, every set under +result_sets+.
|
|
1083
|
+
sets.each { |row| yield row }
|
|
1084
|
+
return sets
|
|
1085
|
+
end
|
|
1068
1086
|
summary
|
|
1069
1087
|
end
|
|
1070
1088
|
|
|
@@ -1388,27 +1406,9 @@ module Skaidb
|
|
|
1388
1406
|
tag = r.u8
|
|
1389
1407
|
case tag
|
|
1390
1408
|
when 0 # Rows
|
|
1391
|
-
|
|
1392
|
-
columns = Array.new(ncols) { r.text }
|
|
1393
|
-
nrows = r.u32
|
|
1394
|
-
rows = Array.new(nrows) do
|
|
1395
|
-
ncells = r.u32
|
|
1396
|
-
Array.new(ncells) { Skaidb.decode_value(Reader.new(r.blob)) }
|
|
1397
|
-
end
|
|
1398
|
-
Result.new(fields: columns, rows: rows, cmd_tuples: 0)
|
|
1409
|
+
parse_rows(r)
|
|
1399
1410
|
when 8 # ResultSets: a CALL whose body EMITted
|
|
1400
|
-
|
|
1401
|
-
ncols = r.u32
|
|
1402
|
-
columns = Array.new(ncols) { r.text }
|
|
1403
|
-
nrows = r.u32
|
|
1404
|
-
rows = Array.new(nrows) do
|
|
1405
|
-
ncells = r.u32
|
|
1406
|
-
Array.new(ncells) { Skaidb.decode_value(Reader.new(r.blob)) }
|
|
1407
|
-
end
|
|
1408
|
-
Result.new(fields: columns, rows: rows, cmd_tuples: 0)
|
|
1409
|
-
end
|
|
1410
|
-
last = sets.last || Result.new(fields: [], rows: [], cmd_tuples: 0)
|
|
1411
|
-
Result.new(fields: last.fields, rows: last.rows, cmd_tuples: 0, result_sets: sets)
|
|
1411
|
+
parse_result_sets(r)
|
|
1412
1412
|
when 1 # Mutation
|
|
1413
1413
|
Result.new(fields: [], rows: [], cmd_tuples: r.u64, kind: :mutation)
|
|
1414
1414
|
when 2 # Ddl
|
|
@@ -1419,6 +1419,29 @@ module Skaidb
|
|
|
1419
1419
|
raise ConnectionError, "unknown response tag #{tag}"
|
|
1420
1420
|
end
|
|
1421
1421
|
end
|
|
1422
|
+
|
|
1423
|
+
# A Rows body (PROTOCOL.md §3.2), also one set of ResultSets; +r+ sits
|
|
1424
|
+
# just past the tag.
|
|
1425
|
+
def parse_rows(r)
|
|
1426
|
+
ncols = r.u32
|
|
1427
|
+
columns = Array.new(ncols) { r.text }
|
|
1428
|
+
nrows = r.u32
|
|
1429
|
+
rows = Array.new(nrows) do
|
|
1430
|
+
ncells = r.u32
|
|
1431
|
+
Array.new(ncells) { Skaidb.decode_value(Reader.new(r.blob)) }
|
|
1432
|
+
end
|
|
1433
|
+
Result.new(fields: columns, rows: rows, cmd_tuples: 0)
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
# A ResultSets body (a CALL whose body EMITted), +r+ just past the tag:
|
|
1437
|
+
# the LAST set is the Result's own rows, every set is under
|
|
1438
|
+
# +result_sets+. The buffered path and a stream request both receive
|
|
1439
|
+
# this frame, and both decode it here.
|
|
1440
|
+
def parse_result_sets(r)
|
|
1441
|
+
sets = Array.new(r.u32) { parse_rows(r) }
|
|
1442
|
+
last = sets.last || Result.new(fields: [], rows: [], cmd_tuples: 0)
|
|
1443
|
+
Result.new(fields: last.fields, rows: last.rows, cmd_tuples: 0, result_sets: sets)
|
|
1444
|
+
end
|
|
1422
1445
|
end
|
|
1423
1446
|
|
|
1424
1447
|
# ---- module entry point --------------------------------------------------
|
|
@@ -1545,13 +1568,18 @@ module Skaidb
|
|
|
1545
1568
|
class Pool
|
|
1546
1569
|
POLICIES = %i[random round_robin least_latency].freeze
|
|
1547
1570
|
IDLE_ORDERS = %i[lifo fifo].freeze
|
|
1548
|
-
# Latency EWMA:
|
|
1549
|
-
#
|
|
1550
|
-
#
|
|
1551
|
-
#
|
|
1552
|
-
#
|
|
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
|
|
1553
1579
|
EWMA_WEIGHT = 0.05
|
|
1554
1580
|
EWMA_CLAMP = 10.0
|
|
1581
|
+
# Round trips a latency ejection's warning reports the slowest of.
|
|
1582
|
+
RECENT_SAMPLES = 10
|
|
1555
1583
|
# Round trips a member (and at least one other) needs before its average
|
|
1556
1584
|
# can eject it.
|
|
1557
1585
|
EWMA_MIN_SAMPLES = 10
|
|
@@ -1568,12 +1596,13 @@ module Skaidb
|
|
|
1568
1596
|
MAINTAIN_TICK = 0.25
|
|
1569
1597
|
# Read-your-writes keys kept before the expired ones are swept.
|
|
1570
1598
|
AFFINITY_SWEEP = 4096
|
|
1571
|
-
private_constant :
|
|
1572
|
-
: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
|
|
1573
1602
|
|
|
1574
1603
|
# One member as the pool sees it.
|
|
1575
1604
|
class Member
|
|
1576
|
-
attr_reader :endpoint, :host, :port, :seed
|
|
1605
|
+
attr_reader :endpoint, :host, :port, :seed, :firsts, :recent
|
|
1577
1606
|
attr_accessor :canonical, :listed, :ewma, :samples, :last_sample, :errors,
|
|
1578
1607
|
:ejected_until, :ejections, :readmitted_at, :in_use, :probing
|
|
1579
1608
|
|
|
@@ -1585,6 +1614,8 @@ module Skaidb
|
|
|
1585
1614
|
@canonical = nil # its own SHOW ENDPOINTS name
|
|
1586
1615
|
@listed = :ready # :ready, :resyncing, :unreachable, :unlisted
|
|
1587
1616
|
@ewma = nil # seconds
|
|
1617
|
+
@firsts = [] # round trips before the average is seeded
|
|
1618
|
+
@recent = [] # the last RECENT_SAMPLES round trips
|
|
1588
1619
|
@samples = 0
|
|
1589
1620
|
@last_sample = 0.0
|
|
1590
1621
|
@errors = 0 # consecutive transport failures
|
|
@@ -1594,6 +1625,14 @@ module Skaidb
|
|
|
1594
1625
|
@in_use = 0
|
|
1595
1626
|
@probing = false
|
|
1596
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
|
|
1597
1636
|
end
|
|
1598
1637
|
private_constant :Member
|
|
1599
1638
|
|
|
@@ -1764,15 +1803,21 @@ module Skaidb
|
|
|
1764
1803
|
next if m.nil?
|
|
1765
1804
|
|
|
1766
1805
|
m.errors = 0
|
|
1806
|
+
m.recent.push(elapsed)
|
|
1807
|
+
m.recent.shift if m.recent.length > RECENT_SAMPLES
|
|
1767
1808
|
if m.ewma.nil?
|
|
1768
|
-
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
|
|
1769
1814
|
else
|
|
1770
1815
|
cap = EWMA_CLAMP * [m.ewma, @eject_floor].max
|
|
1771
1816
|
m.ewma += EWMA_WEIGHT * ([elapsed, cap].min - m.ewma)
|
|
1772
1817
|
end
|
|
1773
1818
|
m.samples += 1
|
|
1774
1819
|
m.last_sample = now
|
|
1775
|
-
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?
|
|
1776
1821
|
|
|
1777
1822
|
others = @members.each_value.select do |o|
|
|
1778
1823
|
!o.equal?(m) && usable?(o) && o.ewma && o.samples >= EWMA_MIN_SAMPLES
|
|
@@ -1781,7 +1826,9 @@ module Skaidb
|
|
|
1781
1826
|
|
|
1782
1827
|
median = others[(others.length - 1) / 2]
|
|
1783
1828
|
if m.ewma > @eject_factor * median && m.ewma - median > @eject_floor
|
|
1784
|
-
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))
|
|
1785
1832
|
end
|
|
1786
1833
|
end
|
|
1787
1834
|
end
|
|
@@ -1986,8 +2033,7 @@ module Skaidb
|
|
|
1986
2033
|
if ok && !@closed
|
|
1987
2034
|
m.ejected_until = nil
|
|
1988
2035
|
m.errors = 0
|
|
1989
|
-
m.
|
|
1990
|
-
m.samples = 0
|
|
2036
|
+
m.reset_latency
|
|
1991
2037
|
m.readmitted_at = now
|
|
1992
2038
|
log("re-admitted #{m.endpoint}")
|
|
1993
2039
|
if @idle.length < @maxsize
|
|
@@ -2089,8 +2135,7 @@ module Skaidb
|
|
|
2089
2135
|
state = %w[ready resyncing unreachable].include?(state) ? state.to_sym : :ready
|
|
2090
2136
|
if state == :ready && m.listed != :ready
|
|
2091
2137
|
# Back in rotation: measured afresh, like a re-admission.
|
|
2092
|
-
m.
|
|
2093
|
-
m.samples = 0
|
|
2138
|
+
m.reset_latency
|
|
2094
2139
|
m.errors = 0
|
|
2095
2140
|
end
|
|
2096
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
|
+
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
|