roundhouse_ui 0.1.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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +166 -0
  4. data/Rakefile +3 -0
  5. data/app/assets/javascripts/roundhouse_ui/turbo.min.js +35 -0
  6. data/app/assets/stylesheets/roundhouse_ui/application.css +15 -0
  7. data/app/controllers/concerns/roundhouse_ui/job_set_browsing.rb +41 -0
  8. data/app/controllers/roundhouse_ui/application_controller.rb +75 -0
  9. data/app/controllers/roundhouse_ui/assets_controller.rb +16 -0
  10. data/app/controllers/roundhouse_ui/audit_controller.rb +7 -0
  11. data/app/controllers/roundhouse_ui/busy_controller.rb +29 -0
  12. data/app/controllers/roundhouse_ui/capsules_controller.rb +27 -0
  13. data/app/controllers/roundhouse_ui/dashboard_controller.rb +26 -0
  14. data/app/controllers/roundhouse_ui/dead_controller.rb +46 -0
  15. data/app/controllers/roundhouse_ui/errors_controller.rb +50 -0
  16. data/app/controllers/roundhouse_ui/jobs_controller.rb +94 -0
  17. data/app/controllers/roundhouse_ui/metrics_controller.rb +8 -0
  18. data/app/controllers/roundhouse_ui/queues_controller.rb +40 -0
  19. data/app/controllers/roundhouse_ui/redis_controller.rb +21 -0
  20. data/app/controllers/roundhouse_ui/retries_controller.rb +34 -0
  21. data/app/controllers/roundhouse_ui/scheduled_controller.rb +34 -0
  22. data/app/controllers/roundhouse_ui/snapshots_controller.rb +26 -0
  23. data/app/controllers/roundhouse_ui/workers_controller.rb +33 -0
  24. data/app/helpers/roundhouse_ui/application_helper.rb +4 -0
  25. data/app/helpers/roundhouse_ui/nav_helper.rb +24 -0
  26. data/app/helpers/roundhouse_ui/observability_helper.rb +13 -0
  27. data/app/views/layouts/roundhouse_ui/application.html.erb +365 -0
  28. data/app/views/roundhouse_ui/audit/index.html.erb +21 -0
  29. data/app/views/roundhouse_ui/busy/index.html.erb +23 -0
  30. data/app/views/roundhouse_ui/capsules/index.html.erb +22 -0
  31. data/app/views/roundhouse_ui/dashboard/show.html.erb +68 -0
  32. data/app/views/roundhouse_ui/dead/index.html.erb +46 -0
  33. data/app/views/roundhouse_ui/errors/index.html.erb +28 -0
  34. data/app/views/roundhouse_ui/jobs/_form.html.erb +24 -0
  35. data/app/views/roundhouse_ui/jobs/edit.html.erb +2 -0
  36. data/app/views/roundhouse_ui/jobs/new.html.erb +2 -0
  37. data/app/views/roundhouse_ui/jobs/show.html.erb +33 -0
  38. data/app/views/roundhouse_ui/metrics/show.html.erb +49 -0
  39. data/app/views/roundhouse_ui/queues/index.html.erb +45 -0
  40. data/app/views/roundhouse_ui/redis/show.html.erb +59 -0
  41. data/app/views/roundhouse_ui/retries/index.html.erb +33 -0
  42. data/app/views/roundhouse_ui/scheduled/index.html.erb +29 -0
  43. data/app/views/roundhouse_ui/shared/_pager.html.erb +15 -0
  44. data/app/views/roundhouse_ui/snapshots/index.html.erb +25 -0
  45. data/app/views/roundhouse_ui/workers/index.html.erb +39 -0
  46. data/config/routes.rb +54 -0
  47. data/lib/roundhouse_ui/audit.rb +25 -0
  48. data/lib/roundhouse_ui/cancel_middleware.rb +19 -0
  49. data/lib/roundhouse_ui/cancellation.rb +37 -0
  50. data/lib/roundhouse_ui/engine.rb +5 -0
  51. data/lib/roundhouse_ui/fetch.rb +36 -0
  52. data/lib/roundhouse_ui/metrics.rb +51 -0
  53. data/lib/roundhouse_ui/observability.rb +46 -0
  54. data/lib/roundhouse_ui/pause.rb +59 -0
  55. data/lib/roundhouse_ui/redaction.rb +33 -0
  56. data/lib/roundhouse_ui/snapshots.rb +90 -0
  57. data/lib/roundhouse_ui/version.rb +3 -0
  58. data/lib/roundhouse_ui.rb +73 -0
  59. data/lib/tasks/roundhouse_ui_tasks.rake +4 -0
  60. metadata +131 -0
