From 6f90cee2991563012f4868bf8544e73ad0884e3d Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Tue, 13 Mar 2018 16:47:28 -0700 Subject: [PATCH 1/7] Moves run_at support functionality into a controller method which can be called using a `before_action` callback. * Added config `work_off_timeout` to control run_at check rate * Removed max_attempts setting as this was not having any effect * Updated docs --- README.md | 85 ++++++++------------- lib/workless.rb | 12 ++- lib/workless/controllers/helpers.rb | 22 ++++++ lib/workless/initialize.rb | 2 +- lib/workless/middleware/workless_checker.rb | 30 -------- lib/workless/railtie.rb | 1 - spec/workless/middleware_spec.rb | 30 -------- 7 files changed, 67 insertions(+), 115 deletions(-) create mode 100644 lib/workless/controllers/helpers.rb delete mode 100644 lib/workless/middleware/workless_checker.rb delete mode 100644 spec/workless/middleware_spec.rb diff --git a/README.md b/README.md index c912c5d..b09fc41 100644 --- a/README.md +++ b/README.md @@ -7,73 +7,61 @@ This is an addon for delayed_job (> 2.0.0) http://github.com/collectiveidea/delayed_job It is designed to be used when you're using Heroku as a host and have the need to do background work with delayed job but you don't want to leave the workers running all the time as it costs money. -By adding the gem to your project and configuring our Heroku app with some config variables workless should do the rest. - -:warning: **[The Legacy API will be sunset on April 15th, 2017](https://devcenter.heroku.com/changelog-items/862)** :warning: -Please upgrade to version 2.0.0 as soon as you can. Version 2.0.0 is released on March 1st, 2017. +## Installation -## Heroku Stack Heroku-16 update -Version 2.2.0 changed the config for setting the Heroku API key. This will now reside in WORKLESS_API_KEY. Please change this key in your Heroku setup when upgrading this gem! +Add the workless gem and the delayed_job gem to your project Gemfile and update your bundle. Its is recommended to specify the gem version for delayed_job -## Updates +
+gem "delayed_job_active_record"
+gem 'workless', git: 'https://github.com/patricklindsay/workless.git', tag: 'v3.0.0'
+
-* Version 2.2.0 Revitalized by @davidakachaos through his workless_revived project, now merged for a 2.2.0 release. -* Version 1.3.0 DROPS SUPPORT FOR OLDER RUBY AND RAILS VERSIONS! -* Version 1.2.5 Added middleware to check on delayed jobs, fixed Rails 5 support -* Version 1.2.4 drops support for older versions! -* Version 1.2.3 replaces multiple commit callback with two callbacks for compatibility by @lostboy -* Version 1.2.2 includes after_commit fix by @collectiveip -* Version 1.2.1 includes support for Rails 4 & DJ 4 by @florentmorin -* Version 1.2.0 includes new support for Sequel by @davidakachaos -* Version 1.1.3 includes changes by @radanskoric to reduce number of heroku api calls -* Version 1.1.2 includes a change by @davidakachaos to scale workers using after_commit -* Version 1.1.1 includes a fix from @filiptepper and @fixr to correctly scale workers -* Version 1.1.0 has been released, this adds support for scaling using multiple workers thanks to @jaimeiniesta and @davidakachaos. -* Version 1.0.0 has been released, this brings compatibility with delayed_job 3 and compatibility with Rails 2.3.x and up. +If you don't specify delayed_job in your Gemfile workless will bring it in, most likely the latest version (4.1.2) -## Compatibility +Add your Heroku app name & [API key](https://devcenter.heroku.com/articles/authentication) as config vars to your Heroku instance. -Workless should work correctly with Rubies 2.0.0 and up. It is compatible with Delayed Job since version 2.0.7 up to the latest version 4.1.2, the table below shows tested compatibility with ruby, rails and delayed_job +
+heroku config:add WORKLESS_API_KEY=yourapikey APP_NAME=yourherokuappname
+
-Ruby | Rails | Delayed Job ----------- | ------ | ----- -2.2.5 | 4.2 | 2.1.4 -2.3.1 | 5.0 | 4.1.2 -2.4.1 | 5.1 | 4.1.3 +If you're wanting to use `run_at` functionality add this to your `ApplicationController`; -## Installation +
+before_action :work_off_delayed_jobs
+
-Add the workless gem and the delayed_job gem to your project Gemfile and update your bundle. Its is recommended to specify the gem version for delayed_job +You're good to go! Whenever a job is created Workless will automatically provision a workers and turn them off when all jobs are complete. -### For rails 4.x with latest delayed_job 3.x using active record -
-gem "delayed_job_active_record"
-gem "workless", "~> 2.2.0"
-
+## How does Workless work? -### For rails 5.x with latest delayed_job 3.x using active record +Workless activates workers in two ways; -
-gem "delayed_job_active_record"
-gem "workless", "~> 2.0.0"
-
+1. + * When a job is created, a `create` callback starts a worker. + * The worker runs the job, which removes it from the database. + * A `destroy` callback stops the worker. +2. + * Upon each controller request Workless checks if workers need to be activated. This check is limited with a default timeout of 1 minute and can be configured through the `work_off_timeout` variable. -If you don't specify delayed_job in your Gemfile workless will bring it in, most likely the latest version (4.1.2) +## Failing Jobs -Add your Heroku app name / [API key](https://devcenter.heroku.com/articles/authentication) as config vars to your Heroku instance. +In the case of failed jobs Workless will only shut down the DJ worker if all attempts have been tried. By default Delayed Job will try 25 times to process a job with ever increasing time delays between each unsuccessful attempt. It is recommended to set the `max_attempts` to something lower such as `3`.
-heroku config:add WORKLESS_API_KEY=yourapikey APP_NAME=yourherokuappname
+delayed::worker.max_attempts = 3
 
