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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1eb0a27dc233c6009a4a78103adb88ddc76aa813dd53e1f5905e419f028f4d11
4
- data.tar.gz: 511c286faf085250e00bf5fb7ce4f45bfaac44731a60d1a1c92779095e758147
3
+ metadata.gz: ef260933f908739609343cb6b72e5fbbe75a01d0c93dd8fba5d1c4983e807f26
4
+ data.tar.gz: 22b576f3c302378c1ca2e8075893c3c4b9404329f6dacc22c281ad4b7c31f688
5
5
  SHA512:
6
- metadata.gz: 1663b0475259d7bd30b5dd5718dbccc38bd53e1b4494a6574d4159229943f9a789754549171bee406134def631524733f4bb9e01e901ff552f3ad778f15f5599
7
- data.tar.gz: 8a32c328155045a7c19049bba52478dbd2b806b6cf875af3d83cf7a05dfc63d1eda59ae36d66dcd19a4d5494ca8613b13a62620bddbf13fd57d08f6d297927d3
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 to `History.max_entries`
10
- # independently on every write, oldest first.
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, max)
35
- write_and_trim(c, LIST_KEYS.fetch(status), raw, finished_at, max)
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, max)
66
+ def write_and_trim(conn, key, raw, score)
53
67
  conn.zadd(key, score, raw)
54
- conn.zremrangebyrank(key, 0, -(max + 1))
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. Reads the shared
60
- # "all" list once (rather than "success" and "failed" separately) since
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
- # Same caveat as any other read of this list: only entries still
66
- # within `History.max_entries` are counted, so a very busy queue's
67
- # oldest requested days may already have been trimmed away.
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
- since = Time.now.to_f - (days * 86_400)
70
- Cogworker.config.redis do |c|
71
- c.zrangebyscore(LIST_KEYS.fetch('all'), since, '+inf').each_with_object({}) do |raw, counts|
72
- entry = JSON.parse(raw)
73
- day = Time.at(entry['finished_at']).utc.strftime('%Y-%m-%d')
74
- bucket = (counts[day] ||= { 'success' => 0, 'failed' => 0 })
75
- bucket[entry['status']] += 1 if bucket.key?(entry['status'])
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
@@ -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
- # length-capped log meant for browsing/auditing past runs.
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
- DEFAULT_MAX_ENTRIES = 1000
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 entries are retained (oldest trimmed first), independently
14
- # for the "all", "success", and "failed" lists — read fresh on every
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 = 5000`.
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, max_entries: DEFAULT_MAX_ENTRIES)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cogworker
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cogworker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - vdonec