sidekiq 6.5.10 → 8.1.7

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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +568 -18
  3. data/README.md +66 -38
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/multi_queue_bench +271 -0
  7. data/bin/sidekiq +3 -8
  8. data/bin/sidekiqload +215 -118
  9. data/bin/sidekiqmon +3 -0
  10. data/bin/webload +69 -0
  11. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  12. data/lib/generators/sidekiq/job_generator.rb +17 -3
  13. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  14. data/lib/sidekiq/api.rb +526 -218
  15. data/lib/sidekiq/capsule.rb +135 -0
  16. data/lib/sidekiq/cli.rb +88 -96
  17. data/lib/sidekiq/client.rb +114 -47
  18. data/lib/sidekiq/component.rb +71 -4
  19. data/lib/sidekiq/config.rb +316 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +64 -0
  22. data/lib/sidekiq/fetch.rb +12 -14
  23. data/lib/sidekiq/iterable_job.rb +56 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +322 -0
  29. data/lib/sidekiq/job.rb +397 -10
  30. data/lib/sidekiq/job_logger.rb +14 -11
  31. data/lib/sidekiq/job_retry.rb +78 -28
  32. data/lib/sidekiq/job_util.rb +58 -16
  33. data/lib/sidekiq/launcher.rb +86 -70
  34. data/lib/sidekiq/loader.rb +57 -0
  35. data/lib/sidekiq/logger.rb +25 -94
  36. data/lib/sidekiq/manager.rb +11 -13
  37. data/lib/sidekiq/metrics/query.rb +84 -48
  38. data/lib/sidekiq/metrics/shared.rb +29 -14
  39. data/lib/sidekiq/metrics/tracking.rb +49 -30
  40. data/lib/sidekiq/middleware/chain.rb +19 -18
  41. data/lib/sidekiq/middleware/current_attributes.rb +78 -21
  42. data/lib/sidekiq/middleware/i18n.rb +2 -0
  43. data/lib/sidekiq/middleware/modules.rb +2 -0
  44. data/lib/sidekiq/monitor.rb +15 -5
  45. data/lib/sidekiq/paginator.rb +26 -4
  46. data/lib/sidekiq/processor.rb +78 -65
  47. data/lib/sidekiq/profiler.rb +73 -0
  48. data/lib/sidekiq/rails.rb +45 -60
  49. data/lib/sidekiq/redis_client_adapter.rb +35 -72
  50. data/lib/sidekiq/redis_connection.rb +54 -114
  51. data/lib/sidekiq/ring_buffer.rb +3 -0
  52. data/lib/sidekiq/scheduled.rb +28 -30
  53. data/lib/sidekiq/systemd.rb +2 -0
  54. data/lib/sidekiq/test_api.rb +331 -0
  55. data/lib/sidekiq/testing/inline.rb +2 -30
  56. data/lib/sidekiq/testing.rb +2 -342
  57. data/lib/sidekiq/transaction_aware_client.rb +23 -9
  58. data/lib/sidekiq/tui/controls.rb +53 -0
  59. data/lib/sidekiq/tui/filtering.rb +53 -0
  60. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  61. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  62. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  63. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  64. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  65. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  66. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  67. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  68. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  69. data/lib/sidekiq/tui/tabs.rb +15 -0
  70. data/lib/sidekiq/tui.rb +390 -0
  71. data/lib/sidekiq/version.rb +6 -1
  72. data/lib/sidekiq/web/action.rb +149 -64
  73. data/lib/sidekiq/web/application.rb +379 -281
  74. data/lib/sidekiq/web/config.rb +117 -0
  75. data/lib/sidekiq/web/helpers.rb +190 -71
  76. data/lib/sidekiq/web/router.rb +62 -75
  77. data/lib/sidekiq/web.rb +68 -101
  78. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  79. data/lib/sidekiq.rb +85 -269
  80. data/sidekiq.gemspec +15 -11
  81. data/web/assets/images/logo.png +0 -0
  82. data/web/assets/images/status.png +0 -0
  83. data/web/assets/javascripts/application.js +84 -24
  84. data/web/assets/javascripts/base-charts.js +120 -0
  85. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  86. data/web/assets/javascripts/dashboard-charts.js +194 -0
  87. data/web/assets/javascripts/dashboard.js +18 -234
  88. data/web/assets/javascripts/metrics.js +138 -120
  89. data/web/assets/stylesheets/style.css +776 -0
  90. data/web/locales/ar.yml +72 -70
  91. data/web/locales/cs.yml +64 -62
  92. data/web/locales/da.yml +62 -53
  93. data/web/locales/de.yml +67 -65
  94. data/web/locales/el.yml +4 -7
  95. data/web/locales/en.yml +89 -71
  96. data/web/locales/es.yml +91 -68
  97. data/web/locales/fa.yml +67 -65
  98. data/web/locales/fr.yml +82 -67
  99. data/web/locales/gd.yml +110 -0
  100. data/web/locales/he.yml +67 -64
  101. data/web/locales/hi.yml +95 -59
  102. data/web/locales/it.yml +94 -54
  103. data/web/locales/ja.yml +69 -70
  104. data/web/locales/ko.yml +54 -52
  105. data/web/locales/lt.yml +68 -66
  106. data/web/locales/nb.yml +63 -61
  107. data/web/locales/nl.yml +54 -52
  108. data/web/locales/pl.yml +47 -45
  109. data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
  110. data/web/locales/pt.yml +53 -51
  111. data/web/locales/ru.yml +69 -66
  112. data/web/locales/sv.yml +55 -53
  113. data/web/locales/ta.yml +100 -64
  114. data/web/locales/tr.yml +102 -0
  115. data/web/locales/uk.yml +87 -61
  116. data/web/locales/ur.yml +66 -64
  117. data/web/locales/vi.yml +69 -67
  118. data/web/locales/zh-CN.yml +107 -0
  119. data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
  120. data/web/views/_footer.html.erb +32 -0
  121. data/web/views/_job_info.html.erb +115 -0
  122. data/web/views/_metrics_period_select.html.erb +15 -0
  123. data/web/views/_nav.html.erb +45 -0
  124. data/web/views/_paging.html.erb +26 -0
  125. data/web/views/_poll_link.html.erb +4 -0
  126. data/web/views/_summary.html.erb +40 -0
  127. data/web/views/busy.html.erb +155 -0
  128. data/web/views/dashboard.html.erb +104 -0
  129. data/web/views/dead.html.erb +38 -0
  130. data/web/views/filtering.html.erb +6 -0
  131. data/web/views/layout.html.erb +26 -0
  132. data/web/views/metrics.html.erb +85 -0
  133. data/web/views/metrics_for_job.html.erb +58 -0
  134. data/web/views/morgue.html.erb +69 -0
  135. data/web/views/profiles.html.erb +43 -0
  136. data/web/views/queue.html.erb +57 -0
  137. data/web/views/queues.html.erb +46 -0
  138. data/web/views/retries.html.erb +77 -0
  139. data/web/views/retry.html.erb +39 -0
  140. data/web/views/scheduled.html.erb +64 -0
  141. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  142. metadata +113 -70
  143. data/lib/sidekiq/delay.rb +0 -43
  144. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  145. data/lib/sidekiq/extensions/active_record.rb +0 -43
  146. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  147. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  148. data/lib/sidekiq/metrics/deploy.rb +0 -47
  149. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  150. data/lib/sidekiq/worker.rb +0 -370
  151. data/web/assets/javascripts/graph.js +0 -16
  152. data/web/assets/stylesheets/application-dark.css +0 -143
  153. data/web/assets/stylesheets/application-rtl.css +0 -242
  154. data/web/assets/stylesheets/application.css +0 -999
  155. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  156. data/web/assets/stylesheets/bootstrap.css +0 -5
  157. data/web/locales/zh-cn.yml +0 -93
  158. data/web/views/_footer.erb +0 -20
  159. data/web/views/_job_info.erb +0 -89
  160. data/web/views/_nav.erb +0 -52
  161. data/web/views/_paging.erb +0 -23
  162. data/web/views/_poll_link.erb +0 -4
  163. data/web/views/_status.erb +0 -4
  164. data/web/views/_summary.erb +0 -40
  165. data/web/views/busy.erb +0 -137
  166. data/web/views/dashboard.erb +0 -84
  167. data/web/views/dead.erb +0 -34
  168. data/web/views/layout.erb +0 -42
  169. data/web/views/metrics.erb +0 -69
  170. data/web/views/metrics_for_job.erb +0 -87
  171. data/web/views/morgue.erb +0 -78
  172. data/web/views/queue.erb +0 -59
  173. data/web/views/queues.erb +0 -38
  174. data/web/views/retries.erb +0 -83
  175. data/web/views/retry.erb +0 -34
  176. data/web/views/scheduled.erb +0 -57
  177. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -0,0 +1,390 @@