-## Failing Jobs - -In the case of failed jobs Workless will only shut down the dj worker if all attempts have been tried. By default Delayed Job will try 25 times to process a job with ever increasing time delays between each unsucessful attempt. Because of this Workless configures Delayed Job to try failed jobs only 3 times to reduce the amount of time a worker can be running while trying to process them. ## Configuration +Configure the timeout Workless uses between checking if workers are required (default is 1 minute); + +
+Workless.work_off_timeout = 30.seconds
+
+ Workless can be disabled by using the null scaler that will ignore the workers requests to scale up and down. In an environment file add this in the config block:
@@ -106,13 +94,6 @@ heroku config:add WORKLESS_WORKERS_RATIO=50
 
 In this example, it will scale up to a maximum of 10 workers, firing up 1 worker for every 50 jobs on the queue. The minimum will be 0 workers, but you could set it to a higher value if you want.
 
-## How does Workless work?
-
-- `Delayed::Workless::Scaler` is mixed into the `Delayed::Job` class, which adds a bunch of callbacks to it.
-- When a job is created on the database, a `create` callback starts a worker.
-- The worker runs the job, which removes it from the database.
-- A `destroy` callback stops the worker.
-
 ## Note on Patches/Pull Requests
 
 * Please fork the project.
diff --git a/lib/workless.rb b/lib/workless.rb
index a7a198b..6700267 100644
--- a/lib/workless.rb
+++ b/lib/workless.rb
@@ -2,5 +2,15 @@
 
 require File.dirname(__FILE__) + '/workless/scalers/base'
 require File.dirname(__FILE__) + '/workless/scaler'
-require File.dirname(__FILE__) + '/workless/middleware/workless_checker' if defined?(Rails::Railtie)
+require File.dirname(__FILE__) + '/workless/controllers/helpers'
 require File.dirname(__FILE__) + '/workless/railtie' if defined?(Rails::Railtie)
+
+ActiveSupport.on_load(:action_controller) do
+  include Workless::Controllers::Helpers
+end
+
+module Workless
+  # The minimum timeout between Workless checking if jobs need to be worked
+  mattr_accessor :work_off_timeout
+  @@work_off_timeout = 1.minute
+end
diff --git a/lib/workless/controllers/helpers.rb b/lib/workless/controllers/helpers.rb
new file mode 100644
index 0000000..64a8aa6
--- /dev/null
+++ b/lib/workless/controllers/helpers.rb
@@ -0,0 +1,22 @@
+module Workless
+  module Controllers
+    module Helpers
+      # Keep a timestamp of when job queue & worker count was checked
+      @@last_job_work_off_timestamp = nil
+
+      # Checks if workers need to be provisioned. Check is limited to once every 'work_off_timeout'
+      def work_off_delayed_jobs
+        return unless work_off_delayed_jobs?
+
+        @@last_job_work_off_timestamp = Time.now
+        Delayed::Job.scaler.up unless Delayed::Job.scaler.jobs.empty?
+      end
+
+      def work_off_delayed_jobs?
+        return true unless @@last_job_work_off_timestamp.present?
+
+        Time.now >= @@last_job_work_off_timestamp + Workless.work_off_timeout
+      end
+    end
+  end
+end
diff --git a/lib/workless/initialize.rb b/lib/workless/initialize.rb
index 90fb33f..dbe48e8 100644
--- a/lib/workless/initialize.rb
+++ b/lib/workless/initialize.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
-Delayed::Worker.max_attempts ||= 3
 Delayed::Backend::ActiveRecord::Job.send(:include, Delayed::Workless::Scaler) if defined?(Delayed::Backend::ActiveRecord::Job)
 Delayed::Backend::Mongoid::Job.send(:include, Delayed::Workless::Scaler) if defined?(Delayed::Backend::Mongoid::Job)
 Delayed::Backend::MongoMapper::Job.send(:include, Delayed::Workless::Scaler) if defined?(Delayed::Backend::MongoMapper::Job)
 Delayed::Backend::Sequel::Job.send(:include, Delayed::Workless::Scaler) if defined?(Delayed::Backend::Sequel::Job)
+
diff --git a/lib/workless/middleware/workless_checker.rb b/lib/workless/middleware/workless_checker.rb
deleted file mode 100644
index 4aa54f7..0000000
--- a/lib/workless/middleware/workless_checker.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-class WorklessChecker
-  def initialize(app)
-    @app = app
-  end
-
-  def call(env)
-    status, headers, response = @app.call(env)
-    return [status, headers, response] if file?(headers) || empty?(response)
-
-    Delayed::Job.scaler.up unless Delayed::Job.scaler.jobs.empty?
-
-    [status, headers, response]
-  end
-
-  # fix issue if response's body is a Proc
-  def empty?(response)
-    # response may be ["Not Found"], ["Move Permanently"], etc.
-    (response.is_a?(Array) && response.size <= 1) ||
-      !response.respond_to?(:body) ||
-      !response.body.respond_to?(:empty?) ||
-      response.body.empty?
-  end
-
-  # if send file?
-  def file?(headers)
-    headers['Content-Transfer-Encoding'] == 'binary'
-  end
-end
diff --git a/lib/workless/railtie.rb b/lib/workless/railtie.rb
index b4787b7..022c69f 100644
--- a/lib/workless/railtie.rb
+++ b/lib/workless/railtie.rb
@@ -5,7 +5,6 @@ module Delayed
   class Railtie < Rails::Railtie
     initializer :after_initialize do |config|
       require 'workless/initialize'