@@ -0,0 +1,59 @@
1
+ require "set"
2
+ require "sidekiq"
3
+
4
+ module RoundhouseUi
5
+ # Roundhouse's own queue-pause registry — pure OSS, no Sidekiq Pro.
6
+ #
7
+ # Paused queue names live in a Redis set. RoundhouseUi::Fetch consults this set
8
+ # and skips paused queues when pulling work, so a paused queue stops being
9
+ # consumed without stopping the worker process.
10
+ module Pause
11
+ KEY = "roundhouse:paused"
12
+ FETCH_FLAG = "roundhouse:fetch_alive" # liveness beacon set by the fetcher
13
+
14
+ module_function
15
+
16
+ def pause!(queue)
17
+ Sidekiq.redis { |conn| conn.call("SADD", KEY, queue.to_s) }
18
+ end
19
+
20
+ def unpause!(queue)
21
+ Sidekiq.redis { |conn| conn.call("SREM", KEY, queue.to_s) }
22
+ end
23
+
24
+ def paused?(queue)
25
+ Sidekiq.redis { |conn| conn.call("SISMEMBER", KEY, queue.to_s) } == 1
26
+ end
27
+
28
+ def paused_queues
29
+ Sidekiq.redis { |conn| conn.call("SMEMBERS", KEY) }.sort
30
+ end
31
+
32
+ def paused_set
33
+ Set.new(Sidekiq.redis { |conn| conn.call("SMEMBERS", KEY) })
34
+ end
35
+
36
+ # Given the redis queue keys BasicFetch would poll (e.g. "queue:default"),
37
+ # drop any whose queue is paused. Pure given the paused set, so it's unit
38
+ # testable without a running Sidekiq.
39
+ def reject_paused(queue_keys)
40
+ paused = paused_set
41
+ return queue_keys if paused.empty?
42
+
43
+ queue_keys.reject { |key| paused.include?(key.to_s.delete_prefix("queue:")) }
44
+ end
45
+
46
+ # The fetcher calls this periodically so the web UI can tell whether pausing
47
+ # is actually enforced (the worker and web run in separate processes). The
48
+ # short TTL means the flag disappears soon after all Roundhouse fetchers stop.
49
+ def mark_fetch_alive!(ttl = 30)
50
+ Sidekiq.redis { |conn| conn.call("SET", FETCH_FLAG, "1", "EX", ttl) }
51
+ end
52
+
53
+ # True when a RoundhouseUi::Fetch has reported in recently — i.e. pausing
54
+ # will take effect. When false, the UI warns instead of pretending.
55
+ def fetch_installed?
56
+ Sidekiq.redis { |conn| conn.call("EXISTS", FETCH_FLAG) } == 1
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,33 @@
1
+ module RoundhouseUi
2
+ # Masks sensitive values when displaying job arguments. Configure the key
3
+ # patterns to redact (matched case-insensitively as substrings):
4
+ #
5
+ # RoundhouseUi.redact_args = %w[password token secret api_key authorization]
6
+ #
7
+ # Walks arrays/hashes so nested keyword args are covered. No-op by default.
8
+ module Redaction
9
+ MASK = "«redacted»".freeze
10
+
11
+ module_function
12
+
13
+ def apply(value, patterns = RoundhouseUi.redact_args)
14
+ return value if patterns.nil? || patterns.empty?
15
+
16
+ case value
17
+ when Hash
18
+ value.each_with_object({}) do |(k, v), out|
19
+ out[k] = sensitive?(k, patterns) ? MASK : apply(v, patterns)
20
+ end
21
+ when Array
22
+ value.map { |e| apply(e, patterns) }
23
+ else
24
+ value
25
+ end
26
+ end
27
+
28
+ def sensitive?(key, patterns)
29
+ key = key.to_s.downcase
30
+ patterns.any? { |p| key.include?(p.to_s.downcase) }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,90 @@
1
+ require "json"
2
+ require "sidekiq/api"
3
+
4
+ module RoundhouseUi
5
+ # Back up a queue's jobs before you purge it, and restore them later — the
6
+ # safety net that makes clearing a stuck queue non-destructive.
7
+ #
8
+ # Storage is pluggable via RoundhouseUi.snapshot_store. The default RedisStore
9
+ # keeps blobs in Redis (simple, dependency-free); for large/stuck queues you'll
10
+ # want a file or S3 store so the backup doesn't sit in the same Redis you're
11
+ # trying to relieve. Any object responding to write/read/delete/ids works.
12
+ module Snapshots
13
+ module_function
14
+
15
+ def store
16
+ RoundhouseUi.snapshot_store
17
+ end
18
+
19
+ # Copy every job currently on `queue_name` into a snapshot. Non-destructive —
20
+ # the queue is left untouched (purge separately if you want to clear it).
21
+ def take(queue_name)
22
+ payloads = []
23
+ Sidekiq::Queue.new(queue_name).each { |job| payloads << job.value }
24
+
25
+ id = "#{queue_name}-#{Time.now.to_i}-#{rand(10_000)}"
26
+ store.write(id, JSON.dump(
27
+ "queue" => queue_name, "created_at" => Time.now.to_f, "count" => payloads.size, "jobs" => payloads
28
+ ))
29
+ metadata(id)
30
+ end
31
+
32
+ def all
33
+ store.ids.filter_map { |id| metadata(id) }.sort_by { |m| -m[:created_at].to_f }
34
+ end
35
+
36
+ def metadata(id)
37
+ raw = store.read(id)
38
+ return nil unless raw
39
+
40
+ data = JSON.parse(raw)
41
+ { id: id, queue: data["queue"], count: data["count"] || data["jobs"].size, created_at: data["created_at"] }
42
+ end
43
+
44
+ # Re-enqueue a snapshot's jobs onto their original queue. Returns the count.
45
+ def restore(id)
46
+ raw = store.read(id)
47
+ return 0 unless raw
48
+
49
+ data = JSON.parse(raw)
50
+ key = "queue:#{data["queue"]}"
51
+ data["jobs"].each { |payload| Sidekiq.redis { |conn| conn.call("RPUSH", key, payload) } }
52
+ data["jobs"].size
53
+ end
54
+
55
+ def delete(id)
56
+ store.delete(id)
57
+ end
58
+
59
+ # Default store: blobs + an index set, all in Redis.
60
+ class RedisStore
61
+ INDEX = "roundhouse:snapshots"
62
+
63
+ def write(id, blob)
64
+ Sidekiq.redis do |conn|
65
+ conn.call("SET", key(id), blob)
66
+ conn.call("SADD", INDEX, id)
67
+ end
68
+ end
69
+
70
+ def read(id)
71
+ Sidekiq.redis { |conn| conn.call("GET", key(id)) }
72
+ end
73
+
74
+ def delete(id)
75
+ Sidekiq.redis do |conn|
76
+ conn.call("DEL", key(id))
77
+ conn.call("SREM", INDEX, id)
78
+ end
79
+ end
80
+
81
+ def ids
82
+ Sidekiq.redis { |conn| conn.call("SMEMBERS", INDEX) }
83
+ end
84
+
85
+ private
86
+
87
+ def key(id) = "roundhouse:snapshot:#{id}"
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module RoundhouseUi
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,73 @@
1
+ require "roundhouse_ui/version"
2
+ require "roundhouse_ui/engine"
3
+ require "sidekiq/api"
4
+ require "roundhouse_ui/pause"
5
+ require "roundhouse_ui/fetch"
6
+ require "roundhouse_ui/snapshots"
7
+ require "roundhouse_ui/observability"
8
+ require "roundhouse_ui/audit"
9
+ require "roundhouse_ui/redaction"
10
+ require "roundhouse_ui/cancellation"
11
+ require "roundhouse_ui/cancel_middleware"
12
+ require "roundhouse_ui/metrics"
13
+
14
+ # Brand name is "Roundhouse"; the gem and Ruby namespace are RoundhouseUi
15
+ # (matching the published gem name `roundhouse_ui`).
16
+ module RoundhouseUi
17
+ class << self
18
+ # When true, destructive actions (purge, retry, delete, …) are disabled.
19
+ # Mount Roundhouse read-only where operators should only observe.
20
+ attr_accessor :read_only
21
+
22
+ # Pluggable snapshot storage. Defaults to Redis; assign any object that
23
+ # responds to write(id, blob) / read(id) / delete(id) / ids.
24
+ attr_writer :snapshot_store
25
+
26
+ def snapshot_store
27
+ @snapshot_store ||= Snapshots::RedisStore.new
28
+ end
29
+
30
+ # Pluggable APM deep-links. Defaults to no links; assign a DatadogAdapter
31
+ # (or your own) to deep-link jobs out to your observability tool.
32
+ attr_writer :observability
33
+
34
+ def observability
35
+ @observability ||= Observability::NullAdapter.new
36
+ end
37
+
38
+ # How the audit log names the person taking an action. Auth is the host's
39
+ # job, so give Roundhouse a callable that pulls the actor from the request:
40
+ #
41
+ # RoundhouseUi.actor_resolver = ->(controller) { controller.current_user&.email }
42
+ #
43
+ # Defaults to "anonymous".
44
+ attr_accessor :actor_resolver
45
+
46
+ # Opt-in: enqueue brand-new jobs and edit/re-enqueue existing ones from the
47
+ # UI. Off by default — it's a sharp tool (bad edits create unrunnable jobs).
48
+ attr_accessor :allow_job_editing
49
+
50
+ # Argument keys (substring, case-insensitive) to mask when displaying jobs.
51
+ # e.g. RoundhouseUi.redact_args = %w[password token secret]. Default: none.
52
+ attr_accessor :redact_args
53
+
54
+ # Configure in an initializer:
55
+ #
56
+ # RoundhouseUi.configure do |c|
57
+ # c.read_only = !Rails.env.development?
58
+ # end
59
+ def configure
60
+ yield self
61
+ end
62
+
63
+ # Cooperative cancellation check for long-running jobs:
64
+ # raise SomeStop if RoundhouseUi.cancelled?(jid)
65
+ def cancelled?(jid)
66
+ Cancellation.cancelled?(jid)
67
+ end
68
+ end
69
+
70
+ self.read_only = false
71
+ self.allow_job_editing = false
72
+ self.redact_args = []
73
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :roundhouse_ui do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: roundhouse_ui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - R.J. Robinson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sidekiq
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ description: 'A mountable Rails engine that replaces the Sidekiq Web UI with a real-time
41
+ control plane: live queues, search, bulk actions, stuck-queue detection, snapshots,
42
+ and pluggable observability.'
43
+ email:
44
+ - rj@trainual.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - app/assets/javascripts/roundhouse_ui/turbo.min.js
53
+ - app/assets/stylesheets/roundhouse_ui/application.css
54
+ - app/controllers/concerns/roundhouse_ui/job_set_browsing.rb
55
+ - app/controllers/roundhouse_ui/application_controller.rb
56
+ - app/controllers/roundhouse_ui/assets_controller.rb
57
+ - app/controllers/roundhouse_ui/audit_controller.rb
58
+ - app/controllers/roundhouse_ui/busy_controller.rb
59
+ - app/controllers/roundhouse_ui/capsules_controller.rb
60
+ - app/controllers/roundhouse_ui/dashboard_controller.rb
61
+ - app/controllers/roundhouse_ui/dead_controller.rb
62
+ - app/controllers/roundhouse_ui/errors_controller.rb
63
+ - app/controllers/roundhouse_ui/jobs_controller.rb
64
+ - app/controllers/roundhouse_ui/metrics_controller.rb
65
+ - app/controllers/roundhouse_ui/queues_controller.rb
66
+ - app/controllers/roundhouse_ui/redis_controller.rb
67
+ - app/controllers/roundhouse_ui/retries_controller.rb
68
+ - app/controllers/roundhouse_ui/scheduled_controller.rb
69
+ - app/controllers/roundhouse_ui/snapshots_controller.rb
70
+ - app/controllers/roundhouse_ui/workers_controller.rb
71
+ - app/helpers/roundhouse_ui/application_helper.rb
72
+ - app/helpers/roundhouse_ui/nav_helper.rb
73
+ - app/helpers/roundhouse_ui/observability_helper.rb
74
+ - app/views/layouts/roundhouse_ui/application.html.erb
75
+ - app/views/roundhouse_ui/audit/index.html.erb
76
+ - app/views/roundhouse_ui/busy/index.html.erb
77
+ - app/views/roundhouse_ui/capsules/index.html.erb
78
+ - app/views/roundhouse_ui/dashboard/show.html.erb
79
+ - app/views/roundhouse_ui/dead/index.html.erb
80
+ - app/views/roundhouse_ui/errors/index.html.erb
81
+ - app/views/roundhouse_ui/jobs/_form.html.erb
82
+ - app/views/roundhouse_ui/jobs/edit.html.erb
83
+ - app/views/roundhouse_ui/jobs/new.html.erb
84
+ - app/views/roundhouse_ui/jobs/show.html.erb
85
+ - app/views/roundhouse_ui/metrics/show.html.erb
86
+ - app/views/roundhouse_ui/queues/index.html.erb
87
+ - app/views/roundhouse_ui/redis/show.html.erb
88
+ - app/views/roundhouse_ui/retries/index.html.erb
89
+ - app/views/roundhouse_ui/scheduled/index.html.erb
90
+ - app/views/roundhouse_ui/shared/_pager.html.erb
91
+ - app/views/roundhouse_ui/snapshots/index.html.erb
92
+ - app/views/roundhouse_ui/workers/index.html.erb
93
+ - config/routes.rb
94
+ - lib/roundhouse_ui.rb
95
+ - lib/roundhouse_ui/audit.rb
96
+ - lib/roundhouse_ui/cancel_middleware.rb
97
+ - lib/roundhouse_ui/cancellation.rb
98
+ - lib/roundhouse_ui/engine.rb
99
+ - lib/roundhouse_ui/fetch.rb
100
+ - lib/roundhouse_ui/metrics.rb
101
+ - lib/roundhouse_ui/observability.rb
102
+ - lib/roundhouse_ui/pause.rb
103
+ - lib/roundhouse_ui/redaction.rb
104
+ - lib/roundhouse_ui/snapshots.rb
105
+ - lib/roundhouse_ui/version.rb
106
+ - lib/tasks/roundhouse_ui_tasks.rake
107
+ homepage: https://github.com/rjrobinson/roundhouse_ui
108
+ licenses:
109
+ - MIT
110
+ metadata:
111
+ source_code_uri: https://github.com/rjrobinson/roundhouse_ui
112
+ changelog_uri: https://github.com/rjrobinson/roundhouse_ui/blob/main/CHANGELOG.md
113
+ rubygems_mfa_required: 'true'
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '3.1'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 4.0.10
129
+ specification_version: 4
130
+ summary: Roundhouse — a modern, real-time web UI for Sidekiq.
131
+ test_files: []