background_worker 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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: []