-      config.middleware.use WorklessChecker
     end
   end
 end
diff --git a/spec/workless/middleware_spec.rb b/spec/workless/middleware_spec.rb
deleted file mode 100644
index a0e2734..0000000
--- a/spec/workless/middleware_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require 'rack/mock'
-require 'rack/test'
-require 'delayed_job'
-# Load the middleware
-require_relative '../../lib/workless/middleware/workless_checker.rb'
-
-describe WorklessChecker do
-  let(:app) { lambda {|_env| [200, {'Content-Type' => 'text/plain'}, ['OK']]} }
-  subject { WorklessChecker.new(app) }
-
-  context "when a GET request comes in" do
-    let(:request) { Rack::MockRequest.new(subject) }
-    before(:each) do
-      request.get("/some/path", 'CONTENT_TYPE' => 'text/plain')
-    end
-
-    context "when there are no jobs" do
-      pending 'Figure out how to test middleware'
-      before do
-        # Delayed::Job.scaler.stub(:jobs).and_return(NumWorkers.new(0))
-      end
-      it 'should not scale up' do
-        # Delayed::Job.scaler.should_not_receive(:up)
-      end
-    end
-  end
-end
\ No newline at end of file

From e70ef68554bfee81d246fbda737374cb291b679d Mon Sep 17 00:00:00 2001
From: Patrick Lindsay 
Date: Tue, 13 Mar 2018 18:38:13 -0700
Subject: [PATCH 2/7] [IMPROVEMENT] run_at support - Workers are only active
 when they have no run_at set or if the run_at is before the next workoff
 check

---
 README.md                           | 20 +++-----------------
 lib/workless/controllers/helpers.rb |  2 +-
 lib/workless/scaler.rb              |  7 ++++---
 lib/workless/scalers/base.rb        |  3 ++-
 4 files changed, 10 insertions(+), 22 deletions(-)

