background_worker 0.0.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d96a3cbffa01ee490b1a2542a80b8fdf9593aacc
4
+ data.tar.gz: 609ef34234495ba8fa0f17da87ead826a2c134b5
5
+ SHA512:
6
+ metadata.gz: 2ae408b3eeb8261323d747d279c518a9a8e07906b86eb9b5c74d4d7b9830e58ef16dd146b480d4407e1a7fe7054d894c5ac7f8f65a29a48d5f063cd00ae61b8c
7
+ data.tar.gz: dd3a09b5ebe64ec8bffd43d8381b1024c1cc3344f74e24ba0cc423b7b0ad12cd37032e666ad50a4679eaed113f4f471d6104ee4fab1b2a834dd4fc67f1430820
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ Metrics/LineLength:
2
+ Max: 100
3
+
4
+ Documentation:
5
+ Enabled: false
6
+
7
+ Style/BlockDelimiters:
8
+ EnforcedStyle: semantic
9
+
10
+ Style/TrivialAccessors:
11
+ AllowPredicates: true
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in background_worker.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 The Sealink Travel Group
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.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ Background Worker
2
+ =================
3
+
4
+ Provides a worker abstraction with an additional status channel.
5
+
6
+ Start by making a worker class which extends from BackgroundWorker::Base
7
+
8
+ class MyWorker < BackgroundWorker::Base
9
+ def my_task(options={})
10
+ report_progress('Starting')
11
+ if options[:message].blank?
12
+ report_failed("No message provided")
13
+ return
14
+ end
15
+
16
+ puts options[:message]
17
+ {original_message: message}
18
+ end
19
+ end
20
+
21
+ Then, when you want to perform a task in the background, use
22
+ klass#perform_in_background which exists in Base:
23
+
24
+ worker_id = MyWorker.perform_in_background(:my_task, message: "hello!")
25
+
26
+ # Backgrounded
27
+
28
+ By default this will call your instance method in the foreground -- you have to
29
+ provide an #enqueue_with configuration like so:
30
+
31
+ BackgroundWorker.configure(
32
+ enqueue_with: -> klass, method_name, options {
33
+ Resque.enqueue(klass, method_name, options)
34
+ }
35
+ )
36
+
37
+ This is independent of the status reporting which (currently) always uses Redis.
38
+
39
+ # Getting the status
40
+
41
+ The worker_id you are returned can be used to get the status, and
42
+ whether the worker has finished successfully, failed, or in progress:
43
+
44
+ state = BackgroundWorker.get_state_of(worker_id)
45
+
46
+ The state is represented by a hash with the following keys:
47
+
48
+ message: Reported message
49
+ detailed_message: Detailed version of above when provided
50
+ status: :successful, :failed, or null if still processing
51
+ completed: True if report_failed, report_successful called (or worker
52
+ finished without exception -- which calls report_successful)
53
+ data: Arbitrary data returned by worker method on success or report_failed
54
+
55
+ If an exception is raised, the worker will call #report_failed with the
56
+ details. You can provide a callback with #after_exception in the config
57
+
58
+ # INSTALLATION
59
+
60
+ gem install background_worker
61
+
62
+ or add to your Gemfile:
63
+ gem 'background_worker'
64
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'background_worker/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'background_worker'
8
+ spec.version = BackgroundWorker::VERSION
9
+ spec.authors = ['Michael Noack', 'Adam Davies', 'Alessandro Berardi']
10
+ spec.email = ['development@travellink.com.au',
11
+ 'adzdavies@gmail.com',
12
+ 'berardialessandro@gmail.com']
13
+ spec.summary = 'Background worker abstraction with status updates'
14
+ spec.description = 'See README for full details'
15
+ spec.homepage = 'http://github.com/sealink/background_worker'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rubocop'
26
+ end
@@ -0,0 +1,86 @@
1
+ module BackgroundWorker
2
+ class Base
3
+ attr_accessor :uid, :state
4
+
5
+ def initialize(options = {})
6
+ Time.zone = Setting.time_zone
7
+
8
+ @uid = options['uid']
9
+
10
+ # Store state persistently, to enable status checkups & progress reporting
11
+ @state = BackgroundWorker::PersistentState.new(@uid, options.except('uid'))
12
+ log("Created #{self.class}")
13
+ log("Options are: #{options.pretty_inspect}")
14
+ end
15
+
16
+ # Report progress...
17
+ def report_progress(message)
18
+ state.message = message
19
+ state.save
20
+ end
21
+
22
+ # Report a minor progress -- may get called a lot, so don't save it so often
23
+ def report_minor_progress(message)
24
+ state.message = message
25
+
26
+ # Only report minor events once per second
27
+ @last_report ||= Time.now - 2
28
+ time_elapsed = Time.now - @last_report
29
+ return unless time_elapsed > 1
30
+
31
+ @last_report = Time.now
32
+ state.save
33
+ end
34
+
35
+ def report_successful(message = 'Finished successfully')
36
+ state.set_completed(message, :successful)
37
+ end
38
+
39
+ def report_failed(message = 'Failed', detailed_message = nil)
40
+ state.detailed_message = detailed_message
41
+ state.set_completed(message, :failed)
42
+ end
43
+
44
+ def logger
45
+ BackgroundWorker.logger
46
+ end
47
+
48
+ def log(message, options = {})
49
+ severity = options.fetch(:severity, :info)
50
+ logger.send(severity, "uid=#{uid} #{message}")
51
+ end
52
+
53
+ class << self
54
+ def get_state_of(worker_id)
55
+ BackgroundWorker::PersistentState.get_state_of(worker_id)
56
+ end
57
+
58
+ # Public method to do in background...
59
+ def perform_in_background(method_name, options = {})
60
+ method_name = method_name.to_sym
61
+ options[:uid] ||= BackgroundWorker::Uid.new(to_s, method_name).generate
62
+
63
+ # Store into shared-cache before kicking job off
64
+ BackgroundWorker::PersistentState.new(options[:uid], options.except(:uid))
65
+
66
+ # Enqueue to the background queue
67
+ BackgroundWorker.enqueue(self, method_name, options)
68
+
69
+ options[:uid]
70
+ end
71
+
72
+ # This method is called by the job runner
73
+ #
74
+ # It will just call your preferred method in the worker.
75
+ def perform(method_name, options = {})
76
+ BackgroundWorker.verify_active_connections!
77
+
78
+
79
+ worker = new(options)
80
+ execution = WorkerExecution.new(worker, method_name, options)
81
+ execution.call
82
+
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,40 @@
1
+ require 'logger'
2
+
3
+ module BackgroundWorker
4
+ class Config
5
+
6
+ attr_reader :logger
7
+
8
+ # Configuration includes following options:
9
+ # logger: what logger to user throughout
10
+ # enqueue_with: callback to execute the process
11
+ # after_exception: callback to handle exceptions (for example, error reporting service)
12
+ #
13
+ # eg:
14
+ # Config.new(
15
+ # logger: Rails.logger,
16
+ # enqueue_with: -> klass, method_name, opts { Resque.enqueue(klass, method_name, opts) },
17
+ # after_exception: -> e { Airbrake.notify(e) }
18
+ # )
19
+ def initialize(attrs)
20
+ @logger = attrs.fetch(:logger, ::Logger.new(STDOUT))
21
+ @enqueue_with = attrs.fetch(:enqueue_with, method(:foreground_enqueue))
22
+ @after_exception = attrs.fetch(:after_exception, method(:default_after_exception))
23
+ end
24
+
25
+ # Callback fired when an exception occurs
26
+ def after_exception(e)
27
+ @after_exception.call(e)
28
+ end
29
+
30
+ def foreground_enqueue(klass, method_name, opts)
31
+ klass.perform(method_name, opts)
32
+ end
33
+
34
+ def default_after_exception(e)
35
+ logger.error '** No after_exception handler installed **'
36
+ logger.error "Exception: #{e}"
37
+ logger.error "#{e.backtrace.join("\n")}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ # Progress reporter is used by background processes, to communicate with
2
+ # standard rails controllers.
3
+ #
4
+ # It works by storing a hash of all progress-report data in a redis value
5
+ # keyed by the worker_uid.
6
+ module BackgroundWorker
7
+ class PersistentState
8
+ attr_accessor :message, :detailed_message, :status, :completed, :data
9
+
10
+ def initialize(worker_uid, data)
11
+ @message = 'Waiting for task to queue...'
12
+ @status = :processing
13
+ @completed = false
14
+
15
+ @worker_uid = worker_uid
16
+ @data = data
17
+ save
18
+ end
19
+
20
+ def set_completed(message, status)
21
+ self.status = status
22
+ self.message = message
23
+
24
+ self.completed = true
25
+ save
26
+ end
27
+
28
+ # Save persistently (well for an hour at least)
29
+ def save
30
+ Rails.cache.write(@worker_uid, generate_persistent_hash, expires_in: 1.hour)
31
+ end
32
+
33
+ # Get a report out the queue
34
+ # (was .get_report, then .progress)
35
+ def self.get_state_of(worker_uid)
36
+ Rails.cache.read(worker_uid)
37
+ end
38
+
39
+ private
40
+
41
+ # Generate a hash of this objects state
42
+ # (representing this status progress report)
43
+ def generate_persistent_hash
44
+ {
45
+ message: message,
46
+ detailed_message: detailed_message,
47
+ status: status,
48
+ completed: completed,
49
+ data: data
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ # Generates a unique identifier for a particular job identified by class_name/method
2
+ module BackgroundWorker
3
+ class Uid
4
+ attr_reader :class_name, :method
5
+
6
+ def initialize(class_name, method)
7
+ @class_name = class_name
8
+ @method = method
9
+ end
10
+
11
+ def generate
12
+ "#{generate_uid_name}:#{generate_uid_hash}"
13
+ end
14
+
15
+ private
16
+
17
+ def generate_uid_hash
18
+ ::Digest::MD5.hexdigest("#{class_name}:#{method}:#{rand(1 << 64)}:#{Time.now}")
19
+ end
20
+
21
+ def generate_uid_name
22
+ "#{class_name.underscore}/#{method}".split('/').join(':')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module BackgroundWorker
2
+ VERSION = '0.0.3'
3
+ end
@@ -0,0 +1,44 @@
1
+ module BackgroundWorker
2
+ class WorkerExecution
3
+ attr_reader :worker, :method_name, :options
4
+
5
+ def initialize(worker, method_name, options)
6
+ fail ArgumentError, "'uid' is required to identify worker" unless options['uid'].present?
7
+ @worker = worker
8
+ @method_name = method_name
9
+ @options = options
10
+ end
11
+
12
+ def call
13
+ worker.send(method_name, options)
14
+ report_implicitly_successful unless completed?
15
+
16
+ rescue StandardError => e
17
+ log_worker_error(e)
18
+ BackgroundWorker.after_exception.call(e)
19
+
20
+ ensure
21
+ log_worker_finality
22
+ end
23
+
24
+ private
25
+
26
+ def completed?
27
+ worker.state.completed
28
+ end
29
+
30
+ def report_implicitly_successful
31
+ worker.report_successful
32
+ end
33
+
34
+ def log_worker_error(e)
35
+ worker.log("Implicit failure: Exception: #{e}", severity: :error)
36
+ worker.report_failed("An unhandled error occurred: #{e}") unless completed?
37
+ end
38
+
39
+ def log_worker_finality
40
+ worker.log "Final state: #{worker.state.data}"
41
+ worker.log "Job was #{worker.state.status}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ require 'background_worker/version'
2
+ require 'background_worker/config'
3
+ require 'background_worker/uid'
4
+ require 'background_worker/base'
5
+ require 'background_worker/persistent_state'
6
+
7
+ module BackgroundWorker
8
+ # Configure worker
9
+ #
10
+ # eg:
11
+ # BackgroundWorker.configure(
12
+ # logger: Rails.logger,
13
+ # enqueue_with: -> klass, method_name, opts { Resque.enqueue(klass, method_name, opts) },
14
+ # after_exception: -> e { Airbrake.notify(e) }
15
+ # )
16
+ def self.configure(options)
17
+ @config = Config.new(options)
18
+ end
19
+
20
+ def self.enqueue(klass, method_name, options)
21
+ config.enqueue_with.call(klass, method_name, options)
22
+ end
23
+
24
+ def self.logger
25
+ config.logger
26
+ end
27
+
28
+ def self.verify_active_connections!
29
+ Rails.cache.reconnect if defined?(Rails)
30
+ ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)
31
+ end
32
+
33
+ def self.after_exception(e)
34
+ config.after_exception(e)
35
+ end
36
+
37
+ def self.config
38
+ raise "Not configured!" unless @config
39
+ @config
40
+ end
41
+
42
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: background_worker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Michael Noack
8
+ - Adam Davies
9
+ - Alessandro Berardi
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2015-04-30 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rake
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: rubocop
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ description: See README for full details
58
+ email:
59
+ - development@travellink.com.au
60
+ - adzdavies@gmail.com
61
+ - berardialessandro@gmail.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - ".rubocop.yml"
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - background_worker.gemspec
72
+ - lib/background_worker.rb
73
+ - lib/background_worker/base.rb
74
+ - lib/background_worker/config.rb
75
+ - lib/background_worker/persistent_state.rb
76
+ - lib/background_worker/uid.rb
77
+ - lib/background_worker/version.rb
78
+ - lib/background_worker/worker_execution.rb
79
+ homepage: http://github.com/sealink/background_worker
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.4.5
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Background worker abstraction with status updates
103
+ test_files: []