ultra-fast-tool 0.0.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.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/sidekiq-8.1.6/Changes.md +2515 -0
  3. data/sidekiq-8.1.6/LICENSE.txt +9 -0
  4. data/sidekiq-8.1.6/README.md +124 -0
  5. data/sidekiq-8.1.6/bin/kiq +17 -0
  6. data/sidekiq-8.1.6/bin/lint-herb +13 -0
  7. data/sidekiq-8.1.6/bin/multi_queue_bench +271 -0
  8. data/sidekiq-8.1.6/bin/sidekiq +37 -0
  9. data/sidekiq-8.1.6/bin/sidekiqload +256 -0
  10. data/sidekiq-8.1.6/bin/sidekiqmon +11 -0
  11. data/sidekiq-8.1.6/bin/webload +69 -0
  12. data/sidekiq-8.1.6/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  13. data/sidekiq-8.1.6/lib/generators/sidekiq/job_generator.rb +71 -0
  14. data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job.rb.erb +9 -0
  15. data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  16. data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  17. data/sidekiq-8.1.6/lib/sidekiq/api.rb +1466 -0
  18. data/sidekiq-8.1.6/lib/sidekiq/capsule.rb +135 -0
  19. data/sidekiq-8.1.6/lib/sidekiq/cli.rb +433 -0
  20. data/sidekiq-8.1.6/lib/sidekiq/client.rb +306 -0
  21. data/sidekiq-8.1.6/lib/sidekiq/component.rb +132 -0
  22. data/sidekiq-8.1.6/lib/sidekiq/config.rb +316 -0
  23. data/sidekiq-8.1.6/lib/sidekiq/deploy.rb +64 -0
  24. data/sidekiq-8.1.6/lib/sidekiq/embedded.rb +64 -0
  25. data/sidekiq-8.1.6/lib/sidekiq/fetch.rb +89 -0
  26. data/sidekiq-8.1.6/lib/sidekiq/iterable_job.rb +56 -0
  27. data/sidekiq-8.1.6/lib/sidekiq/job/interrupt_handler.rb +24 -0
  28. data/sidekiq-8.1.6/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  29. data/sidekiq-8.1.6/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  30. data/sidekiq-8.1.6/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  31. data/sidekiq-8.1.6/lib/sidekiq/job/iterable.rb +322 -0
  32. data/sidekiq-8.1.6/lib/sidekiq/job.rb +400 -0
  33. data/sidekiq-8.1.6/lib/sidekiq/job_logger.rb +54 -0
  34. data/sidekiq-8.1.6/lib/sidekiq/job_retry.rb +332 -0
  35. data/sidekiq-8.1.6/lib/sidekiq/job_util.rb +113 -0
  36. data/sidekiq-8.1.6/lib/sidekiq/launcher.rb +283 -0
  37. data/sidekiq-8.1.6/lib/sidekiq/loader.rb +57 -0
  38. data/sidekiq-8.1.6/lib/sidekiq/logger.rb +87 -0
  39. data/sidekiq-8.1.6/lib/sidekiq/manager.rb +134 -0
  40. data/sidekiq-8.1.6/lib/sidekiq/metrics/query.rb +184 -0
  41. data/sidekiq-8.1.6/lib/sidekiq/metrics/shared.rb +109 -0
  42. data/sidekiq-8.1.6/lib/sidekiq/metrics/tracking.rb +153 -0
  43. data/sidekiq-8.1.6/lib/sidekiq/middleware/chain.rb +207 -0
  44. data/sidekiq-8.1.6/lib/sidekiq/middleware/current_attributes.rb +120 -0
  45. data/sidekiq-8.1.6/lib/sidekiq/middleware/i18n.rb +44 -0
  46. data/sidekiq-8.1.6/lib/sidekiq/middleware/modules.rb +23 -0
  47. data/sidekiq-8.1.6/lib/sidekiq/monitor.rb +143 -0
  48. data/sidekiq-8.1.6/lib/sidekiq/paginator.rb +74 -0
  49. data/sidekiq-8.1.6/lib/sidekiq/processor.rb +298 -0
  50. data/sidekiq-8.1.6/lib/sidekiq/profiler.rb +73 -0
  51. data/sidekiq-8.1.6/lib/sidekiq/rails.rb +63 -0
  52. data/sidekiq-8.1.6/lib/sidekiq/redis_client_adapter.rb +117 -0
  53. data/sidekiq-8.1.6/lib/sidekiq/redis_connection.rb +115 -0
  54. data/sidekiq-8.1.6/lib/sidekiq/ring_buffer.rb +32 -0
  55. data/sidekiq-8.1.6/lib/sidekiq/scheduled.rb +235 -0
  56. data/sidekiq-8.1.6/lib/sidekiq/sd_notify.rb +149 -0
  57. data/sidekiq-8.1.6/lib/sidekiq/systemd.rb +26 -0
  58. data/sidekiq-8.1.6/lib/sidekiq/test_api.rb +331 -0
  59. data/sidekiq-8.1.6/lib/sidekiq/testing/inline.rb +2 -0
  60. data/sidekiq-8.1.6/lib/sidekiq/testing.rb +3 -0
  61. data/sidekiq-8.1.6/lib/sidekiq/transaction_aware_client.rb +59 -0
  62. data/sidekiq-8.1.6/lib/sidekiq/tui/controls.rb +53 -0
  63. data/sidekiq-8.1.6/lib/sidekiq/tui/filtering.rb +53 -0
  64. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  65. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/busy.rb +118 -0
  66. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/dead.rb +19 -0
  67. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/home.rb +144 -0
  68. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  69. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/queues.rb +95 -0
  70. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/retries.rb +19 -0
  71. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  72. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  73. data/sidekiq-8.1.6/lib/sidekiq/tui/tabs.rb +15 -0
  74. data/sidekiq-8.1.6/lib/sidekiq/tui.rb +382 -0
  75. data/sidekiq-8.1.6/lib/sidekiq/version.rb +10 -0
  76. data/sidekiq-8.1.6/lib/sidekiq/web/action.rb +178 -0
  77. data/sidekiq-8.1.6/lib/sidekiq/web/application.rb +480 -0
  78. data/sidekiq-8.1.6/lib/sidekiq/web/config.rb +117 -0
  79. data/sidekiq-8.1.6/lib/sidekiq/web/helpers.rb +474 -0
  80. data/sidekiq-8.1.6/lib/sidekiq/web/router.rb +91 -0
  81. data/sidekiq-8.1.6/lib/sidekiq/web.rb +140 -0
  82. data/sidekiq-8.1.6/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  83. data/sidekiq-8.1.6/lib/sidekiq.rb +161 -0
  84. data/sidekiq-8.1.6/sidekiq.gemspec +31 -0
  85. data/sidekiq-8.1.6/web/assets/images/apple-touch-icon.png +0 -0
  86. data/sidekiq-8.1.6/web/assets/images/favicon.ico +0 -0
  87. data/sidekiq-8.1.6/web/assets/images/logo.png +0 -0
  88. data/sidekiq-8.1.6/web/assets/images/status.png +0 -0
  89. data/sidekiq-8.1.6/web/assets/javascripts/application.js +204 -0
  90. data/sidekiq-8.1.6/web/assets/javascripts/base-charts.js +120 -0
  91. data/sidekiq-8.1.6/web/assets/javascripts/chart.min.js +13 -0
  92. data/sidekiq-8.1.6/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  93. data/sidekiq-8.1.6/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  94. data/sidekiq-8.1.6/web/assets/javascripts/dashboard-charts.js +194 -0
  95. data/sidekiq-8.1.6/web/assets/javascripts/dashboard.js +63 -0
  96. data/sidekiq-8.1.6/web/assets/javascripts/metrics.js +280 -0
  97. data/sidekiq-8.1.6/web/assets/stylesheets/style.css +776 -0
  98. data/sidekiq-8.1.6/web/locales/ar.yml +89 -0
  99. data/sidekiq-8.1.6/web/locales/cs.yml +80 -0
  100. data/sidekiq-8.1.6/web/locales/da.yml +77 -0
  101. data/sidekiq-8.1.6/web/locales/de.yml +83 -0
  102. data/sidekiq-8.1.6/web/locales/el.yml +89 -0
  103. data/sidekiq-8.1.6/web/locales/en.yml +110 -0
  104. data/sidekiq-8.1.6/web/locales/es.yml +109 -0
  105. data/sidekiq-8.1.6/web/locales/fa.yml +82 -0
  106. data/sidekiq-8.1.6/web/locales/fr.yml +100 -0
  107. data/sidekiq-8.1.6/web/locales/gd.yml +110 -0
  108. data/sidekiq-8.1.6/web/locales/he.yml +82 -0
  109. data/sidekiq-8.1.6/web/locales/hi.yml +77 -0
  110. data/sidekiq-8.1.6/web/locales/it.yml +109 -0
  111. data/sidekiq-8.1.6/web/locales/ja.yml +92 -0
  112. data/sidekiq-8.1.6/web/locales/ko.yml +70 -0
  113. data/sidekiq-8.1.6/web/locales/lt.yml +85 -0
  114. data/sidekiq-8.1.6/web/locales/nb.yml +79 -0
  115. data/sidekiq-8.1.6/web/locales/nl.yml +70 -0
  116. data/sidekiq-8.1.6/web/locales/pl.yml +61 -0
  117. data/sidekiq-8.1.6/web/locales/pt-BR.yml +97 -0
  118. data/sidekiq-8.1.6/web/locales/pt.yml +69 -0
  119. data/sidekiq-8.1.6/web/locales/ru.yml +85 -0
  120. data/sidekiq-8.1.6/web/locales/sv.yml +70 -0
  121. data/sidekiq-8.1.6/web/locales/ta.yml +77 -0
  122. data/sidekiq-8.1.6/web/locales/tr.yml +102 -0
  123. data/sidekiq-8.1.6/web/locales/uk.yml +102 -0
  124. data/sidekiq-8.1.6/web/locales/ur.yml +82 -0
  125. data/sidekiq-8.1.6/web/locales/vi.yml +85 -0
  126. data/sidekiq-8.1.6/web/locales/zh-CN.yml +107 -0
  127. data/sidekiq-8.1.6/web/locales/zh-TW.yml +103 -0
  128. data/sidekiq-8.1.6/web/views/_footer.html.erb +32 -0
  129. data/sidekiq-8.1.6/web/views/_job_info.html.erb +115 -0
  130. data/sidekiq-8.1.6/web/views/_metrics_period_select.html.erb +15 -0
  131. data/sidekiq-8.1.6/web/views/_nav.html.erb +45 -0
  132. data/sidekiq-8.1.6/web/views/_paging.html.erb +26 -0
  133. data/sidekiq-8.1.6/web/views/_poll_link.html.erb +4 -0
  134. data/sidekiq-8.1.6/web/views/_summary.html.erb +40 -0
  135. data/sidekiq-8.1.6/web/views/busy.html.erb +155 -0
  136. data/sidekiq-8.1.6/web/views/dashboard.html.erb +104 -0
  137. data/sidekiq-8.1.6/web/views/dead.html.erb +38 -0
  138. data/sidekiq-8.1.6/web/views/filtering.html.erb +6 -0
  139. data/sidekiq-8.1.6/web/views/layout.html.erb +26 -0
  140. data/sidekiq-8.1.6/web/views/metrics.html.erb +85 -0
  141. data/sidekiq-8.1.6/web/views/metrics_for_job.html.erb +58 -0
  142. data/sidekiq-8.1.6/web/views/morgue.html.erb +69 -0
  143. data/sidekiq-8.1.6/web/views/profiles.html.erb +43 -0
  144. data/sidekiq-8.1.6/web/views/queue.html.erb +57 -0
  145. data/sidekiq-8.1.6/web/views/queues.html.erb +46 -0
  146. data/sidekiq-8.1.6/web/views/retries.html.erb +77 -0
  147. data/sidekiq-8.1.6/web/views/retry.html.erb +39 -0
  148. data/sidekiq-8.1.6/web/views/scheduled.html.erb +64 -0
  149. data/sidekiq-8.1.6/web/views/scheduled_job_info.html.erb +8 -0
  150. data/ultra-fast-tool.gemspec +11 -0
  151. metadata +189 -0
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ class Web
5
+ ##
6
+ # Configure the Sidekiq::Web instance in this process:
7
+ #
8
+ # require "sidekiq/web"
9
+ # Sidekiq::Web.configure do |config|
10
+ # config.register(MyExtension, name: "myext", tab: "TabName", index: "tabpage/")
11
+ # end
12
+ #
13
+ # This should go in your `config/routes.rb` or similar. It
14
+ # does not belong in your initializer since Web should not be
15
+ # loaded in some processes (like an actual Sidekiq process).
16
+ # See `examples/webui-ext` for a sample web extension.
17
+ class Config
18
+ extend Forwardable
19
+
20
+ OPTIONS = {
21
+ # By default we support direct uploads to p.f.c since the UI is a JS SPA
22
+ # and very difficult for us to vendor or provide ourselves. If you are worried
23
+ # about data security and wish to self-host, you can change these URLs.
24
+ profile_view_url: "https://profiler.firefox.com/public/%s",
25
+ profile_store_url: "https://api.profiler.firefox.com/compressed-store"
26
+ }
27
+
28
+ ##
29
+ # Allows users to add custom rows to all of the Job
30
+ # tables, e.g. Retries, Dead, Scheduled, with custom
31
+ # links to other systems, see _job_info.erb and test
32
+ # in web_test.rb
33
+ #
34
+ # Sidekiq::Web.configure do |cfg|
35
+ # cfg.custom_job_info_rows << JobLogLink.new
36
+ # end
37
+ #
38
+ # class JobLogLink
39
+ # def add_pair(job)
40
+ # yield "External Logs", "<a href='https://example.com/logs/#{job.jid}'>Logs for #{job.jid}</a>"
41
+ # end
42
+ # end
43
+ attr_accessor :custom_job_info_rows
44
+
45
+ attr_reader :tabs
46
+ attr_reader :locales
47
+ attr_reader :views
48
+ attr_reader :middlewares
49
+
50
+ # Adds the "Back to App" link in the header
51
+ attr_accessor :app_url
52
+ attr_accessor :assets_path
53
+
54
+ def initialize
55
+ @options = OPTIONS.dup
56
+ @locales = LOCALES
57
+ @views = VIEWS
58
+ @assets_path = ASSETS
59
+ @tabs = DEFAULT_TABS.dup
60
+ @middlewares = []
61
+ @custom_job_info_rows = []
62
+ end
63
+
64
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
65
+
66
+ def use(*args, &block)
67
+ middlewares << [args, block]
68
+ end
69
+
70
+ # Register a class as a Sidekiq Web UI extension. The class should
71
+ # provide one or more tabs which map to an index route. Options:
72
+ #
73
+ # @param extclass [Class] Class which contains the HTTP actions, required
74
+ # @param name [String] the name of the extension, used to namespace assets
75
+ # @param tab [String | Array] labels(s) of the UI tabs
76
+ # @param index [String | Array] index route(s) for each tab
77
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
78
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
79
+ # @param cache_for [Integer] amount of time to cache assets, default one day
80
+ #
81
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
82
+ # and `views/` subdirectories.
83
+ def register_extension(extclass, name:, tab:, index:, root_dir: nil, cache_for: 86400, asset_paths: nil)
84
+ tab = Array(tab)
85
+ index = Array(index)
86
+ tab.zip(index).each do |tab, index|
87
+ tabs[tab] = index
88
+ end
89
+ if root_dir
90
+ locdir = File.join(root_dir, "locales")
91
+ locales << locdir if File.directory?(locdir)
92
+
93
+ if asset_paths && name
94
+ # if you have {root}/assets/{name}/js/scripts.js
95
+ # and {root}/assets/{name}/css/styles.css
96
+ # you would pass in:
97
+ # asset_paths: ["js", "css"]
98
+ # See script_tag and style_tag in web/helpers.rb
99
+ assdir = File.join(root_dir, "assets")
100
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
101
+ assetprops = {
102
+ urls: assurls,
103
+ root: assdir,
104
+ cascade: true
105
+ }
106
+ assetprops[:header_rules] = [[:all, {"cache-control" => "private, max-age=#{cache_for.to_i}"}]] if cache_for
107
+ middlewares << [[Rack::Static, assetprops], nil]
108
+ end
109
+ end
110
+
111
+ yield self if block_given?
112
+ extclass.registered(Web::Application)
113
+ end
114
+ alias_method :register, :register_extension
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,474 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "cgi/escape"
5
+
6
+ module Sidekiq
7
+ # These methods are available to pages within the Web UI and UI extensions.
8
+ # They are not public APIs for applications to use.
9
+ module WebHelpers
10
+ def store_name
11
+ hash = redis_info
12
+ return "Dragonfly" if hash.has_key?("dragonfly_version")
13
+ return "Valkey" if hash.has_key?("valkey_version")
14
+ "Redis"
15
+ end
16
+
17
+ def store_version
18
+ hash = redis_info
19
+ return hash["dragonfly_version"] if hash.has_key?("dragonfly_version")
20
+ return hash["valkey_version"] if hash.has_key?("valkey_version")
21
+ hash["redis_version"]
22
+ end
23
+
24
+ def style_tag(location, **kwargs)
25
+ global = location.match?(/:\/\//)
26
+ location = root_path + location if !global && !location.start_with?(root_path)
27
+ attrs = {
28
+ type: "text/css",
29
+ media: "screen",
30
+ rel: "stylesheet",
31
+ nonce: csp_nonce,
32
+ href: location
33
+ }
34
+ add_to_head do
35
+ html_tag(:link, attrs.merge(kwargs))
36
+ end
37
+ end
38
+
39
+ def script_tag(location, **kwargs)
40
+ global = location.match?(/:\/\//)
41
+ location = root_path + location if !global && !location.start_with?(root_path)
42
+ attrs = {
43
+ type: "text/javascript",
44
+ nonce: csp_nonce,
45
+ src: location
46
+ }
47
+ html_tag(:script, attrs.merge(kwargs)) {}
48
+ end
49
+
50
+ # NB: keys and values are not escaped; do not allow user input
51
+ # in the attributes
52
+ private def html_tag(tagname, attrs)
53
+ s = "<#{tagname}"
54
+ attrs.each_pair do |k, v|
55
+ next unless v
56
+ s << " #{k}=\"#{v}\""
57
+ end
58
+ if block_given?
59
+ s << ">"
60
+ yield s
61
+ s << "</#{tagname}>"
62
+ else
63
+ s << " />"
64
+ end
65
+ s
66
+ end
67
+
68
+ def strings(lang)
69
+ @@strings ||= {}
70
+
71
+ # Allow sidekiq-web extensions to add locale paths
72
+ # so extensions can be localized
73
+ @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
74
+ find_locale_files(lang).each do |file|
75
+ strs = parse_yaml(file)
76
+ global.merge!(strs[lang])
77
+ end
78
+ end
79
+ end
80
+
81
+ def parse_yaml(path)
82
+ locale = nil
83
+ map = {}
84
+ IO.readlines(path, chomp: true).each do |line|
85
+ case line
86
+ when /\A\s*\#.*/
87
+ # line comment
88
+ when !locale && /\A([a-zA-Z\-_]+):/
89
+ locale = $1
90
+ map[locale] = {}
91
+ when /\A\s+(\w+):\s+(.+)\z/
92
+ # A few values have double quotes to include special characters in YAML.
93
+ # Strip them off manually as our greedy match will include them.
94
+ key = $1
95
+ s = $2
96
+ s = s[1..] if s[0] == "\""
97
+ s = s[0..-2] if s[-1] == "\""
98
+ map[locale][key] = s
99
+ else
100
+ raise ArgumentError, "unable to parse #{path}: #{line}"
101
+ end
102
+ end
103
+ map
104
+ end
105
+
106
+ def to_json(x)
107
+ Sidekiq.dump_json(x)
108
+ end
109
+
110
+ def singularize(str, count)
111
+ if count == 1 && str.respond_to?(:singularize) # rails
112
+ str.singularize
113
+ else
114
+ str
115
+ end
116
+ end
117
+
118
+ def clear_caches
119
+ @@strings = nil
120
+ @@locale_files = nil
121
+ @@available_locales = nil
122
+ end
123
+
124
+ def locale_files
125
+ @@locale_files ||= config.locales.flat_map { |path|
126
+ Dir["#{path}/*.yml"]
127
+ }
128
+ end
129
+
130
+ def available_locales
131
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
132
+ end
133
+
134
+ def find_locale_files(lang)
135
+ locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
136
+ end
137
+
138
+ def language_name(locale)
139
+ strings(locale).fetch("LanguageName", locale)
140
+ end
141
+
142
+ def search(jobset, substr)
143
+ resultset = jobset.scan(substr).to_a
144
+ @current_page = 1
145
+ @count = @total_size = resultset.size
146
+ resultset
147
+ end
148
+
149
+ def filtering(which, placeholder_key: "AnyJobContent", label_key: "Filter")
150
+ erb(:filtering, locals: {which:, placeholder_key:, label_key:})
151
+ end
152
+
153
+ def filter_link(str, within = "retries")
154
+ hstr = h(str)
155
+ if within.nil?
156
+ hstr
157
+ else
158
+ "<a href='#{root_path}#{within}?substr=#{hstr}'>#{hstr}</a>"
159
+ end
160
+ end
161
+
162
+ def display_tags(job, within = "retries")
163
+ job.tags.map { |tag|
164
+ "<span class='label label-info jobtag jobtag-#{h(tag)}'>#{filter_link(tag, within)}</span>"
165
+ }.join(" ")
166
+ end
167
+
168
+ # This view helper provide ability display you html code in
169
+ # to head of page. Example:
170
+ #
171
+ # <% add_to_head do %>
172
+ # <link rel="stylesheet" .../>
173
+ # <meta .../>
174
+ # <% end %>
175
+ #
176
+ def add_to_head
177
+ @head_html ||= []
178
+ @head_html << yield.dup if block_given?
179
+ end
180
+
181
+ def display_custom_head
182
+ @head_html.join if defined?(@head_html)
183
+ end
184
+
185
+ def text_direction
186
+ get_locale["TextDirection"] || "ltr"
187
+ end
188
+
189
+ def rtl?
190
+ text_direction == "rtl"
191
+ end
192
+
193
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
194
+ # Returns an array of language tags ordered by their quality value
195
+ #
196
+ # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
197
+ def user_preferred_languages
198
+ languages = env["HTTP_ACCEPT_LANGUAGE"]
199
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
200
+ locale, quality = language.split(";q=", 2)
201
+ locale = nil if locale == "*" # Ignore wildcards
202
+ quality = quality ? quality.to_f : 1.0
203
+ [locale, quality]
204
+ }.sort { |(_, left), (_, right)|
205
+ right <=> left
206
+ }.map(&:first).compact
207
+ end
208
+
209
+ # Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2"
210
+ # this method will try to best match the available locales to the user's preferred languages.
211
+ def locale
212
+ # session[:locale] is set via the locale selector from the footer
213
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
214
+ l
215
+ else
216
+ matched_locale = nil
217
+ # Attempt to find a case-insensitive exact match first
218
+ user_preferred_languages.each do |preferred|
219
+ # We only care about the language and primary subtag
220
+ # "en-GB-oxendict" becomes "en-GB"
221
+ language_tag = preferred.split("-")[0..1].join("-")
222
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
223
+ break if matched_locale
224
+ end
225
+
226
+ return matched_locale if matched_locale
227
+
228
+ # Find the first base language match
229
+ # "en-US,es-MX;q=0.9" matches "en"
230
+ user_preferred_languages.each do |preferred|
231
+ base_language = preferred.split("-", 2).first
232
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
233
+ break if matched_locale
234
+ end
235
+
236
+ matched_locale || "en"
237
+ end
238
+ end
239
+
240
+ # sidekiq/sidekiq#3243
241
+ def unfiltered?
242
+ s = url_params("substr")
243
+ yield unless s && s.size > 0
244
+ end
245
+
246
+ def get_locale
247
+ strings(locale)
248
+ end
249
+
250
+ def t(msg, options = {})
251
+ string = get_locale[msg] || strings("en")[msg] || msg
252
+ if options.empty?
253
+ string
254
+ else
255
+ string % options
256
+ end
257
+ end
258
+
259
+ def sort_direction_label
260
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
261
+ end
262
+
263
+ def workset
264
+ @work ||= Sidekiq::WorkSet.new
265
+ end
266
+
267
+ def processes
268
+ @processes ||= Sidekiq::ProcessSet.new
269
+ end
270
+
271
+ # Sorts processes by hostname following the natural sort order
272
+ def sorted_processes
273
+ @sorted_processes ||= begin
274
+ return processes unless processes.all? { |p| p["hostname"] }
275
+
276
+ processes.to_a.sort_by do |process|
277
+ # Kudos to `shurikk` on StackOverflow
278
+ # https://stackoverflow.com/a/15170063/575547
279
+ process["hostname"].split(/(\d+)/).map { |a| /\d+/.match?(a) ? a.to_i : a }
280
+ end
281
+ end
282
+ end
283
+
284
+ def stats
285
+ @stats ||= Sidekiq::Stats.new
286
+ end
287
+
288
+ def redis_url
289
+ Sidekiq.redis do |conn|
290
+ conn.config.server_url
291
+ end
292
+ end
293
+
294
+ def redis_info
295
+ @info ||= Sidekiq.default_configuration.redis_info
296
+ end
297
+
298
+ def root_path
299
+ "#{env["SCRIPT_NAME"]}/"
300
+ end
301
+
302
+ def current_path
303
+ @current_path ||= request.path_info.gsub(/^\//, "")
304
+ end
305
+
306
+ def current_status
307
+ (workset.size == 0) ? "idle" : "active"
308
+ end
309
+
310
+ def relative_time(time)
311
+ stamp = time.getutc.iso8601
312
+ %(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
313
+ end
314
+
315
+ def queue_names_by_capsule(pro)
316
+ cap = pro.capsules
317
+ if cap
318
+ cap.map { |k, v| v["weights"].keys.join(", ") }.join("; ")
319
+ else
320
+ # DEPRECATED Backwards compatibility with older processes.
321
+ # 'capsules' element added in v8.0.9
322
+ pro.queues.join(", ")
323
+ end
324
+ end
325
+
326
+ def job_params(job, score)
327
+ "#{score}-#{job["jid"]}"
328
+ end
329
+
330
+ def parse_key(key)
331
+ score, jid = key.split("-", 2)
332
+ [score.to_f, jid]
333
+ end
334
+
335
+ SAFE_QPARAMS = %w[page direction only]
336
+
337
+ # Merge options with current params, filter safe params, and stringify to query string
338
+ def qparams(options)
339
+ stringified_options = options.transform_keys(&:to_s)
340
+
341
+ to_query_string(request.params.merge(stringified_options))
342
+ end
343
+
344
+ def to_query_string(hash)
345
+ hash.map { |key, value|
346
+ SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
347
+ }.compact.join("&")
348
+ end
349
+
350
+ def truncate(text, truncate_after_chars = 2000)
351
+ (truncate_after_chars && text.size > truncate_after_chars) ? "#{text[0..truncate_after_chars]}..." : text
352
+ end
353
+
354
+ def display_args(args, truncate_after_chars = 2000)
355
+ return "Invalid job payload, args is nil" if args.nil?
356
+ return "Invalid job payload, args must be an Array, not #{args.class.name}" unless args.is_a?(Array)
357
+
358
+ begin
359
+ args.map { |arg|
360
+ h(truncate(to_display(arg), truncate_after_chars))
361
+ }.join(", ")
362
+ rescue
363
+ "Illegal job arguments: #{h args.inspect}"
364
+ end
365
+ end
366
+
367
+ def csrf_tag
368
+ ""
369
+ end
370
+
371
+ def csp_nonce
372
+ env[:csp_nonce]
373
+ end
374
+
375
+ def to_display(arg)
376
+ arg.inspect
377
+ rescue
378
+ begin
379
+ arg.to_s
380
+ rescue => ex
381
+ "Cannot display argument: [#{ex.class.name}] #{ex.message}"
382
+ end
383
+ end
384
+
385
+ RETRY_JOB_KEYS = Set.new(%w[
386
+ queue class args retry_count retried_at failed_at
387
+ jid error_message error_class backtrace
388
+ error_backtrace enqueued_at retry wrapped
389
+ created_at tags display_class
390
+ ])
391
+
392
+ def retry_extra_items(retry_job)
393
+ @retry_extra_items ||= {}.tap do |extra|
394
+ retry_job.item.each do |key, value|
395
+ extra[key] = value unless RETRY_JOB_KEYS.include?(key)
396
+ end
397
+ end
398
+ end
399
+
400
+ def format_memory(rss_kb)
401
+ return "0" if rss_kb.nil? || rss_kb == 0
402
+
403
+ if rss_kb < 100_000
404
+ "#{number_with_delimiter(rss_kb)} KB"
405
+ elsif rss_kb < 10_000_000
406
+ "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
407
+ else
408
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
409
+ end
410
+ end
411
+
412
+ def number_with_delimiter(number, options = {})
413
+ precision = options[:precision] || 0
414
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
415
+ end
416
+
417
+ def h(text)
418
+ ::CGI.escapeHTML(text.to_s)
419
+ rescue ArgumentError => e
420
+ raise unless e.message.eql?("invalid byte sequence in UTF-8")
421
+ text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
422
+ retry
423
+ end
424
+
425
+ # Any paginated list that performs an action needs to redirect
426
+ # back to the proper page after performing that action.
427
+ def redirect_with_query(url)
428
+ r = request.referer
429
+ if r && r =~ /\?/
430
+ ref = URI(r)
431
+ redirect("#{url}?#{ref.query}")
432
+ else
433
+ redirect url
434
+ end
435
+ end
436
+
437
+ def environment_title_prefix
438
+ environment = Sidekiq.default_configuration[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
439
+
440
+ "[#{environment.upcase}] " unless environment == "production"
441
+ end
442
+
443
+ def product_version
444
+ "Sidekiq v#{Sidekiq::VERSION}"
445
+ end
446
+
447
+ def server_utc_time
448
+ Time.now.utc.strftime("%H:%M:%S UTC")
449
+ end
450
+
451
+ def pollable?
452
+ # there's no point to refreshing the metrics pages every N seconds
453
+ !(current_path == "" || current_path.index("metrics"))
454
+ end
455
+
456
+ def retry_or_delete_or_kill(job, params)
457
+ if params["retry"]
458
+ job.retry
459
+ elsif params["delete"]
460
+ job.delete
461
+ elsif params["kill"]
462
+ job.kill
463
+ end
464
+ end
465
+
466
+ def delete_or_add_queue(job, params)
467
+ if params["delete"]
468
+ job.delete
469
+ elsif params["add_to_queue"]
470
+ job.add_to_queue
471
+ end
472
+ end
473
+ end
474
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Sidekiq
6
+ class Web
7
+ # Provides an API to declare endpoints, along with a match
8
+ # API to dynamically route a request to an endpoint.
9
+ module Router
10
+ def head(path, &) = route(:head, path, &)
11
+
12
+ def get(path, &) = route(:get, path, &)
13
+
14
+ def post(path, &) = route(:post, path, &)
15
+
16
+ def put(path, &) = route(:put, path, &)
17
+
18
+ def patch(path, &) = route(:patch, path, &)
19
+
20
+ def delete(path, &) = route(:delete, path, &)
21
+
22
+ def route(*methods, path, &block)
23
+ methods.each do |method|
24
+ raise ArgumentError, "Invalid method #{method}. Must be one of #{@routes.keys.join(",")}" unless route_cache.has_key?(method)
25
+ route_cache[method] << Route.new(method, path, block)
26
+ end
27
+ end
28
+
29
+ def match(env)
30
+ request_method = env["REQUEST_METHOD"].downcase.to_sym
31
+ path_info = ::URI::RFC2396_PARSER.unescape env["PATH_INFO"]
32
+
33
+ # There are servers which send an empty string when requesting the root.
34
+ # These servers should be ashamed of themselves.
35
+ path_info = "/" if path_info == ""
36
+
37
+ route_cache[request_method].each do |route|
38
+ params = route.match(request_method, path_info)
39
+ if params
40
+ env["rack.route_params"] = params
41
+ return Action.new(env, route.block)
42
+ end
43
+ end
44
+
45
+ nil
46
+ end
47
+
48
+ def route_cache
49
+ @@routes ||= {get: [], post: [], put: [], patch: [], delete: [], head: []}
50
+ end
51
+ end
52
+
53
+ class Route
54
+ attr_accessor :request_method, :pattern, :block, :name
55
+
56
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
57
+
58
+ def initialize(request_method, pattern, block)
59
+ @request_method = request_method
60
+ @pattern = pattern
61
+ @block = block
62
+ end
63
+
64
+ def matcher
65
+ @matcher ||= compile
66
+ end
67
+
68
+ def compile
69
+ if pattern.match?(NAMED_SEGMENTS_PATTERN)
70
+ p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
71
+
72
+ Regexp.new("\\A#{p}\\Z")
73
+ else
74
+ pattern
75
+ end
76
+ end
77
+
78
+ EMPTY = {}.freeze
79
+
80
+ def match(request_method, path)
81
+ case matcher
82
+ when String
83
+ EMPTY if path == matcher
84
+ else
85
+ path_match = path.match(matcher)
86
+ path_match&.named_captures&.transform_keys(&:to_sym)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end