diff --git a/README.md b/README.md
index b09fc41..9259810 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Add your Heroku app name & [API key](https://devcenter.heroku.com/articles/authe
 heroku config:add WORKLESS_API_KEY=yourapikey APP_NAME=yourherokuappname
 
-If you're wanting to use `run_at` functionality add this to your `ApplicationController`; +Lastly, add the below callback to your `ApplicationController`.
 before_action :work_off_delayed_jobs
@@ -36,22 +36,8 @@ You're good to go! Whenever a job is created Workless will automatically provisi
 ## How does Workless work?
 
 Workless activates workers in two ways;
-
-1.
-  * When a job is created, a `create` callback starts a worker.
-  * The worker runs the job, which removes it from the database.
-  * A `destroy` callback stops the worker.
-2.
-  * Upon each controller request Workless checks if workers need to be activated. This check is limited with a default timeout of 1 minute and can be configured through the `work_off_timeout` variable.
-
-
-## Failing Jobs
-
-In the case of failed jobs Workless will only shut down the DJ worker if all attempts have been tried. By default Delayed Job will try 25 times to process a job with ever increasing time delays between each unsuccessful attempt. It is recommended to set the `max_attempts` to something lower such as `3`.
-
-
-delayed::worker.max_attempts = 3
-
+1. When a job is created a callback starts a worker so long as the job is to be ran straight away or before the next check (defined by `Workless.work_off_timeout`) +2. Upon each controller request Workless checks if workers need to be activated. This picks up scheduled or previously failed jobs. ## Configuration diff --git a/lib/workless/controllers/helpers.rb b/lib/workless/controllers/helpers.rb index 64a8aa6..5278418 100644 --- a/lib/workless/controllers/helpers.rb +++ b/lib/workless/controllers/helpers.rb @@ -4,7 +4,7 @@ module Helpers # Keep a timestamp of when job queue & worker count was checked @@last_job_work_off_timestamp = nil - # Checks if workers need to be provisioned. Check is limited to once every 'work_off_timeout' + # Checks if workers need to be provisioned. Limited to once every 'work_off_timeout' def work_off_delayed_jobs return unless work_off_delayed_jobs? diff --git a/lib/workless/scaler.rb b/lib/workless/scaler.rb index 0daa45d..beaaa34 100644 --- a/lib/workless/scaler.rb +++ b/lib/workless/scaler.rb @@ -18,7 +18,7 @@ def self.included(base) self.class.scaler.down end after_commit(on: :create) do - self.class.scaler.up + self.class.scaler.up unless Delayed::Job.scaler.jobs.empty? end end elsif base.to_s =~ /Sequel/ @@ -28,7 +28,7 @@ def self.included(base) end base.send(:define_method, 'after_create') do super - self.class.scaler.up + self.class.scaler.up unless Delayed::Job.scaler.jobs.empty? end base.send(:define_method, 'after_update') do super @@ -37,7 +37,8 @@ def self.included(base) else base.class_eval do after_destroy 'self.class.scaler.down' - after_create 'self.class.scaler.up' + after_create 'self.class.scaler.up', unless: proc { Delayed::Job.scaler.jobs.empty? } + after_update 'self.class.scaler.down', unless: proc { |r| r.failed_at.nil? } end end diff --git a/lib/workless/scalers/base.rb b/lib/workless/scalers/base.rb index 2ad81d8..616e673 100644 --- a/lib/workless/scalers/base.rb +++ b/lib/workless/scalers/base.rb @@ -7,7 +7,8 @@ module Workless module Scaler class Base def self.jobs - Delayed::Job.where(failed_at: nil) + next_check_at = Time.now + Workless.work_off_timeout + Delayed::Job.where(failed_at: nil).where("run_at is NULL or run_at < ?", next_check_at) end end From 121e8ae6cc09ae24ae89abd8be885e64a45f826f Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Sat, 17 Mar 2018 20:44:13 -0700 Subject: [PATCH 3/7] Update configuration call to specify root namespace --- lib/workless/scalers/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/workless/scalers/base.rb b/lib/workless/scalers/base.rb index 616e673..348ff07 100644 --- a/lib/workless/scalers/base.rb +++ b/lib/workless/scalers/base.rb @@ -7,7 +7,7 @@ module Workless module Scaler class Base def self.jobs - next_check_at = Time.now + Workless.work_off_timeout + next_check_at = Time.now + ::Workless.work_off_timeout Delayed::Job.where(failed_at: nil).where("run_at is NULL or run_at < ?", next_check_at) end end From 1313de2dee0921227154474c78edf8c19b6d202a Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Sun, 18 Mar 2018 13:17:59 -0700 Subject: [PATCH 4/7] Update APP_NAME var to HEROKU_APP_NAME to be automatically compatible with review apps. Note: Ideally this would be set through gem configuration rather than ENV var --- README.md | 2 +- lib/workless/scalers/heroku.rb | 6 +++--- spec/spec_helper.rb | 2 +- spec/workless/mongoid_scaling_spec.rb | 2 +- spec/workless/scalers/heroku_multiple_workers_spec.rb | 2 +- spec/workless/scalers/heroku_spec.rb | 4 ++-- spec/workless/scalers/run_at_spec.rb | 2 +- spec/workless/sequel_scaling_spec.rb | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9259810..b74e409 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ If you don't specify delayed_job in your Gemfile workless will bring it in, most Add your Heroku app name & [API key](https://devcenter.heroku.com/articles/authentication) as config vars to your Heroku instance.
-heroku config:add WORKLESS_API_KEY=yourapikey APP_NAME=yourherokuappname
+heroku config:add WORKLESS_API_KEY=yourapikey HEROKU_APP_NAME=yourherokuappname
 
Lastly, add the below callback to your `ApplicationController`. diff --git a/lib/workless/scalers/heroku.rb b/lib/workless/scalers/heroku.rb index 98ee8fd..8c0cc3e 100644 --- a/lib/workless/scalers/heroku.rb +++ b/lib/workless/scalers/heroku.rb @@ -11,17 +11,17 @@ class Heroku < Base def self.up return unless workers_needed > min_workers && workers < workers_needed updates = { "quantity": workers_needed } - client.formation.update(ENV['APP_NAME'], 'worker', updates) + client.formation.update(ENV['HEROKU_APP_NAME'], 'worker', updates) end def self.down return if workers == workers_needed updates = { "quantity": workers_needed } - client.formation.update(ENV['APP_NAME'], 'worker', updates) + client.formation.update(ENV['HEROKU_APP_NAME'], 'worker', updates) end def self.workers - client.formation.info(ENV['APP_NAME'], 'worker')['quantity'].to_i + client.formation.info(ENV['HEROKU_APP_NAME'], 'worker')['quantity'].to_i end # Returns the number of workers needed based on the current number of pending jobs and the settings defined by: diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index aec0492..74194ee 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -92,7 +92,7 @@ def count Delayed::MongoMapper::Job::Mock.send(:include, Delayed::Workless::Scaler) Delayed::Sequel::Job::Mock.send(:include, Delayed::Workless::Scaler) -ENV['APP_NAME'] = 'TestHerokuApp' +ENV['HEROKU_APP_NAME'] = 'TestHerokuApp' RSpec.configure do |config| config.expect_with(:rspec) { |c| c.syntax = :should } diff --git a/spec/workless/mongoid_scaling_spec.rb b/spec/workless/mongoid_scaling_spec.rb index 7b8e39c..89b1af7 100644 --- a/spec/workless/mongoid_scaling_spec.rb +++ b/spec/workless/mongoid_scaling_spec.rb @@ -85,7 +85,7 @@ def if_there_are_jobs(num) def should_scale_workers_to(num) updates = { "quantity": num } - Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['APP_NAME'], 'worker', updates) + Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates) end def should_not_scale_workers diff --git a/spec/workless/scalers/heroku_multiple_workers_spec.rb b/spec/workless/scalers/heroku_multiple_workers_spec.rb index b9cf1e1..ba6f2b2 100644 --- a/spec/workless/scalers/heroku_multiple_workers_spec.rb +++ b/spec/workless/scalers/heroku_multiple_workers_spec.rb @@ -220,7 +220,7 @@ def if_there_are_jobs(num) def should_scale_workers_to(num) updates = { "quantity": num } - Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['APP_NAME'], 'worker', updates) + Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates) end def should_not_scale_workers diff --git a/spec/workless/scalers/heroku_spec.rb b/spec/workless/scalers/heroku_spec.rb index fbde06b..949092e 100644 --- a/spec/workless/scalers/heroku_spec.rb +++ b/spec/workless/scalers/heroku_spec.rb @@ -19,7 +19,7 @@ it 'should set the workers to 1' do updates = { "quantity": 1 } - Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['APP_NAME'], 'worker', updates) + Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates) Delayed::Workless::Scaler::Heroku.up end end @@ -59,7 +59,7 @@ it 'should set the workers to 0' do updates = { "quantity": 0 } - Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['APP_NAME'], 'worker', updates) + Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates) Delayed::Workless::Scaler::Heroku.down end end diff --git a/spec/workless/scalers/run_at_spec.rb b/spec/workless/scalers/run_at_spec.rb index 8145af7..66a7de8 100644 --- a/spec/workless/scalers/run_at_spec.rb +++ b/spec/workless/scalers/run_at_spec.rb @@ -38,7 +38,7 @@ def if_there_are_jobs(num) def should_scale_workers_to(num) updates = { "quantity": num } - Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['APP_NAME'], 'worker', updates) + Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates) end def should_not_scale_workers diff --git a/spec/workless/sequel_scaling_spec.rb b/spec/workless/sequel_scaling_spec.rb index ccc9d3a..f34838a 100644 --- a/spec/workless/sequel_scaling_spec.rb +++ b/spec/workless/sequel_scaling_spec.rb @@ -77,7 +77,7 @@ def if_there_are_jobs(num) def should_scale_workers_to(num) updates = { "quantity": num } - Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['APP_NAME'], 'worker', updates) + Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates) end def should_not_scale_workers From 4e0841c4cacecab89f2bae7c5f632ecd200b23de Mon Sep 17 00:00:00 2001 From: Patrick Lindsay Date: Sun, 18 Mar 2018 14:26:40 -0700 Subject: [PATCH 5/7] Add heroku_app_name config variable --- README.md | 13 ++++++++++++- lib/workless.rb | 4 ++++ lib/workless/scalers/heroku.rb | 6 +++--- spec/spec_helper.rb | 2 +- spec/workless/mongoid_scaling_spec.rb | 2 +- .../scalers/heroku_multiple_workers_spec.rb | 2 +- spec/workless/scalers/heroku_spec.rb | 4 ++-- spec/workless/scalers/run_at_spec.rb | 2 +- spec/workless/sequel_scaling_spec.rb | 2 +- 9 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b74e409..e0c7264 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,23 @@ Workless activates workers in two ways; ## Configuration +### Run At Timing Configure the timeout Workless uses between checking if workers are required (default is 1 minute);
-Workless.work_off_timeout = 30.seconds
+workless.work_off_timeout = 30.seconds
 
+### Specifying the Application + +You can specify what Heroku application you're using either by setting the environment variable `HEROKU_APP_NAME` or by setting configuration variable. By default this configuration variable is set to `ENV['HEROKU_APP_NAME']` + +
+workless.heroku_app_name = 'skynet-app'
+
+ +### Disabling + Workless can be disabled by using the null scaler that will ignore the workers requests to scale up and down. In an environment file add this in the config block:
diff --git a/lib/workless.rb b/lib/workless.rb
index 6700267..87ac196 100644
--- a/lib/workless.rb
+++ b/lib/workless.rb
@@ -13,4 +13,8 @@ module Workless
   # The minimum timeout between Workless checking if jobs need to be worked
   mattr_accessor :work_off_timeout
   @@work_off_timeout = 1.minute
+
+  # The name of your Heroku application which will be scaled
+  mattr_accessor :heroku_app_name
+  @@heroku_app_name = ENV['HEROKU_APP_NAME']
 end
diff --git a/lib/workless/scalers/heroku.rb b/lib/workless/scalers/heroku.rb
index 8c0cc3e..c93106d 100644
--- a/lib/workless/scalers/heroku.rb
+++ b/lib/workless/scalers/heroku.rb
@@ -11,17 +11,17 @@ class Heroku < Base
         def self.up
           return unless workers_needed > min_workers && workers < workers_needed
           updates = { "quantity": workers_needed }
-          client.formation.update(ENV['HEROKU_APP_NAME'], 'worker', updates)
+          client.formation.update(::Workless.heroku_app_name, 'worker', updates)
         end
 
         def self.down
           return if workers == workers_needed
           updates = { "quantity": workers_needed }
-          client.formation.update(ENV['HEROKU_APP_NAME'], 'worker', updates)
+          client.formation.update(::Workless.heroku_app_name, 'worker', updates)
         end
 
         def self.workers
-          client.formation.info(ENV['HEROKU_APP_NAME'], 'worker')['quantity'].to_i
+          client.formation.info(::Workless.heroku_app_name, 'worker')['quantity'].to_i
         end
 
         # Returns the number of workers needed based on the current number of pending jobs and the settings defined by:
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 74194ee..a958665 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -92,7 +92,7 @@ def count
 Delayed::MongoMapper::Job::Mock.send(:include, Delayed::Workless::Scaler)
 Delayed::Sequel::Job::Mock.send(:include, Delayed::Workless::Scaler)
 
-ENV['HEROKU_APP_NAME'] = 'TestHerokuApp'
+::Workless.heroku_app_name = 'TestHerokuApp'
 
 RSpec.configure do |config|
   config.expect_with(:rspec) { |c| c.syntax = :should }
diff --git a/spec/workless/mongoid_scaling_spec.rb b/spec/workless/mongoid_scaling_spec.rb
index 89b1af7..bb96611 100644
--- a/spec/workless/mongoid_scaling_spec.rb
+++ b/spec/workless/mongoid_scaling_spec.rb
@@ -85,7 +85,7 @@ def if_there_are_jobs(num)
 
   def should_scale_workers_to(num)
     updates = { "quantity": num }
-    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates)
+    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with('TestHerokuApp', 'worker', updates)
   end
 
   def should_not_scale_workers
