quonfig 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 +6 -0
- data/README.md +7 -0
- data/lib/quonfig/client.rb +12 -0
- data/lib/quonfig/config_loader.rb +23 -5
- data/lib/quonfig/telemetry/failover_aggregator.rb +4 -2
- data/lib/quonfig/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ecc1b6f2a6c327c36d7da4cf091623b8c44b81d6949b887356301961229a684
|
|
4
|
+
data.tar.gz: bf357130f83dc29baf032c8dd0db652c8910797aac2fc6a499ec3461e5b56ff6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08951e8e2261dce91f0308de7920ea46eef5077979efdd0bd8df55b403719d8fb1f2e7f6ed863a12d38108b30cdef8b92d08832059a68c45b96425a5ced249f6'
|
|
7
|
+
data.tar.gz: 81738455b310e66693801fe2b2aa3ff027e676b28b032398271d1de17c9421212d5e0c9d3b20c37164a7299c74f2347a52f371abfabc36d8ebca3c75492d17d0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.1 - 2026-09-11
|
|
4
|
+
|
|
5
|
+
- **Fix (fork/telemetry): a forked child now reports under its OWN `instanceHash` (qfg-xcym).** `@instance_hash` was minted once in `Client#initialize` and survived `fork(2)`, so every forked child POSTed its telemetry under the **parent's** hash. The Quonfig Debugger groups SDK last-seen by that hash, so an 8-worker Puma cluster (or a `parallel`-gem batch) collapsed into a single instance row with the parent's and the children's windows interleaved on top of each other. A child now mints a fresh hash as part of the same post-fork rebuild that gives it fresh aggregators and a fresh reporter — before the reporter is constructed, so the hash the child POSTs under is its own. This is parity with Reforge, where a forked process simply builds a whole new client. The parent's hash is never touched, and a child's row count is unchanged (it already reported its own window; it just did so under the wrong identity). Present since 0.0.16.
|
|
6
|
+
- **Fix (telemetry): only a STRICTLY older payload counts as `guardRejected` (qfg-rr5b).** The reject-older install guard treated an **equal**-generation payload as "older" and counted it, but two server behaviors re-deliver the envelope a client already holds in perfectly normal steady state: api-delivery's SSE `sendInitialConfig` re-sends the current envelope on every connect regardless of the `Last-Event-Id` the client sends, and a config poll against an empty per-leg ETag slot — a fresh transport, a reconnect, the fallback poller's engage fetch — answers with a full 200 at the same generation. A healthy client therefore reported `guardRejected >= 1` from initialization alone and picked up another on every SSE reconnect, which is noise in a field whose entire purpose is to say "a leg tried to move us **backwards**". **Semantic narrowing worth noting if you query this field: equal-generation re-deliveries are no longer counted, so `guardRejected` will read lower than on 1.4.0 for the same traffic — that is the fix, not a regression.** Nothing else about the guard changed: an equal-generation envelope is still not installed, still returns `:not_modified`, and still advances the liveness stamp exactly where it did before, and the unversioned (`generation <= 0`) carve-out is untouched. No wire, ClickHouse, or dashboard-query change is required. This is the Ruby half of a change being applied across all six backend SDKs (sdk-python ships it in 1.4.1; Go, Java, .NET and Node follow in their next releases), so counts may differ across languages until then.
|
|
7
|
+
- **Docs (fork): a child forked from inside an `on_update` callback must exit (qfg-ec2q).** `on_update` runs on the SDK's SSE reader thread, so a child created with non-block `fork` (no block) from inside that callback inherits that thread as its one surviving thread. If the child does its work and *returns* from the callback instead of exiting, the inherited reader resumes its read loop on the **parent's** socket and consumes frames the parent never sees. The README fork section now says to `exit!` (or `exec`) from such a child. Block-form `fork { ... }`, any child that exits, the documented patterns, and the `parallel` gem are all unaffected — no code change.
|
|
8
|
+
|
|
3
9
|
## 1.4.0 - 2026-09-10
|
|
4
10
|
|
|
5
11
|
> **If you fork worker processes and keep evaluating in the parent, upgrade.** On 1.0.0–1.3.0 the parent process went permanently dark after any `fork(2)` — it kept serving whatever config snapshot it held at the moment of the fork, forever, while `connection_state` reported `:connected`.
|
data/README.md
CHANGED
|
@@ -494,6 +494,13 @@ Caveats:
|
|
|
494
494
|
- In datadir mode a child whose workspace fails to load never dials the
|
|
495
495
|
network: it logs the failure and, if `data_dir_auto_reload` is on, watches
|
|
496
496
|
for a repaired workspace; otherwise the next use retries the load.
|
|
497
|
+
- **A child forked from inside an `on_update` callback must exit.**
|
|
498
|
+
`on_update` runs on the SDK's SSE reader thread, so a child created with
|
|
499
|
+
non-block `fork` (no block) from inside that callback must `exit!` (or
|
|
500
|
+
`exec`) rather than return from the callback — a child that returns lets the
|
|
501
|
+
inherited reader thread resume on the parent's socket, where it consumes
|
|
502
|
+
frames the parent never sees. Block-form `fork { ... }` and any child that
|
|
503
|
+
exits are unaffected.
|
|
497
504
|
|
|
498
505
|
### Puma (clustered mode)
|
|
499
506
|
|
data/lib/quonfig/client.rb
CHANGED
|
@@ -437,6 +437,18 @@ module Quonfig
|
|
|
437
437
|
# fetches (or loads) its own on first use.
|
|
438
438
|
reset_store_in_child!
|
|
439
439
|
|
|
440
|
+
# A fresh instance hash (qfg-xcym). `@instance_hash` identifies this
|
|
441
|
+
# SDK instance in every telemetry payload, and app-quonfig's Debugger
|
|
442
|
+
# groups SDK last-seen by it — so a child that keeps the parent's hash
|
|
443
|
+
# collapses an 8-worker Puma cluster into ONE row with the parent's and
|
|
444
|
+
# the children's windows interleaved. Reforge does not have this
|
|
445
|
+
# problem: `Reforge.fork` builds a whole new Client, which mints its
|
|
446
|
+
# own. This MUST run before +rebuild_aggregators_in_child!+: the
|
|
447
|
+
# reporter captures the hash at construction, so minting after the
|
|
448
|
+
# rebuild would leave the child POSTing under the parent's identity.
|
|
449
|
+
# The parent's hash is untouched — the hook never runs there.
|
|
450
|
+
@instance_hash = SecureRandom.uuid
|
|
451
|
+
|
|
440
452
|
# Fresh aggregators. The parent flushes its own copy; a child that
|
|
441
453
|
# flushed inherited data would double-report it. The reporter is BUILT
|
|
442
454
|
# here (so the child's config loader points at the child's failover
|
|
@@ -392,11 +392,29 @@ module Quonfig
|
|
|
392
392
|
# ordering info, so it is never rejected as "older"; freezing the client
|
|
393
393
|
# on stale config would be worse (mirrors sdk-node).
|
|
394
394
|
unless @held_generation.nil? || incoming_gen <= 0 || incoming_gen > @held_generation
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
395
|
+
if incoming_gen < @held_generation
|
|
396
|
+
@logger.debug "Reject-older guard: dropping incoming generation #{incoming_gen} < held #{@held_generation} (source=#{source})"
|
|
397
|
+
# Failover observability (qfg-41nh.18): count the guard rejection.
|
|
398
|
+
# This single guard covers BOTH the HTTP config-fetch path and the
|
|
399
|
+
# SSE message path (apply_envelope) — every network install funnels
|
|
400
|
+
# here. Only a STRICTLY older payload is counted (qfg-rr5b): that is
|
|
401
|
+
# the one thing `guardRejected` is meant to report, "a leg tried to
|
|
402
|
+
# move us backwards", which is what the sdk_failover dashboard panel
|
|
403
|
+
# is for.
|
|
404
|
+
@failover_aggregator&.record_guard_rejected
|
|
405
|
+
else
|
|
406
|
+
# Equal generation: a re-delivery of the envelope we already hold, and
|
|
407
|
+
# a silent no-op (qfg-rr5b). Two server behaviors produce it in normal
|
|
408
|
+
# steady state — api-delivery's SSE `sendInitialConfig` re-sends the
|
|
409
|
+
# current envelope on every connect regardless of the Last-Event-Id
|
|
410
|
+
# we send, and a config poll on an empty per-leg ETag slot (a
|
|
411
|
+
# fresh transport, a reconnect, the fallback poller's engage fetch)
|
|
412
|
+
# returns a full 200 at the same generation. Counting those as
|
|
413
|
+
# `guardRejected` made a healthy client report failover activity from
|
|
414
|
+
# init alone. Not installed, not counted — but still :not_modified, so
|
|
415
|
+
# the caller's liveness stamp is unchanged.
|
|
416
|
+
@logger.debug "Same-generation re-delivery: ignoring incoming generation #{incoming_gen} (source=#{source})"
|
|
417
|
+
end
|
|
400
418
|
return :not_modified
|
|
401
419
|
end
|
|
402
420
|
|
|
@@ -45,8 +45,10 @@ module Quonfig
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# Count one install dropped by the reject-older ordering guard
|
|
49
|
-
#
|
|
48
|
+
# Count one install dropped by the reject-older ordering guard because it
|
|
49
|
+
# was STRICTLY older than the held generation, on any install path (HTTP
|
|
50
|
+
# config-fetch or SSE). An equal-generation re-delivery is dropped too but
|
|
51
|
+
# is not counted (qfg-rr5b).
|
|
50
52
|
def record_guard_rejected
|
|
51
53
|
@mutex.synchronize do
|
|
52
54
|
@start_at_ms ||= Quonfig::TimeHelpers.now_in_ms
|
data/lib/quonfig/version.rb
CHANGED