1
+ # https://sr.ht/~kerrick/ratatui_ruby/
2
+ # https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples/
3
+ gem "ratatui_ruby", ">=1.4.0"
4
+ require "ratatui_ruby"
5
+
6
+ RatatuiRuby.debug_mode! if !!ENV["DEBUG"]
7
+
8
+ require "sidekiq/api"
9
+ require "sidekiq/paginator"
10
+
11
+ require_relative "tui/filtering"
12
+ require_relative "tui/controls"
13
+ require_relative "tui/tabs"
14
+
15
+ module Sidekiq
16
+ class TUI
17
+ include Sidekiq::Component
18
+
19
+ PageOptions = Data.define(:page, :size)
20
+
21
+ REFRESH_INTERVAL_SECONDS = 2
22
+ LOCALE_DIRECTORIES = [File.expand_path("#{File.dirname(__FILE__)}/../../web/locales")]
23
+
24
+ attr_reader :lang
25
+
26
+ # language is meant to be a locale code, e.g.
27
+ # LANG=en_US.utf-8
28
+ def initialize(cfg, language: ENV["LANG"] || "en")
29
+ @lang = language
30
+ @config = cfg
31
+ @base_style = nil
32
+ @last_refresh = Time.at(0)
33
+ @fps = Array.new(2) { 0 }
34
+ @previous_fps = 0
35
+ @showing = :main
36
+ end
37
+
38
+ def prepare(tui)
39
+ load_locale
40
+
41
+ @tui = tui
42
+ @highlight_style = @tui.style(fg: :light_red, modifiers: [:underlined])
43
+ @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
44
+ # eager load tabs
45
+ all
46
+ end
47
+
48
+ def run_loop
49
+ # Must log to a file, terminal is now controlled by Ratatui
50
+ config.logger = Logger.new("tui.log")
51
+
52
+ loop do
53
+ refresh_data if should_refresh?
54
+ render
55
+ break if handle_input == :quit
56
+ end
57
+ end
58
+
59
+ def render
60
+ track_fps do
61
+ if @showing == :main
62
+ @tui.draw do |frame|
63
+ main_area, controls_area = @tui.layout_split(
64
+ frame.area,
65
+ direction: :vertical,
66
+ constraints: [
67
+ @tui.constraint_fill(1),
68
+ @tui.constraint_length(5)
69
+ ]
70
+ )
71
+
72
+ # Split main area into tabs and content
73
+ tabs_area, content_area = @tui.layout_split(
74
+ main_area,
75
+ direction: :vertical,
76
+ constraints: [
77
+ @tui.constraint_length(3),
78
+ @tui.constraint_fill(1)
79
+ ]
80
+ )
81
+
82
+ all_tabs = all
83
+ tabs = @tui.tabs(
84
+ titles: all_tabs.map { |tab| t(tab.name) },
85
+ selected_index: all_tabs.index(current_tab),
86
+ block: @tui.block(title: " #{Sidekiq::NAME}", borders: [:all], title_style: @tui.style(fg: :light_red, modifiers: [:bold])),
87
+ divider: " | ",
88
+ highlight_style: @highlight_style,
89
+ style: @base_style
90
+ )
91
+ frame.render_widget(tabs, tabs_area)
92
+
93
+ render_content_area(frame, content_area)
94
+ render_controls(frame, controls_area)
95
+ end
96
+ end
97
+
98
+ if @showing == :help
99
+ @tui.draw do |frame|
100
+ main_area, controls_area = @tui.layout_split(
101
+ frame.area,
102
+ direction: :vertical,
103
+ constraints: [
104
+ @tui.constraint_fill(1),
105
+ @tui.constraint_length(4)
106
+ ]
107
+ )
108
+
109
+ help_data = [
110
+ ["Esc", "Close this window"],
111
+ ["←/→", "Move between tabs"],
112
+ ["h/l", "Move to prev/next page of data"],
113
+ ["j/k", "Move to prev/next row in current page"],
114
+ ["x", "Select/deselect current row"],
115
+ ["A", "Select/deselect All rows in current page"],
116
+ ["q", "Quit"]
117
+ ]
118
+
119
+ # striped styling like other tables in the app
120
+ help_rows = help_data.map.with_index { |cells, idx|
121
+ @tui.table_row(
122
+ cells: cells,
123
+ style: idx.even? ? nil : @tui.style(bg: :dark_gray)
124
+ )
125
+ }
126
+
127
+ table = @tui.table(
128
+ block: @tui.block(
129
+ title: " #{Sidekiq::NAME} - Welcome to the Sidekiq Terminal UI ",
130
+ borders: [:all],
131
+ title_style: @tui.style(fg: :light_red, modifiers: [:bold])
132
+ ),
133
+ header: ["Key", "Action"],
134
+ rows: help_rows,
135
+ widths: [
136
+ @tui.constraint_length(15),
137
+ @tui.constraint_fill(1)
138
+ ],
139
+ column_spacing: 1
140
+ )
141
+ frame.render_widget(table, main_area)
142
+ controls = @tui.block(
143
+ title: t("Controls"),
144
+ borders: [:all],
145
+ children: [
146
+ @tui.paragraph(
147
+ text: [hotkey_line("Esc", "Close ")]
148
+ )
149
+ ]
150
+ )
151
+ frame.render_widget(controls, controls_area)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def render_content_area(frame, content_area)
158
+ return render_error(frame, content_area, current_tab.error) if current_tab.error
159
+
160
+ current_tab.render(@tui, frame, content_area)
161
+ end
162
+
163
+ def render_controls(frame, area)
164
+ active_keys = current_tab.controls.filter { |hash| hash[:description] }
165
+
166
+ # Dynamically split controls based on terminal width
167
+ # Estimate space needed: each control needs ~key_length + description_length + 4 chars for ": " and spacing
168
+ available_width = area.width - 4 # Account for borders
169
+
170
+ lines = []
171
+ current_line = []
172
+ current_width = 0
173
+
174
+ active_keys.each do |hash|
175
+ # Estimate control width: key + ": " + description + " " (spacing)
176
+ key_text = hash[:display] || hash[:code]
177
+ control_width = key_text.length + 2 + t(hash[:description]).length + 2
178
+
179
+ if current_width + control_width > available_width && !current_line.empty?
180
+ # Start a new line
181
+ lines << controls_line(current_line)
182
+ current_line = [hash]
183
+ current_width = control_width
184
+ else
185
+ current_line << hash
186
+ current_width += control_width
187
+ end
188
+ end
189
+
190
+ # Add the last line
191
+ lines << controls_line(current_line) unless current_line.empty?
192
+
193
+ # Ensure we have at least 2 lines for layout consistency
194
+ while lines.length < 2
195
+ lines << @tui.text_line(spans: [])
196
+ end
197
+
198
+ footer = [
199
+ @tui.text_span(content: "Redis: #{redis_url} "),
200
+ @tui.text_span(content: "#{t("Now")}: #{Time.now.utc} "),
201
+ @tui.text_span(content: "#{t("Locale")}: #{@lang}")
202
+ ]
203
+
204
+ if current_tab.data[:filter]
205
+ @filter_style = @tui.style(fg: :white, bg: :dark_gray)
206
+ footer += [
207
+ @tui.text_span(content: " #{t("Filter")}: ", style: @filter_style),
208
+ @tui.text_span(content: current_tab.data[:filter], style: @filter_style),
209
+ @tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink]))
210
+ ]
211
+ end
212
+ footer << @tui.text_span(content: " FPS: #{previous_fps}") if debugging?
213
+ lines << @tui.text_line(spans: footer)
214
+
215
+ controls = @tui.block(title: t("Controls"), borders: [:all],
216
+ children: [@tui.paragraph(text: lines)])
217
+ frame.render_widget(controls, area)
218
+ end
219
+
220
+ def hotkey_line(key, description)
221
+ @tui.text_line(spans: [
222
+ @tui.text_span(content: key, style: @hotkey_style),
223
+ @tui.text_span(content: ": #{description}")
224
+ ])
225
+ end
226
+
227
+ def controls_line(keys)
228
+ @tui.text_line(spans: keys.map { |hash|
229
+ [
230
+ @tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
231
+ @tui.text_span(content: ": #{t(hash[:description])} ")
232
+ ]
233
+ }.flatten)
234
+ end
235
+
236
+ def handle_input
237
+ # We shouldn't need more than 10 FPS for a data-oriented app.
238
+ # This throttles down our CPU usage. Default is 60 FPS.
239
+ case @tui.poll_event(timeout: 0.1)
240
+ in {type: :key, code: "esc"} if @showing == :help
241
+ @showing = :main
242
+ in {type: :key, code: code} if current_tab.filtering? && code.length == 1
243
+ current_tab.append_to_filter(code)
244
+ current_tab.refresh_data
245
+ in {type: :key, code:, modifiers:}
246
+ control = current_tab.controls.find { |ctrl|
247
+ ctrl[:code] == code &&
248
+ (ctrl[:modifiers] || []) == (modifiers || [])
249
+ }
250
+ return unless control
251
+ control[:action].call(self, current_tab).tap {
252
+ refresh_data if control[:refresh]
253
+ }
254
+ else
255
+ # Ignore other events
256
+ end
257
+ rescue => ex
258
+ logger.error { [ex.message, ex.backtrace] }
259
+ end
260
+
261
+ def redis_url
262
+ Sidekiq.redis do |conn|
263
+ conn.config.server_url
264
+ end
265
+ rescue
266
+ "N/A"
267
+ end
268
+
269
+ def should_refresh?
270
+ Time.now - @last_refresh >= REFRESH_INTERVAL_SECONDS
271
+ end
272
+
273
+ def refresh_data
274
+ # logger.info GC.stat
275
+ current_tab.refresh_data
276
+ @last_refresh = Time.now
277
+ rescue => e
278
+ handle_exception(e)
279
+ current_tab.error = e
280
+ end
281
+
282
+ def render_error(frame, area, err)
283
+ header = [@tui.text_line(
284
+ spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
285
+ alignment: :center
286
+ )]
287
+ lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }
288
+
289
+ frame.render_widget(
290
+ @tui.paragraph(
291
+ text: header + lines,
292
+ alignment: :left,
293
+ block: @tui.block(title: t("Error"), borders: [:all], border_style: @tui.style(fg: :light_red))
294
+ ),
295
+ area
296
+ )
297
+ end
298
+
299
+ def show_help
300
+ @showing = :help
301
+ end
302
+
303
+ def all
304
+ @all ||= Tabs::All.map { |kls| kls.new(self) }
305
+ end
306
+
307
+ def current_tab
308
+ @current ||= @all.first
309
+ end
310
+
311
+ # Navigate tabs to the left or right.
312
+ # @param direction [Symbol] :left or :right
313
+ def navigate(direction)
314
+ index_change = (direction == :right) ? 1 : -1
315
+ @current = @all[(@all.index(current_tab) + index_change) % @all.size]
316
+ @current.reset_data
317
+ end
318
+
319
+ public def t(msg, options = nil)
320
+ string = @strings[msg] || msg
321
+ if options.nil?
322
+ string
323
+ else
324
+ string % options
325
+ end
326
+ end
327
+
328
+ def load_strings(lang)
329
+ {}.tap do |all|
330
+ find_locale_files(lang).each do |file|
331
+ strs = YAML.safe_load_file(file)
332
+ all.merge! strs[lang]
333
+ end
334
+ end
335
+ end
336
+
337
+ def locale_files
338
+ @@locale_files ||= LOCALE_DIRECTORIES.flat_map { |path|
339
+ Dir["#{path}/*.yml"]
340
+ }
341
+ end
342
+
343
+ def available_locales
344
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
345
+ end
346
+
347
+ def find_locale_files(lang)
348
+ locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
349
+ end
350
+
351
+ def load_locale
352
+ require "yaml"
353
+ lang = @lang.split(".").first # "en_US"
354
+ while lang.size > 0
355
+ hash = load_strings(lang)
356
+ if hash.size > 0
357
+ # found a working language dataset
358
+ @lang = lang
359
+ @strings = hash
360
+ Sidekiq.logger.debug { "using the #{lang} locale" }
361
+ break
362
+ end
363
+ # Try "en_US", "en_U", "en_", "en"
364
+ # It's ugly and bruteforce but it works
365
+ lang = lang[..-2]
366
+ end
367
+ end
368
+
369
+ def track_fps
370
+ # We hold two fps buckets: one for current second, one for previous second
371
+ idx = Time.now.to_i % 2
372
+ @fps[idx] += 1
373
+ yield
374
+ end
375
+
376
+ def previous_fps
377
+ curidx = Time.now.to_i % 2
378
+ prev = (curidx == 1) ? 0 : 1
379
+ if (val = @fps[prev]) != 0
380
+ @previous_fps = val
381
+ @fps[prev] = 0
382
+ end
383
+ @previous_fps
384
+ end
385
+
386
+ def debugging?
387
+ !!ENV["DEBUG"]
388
+ end
389
+ end
390
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.5.10"
4
+ VERSION = "8.1.7"
5
+ MAJOR = 8
6
+
7
+ def self.gem_version
8
+ Gem::Version.new(VERSION)
9
+ end
5
10
  end
