├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── active_job_status-1.0.0.gem ├── active_job_status.gemspec ├── gemfiles └── redis-activesupport.gemfile ├── lib ├── active_job_status.rb └── active_job_status │ ├── configure_redis.rb │ ├── hooks.rb │ ├── job_batch.rb │ ├── job_status.rb │ ├── job_tracker.rb │ ├── redis.rb │ ├── trackable_job.rb │ └── version.rb └── spec ├── active_job_status_spec.rb ├── job_batch_spec.rb ├── job_status_spec.rb ├── job_tracker_spec.rb ├── spec_helper.rb ├── support ├── helpers.rb └── time_helpers.rb └── trackable_job_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /gemfiles/*.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | *.bundle 12 | *.so 13 | *.o 14 | *.a 15 | mkmf.log 16 | .DS_Store 17 | *.swp 18 | *.gem -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - 2.3.1 5 | script: bundle exec rake 6 | services: 7 | - redis 8 | gemfile: 9 | - Gemfile 10 | - gemfiles/redis-activesupport.gemfile 11 | notifications: 12 | email: 13 | recipients: 14 | - cdale77@gmail.com 15 | on_failure: change 16 | on_success: never 17 | env: 18 | global: 19 | secure: MK+CWQzMGTGzDRGfwtkJoAySnjcmmbEF4Fl9DDRaUD0q/LGTEV3TrOo+/QAeT945Vbs+CRnSMdGmVdBsLLiGlU56SrLQs8cEI/m7KyDcMjHJfuSkV5hdoUxSGgvvDlhLAZHBhmBnDnuIB7s0ExCyyJGamPOoZbVNgLYpF0XnxsY= 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ActiveJobStatus 2 | ## 1.2.1 3 | - Fixes bug with JobBatch#completed? (https://github.com/cdale77/active_job_status/pull/19) 4 | 5 | ## 1.2.0 6 | - Add support for Rails 5 7 | - Adds many small improvements (see https://github.com/cdale77/active_job_status/pull/15) 8 | 9 | ## 1.1.0 10 | - Add support for Redis via the Readthis gem 11 | 12 | ## 1.0.0 13 | - Move TrackableJob inside ActiveJobStatus namespace. Bump version to 1.0.0, as 14 | this may be a breaking change for some users. 15 | 16 | ## 0.0.5 17 | - Use ActiveSupport::Cache instead of Redis, reducing dependencies. Changes 18 | from Gabe Kopley. 19 | 20 | ## 0.0.4 21 | - Change name of JobStatus::get_status argument batch_key to be batch_id 22 | - Add changelog 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_job_status.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Brad Johnson 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/cdale77/active_job_status/badges/gpa.svg)](https://codeclimate.com/github/cdale77/active_job_status) 2 | [![Test Coverage](https://codeclimate.com/github/cdale77/active_job_status/badges/coverage.svg)](https://codeclimate.com/github/cdale77/active_job_status) 3 | [![Build Status](https://travis-ci.org/cdale77/active_job_status.svg?branch=master)](https://travis-ci.org/cdale77/active_job_status) 4 | 5 | # ActiveJobStatus 6 | 7 | Provides simple job status information for ActiveJob. 8 | 9 | This gem uses ActiveJob callbacks to set simple ActiveSupport::Cache 10 | values to track job status 11 | and batches of jobs. To prevent it from taking over all of your memory, both jobs 12 | and batch tracking information expires in 72 hours. Currently you can set 13 | batches to expire at a different interval, but not jobs. 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'active_job_status' 21 | ``` 22 | 23 | And then execute: 24 | 25 | $ bundle 26 | 27 | Or install it yourself as: 28 | 29 | $ gem install active_job_status 30 | 31 | ## Configuration 32 | 33 | You need to tell ActiveJobStatus about your memory store. By default, tell 34 | ActiveJobStatus to use Rails' built in memory store: 35 | 36 | # config/initializers/active_job_status.rb 37 | ActiveJobStatus.store = ActiveSupport::Cache::MemoryStore.new 38 | 39 | If you are using Resque or Sidekiq, or have Redis in your stack already for 40 | another reason, it's a good idea to tell ActiveJobStatus to use Redis for 41 | storing job metadata. To do so, you'll first need to configure 42 | ActiveSupport::Cache to use Redis for its store 43 | (perhaps by using [this gem](https://github.com/redis-store/redis-rails)). Then 44 | use the following initializer to tell ActiveJob to use the proper store. 45 | ActiveJob status will detect Redis and use some nice optimizations. 46 | 47 | # config/initializers/active_job_status.rb 48 | ActiveJobStatus.store = ActiveSupport::Cache::RedisStore.new 49 | # or if you are using https://github.com/sorentwo/readthis 50 | ActiveJobStatus.store = ActiveSupport::Cache::ReadthisStore.new 51 | 52 | ## Usage 53 | 54 | Have your jobs inherit from ActiveJobStatus::TrackableJob instead of ActiveJob::Base: 55 | 56 | *Note! Previous versions of this gem did not namespace TrackableJob inside of 57 | ActiveJob Status -- it was in the global namespace. If 58 | upgrading from versions < 1.0, you may need to update your code.* 59 | 60 | class MyJob < ActiveJobStatus::TrackableJob 61 | end 62 | 63 | Or you can just include ActiveJobStatus::Hooks into your job if you are using 64 | version 1.2 or greater. You might want to use this approach if you are using an 65 | `ApplicationJob` and do not want all your jobs to be trackable. 66 | 67 | class MyJob < ActiveJob::Base 68 | include ActiveJobStatus::Hooks 69 | end 70 | 71 | or 72 | 73 | class MyJob < ApplicationJob 74 | include ActiveJobStatus::Hooks 75 | end 76 | 77 | class ApplicationJob < ActiveJob::Base 78 | end 79 | 80 | 81 | ### Job Status 82 | 83 | Check the status of a job using the ActiveJob job_id. Status of a job will only 84 | be available for 72 hours after the job is queued. For right now you can't 85 | change that. 86 | 87 | my_job = MyJob.perform_later 88 | job_status = ActiveJobStatus.fetch(my_job.job_id) 89 | job_status.queued? 90 | job_status.working? 91 | job_status.completed? 92 | job_status.status 93 | # => :queued, :working, :completed, nil 94 | 95 | ### Job Batches 96 | For job batches you an use any key you want (for example, you might use a 97 | primary key or UUID from your database). If another batch with the same key 98 | exists, its jobs will be overwritten with the supplied list. 99 | 100 | my_key = "230923asdlkj230923" 101 | my_jobs = [my_first_job.job_id, my_second_job.job_id] 102 | my_batch = ActiveJobStatus::JobBatch.new(batch_id: my_key, job_ids: my_jobs) 103 | 104 | Batches expire after 72 hours (259200 seconds). 105 | You can change that by passing the initializer an integer value (in seconds). 106 | 107 | my_key = "230923asdlkj230923" 108 | my_jobs = [my_first_job.job_id, my_second_job.job_id] 109 | my_batch = ActiveJobStatus::JobBatch.new(batch_id: my_key, 110 | job_ids: my_jobs, 111 | expire_in: 500000) 112 | 113 | You can easily add jobs to the batch: 114 | 115 | new_jobs = [some_new_job.job_id, another_new_job.job_id] 116 | my_batch.add_jobs(job_ids: new_jobs) 117 | 118 | And you can ask the batch if all the jobs are completed or not: 119 | 120 | my_batch.completed? 121 | # => true, false 122 | 123 | You can ask the batch for other bits of information: 124 | 125 | batch.batch_id 126 | # => "230923asdlkj230923" 127 | batch.job_ids 128 | # => ["b67af7a0-3ed2-4661-a2d5-ff6b6a254886", "6c0216b9-ea0c-4ee9-a3b2-501faa919a66"] 129 | 130 | You can also search for batches: 131 | ActiveJobStatus::JobBatch.find(batch_id: my_key) 132 | 133 | This method will return nil if no associated job ids can be found, otherwise it will 134 | This method will return nil no associated job ids can be found, otherwise it will 135 | return an ActiveJobStatus::JobBatch object. 136 | 137 | ## Contributing 138 | 139 | 1. Fork it ( https://github.com/[my-github-username]/active_job_status/fork ) 140 | 2. Create your feature branch (`git checkout -b my-new-feature`) 141 | 3. Commit your changes (`git commit -am 'Add some feature'`) 142 | 4. Push to the branch (`git push origin my-new-feature`) 143 | 5. Create a new Pull Request 144 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'bundler/gem_tasks' 3 | 4 | # Default directory to look in is `/specs` 5 | # Run with `rake spec` 6 | RSpec::Core::RakeTask.new(:spec) do |task| 7 | task.rspec_opts = ['--color'] 8 | end 9 | 10 | task :default => :spec 11 | -------------------------------------------------------------------------------- /active_job_status-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdale77/active_job_status/e03a109137d5c6a9e6efdc62aea6b636322809a1/active_job_status-1.0.0.gem -------------------------------------------------------------------------------- /active_job_status.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_job_status/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "active_job_status" 8 | spec.version = ActiveJobStatus::VERSION 9 | spec.authors = ["Brad Johnson"] 10 | spec.email = ["cdale77@gmail.com"] 11 | spec.summary = "Job status and batches for ActiveJob" 12 | spec.description = "Job status and batches for ActiveJob. Create trackable jobs, check their status, and batch them together." 13 | spec.homepage = "https://github.com/cdale77/active_job_status" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.7" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | spec.add_development_dependency "rspec", "~> 3.0" 24 | spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4" 25 | 26 | spec.add_runtime_dependency "activejob", "> 4.2" 27 | spec.add_runtime_dependency "activesupport", "> 4.2" 28 | 29 | spec.post_install_message = "If updating from a version below 1.0, please note " \ 30 | "TrackabeJob is now namespaced inside of ActiveJob. " \ 31 | "You will need update your code." 32 | end 33 | -------------------------------------------------------------------------------- /gemfiles/redis-activesupport.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: "#{__dir__}/.." 4 | 5 | gem "redis-activesupport" 6 | -------------------------------------------------------------------------------- /lib/active_job_status.rb: -------------------------------------------------------------------------------- 1 | require "active_job_status/hooks" 2 | require "active_job_status/trackable_job" 3 | require "active_job_status/job_tracker" 4 | require "active_job_status/job_status" 5 | require "active_job_status/job_batch" 6 | require "active_job_status/version" 7 | require "active_job_status/configure_redis" if defined? Rails 8 | 9 | module ActiveJobStatus 10 | class NoStoreError < StandardError; end 11 | 12 | class << self 13 | attr_accessor :expiration 14 | attr_writer :store 15 | 16 | def get_status(job_id) 17 | fetch(job_id).status 18 | end 19 | 20 | def fetch(job_id) 21 | status = store.fetch(job_id) 22 | JobStatus.new(status) 23 | end 24 | 25 | def store 26 | unless @store 27 | raise NoStoreError, "can't use ActiveJobStatus without store" 28 | end 29 | @store 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/active_job_status/configure_redis.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | class ConfigureRedis < Rails::Railtie 3 | initializer "configure_redis.configure_rails_initializers" do 4 | if defined? ActiveSupport::Cache::RedisStore || defined? ActiveSupport::Cache::ReadthisStore 5 | require "active_job_status/redis" 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/active_job_status/hooks.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobStatus 2 | module Hooks 3 | def self.included(base) 4 | base.class_eval do 5 | before_enqueue { job_tracker.enqueued } 6 | 7 | before_perform { job_tracker.performing } 8 | 9 | after_perform { job_tracker.completed } 10 | end 11 | end 12 | 13 | private 14 | 15 | def job_tracker 16 | @job_tracker ||= ActiveJobStatus::JobTracker.new(job_id: job_id) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/active_job_status/job_batch.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobStatus 2 | class JobBatch 3 | 4 | attr_reader :batch_id 5 | attr_reader :job_ids 6 | 7 | def initialize(batch_id:, job_ids:, expire_in: 259200, store_data: true) 8 | @batch_id = batch_id 9 | @job_ids = job_ids 10 | # the store_data flag is used by the ::find method return a JobBatch 11 | # object without re-saving the data 12 | self.store_data(expire_in: expire_in) if store_data 13 | end 14 | 15 | def store_data(expire_in:) 16 | ActiveJobStatus.store.delete(@batch_id) # delete any old batches 17 | if ["ActiveSupport::Cache::RedisStore", "ActiveSupport::Cache::ReadthisStore"].include? ActiveJobStatus.store.class.to_s 18 | ActiveJobStatus.store.sadd(@batch_id, @job_ids) 19 | ActiveJobStatus.store.expire(@batch_id, expire_in) 20 | else 21 | ActiveJobStatus.store.write(@batch_id, @job_ids, expires_in: expire_in) 22 | end 23 | end 24 | 25 | def add_jobs(job_ids:) 26 | @job_ids = @job_ids + job_ids 27 | if ["ActiveSupport::Cache::RedisStore", "ActiveSupport::Cache::ReadthisStore"].include? ActiveJobStatus.store.class.to_s 28 | # Save an extra redis query and perform atomic operation 29 | ActiveJobStatus.store.sadd(@batch_id, job_ids) 30 | else 31 | existing_job_ids = ActiveJobStatus.store.fetch(@batch_id) 32 | ActiveJobStatus.store.write(@batch_id, existing_job_ids.to_a | job_ids) 33 | end 34 | end 35 | 36 | def completed? 37 | # if all statuses are either nil or completed, the batch is done 38 | job_statuses.all? { |job_status| job_status.empty? || job_status.completed? } 39 | end 40 | 41 | def self.find(batch_id:) 42 | if ["ActiveSupport::Cache::RedisStore", "ActiveSupport::Cache::ReadthisStore"].include? ActiveJobStatus.store.class.to_s 43 | job_ids = ActiveJobStatus.store.smembers(batch_id) 44 | else 45 | job_ids = ActiveJobStatus.store.fetch(batch_id).to_a 46 | end 47 | 48 | if job_ids.any? 49 | ActiveJobStatus::JobBatch.new(batch_id: batch_id, 50 | job_ids: job_ids, 51 | store_data: false) 52 | end 53 | end 54 | 55 | private 56 | 57 | # returns ActiveJobStatus::JobStatus 58 | # for each job_id 59 | def job_statuses 60 | @job_ids.map { |job_id| ActiveJobStatus.fetch(job_id) } 61 | end 62 | 63 | def write(key, job_ids, expire_in=nil) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/active_job_status/job_status.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobStatus 2 | class JobStatus 3 | ENQUEUED = :queued 4 | WORKING = :working 5 | COMPLETED = :completed 6 | 7 | attr_reader :status 8 | 9 | def initialize(status) 10 | @status = status && status.to_sym 11 | end 12 | 13 | def queued? 14 | status == ENQUEUED 15 | end 16 | 17 | def working? 18 | status == WORKING 19 | end 20 | 21 | def completed? 22 | status == COMPLETED 23 | end 24 | 25 | def empty? 26 | status.nil? 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/active_job_status/job_tracker.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobStatus 2 | class JobTracker 3 | DEFAULT_EXPIRATION = 72.hours.freeze 4 | 5 | def initialize(job_id:, store: ActiveJobStatus.store, expiration: ActiveJobStatus.expiration) 6 | @job_id = job_id 7 | @store = store 8 | @expiration = expiration 9 | end 10 | 11 | def enqueued 12 | store.write( 13 | job_id, 14 | JobStatus::ENQUEUED.to_s, 15 | expires_in: expiration || DEFAULT_EXPIRATION 16 | ) 17 | end 18 | 19 | def performing 20 | store.write( 21 | job_id, 22 | JobStatus::WORKING.to_s 23 | ) 24 | end 25 | 26 | def completed 27 | store.write( 28 | job_id, 29 | JobStatus::COMPLETED.to_s 30 | ) 31 | end 32 | 33 | def deleted 34 | store.delete(job_id) 35 | end 36 | 37 | private 38 | 39 | attr_reader :job_id, :store, :expiration 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/active_job_status/redis.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobStatus 2 | module Redis 3 | def sadd(key, elements) 4 | instrument(:sadd, key, elements: elements) do 5 | @data.sadd key, elements 6 | end 7 | end 8 | 9 | def smembers(key) 10 | instrument(:smembers, key) do 11 | @data.smembers key 12 | end 13 | end 14 | end 15 | end 16 | 17 | if defined? ActiveSupport::Cache::RedisStore 18 | ActiveSupport::Cache::RedisStore.include( 19 | ActiveJobStatus::Redis 20 | ) 21 | elsif defined? ActiveSupport::Cache::ReadthisStore 22 | ActiveSupport::Cache::ReadthisStore.include( 23 | ActiveJobStatus::Redis 24 | ) 25 | end -------------------------------------------------------------------------------- /lib/active_job_status/trackable_job.rb: -------------------------------------------------------------------------------- 1 | require "active_job" 2 | require "active_job_status/hooks" 3 | 4 | module ActiveJobStatus 5 | class TrackableJob < ActiveJob::Base 6 | include ActiveJobStatus::Hooks 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/active_job_status/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobStatus 2 | VERSION = "1.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/active_job_status_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ActiveJobStatus do 4 | describe ".store" do 5 | it "raises error when store has not set" do 6 | expect { ActiveJobStatus.store }.to raise_error(ActiveJobStatus::NoStoreError) 7 | end 8 | 9 | context "when store has set" do 10 | let(:store) { new_store } 11 | 12 | before do 13 | ActiveJobStatus.store = store 14 | end 15 | 16 | it "returns configured store" do 17 | expect { ActiveJobStatus.store }.to_not raise_error 18 | expect(ActiveJobStatus.store).to eq store 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/job_batch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ActiveJobStatus::JobBatch do 4 | 5 | let!(:batch_id) { Time.now } 6 | 7 | let!(:store) { ActiveJobStatus.store = new_store } 8 | 9 | let!(:job1) { ActiveJobStatus::TrackableJob.perform_later } 10 | let!(:job2) { ActiveJobStatus::TrackableJob.perform_later } 11 | let!(:job3) { ActiveJobStatus::TrackableJob.perform_later } 12 | let!(:job4) { ActiveJobStatus::TrackableJob.perform_later } 13 | 14 | let!(:first_jobs) { [job1.job_id, job2.job_id] } 15 | let!(:addl_jobs) { [job3.job_id, job4.job_id] } 16 | let!(:total_jobs) { first_jobs + addl_jobs } 17 | 18 | let!(:batch) { ActiveJobStatus::JobBatch.new(batch_id: batch_id, 19 | job_ids: first_jobs) } 20 | 21 | 22 | describe "#initialize" do 23 | it "should create an object" do 24 | expect(batch).to be_an_instance_of ActiveJobStatus::JobBatch 25 | end 26 | it "should write to the cache store" do 27 | expect( 28 | ActiveJobStatus::JobBatch.find(batch_id: batch_id).job_ids 29 | ).to match_array(first_jobs) 30 | end 31 | end 32 | 33 | describe "#job_ids" do 34 | describe "when jobs are present" do 35 | it "should return an array of job ids" do 36 | expect(batch.job_ids).to match_array(first_jobs) 37 | end 38 | end 39 | end 40 | 41 | describe "#add_jobs" do 42 | it "should add jobs to the set" do 43 | batch.add_jobs(job_ids: addl_jobs) 44 | total_jobs.each do |job_id| 45 | expect(ActiveJobStatus::JobBatch.find(batch_id: batch_id).job_ids).to \ 46 | include job_id 47 | end 48 | end 49 | end 50 | 51 | describe "#completed?" do 52 | it "should be false when jobs are queued" do 53 | update_store(id_array: total_jobs, job_status: :queued) 54 | expect(batch.completed?).to be_falsey 55 | end 56 | it "should be false when jobs are working" do 57 | update_store(id_array: total_jobs, job_status: :working) 58 | expect(batch.completed?).to be_falsey 59 | end 60 | it "should be false with mixed jobs status of working and completed" do 61 | update_store_mixed_status(id_array: total_jobs, 62 | job_status_array: [:working, :completed]) 63 | expect(batch.completed?).to be_falsey 64 | end 65 | it "should be true when jobs are all completed" do 66 | update_store(id_array: total_jobs, job_status: :completed) 67 | expect(batch.completed?).to be_truthy 68 | end 69 | it "should be true when jobs are not in the store" do 70 | clear_store(id_array: total_jobs) 71 | expect(batch.completed?).to be_truthy 72 | end 73 | end 74 | 75 | describe "::find" do 76 | describe "when a batch is present" do 77 | it "should return a JobBatch object" do 78 | expect(ActiveJobStatus::JobBatch.find(batch_id: batch_id)).to \ 79 | be_an_instance_of ActiveJobStatus::JobBatch 80 | end 81 | end 82 | 83 | describe "when no batch is present" do 84 | it "should return nil" do 85 | expect(ActiveJobStatus::JobBatch.find(batch_id: "baz")).to be_nil 86 | end 87 | end 88 | end 89 | 90 | describe "expiring job" do 91 | it "should allow the expiration time to be set in seconds" do 92 | expect(ActiveJobStatus::JobBatch.new(batch_id: "newkey", 93 | job_ids: first_jobs, 94 | expire_in: 200000)).to \ 95 | be_an_instance_of ActiveJobStatus::JobBatch 96 | end 97 | it "should expire" do 98 | ActiveJobStatus::JobBatch.new(batch_id: "expiry", 99 | job_ids: first_jobs, 100 | expire_in: 1) 101 | travel(2.seconds) do 102 | expect( 103 | ActiveJobStatus::JobBatch.find(batch_id: "expiry") 104 | ).to be_nil 105 | end 106 | end 107 | end 108 | 109 | ##### HELPERS 110 | 111 | def update_store(id_array: [], job_status: :queued) 112 | id_array.each do |id| 113 | store.write(id, job_status.to_s) 114 | end 115 | end 116 | 117 | def update_store_mixed_status(id_array: [], job_status_array: [:queued]) 118 | n_element = job_status_array.count 119 | id_array.each_with_index do |id, index| 120 | store.write(id, job_status_array[index % n_element].to_s) 121 | end 122 | end 123 | 124 | def clear_store(id_array: []) 125 | id_array.each do |id| 126 | store.delete(id) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/job_status_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ActiveJobStatus::JobStatus do 4 | let(:job_status) { described_class.new(status) } 5 | 6 | context 'when queued' do 7 | let(:status) { 'queued' } 8 | 9 | it 'returns the correct state' do 10 | expect(job_status.queued?).to eq true 11 | expect(job_status.working?).to eq false 12 | expect(job_status.completed?).to eq false 13 | expect(job_status.empty?).to eq false 14 | expect(job_status.status).to eq :queued 15 | end 16 | end 17 | 18 | context 'when working' do 19 | let(:status) { 'working' } 20 | 21 | it 'returns the correct state' do 22 | expect(job_status.queued?).to eq false 23 | expect(job_status.working?).to eq true 24 | expect(job_status.completed?).to eq false 25 | expect(job_status.empty?).to eq false 26 | expect(job_status.status).to eq :working 27 | end 28 | end 29 | 30 | context 'when completed' do 31 | let(:status) { 'completed' } 32 | 33 | it 'returns the correct state' do 34 | expect(job_status.queued?).to eq false 35 | expect(job_status.working?).to eq false 36 | expect(job_status.completed?).to eq true 37 | expect(job_status.empty?).to eq false 38 | expect(job_status.status).to eq :completed 39 | end 40 | end 41 | 42 | context 'when nil' do 43 | let(:status) { nil } 44 | 45 | it 'returns the correct state' do 46 | expect(job_status.queued?).to eq false 47 | expect(job_status.working?).to eq false 48 | expect(job_status.completed?).to eq false 49 | expect(job_status.empty?).to eq true 50 | expect(job_status.status).to eq nil 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/job_tracker_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ActiveJobStatus::JobTracker do 4 | let!(:store) { ActiveJobStatus.store = new_store } 5 | let(:job_id) { 'j0b-1d' } 6 | let(:tracker) { described_class.new(job_id: job_id) } 7 | 8 | describe "#enqueued" do 9 | it "starts tracking the job" do 10 | tracker.enqueued 11 | expect(store.fetch(job_id)).to eq "queued" 12 | end 13 | 14 | context 'with default expiration period' do 15 | before { ActiveJobStatus.expiration = nil } 16 | 17 | it 'expires in 72 hours' do 18 | expect(store).to receive(:write).with(job_id, "queued", expires_in: 72.hours) 19 | tracker.enqueued 20 | end 21 | end 22 | 23 | context 'with default expiration period' do 24 | before { ActiveJobStatus.expiration = 10.seconds } 25 | 26 | it 'expires in the given period' do 27 | expect(store).to receive(:write).with(job_id, "queued", expires_in: 10.seconds) 28 | tracker.enqueued 29 | end 30 | end 31 | end 32 | 33 | describe "#performing" do 34 | it "updates the job status" do 35 | tracker.performing 36 | expect(store.fetch(job_id)).to eq "working" 37 | end 38 | end 39 | 40 | describe "#completed" do 41 | it "updates the job status" do 42 | tracker.completed 43 | expect(store.fetch(job_id)).to eq "completed" 44 | end 45 | end 46 | 47 | describe "#deleted" do 48 | it "removes the job from the store" do 49 | tracker.deleted 50 | expect(store.fetch(job_id)).to eq nil 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | 3 | Bundler.require(:default, :development) 4 | 5 | CodeClimate::TestReporter.start 6 | Dir["#{__dir__}/support/*.rb"].each {|file| require file } 7 | 8 | include ActiveJob::TestHelper 9 | 10 | ActiveJob::Base.queue_adapter = :test 11 | 12 | RSpec.configure do |c| 13 | c.include Helpers 14 | end 15 | 16 | if defined? ActiveSupport::Cache::RedisStore || defined? ActiveSupport::Cache::ReadthisStore 17 | require "active_job_status/redis" 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/helpers.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | def new_store 3 | if defined? ActiveSupport::Cache::RedisStore 4 | puts "Using RedisStore" 5 | ActiveSupport::Cache::RedisStore.new 6 | elsif defined? ActiveSupport::Cache::ReadthisStore 7 | puts "Using ReadthisStore" 8 | ActiveSupport::Cache::ReadthisStore.new 9 | else 10 | puts "Using MemoryStore" 11 | ActiveSupport::Cache::MemoryStore.new 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/time_helpers.rb: -------------------------------------------------------------------------------- 1 | if defined? ActiveSupport::Cache::RedisStore || defined? ActiveSupport::Cache::ReadthisStore 2 | # For redis we need to sleep to test 3 | def travel(interval) 4 | sleep interval 5 | yield 6 | end 7 | else 8 | require "active_support/testing/time_helpers" 9 | include ActiveSupport::Testing::TimeHelpers 10 | end 11 | -------------------------------------------------------------------------------- /spec/trackable_job_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ActiveJobStatus::TrackableJob do 4 | class DummyJob < ActiveJobStatus::TrackableJob 5 | def perform; end; 6 | end 7 | 8 | let(:job) { DummyJob.new } 9 | 10 | describe 'queueing' do 11 | it "should have a job id" do 12 | expect(job.job_id).to_not be_blank 13 | end 14 | end 15 | 16 | describe 'tracking hooks' do 17 | let(:job_tracker) { instance_double(ActiveJobStatus::JobTracker) } 18 | 19 | before do 20 | allow(job).to receive(:job_id) { 'j0b-1d' } 21 | end 22 | 23 | describe 'before enqueue' do 24 | it 'starts to track the job with JobTracker' do 25 | expect(ActiveJobStatus::JobTracker).to receive(:new).with(job_id: 'j0b-1d') { job_tracker } 26 | expect(job_tracker).to receive(:enqueued) 27 | job.enqueue 28 | end 29 | end 30 | 31 | describe 'before/after perform' do 32 | it 'updates the job status with JobTracker and then remove it' do 33 | expect(ActiveJobStatus::JobTracker).to receive(:new).with(job_id: 'j0b-1d') { job_tracker } 34 | expect(job_tracker).to receive(:performing) 35 | expect(job_tracker).to receive(:completed) 36 | job.perform_now 37 | end 38 | end 39 | end 40 | end 41 | --------------------------------------------------------------------------------