abmeter 0.2.4 → 0.3.0
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 +14 -0
- data/lib/abmeter/async_submitter.rb +82 -15
- data/lib/abmeter/core/assignment_config/experiment.rb +1 -0
- data/lib/abmeter/version.rb +1 -1
- data/lib/abmeter.rb +15 -2
- 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: 2d4ea3bdc7addec399a8644a3d8d2ee8f0329b4d85b117a56f4df51c58e04816
|
|
4
|
+
data.tar.gz: b03e52a1fe306fe68b7dd63879ff067e59f358bdf885d4a8905e86d743ba282d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18f1020d7926f3e7026441877010f730ef7ffc06078ad738a7be0121c9399465d3b056e39b6d0133f97d32fced489bec9a6f26c9d99f4f817e223b8927260eaf
|
|
7
|
+
data.tar.gz: 9952d1a56ed63530d25200b21d88b0aaf3aec868752b72c46029e5905af5ff20af1eb45c472323afa20251694949ba9df0ff611d13a1633d0a110546d1aa41a5
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A simple A/B testing client library for Ruby applications.
|
|
4
4
|
|
|
5
|
+
> **Breaking change in 0.3.0:** `ABMeter.reset!` now discards queued data without any network I/O; use `ABMeter.reset` for the previous drain-on-exit behavior.
|
|
6
|
+
|
|
5
7
|
## Supported Ruby versions
|
|
6
8
|
|
|
7
9
|
`abmeter` supports **Ruby 3.2 and newer**.
|
|
@@ -38,6 +40,18 @@ user = ABMeter.user(id: current_user.id, email: current_user.email)
|
|
|
38
40
|
ABMeter.event(`user_purchases_plan`, user, {plan: purchased_plan.name, price: purchased_plan.price})
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
## Shutdown
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# Graceful — bounded blocking, flushes pending exposures/events.
|
|
47
|
+
# Returns true on clean shutdown, false if the timeout elapsed.
|
|
48
|
+
ABMeter.reset(timeout: 5.0)
|
|
49
|
+
|
|
50
|
+
# Immediate — non-blocking, discards anything still queued.
|
|
51
|
+
# Use only when the process is exiting *right now* and you cannot afford I/O.
|
|
52
|
+
ABMeter.reset!
|
|
53
|
+
```
|
|
54
|
+
|
|
41
55
|
## Development
|
|
42
56
|
|
|
43
57
|
The gem uses [mise](https://mise.jdx.dev/) to pin Ruby (`mise.toml`). Pure-Ruby — no Postgres / Redis required.
|
|
@@ -4,6 +4,7 @@ module ABMeter
|
|
|
4
4
|
BATCH_SIZE = 100
|
|
5
5
|
MAX_SUBMIT_ATTEMPTS = 3
|
|
6
6
|
MAX_RETRY_QUEUE_SIZE = 1000
|
|
7
|
+
DEFAULT_SHUTDOWN_TIMEOUT = 5.0 # seconds — max time a graceful shutdown blocks before killing the worker
|
|
7
8
|
|
|
8
9
|
@queue = Queue.new
|
|
9
10
|
@retry_queue = []
|
|
@@ -12,6 +13,9 @@ module ABMeter
|
|
|
12
13
|
@worker_thread = nil
|
|
13
14
|
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
14
15
|
@logger = nil
|
|
16
|
+
@stopping = false
|
|
17
|
+
@stop_mutex = Mutex.new
|
|
18
|
+
@stop_signal = ConditionVariable.new
|
|
15
19
|
|
|
16
20
|
class << self
|
|
17
21
|
attr_reader :api_client, :flush_interval, :logger, :retry_queue
|
|
@@ -63,20 +67,47 @@ module ABMeter
|
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
# Graceful, lossless, bounded blocking. Signals the worker to stop, which
|
|
71
|
+
# triggers a final flush of the main and retry queues, then waits up to
|
|
72
|
+
# `timeout` seconds for that flush to finish and the worker to exit. Kills
|
|
73
|
+
# the worker (dropping anything still unflushed) if the timeout elapses.
|
|
74
|
+
# Returns true on clean shutdown, false on timeout.
|
|
75
|
+
def shutdown(timeout: DEFAULT_SHUTDOWN_TIMEOUT)
|
|
76
|
+
worker = @worker_thread
|
|
77
|
+
return true unless worker
|
|
78
|
+
|
|
79
|
+
request_stop
|
|
80
|
+
joined = worker.join(timeout)
|
|
81
|
+
if joined.nil?
|
|
82
|
+
worker.kill
|
|
83
|
+
worker.join
|
|
84
|
+
log_error("Graceful shutdown timed out after #{timeout}s, killing worker (queued: #{@queue.size}, retry: #{@retry_queue.size})")
|
|
85
|
+
end
|
|
86
|
+
!joined.nil?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Immediate, lossy, non-blocking. Kills the worker and discards queued
|
|
90
|
+
# items without any network I/O.
|
|
91
|
+
def shutdown!
|
|
67
92
|
@worker_thread&.kill
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
dropped_queue = @queue.size
|
|
94
|
+
dropped_retry = @retry_queue.size
|
|
95
|
+
if dropped_queue.positive? || dropped_retry.positive?
|
|
96
|
+
log_error("Immediate shutdown, dropping items (dropped_queue: #{dropped_queue}, dropped_retry: #{dropped_retry})")
|
|
97
|
+
end
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def reset(timeout: DEFAULT_SHUTDOWN_TIMEOUT)
|
|
102
|
+
result = shutdown(timeout: timeout)
|
|
103
|
+
clear_state
|
|
104
|
+
result
|
|
70
105
|
end
|
|
71
106
|
|
|
72
107
|
def reset!
|
|
73
|
-
shutdown
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@api_client = nil
|
|
77
|
-
@worker_thread = nil
|
|
78
|
-
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
79
|
-
@logger = nil
|
|
108
|
+
shutdown!
|
|
109
|
+
clear_state
|
|
110
|
+
true
|
|
80
111
|
end
|
|
81
112
|
|
|
82
113
|
def worker_alive?
|
|
@@ -92,17 +123,53 @@ module ABMeter
|
|
|
92
123
|
def start_worker
|
|
93
124
|
return if worker_alive?
|
|
94
125
|
|
|
126
|
+
@stopping = false
|
|
95
127
|
@worker_thread = Thread.new do
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
until @stopping
|
|
129
|
+
begin
|
|
130
|
+
wait_for_next_flush
|
|
131
|
+
flush unless @stopping
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
# Log error but keep worker running
|
|
134
|
+
log_error("Worker error: #{e.message}")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
# Final drain on graceful stop — bounded by the caller's join timeout.
|
|
138
|
+
begin
|
|
139
|
+
flush until @queue.empty? && @retry_queue.empty?
|
|
99
140
|
rescue StandardError => e
|
|
100
|
-
|
|
101
|
-
log_error("Worker error: #{e.message}")
|
|
141
|
+
log_error("Final flush error: #{e.message}")
|
|
102
142
|
end
|
|
103
143
|
end
|
|
104
144
|
end
|
|
105
145
|
|
|
146
|
+
# Sleeps for @flush_interval, waking immediately when request_stop
|
|
147
|
+
# signals. Re-checking @stopping under @stop_mutex closes the
|
|
148
|
+
# lost-wakeup window between the worker loop's flag check and the wait
|
|
149
|
+
# (a bare sleep + Thread#wakeup would drop a signal sent in that gap).
|
|
150
|
+
def wait_for_next_flush
|
|
151
|
+
@stop_mutex.synchronize do
|
|
152
|
+
@stop_signal.wait(@stop_mutex, @flush_interval) unless @stopping
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def request_stop
|
|
157
|
+
@stop_mutex.synchronize do
|
|
158
|
+
@stopping = true
|
|
159
|
+
@stop_signal.broadcast
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def clear_state
|
|
164
|
+
@queue = Queue.new
|
|
165
|
+
@retry_queue = []
|
|
166
|
+
@api_client = nil
|
|
167
|
+
@worker_thread = nil
|
|
168
|
+
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
169
|
+
@logger = nil
|
|
170
|
+
@stopping = false
|
|
171
|
+
end
|
|
172
|
+
|
|
106
173
|
def submit_exposures(exposures)
|
|
107
174
|
submit_batch(:exposure, exposures) { |exposures| @api_client.submit_exposures(exposures) }
|
|
108
175
|
end
|
data/lib/abmeter/version.rb
CHANGED
data/lib/abmeter.rb
CHANGED
|
@@ -64,12 +64,25 @@ module ABMeter
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# Graceful, lossless, bounded blocking. Flushes pending exposures/events
|
|
68
|
+
# before clearing state. Returns true on clean shutdown, false if the
|
|
69
|
+
# timeout elapsed and the worker had to be killed.
|
|
70
|
+
def reset(timeout: AsyncSubmitter::DEFAULT_SHUTDOWN_TIMEOUT)
|
|
71
|
+
result = AsyncSubmitter.reset(timeout: timeout)
|
|
72
|
+
@config = nil
|
|
73
|
+
@client = nil
|
|
74
|
+
@resolver_provider = nil
|
|
75
|
+
result
|
|
76
|
+
end
|
|
69
77
|
|
|
78
|
+
# Immediate, lossy, non-blocking. Kills the worker thread and discards
|
|
79
|
+
# queued exposures/events without any network I/O.
|
|
80
|
+
def reset!
|
|
81
|
+
AsyncSubmitter.reset!
|
|
70
82
|
@config = nil
|
|
71
83
|
@client = nil
|
|
72
84
|
@resolver_provider = nil
|
|
85
|
+
nil
|
|
73
86
|
end
|
|
74
87
|
|
|
75
88
|
def track_event(event_name, user_id, data)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abmeter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ABMeter
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|