cogworker 0.2.0 → 0.2.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/lib/cogworker/history/storage.rb +58 -22
- data/lib/cogworker/history.rb +57 -7
- data/lib/cogworker/redis_keys.rb +1 -0
- data/lib/cogworker/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: ef260933f908739609343cb6b72e5fbbe75a01d0c93dd8fba5d1c4983e807f26
|
|
4
|
+
data.tar.gz: 22b576f3c302378c1ca2e8075893c3c4b9404329f6dacc22c281ad4b7c31f688
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a48c5530445af6a80846ef5313202f88a3638ecdb33614e7f86baba87c33c4e214add29febf257e025cdf1921ad76bc35140cd91828101aa556a4fb0a461e662
|
|
7
|
+
data.tar.gz: f6b2cdaef9cf94adb3823d29cffca99d50697a7553dc6178357fb6cf435ba8fb82320be291e1b04bd5075ab05ad2cf86cb5b2ed67a4f6420e20283360ec399f0
|
|
@@ -6,8 +6,14 @@ module Cogworker
|
|
|
6
6
|
module History
|
|
7
7
|
# Three parallel ZSETs (score = finished_at), so any of the three
|
|
8
8
|
# filters (all/success/failed) is a plain `ZREVRANGE` — no per-request
|
|
9
|
-
# `ZUNIONSTORE`/scan needed. Each is trimmed
|
|
10
|
-
#
|
|
9
|
+
# `ZUNIONSTORE`/scan needed. Each is trimmed independently on every
|
|
10
|
+
# write, oldest first, by age (`History.retention_days`, the primary,
|
|
11
|
+
# expected limit) and then by count (`History.max_entries`, a safety
|
|
12
|
+
# ceiling only — see both constants' own comments in `history.rb`) —
|
|
13
|
+
# see `write_and_trim`. Alongside those, `record` also maintains one
|
|
14
|
+
# small per-UTC-day Hash counter (`daily_counts` reads these back) —
|
|
15
|
+
# kept deliberately separate from, and unaffected by, either trim above
|
|
16
|
+
# (see `daily_counts`' own comment).
|
|
11
17
|
module Storage
|
|
12
18
|
LIST_KEYS = {
|
|
13
19
|
'all' => 'cogworker:history:all',
|
|
@@ -29,13 +35,21 @@ module Cogworker
|
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
raw = JSON.generate(entry)
|
|
32
|
-
max = Cogworker::History.max_entries
|
|
33
38
|
Cogworker.config.redis do |c|
|
|
34
|
-
write_and_trim(c, LIST_KEYS.fetch('all'), raw, finished_at
|
|
35
|
-
write_and_trim(c, LIST_KEYS.fetch(status), raw, finished_at
|
|
39
|
+
write_and_trim(c, LIST_KEYS.fetch('all'), raw, finished_at)
|
|
40
|
+
write_and_trim(c, LIST_KEYS.fetch(status), raw, finished_at)
|
|
41
|
+
# Independent of the two trims above — see `daily_counts`' own
|
|
42
|
+
# comment for why "Runs per day" can't just read the `all` list.
|
|
43
|
+
bump_daily_count(c, finished_at, status)
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
|
|
47
|
+
def bump_daily_count(conn, finished_at, status)
|
|
48
|
+
daily_key = RedisKeys.history_daily_bucket(Time.at(finished_at).utc.strftime('%Y-%m-%d'))
|
|
49
|
+
conn.hincrby(daily_key, status, 1)
|
|
50
|
+
conn.expire(daily_key, Cogworker::History.daily_stats_retention_days * 86_400)
|
|
51
|
+
end
|
|
52
|
+
|
|
39
53
|
# Newest-first page of `count`/`status` entries: `[entries, total]`.
|
|
40
54
|
def page(status, page_number, per_page)
|
|
41
55
|
key = LIST_KEYS.fetch(status, LIST_KEYS.fetch('all'))
|
|
@@ -49,33 +63,55 @@ module Cogworker
|
|
|
49
63
|
end
|
|
50
64
|
end
|
|
51
65
|
|
|
52
|
-
def write_and_trim(conn, key, raw, score
|
|
66
|
+
def write_and_trim(conn, key, raw, score)
|
|
53
67
|
conn.zadd(key, score, raw)
|
|
54
|
-
|
|
68
|
+
# Age first — the primary, expected trim in normal operation: gone
|
|
69
|
+
# once older than `History.retention_days`, regardless of how few
|
|
70
|
+
# entries that leaves. Count second, as a safety ceiling only — see
|
|
71
|
+
# `History::DEFAULT_MAX_ENTRIES`'s own comment for why both run on
|
|
72
|
+
# every write rather than just one. Both read fresh from
|
|
73
|
+
# `Cogworker::History` here (rather than being passed in) so a
|
|
74
|
+
# config change takes effect on the very next write, same as
|
|
75
|
+
# `max_entries` always has.
|
|
76
|
+
cutoff = Time.now.to_f - (Cogworker::History.retention_days * 86_400)
|
|
77
|
+
conn.zremrangebyscore(key, '-inf', cutoff)
|
|
78
|
+
conn.zremrangebyrank(key, 0, -(Cogworker::History.max_entries + 1))
|
|
55
79
|
end
|
|
56
80
|
|
|
57
81
|
# Success/failed counts per UTC calendar day, for the last `days` days
|
|
58
82
|
# (today included) — `{ 'YYYY-MM-DD' => { 'success' => n, 'failed' => n } }`,
|
|
59
|
-
# a day with no entries simply absent from the Hash.
|
|
60
|
-
#
|
|
61
|
-
# every entry already carries its own `status`. Bucketing uses UTC,
|
|
62
|
-
# not the viewer's browser timezone (unlike `Layout.time_tag`
|
|
83
|
+
# a day with no entries simply absent from the Hash. Bucketing uses
|
|
84
|
+
# UTC, not the viewer's browser timezone (unlike `Layout.time_tag`
|
|
63
85
|
# elsewhere) — this is a server-rendered daily aggregate, not a single
|
|
64
86
|
# instant, so there's no one "browser day" to convert into.
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
87
|
+
#
|
|
88
|
+
# Reads `RedisKeys.history_daily_bucket(day)` (one small Hash per day,
|
|
89
|
+
# `record` above keeps them updated) rather than scanning the `all`
|
|
90
|
+
# list and bucketing entries by hand — deliberately: even now that the
|
|
91
|
+
# `all`/`success`/`failed` lists are trimmed by age (`History.
|
|
92
|
+
# retention_days`) rather than purely by count, that's still a
|
|
93
|
+
# *different*, independently configured window than the daily
|
|
94
|
+
# buckets' own (`History.daily_stats_retention_days`, TTL-based, same
|
|
95
|
+
# idea as `Cogworker::Throughput`'s hourly buckets) — a "6 months"
|
|
96
|
+
# chart request would find nothing if it depended on `all` still
|
|
97
|
+
# holding 6 months of entries, since `retention_days` defaults to
|
|
98
|
+
# far less than that. The daily buckets are sized (and meant) to
|
|
99
|
+
# outlive the raw per-entry lists, so "Runs per day" stays accurate
|
|
100
|
+
# regardless of how short `retention_days` is configured.
|
|
68
101
|
def daily_counts(days)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
102
|
+
now = Time.now.utc
|
|
103
|
+
day_strings = (days - 1).downto(0).map { |offset| (now - (offset * 86_400)).strftime('%Y-%m-%d') }
|
|
104
|
+
raw = Cogworker.config.redis { |c| c.pipelined { |pipe| day_strings.each { |d| daily_hmget(pipe, d) } } }
|
|
105
|
+
day_strings.zip(raw).each_with_object({}) do |(day, (success, failed)), counts|
|
|
106
|
+
next if success.nil? && failed.nil?
|
|
107
|
+
|
|
108
|
+
counts[day] = { 'success' => success.to_i, 'failed' => failed.to_i }
|
|
77
109
|
end
|
|
78
110
|
end
|
|
111
|
+
|
|
112
|
+
def daily_hmget(pipe, day)
|
|
113
|
+
pipe.hmget(RedisKeys.history_daily_bucket(day), 'success', 'failed')
|
|
114
|
+
end
|
|
79
115
|
end
|
|
80
116
|
end
|
|
81
117
|
end
|
data/lib/cogworker/history.rb
CHANGED
|
@@ -4,29 +4,79 @@ module Cogworker
|
|
|
4
4
|
# Execution history: every job run (success and failure alike) is recorded
|
|
5
5
|
# with its full args, timing, and — on failure — error class/message/
|
|
6
6
|
# backtrace. Distinct from the Status layer (`Cogworker::Status`), which
|
|
7
|
-
# tracks *current* per-jid state with a TTL; History is an append-only
|
|
8
|
-
#
|
|
7
|
+
# tracks *current* per-jid state with a TTL; History is an append-only log
|
|
8
|
+
# meant for browsing/auditing past runs, trimmed by age first.
|
|
9
9
|
module History
|
|
10
|
-
|
|
10
|
+
# The primary, expected trim for the "all"/"success"/"failed" lists —
|
|
11
|
+
# anything older than this many days is gone on the next write,
|
|
12
|
+
# regardless of how few (or many) entries that leaves. See `Storage.
|
|
13
|
+
# write_and_trim`.
|
|
14
|
+
DEFAULT_RETENTION_DAYS = 30
|
|
15
|
+
# A safety ceiling, not the everyday trim mechanism — `retention_days`
|
|
16
|
+
# above is. Each entry in the lists it bounds carries its full args and,
|
|
17
|
+
# on failure, up to 200 backtrace lines, so a burst of traffic under a
|
|
18
|
+
# purely time-based trim could otherwise grow Redis memory unboundedly
|
|
19
|
+
# until those entries finally age out; this caps that. Sized well above
|
|
20
|
+
# what `retention_days`' default window is expected to hold in normal
|
|
21
|
+
# operation specifically so it stays a rare safety net rather than the
|
|
22
|
+
# thing actually doing the trimming day to day — if it's the one
|
|
23
|
+
# regularly cutting entries short of `retention_days`, raise it (or
|
|
24
|
+
# lower `retention_days`) to match this queue's real volume.
|
|
25
|
+
DEFAULT_MAX_ENTRIES = 50_000
|
|
26
|
+
# "Runs per day" (`Storage.daily_counts`) reads a separate, per-UTC-day
|
|
27
|
+
# counter (`RedisKeys.history_daily_bucket`) rather than the capped
|
|
28
|
+
# `all`/`success`/`failed` lists above, specifically so those lists'
|
|
29
|
+
# own trim (age- or count-based) doesn't also blow away the daily
|
|
30
|
+
# chart's older days — see `Storage`'s own comment. Each day's counter
|
|
31
|
+
# self-expires via `EXPIRE`, the same TTL-based cleanup `Cogworker::
|
|
32
|
+
# Throughput` already uses for its hourly buckets, rather than a
|
|
33
|
+
# count-based trim: 400 days comfortably covers the widest period the
|
|
34
|
+
# built-in chart offers (6 months / 182 days) with real margin, so a
|
|
35
|
+
# viewer opening that period on day 1 of a new retention window still
|
|
36
|
+
# sees its oldest days.
|
|
37
|
+
DEFAULT_DAILY_STATS_RETENTION_DAYS = 400
|
|
11
38
|
|
|
12
39
|
class << self
|
|
13
|
-
# How many
|
|
14
|
-
#
|
|
40
|
+
# How many days an entry survives in the "all"/"success"/"failed"
|
|
41
|
+
# lists before `write_and_trim` removes it — read fresh on every
|
|
15
42
|
# write, so changing it takes effect immediately, no need to rebuild
|
|
16
43
|
# the middleware chain. Configurable via
|
|
44
|
+
# `configure_server_middleware(config, retention_days: N)`, or
|
|
45
|
+
# directly: `Cogworker::History.retention_days = 90`.
|
|
46
|
+
attr_writer :retention_days
|
|
47
|
+
# The count-based safety ceiling alongside `retention_days` above —
|
|
48
|
+
# see `DEFAULT_MAX_ENTRIES`. Configurable via
|
|
17
49
|
# `configure_server_middleware(config, max_entries: N)`, or directly:
|
|
18
|
-
# `Cogworker::History.max_entries =
|
|
50
|
+
# `Cogworker::History.max_entries = 50_000`.
|
|
19
51
|
attr_writer :max_entries
|
|
52
|
+
# How many days a "Runs per day" daily counter survives before
|
|
53
|
+
# self-expiring — independent of `retention_days`/`max_entries` above
|
|
54
|
+
# (see `DEFAULT_DAILY_STATS_RETENTION_DAYS`). Configurable via
|
|
55
|
+
# `configure_server_middleware(config, daily_stats_retention_days: N)`,
|
|
56
|
+
# or directly: `Cogworker::History.daily_stats_retention_days = 800`.
|
|
57
|
+
attr_writer :daily_stats_retention_days
|
|
58
|
+
|
|
59
|
+
def retention_days
|
|
60
|
+
@retention_days ||= DEFAULT_RETENTION_DAYS
|
|
61
|
+
end
|
|
20
62
|
|
|
21
63
|
def max_entries
|
|
22
64
|
@max_entries ||= DEFAULT_MAX_ENTRIES
|
|
23
65
|
end
|
|
66
|
+
|
|
67
|
+
def daily_stats_retention_days
|
|
68
|
+
@daily_stats_retention_days ||= DEFAULT_DAILY_STATS_RETENTION_DAYS
|
|
69
|
+
end
|
|
24
70
|
end
|
|
25
71
|
|
|
26
72
|
module_function
|
|
27
73
|
|
|
28
|
-
def configure_server_middleware(config,
|
|
74
|
+
def configure_server_middleware(config, retention_days: DEFAULT_RETENTION_DAYS,
|
|
75
|
+
max_entries: DEFAULT_MAX_ENTRIES,
|
|
76
|
+
daily_stats_retention_days: DEFAULT_DAILY_STATS_RETENTION_DAYS)
|
|
77
|
+
self.retention_days = retention_days
|
|
29
78
|
self.max_entries = max_entries
|
|
79
|
+
self.daily_stats_retention_days = daily_stats_retention_days
|
|
30
80
|
config.server_middleware { |chain| chain.add(Middleware) }
|
|
31
81
|
end
|
|
32
82
|
end
|
data/lib/cogworker/redis_keys.rb
CHANGED
|
@@ -32,5 +32,6 @@ module Cogworker
|
|
|
32
32
|
def periodic_lock(pjid, slot) = "periodic:lock:#{pjid}:#{slot}"
|
|
33
33
|
def unique_lock(digest) = "cogworker:unique:#{digest}"
|
|
34
34
|
def throughput_bucket(hour) = "cogworker:throughput:#{hour}"
|
|
35
|
+
def history_daily_bucket(day) = "cogworker:history:daily:#{day}"
|
|
35
36
|
end
|
|
36
37
|
end
|
data/lib/cogworker/version.rb
CHANGED