@@ -1,93 +1,178 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
4
+
3
5
  module Sidekiq
4
- class WebAction
5
- RACK_SESSION = "rack.session"
6
+ class Web
7
+ ##
8
+ # These instance methods are available to all executing ERB
9
+ # templates.
10
+ class Action
11
+ attr_accessor :env, :block
12
+
13
+ def initialize(env, block)
14
+ @_erb = false
15
+ @env = env
16
+ @block = block
17
+ end
6
18
 
7
- attr_accessor :env, :block, :type
19
+ def config
20
+ env[:web_config]
21
+ end
8
22
 
9
- def settings
10
- Web.settings
11
- end
23
+ def request
24
+ @request ||= ::Rack::Request.new(env)
25
+ end
12
26
 
13
- def request
14
- @request ||= ::Rack::Request.new(env)
15
- end
27
+ def halt(res, msg = nil)
28
+ throw :halt, [res, {"content-type" => "text/plain"}, [msg || res.to_s]]
29
+ end
16
30
 
17
- def halt(res)
18
- throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
19
- end
31
+ # external redirect
32
+ def redirect_to(url)
33
+ throw :halt, [302, {"location" => url}, []]
34
+ end
20
35
 
21
- def redirect(location)
22
- throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
23
- end
36
+ def header(key, value)
37
+ env["response_headers"][key] = value.to_s
38
+ end
24
39
 
