sidekiq-status 0.2.0 → 0.3.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.
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