roundhouse-x 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 (168) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +16 -0
  4. data/3.0-Upgrade.md +70 -0
  5. data/Changes.md +1127 -0
  6. data/Gemfile +27 -0
  7. data/LICENSE +7 -0
  8. data/README.md +52 -0
  9. data/Rakefile +9 -0
  10. data/bin/roundhouse +19 -0
  11. data/bin/roundhousectl +93 -0
  12. data/lib/generators/roundhouse/templates/worker.rb.erb +9 -0
  13. data/lib/generators/roundhouse/templates/worker_spec.rb.erb +6 -0
  14. data/lib/generators/roundhouse/templates/worker_test.rb.erb +8 -0
  15. data/lib/generators/roundhouse/worker_generator.rb +49 -0
  16. data/lib/roundhouse/actor.rb +39 -0
  17. data/lib/roundhouse/api.rb +859 -0
  18. data/lib/roundhouse/cli.rb +396 -0
  19. data/lib/roundhouse/client.rb +210 -0
  20. data/lib/roundhouse/core_ext.rb +105 -0
  21. data/lib/roundhouse/exception_handler.rb +30 -0
  22. data/lib/roundhouse/fetch.rb +154 -0
  23. data/lib/roundhouse/launcher.rb +98 -0
  24. data/lib/roundhouse/logging.rb +104 -0
  25. data/lib/roundhouse/manager.rb +236 -0
  26. data/lib/roundhouse/middleware/chain.rb +149 -0
  27. data/lib/roundhouse/middleware/i18n.rb +41 -0
  28. data/lib/roundhouse/middleware/server/active_record.rb +13 -0
  29. data/lib/roundhouse/middleware/server/logging.rb +40 -0
  30. data/lib/roundhouse/middleware/server/retry_jobs.rb +206 -0
  31. data/lib/roundhouse/monitor.rb +124 -0
  32. data/lib/roundhouse/paginator.rb +42 -0
  33. data/lib/roundhouse/processor.rb +159 -0
  34. data/lib/roundhouse/rails.rb +24 -0
  35. data/lib/roundhouse/redis_connection.rb +77 -0
  36. data/lib/roundhouse/scheduled.rb +115 -0
  37. data/lib/roundhouse/testing/inline.rb +28 -0
  38. data/lib/roundhouse/testing.rb +193 -0
  39. data/lib/roundhouse/util.rb +68 -0
  40. data/lib/roundhouse/version.rb +3 -0
  41. data/lib/roundhouse/web.rb +264 -0
  42. data/lib/roundhouse/web_helpers.rb +249 -0
  43. data/lib/roundhouse/worker.rb +90 -0
  44. data/lib/roundhouse.rb +177 -0
  45. data/roundhouse.gemspec +27 -0
  46. data/test/config.yml +9 -0
  47. data/test/env_based_config.yml +11 -0
  48. data/test/fake_env.rb +0 -0
  49. data/test/fixtures/en.yml +2 -0
  50. data/test/helper.rb +49 -0
  51. data/test/test_api.rb +521 -0
  52. data/test/test_cli.rb +389 -0
  53. data/test/test_client.rb +294 -0
  54. data/test/test_exception_handler.rb +55 -0
  55. data/test/test_fetch.rb +206 -0
  56. data/test/test_logging.rb +34 -0
  57. data/test/test_manager.rb +169 -0
  58. data/test/test_middleware.rb +160 -0
  59. data/test/test_monitor.rb +258 -0
  60. data/test/test_processor.rb +176 -0
  61. data/test/test_rails.rb +23 -0
  62. data/test/test_redis_connection.rb +127 -0
  63. data/test/test_retry.rb +390 -0
  64. data/test/test_roundhouse.rb +87 -0
  65. data/test/test_scheduled.rb +120 -0
  66. data/test/test_scheduling.rb +75 -0
  67. data/test/test_testing.rb +78 -0
  68. data/test/test_testing_fake.rb +240 -0
  69. data/test/test_testing_inline.rb +65 -0
  70. data/test/test_util.rb +18 -0
  71. data/test/test_web.rb +605 -0
  72. data/test/test_web_helpers.rb +52 -0
  73. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  74. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  75. data/web/assets/images/logo.png +0 -0
  76. data/web/assets/images/status/active.png +0 -0
  77. data/web/assets/images/status/idle.png +0 -0
  78. data/web/assets/images/status-sd8051fd480.png +0 -0
  79. data/web/assets/javascripts/application.js +83 -0
  80. data/web/assets/javascripts/dashboard.js +300 -0
  81. data/web/assets/javascripts/locales/README.md +27 -0
  82. data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
  83. data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
  84. data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
  85. data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
  86. data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
  87. data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
  88. data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
  89. data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
  90. data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
  91. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
  92. data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
  93. data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
  94. data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
  95. data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
  96. data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
  97. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
  98. data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
  99. data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
  100. data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
  101. data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
  102. data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
  103. data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
  104. data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
  105. data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
  106. data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
  107. data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
  108. data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
  109. data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
  110. data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
  111. data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
  112. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
  113. data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
  114. data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
  115. data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
  116. data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
  117. data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
  118. data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
  119. data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
  120. data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
  121. data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
  122. data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
  123. data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
  124. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
  125. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
  126. data/web/assets/stylesheets/application.css +746 -0
  127. data/web/assets/stylesheets/bootstrap.css +9 -0
  128. data/web/locales/cs.yml +68 -0
  129. data/web/locales/da.yml +68 -0
  130. data/web/locales/de.yml +69 -0
  131. data/web/locales/el.yml +68 -0
  132. data/web/locales/en.yml +77 -0
  133. data/web/locales/es.yml +69 -0
  134. data/web/locales/fr.yml +69 -0
  135. data/web/locales/hi.yml +75 -0
  136. data/web/locales/it.yml +69 -0
  137. data/web/locales/ja.yml +69 -0
  138. data/web/locales/ko.yml +68 -0
  139. data/web/locales/nl.yml +68 -0
  140. data/web/locales/no.yml +69 -0
  141. data/web/locales/pl.yml +59 -0
  142. data/web/locales/pt-br.yml +68 -0
  143. data/web/locales/pt.yml +67 -0
  144. data/web/locales/ru.yml +75 -0
  145. data/web/locales/sv.yml +68 -0
  146. data/web/locales/ta.yml +75 -0
  147. data/web/locales/zh-cn.yml +68 -0
  148. data/web/locales/zh-tw.yml +68 -0
  149. data/web/views/_footer.erb +22 -0
  150. data/web/views/_job_info.erb +84 -0
  151. data/web/views/_nav.erb +66 -0
  152. data/web/views/_paging.erb +23 -0
  153. data/web/views/_poll_js.erb +5 -0
  154. data/web/views/_poll_link.erb +7 -0
  155. data/web/views/_status.erb +4 -0
  156. data/web/views/_summary.erb +40 -0
  157. data/web/views/busy.erb +90 -0
  158. data/web/views/dashboard.erb +75 -0
  159. data/web/views/dead.erb +34 -0
  160. data/web/views/layout.erb +31 -0
  161. data/web/views/morgue.erb +71 -0
  162. data/web/views/queue.erb +45 -0
  163. data/web/views/queues.erb +27 -0
  164. data/web/views/retries.erb +74 -0
  165. data/web/views/retry.erb +34 -0
  166. data/web/views/scheduled.erb +54 -0
  167. data/web/views/scheduled_job_info.erb +8 -0
  168. metadata +404 -0