diff --git a/spec/workless/scalers/heroku_multiple_workers_spec.rb b/spec/workless/scalers/heroku_multiple_workers_spec.rb
index ba6f2b2..aaa07d0 100644
--- a/spec/workless/scalers/heroku_multiple_workers_spec.rb
+++ b/spec/workless/scalers/heroku_multiple_workers_spec.rb
@@ -220,7 +220,7 @@ def if_there_are_jobs(num)
 
   def should_scale_workers_to(num)
     updates = { "quantity": num }
-    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates)
+    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with('TestHerokuApp', 'worker', updates)
   end
 
   def should_not_scale_workers
diff --git a/spec/workless/scalers/heroku_spec.rb b/spec/workless/scalers/heroku_spec.rb
index 949092e..56031e2 100644
--- a/spec/workless/scalers/heroku_spec.rb
+++ b/spec/workless/scalers/heroku_spec.rb
@@ -19,7 +19,7 @@
 
       it 'should set the workers to 1' do
         updates = { "quantity": 1 }
-        Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates)
+        Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with('TestHerokuApp', 'worker', updates)
         Delayed::Workless::Scaler::Heroku.up
       end
     end
@@ -59,7 +59,7 @@
 
       it 'should set the workers to 0' do
         updates = { "quantity": 0 }
