smplkit 3.0.95 → 3.0.97
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/lib/smplkit/account/client.rb +128 -0
- data/lib/smplkit/account/models.rb +71 -0
- data/lib/smplkit/api_support.rb +91 -0
- data/lib/smplkit/audit/buffer.rb +3 -1
- data/lib/smplkit/audit/categories.rb +21 -10
- data/lib/smplkit/audit/client.rb +18 -9
- data/lib/smplkit/audit/event_types.rb +26 -10
- data/lib/smplkit/audit/events.rb +93 -17
- data/lib/smplkit/{management/audit.rb → audit/forwarders.rb} +93 -85
- data/lib/smplkit/audit/models.rb +86 -32
- data/lib/smplkit/audit/resource_types.rb +21 -9
- data/lib/smplkit/buffers.rb +250 -0
- data/lib/smplkit/client.rb +161 -70
- data/lib/smplkit/config/client.rb +874 -186
- data/lib/smplkit/config/helpers.rb +44 -6
- data/lib/smplkit/config/models.rb +114 -7
- data/lib/smplkit/config_resolution.rb +17 -9
- data/lib/smplkit/errors.rb +14 -3
- data/lib/smplkit/flags/client.rb +602 -116
- data/lib/smplkit/flags/models.rb +110 -8
- data/lib/smplkit/flags/types.rb +8 -9
- data/lib/smplkit/jobs/client.rb +306 -0
- data/lib/smplkit/jobs/models.rb +47 -18
- data/lib/smplkit/logging/client.rb +755 -191
- data/lib/smplkit/logging/helpers.rb +5 -1
- data/lib/smplkit/logging/levels.rb +3 -1
- data/lib/smplkit/logging/models.rb +163 -6
- data/lib/smplkit/logging/normalize.rb +3 -1
- data/lib/smplkit/logging/resolution.rb +4 -4
- data/lib/smplkit/logging/sources.rb +1 -1
- data/lib/smplkit/platform/client.rb +597 -0
- data/lib/smplkit/platform/models.rb +282 -0
- data/lib/smplkit/{management → platform}/types.rb +21 -4
- data/lib/smplkit/transport.rb +103 -0
- data/lib/smplkit/ws.rb +1 -1
- data/lib/smplkit.rb +18 -6
- metadata +11 -7
- data/lib/smplkit/management/buffer.rb +0 -198
- data/lib/smplkit/management/client.rb +0 -1074
- data/lib/smplkit/management/jobs.rb +0 -226
- data/lib/smplkit/management/models.rb +0 -178
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
# Registration buffers backing the SDK's batched-discovery sub-clients.
|
|
6
|
+
#
|
|
7
|
+
# Four buffer types, each owned by the sub-client that drains it:
|
|
8
|
+
#
|
|
9
|
+
# - +ContextRegistrationBuffer+ -> +client.platform.contexts._buffer+
|
|
10
|
+
# - +FlagRegistrationBuffer+ -> +client.flags._buffer+
|
|
11
|
+
# - +ConfigRegistrationBuffer+ -> +client.config._buffer+
|
|
12
|
+
# - +LoggerRegistrationBuffer+ -> +client.logging.loggers._buffer+
|
|
13
|
+
#
|
|
14
|
+
# There is exactly one buffer + one bulk-flush implementation per resource.
|
|
15
|
+
module Smplkit
|
|
16
|
+
# When the deduplication LRU exceeds this size, the oldest entry is
|
|
17
|
+
# evicted. The next observation of an evicted entry will re-flush.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
CONTEXT_REGISTRATION_LRU_SIZE = 10_000
|
|
21
|
+
|
|
22
|
+
# Pending-queue size that triggers an immediate background flush from
|
|
23
|
+
# inside +register+. The periodic timer on +Client+ covers the tail
|
|
24
|
+
# case for low-traffic services.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
CONTEXT_BATCH_FLUSH_SIZE = 100
|
|
28
|
+
# @api private
|
|
29
|
+
FLAG_BATCH_FLUSH_SIZE = 50
|
|
30
|
+
# @api private
|
|
31
|
+
LOGGER_BATCH_FLUSH_SIZE = 50
|
|
32
|
+
# @api private
|
|
33
|
+
CONFIG_BATCH_FLUSH_SIZE = 50
|
|
34
|
+
|
|
35
|
+
# Thread-safe batch buffer for context registration.
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
class ContextRegistrationBuffer
|
|
39
|
+
def initialize
|
|
40
|
+
@seen = {}
|
|
41
|
+
@pending = []
|
|
42
|
+
@lock = Mutex.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Queue any unseen contexts.
|
|
46
|
+
def observe(contexts)
|
|
47
|
+
@lock.synchronize do
|
|
48
|
+
contexts.each do |ctx|
|
|
49
|
+
cache_key = [ctx.type, ctx.key]
|
|
50
|
+
next if @seen.key?(cache_key)
|
|
51
|
+
|
|
52
|
+
@seen.shift if @seen.size >= CONTEXT_REGISTRATION_LRU_SIZE
|
|
53
|
+
@seen[cache_key] = ctx.attributes
|
|
54
|
+
@pending << { "type" => ctx.type, "key" => ctx.key, "attributes" => ctx.attributes.dup }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return and clear the current pending batch.
|
|
60
|
+
def drain
|
|
61
|
+
@lock.synchronize do
|
|
62
|
+
batch = @pending
|
|
63
|
+
@pending = []
|
|
64
|
+
batch
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pending_count
|
|
69
|
+
@lock.synchronize { @pending.length }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Thread-safe batch buffer for flag declarations.
|
|
74
|
+
#
|
|
75
|
+
# Use +peek+ + +commit(ids)+ for the send path so a failed POST leaves
|
|
76
|
+
# declarations queued for the next attempt; the legacy +drain+ is
|
|
77
|
+
# unconditional and used only by tests.
|
|
78
|
+
#
|
|
79
|
+
# @api private
|
|
80
|
+
class FlagRegistrationBuffer
|
|
81
|
+
def initialize
|
|
82
|
+
@seen = {}
|
|
83
|
+
@pending = []
|
|
84
|
+
@lock = Mutex.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add(declaration)
|
|
88
|
+
@lock.synchronize do
|
|
89
|
+
next if @seen.key?(declaration.id)
|
|
90
|
+
|
|
91
|
+
@seen[declaration.id] = true
|
|
92
|
+
item = { "id" => declaration.id, "type" => declaration.type, "default" => declaration.default }
|
|
93
|
+
item["service"] = declaration.service if declaration.service
|
|
94
|
+
item["environment"] = declaration.environment if declaration.environment
|
|
95
|
+
@pending << item
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def peek
|
|
100
|
+
@lock.synchronize { @pending.dup }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def commit(ids)
|
|
104
|
+
return if ids.nil? || ids.empty?
|
|
105
|
+
|
|
106
|
+
committed = ids.to_set
|
|
107
|
+
@lock.synchronize { @pending.reject! { |item| committed.include?(item["id"]) } }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def drain
|
|
111
|
+
@lock.synchronize do
|
|
112
|
+
batch = @pending
|
|
113
|
+
@pending = []
|
|
114
|
+
batch
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def pending_count
|
|
119
|
+
@lock.synchronize { @pending.length }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Thread-safe batch buffer for config declarations.
|
|
124
|
+
#
|
|
125
|
+
# Configs differ from flags/loggers because each entry carries a nested
|
|
126
|
+
# +items+ dict that grows incrementally as the customer's code touches more
|
|
127
|
+
# typed getters on a declared handle. The buffer therefore stores per-config
|
|
128
|
+
# metadata permanently (so post-flush deltas can be re-attributed to the
|
|
129
|
+
# right service/environment) and dedups items per +(config_id, item_key)+ so
|
|
130
|
+
# we never re-send an item that the server has already accepted.
|
|
131
|
+
#
|
|
132
|
+
# Call sites:
|
|
133
|
+
#
|
|
134
|
+
# - +declare+ once per +client.config.bind(id, ...)+.
|
|
135
|
+
# - +add_item+ for every introspected leaf field, or anything else the
|
|
136
|
+
# runtime client observes. Repeated calls with the same
|
|
137
|
+
# +(config_id, item_key)+ after a successful flush are no-ops.
|
|
138
|
+
# - +drain+ returns the pending payload list, clears the pending buffer, and
|
|
139
|
+
# records what was sent.
|
|
140
|
+
#
|
|
141
|
+
# The buffer never drops metadata — only +pending+ is cleared on flush. If
|
|
142
|
+
# the customer's code declares new items via typed getters after a flush, a
|
|
143
|
+
# fresh pending entry is created using the stored metadata so the server can
|
|
144
|
+
# route the delta to the right source row.
|
|
145
|
+
#
|
|
146
|
+
# @api private
|
|
147
|
+
class ConfigRegistrationBuffer
|
|
148
|
+
def initialize
|
|
149
|
+
@pending = {} # config_id -> { id:, items: {}, ...meta }
|
|
150
|
+
@meta = {} # config_id -> { service:, environment:, parent:, name:, description: }
|
|
151
|
+
@sent_items = {} # "#{config_id}::#{item_key}" -> true
|
|
152
|
+
@lock = Mutex.new
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Register a configuration. Idempotent within a process.
|
|
156
|
+
def declare(config_id, service:, environment:, parent: nil, name: nil, description: nil)
|
|
157
|
+
@lock.synchronize do
|
|
158
|
+
next if @meta.key?(config_id)
|
|
159
|
+
|
|
160
|
+
@meta[config_id] = {
|
|
161
|
+
service: service, environment: environment,
|
|
162
|
+
parent: parent, name: name, description: description
|
|
163
|
+
}
|
|
164
|
+
@pending[config_id] = build_entry(config_id)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Queue an item declaration if not already sent.
|
|
169
|
+
#
|
|
170
|
+
# Must be preceded by +declare+ for the same +config_id+; otherwise the
|
|
171
|
+
# call is dropped (no implicit declaration).
|
|
172
|
+
def add_item(config_id, item_key, item_type, default, description = nil)
|
|
173
|
+
@lock.synchronize do
|
|
174
|
+
next unless @meta.key?(config_id)
|
|
175
|
+
next if @sent_items.key?("#{config_id}::#{item_key}")
|
|
176
|
+
|
|
177
|
+
entry = (@pending[config_id] ||= build_entry(config_id))
|
|
178
|
+
next if entry["items"].key?(item_key)
|
|
179
|
+
|
|
180
|
+
item = { "value" => default, "type" => item_type }
|
|
181
|
+
item["description"] = description unless description.nil?
|
|
182
|
+
entry["items"][item_key] = item
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Return and clear the pending batch; record sent items.
|
|
187
|
+
def drain
|
|
188
|
+
@lock.synchronize do
|
|
189
|
+
entries = @pending.values
|
|
190
|
+
entries.each do |entry|
|
|
191
|
+
entry["items"].each_key { |item_key| @sent_items["#{entry["id"]}::#{item_key}"] = true }
|
|
192
|
+
end
|
|
193
|
+
@pending = {}
|
|
194
|
+
entries
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def pending_count
|
|
199
|
+
@lock.synchronize { @pending.size }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def build_entry(config_id)
|
|
205
|
+
meta = @meta[config_id]
|
|
206
|
+
entry = { "id" => config_id, "items" => {} }
|
|
207
|
+
%i[service environment parent name description].each do |k|
|
|
208
|
+
v = meta[k]
|
|
209
|
+
entry[k.to_s] = v unless v.nil?
|
|
210
|
+
end
|
|
211
|
+
entry
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Thread-safe batch buffer for logger discovery.
|
|
216
|
+
#
|
|
217
|
+
# @api private
|
|
218
|
+
class LoggerRegistrationBuffer
|
|
219
|
+
def initialize
|
|
220
|
+
@seen = {}
|
|
221
|
+
@pending = []
|
|
222
|
+
@lock = Mutex.new
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def add(source)
|
|
226
|
+
@lock.synchronize do
|
|
227
|
+
next if @seen.key?(source.name)
|
|
228
|
+
|
|
229
|
+
@seen[source.name] = source.resolved_level
|
|
230
|
+
item = { "id" => source.name, "resolved_level" => source.resolved_level&.to_s }
|
|
231
|
+
item["level"] = source.level&.to_s if source.level
|
|
232
|
+
item["service"] = source.service if source.service
|
|
233
|
+
item["environment"] = source.environment if source.environment
|
|
234
|
+
@pending << item
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def drain
|
|
239
|
+
@lock.synchronize do
|
|
240
|
+
batch = @pending
|
|
241
|
+
@pending = []
|
|
242
|
+
batch
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def pending_count
|
|
247
|
+
@lock.synchronize { @pending.length }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
data/lib/smplkit/client.rb
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "concurrent"
|
|
4
4
|
|
|
5
|
-
require "smplkit/audit/client"
|
|
6
|
-
|
|
7
5
|
module Smplkit
|
|
8
6
|
# Synchronous entry point for the smplkit SDK.
|
|
9
7
|
#
|
|
@@ -20,19 +18,28 @@ module Smplkit
|
|
|
20
18
|
# # ...
|
|
21
19
|
# end
|
|
22
20
|
#
|
|
23
|
-
# All parameters are optional. When omitted, the SDK resolves
|
|
24
|
-
#
|
|
25
|
-
#
|
|
21
|
+
# All parameters are optional. When omitted, the SDK resolves each one in
|
|
22
|
+
# precedence order, lowest to highest: built-in defaults, then the
|
|
23
|
+
# +~/.smplkit+ configuration file, then +SMPLKIT_*+ environment variables,
|
|
24
|
+
# then the explicit constructor arguments (a value supplied at a higher
|
|
25
|
+
# level overrides the lower ones).
|
|
26
26
|
#
|
|
27
27
|
# +Smplkit::Client+ is thread-safe by construction. Background work runs on
|
|
28
28
|
# internal SDK-owned threads; public methods block the calling thread and
|
|
29
29
|
# return values directly.
|
|
30
30
|
class Client
|
|
31
|
+
# Periodic flush of all sub-client registration buffers (contexts, flags,
|
|
32
|
+
# loggers). Threshold flushes still fire immediately when buffers fill up;
|
|
33
|
+
# this timer is the liveness guarantee for the tail.
|
|
31
34
|
PERIODIC_FLUSH_INTERVAL = 60.0
|
|
32
35
|
|
|
33
|
-
attr_reader :
|
|
36
|
+
attr_reader :platform, :account, :config, :flags, :logging, :audit, :jobs
|
|
34
37
|
|
|
35
|
-
# Construct, yield to the block, and close on exit.
|
|
38
|
+
# Construct a client, yield it to the block, and close it on exit.
|
|
39
|
+
#
|
|
40
|
+
# @param kwargs [Hash] The same keyword arguments as {#initialize}.
|
|
41
|
+
# @yieldparam client [Client] The constructed client.
|
|
42
|
+
# @return [Object] The block's return value.
|
|
36
43
|
def self.open(**kwargs)
|
|
37
44
|
client = new(**kwargs)
|
|
38
45
|
begin
|
|
@@ -42,7 +49,17 @@ module Smplkit
|
|
|
42
49
|
end
|
|
43
50
|
end
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
# @param api_key [String, nil] API key for authenticating with the smplkit platform.
|
|
53
|
+
# @param environment [String, nil] The environment to connect to (e.g. +"production"+).
|
|
54
|
+
# @param service [String, nil] Service name (e.g. +"user-service"+).
|
|
55
|
+
# @param profile [String, nil] Named profile section to read from +~/.smplkit+.
|
|
56
|
+
# @param base_domain [String, nil] Base domain for API requests (default +"smplkit.com"+).
|
|
57
|
+
# @param scheme [String, nil] URL scheme (default +"https"+).
|
|
58
|
+
# @param debug [Boolean, nil] Enable debug logging in the SDK.
|
|
59
|
+
# @param telemetry [Boolean, nil] Enable anonymous usage telemetry (default +true+).
|
|
60
|
+
# @param extra_headers [Hash{String => String}, nil] Extra HTTP headers attached to
|
|
61
|
+
# every request the client sends.
|
|
62
|
+
def initialize(api_key: nil, environment: nil, service: nil, profile: nil,
|
|
46
63
|
base_domain: nil, scheme: nil, debug: nil, telemetry: nil,
|
|
47
64
|
extra_headers: nil)
|
|
48
65
|
cfg = ConfigResolution.resolve_config(
|
|
@@ -56,6 +73,7 @@ module Smplkit
|
|
|
56
73
|
@service = cfg.service
|
|
57
74
|
@base_domain = cfg.base_domain
|
|
58
75
|
@scheme = cfg.scheme
|
|
76
|
+
@extra_headers = extra_headers
|
|
59
77
|
|
|
60
78
|
masked_key = cfg.api_key.length > 10 ? "#{cfg.api_key[0, 10]}..." : cfg.api_key
|
|
61
79
|
Smplkit.debug(
|
|
@@ -65,54 +83,92 @@ module Smplkit
|
|
|
65
83
|
"debug=#{cfg.debug} telemetry=#{cfg.telemetry}"
|
|
66
84
|
)
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@
|
|
86
|
+
# Build the per-service HTTP transports + the context-registration buffer.
|
|
87
|
+
# Side-effect-free: each transport connects lazily on first call.
|
|
88
|
+
# client.platform owns the buffer; client.config/flags/logging/jobs borrow
|
|
89
|
+
# their transports from here.
|
|
90
|
+
@transports = Transport.build_service_transports(Transport.to_transport_config(cfg, extra_headers))
|
|
73
91
|
|
|
74
|
-
app_url =
|
|
75
|
-
flags_url = ConfigResolution.service_url(cfg.scheme, "flags", cfg.base_domain)
|
|
76
|
-
logging_url = ConfigResolution.service_url(cfg.scheme, "logging", cfg.base_domain)
|
|
92
|
+
app_url = @transports.app_url
|
|
77
93
|
audit_url = ConfigResolution.service_url(cfg.scheme, "audit", cfg.base_domain)
|
|
94
|
+
|
|
95
|
+
# Alias the shared HTTP transports — single connection pool per service.
|
|
96
|
+
@http_client = @transports.config_http
|
|
97
|
+
@app_http = @transports.app_http
|
|
78
98
|
@app_base_url = app_url
|
|
79
99
|
|
|
100
|
+
# Metrics reporter
|
|
80
101
|
@metrics = if cfg.telemetry
|
|
81
|
-
MetricsReporter.new(http_client: @
|
|
82
|
-
environment: cfg.environment,
|
|
83
|
-
service: cfg.service)
|
|
102
|
+
MetricsReporter.new(http_client: @app_http, environment: cfg.environment, service: cfg.service)
|
|
84
103
|
end
|
|
85
104
|
|
|
86
105
|
@ws_manager = nil
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
# Platform's cross-cutting CRUD on one client; wired into this parent so
|
|
107
|
+
# it borrows the shared app transport, and owns the context-registration
|
|
108
|
+
# buffer. Built BEFORE flags so the contexts seam below is available.
|
|
109
|
+
@platform = Platform::PlatformClient.new(app_transport: @app_http)
|
|
110
|
+
# Account-level settings on one client; built from the app url + api key
|
|
111
|
+
# (the settings sub-client uses Faraday directly).
|
|
112
|
+
@account = Account::AccountClient.new(api_key: cfg.api_key, base_url: app_url, extra_headers: extra_headers)
|
|
113
|
+
# Config's full surface on one client; wired into this parent so it
|
|
114
|
+
# borrows the shared config transport and WebSocket.
|
|
115
|
+
@config = Config::ConfigClient.new(parent: self, transport: @transports.config_http, metrics: @metrics)
|
|
116
|
+
# Flags' full surface on one client; wired into this parent so it borrows
|
|
117
|
+
# the shared flags transport and WebSocket. ``contexts`` is the injection
|
|
118
|
+
# seam for evaluation-context registration, wired to
|
|
119
|
+
# ``client.platform.contexts``.
|
|
120
|
+
@flags = Flags::FlagsClient.new(
|
|
121
|
+
parent: self, transport: @transports.flags_http, contexts: @platform.contexts, metrics: @metrics
|
|
122
|
+
)
|
|
123
|
+
# Logging's full surface on one client; wired into this parent so it
|
|
124
|
+
# borrows the shared logging transport and WebSocket. The two management
|
|
125
|
+
# sub-clients live at client.logging.loggers / client.logging.log_groups.
|
|
126
|
+
@logging = Logging::LoggingClient.new(parent: self, transport: @transports.logging_http, metrics: @metrics)
|
|
127
|
+
# Audit's full surface on one client; this runtime instance carries the
|
|
128
|
+
# configured environment as ``X-Smplkit-Environment`` and owns its own
|
|
129
|
+
# transport (closed in ``close``).
|
|
130
|
+
@audit = Audit::AuditClient.new(
|
|
131
|
+
api_key: cfg.api_key, base_url: audit_url, environment: cfg.environment, extra_headers: extra_headers
|
|
132
|
+
)
|
|
133
|
+
# Jobs has no runtime/management split — reuse the shared jobs transport
|
|
134
|
+
# (single connection pool) so ``client.jobs`` is one-stop.
|
|
135
|
+
@jobs = Jobs::JobsClient.new(auth_client: @transports.jobs_http)
|
|
94
136
|
|
|
137
|
+
# Construction is side-effect-free: no background threads, no phone-home.
|
|
138
|
+
# The periodic registration-buffer flush and the service-context
|
|
139
|
+
# registration are deferred until the first config/flags/logging operation
|
|
140
|
+
# or set_context via ``_ensure_started`` — so an audit-only or jobs-only
|
|
141
|
+
# customer pays zero threads and zero network at construction.
|
|
95
142
|
@closed = false
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@
|
|
99
|
-
|
|
100
|
-
end
|
|
143
|
+
@started = false
|
|
144
|
+
@start_lock = Mutex.new
|
|
145
|
+
@flush_timer = nil
|
|
146
|
+
@init_thread = nil
|
|
101
147
|
end
|
|
102
148
|
|
|
103
|
-
#
|
|
149
|
+
# Optionally pre-warm the SDK and block until the live socket is up.
|
|
150
|
+
#
|
|
151
|
+
# Eagerly connects config and flags — flushing discovery, pre-fetching all
|
|
152
|
+
# flags and configs into the local cache, opening the live-updates WebSocket
|
|
153
|
+
# — and waits for the handshake to complete. After this returns, +flag.get+
|
|
154
|
+
# / +client.config.subscribe+ hit cache (no first-request connect tax) and
|
|
155
|
+
# any +on_change+ listeners receive every server event from this point
|
|
156
|
+
# forward.
|
|
104
157
|
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
158
|
+
# Optional: config and flags connect lazily on first live use, so this is
|
|
159
|
+
# purely a pre-warm / WebSocket-ready barrier. Logging integration is *not*
|
|
160
|
+
# connected here — call +client.logging.install+ separately if you want it
|
|
161
|
+
# (it installs adapters and hooks into your application's logger, which
|
|
162
|
+
# should be opt-in).
|
|
110
163
|
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
164
|
+
# @param timeout [Float] Maximum seconds to wait for the live-updates
|
|
165
|
+
# WebSocket handshake before giving up. Defaults to +10.0+.
|
|
166
|
+
# @return [void]
|
|
167
|
+
# @raise [Smplkit::TimeoutError] If the WebSocket fails to connect within
|
|
168
|
+
# +timeout+ seconds.
|
|
113
169
|
def wait_until_ready(timeout: 10.0)
|
|
114
|
-
@flags.
|
|
115
|
-
@config.
|
|
170
|
+
@flags._ensure_connected
|
|
171
|
+
@config._ensure_connected
|
|
116
172
|
ws = _ensure_ws
|
|
117
173
|
deadline = monotonic_now + timeout
|
|
118
174
|
while ws.connection_status != "connected"
|
|
@@ -127,21 +183,34 @@ module Smplkit
|
|
|
127
183
|
|
|
128
184
|
# Stash +contexts+ as the current request's evaluation context.
|
|
129
185
|
#
|
|
186
|
+
# Typical use is from middleware — set the context once at request entry and
|
|
187
|
+
# every +flag.get+ (and other context-sensitive evaluations) inside that
|
|
188
|
+
# request automatically picks it up.
|
|
189
|
+
#
|
|
190
|
+
# Each unique +(type, key)+ is also registered with the platform
|
|
191
|
+
# (deduplicated via an LRU; sent in the background).
|
|
192
|
+
#
|
|
130
193
|
# Two usage shapes:
|
|
131
194
|
#
|
|
132
195
|
# # Fire-and-forget (typical middleware)
|
|
133
196
|
# client.set_context([Smplkit::Context.new("user", "u-123")])
|
|
134
197
|
#
|
|
135
|
-
# # Scoped block (impersonation or one-off override)
|
|
198
|
+
# # Scoped block (e.g. impersonation or one-off override)
|
|
136
199
|
# client.set_context([Smplkit::Context.new("user", "impersonated")]) do
|
|
137
200
|
# # ...
|
|
138
201
|
# end
|
|
139
202
|
# # original context restored here
|
|
140
203
|
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
204
|
+
# @param contexts [Array<Smplkit::Context>] The contexts to make active for
|
|
205
|
+
# the current thread (e.g. the request's user and account). An empty array
|
|
206
|
+
# clears any registration step but still returns a scope.
|
|
207
|
+
# @yield When a block is given, the contexts are active only for its
|
|
208
|
+
# duration and the previous context is restored on exit.
|
|
209
|
+
# @return [Object, Smplkit::ContextScope] The block's return value when a
|
|
210
|
+
# block is given; otherwise a scope you can ignore for fire-and-forget use.
|
|
143
211
|
def set_context(contexts, &block)
|
|
144
|
-
|
|
212
|
+
_ensure_started
|
|
213
|
+
@platform.contexts.register(contexts) if contexts && !contexts.empty?
|
|
145
214
|
|
|
146
215
|
scope = Smplkit.set_request_context(contexts || [])
|
|
147
216
|
if block
|
|
@@ -151,19 +220,25 @@ module Smplkit
|
|
|
151
220
|
end
|
|
152
221
|
end
|
|
153
222
|
|
|
223
|
+
# Release all resources held by this client.
|
|
224
|
+
#
|
|
225
|
+
# @return [void]
|
|
154
226
|
def close
|
|
155
227
|
Smplkit.debug("lifecycle", "Client.close called")
|
|
156
228
|
@closed = true
|
|
157
229
|
@flush_timer&.shutdown
|
|
230
|
+
@flush_timer = nil
|
|
158
231
|
final_flush
|
|
159
232
|
@metrics&.close
|
|
160
233
|
@logging._close
|
|
161
234
|
@flags._close
|
|
162
|
-
@config._close
|
|
163
235
|
@audit._close
|
|
164
236
|
@ws_manager&.stop
|
|
165
237
|
@ws_manager = nil
|
|
166
|
-
|
|
238
|
+
# Close the shared per-service HTTP transports (app/config/flags/logging/
|
|
239
|
+
# jobs). client.platform/account borrow the app transport and close
|
|
240
|
+
# nothing; client.audit owns and closed its own transport above.
|
|
241
|
+
@transports.close
|
|
167
242
|
end
|
|
168
243
|
|
|
169
244
|
# Internal accessors used by sub-clients --------------------------------
|
|
@@ -175,15 +250,29 @@ module Smplkit
|
|
|
175
250
|
def _metrics = @metrics
|
|
176
251
|
def _extra_headers = @extra_headers
|
|
177
252
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
253
|
+
# Start the deferred background machinery exactly once.
|
|
254
|
+
#
|
|
255
|
+
# Idempotent and thread-safe (lock + flag); a no-op after +close+. Triggered
|
|
256
|
+
# by the first config/flags/logging operation, +set_context+,
|
|
257
|
+
# +wait_until_ready+, or WebSocket open — never at construction.
|
|
258
|
+
def _ensure_started
|
|
259
|
+
@start_lock.synchronize do
|
|
260
|
+
return if @started || @closed
|
|
261
|
+
|
|
262
|
+
@started = true
|
|
183
263
|
end
|
|
264
|
+
schedule_periodic_flush
|
|
265
|
+
@init_thread = Thread.new { register_service_context }
|
|
184
266
|
end
|
|
185
267
|
|
|
186
|
-
def
|
|
268
|
+
def _ensure_ws
|
|
269
|
+
_ensure_started
|
|
270
|
+
if @ws_manager.nil?
|
|
271
|
+
@ws_manager = SharedWebSocket.new(app_base_url: @app_base_url, api_key: @api_key, metrics: @metrics)
|
|
272
|
+
@ws_manager.start
|
|
273
|
+
end
|
|
274
|
+
@ws_manager
|
|
275
|
+
end
|
|
187
276
|
|
|
188
277
|
private
|
|
189
278
|
|
|
@@ -198,38 +287,40 @@ module Smplkit
|
|
|
198
287
|
@flush_timer.execute
|
|
199
288
|
end
|
|
200
289
|
|
|
201
|
-
# Extracted as a private method so the timer body is reachable from
|
|
202
|
-
#
|
|
290
|
+
# Extracted as a private method so the timer body is reachable from tests
|
|
291
|
+
# without poking into Concurrent::TimerTask internals.
|
|
203
292
|
def run_periodic_flush
|
|
204
293
|
return if @closed
|
|
205
294
|
|
|
206
|
-
@
|
|
207
|
-
@
|
|
208
|
-
@
|
|
295
|
+
@platform.contexts.flush
|
|
296
|
+
@flags.flush
|
|
297
|
+
@logging.loggers.flush
|
|
298
|
+
@config.flush
|
|
209
299
|
rescue StandardError => e
|
|
210
300
|
Smplkit.debug("registration", "periodic flush failed: #{e.class}: #{e.message}")
|
|
211
301
|
end
|
|
212
302
|
|
|
213
303
|
def final_flush
|
|
214
|
-
[@
|
|
215
|
-
|
|
304
|
+
[@platform.contexts, @flags, @logging.loggers, @config].each do |target|
|
|
305
|
+
target.flush
|
|
216
306
|
rescue StandardError => e
|
|
217
307
|
Smplkit.debug("registration", "final flush failed: #{e.class}: #{e.message}")
|
|
218
308
|
end
|
|
219
309
|
end
|
|
220
310
|
|
|
221
|
-
def register_service_context
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
311
|
+
def register_service_context
|
|
312
|
+
# Register the environment and/or service as context instances. Only the
|
|
313
|
+
# values that are set are registered; if neither environment nor service
|
|
314
|
+
# was provided the POST is skipped entirely (an audit/jobs-only customer
|
|
315
|
+
# has nothing to register).
|
|
226
316
|
contexts = []
|
|
227
|
-
contexts << Smplkit::Context.new("environment",
|
|
228
|
-
contexts << Smplkit::Context.new("service",
|
|
229
|
-
|
|
230
|
-
|
|
317
|
+
contexts << Smplkit::Context.new("environment", @environment) if @environment
|
|
318
|
+
contexts << Smplkit::Context.new("service", @service, { "name" => @service }) if @service
|
|
319
|
+
return if contexts.empty?
|
|
320
|
+
|
|
321
|
+
@platform.contexts.register(contexts, flush: true)
|
|
231
322
|
rescue StandardError => e
|
|
232
|
-
Smplkit.debug("lifecycle", "register service context failed (app: #{
|
|
323
|
+
Smplkit.debug("lifecycle", "register service context failed (app: #{@app_base_url}): #{e.class}: #{e.message}")
|
|
233
324
|
end
|
|
234
325
|
end
|
|
235
326
|
end
|