forge_ops_tracker 0.16.0 → 0.17.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/CHANGELOG.md +20 -0
- data/lib/forge_ops_tracker/change_snapshot.rb +1 -1
- data/lib/forge_ops_tracker/client.rb +13 -9
- data/lib/forge_ops_tracker/delivery_queue.rb +13 -2
- data/lib/forge_ops_tracker/delivery_status.rb +74 -0
- data/lib/forge_ops_tracker/failure_event_buffer.rb +15 -4
- data/lib/forge_ops_tracker/infrastructure_metric_buffer.rb +8 -7
- data/lib/forge_ops_tracker/metric_buffer.rb +11 -9
- data/lib/forge_ops_tracker/performance_flusher.rb +5 -1
- data/lib/forge_ops_tracker/query_explainer.rb +2 -1
- data/lib/forge_ops_tracker/session_flusher.rb +4 -1
- data/lib/forge_ops_tracker/version.rb +1 -1
- data/lib/forge_ops_tracker.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 304c9a15155ecdf53fe8e181dd7f43b56a08d7397e7d63555e428185ece06560
|
|
4
|
+
data.tar.gz: d4d2cb88ffec650e579c7279c590e4eaee8f288bc6ef7041d2c2f5785767fa2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfd3a51d60e0decd113e2a8bd50db101ebd4ec57ebb468a522413b4f9ffa0c5c15d5440939c7a6f772561ce6ffe5c7078322d2903f0d3ec87006f1dd368c1718
|
|
7
|
+
data.tar.gz: 1646c0b506c357122c513604d031974318df0086a0c2a962c99e80143624be459755fab5dfff3df7718f24bb6caebe699ba5325fafb79b35a92f76ffa0783b63
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
- Fixed: a batch the server refused was re-sent at every flush for as long as the process lived,
|
|
6
|
+
with everything recorded since added to it. Performance samples, custom and infrastructure
|
|
7
|
+
metrics, session counts and failure events are now dropped when the server answers with a 4xx
|
|
8
|
+
other than 408 or 429 (a 400, 401, 403, 413 or 422, for example), since sending the same batch
|
|
9
|
+
again can only fail the same way. Once a metric buffer had filled up behind such a batch, every
|
|
10
|
+
new metric was dropped too; that no longer happens.
|
|
11
|
+
- A batch that could not be delivered for now (a network error, a timeout, a 5xx, a 408 or a 429)
|
|
12
|
+
is still kept for the next flush, but only for 5 flushes in a row. After the fifth it is dropped
|
|
13
|
+
and the buffer starts clean, so a long outage no longer holds the same data in memory forever.
|
|
14
|
+
- The failure event buffer now holds at most 1000 entries; further ones are dropped until a flush
|
|
15
|
+
lets go of them, the same way the metric buffers already behave.
|
|
16
|
+
- With a `config.logger`, each drop is logged once at debug with what was dropped and why, and a
|
|
17
|
+
non-2xx answer is logged with its status code.
|
|
18
|
+
- Internal: `ForgeOpsTracker::Client#deliver` and its sibling `deliver_*` methods now return
|
|
19
|
+
`:delivered`, `:rejected` or `:failed` instead of `true`/`false`. All three are truthy, so code
|
|
20
|
+
calling `Client` directly should compare against `:delivered`. Nothing in the documented API
|
|
21
|
+
changed.
|
|
22
|
+
|
|
3
23
|
## 0.16.0 (2026-10-02)
|
|
4
24
|
|
|
5
25
|
- Changed: outside Rails, the environment now defaults to `production` when nothing sets it (it
|
|
@@ -66,7 +66,7 @@ module ForgeOpsTracker
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def deliver
|
|
69
|
-
client.deliver_change_snapshot(payload)
|
|
69
|
+
DeliveryStatus.from(client.deliver_change_snapshot(payload)) == DeliveryStatus::DELIVERED
|
|
70
70
|
rescue StandardError => e
|
|
71
71
|
configuration.logger&.debug { "[ForgeOpsTracker] change snapshot failed: #{e.class}: #{e.message}" }
|
|
72
72
|
false
|
|
@@ -5,8 +5,10 @@ require "uri"
|
|
|
5
5
|
module ForgeOpsTracker
|
|
6
6
|
# Delivers one payload over HTTP. Every failure mode; DNS, connection,
|
|
7
7
|
# timeout, TLS, a non-2xx response; is caught here and turned into a
|
|
8
|
-
#
|
|
9
|
-
# unreachable tracker must never be able to break the host app.
|
|
8
|
+
# DeliveryStatus rather than a raised exception, since a broken or
|
|
9
|
+
# unreachable tracker must never be able to break the host app. Every
|
|
10
|
+
# deliver_* method answers one of DeliveryStatus::DELIVERED, REJECTED or
|
|
11
|
+
# FAILED (all three are truthy, so compare, never test for truth).
|
|
10
12
|
class Client
|
|
11
13
|
def initialize(configuration)
|
|
12
14
|
@configuration = configuration
|
|
@@ -16,8 +18,8 @@ module ForgeOpsTracker
|
|
|
16
18
|
post(configuration.ingestion_uri, payload)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
# Same delivery contract as deliver above (never raises, returns a
|
|
20
|
-
#
|
|
21
|
+
# Same delivery contract as deliver above (never raises, returns a
|
|
22
|
+
# DeliveryStatus), just a different endpoint under the same DSN's host and
|
|
21
23
|
# api_key; see Configuration#session_checkins_uri.
|
|
22
24
|
def deliver_session_checkin(payload)
|
|
23
25
|
post(configuration.session_checkins_uri, payload)
|
|
@@ -61,7 +63,7 @@ module ForgeOpsTracker
|
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
# Same delivery contract again; see Configuration#changes_uri and ForgeOpsTracker.record_change.
|
|
64
|
-
# One change per call. A plan without change tracking answers 403, which is
|
|
66
|
+
# One change per call. A plan without change tracking answers 403, which is a REJECTED here.
|
|
65
67
|
def deliver_change(payload)
|
|
66
68
|
post(configuration.changes_uri, payload)
|
|
67
69
|
end
|
|
@@ -72,7 +74,7 @@ module ForgeOpsTracker
|
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
# Same again; see Configuration#query_plans_uri and QueryExplainer. One plan per call. A plan
|
|
75
|
-
# without performance monitoring answers 403, which is
|
|
77
|
+
# without performance monitoring answers 403, which is a REJECTED here.
|
|
76
78
|
def deliver_query_plan(payload)
|
|
77
79
|
post(configuration.query_plans_uri, payload)
|
|
78
80
|
end
|
|
@@ -81,13 +83,15 @@ module ForgeOpsTracker
|
|
|
81
83
|
attr_reader :configuration
|
|
82
84
|
|
|
83
85
|
def post(uri, payload)
|
|
84
|
-
return
|
|
86
|
+
return DeliveryStatus::FAILED unless uri
|
|
85
87
|
|
|
86
88
|
response = http_for(uri).request(build_request(uri, payload))
|
|
87
|
-
response.
|
|
89
|
+
status = DeliveryStatus.for_code(response.code)
|
|
90
|
+
log { "delivery to #{uri.path} answered HTTP #{response.code}" } unless status == DeliveryStatus::DELIVERED
|
|
91
|
+
status
|
|
88
92
|
rescue StandardError => e
|
|
89
93
|
log { "delivery failed: #{e.class}: #{e.message}" }
|
|
90
|
-
|
|
94
|
+
DeliveryStatus::FAILED
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
def http_for(uri)
|
|
@@ -39,7 +39,7 @@ module ForgeOpsTracker
|
|
|
39
39
|
rescue ThreadError
|
|
40
40
|
break
|
|
41
41
|
end
|
|
42
|
-
|
|
42
|
+
deliver(payload)
|
|
43
43
|
end
|
|
44
44
|
sleep 0.01 while @in_flight.positive? && monotonic_now < deadline
|
|
45
45
|
rescue StandardError => e
|
|
@@ -78,7 +78,7 @@ module ForgeOpsTracker
|
|
|
78
78
|
payload = @queue.pop
|
|
79
79
|
@in_flight += 1
|
|
80
80
|
begin
|
|
81
|
-
|
|
81
|
+
deliver(payload)
|
|
82
82
|
ensure
|
|
83
83
|
@in_flight -= 1
|
|
84
84
|
end
|
|
@@ -87,6 +87,17 @@ module ForgeOpsTracker
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# One attempt per payload, as it has always been: a queued event is never held back for a
|
|
91
|
+
# retry, whatever the outcome, so all that is left to do with a refusal is to say so once.
|
|
92
|
+
def deliver(payload)
|
|
93
|
+
case DeliveryStatus.from(client.public_send(@deliver_with, payload))
|
|
94
|
+
when DeliveryStatus::REJECTED
|
|
95
|
+
log { "dropping event: the server rejected it and will not accept a retry" }
|
|
96
|
+
when DeliveryStatus::FAILED
|
|
97
|
+
log { "dropping event: delivery failed" }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
90
101
|
# Registered on the first push, not at load, so a process that never reports anything never
|
|
91
102
|
# waits on exit at all.
|
|
92
103
|
def register_drain
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module ForgeOpsTracker
|
|
2
|
+
# What became of one delivery attempt, as every Client#deliver_* method reports it. A bare
|
|
3
|
+
# true/false could not tell "the server will never take this payload" apart from "try again
|
|
4
|
+
# later", so a batch a plan or a validation rule refused was re-sent at every flush for as long
|
|
5
|
+
# as the process lived.
|
|
6
|
+
module DeliveryStatus
|
|
7
|
+
# Any 2xx.
|
|
8
|
+
DELIVERED = :delivered
|
|
9
|
+
# A 4xx other than 408 and 429: sending the same payload again can only fail the same way.
|
|
10
|
+
REJECTED = :rejected
|
|
11
|
+
# A network error, a timeout, a 5xx, a 408 or a 429: worth another attempt later.
|
|
12
|
+
FAILED = :failed
|
|
13
|
+
|
|
14
|
+
RETRYABLE_CLIENT_ERRORS = [ 408, 429 ].freeze
|
|
15
|
+
|
|
16
|
+
def self.for_code(code)
|
|
17
|
+
code = code.to_i
|
|
18
|
+
return DELIVERED if code.between?(200, 299)
|
|
19
|
+
return REJECTED if code.between?(400, 499) && !RETRYABLE_CLIENT_ERRORS.include?(code)
|
|
20
|
+
|
|
21
|
+
FAILED
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Reads whatever a client handed back. A status passes through; anything else is read the way
|
|
25
|
+
# the old boolean contract was (truthy delivered, falsy failed for now), so a client injected
|
|
26
|
+
# through a `client:` keyword that still answers true/false keeps working.
|
|
27
|
+
def self.from(result)
|
|
28
|
+
return result if [ DELIVERED, REJECTED, FAILED ].include?(result)
|
|
29
|
+
|
|
30
|
+
result ? DELIVERED : FAILED
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Decides, after each flush of one buffer/flusher, whether the batch it just sent is done with.
|
|
35
|
+
# Delivered and rejected batches are let go of at once; one that failed for now is kept, but only
|
|
36
|
+
# for MAX_FAILED_FLUSHES flushes in a row, so an outage (or a server answering 500 to one
|
|
37
|
+
# particular batch) can never pin the same data in memory for the life of the process.
|
|
38
|
+
class FlushOutcome
|
|
39
|
+
MAX_FAILED_FLUSHES = 5
|
|
40
|
+
|
|
41
|
+
# what names the batch in the debug log ("metrics", "performance samples").
|
|
42
|
+
def initialize(configuration, what)
|
|
43
|
+
@configuration = configuration
|
|
44
|
+
@what = what
|
|
45
|
+
@failed_flushes = 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# True when the caller should remove what it just sent (delivered, or dropped), false when it
|
|
49
|
+
# should keep it for the next flush.
|
|
50
|
+
def release?(result)
|
|
51
|
+
case DeliveryStatus.from(result)
|
|
52
|
+
when DeliveryStatus::DELIVERED
|
|
53
|
+
@failed_flushes = 0
|
|
54
|
+
true
|
|
55
|
+
when DeliveryStatus::REJECTED
|
|
56
|
+
@failed_flushes = 0
|
|
57
|
+
log { "dropping buffered #{@what}: the server rejected them and will not accept a retry" }
|
|
58
|
+
true
|
|
59
|
+
else
|
|
60
|
+
@failed_flushes += 1
|
|
61
|
+
return false if @failed_flushes < MAX_FAILED_FLUSHES
|
|
62
|
+
|
|
63
|
+
@failed_flushes = 0
|
|
64
|
+
log { "dropping buffered #{@what}: #{MAX_FAILED_FLUSHES} flushes in a row failed" }
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
def log
|
|
71
|
+
@configuration.logger&.debug { "[ForgeOpsTracker] #{yield}" }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -8,12 +8,17 @@ module ForgeOpsTracker
|
|
|
8
8
|
# kind of thing worth an accurate individual record of, not something to pre-aggregate away the
|
|
9
9
|
# way PerformanceFlusher's own count/sum/max buckets duration.
|
|
10
10
|
class FailureEventBuffer
|
|
11
|
+
# Same cap, and the same "drop the newest, not the oldest" reasoning, as MetricBuffer's own
|
|
12
|
+
# MAX_ENTRIES: a burst of failures during an outage must not grow this without bound.
|
|
13
|
+
MAX_ENTRIES = 1000
|
|
14
|
+
|
|
11
15
|
def initialize(configuration, client: Client.new(configuration))
|
|
12
16
|
@configuration = configuration
|
|
13
17
|
@client = client
|
|
14
18
|
@mutex = Mutex.new
|
|
15
19
|
@entries = []
|
|
16
20
|
@thread = nil
|
|
21
|
+
@outcome = FlushOutcome.new(configuration, "failure events")
|
|
17
22
|
|
|
18
23
|
# Flushes whatever's already buffered on a normal process exit, so the last partial window
|
|
19
24
|
# isn't silently dropped; same reasoning every other buffer/flusher in this gem documents.
|
|
@@ -24,6 +29,8 @@ module ForgeOpsTracker
|
|
|
24
29
|
ensure_worker_started
|
|
25
30
|
|
|
26
31
|
@mutex.synchronize do
|
|
32
|
+
return false if @entries.size >= MAX_ENTRIES
|
|
33
|
+
|
|
27
34
|
@entries << {
|
|
28
35
|
kind: kind, transaction_name: transaction_name, error_class: error_class, error_message: error_message,
|
|
29
36
|
environment: configuration.environment.to_s, release: configuration.release,
|
|
@@ -33,18 +40,22 @@ module ForgeOpsTracker
|
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# Snapshots and resets the buffered entries, then delivers them as one batch. Same "a failed
|
|
36
|
-
# delivery keeps every entry for the next attempt" contract as MetricBuffer#flush
|
|
43
|
+
# delivery keeps every entry for the next attempt" contract as MetricBuffer#flush, with the
|
|
44
|
+
# same two exceptions: a rejected batch, and one that failed FlushOutcome::MAX_FAILED_FLUSHES
|
|
45
|
+
# flushes in a row, are dropped.
|
|
37
46
|
def flush
|
|
38
47
|
snapshot = nil
|
|
39
48
|
|
|
40
49
|
@mutex.synchronize do
|
|
41
50
|
return if @entries.empty?
|
|
42
|
-
snapshot = @entries
|
|
51
|
+
snapshot = @entries.dup
|
|
43
52
|
end
|
|
44
53
|
|
|
45
|
-
return unless client.deliver_failure_events(snapshot)
|
|
54
|
+
return unless @outcome.release?(client.deliver_failure_events(snapshot))
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
# Exactly the entries just sent: anything recorded while the request was in flight sits after
|
|
57
|
+
# them and stays for the next flush.
|
|
58
|
+
@mutex.synchronize { @entries.shift(snapshot.size) }
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
private
|
|
@@ -3,16 +3,16 @@ require "thread"
|
|
|
3
3
|
module ForgeOpsTracker
|
|
4
4
|
# Collects individual ForgeOpsTracker.capture_infrastructure_metric readings in-process and
|
|
5
5
|
# periodically flushes them as one batch, same shape as MetricBuffer (a sibling class, not a
|
|
6
|
-
# shared base
|
|
6
|
+
# shared base; this gem keeps SessionFlusher/PerformanceFlusher as separate, independently-
|
|
7
7
|
# readable classes despite their own similar shape too, and these two follow that precedent).
|
|
8
8
|
# Its main real caller is a short-lived cron/script process that exits right after a handful of
|
|
9
9
|
# capture calls, so the at_exit flush below is what actually matters in practice; the periodic
|
|
10
10
|
# background-thread timer still runs for a long-lived process that chooses to call this
|
|
11
11
|
# continuously instead, but rarely gets the chance to fire in the cron case.
|
|
12
12
|
class InfrastructureMetricBuffer
|
|
13
|
-
# Once this many entries are buffered, further ones are dropped until a flush
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# Once this many entries are buffered, further ones are dropped until a flush lets go of them:
|
|
14
|
+
# an unreachable server fails every flush, and an uncapped buffer would keep growing until
|
|
15
|
+
# FlushOutcome gives up on the batch. Dropping the newest rather than the oldest keeps the entries a flush
|
|
16
16
|
# is delivering at the front of the array, which is what makes removing exactly them exact.
|
|
17
17
|
MAX_ENTRIES = 1000
|
|
18
18
|
|
|
@@ -22,6 +22,7 @@ module ForgeOpsTracker
|
|
|
22
22
|
@mutex = Mutex.new
|
|
23
23
|
@entries = []
|
|
24
24
|
@thread = nil
|
|
25
|
+
@outcome = FlushOutcome.new(configuration, "infrastructure metrics")
|
|
25
26
|
|
|
26
27
|
at_exit { flush }
|
|
27
28
|
end
|
|
@@ -38,7 +39,7 @@ module ForgeOpsTracker
|
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
# Same reset-on-success/keep-on-failure contract as MetricBuffer#flush; see that class's own
|
|
42
|
+
# Same reset-on-success/keep-on-failure/drop-on-rejection contract as MetricBuffer#flush; see that class's own
|
|
42
43
|
# comment for the accepted race this shares with it.
|
|
43
44
|
def flush
|
|
44
45
|
snapshot = nil
|
|
@@ -48,9 +49,9 @@ module ForgeOpsTracker
|
|
|
48
49
|
snapshot = @entries.dup
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
return unless client.deliver_infrastructure_metrics(snapshot)
|
|
52
|
+
return unless @outcome.release?(client.deliver_infrastructure_metrics(snapshot))
|
|
52
53
|
|
|
53
|
-
# Exactly the entries just
|
|
54
|
+
# Exactly the entries just sent: anything recorded while the request was in flight sits
|
|
54
55
|
# after them and stays for the next flush (resetting the whole array here lost it).
|
|
55
56
|
@mutex.synchronize { @entries.shift(snapshot.size) }
|
|
56
57
|
end
|
|
@@ -10,9 +10,9 @@ module ForgeOpsTracker
|
|
|
10
10
|
# they'll want a genuinely accurate count/sum of later, not something to pre-aggregate away
|
|
11
11
|
# client-side, so CustomMetricsController stores one row per entry as-is.
|
|
12
12
|
class MetricBuffer
|
|
13
|
-
# Once this many entries are buffered, further ones are dropped until a flush
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# Once this many entries are buffered, further ones are dropped until a flush lets go of them:
|
|
14
|
+
# an unreachable server fails every flush, and an uncapped buffer would keep growing until
|
|
15
|
+
# FlushOutcome gives up on the batch. Dropping the newest rather than the oldest keeps the entries a flush
|
|
16
16
|
# is delivering at the front of the array, which is what makes removing exactly them exact.
|
|
17
17
|
MAX_ENTRIES = 1000
|
|
18
18
|
|
|
@@ -22,6 +22,7 @@ module ForgeOpsTracker
|
|
|
22
22
|
@mutex = Mutex.new
|
|
23
23
|
@entries = []
|
|
24
24
|
@thread = nil
|
|
25
|
+
@outcome = FlushOutcome.new(configuration, "metrics")
|
|
25
26
|
|
|
26
27
|
# Flushes whatever's already buffered on a normal process exit, so the last partial window
|
|
27
28
|
# isn't silently dropped; same reasoning every other buffer/flusher in this gem documents.
|
|
@@ -44,10 +45,11 @@ module ForgeOpsTracker
|
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
# Snapshots and resets the buffered entries, then delivers them as one batch. A
|
|
48
|
-
#
|
|
49
|
-
# just grows instead of losing what was already buffered
|
|
50
|
-
#
|
|
48
|
+
# Snapshots and resets the buffered entries, then delivers them as one batch. A delivery that
|
|
49
|
+
# failed for now keeps every entry where it is rather than resetting, so the next flush's batch
|
|
50
|
+
# just grows instead of losing what was already buffered (a rejected batch, or one that failed
|
|
51
|
+
# FlushOutcome::MAX_FAILED_FLUSHES flushes in a row, is dropped instead); same reasoning
|
|
52
|
+
# SessionFlusher#flush already documents, including the same small, accepted race (an entry recorded during the
|
|
51
53
|
# in-flight HTTP request gets folded into this snapshot's own reset rather than kept for the
|
|
52
54
|
# next one) that class already lives with, for the same reason: simple and consistent beats a
|
|
53
55
|
# cleverer dedup that would only trade one rare edge case for a subtler one.
|
|
@@ -59,9 +61,9 @@ module ForgeOpsTracker
|
|
|
59
61
|
snapshot = @entries.dup
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
return unless client.deliver_metrics(snapshot)
|
|
64
|
+
return unless @outcome.release?(client.deliver_metrics(snapshot))
|
|
63
65
|
|
|
64
|
-
# Exactly the entries just
|
|
66
|
+
# Exactly the entries just sent: anything recorded while the request was in flight sits
|
|
65
67
|
# after them and stays for the next flush (resetting the whole array here lost it).
|
|
66
68
|
@mutex.synchronize { @entries.shift(snapshot.size) }
|
|
67
69
|
end
|
|
@@ -17,6 +17,7 @@ module ForgeOpsTracker
|
|
|
17
17
|
@buckets = {}
|
|
18
18
|
@period_started_at = Time.now.utc
|
|
19
19
|
@thread = nil
|
|
20
|
+
@outcome = FlushOutcome.new(configuration, "performance samples")
|
|
20
21
|
|
|
21
22
|
# Flushes whatever's already tallied on a normal process exit, so the last partial window
|
|
22
23
|
# (anything shorter than a full performance_flush_interval) isn't silently dropped; same
|
|
@@ -48,6 +49,9 @@ module ForgeOpsTracker
|
|
|
48
49
|
# just grows instead of losing what was already tallied; same reasoning SessionFlusher#flush
|
|
49
50
|
# already documents, and the same reason PerformanceSamplesController accepts a batch rather
|
|
50
51
|
# than a single row: a failed flush shouldn't have to re-deliver by transaction one at a time.
|
|
52
|
+
# That only holds for a delivery that failed for now, and only FlushOutcome::MAX_FAILED_FLUSHES
|
|
53
|
+
# times in a row: a batch the server rejected, or one that keeps failing, is removed exactly
|
|
54
|
+
# the way a delivered one is, since re-sending it could never succeed.
|
|
51
55
|
#
|
|
52
56
|
# Only exactly what this snapshot delivered is removed afterward (subtracted from whatever is in
|
|
53
57
|
# each bucket by then), never the whole hash reset: #record can run on another thread while the
|
|
@@ -84,7 +88,7 @@ module ForgeOpsTracker
|
|
|
84
88
|
end
|
|
85
89
|
end
|
|
86
90
|
|
|
87
|
-
return unless snapshot && client.deliver_performance_samples(snapshot)
|
|
91
|
+
return unless snapshot && @outcome.release?(client.deliver_performance_samples(snapshot))
|
|
88
92
|
|
|
89
93
|
@mutex.synchronize do
|
|
90
94
|
sent.each do |key, delivered|
|
|
@@ -135,10 +135,11 @@ module ForgeOpsTracker
|
|
|
135
135
|
return false
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
client.deliver_query_plan(
|
|
138
|
+
result = client.deliver_query_plan(
|
|
139
139
|
statement: job.statement, db_system: "postgresql", plan: masked_plan,
|
|
140
140
|
duration_ms: job.duration_ms, environment: configuration.environment, captured_at: job.captured_at
|
|
141
141
|
)
|
|
142
|
+
DeliveryStatus.from(result) == DeliveryStatus::DELIVERED
|
|
142
143
|
rescue StandardError => e
|
|
143
144
|
log { "EXPLAIN failed: #{e.class}: #{e.message}" }
|
|
144
145
|
false
|
|
@@ -16,6 +16,7 @@ module ForgeOpsTracker
|
|
|
16
16
|
@crashed_sessions_count = 0
|
|
17
17
|
@period_started_at = Time.now.utc
|
|
18
18
|
@thread = nil
|
|
19
|
+
@outcome = FlushOutcome.new(configuration, "session counts")
|
|
19
20
|
|
|
20
21
|
# Flushes whatever's already tallied on a normal process exit, so the last partial window
|
|
21
22
|
# (anything shorter than a full session_flush_interval) isn't silently dropped.
|
|
@@ -34,6 +35,8 @@ module ForgeOpsTracker
|
|
|
34
35
|
# Snapshots and resets the in-process counters, then delivers them. A failed delivery keeps
|
|
35
36
|
# the counts where they are rather than resetting, so the next flush's window just grows
|
|
36
37
|
# instead of losing what was already tallied; there's no other copy of this data anywhere.
|
|
38
|
+
# Counts the server rejected, or that failed FlushOutcome::MAX_FAILED_FLUSHES flushes in a
|
|
39
|
+
# row, are reset all the same: sending them again could never succeed.
|
|
37
40
|
def flush
|
|
38
41
|
snapshot = nil
|
|
39
42
|
|
|
@@ -50,7 +53,7 @@ module ForgeOpsTracker
|
|
|
50
53
|
}
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
return unless snapshot && client.deliver_session_checkin(snapshot)
|
|
56
|
+
return unless snapshot && @outcome.release?(client.deliver_session_checkin(snapshot))
|
|
54
57
|
|
|
55
58
|
@mutex.synchronize do
|
|
56
59
|
@sessions_count = 0
|
data/lib/forge_ops_tracker.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "forge_ops_tracker/configuration"
|
|
|
4
4
|
require "forge_ops_tracker/pii_scrubber"
|
|
5
5
|
require "forge_ops_tracker/sql_statement"
|
|
6
6
|
require "forge_ops_tracker/event_builder"
|
|
7
|
+
require "forge_ops_tracker/delivery_status"
|
|
7
8
|
require "forge_ops_tracker/client"
|
|
8
9
|
require "forge_ops_tracker/delivery_queue"
|
|
9
10
|
require "forge_ops_tracker/change"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forge_ops_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ForgeOps
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/forge_ops_tracker/client.rb
|
|
110
110
|
- lib/forge_ops_tracker/configuration.rb
|
|
111
111
|
- lib/forge_ops_tracker/delivery_queue.rb
|
|
112
|
+
- lib/forge_ops_tracker/delivery_status.rb
|
|
112
113
|
- lib/forge_ops_tracker/error_subscriber.rb
|
|
113
114
|
- lib/forge_ops_tracker/event_builder.rb
|
|
114
115
|
- lib/forge_ops_tracker/failure_event_buffer.rb
|