25
- def params
26
- indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
40
+ # internal redirect
41
+ def redirect(location)
42
+ throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
43
+ end
27
44
 
28
- indifferent_hash.merge! request.params
29
- route_params.each { |k, v| indifferent_hash[k.to_s] = v }
45
+ def reload_page
46
+ current_location = request.referer.gsub(request.base_url, "")
47
+ redirect current_location
48
+ end
30
49
 
31
- indifferent_hash
32
- end
50
+ # stuff after ? or form input
51
+ # uses String keys, no Symbols!
52
+ def url_params(key)
53
+ warn { "URL parameter `#{key}` should be accessed via String, not Symbol (at #{caller(3..3).first})" } if key.is_a?(Symbol)
54
+ request.params[key.to_s]
55
+ end
33
56
 
34
- def route_params
35
- env[WebRouter::ROUTE_PARAMS]
36
- end
57
+ # variables embedded in path, `/metrics/:name`
58
+ # uses Symbol keys, no Strings!
59
+ def route_params(key)
60
+ warn { "Route parameter `#{key}` should be accessed via Symbol, not String (at #{caller(3..3).first})" } if key.is_a?(String)
61
+ env["rack.route_params"][key.to_sym]
62
+ end
37
63
 
38
- def session
39
- env[RACK_SESSION]
40
- end
64
+ def params
65
+ warn { "Direct access to Rack parameters is discouraged, use `url_params` or `route_params` (at #{caller(3..3).first})" }
66
+ request.params
67
+ end
41
68
 