-        Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates)
+        Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with('TestHerokuApp', 'worker', updates)
         Delayed::Workless::Scaler::Heroku.down
       end
     end
diff --git a/spec/workless/scalers/run_at_spec.rb b/spec/workless/scalers/run_at_spec.rb
index 66a7de8..8a5cba0 100644
--- a/spec/workless/scalers/run_at_spec.rb
+++ b/spec/workless/scalers/run_at_spec.rb
@@ -38,7 +38,7 @@ def if_there_are_jobs(num)
 
   def should_scale_workers_to(num)
     updates = { "quantity": num }
-    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates)
+    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with('TestHerokuApp', 'worker', updates)
   end
 
   def should_not_scale_workers
diff --git a/spec/workless/sequel_scaling_spec.rb b/spec/workless/sequel_scaling_spec.rb
index f34838a..6769780 100644
--- a/spec/workless/sequel_scaling_spec.rb
+++ b/spec/workless/sequel_scaling_spec.rb
@@ -77,7 +77,7 @@ def if_there_are_jobs(num)
 
   def should_scale_workers_to(num)
     updates = { "quantity": num }
-    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with(ENV['HEROKU_APP_NAME'], 'worker', updates)
+    Delayed::Workless::Scaler::Heroku.client.formation.should_receive(:update).once.with('TestHerokuApp', 'worker', updates)
   end
 
   def should_not_scale_workers

From ced5496f9862e67855b3b5c26d929478bda31afb Mon Sep 17 00:00:00 2001
From: Patrick Lindsay 
Date: Sun, 18 Mar 2018 15:04:09 -0700
Subject: [PATCH 6/7] Add basic error handling when communication with Heroku
 API

---
 lib/workless/scalers/heroku.rb | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/lib/workless/scalers/heroku.rb b/lib/workless/scalers/heroku.rb
index c93106d..edf1760 100644
--- a/lib/workless/scalers/heroku.rb
+++ b/lib/workless/scalers/heroku.rb
@@ -12,16 +12,23 @@ def self.up
           return unless workers_needed > min_workers && workers < workers_needed
           updates = { "quantity": workers_needed }
           client.formation.update(::Workless.heroku_app_name, 'worker', updates)
+        rescue => error
+          handle_api_error(error.message)
         end
 
         def self.down
           return if workers == workers_needed
           updates = { "quantity": workers_needed }
           client.formation.update(::Workless.heroku_app_name, 'worker', updates)
+        rescue => error
+          handle_api_error(error.message)
         end
 
         def self.workers
           client.formation.info(::Workless.heroku_app_name, 'worker')['quantity'].to_i
+        rescue => error
+          handle_api_error(error.message)
+          0
         end
 
         # Returns the number of workers needed based on the current number of pending jobs and the settings defined by:
@@ -49,6 +56,11 @@ def self.max_workers
         def self.min_workers
           ENV['WORKLESS_MIN_WORKERS'].present? ? ENV['WORKLESS_MIN_WORKERS'].to_i : 0
         end
+
+        def self.handle_api_error(message)
+          Rails.logger.error("Workless: Error connecting to Heroku - #{message}")
+          nil
+        end
       end
     end
   end

From 1b1fd0f838a8b2d31d401598649bebcb2b015bb2 Mon Sep 17 00:00:00 2001
From: Patrick Lindsay 
Date: Wed, 6 Jun 2018 09:26:32 -0700
Subject: [PATCH 7/7] Make Heroku error handling configurable

---
 .gitignore                           |   5 -
 .ruby-version                        |   1 -
 Gemfile.lock                         | 185 +++++++++++++++++++++++++++
 README.md                            |  20 ++-
 lib/workless.rb                      |   5 +
 lib/workless/heroku_error_handler.rb |  12 ++
 lib/workless/scalers/heroku.rb       |  11 +-
 spec/workless/scalers/heroku_spec.rb |   7 +
 workless.gemspec                     |   1 +
 9 files changed, 232 insertions(+), 15 deletions(-)
 delete mode 100644 .ruby-version
 create mode 100644 Gemfile.lock
 create mode 100644 lib/workless/heroku_error_handler.rb

diff --git a/.gitignore b/.gitignore
index ea0dbff..b7efb51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,8 +21,3 @@ coverage
 rdoc
 pkg
 .rvmrc
