sidekiq 6.2.2 → 8.1.5

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +726 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +70 -39
  5. data/bin/kiq +17 -0
  6. data/bin/lint-herb +13 -0
  7. data/bin/multi_queue_bench +271 -0
  8. data/bin/sidekiq +4 -9
  9. data/bin/sidekiqload +214 -115
  10. data/bin/sidekiqmon +4 -1
  11. data/bin/webload +69 -0
  12. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  13. data/lib/generators/sidekiq/job_generator.rb +71 -0
  14. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
  15. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  16. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  17. data/lib/sidekiq/api.rb +729 -264
  18. data/lib/sidekiq/capsule.rb +135 -0
  19. data/lib/sidekiq/cli.rb +124 -100
  20. data/lib/sidekiq/client.rb +153 -106
  21. data/lib/sidekiq/component.rb +132 -0
  22. data/lib/sidekiq/config.rb +320 -0
  23. data/lib/sidekiq/deploy.rb +64 -0
  24. data/lib/sidekiq/embedded.rb +64 -0
  25. data/lib/sidekiq/fetch.rb +27 -26
  26. data/lib/sidekiq/iterable_job.rb +56 -0
  27. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  28. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  29. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  30. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  31. data/lib/sidekiq/job/iterable.rb +322 -0
  32. data/lib/sidekiq/job.rb +397 -5
  33. data/lib/sidekiq/job_logger.rb +23 -32
  34. data/lib/sidekiq/job_retry.rb +141 -68
  35. data/lib/sidekiq/job_util.rb +113 -0
  36. data/lib/sidekiq/launcher.rb +122 -98
  37. data/lib/sidekiq/loader.rb +57 -0
  38. data/lib/sidekiq/logger.rb +27 -106
  39. data/lib/sidekiq/manager.rb +41 -43
  40. data/lib/sidekiq/metrics/query.rb +184 -0
  41. data/lib/sidekiq/metrics/shared.rb +109 -0
  42. data/lib/sidekiq/metrics/tracking.rb +153 -0
  43. data/lib/sidekiq/middleware/chain.rb +96 -51
  44. data/lib/sidekiq/middleware/current_attributes.rb +120 -0
  45. data/lib/sidekiq/middleware/i18n.rb +8 -4
  46. data/lib/sidekiq/middleware/modules.rb +23 -0
  47. data/lib/sidekiq/monitor.rb +16 -6
  48. data/lib/sidekiq/paginator.rb +37 -10
  49. data/lib/sidekiq/processor.rb +105 -87
  50. data/lib/sidekiq/profiler.rb +73 -0
  51. data/lib/sidekiq/rails.rb +49 -36
  52. data/lib/sidekiq/redis_client_adapter.rb +117 -0
  53. data/lib/sidekiq/redis_connection.rb +55 -86
  54. data/lib/sidekiq/ring_buffer.rb +32 -0
  55. data/lib/sidekiq/scheduled.rb +106 -50
  56. data/lib/sidekiq/systemd.rb +2 -0
  57. data/lib/sidekiq/test_api.rb +331 -0
  58. data/lib/sidekiq/testing/inline.rb +2 -30
  59. data/lib/sidekiq/testing.rb +2 -342
  60. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  61. data/lib/sidekiq/tui/controls.rb +53 -0
  62. data/lib/sidekiq/tui/filtering.rb +53 -0
  63. data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  64. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  65. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  66. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  67. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  68. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  69. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  70. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  71. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  72. data/lib/sidekiq/tui/tabs.rb +15 -0
  73. data/lib/sidekiq/tui.rb +382 -0
  74. data/lib/sidekiq/version.rb +6 -1
  75. data/lib/sidekiq/web/action.rb +149 -64
  76. data/lib/sidekiq/web/application.rb +376 -268
  77. data/lib/sidekiq/web/config.rb +117 -0
  78. data/lib/sidekiq/web/helpers.rb +213 -87
  79. data/lib/sidekiq/web/router.rb +61 -74
  80. data/lib/sidekiq/web.rb +71 -100
  81. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  82. data/lib/sidekiq.rb +95 -196
  83. data/sidekiq.gemspec +14 -11
  84. data/web/assets/images/logo.png +0 -0
  85. data/web/assets/images/status.png +0 -0
  86. data/web/assets/javascripts/application.js +171 -57
  87. data/web/assets/javascripts/base-charts.js +120 -0
  88. data/web/assets/javascripts/chart.min.js +13 -0
  89. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  90. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  91. data/web/assets/javascripts/dashboard-charts.js +194 -0
  92. data/web/assets/javascripts/dashboard.js +41 -274
  93. data/web/assets/javascripts/metrics.js +280 -0
  94. data/web/assets/stylesheets/style.css +776 -0
  95. data/web/locales/ar.yml +72 -70
  96. data/web/locales/cs.yml +64 -62
  97. data/web/locales/da.yml +62 -53
  98. data/web/locales/de.yml +67 -65
  99. data/web/locales/el.yml +45 -24
  100. data/web/locales/en.yml +93 -69
  101. data/web/locales/es.yml +91 -68
  102. data/web/locales/fa.yml +67 -65
  103. data/web/locales/fr.yml +82 -67
  104. data/web/locales/gd.yml +110 -0
  105. data/web/locales/he.yml +67 -64
  106. data/web/locales/hi.yml +61 -59
  107. data/web/locales/it.yml +94 -54
  108. data/web/locales/ja.yml +74 -68
  109. data/web/locales/ko.yml +54 -52
  110. data/web/locales/lt.yml +68 -66
  111. data/web/locales/nb.yml +63 -61
  112. data/web/locales/nl.yml +54 -52
  113. data/web/locales/pl.yml +47 -45
  114. data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
  115. data/web/locales/pt.yml +53 -51
  116. data/web/locales/ru.yml +69 -66
  117. data/web/locales/sv.yml +55 -53
  118. data/web/locales/ta.yml +62 -60
  119. data/web/locales/tr.yml +102 -0
  120. data/web/locales/uk.yml +87 -61
  121. data/web/locales/ur.yml +66 -64
  122. data/web/locales/vi.yml +69 -67
  123. data/web/locales/zh-CN.yml +107 -0
  124. data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
  125. data/web/views/_footer.html.erb +32 -0
  126. data/web/views/_job_info.html.erb +115 -0
  127. data/web/views/_metrics_period_select.html.erb +15 -0
  128. data/web/views/_nav.html.erb +45 -0
  129. data/web/views/_paging.html.erb +26 -0
  130. data/web/views/_poll_link.html.erb +4 -0
  131. data/web/views/_summary.html.erb +40 -0
  132. data/web/views/busy.html.erb +151 -0
  133. data/web/views/dashboard.html.erb +104 -0
  134. data/web/views/dead.html.erb +38 -0
  135. data/web/views/filtering.html.erb +6 -0
  136. data/web/views/layout.html.erb +26 -0
  137. data/web/views/metrics.html.erb +85 -0
  138. data/web/views/metrics_for_job.html.erb +58 -0
  139. data/web/views/morgue.html.erb +69 -0
  140. data/web/views/profiles.html.erb +43 -0
  141. data/web/views/queue.html.erb +57 -0
  142. data/web/views/queues.html.erb +46 -0
  143. data/web/views/retries.html.erb +77 -0
  144. data/web/views/retry.html.erb +39 -0
  145. data/web/views/scheduled.html.erb +64 -0
  146. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  147. metadata +130 -61
  148. data/LICENSE +0 -9
  149. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  150. data/lib/sidekiq/delay.rb +0 -41
  151. data/lib/sidekiq/exception_handler.rb +0 -27
  152. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  153. data/lib/sidekiq/extensions/active_record.rb +0 -43
  154. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  155. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  156. data/lib/sidekiq/util.rb +0 -95
  157. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  158. data/lib/sidekiq/worker.rb +0 -244
  159. data/web/assets/stylesheets/application-dark.css +0 -147
  160. data/web/assets/stylesheets/application-rtl.css +0 -246
  161. data/web/assets/stylesheets/application.css +0 -1053
  162. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  163. data/web/assets/stylesheets/bootstrap.css +0 -5
  164. data/web/locales/zh-cn.yml +0 -68
  165. data/web/views/_footer.erb +0 -20
  166. data/web/views/_job_info.erb +0 -89
  167. data/web/views/_nav.erb +0 -52
  168. data/web/views/_paging.erb +0 -23
  169. data/web/views/_poll_link.erb +0 -7
  170. data/web/views/_status.erb +0 -4
  171. data/web/views/_summary.erb +0 -40
  172. data/web/views/busy.erb +0 -132
  173. data/web/views/dashboard.erb +0 -83
  174. data/web/views/dead.erb +0 -34
  175. data/web/views/layout.erb +0 -42
  176. data/web/views/morgue.erb +0 -78
  177. data/web/views/queue.erb +0 -55
  178. data/web/views/queues.erb +0 -38
  179. data/web/views/retries.erb +0 -83
  180. data/web/views/retry.erb +0 -34
  181. data/web/views/scheduled.erb +0 -57
