PageRenderTime 2741ms CodeModel.GetById 41ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/qless/server.rb

https://github.com/CalculatedContent/qless
Ruby | 440 lines | 353 code | 56 blank | 31 comment | 17 complexity | ac0d9796cab97569d4fe39eff146fee5 MD5 | raw file
  1. require 'sinatra/base'
  2. require 'qless'
  3. # Much of this is shamelessly poached from the resque web client
  4. module Qless
  5. class Server < Sinatra::Base
  6. # Path-y-ness
  7. dir = File.dirname(File.expand_path(__FILE__))
  8. set :views , "#{dir}/server/views"
  9. set :public_folder, "#{dir}/server/static"
  10. # For debugging purposes at least, I want this
  11. set :reload_templates, true
  12. # I'm not sure what this option is -- I'll look it up later
  13. # set :static, true
  14. attr_reader :client
  15. def initialize(client)
  16. @client = client
  17. super
  18. end
  19. helpers do
  20. include Rack::Utils
  21. def url_path(*path_parts)
  22. [ path_prefix, path_parts ].join("/").squeeze('/')
  23. end
  24. alias_method :u, :url_path
  25. def path_prefix
  26. request.env['SCRIPT_NAME']
  27. end
  28. def url_with_modified_query
  29. url = URI(request.url)
  30. existing_query = Rack::Utils.parse_query(url.query)
  31. url.query = Rack::Utils.build_query(yield existing_query)
  32. url.to_s
  33. end
  34. def page_url(offset)
  35. url_with_modified_query do |query|
  36. query.merge('page' => current_page + offset)
  37. end
  38. end
  39. def next_page_url
  40. page_url 1
  41. end
  42. def prev_page_url
  43. page_url -1
  44. end
  45. def current_page
  46. @current_page ||= begin
  47. Integer(params[:page])
  48. rescue
  49. 1
  50. end
  51. end
  52. PAGE_SIZE = 25
  53. def pagination_values
  54. start = (current_page - 1) * PAGE_SIZE
  55. [start, start + PAGE_SIZE]
  56. end
  57. def paginated(qless_object, method, *args)
  58. qless_object.send(method, *(args + pagination_values))
  59. end
  60. def tabs
  61. return [
  62. {:name => 'Queues' , :path => '/queues' },
  63. {:name => 'Workers' , :path => '/workers' },
  64. {:name => 'Track' , :path => '/track' },
  65. {:name => 'Failed' , :path => '/failed' },
  66. {:name => 'Config' , :path => '/config' },
  67. {:name => 'About' , :path => '/about' }
  68. ]
  69. end
  70. def application_name
  71. return client.config['application']
  72. end
  73. def queues
  74. return client.queues.counts
  75. end
  76. def tracked
  77. return client.jobs.tracked
  78. end
  79. def workers
  80. return client.workers.counts
  81. end
  82. def failed
  83. return client.jobs.failed
  84. end
  85. # Return the supplied object back as JSON
  86. def json(obj)
  87. content_type :json
  88. obj.to_json
  89. end
  90. # Make the id acceptable as an id / att in HTML
  91. def sanitize_attr(attr)
  92. return attr.gsub(/[^a-zA-Z\:\_]/, '-')
  93. end
  94. # What are the top tags? Since it might go on, say, every
  95. # page, then we should probably be caching it
  96. def top_tags
  97. @top_tags ||= {
  98. :top => client.tags,
  99. :fetched => Time.now
  100. }
  101. if (Time.now - @top_tags[:fetched]) > 60 then
  102. @top_tags = {
  103. :top => client.tags,
  104. :fetched => Time.now
  105. }
  106. end
  107. @top_tags[:top]
  108. end
  109. def strftime(t)
  110. # From http://stackoverflow.com/questions/195740/how-do-you-do-relative-time-in-rails
  111. diff_seconds = Time.now - t
  112. case diff_seconds
  113. when 0 .. 59
  114. "#{diff_seconds.to_i} seconds ago"
  115. when 60 ... 3600
  116. "#{(diff_seconds/60).to_i} minutes ago"
  117. when 3600 ... 3600*24
  118. "#{(diff_seconds/3600).to_i} hours ago"
  119. when (3600*24) ... (3600*24*30)
  120. "#{(diff_seconds/(3600*24)).to_i} days ago"
  121. else
  122. t.strftime('%b %e, %Y %H:%M:%S %Z (%z)')
  123. end
  124. end
  125. end
  126. get '/?' do
  127. erb :overview, :layout => true, :locals => { :title => "Overview" }
  128. end
  129. # Returns a JSON blob with the job counts for various queues
  130. get '/queues.json' do
  131. json(client.queues.counts)
  132. end
  133. get '/queues/?' do
  134. erb :queues, :layout => true, :locals => {
  135. :title => 'Queues'
  136. }
  137. end
  138. # Return the job counts for a specific queue
  139. get '/queues/:name.json' do
  140. json(client.queues[params[:name]].counts)
  141. end
  142. filtered_tabs = %w[ running scheduled stalled depends recurring ].to_set
  143. get '/queues/:name/?:tab?' do
  144. queue = client.queues[params[:name]]
  145. tab = params.fetch('tab', 'stats')
  146. jobs = if tab == 'waiting'
  147. queue.peek(20)
  148. elsif filtered_tabs.include?(tab)
  149. paginated(queue.jobs, tab).map { |jid| client.jobs[jid] }
  150. else
  151. []
  152. end
  153. erb :queue, :layout => true, :locals => {
  154. :title => "Queue #{params[:name]}",
  155. :tab => tab,
  156. :jobs => jobs,
  157. :queue => client.queues[params[:name]].counts,
  158. :stats => queue.stats
  159. }
  160. end
  161. get '/failed.json' do
  162. json(client.jobs.failed)
  163. end
  164. get '/failed/?' do
  165. # qless-core doesn't provide functionality this way, so we'll
  166. # do it ourselves. I'm not sure if this is how the core library
  167. # should behave or not.
  168. erb :failed, :layout => true, :locals => {
  169. :title => 'Failed',
  170. :failed => client.jobs.failed.keys.map { |t| client.jobs.failed(t).tap { |f| f['type'] = t } }
  171. }
  172. end
  173. get '/failed/:type/?' do
  174. erb :failed_type, :layout => true, :locals => {
  175. :title => 'Failed | ' + params[:type],
  176. :type => params[:type],
  177. :failed => paginated(client.jobs, :failed, params[:type])
  178. }
  179. end
  180. get '/track/?' do
  181. erb :track, :layout => true, :locals => {
  182. :title => 'Track'
  183. }
  184. end
  185. get '/jobs/:jid' do
  186. erb :job, :layout => true, :locals => {
  187. :title => "Job | #{params[:jid]}",
  188. :jid => params[:jid],
  189. :job => client.jobs[params[:jid]]
  190. }
  191. end
  192. get '/workers/?' do
  193. erb :workers, :layout => true, :locals => {
  194. :title => 'Workers'
  195. }
  196. end
  197. get '/workers/:worker' do
  198. erb :worker, :layout => true, :locals => {
  199. :title => 'Worker | ' + params[:worker],
  200. :worker => client.workers[params[:worker]].tap { |w|
  201. w['jobs'] = w['jobs'].map { |j| client.jobs[j] }
  202. w['stalled'] = w['stalled'].map { |j| client.jobs[j] }
  203. w['name'] = params[:worker]
  204. }
  205. }
  206. end
  207. get '/tag/?' do
  208. jobs = paginated(client.jobs, :tagged, params[:tag])
  209. erb :tag, :layout => true, :locals => {
  210. :title => "Tag | #{params[:tag]}",
  211. :tag => params[:tag],
  212. :jobs => jobs['jobs'].map { |jid| client.jobs[jid] },
  213. :total => jobs['total']
  214. }
  215. end
  216. get '/config/?' do
  217. erb :config, :layout => true, :locals => {
  218. :title => 'Config',
  219. :options => client.config.all
  220. }
  221. end
  222. get '/about/?' do
  223. erb :about, :layout => true, :locals => {
  224. :title => 'About'
  225. }
  226. end
  227. # These are the bits where we accept AJAX requests
  228. post "/track/?" do
  229. # Expects a JSON-encoded hash with a job id, and optionally some tags
  230. data = JSON.parse(request.body.read)
  231. job = client.jobs[data["id"]]
  232. if not job.nil?
  233. data.fetch("tags", false) ? job.track(*data["tags"]) : job.track()
  234. if request.xhr?
  235. json({ :tracked => [job.jid] })
  236. else
  237. redirect to('/track')
  238. end
  239. else
  240. if request.xhr?
  241. json({ :tracked => [] })
  242. else
  243. redirect to(request.referrer)
  244. end
  245. end
  246. end
  247. post "/untrack/?" do
  248. # Expects a JSON-encoded array of job ids to stop tracking
  249. jobs = JSON.parse(request.body.read).map { |jid| client.jobs[jid] }.select { |j| not j.nil? }
  250. # Go ahead and cancel all the jobs!
  251. jobs.each do |job|
  252. job.untrack()
  253. end
  254. return json({ :untracked => jobs.map { |job| job.jid } })
  255. end
  256. post "/priority/?" do
  257. # Expects a JSON-encoded dictionary of jid => priority
  258. response = Hash.new
  259. r = JSON.parse(request.body.read)
  260. r.each_pair do |jid, priority|
  261. begin
  262. client.jobs[jid].priority = priority
  263. response[jid] = priority
  264. rescue
  265. response[jid] = 'failed'
  266. end
  267. end
  268. return json(response)
  269. end
  270. post "/tag/?" do
  271. # Expects a JSON-encoded dictionary of jid => [tag, tag, tag]
  272. response = Hash.new
  273. JSON.parse(request.body.read).each_pair do |jid, tags|
  274. begin
  275. client.jobs[jid].tag(*tags)
  276. response[jid] = tags
  277. rescue
  278. response[jid] = 'failed'
  279. end
  280. end
  281. return json(response)
  282. end
  283. post "/untag/?" do
  284. # Expects a JSON-encoded dictionary of jid => [tag, tag, tag]
  285. response = Hash.new
  286. JSON.parse(request.body.read).each_pair do |jid, tags|
  287. begin
  288. client.jobs[jid].untag(*tags)
  289. response[jid] = tags
  290. rescue
  291. response[jid] = 'failed'
  292. end
  293. end
  294. return json(response)
  295. end
  296. post "/move/?" do
  297. # Expects a JSON-encoded hash of id: jid, and queue: queue_name
  298. data = JSON.parse(request.body.read)
  299. if data["id"].nil? or data["queue"].nil?
  300. halt 400, "Need id and queue arguments"
  301. else
  302. job = client.jobs[data["id"]]
  303. if job.nil?
  304. halt 404, "Could not find job"
  305. else
  306. job.move(data["queue"])
  307. return json({ :id => data["id"], :queue => data["queue"]})
  308. end
  309. end
  310. end
  311. post "/undepend/?" do
  312. # Expects a JSON-encoded hash of id: jid, and queue: queue_name
  313. data = JSON.parse(request.body.read)
  314. if data["id"].nil?
  315. halt 400, "Need id"
  316. else
  317. job = client.jobs[data["id"]]
  318. if job.nil?
  319. halt 404, "Could not find job"
  320. else
  321. job.undepend(data['dependency'])
  322. return json({:id => data["id"]})
  323. end
  324. end
  325. end
  326. post "/retry/?" do
  327. # Expects a JSON-encoded hash of id: jid, and queue: queue_name
  328. data = JSON.parse(request.body.read)
  329. if data["id"].nil?
  330. halt 400, "Need id"
  331. else
  332. job = client.jobs[data["id"]]
  333. if job.nil?
  334. halt 404, "Could not find job"
  335. else
  336. queue = job.history[-1]["q"]
  337. job.move(queue)
  338. return json({ :id => data["id"], :queue => queue})
  339. end
  340. end
  341. end
  342. # Retry all the failures of a particular type
  343. post "/retryall/?" do
  344. # Expects a JSON-encoded hash of type: failure-type
  345. data = JSON.parse(request.body.read)
  346. if data["type"].nil?
  347. halt 400, "Neet type"
  348. else
  349. return json(client.jobs.failed(data["type"], 0, 500)['jobs'].map do |job|
  350. queue = job.history[-1]["q"]
  351. job.move(queue)
  352. { :id => job.jid, :queue => queue}
  353. end)
  354. end
  355. end
  356. post "/cancel/?" do
  357. # Expects a JSON-encoded array of job ids to cancel
  358. jobs = JSON.parse(request.body.read).map { |jid| client.jobs[jid] }.select { |j| not j.nil? }
  359. # Go ahead and cancel all the jobs!
  360. jobs.each do |job|
  361. job.cancel()
  362. end
  363. if request.xhr?
  364. return json({ :canceled => jobs.map { |job| job.jid } })
  365. else
  366. redirect to(request.referrer)
  367. end
  368. end
  369. post "/cancelall/?" do
  370. # Expects a JSON-encoded hash of type: failure-type
  371. data = JSON.parse(request.body.read)
  372. if data["type"].nil?
  373. halt 400, "Neet type"
  374. else
  375. return json(client.jobs.failed(data["type"])['jobs'].map do |job|
  376. job.cancel()
  377. { :id => job.jid }
  378. end)
  379. end
  380. end
  381. # start the server if ruby file executed directly
  382. run! if app_file == $0
  383. end
  384. end