-
-## PROJECT::SPECIFIC
-Gemfile.lock
-*.gem
-gemfiles/*.lock
diff --git a/.ruby-version b/.ruby-version
deleted file mode 100644
index 21bb5e1..0000000
--- a/.ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-2.2.5
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..e42cf9c
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,185 @@
+PATH
+  remote: .
+  specs:
+    workless (2.2.0)
+      delayed_job (>= 2.0.7)
+      platform-api
+      rails
+      rush
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    actioncable (5.1.5)
+      actionpack (= 5.1.5)
+      nio4r (~> 2.0)
+      websocket-driver (~> 0.6.1)
+    actionmailer (5.1.5)
+      actionpack (= 5.1.5)
+      actionview (= 5.1.5)
+      activejob (= 5.1.5)
+      mail (~> 2.5, >= 2.5.4)
+      rails-dom-testing (~> 2.0)
+    actionpack (5.1.5)
+      actionview (= 5.1.5)
+      activesupport (= 5.1.5)
+      rack (~> 2.0)
+      rack-test (>= 0.6.3)
+      rails-dom-testing (~> 2.0)
+      rails-html-sanitizer (~> 1.0, >= 1.0.2)
+    actionview (5.1.5)
+      activesupport (= 5.1.5)
+      builder (~> 3.1)
+      erubi (~> 1.4)
+      rails-dom-testing (~> 2.0)
+      rails-html-sanitizer (~> 1.0, >= 1.0.3)
+    activejob (5.1.5)
+      activesupport (= 5.1.5)
+      globalid (>= 0.3.6)
+    activemodel (5.1.5)
+      activesupport (= 5.1.5)
+    activerecord (5.1.5)
+      activemodel (= 5.1.5)
+      activesupport (= 5.1.5)
+      arel (~> 8.0)
+    activesupport (5.1.5)
+      concurrent-ruby (~> 1.0, >= 1.0.2)
+      i18n (~> 0.7)
+      minitest (~> 5.1)
+      tzinfo (~> 1.1)
+    arel (8.0.0)
+    builder (3.2.3)
+    codeclimate-test-reporter (1.0.8)
+      simplecov (<= 0.13)
+    coderay (1.1.2)
+    concurrent-ruby (1.0.5)
+    coveralls (0.7.2)
+      multi_json (~> 1.3)
+      rest-client (= 1.6.7)
+      simplecov (>= 0.7)
+      term-ansicolor (= 1.2.2)
+      thor (= 0.18.1)
+    crass (1.0.3)
+    delayed_job (4.1.4)
+      activesupport (>= 3.0, < 5.2)
+    diff-lcs (1.3)
+    docile (1.1.5)
+    erubi (1.7.1)
+    erubis (2.7.0)
+    excon (0.60.0)
+    globalid (0.4.1)
+      activesupport (>= 4.2.0)
+    heroics (0.0.24)
+      erubis (~> 2.0)
+      excon
+      moneta
+      multi_json (>= 1.9.2)
+    i18n (0.9.5)
+      concurrent-ruby (~> 1.0)
+    json (2.1.0)
+    loofah (2.2.0)
+      crass (~> 1.0.2)
+      nokogiri (>= 1.5.9)
+    mail (2.7.0)
+      mini_mime (>= 0.1.1)
+    method_source (0.9.0)
+    mime-types (3.1)
+      mime-types-data (~> 3.2015)
+    mime-types-data (3.2016.0521)
+    mini_mime (1.0.0)
+    mini_portile2 (2.3.0)
+    minitest (5.11.3)
+    moneta (0.8.1)
+    multi_json (1.13.1)
+    nio4r (2.2.0)
+    nokogiri (1.8.2)
+      mini_portile2 (~> 2.3.0)
+    platform-api (2.1.0)
+      heroics (~> 0.0.23)
+      moneta (~> 0.8.1)
+    pry (0.11.3)
+      coderay (~> 1.1.0)
+      method_source (~> 0.9.0)
+    pry-rails (0.3.6)
+      pry (>= 0.10.4)
+    rack (2.0.4)
+    rack-test (0.8.3)
+      rack (>= 1.0, < 3)
+    rails (5.1.5)
+      actioncable (= 5.1.5)
+      actionmailer (= 5.1.5)
+      actionpack (= 5.1.5)
+      actionview (= 5.1.5)
+      activejob (= 5.1.5)
+      activemodel (= 5.1.5)
+      activerecord (= 5.1.5)
+      activesupport (= 5.1.5)
+      bundler (>= 1.3.0)
+      railties (= 5.1.5)
+      sprockets-rails (>= 2.0.0)
+    rails-dom-testing (2.0.3)
+      activesupport (>= 4.2.0)
+      nokogiri (>= 1.6)
+    rails-html-sanitizer (1.0.3)
+      loofah (~> 2.0)
+    railties (5.1.5)
+      actionpack (= 5.1.5)
+      activesupport (= 5.1.5)
+      method_source
+      rake (>= 0.8.7)
+      thor (>= 0.18.1, < 2.0)
+    rake (12.3.0)
+    rest-client (1.6.7)
+      mime-types (>= 1.16)
+    rspec (3.7.0)
+      rspec-core (~> 3.7.0)
+      rspec-expectations (~> 3.7.0)
+      rspec-mocks (~> 3.7.0)
+    rspec-core (3.7.1)
+      rspec-support (~> 3.7.0)
+    rspec-expectations (3.7.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.7.0)
+    rspec-mocks (3.7.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.7.0)
+    rspec-support (3.7.1)
+    rush (0.6.8)
+      session
+    session (3.2.0)
+    simplecov (0.13.0)
+      docile (~> 1.1.0)
+      json (>= 1.8, < 3)
+      simplecov-html (~> 0.10.0)
+    simplecov-html (0.10.2)
+    sprockets (3.7.1)
+      concurrent-ruby (~> 1.0)
+      rack (> 1, < 3)
+    sprockets-rails (3.2.1)
+      actionpack (>= 4.0)
+      activesupport (>= 4.0)
+      sprockets (>= 3.0.0)
+    term-ansicolor (1.2.2)
+      tins (~> 0.8)
+    thor (0.18.1)
+    thread_safe (0.3.6)
+    tins (0.13.2)
+    tzinfo (1.2.5)
+      thread_safe (~> 0.1)
+    websocket-driver (0.6.5)
+      websocket-extensions (>= 0.1.0)
+    websocket-extensions (0.1.3)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  codeclimate-test-reporter (~> 1.0.0)
+  coveralls
+  pry-rails (~> 0.3)
+  rspec
+  simplecov
+  workless!
+
+BUNDLED WITH
+   1.16.1
diff --git a/README.md b/README.md
index e0c7264..254927c 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ Workless activates workers in two ways;
 Configure the timeout Workless uses between checking if workers are required (default is 1 minute);
 
 
-workless.work_off_timeout = 30.seconds
+Workless.work_off_timeout = 30.seconds
 
### Specifying the Application @@ -54,7 +54,23 @@ workless.work_off_timeout = 30.seconds You can specify what Heroku application you're using either by setting the environment variable `HEROKU_APP_NAME` or by setting configuration variable. By default this configuration variable is set to `ENV['HEROKU_APP_NAME']`
-workless.heroku_app_name = 'skynet-app'
+Workless.heroku_app_name = 'skynet-app'
+
+ +### Heroku Error Handling + +By default if any error occurs when communicating with Heroku this will be raised up into the application. Although it's good to be aware of the error this may end up bringing down your application. This default behaviour can be changed by implementing a custom Heroku error handler. The example below silents the error whilst also raising it through Rollbar. + +``` +class MyHerokuErrorHandler + def self.handle(error) + Rollbar.error(error) + end +end +``` + +
+Workless.heroku_error_handler = 'MyHerokuErrorHandler'
 
### Disabling diff --git a/lib/workless.rb b/lib/workless.rb index 87ac196..62b01a2 100644 --- a/lib/workless.rb +++ b/lib/workless.rb @@ -3,6 +3,7 @@ require File.dirname(__FILE__) + '/workless/scalers/base' require File.dirname(__FILE__) + '/workless/scaler' require File.dirname(__FILE__) + '/workless/controllers/helpers' +require File.dirname(__FILE__) + '/workless/heroku_error_handler' require File.dirname(__FILE__) + '/workless/railtie' if defined?(Rails::Railtie) ActiveSupport.on_load(:action_controller) do @@ -17,4 +18,8 @@ module Workless # The name of your Heroku application which will be scaled mattr_accessor :heroku_app_name @@heroku_app_name = ENV['HEROKU_APP_NAME'] + + # Handler used when an error occurs communicating with Heroku + mattr_accessor :heroku_error_handler + @@heroku_error_handler = 'Workless::HerokuErrorHandler' end diff --git a/lib/workless/heroku_error_handler.rb b/lib/workless/heroku_error_handler.rb new file mode 100644 index 0000000..00253b9 --- /dev/null +++ b/lib/workless/heroku_error_handler.rb @@ -0,0 +1,12 @@ +module Workless + # Default behaviour to handle Heroku errors is to raise the error. This can be changed by implementing your own class which defines .handle and then updating Workless.heroku_error_handler configuration to link your custom class + class HerokuErrorHandler + + # Raises error + # + # @param error [Error] raised from communicating with Heroku + def self.handle(error) + raise error + end + end +end diff --git a/lib/workless/scalers/heroku.rb b/lib/workless/scalers/heroku.rb index edf1760..e0eb4d9 100644 --- a/lib/workless/scalers/heroku.rb +++ b/lib/workless/scalers/heroku.rb @@ -13,7 +13,7 @@ def self.up updates = { "quantity": workers_needed } client.formation.update(::Workless.heroku_app_name, 'worker', updates) rescue => error - handle_api_error(error.message) + handle_api_error(error) end def self.down @@ -21,14 +21,11 @@ def self.down updates = { "quantity": workers_needed } client.formation.update(::Workless.heroku_app_name, 'worker', updates) rescue => error - handle_api_error(error.message) + handle_api_error(error) end def self.workers client.formation.info(::Workless.heroku_app_name, 'worker')['quantity'].to_i - rescue => error - handle_api_error(error.message) - 0 end # Returns the number of workers needed based on the current number of pending jobs and the settings defined by: @@ -57,8 +54,8 @@ def self.min_workers ENV['WORKLESS_MIN_WORKERS'].present? ? ENV['WORKLESS_MIN_WORKERS'].to_i : 0 end - def self.handle_api_error(message) - Rails.logger.error("Workless: Error connecting to Heroku - #{message}") + def self.handle_api_error(error) + ::Workless.heroku_error_handler.constantize.handle(error) nil end end diff --git a/spec/workless/scalers/heroku_spec.rb b/spec/workless/scalers/heroku_spec.rb index 56031e2..f7da156 100644 --- a/spec/workless/scalers/heroku_spec.rb +++ b/spec/workless/scalers/heroku_spec.rb @@ -12,6 +12,13 @@ Delayed::Workless::Scaler::Heroku.stub(:jobs).and_return(NumWorkers.new(10)) end + context 'when an error occurs' do + it 'handles the error' do + Workless::HerokuErrorHandler.should_receive(:handle) + Delayed::Workless::Scaler::Heroku.up + end + end + context 'without workers' do before do Delayed::Workless::Scaler::Heroku.stub(:workers).and_return(0) diff --git a/workless.gemspec b/workless.gemspec index 40b0e78..de5a453 100644 --- a/workless.gemspec +++ b/workless.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.2.4' s.add_development_dependency('rspec') + s.add_development_dependency 'pry-rails', '~> 0.3' # Debugger s.post_install_message = %q{ Workless 2.2.0 introduces a backwards-incompatible change!