@@ -0,0 +1,264 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'sinatra/base'
4
+
5
+ require 'roundhouse'
6
+ require 'roundhouse/api'
7
+ require 'roundhouse/paginator'
8
+ require 'roundhouse/web_helpers'
9
+
10
+ module Roundhouse
11
+ class Web < Sinatra::Base
12
+ include Roundhouse::Paginator
13
+
14
+ enable :sessions
15
+ use Rack::Protection, :use => :authenticity_token unless ENV['RACK_ENV'] == 'test'
16
+
17
+ set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
18
+ set :public_folder, proc { "#{root}/assets" }
19
+ set :views, proc { "#{root}/views" }
20
+ set :locales, ["#{root}/locales"]
21
+
22
+ helpers WebHelpers
23
+
24
+ DEFAULT_TABS = {
25
+ "Dashboard" => '',
26
+ "Busy" => 'busy',
27
+ "Queues" => 'queues',
28
+ "Retries" => 'retries',
29
+ "Scheduled" => 'scheduled',
30
+ "Dead" => 'morgue',
31
+ }
32
+
33
+ class << self
34
+ def default_tabs
35
+ DEFAULT_TABS
36
+ end
37
+
38
+ def custom_tabs
39
+ @custom_tabs ||= {}
40
+ end
41
+ alias_method :tabs, :custom_tabs
42
+
43
+ attr_accessor :app_url
44
+ end
45
+
46
+ get "/busy" do
47
+ erb :busy
48
+ end
49
+
50
+ post "/busy" do
51
+ if params['identity']
52
+ p = Roundhouse::Process.new('identity' => params['identity'])
53
+ p.quiet! if params[:quiet]
54
+ p.stop! if params[:stop]
55
+ else
56
+ processes.each do |pro|
57
+ pro.quiet! if params[:quiet]
58
+ pro.stop! if params[:stop]
59
+ end
60
+ end
61
+ redirect "#{root_path}busy"
62
+ end
63
+
64
+ get "/queues" do
65
+ @queues = Roundhouse::Queue.all
66
+ erb :queues
67
+ end
68
+
69
+ get "/queues/:name" do
70
+ halt 404 unless params[:name]
71
+ @count = (params[:count] || 25).to_i
72
+ @name = params[:name]
73
+ @queue = Roundhouse::Queue.new(@name)
74
+ (@current_page, @total_size, @messages) = page("queue:#{@name}", params[:page], @count)
75
+ @messages = @messages.map { |msg| Roundhouse::Job.new(msg, @name) }
76
+ erb :queue
77
+ end
78
+
79
+ post "/queues/:name" do
80
+ Roundhouse::Queue.new(params[:name]).clear
81
+ redirect "#{root_path}queues"
82
+ end
83
+
84
+ post "/queues/:name/delete" do
85
+ Roundhouse::Job.new(params[:key_val], params[:name]).delete
86
+ redirect_with_query("#{root_path}queues/#{params[:name]}")
87
+ end
88
+
89
+ get '/morgue' do
90
+ @count = (params[:count] || 25).to_i
91
+ (@current_page, @total_size, @dead) = page("dead", params[:page], @count, reverse: true)
92
+ @dead = @dead.map { |msg, score| Roundhouse::SortedEntry.new(nil, score, msg) }
93
+ erb :morgue
94
+ end
95
+
96
+ get "/morgue/:key" do
97
+ halt 404 unless params['key']
98
+ @dead = Roundhouse::DeadSet.new.fetch(*parse_params(params['key'])).first
99
+ redirect "#{root_path}morgue" if @dead.nil?
100
+ erb :dead
101
+ end
102
+
103
+ post '/morgue' do
104
+ redirect request.path unless params['key']
105
+
106
+ params['key'].each do |key|
107
+ job = Roundhouse::DeadSet.new.fetch(*parse_params(key)).first
108
+ retry_or_delete_or_kill job, params if job
109
+ end
110
+ redirect_with_query("#{root_path}morgue")
111
+ end
112
+
113
+ post "/morgue/all/delete" do
114
+ Roundhouse::DeadSet.new.clear
115
+ redirect "#{root_path}morgue"
116
+ end
117
+
118
+ post "/morgue/all/retry" do
119
+ Roundhouse::DeadSet.new.retry_all
120
+ redirect "#{root_path}morgue"
121
+ end
122
+
123
+ post "/morgue/:key" do
124
+ halt 404 unless params['key']
125
+ job = Roundhouse::DeadSet.new.fetch(*parse_params(params['key'])).first
126
+ retry_or_delete_or_kill job, params if job
127
+ redirect_with_query("#{root_path}morgue")
128
+ end
129
+
130
+
131
+ get '/retries' do
132
+ @count = (params[:count] || 25).to_i
133
+ (@current_page, @total_size, @retries) = page("retry", params[:page], @count)
134
+ @retries = @retries.map { |msg, score| Roundhouse::SortedEntry.new(nil, score, msg) }
135
+ erb :retries
136
+ end
137
+
138
+ get "/retries/:key" do
139
+ @retry = Roundhouse::RetrySet.new.fetch(*parse_params(params['key'])).first
140
+ redirect "#{root_path}retries" if @retry.nil?
141
+ erb :retry
142
+ end
143
+
144
+ post '/retries' do
145
+ redirect request.path unless params['key']
146
+
147
+ params['key'].each do |key|
148
+ job = Roundhouse::RetrySet.new.fetch(*parse_params(key)).first
149
+ retry_or_delete_or_kill job, params if job
150
+ end
151
+ redirect_with_query("#{root_path}retries")
152
+ end
153
+
154
+ post "/retries/all/delete" do
155
+ Roundhouse::RetrySet.new.clear
156
+ redirect "#{root_path}retries"
157
+ end
158
+
159
+ post "/retries/all/retry" do
160
+ Roundhouse::RetrySet.new.retry_all
161
+ redirect "#{root_path}retries"
162
+ end
163
+
164
+ post "/retries/:key" do
165
+ job = Roundhouse::RetrySet.new.fetch(*parse_params(params['key'])).first
166
+ retry_or_delete_or_kill job, params if job
167
+ redirect_with_query("#{root_path}retries")
168
+ end
169
+
170
+ get '/scheduled' do
171
+ @count = (params[:count] || 25).to_i
172
+ (@current_page, @total_size, @scheduled) = page("schedule", params[:page], @count)
173
+ @scheduled = @scheduled.map { |msg, score| Roundhouse::SortedEntry.new(nil, score, msg) }
174
+ erb :scheduled
175
+ end
176
+
177
+ get "/scheduled/:key" do
178
+ @job = Roundhouse::ScheduledSet.new.fetch(*parse_params(params['key'])).first
179
+ redirect "#{root_path}scheduled" if @job.nil?
180
+ erb :scheduled_job_info
181
+ end
182
+
183
+ post '/scheduled' do
184
+ redirect request.path unless params['key']
185
+
186
+ params['key'].each do |key|
187
+ job = Roundhouse::ScheduledSet.new.fetch(*parse_params(key)).first
188
+ delete_or_add_queue job, params if job
189
+ end
190
+ redirect_with_query("#{root_path}scheduled")
191
+ end
192
+
193
+ post "/scheduled/:key" do
194
+ halt 404 unless params['key']
195
+ job = Roundhouse::ScheduledSet.new.fetch(*parse_params(params['key'])).first
196
+ delete_or_add_queue job, params if job
197
+ redirect_with_query("#{root_path}scheduled")
198
+ end
199
+
200
+ get '/' do
201
+ @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
202
+ stats_history = Roundhouse::Stats::History.new((params[:days] || 30).to_i)
203
+ @processed_history = stats_history.processed
204
+ @failed_history = stats_history.failed
205
+ erb :dashboard
206
+ end
207
+
208
+ REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
209
+
210
+ get '/dashboard/stats' do
211
+ redirect "#{root_path}stats"
212
+ end
213
+
214
+ get '/stats' do
215
+ roundhouse_stats = Roundhouse::Stats.new
216
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
217
+
218
+ content_type :json
219
+ Roundhouse.dump_json(
220
+ roundhouse: {
221
+ processed: roundhouse_stats.processed,
222
+ failed: roundhouse_stats.failed,
223
+ busy: roundhouse_stats.workers_size,
224
+ processes: roundhouse_stats.processes_size,
225
+ enqueued: roundhouse_stats.enqueued,
226
+ scheduled: roundhouse_stats.scheduled_size,
227
+ retries: roundhouse_stats.retry_size,
228
+ dead: roundhouse_stats.dead_size,
229
+ default_latency: roundhouse_stats.default_queue_latency
230
+ },
231
+ redis: redis_stats
232
+ )
233
+ end
234
+
235
+ get '/stats/queues' do
236
+ queue_stats = Roundhouse::Stats::Queues.new
237
+
238
+ content_type :json
239
+ Roundhouse.dump_json(
240
+ queue_stats.lengths
241
+ )
242
+ end
243
+
244
+ private
245
+
246
+ def retry_or_delete_or_kill job, params
247
+ if params['retry']
248
+ job.retry
249
+ elsif params['delete']
250
+ job.delete
251
+ elsif params['kill']
252
+ job.kill
253
+ end
254
+ end
255
+
256
+ def delete_or_add_queue job, params
257
+ if params['delete']
258
+ job.delete
259
+ elsif params['add_to_queue']
260
+ job.add_to_queue
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,249 @@
1
+ require 'uri'
2
+
3
+ module Roundhouse
4
+ # This is not a public API
5
+ module WebHelpers
6
+ def strings(lang)
7
+ @@strings ||= {}
8
+ @@strings[lang] ||= begin
9
+ # Allow roundhouse-web extensions to add locale paths
10
+ # so extensions can be localized
11
+ settings.locales.each_with_object({}) do |path, global|
12
+ find_locale_files(lang).each do |file|
13
+ strs = YAML.load(File.open(file))
14
+ global.deep_merge!(strs[lang])
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def locale_files
21
+ @@locale_files = settings.locales.flat_map do |path|
22
+ Dir["#{path}/*.yml"]
23
+ end
24
+ end
25
+
26
+ def find_locale_files(lang)
27
+ locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
28
+ end
29
+
30
+ # This is a hook for a Roundhouse Pro feature. Please don't touch.
31
+ def filtering(*)
32
+ end
33
+
34
+ # This view helper provide ability display you html code in
35
+ # to head of page. Example:
36
+ #
37
+ # <% add_to_head do %>
38
+ # <link rel="stylesheet" .../>
39
+ # <meta .../>
40
+ # <% end %>
41
+ #
42
+ def add_to_head(&block)
43
+ @head_html ||= []
44
+ @head_html << block if block_given?
45
+ end
46
+
47
+ def display_custom_head
48
+ return unless defined?(@head_html)
49
+ @head_html.map { |block| capture(&block) }.join
50
+ end
51
+
52
+ # Simple capture method for erb templates. The origin was
53
+ # capture method from sinatra-contrib library.
54
+ def capture(&block)
55
+ block.call
56
+ eval('', block.binding)
57
+ end
58
+
59
+ # Given a browser request Accept-Language header like
60
+ # "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2", this function
61
+ # will return "fr" since that's the first code with a matching
62
+ # locale in web/locales
63
+ def locale
64
+ @locale ||= begin
65
+ locale = 'en'.freeze
66
+ languages = request.env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
67
+ languages.downcase.split(','.freeze).each do |lang|
68
+ next if lang == '*'.freeze
69
+ lang = lang.split(';'.freeze)[0]
70
+ break locale = lang if find_locale_files(lang).any?
71
+ end
72
+ locale
73
+ end
74
+ end
75
+
76
+ def get_locale
77
+ strings(locale)
78
+ end
79
+
80
+ def t(msg, options={})
81
+ string = get_locale[msg] || msg
82
+ if options.empty?
83
+ string
84
+ else
85
+ string % options
86
+ end
87
+ end
88
+
89
+ def workers
90
+ @workers ||= Roundhouse::Workers.new
91
+ end
92
+
93
+ def processes
94
+ @processes ||= Roundhouse::ProcessSet.new
95
+ end
96
+
97
+ def stats
98
+ @stats ||= Roundhouse::Stats.new
99
+ end
100
+
101
+ def retries_with_score(score)
102
+ Roundhouse.redis do |conn|
103
+ conn.zrangebyscore('retry', score, score)
104
+ end.map { |msg| Roundhouse.load_json(msg) }
105
+ end
106
+
107
+ def location
108
+ Roundhouse.redis { |conn| conn.client.location }
109
+ end
110
+
111
+ def redis_connection
112
+ Roundhouse.redis { |conn| conn.client.id }
113
+ end
114
+
115
+ def namespace
116
+ @@ns ||= Roundhouse.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
117
+ end
118
+
119
+ def redis_info
120
+ Roundhouse.redis do |conn|
121
+ # admin commands can't go through redis-namespace starting
122
+ # in redis-namespace 2.0
123
+ if conn.respond_to?(:namespace)
124
+ conn.redis.info
125
+ else
126
+ conn.info
127
+ end
128
+ end
129
+ end
130
+
131
+ def root_path
132
+ "#{env['SCRIPT_NAME']}/"
133
+ end
134
+
135
+ def current_path
136
+ @current_path ||= request.path_info.gsub(/^\//,'')
137
+ end
138
+
139
+ def current_status
140
+ workers.size == 0 ? 'idle' : 'active'
141
+ end
142
+
143
+ def relative_time(time)
144
+ %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
145
+ end
146
+
147
+ def job_params(job, score)
148
+ "#{score}-#{job['jid']}"
149
+ end
150
+
151
+ def parse_params(params)
152
+ score, jid = params.split("-")
153
+ [score.to_f, jid]
154
+ end
155
+
156
+ SAFE_QPARAMS = %w(page poll)
157
+
158
+ # Merge options with current params, filter safe params, and stringify to query string
159
+ def qparams(options)
160
+ options = options.stringify_keys
161
+ params.merge(options).map do |key, value|
162
+ SAFE_QPARAMS.include?(key) ? "#{key}=#{value}" : next
163
+ end.join("&")
164
+ end
165
+
166
+ def truncate(text, truncate_after_chars = 2000)
167
+ truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
168
+ end
169
+
170
+ def display_args(args, truncate_after_chars = 2000)
171
+ args.map do |arg|
172
+ h(truncate(to_display(arg)))
173
+ end.join(", ")
174
+ end
175
+
176
+ def csrf_tag
177
+ "<input type='hidden' name='authenticity_token' value='#{session[:csrf]}'/>"
178
+ end
179
+
180
+ def to_display(arg)
181
+ begin
182
+ arg.inspect
183
+ rescue
184
+ begin
185
+ arg.to_s
186
+ rescue => ex
187
+ "Cannot display argument: [#{ex.class.name}] #{ex.message}"
188
+ end
189
+ end
190
+ end
191
+
192
+ RETRY_JOB_KEYS = Set.new(%w(
193
+ queue class args retry_count retried_at failed_at
194
+ jid error_message error_class backtrace
195
+ error_backtrace enqueued_at retry wrapped
196
+ ))
197
+
198
+ def retry_extra_items(retry_job)
199
+ @retry_extra_items ||= {}.tap do |extra|
200
+ retry_job.item.each do |key, value|
201
+ extra[key] = value unless RETRY_JOB_KEYS.include?(key)
202
+ end
203
+ end
204
+ end
205
+
206
+ def number_with_delimiter(number)
207
+ begin
208
+ Float(number)
209
+ rescue ArgumentError, TypeError
210
+ return number
211
+ end
212
+
213
+ options = {delimiter: ',', separator: '.'}
214
+ parts = number.to_s.to_str.split('.')
215
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
216
+ parts.join(options[:separator])
217
+ end
218
+
219
+ def h(text)
220
+ ::Rack::Utils.escape_html(text)
221
+ rescue ArgumentError => e
222
+ raise unless e.message.eql?('invalid byte sequence in UTF-8')
223
+ text.encode!('UTF-16', 'UTF-8', invalid: :replace, replace: '').encode!('UTF-8', 'UTF-16')
224
+ retry
225
+ end
226
+
227
+ # Any paginated list that performs an action needs to redirect
228
+ # back to the proper page after performing that action.
229
+ def redirect_with_query(url)
230
+ r = request.referer
231
+ if r && r =~ /\?/
232
+ ref = URI(r)
233
+ redirect("#{url}?#{ref.query}")
234
+ else
235
+ redirect url
236
+ end
237
+ end
238
+
239
+ def environment_title_prefix
240
+ environment = Roundhouse.options[:environment] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
241
+
242
+ "[#{environment.upcase}] " unless environment == "production"
243
+ end
244
+
245
+ def product_version
246
+ "Roundhouse v#{Roundhouse::VERSION}"
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,90 @@
1
+ require 'roundhouse/client'
2
+ require 'roundhouse/core_ext'
3
+
4
+ module Roundhouse
5
+
6
+ ##
7
+ # Include this module in your worker class and you can easily create
8
+ # asynchronous jobs:
9
+ #
10
+ # class HardWorker
11
+ # include Roundhouse::Worker
12
+ #
13
+ # def perform(*args)
14
+ # # do some work
15
+ # end
16
+ # end
17
+ #
18
+ # Then in your Rails app, you can do this:
19
+ #
20
+ # HardWorker.perform_async(queue_id, 1, 2, 3)
21
+ #
22
+ # Note that perform_async is a class method, perform is an instance method.
23
+ module Worker
24
+ attr_accessor :jid
25
+
26
+ def self.included(base)
27
+ raise ArgumentError, "You cannot include Roundhouse::Worker in an ActiveJob: #{base.name}" if base.ancestors.any? {|c| c.name == 'ActiveJob::Base' }
28
+
29
+ base.extend(ClassMethods)
30
+ base.class_attribute :roundhouse_options_hash
31
+ base.class_attribute :roundhouse_retry_in_block
32
+ base.class_attribute :roundhouse_retries_exhausted_block
33
+ end
34
+
35
+ def logger
36
+ Roundhouse.logger
37
+ end
38
+
39
+ module ClassMethods
40
+
41
+ def perform_async(queue_id, *args)
42
+ client_push('queue_id' => queue_id, 'class' => self, 'args' => args)
43
+ end
44
+
45
+ def perform_in(queue_id, interval, *args)
46
+ int = interval.to_f
47
+ now = Time.now
48
+ ts = (int < 1_000_000_000 ? (now + interval).to_f : int)
49
+
50
+ item = { 'queue_id' => queue_id, 'class' => self, 'args' => args, 'at' => ts }
51
+
52
+ # Optimization to enqueue something now that is scheduled to go out now or in the past
53
+ item.delete('at'.freeze) if ts <= now.to_f
54
+
55
+ client_push(item)
56
+ end
57
+ alias_method :perform_at, :perform_in
58
+
59
+ ##
60
+ # Allows customization for this type of Worker.
61
+ # Legal options:
62
+ #
63
+ # :retry - enable the RetryJobs middleware for this Worker, default *true*
64
+ # :backtrace - whether to save any error backtrace in the retry payload to display in web UI,
65
+ # can be true, false or an integer number of lines to save, default *false*
66
+ # :pool - use the given Redis connection pool to push this type of job to a given shard.
67
+ def roundhouse_options(opts={})
68
+ self.roundhouse_options_hash = get_roundhouse_options.merge(opts.stringify_keys)
69
+ end
70
+
71
+ def roundhouse_retry_in(&block)
72
+ self.roundhouse_retry_in_block = block
73
+ end
74
+
75
+ def roundhouse_retries_exhausted(&block)
76
+ self.roundhouse_retries_exhausted_block = block
77
+ end
78
+
79
+ def get_roundhouse_options # :nodoc:
80
+ self.roundhouse_options_hash ||= Roundhouse.default_worker_options
81
+ end
82
+
83
+ def client_push(item) # :nodoc:
84
+ pool = Thread.current[:roundhouse_via_pool] || get_roundhouse_options['pool'] || Roundhouse.redis_pool
85
+ Roundhouse::Client.new(pool).push(item.stringify_keys)
86
+ end
87
+
88
+ end
89
+ end
90
+ end