42
- def erb(content, options = {})
43
- if content.is_a? Symbol
44
- unless respond_to?(:"_erb_#{content}")
45
- src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
46
- WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
- def _erb_#{content}
48
- #{src}
49
- end
50
- RUBY
51
- end
69
+ def session
70
+ env["rack.session"] || fail(<<~EOM)
71
+ Sidekiq::Web needs a valid Rack session. If this is a Rails app, make
72
+ sure you mount Sidekiq::Web *inside* your application routes:
73
+
74
+
75
+ Rails.application.routes.draw do
76
+ mount Sidekiq::Web => "/sidekiq"
77
+ ....
78
+ end
79
+
80
+
81
+ If this is a Rails app in API mode, you need to enable sessions.
82
+
83
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
84
+
85
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
86
+
87
+ # first, use IRB to create a shared secret key for sessions and commit it
88
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
89
+
90
+ # now use the secret with a session cookie middleware
91
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
92
+ run Sidekiq::Web
93
+
94
+ EOM
52
95
  end
53
96
 
54
- if @_erb
55
- _erb(content, options[:locals])
56
- else
57
- @_erb = true
58
- content = _erb(content, options[:locals])
97
+ def logger
98
+ Sidekiq.logger
99
+ end
59
100
 
60
- _render { content }
101
+ # flash { "Some message to show on redirect" }
102
+ def flash
103
+ msg = yield
104
+ logger.info msg
105
+ session[:skq_flash] = msg
61
106
  end
