roundhouse-x 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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