@@ -0,0 +1,382 @@
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
+ content = @tui.block(
109
+ title: " #{Sidekiq::NAME} ",
110
+ borders: [:all],
111
+ title_style: @tui.style(fg: :light_red, modifiers: [:bold]),
112
+ children: [
113
+ # TODO convert to table
114
+ @tui.paragraph(
115
+ text: [
116
+ @tui.text_line(spans: ["Welcome to the Sidekiq Terminal UI"], alignment: :center),
117
+ @tui.text_line(spans: [
118
+ @tui.text_span(content: "Global hotkeys")
119
+ ]),
120
+ @tui.text_line(spans: []),
121
+ @tui.text_line(spans: [
122
+ @tui.text_span(content: "Esc", style: @hotkey_style),
123
+ @tui.text_span(content: ": Close this window")
124
+ ]),
125
+ @tui.text_line(spans: [
126
+ @tui.text_span(content: "←/→", style: @hotkey_style),
127
+ @tui.text_span(content: ": Move between tabs")
128
+ ]),
129
+ @tui.text_line(spans: [
130
+ @tui.text_span(content: "h/l", style: @hotkey_style),
131
+ @tui.text_span(content: ": Move to prev/next page of data")
132
+ ]),
133
+ @tui.text_line(spans: [
134
+ @tui.text_span(content: "j/k", style: @hotkey_style),
135
+ @tui.text_span(content: ": Move to prev/next row in current page")
136
+ ]),
137
+ @tui.text_line(spans: [
138
+ @tui.text_span(content: "x", style: @hotkey_style),
139
+ @tui.text_span(content: ": Select/deselect current row")
140
+ ]),
141
+ @tui.text_line(spans: [
142
+ @tui.text_span(content: "A", style: @hotkey_style),
143
+ @tui.text_span(content: ": Select/deselect All rows in current page")
144
+ ]),
145
+ @tui.text_line(spans: [
146
+ @tui.text_span(content: "q", style: @hotkey_style),
147
+ @tui.text_span(content: ": Quit")
148
+ ])
149
+ ]
150
+ )
151
+ ]
152
+ )
153
+ frame.render_widget(content, main_area)
154
+ controls = @tui.block(
155
+ title: t("Controls"),
156
+ borders: [:all],
157
+ children: [
158
+ @tui.paragraph(
159
+ text: [
160
+ @tui.text_line(spans: [
161
+ @tui.text_span(content: "Esc", style: @hotkey_style),
162
+ @tui.text_span(content: ": Close ")
163
+ ])
164
+ ]
165
+ )
166
+ ]
167
+ )
168
+ frame.render_widget(controls, controls_area)
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ def render_content_area(frame, content_area)
175
+ return render_error(frame, content_area, current_tab.error) if current_tab.error
176
+
177
+ current_tab.render(@tui, frame, content_area)
178
+ end
179
+
180
+ def render_controls(frame, area)
181
+ active_keys = current_tab.controls.filter { |hash| hash[:description] }
182
+
183
+ # Split controls into two lines, 8 is arbitrary
184
+ # TODO Dynamically split based on term width?
185
+ first = active_keys[...8]
186
+ lines = []
187
+ lines << @tui.text_line(spans: first.map { |hash|
188
+ [
189
+ @tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
190
+ @tui.text_span(content: ": #{t(hash[:description])} ")
191
+ ]
192
+ }.flatten)
193
+
194
+ last = active_keys[8...]
195
+ lines << if last && last.size > 0
196
+ @tui.text_line(spans: last.map { |hash|
197
+ [
198
+ @tui.text_span(content: hash[:display] || hash[:code], style: @hotkey_style),
199
+ @tui.text_span(content: ": #{t(hash[:description])} ")
200
+ ]
201
+ }.flatten)
202
+ else
203
+ @tui.text_line(spans: [])
204
+ end
205
+
206
+ footer = [
207
+ @tui.text_span(content: "Redis: #{redis_url} "),
208
+ @tui.text_span(content: "#{t("Now")}: #{Time.now.utc} "),
209
+ @tui.text_span(content: "#{t("Locale")}: #{@lang}")
210
+ ]
211
+
212
+ if current_tab.data[:filter]
213
+ @filter_style = @tui.style(fg: :white, bg: :dark_gray)
214
+ footer += [
215
+ @tui.text_span(content: " #{t("Filter")}: ", style: @filter_style),
216
+ @tui.text_span(content: current_tab.data[:filter], style: @filter_style),
217
+ @tui.text_span(content: "_", style: @tui.style(fg: :white, bg: :dark_gray, modifiers: [:slow_blink]))
218
+ ]
219
+ end
220
+ footer << @tui.text_span(content: " FPS: #{previous_fps}") if debugging?
221
+ lines << @tui.text_line(spans: footer)
222
+
223
+ controls = @tui.block(title: t("Controls"), borders: [:all],
224
+ children: [@tui.paragraph(text: lines)])
225
+ frame.render_widget(controls, area)
226
+ end
227
+
228
+ def handle_input
229
+ # We shouldn't need more than 10 FPS for a data-oriented app.
230
+ # This throttles down our CPU usage. Default is 60 FPS.
231
+ case @tui.poll_event(timeout: 0.1)
232
+ in {type: :key, code: "esc"} if @showing == :help
233
+ @showing = :main
234
+ in {type: :key, code: code} if current_tab.filtering? && code.length == 1
235
+ current_tab.append_to_filter(code)
236
+ current_tab.refresh_data
237
+ in {type: :key, code:, modifiers:}
238
+ control = current_tab.controls.find { |ctrl|
239
+ ctrl[:code] == code &&
240
+ (ctrl[:modifiers] || []) == (modifiers || [])
241
+ }
242
+ return unless control
243
+ control[:action].call(self, current_tab).tap {
244
+ refresh_data if control[:refresh]
245
+ }
246
+ else
247
+ # Ignore other events
248
+ end
249
+ rescue => ex
250
+ logger.error { [ex.message, ex.backtrace] }
251
+ end
252
+
253
+ def redis_url
254
+ Sidekiq.redis do |conn|
255
+ conn.config.server_url
256
+ end
257
+ rescue
258
+ "N/A"
259
+ end
260
+
261
+ def should_refresh?
262
+ Time.now - @last_refresh >= REFRESH_INTERVAL_SECONDS
263
+ end
264
+
265
+ def refresh_data
266
+ # logger.info GC.stat
267
+ current_tab.refresh_data
268
+ @last_refresh = Time.now
269
+ rescue => e
270
+ handle_exception(e)
271
+ current_tab.error = e
272
+ end
273
+
274
+ def render_error(frame, area, err)
275
+ header = [@tui.text_line(
276
+ spans: [@tui.text_span(content: err.message, style: @tui.style(modifiers: [:bold]))],
277
+ alignment: :center
278
+ )]
279
+ lines = Array(err.backtrace).map { |line| @tui.text_line(spans: [@tui.text_span(content: line)]) }
280
+
281
+ frame.render_widget(
282
+ @tui.paragraph(
283
+ text: header + lines,
284
+ alignment: :left,
285
+ block: @tui.block(title: t("Error"), borders: [:all], border_style: @tui.style(fg: :light_red))
286
+ ),
287
+ area
288
+ )
289
+ end
290
+
291
+ def show_help
292
+ @showing = :help
293
+ end
294
+
295
+ def all
296
+ @all ||= Tabs::All.map { |kls| kls.new(self) }
297
+ end
298
+
299
+ def current_tab
300
+ @current ||= @all.first
301
+ end
302
+
303
+ # Navigate tabs to the left or right.
304
+ # @param direction [Symbol] :left or :right
305
+ def navigate(direction)
306
+ index_change = (direction == :right) ? 1 : -1
307
+ @current = @all[(@all.index(current_tab) + index_change) % @all.size]
308
+ @current.reset_data
309
+ end
310
+
311
+ public def t(msg, options = nil)
312
+ string = @strings[msg] || msg
313
+ if options.nil?
314
+ string
315
+ else
316
+ string % options
317
+ end
318
+ end
319
+
320
+ def load_strings(lang)
321
+ {}.tap do |all|
322
+ find_locale_files(lang).each do |file|
323
+ strs = YAML.safe_load_file(file)
324
+ all.merge! strs[lang]
325
+ end
326
+ end
327
+ end
328
+
329
+ def locale_files
330
+ @@locale_files ||= LOCALE_DIRECTORIES.flat_map { |path|
331
+ Dir["#{path}/*.yml"]
332
+ }
333
+ end
334
+
335
+ def available_locales
336
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
337
+ end
338
+
339
+ def find_locale_files(lang)
340
+ locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
341
+ end
342
+
343
+ def load_locale
344
+ require "yaml"
345
+ lang = @lang.split(".").first # "en_US"
346
+ while lang.size > 0
347
+ hash = load_strings(lang)
348
+ if hash.size > 0
349
+ # found a working language dataset
350
+ @lang = lang
351
+ @strings = hash
352
+ Sidekiq.logger.debug { "using the #{lang} locale" }
353
+ break
354
+ end
355
+ # Try "en_US", "en_U", "en_", "en"
356
+ # It's ugly and bruteforce but it works
357
+ lang = lang[..-2]
358
+ end
359
+ end
360
+
361
+ def track_fps
362
+ # We hold two fps buckets: one for current second, one for previous second
363
+ idx = Time.now.to_i % 2
364
+ @fps[idx] += 1
365
+ yield
366
+ end
367
+
368
+ def previous_fps
369
+ curidx = Time.now.to_i % 2
370
+ prev = (curidx == 1) ? 0 : 1
371
+ if (val = @fps[prev]) != 0
372
+ @previous_fps = val
373
+ @fps[prev] = 0
374
+ end
375
+ @previous_fps
376
+ end
377
+
378
+ def debugging?
379
+ !!ENV["DEBUG"]
380
+ end
381
+ end
382
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.2.2"
4
+ VERSION = "8.1.5"
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)
28
+ throw :halt, [res, {"content-type" => "text/plain"}, [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