camada 0.1.2 → 0.1.3
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 +3 -1
- data/lib/camada/snapshot/client.rb +63 -16
- data/lib/camada/transport.rb +2 -2
- data/lib/camada/version.rb +1 -1
- 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: 66753d06b68709a8360519bc64f67cc2278112b804da0d83bebfbe74ab211ae6
|
|
4
|
+
data.tar.gz: fc1423d70058d49223d3a3462d882fc6898013266c130d4be2835bb5696e9f5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94a9c22c64a0f100c47c6312bc81cd9d4cf0b38a73d30f901a7951815b42b9f111d617f69acd2fa5541945b9626a4259c9ad39e5a2021fd7c8a6a03d38d30809
|
|
7
|
+
data.tar.gz: 07d7bfc337bc64e69781e507efcd1269acbd79e626608e27631aec91004a4de820ad80bc5ba3fbe02d3192005a8f1278fd7825a66df31ea6b7063f87c13c5f9a
|
data/README.md
CHANGED
|
@@ -176,7 +176,9 @@ here (and never raises), while it does at the edge.
|
|
|
176
176
|
|
|
177
177
|
Every entry point runs inside the fail-open envelope: a dead ingest drops telemetry (logged at
|
|
178
178
|
most once a minute, one line, no backtrace), a corrupt snapshot keeps the previous one, a bug in
|
|
179
|
-
the gem costs the request its join, never its response.
|
|
179
|
+
the gem costs the request its join, never its response. A failed snapshot poll (401, 5xx, no
|
|
180
|
+
answer) keeps the rules in place and paces the next one: `Retry-After` in seconds is honoured,
|
|
181
|
+
floored at 5 s and capped at the refresh interval. `CAMADA_DISABLED=1` bypasses everything.
|
|
180
182
|
|
|
181
183
|
## Development
|
|
182
184
|
|
|
@@ -23,6 +23,21 @@ module Camada
|
|
|
23
23
|
# Process.pid with the pid that built the client and starts over in a forked worker.
|
|
24
24
|
COLD = MatchResult.new(reason: "cold") # never loaded yet: fail open, mirrors the collector
|
|
25
25
|
NONE = MatchResult.new
|
|
26
|
+
POLL_FLOOR_S = 5.0
|
|
27
|
+
|
|
28
|
+
# Seconds to wait before the next self-initiated poll after a poll answered `status`, or nil
|
|
29
|
+
# when the answer was 200/204/304 (not paced: the normal cadence governs). A Retry-After in
|
|
30
|
+
# delta-seconds is honoured, floored at 5 s and capped at the current cadence (the cap wins).
|
|
31
|
+
# HTTP-dates and anything else that is not all digits read as absent (edge-analyst only sends
|
|
32
|
+
# delta-seconds). status 0 = no answer. Internal.
|
|
33
|
+
def self.next_poll_delay(status, retry_after, refresh_s)
|
|
34
|
+
return nil if [200, 204, 304].include?(status)
|
|
35
|
+
|
|
36
|
+
ra = retry_after.to_s.gsub(/\A[ \t]+|[ \t]+\z/, "")
|
|
37
|
+
secs = ra.match?(/\A[0-9]+\z/) ? [ra.to_i, 1_000_000_000].min : 0 # overflow reads as huge, not invalid
|
|
38
|
+
cap = [refresh_s.to_f, 0.0].max
|
|
39
|
+
[[secs, POLL_FLOOR_S].max, cap].min.to_f # rubocop:disable Style/ComparableClamp -- not #clamp: it raises when the cap is under the floor
|
|
40
|
+
end
|
|
26
41
|
|
|
27
42
|
class Client
|
|
28
43
|
attr_accessor :transport
|
|
@@ -46,6 +61,7 @@ module Camada
|
|
|
46
61
|
@pinned = !refresh_s.nil?
|
|
47
62
|
@etag = nil
|
|
48
63
|
@loaded_at = nil
|
|
64
|
+
@not_before = 0.0 # a failed poll gates the next self-initiated one until this monotonic time
|
|
49
65
|
fresh_state!
|
|
50
66
|
end
|
|
51
67
|
|
|
@@ -81,24 +97,55 @@ module Camada
|
|
|
81
97
|
@loaded_at.nil? || monotonic - @loaded_at > @refresh_s * 0.9
|
|
82
98
|
end
|
|
83
99
|
|
|
84
|
-
# Kicks a refresh when
|
|
100
|
+
# Kicks a refresh when due; never blocks the request path, never raises.
|
|
85
101
|
def ensure_fresh
|
|
86
102
|
check_fork!
|
|
87
|
-
return if !
|
|
103
|
+
return if !due? || @loading.locked?
|
|
88
104
|
|
|
89
|
-
t = Thread.new {
|
|
105
|
+
t = Thread.new { refresh_if_due }
|
|
90
106
|
t.name = "camada-snapshot-load"
|
|
91
107
|
t.report_on_exception = false
|
|
92
108
|
nil
|
|
93
109
|
end
|
|
94
110
|
|
|
95
|
-
# One synchronous poll (single in-flight): what
|
|
111
|
+
# One synchronous poll (single in-flight): what tests and warm-ups call directly, ignoring the gate.
|
|
96
112
|
def refresh
|
|
113
|
+
poll { true }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Cold (never loaded) and no-snapshot both fail open, mirroring the edge collector.
|
|
117
|
+
def verdict(i)
|
|
118
|
+
return COLD if @loaded_at.nil?
|
|
119
|
+
|
|
120
|
+
m = @matcher
|
|
121
|
+
m ? m.match(i) : NONE
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# What the background kick calls: take the slot, then re-check, so a second kick that lost the
|
|
127
|
+
# race to a poll that has just finished (and gated the next one) does nothing.
|
|
128
|
+
def refresh_if_due
|
|
129
|
+
poll { due? }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Stale and past the failure gate: what every self-initiated poll (request path, timer tick)
|
|
133
|
+
# asks. A gate further away than one cadence means the clock stepped; treat it as open.
|
|
134
|
+
def due?
|
|
135
|
+
return false unless stale?
|
|
136
|
+
|
|
137
|
+
now = monotonic
|
|
138
|
+
@not_before <= now || @not_before - now > @refresh_s
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def poll
|
|
97
142
|
check_fork!
|
|
98
143
|
lock = @loading # bound once: after_fork! swaps the attribute
|
|
99
144
|
return unless lock.try_lock
|
|
100
145
|
|
|
101
146
|
begin
|
|
147
|
+
return unless yield
|
|
148
|
+
|
|
102
149
|
load_once
|
|
103
150
|
rescue StandardError => e # a poll that can never succeed must not be silent, nor fatal
|
|
104
151
|
Guarded.log_rate_limited(e)
|
|
@@ -107,16 +154,6 @@ module Camada
|
|
|
107
154
|
end
|
|
108
155
|
end
|
|
109
156
|
|
|
110
|
-
# Cold (never loaded) and no-snapshot both fail open, mirroring the edge collector.
|
|
111
|
-
def verdict(i)
|
|
112
|
-
return COLD if @loaded_at.nil?
|
|
113
|
-
|
|
114
|
-
m = @matcher
|
|
115
|
-
m ? m.match(i) : NONE
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
private
|
|
119
|
-
|
|
120
157
|
def fresh_state!
|
|
121
158
|
@pid = Process.pid
|
|
122
159
|
@loading = Mutex.new
|
|
@@ -150,8 +187,18 @@ module Camada
|
|
|
150
187
|
headers["x-camada-sdk"] = @sdk if @sdk
|
|
151
188
|
# a tenant without that container is answered with the next one down
|
|
152
189
|
headers["x-camada-snapshot"] = @snapshot_version.to_s if @snapshot_version > 3
|
|
153
|
-
res =
|
|
154
|
-
|
|
190
|
+
res = begin
|
|
191
|
+
@transport.call(HttpRequest.new(method: "GET", url: @url, headers: headers, body: nil, timeout_s: @timeout_s))
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
Guarded.log_rate_limited(e) # still logged (rate-limited), and gated below as status 0
|
|
194
|
+
HttpResponse.new(status: 0, headers: {}, body: "".b) # a transport that raises is no answer
|
|
195
|
+
end
|
|
196
|
+
delay = Snapshot.next_poll_delay(res.status, res.headers["retry-after"], @refresh_s)
|
|
197
|
+
unless delay.nil? # 401/5xx/network: keep what we have (cold stays cold) and back off
|
|
198
|
+
@not_before = monotonic + delay # written before the slot is released
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
@not_before = 0.0
|
|
155
202
|
|
|
156
203
|
# loaded_at is stamped last (even when the body turns out corrupt): "not cold" is what Puma's
|
|
157
204
|
# request threads read as "rules in place", so it must not be visible before the matcher and
|
data/lib/camada/transport.rb
CHANGED
|
@@ -42,11 +42,11 @@ module Camada
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def self.response(status, headers, body)
|
|
45
|
-
if headers["content-encoding"].to_s.downcase == "gzip"
|
|
45
|
+
if !body.empty? && headers["content-encoding"].to_s.downcase == "gzip"
|
|
46
46
|
begin
|
|
47
47
|
body = Zlib::GzipReader.new(StringIO.new(body)).read.b
|
|
48
48
|
rescue StandardError
|
|
49
|
-
return HttpResponse.new(status: 0, headers:
|
|
49
|
+
return HttpResponse.new(status: 0, headers: {}, body: "".b) # a body we cannot read is no answer at all: no retry-after either
|
|
50
50
|
end
|
|
51
51
|
headers.delete("content-encoding")
|
|
52
52
|
end
|
data/lib/camada/version.rb
CHANGED
|
@@ -5,6 +5,6 @@ module Camada
|
|
|
5
5
|
# source: camada.gemspec reads it at build, and the sibling drift guards (camada-backend,
|
|
6
6
|
# camada-web, camada-mkt) parse this file with /^\s*VERSION = "([^"]+)"/m the way they parse
|
|
7
7
|
# a package.json. Plain X.Y.Z only: the analyst's SDK_RE drops anything else.
|
|
8
|
-
VERSION = "0.1.
|
|
8
|
+
VERSION = "0.1.3"
|
|
9
9
|
SDK_ID = "@camada/ruby/#{VERSION}".freeze
|
|
10
10
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: camada
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- camada
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'camada SDK for Ruby: inline enforcement of your snapshot (ordered custom
|
|
14
14
|
rules, then allow, block, challenge), first-party beacon and proof-of-work challenge,
|