62
- end
63
107
 
64
- def render(engine, content, options = {})
65
- raise "Only erb templates are supported" if engine != :erb
108
+ def flash?
109
+ session&.[](:skq_flash)
110
+ end
66
111
 
67
- erb(content, options)
68
- end
112
+ def get_flash
113
+ @flash ||= session.delete(:skq_flash)
114
+ end
69
115
 
70
- def json(payload)
71
- [200, {"content-type" => "application/json", "cache-control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
72
- end
116
+ def erb(content, options = {})
117
+ if content.is_a? Symbol
118
+ unless respond_to?(:"_erb_#{content}")
119
+ views = options[:views] || Web.views
120
+ filename = "#{views}/#{content}.html.erb"
121
+ src = ERB.new(File.read(filename)).src
122
+
123
+ # Need to use lineno less by 1 because erb generates a
124
+ # comment before the source code.
125
+ Action.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
126
+ def _erb_#{content}
127
+ #{src}
128
+ end
129
+ RUBY
130
+ end
131
+ end
73
132
 
74
- def initialize(env, block)
75
- @_erb = false
76
- @env = env
77
- @block = block
78
- @files ||= {}
79
- end
133
+ if @_erb
134
+ _erb(content, options[:locals])
135
+ else
136
+ @_erb = true
137
+ content = _erb(content, options[:locals])
80
138
 
81
- private
139
+ _render { content }
140
+ end
141
+ end
142
+
143
+ def render(engine, content, options = {})
144
+ raise "Only erb templates are supported" if engine != :erb
145
+
146
+ erb(content, options)
147
+ end
148
+
149
+ def json(payload)
150
+ [200,
151
+ {"content-type" => "application/json", "cache-control" => "private, no-store"},
152
+ [Sidekiq.dump_json(payload)]]
153
+ end
82
154
 
83
- def _erb(file, locals)
84
- locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
155
+ private
85
156
 
86
- if file.is_a?(String)
87
- ERB.new(file).result(binding)
88
- else
89
- send(:"_erb_#{file}")
157
+ def warn
158
+ Sidekiq.logger.warn yield
90
159
  end
160
+
161
+ def _erb(file, locals)
162
+ locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
163
+
164
+ if file.is_a?(String)
165
+ ERB.new(file).result(binding)
166
+ else
167
+ send(:"_erb_#{file}")
168
+ end
169
+ end
170
+
171
+ class_eval <<-RUBY, ::Sidekiq::Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
172
+ def _render
173
+ #{ERB.new(File.read(::Sidekiq::Web::LAYOUT)).src}
174
+ end
175
+ RUBY
91
176
  end
92
177
  end
93
178
  end