sidekiq-status 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,2 @@
1
+ language: ruby
2
+ services: redis
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+
4
+ gemspec
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Sidekiq::Status
2
2
 
3
+ [![Code Climate](https://codeclimate.com/github/utgarda/sidekiq-status.png)](https://codeclimate.com/github/utgarda/sidekiq-status)
4
+ [![Build Status](https://secure.travis-ci.org/utgarda/sidekiq-status.png)](http://travis-ci.org/utgarda/sidekiq-status)
5
+
3
6
  An extension to [Sidekiq](http://github.com/mperham/sidekiq) message processing to track your jobs. Inspired
4
7
  by [resque-status](http://github.com/quirkey/resque-status) and mostly copying its features, using Sidekiq's middleware.
5
8
 
@@ -9,6 +12,8 @@ gem install sidekiq-status
9
12
 
10
13
  ## Usage
11
14
 
15
+ ### Configuration
16
+
12
17
  Configure your middleware chains, lookup [Middleware usage](https://github.com/mperham/sidekiq/wiki/Middleware)
13
18
  on Sidekiq wiki for more info.
14
19
 
@@ -24,18 +29,16 @@ end
24
29
 
25
30
  Sidekiq.configure_server do |config|
26
31
  config.server_middleware do |chain|
27
- chain.add Sidekiq::Status::ServerMiddleware
32
+ chain.add Sidekiq::Status::ServerMiddleware, expiration: 30.minutes # default
28
33
  end
29
34
  end
30
35
  ```
31
36
 
32
- When defining those jobs you want to track later, include one more module. Jobs defined without Sidekiq::Status::Worker
33
- will be processed as usual.
37
+ After that you can use your jobs as usual and only include `Sidekiq::Status::Worker` module if you want additional functionality of tracking progress and passing any data from job to client.
34
38
 
35
39
  ``` ruby
36
40
  class MyJob
37
41
  include Sidekiq::Worker
38
- include Sidekiq::Status::Worker
39
42
 
40
43
  def perform(*args)
41
44
  # your code goes here
@@ -43,19 +46,61 @@ class MyJob
43
46
  end
44
47
  ```
45
48
 
49
+ ### Retrieving status
50
+
46
51
  Query for job status any time later:
47
52
 
48
53
  ``` ruby
49
54
  job_id = MyJob.perform_async(*args)
50
- # "queued", "working", "complete" or "failed" , nil after expiry (30 minutes)
51
- status = Sidekiq::Status::get(job_id)
55
+ # :queued, :working, :complete or :failed , nil after expiry (30 minutes)
56
+ status = Sidekiq::Status::status(job_id)
57
+ Sidekiq::Status::queued? job_id
58
+ Sidekiq::Status::working? job_id
59
+ Sidekiq::Status::complete? job_id
60
+ Sidekiq::Status::failed? job_id
61
+ ```
62
+
63
+ ### Tracking progress, saving and retrieveing data associated with job
64
+
65
+ ``` ruby
66
+ class MyJob
67
+ include Sidekiq::Worker
68
+ include Sidekiq::Status::Worker # Important!
69
+
70
+ def perform(*args)
71
+ # your code goes here
72
+
73
+ # the common idiom to track progress of your task
74
+ at 5, 100, "Almost done"
75
+
76
+ # a way to associate data with your job
77
+ store vino: 'veritas'
78
+
79
+ # a way of retrieving said data
80
+ # remember that retrieved data is always is String|nil
81
+ vino = retrieve :vino
82
+ end
83
+ end
84
+
85
+ job_id = MyJob.perform_async(*args)
86
+ data = Sidekiq::Status::get_all job_id
87
+ data # => {status: 'complete', update_time: 1360006573, vino: 'veritas'}
88
+ Sidekiq::Status::get job_id, :vino #=> 'veritas'
89
+ Sidekiq::Status::num job_id #=> 5
90
+ Sidekiq::Status::total job_id #=> 100
91
+ Sidekiq::Status::message job_id #=> "Almost done"
92
+ Sidekiq::Status::pct_complete job_id #=> 5
52
93
  ```
53
94
 
54
95
  ### Features coming
55
- * Progress tracking, messages from running jobs
56
96
  * Stopping jobs by id
57
97
  * Minimal web UI
58
98
 
99
+ ## Thanks
100
+ Andrew Korzhuev
101
+ Jon Moses
102
+ Wayne Hoover
103
+
59
104
  ## License
60
105
  MIT License , see LICENSE for more details.
61
- © 2012 Evgeniy Tsvigun
106
+ © 2012 - 2013 Evgeniy Tsvigun
@@ -4,24 +4,54 @@ require 'sidekiq-status/worker'
4
4
  require 'sidekiq-status/client_middleware'
5
5
  require 'sidekiq-status/server_middleware'
6
6
 
7
- module Sidekiq
8
- module Status
9
- extend Storage
10
- DEFAULT_EXPIRY = 60 * 30
11
- UUID_REGEXP = /[0-9A-F]{24}/i #RegEx for SecureRandom.hex(12) which is the format Sidekiq uses for its jid
7
+ module Sidekiq::Status
8
+ extend Storage
9
+ DEFAULT_EXPIRY = 60 * 30
10
+ STATUS = %w(queued working complete stopped failed).map(&:to_sym).freeze
12
11
 
12
+ class << self
13
13
  # Job status by id
14
14
  # @param [String] id job id returned by async_perform
15
- # @return [String] job status, possible values: "queued" , "working" , "complete"
16
- def self.get(id)
17
- read_field_for_id(id, :status)
15
+ # @return [String] job status, possible values are in STATUS
16
+ def get(job_id, field)
17
+ read_field_for_id(job_id, field)
18
18
  end
19
19
 
20
20
  # Get all status fields for a job
21
21
  # @params [String] id job id returned by async_perform
22
22
  # @return [Hash] hash of all fields stored for the job
23
- def self.get_all(id)
23
+ def get_all(id)
24
24
  read_hash_for_id(id)
25
25
  end
26
+
27
+ def status(job_id)
28
+ status = get(job_id, :status)
29
+ status.to_sym unless status.nil?
30
+ end
31
+
32
+ STATUS.each do |name|
33
+ class_eval(<<-END, __FILE__, __LINE__)
34
+ def #{name}?(job_id)
35
+ status(job_id) == :#{name}
36
+ end
37
+ END
38
+ end
39
+
40
+ # Methods for retrieving job completion
41
+ def num(job_id)
42
+ get(job_id, :num).to_i
43
+ end
44
+
45
+ def total(job_id)
46
+ get(job_id, :total).to_i
47
+ end
48
+
49
+ def pct_complete(job_id)
50
+ (num(job_id).to_f / total(job_id)) * 100
51
+ end
52
+
53
+ def message(job_id)
54
+ get(job_id, :message)
55
+ end
26
56
  end
27
57
  end
@@ -7,9 +7,7 @@ module Sidekiq::Status
7
7
  # @param [Array] msg job arguments
8
8
  # @param [String] queue the queue's name
9
9
  def call(worker_class, msg, queue)
10
- if worker_class.include? Worker
11
- store_for_id(msg['jid'], :status => :queued)
12
- end
10
+ store_status msg['jid'], :queued
13
11
  yield
14
12
  end
15
13
  end
@@ -1,11 +1,13 @@
1
1
  module Sidekiq::Status
2
2
  # Should be in the server middleware chain
3
3
  class ServerMiddleware
4
+ include Storage
5
+
4
6
  # Parameterized initialization, use it when adding middleware to server chain
5
7
  # chain.add Sidekiq::Status::ServerMiddleware, :expiration => 60 * 5
6
8
  # @param [Hash] opts middleware initialization options
7
9
  # @option opts [Fixnum] :expiration ttl for complete jobs
8
- def initialize(opts = {:expiration => 30 * 60})
10
+ def initialize(opts = {})
9
11
  @expiration = opts[:expiration]
10
12
  end
11
13
 
@@ -20,26 +22,18 @@ module Sidekiq::Status
20
22
  # @param [Array] msg job args, should have jid format
21
23
  # @param [String] queue queue name
22
24
  def call(worker, msg, queue)
23
- if worker.is_a? Worker
24
- worker.id = msg['jid']
25
- unless worker.id.is_a?(String) && UUID_REGEXP.match(worker.id)
26
- raise ArgumentError, "First job argument for a #{worker.class.name} should have jid format"
27
- end
28
- worker.store 'status' => 'working'
29
- yield
30
- worker.store 'status' => 'complete'
31
- else
32
- yield
33
- end
25
+ # a way of overriding default expiration time,
26
+ # so worker wouldn't lose its data
27
+ worker.expiration = @expiration if worker.respond_to? :expiration
28
+
29
+ store_status worker.jid, :working, @expiration
30
+ yield
31
+ store_status worker.jid, :complete, @expiration
34
32
  rescue Worker::Stopped
35
- worker.store 'status' => 'stopped'
33
+ store_status worker.jid, :stopped, @expiration
36
34
  rescue
37
- if worker.is_a? Worker
38
- worker.store 'status' => 'failed'
39
- end
35
+ store_status worker.jid, :failed, @expiration
40
36
  raise
41
- ensure
42
- Sidekiq.redis { |conn| conn.expire worker.id, @expiration } if worker.is_a? Worker
43
37
  end
44
38
  end
45
39
  end
@@ -8,24 +8,32 @@ module Sidekiq::Status::Storage
8
8
  # @param [String] id job id
9
9
  # @param [Hash] status_updates updated values
10
10
  # @return [String] Redis operation status code
11
- def store_for_id(id, status_updates)
11
+ def store_for_id(id, status_updates, expiration = nil)
12
12
  Sidekiq.redis do |conn|
13
- answers = conn.multi do
14
- conn.hmset id, 'update_time', Time.now.to_i, *(status_updates.to_a.flatten)
15
- conn.expire id, Sidekiq::Status::DEFAULT_EXPIRY
13
+ conn.multi do
14
+ conn.hmset id, 'update_time', Time.now.to_i, *(status_updates.to_a.flatten(1))
15
+ conn.expire id, (expiration || Sidekiq::Status::DEFAULT_EXPIRY)
16
16
  conn.publish "status_updates", id
17
- end
18
- answers[0]
17
+ end[0]
19
18
  end
20
19
  end
21
20
 
21
+ # Stores job status and sets expiration time to it
22
+ # only in case of :failed or :stopped job
23
+ # @param [String] id job id
24
+ # @param [Symbol] job status
25
+ # @return [String] Redis operation status code
26
+ def store_status(id, status, expiration = nil)
27
+ store_for_id id, {status: status}, expiration
28
+ end
29
+
22
30
  # Gets a single valued from job status hash
23
31
  # @param [String] id job id
24
32
  # @param [String] Symbol field fetched field name
25
33
  # @return [String] Redis operation status code
26
- def read_field_for_id(uuid, field)
34
+ def read_field_for_id(id, field)
27
35
  Sidekiq.redis do |conn|
28
- conn.hmget(uuid, field)[0]
36
+ conn.hmget(id, field)[0]
29
37
  end
30
38
  end
31
39
 
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Status
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
@@ -4,23 +4,31 @@ module Sidekiq::Status::Worker
4
4
  class Stopped < StandardError
5
5
  end
6
6
 
7
- attr_reader :id
8
-
9
- # Worker id initialization
10
- # @param [String] id id generated on client-side
11
- # @raise [RuntimeError] raised in case of second id initialization attempt
12
- # @return [String] id
13
- def id=(id)
14
- raise RuntimeError("Worker ID is already set : #{@id}") if @id
15
- @id=id
16
- end
7
+ attr_accessor :expiration
17
8
 
18
9
  # Stores multiple values into a job's status hash,
19
10
  # sets last update time
20
11
  # @param [Hash] status_updates updated values
21
12
  # @return [String] Redis operation status code
22
13
  def store(hash)
23
- store_for_id(@id, hash)
14
+ store_for_id @jid, hash, @expiration
15
+ end
16
+
17
+ # Read value from job status hash
18
+ # @param String|Symbol hask key
19
+ # @return [String]
20
+ def retrieve(name)
21
+ read_field_for_id @jid, name
22
+ end
23
+
24
+ # Sets current task progress
25
+ # (inspired by resque-status)
26
+ # @param Fixnum number of tasks done
27
+ # @param Fixnum total number of tasks
28
+ # @param String optional message
29
+ # @return [String]
30
+ def at(num, total, message=nil)
31
+ store({num: num, total: total, message: message})
24
32
  end
25
33
 
26
- end
34
+ end
@@ -15,5 +15,6 @@ Gem::Specification.new do |gem|
15
15
  gem.version = Sidekiq::Status::VERSION
16
16
 
17
17
  gem.add_dependency 'sidekiq', '~> 2.7'
18
+ gem.add_development_dependency 'rake'
18
19
  gem.add_development_dependency 'rspec'
19
20
  end
@@ -13,6 +13,7 @@ describe Sidekiq::Status::ClientMiddleware do
13
13
  SecureRandom.should_receive(:hex).once.and_return(job_id)
14
14
  StubJob.perform_async(:arg1 => 'val1').should == job_id
15
15
  redis.hget(job_id, :status).should == 'queued'
16
+ Sidekiq::Status::queued?(job_id).should be_true
16
17
  end
17
18
 
18
19
  it "sets status hash ttl" do
@@ -21,6 +21,7 @@ describe Sidekiq::Status::ServerMiddleware do
21
21
  job_id]
22
22
  end
23
23
  redis.hget(job_id, :status).should == 'complete'
24
+ Sidekiq::Status::complete?(job_id).should be_true
24
25
  end
25
26
 
26
27
  it "sets failed status" do
@@ -31,6 +32,7 @@ describe Sidekiq::Status::ServerMiddleware do
31
32
  }.should == [job_id]*3
32
33
  end
33
34
  redis.hget(job_id, :status).should == 'failed'
35
+ Sidekiq::Status::failed?(job_id).should be_true
34
36
  end
35
37
 
36
38
  it "sets status hash ttl" do
@@ -11,17 +11,53 @@ describe Sidekiq::Status do
11
11
  # so we should wait till they expire
12
12
  before { redis.flushall; sleep 0.1 }
13
13
 
14
- describe ".get" do
15
- it "gets job status by id" do
14
+ describe ".status, .working?, .complete?" do
15
+ it "gets job status by id as symbol" do
16
16
  SecureRandom.should_receive(:hex).once.and_return(job_id)
17
17
 
18
18
  start_server do
19
19
  capture_status_updates(2) {
20
20
  LongJob.perform_async(1).should == job_id
21
21
  }.should == [job_id]*2
22
- Sidekiq::Status.get(job_id).should == "working"
22
+ Sidekiq::Status.status(job_id).should == :working
23
+ Sidekiq::Status.working?(job_id).should be_true
24
+ Sidekiq::Status::queued?(job_id).should be_false
25
+ Sidekiq::Status::failed?(job_id ).should be_false
26
+ Sidekiq::Status::complete?(job_id).should be_false
27
+ Sidekiq::Status::stopped?(job_id).should be_false
28
+ end
29
+ Sidekiq::Status.status(job_id).should == :complete
30
+ Sidekiq::Status.complete?(job_id).should be_true
31
+ end
32
+ end
33
+
34
+ describe ".get" do
35
+ it "gets a single value from data hash as string" do
36
+ SecureRandom.should_receive(:hex).once.and_return(job_id)
37
+
38
+ start_server do
39
+ capture_status_updates(3) {
40
+ DataJob.perform_async.should == job_id
41
+ }.should == [job_id]*3
42
+ Sidekiq::Status.get(job_id, :status).should == 'working'
43
+ end
44
+ Sidekiq::Status.get(job_id, :data).should == 'meow'
45
+ end
46
+ end
47
+
48
+ describe ".num, .total, .pct_complete, .message" do
49
+ it "should return job progress with correct type to it" do
50
+ SecureRandom.should_receive(:hex).once.and_return(job_id)
51
+
52
+ start_server do
53
+ capture_status_updates(3) {
54
+ ProgressJob.perform_async.should == job_id
55
+ }.should == [job_id]*3
23
56
  end
24
- Sidekiq::Status.get(job_id).should == 'complete'
57
+ Sidekiq::Status.num(job_id).should == 100
58
+ Sidekiq::Status.total(job_id).should == 500
59
+ Sidekiq::Status.pct_complete(job_id).should == 20
60
+ Sidekiq::Status.message(job_id).should == 'howdy, partner?'
25
61
  end
26
62
  end
27
63
 
@@ -42,19 +78,19 @@ describe Sidekiq::Status do
42
78
  end
43
79
 
44
80
  context "keeps normal Sidekiq functionality" do
45
- it "does jobs with and without status processing" do
46
- SecureRandom.should_receive(:hex).exactly(4).times.and_return(job_id, job_id_1)
81
+ it "does jobs with and without included worker module" do
82
+ SecureRandom.should_receive(:hex).exactly(4).times.and_return(job_id, job_id, job_id_1, job_id_1)
47
83
  start_server do
48
- capture_status_updates(6) {
84
+ capture_status_updates(12) {
49
85
  StubJob.perform_async.should == job_id
50
86
  NoStatusConfirmationJob.perform_async(1)
51
87
  StubJob.perform_async.should == job_id_1
52
88
  NoStatusConfirmationJob.perform_async(2)
53
- }.should =~ [job_id, job_id_1] * 3
89
+ }.should =~ [job_id, job_id_1] * 6
54
90
  end
55
91
  redis.mget('NoStatusConfirmationJob_1', 'NoStatusConfirmationJob_2').should == %w(done)*2
56
- Sidekiq::Status.get(job_id).should == 'complete'
57
- Sidekiq::Status.get(job_id_1).should == 'complete'
92
+ Sidekiq::Status.status(job_id).should == :complete
93
+ Sidekiq::Status.status(job_id_1).should == :complete
58
94
  end
59
95
 
60
96
  it "retries failed jobs" do
@@ -64,7 +100,7 @@ describe Sidekiq::Status do
64
100
  RetriedJob.perform_async().should == job_id
65
101
  }.should == [job_id] * 5
66
102
  end
67
- Sidekiq::Status.get(job_id).should == 'complete'
103
+ Sidekiq::Status.status(job_id).should == :complete
68
104
  end
69
105
  end
70
106
 
data/spec/spec_helper.rb CHANGED
@@ -65,4 +65,4 @@ def start_server()
65
65
  Timeout::timeout(10) { Process.wait pid } rescue Timeout::Error
66
66
  ensure
67
67
  Process.kill 'KILL', pid rescue "OK" # it's OK if the process is gone already
68
- end
68
+ end
@@ -14,10 +14,26 @@ class LongJob < StubJob
14
14
  end
15
15
  end
16
16
 
17
+ class DataJob < StubJob
18
+ def perform
19
+ sleep 0.1
20
+ store({data: 'meow'})
21
+ retrieve(:data).should == 'meow'
22
+ sleep 0.1
23
+ end
24
+ end
25
+
26
+ class ProgressJob < StubJob
27
+ def perform
28
+ at 100, 500, 'howdy, partner?'
29
+ sleep 0.1
30
+ end
31
+ end
32
+
17
33
  class ConfirmationJob < StubJob
18
34
  def perform(*args)
19
35
  Sidekiq.redis do |conn|
20
- conn.publish "job_messages_#{id}", "while in #perform, status = #{conn.hget id, :status}"
36
+ conn.publish "job_messages_#{jid}", "while in #perform, status = #{conn.hget jid, :status}"
21
37
  end
22
38
  end
23
39
  end
@@ -28,7 +44,6 @@ class NoStatusConfirmationJob
28
44
  Sidekiq.redis do |conn|
29
45
  conn.set "NoStatusConfirmationJob_#{id}", "done"
30
46
  end
31
-
32
47
  end
33
48
  end
34
49
 
@@ -42,7 +57,7 @@ class RetriedJob < StubJob
42
57
  sidekiq_options 'retry' => 'true'
43
58
  def perform()
44
59
  Sidekiq.redis do |conn|
45
- key = "RetriedJob_#{id}"
60
+ key = "RetriedJob_#{jid}"
46
61
  unless conn.exists key
47
62
  conn.set key, 'tried'
48
63
  raise StandardError
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-status
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-04 00:00:00.000000000 Z
12
+ date: 2013-02-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: '2.7'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  - !ruby/object:Gem::Dependency
31
47
  name: rspec
32
48
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +68,8 @@ extra_rdoc_files: []
52
68
  files:
53
69
  - .gitignore
54
70
  - .rspec
71
+ - .travis.yml
72
+ - Gemfile
55
73
  - LICENSE
56
74
  - README.md
57
75
  - Rakefile