simple_job 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = Simple Job
2
+
3
+ == Version 0.0.0
4
+ * Supports declaring queues, easy declaration of job definitions, and enqueuing/polling
5
+ * Contains a queue implementation backed by Amazon SQS
data/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 RevPAR Collective, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,101 @@
1
+ = Simple Job
2
+
3
+ A gem containing libraries that support running background jobs or tasks. It's designed to make it easy to:
4
+
5
+ * Define a job
6
+ * Enqueue a job
7
+ * Poll for and execute jobs
8
+
9
+ It is architected to support multiple types of queue implementations, but currently, it only includes an implementation using AWS SQS (http://aws.amazon.com/sqs/). Alternate queue implementations could be plugged in by the client using lib/simple_job/sqs_job_queue.rb as an example.
10
+
11
+ The AWS SQS queue implementation requires the aws-sdk gem, which must be initialized (by calling AWS.config) for this API to be capable of enqueuing or polling for jobs.
12
+
13
+
14
+ == Queue Configuration
15
+
16
+ Queue configuration must be done by both the client and the server.
17
+
18
+ Only the queues that will be used by each must be defined (a client may configure a subset of the queues used by the server, so long as it configures all the queues that it uses).
19
+
20
+ SQSJobQueue is the queue implementation used by default. This may be overridden by calling SimpleJob::JobQueue.config :implementation => 'my_queue_type'
21
+
22
+ === Minimal configuration - specify queue prefix and define one default queue
23
+
24
+ SimpleJob::SQSJobQueue.config :queue_prefix => 'my-job'
25
+ SimpleJob::SQSJobQueue.define_queue 'normal', :default => true
26
+
27
+ === Complex configuration with explicit queue implementation, non-rails-defined environment, and multiple queues
28
+
29
+ SimpleJob::JobQueue.config :implementation => 'sqs'
30
+ SimpleJob::SQSJobQueue.config :queue_prefix => 'my-job', :environment => 'production'
31
+ SimpleJob::SQSJobQueue.define_queue 'normal', :visibility_timeout => 60, :default => true
32
+ SimpleJob::SQSJobQueue.define_queue 'high-priority', :visibility_timeout => 10
33
+ SimpleJob::SQSJobQueue.define_queue 'long-running', :visibility_timeout => 3600
34
+
35
+
36
+ == Job Definition
37
+
38
+ class FooSender
39
+ include SimpleJob::JobDefinition
40
+
41
+ simple_job_attribute :target, :foo_content # defines getters/setters for each, and
42
+ # adds them to serialized message
43
+
44
+ validates :target, :presence => true # standard ActiveModel validation
45
+
46
+ def execute
47
+ puts "#{foo_content} -> #{target}"
48
+ end
49
+ end
50
+
51
+
52
+ == Job Client Usage
53
+
54
+ === Typical usage of default queue
55
+
56
+ You may call #enqueue with no arguments, in which case JobQueue.default will be used.
57
+
58
+ f = FooSender.new(:target => 'joe', :foo_content => 'foo!') # can also assign attributes with f.target=, f.foo_content=
59
+ if f.enqueue
60
+ puts 'i just sent some foo to joe!'
61
+ else
62
+ puts "the following errors occurred: #{f.errors.full_messages.join('; ')}"
63
+ end
64
+
65
+ json = f.to_json # { "type": "foo_sender", "version": "1", "data": { "target": "joe", "foo_content": "foo!" } }
66
+ f_copy = FooSender.new.from_json(json)
67
+
68
+ === Simple usage with explicit queue
69
+
70
+ To explicitly specify the queue to use, simply specify its type identifier (defined in SQSJobQueue.define_queue declaration) when calling enqueue.
71
+
72
+ f = FooSender.new
73
+ f.target = 'bob'
74
+ f.foo_content = 'foo!'
75
+ f.enqueue!('normal') # raises exception if operation fails
76
+
77
+ === Queue configuration for multiple enqueue operations
78
+
79
+ Alternatively, the queue may be attached to the job upfront for multiple enqueue operations:
80
+
81
+ FooSender.job_queue('high-priority')
82
+ f1 = FooSender.new(:target => 'cookie monster', :foo_content => 'cookies and milk')
83
+ f1.enqueue # high-priority queue will be used
84
+ f2 = FooSender.new(:target => 'oscar the grouch', :foo_content => 'pizza')
85
+ f2.enqueue # high-priority queue will be used
86
+
87
+
88
+ == Job Server Usage
89
+
90
+ Calling #poll on a queue instance will, by default, dispatch each job to the proper registered class based on type and version. Its options are passed through to the underlying implementation. In the case of the SQSJobQueue, poll accepts the options documented for the aws-sdk AWS::SQS::Queue#poll method.
91
+
92
+ JobQueue.default.poll(:poll_interval => 1, :idle_timeout => 60)
93
+
94
+ You can override the default behavior by passing a block to #poll that accepts both a job definition instance and an SQS message.
95
+
96
+ JobQueue.default.poll(:poll_interval => 1, :idle_timeout => 60) do |definition, message|
97
+ logger.debug "received message: #{message.inspect}"
98
+ logger.debug "dispatched to definition #{definition.inspect}; executing..."
99
+ definition.execute
100
+ end
101
+
@@ -0,0 +1,180 @@
1
+ require 'active_model'
2
+ require 'active_support/inflector'
3
+
4
+ module SimpleJob
5
+ module JobDefinition
6
+
7
+ RESERVED_ATTRIBUTES = [ :type, :version, :data ]
8
+
9
+ def self.included(klass)
10
+
11
+ klass.extend(ClassMethods)
12
+
13
+ klass.class_eval do
14
+ include ::ActiveModel::Validations
15
+ include ::ActiveModel::Serializers::JSON
16
+ end
17
+
18
+ klass.include_root_in_json = false
19
+ klass.register_simple_job
20
+
21
+ end
22
+
23
+ # should be overridden by including classes
24
+ if !method_defined?(:execute)
25
+ def execute
26
+ end
27
+ end
28
+
29
+ def attributes
30
+ {
31
+ 'type' => type,
32
+ 'version' => version,
33
+ 'data' => data,
34
+ }
35
+ end
36
+
37
+ def attributes=(attributes)
38
+ attributes.each do |key, value|
39
+ send("#{key}=", value)
40
+ end
41
+ end
42
+
43
+ def data
44
+ @data ||= {}
45
+ self.class.simple_job_attributes.each do |attribute|
46
+ @data[attribute.to_s] ||= nil
47
+ end
48
+ @data
49
+ end
50
+
51
+ def data=(data)
52
+ self.attributes = data
53
+ end
54
+
55
+ def type
56
+ self.class.definition[:type]
57
+ end
58
+
59
+ def type=(type)
60
+ if type.to_sym != self.type
61
+ raise "tried to deserialize object with type #{type}, but this object only " +
62
+ "supports type: #{self.type}"
63
+ end
64
+ end
65
+
66
+ def versions
67
+ self.class.definition[:versions]
68
+ end
69
+
70
+ def version
71
+ versions.first
72
+ end
73
+
74
+ def version=(version)
75
+ if !versions.include?(version.to_s)
76
+ raise "tried to deserialize object with version #{version}, but this object " +
77
+ "only supports versions: #{versions.join(", ")}"
78
+ end
79
+ end
80
+
81
+ def enqueue(queue_type = nil)
82
+ if valid?
83
+ queue = (queue_type && JobQueue[queue_type]) || self.class.job_queue || JobQueue.default
84
+ queue.enqueue(self.to_json)
85
+ else
86
+ false
87
+ end
88
+ end
89
+
90
+ def enqueue!(queue_type = nil)
91
+ enqueue(queue_type) || raise("object is not valid: #{errors.full_messages.join('; ')}")
92
+ end
93
+
94
+ def read_simple_job_attribute(attribute)
95
+ data[attribute.to_s]
96
+ end
97
+
98
+ def write_simple_job_attribute(attribute, value)
99
+ data[attribute.to_s] = value
100
+ end
101
+
102
+ def initialize(attributes = {})
103
+ attributes.each do |key, value|
104
+ send("#{key}=", value)
105
+ end
106
+ end
107
+
108
+ def self.job_definition_class_for(type, version)
109
+ @job_definitions.each do |definition|
110
+ if (definition[:type] == type.to_sym) && (definition[:versions].include?(version))
111
+ return definition[:class]
112
+ end
113
+ end
114
+ nil
115
+ end
116
+
117
+ def self.job_definitions
118
+ @job_definitions ||= []
119
+ end
120
+
121
+ private
122
+
123
+ module ClassMethods
124
+
125
+ def definition
126
+ @definition
127
+ end
128
+
129
+ def register_simple_job(options = {})
130
+ default_type = self.name.underscore.to_sym
131
+
132
+ @definition = {
133
+ :class => self,
134
+ :type => default_type,
135
+ :versions => [ '1' ],
136
+ }.merge(options)
137
+
138
+ @definition[:type] = @definition[:type].to_sym
139
+ @definition[:versions] = Array(@definition[:versions])
140
+ @definition[:versions].collect! { |value| value.to_s }
141
+
142
+ ::SimpleJob::JobDefinition.job_definitions.delete_if { |item| item[:type] == default_type }
143
+ ::SimpleJob::JobDefinition.job_definitions << @definition
144
+ end
145
+
146
+ def job_queue(queue_type = nil)
147
+ @job_queue = JobQueue[queue_type] if queue_type
148
+ @job_queue
149
+ end
150
+
151
+ def simple_job_attributes
152
+ @simple_job_attributes ||= []
153
+ end
154
+
155
+ def simple_job_attribute(*attributes)
156
+ attributes.each do |attribute|
157
+ attribute = attribute.to_sym
158
+
159
+ if RESERVED_ATTRIBUTES.include?(attribute)
160
+ raise "attempted to declare reserved attribute: #{attribute}"
161
+ end
162
+
163
+ simple_job_attributes << attribute
164
+
165
+ class_eval <<-__EOF__
166
+ def #{attribute}
167
+ read_simple_job_attribute(:#{attribute})
168
+ end
169
+
170
+ def #{attribute}=(value)
171
+ write_simple_job_attribute(:#{attribute}, value)
172
+ end
173
+ __EOF__
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ end
180
+ end
@@ -0,0 +1,111 @@
1
+ require 'logger'
2
+ require 'aws-sdk'
3
+
4
+ # Requires the aws-sdk gem, which must be initialized for this API to be capable of queuing requests.
5
+ #
6
+ # Synopsis:
7
+ #
8
+ # include SimpleJob
9
+ #
10
+ # JobQueue::DEFAULT.poll do |message|
11
+ # puts message
12
+ # end
13
+ #
14
+ # == Creating a queue implementation
15
+ #
16
+ # To create a new queue implementation, just extend the SimpleJob::JobQueue
17
+ # class, and call the register_job_queue declaration with an identifier. The
18
+ # class must implement the get_queue class method and the enqueue/poll instance
19
+ # methods to fulfill the interface. The default_queue method may be overridden
20
+ # to set a default queue.
21
+ #
22
+ # Example:
23
+ #
24
+ # class ArrayQueue < SimpleJob::JobQueue
25
+ # register_job_queue 'array', self
26
+ #
27
+ # include Singleton
28
+ # default self.instance
29
+ #
30
+ # def self.get_queue(type, options = {})
31
+ # instance
32
+ # end
33
+ # def enqueue(message, options = {})
34
+ # queue << message
35
+ # end
36
+ # def poll(options = {}, &block)
37
+ # options = {
38
+ # :interval => 1
39
+ # }.merge(options)
40
+ # loop do
41
+ # message = queue.shift
42
+ # yield(message) if message
43
+ # Kernel.sleep(options[:interval])
44
+ # end
45
+ # end
46
+ # private
47
+ # def queue
48
+ # @queue ||= []
49
+ # end
50
+ # end
51
+ #
52
+ # Then you can use the new queue implementation by passing its identifier to
53
+ # JobQueue.config:
54
+ #
55
+ # SimpleJob::JobQueue.config :implementation => 'array'
56
+ module SimpleJob
57
+ class JobQueue
58
+
59
+ def self.register_job_queue(identifier, klass)
60
+ @@registered_job_queues ||= {}
61
+ @@registered_job_queues[identifier.to_s] = klass
62
+ end
63
+
64
+ def self.config(options = {})
65
+ @config ||= {
66
+ :implementation => 'sqs',
67
+ :logger => default_logger,
68
+ }
69
+ @config.merge!(options) if options
70
+ @config
71
+ end
72
+
73
+ def self.[](type, options = {})
74
+ queue_class.get_queue(type, options)
75
+ end
76
+
77
+ def self.default_queue
78
+ raise "default queue not defined"
79
+ end
80
+
81
+ def self.default
82
+ queue_class.default_queue
83
+ end
84
+
85
+ def self.get_queue(type, options = {})
86
+ raise "queue with type #{type} not defined"
87
+ end
88
+
89
+ def self.queue_class
90
+ @@registered_job_queues[config[:implementation].to_s]
91
+ end
92
+
93
+ def enqueue(message, options = {})
94
+ raise NotImplementedError
95
+ end
96
+
97
+ def poll(options = {}, &block)
98
+ raise NotImplementedError
99
+ end
100
+
101
+ private
102
+
103
+ def self.default_logger
104
+ return Rails.logger if defined?(Rails)
105
+ logger = Logger.new(STDERR)
106
+ logger.level = Logger::INFO
107
+ logger
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,97 @@
1
+ module SimpleJob
2
+ class SQSJobQueue < JobQueue
3
+
4
+ # Registers this queue implementation with SimpleJob::JobQueue with identifier "sqs".
5
+ register_job_queue 'sqs', self
6
+
7
+ def self.config(options = {})
8
+ @config ||= {
9
+ :queue_prefix => ENV['SIMPLE_JOB_SQS_JOB_QUEUE_PREFIX'],
10
+ :default_visibility_timeout => 60,
11
+ :environment => (defined?(Rails) && Rails.env) || 'development',
12
+ }
13
+
14
+ @config.merge!(options) if options
15
+
16
+ raise 'must configure :queue_prefix using SQSJobQueue.config' if !@config[:queue_prefix]
17
+
18
+ @config
19
+ end
20
+
21
+ def self.default_queue
22
+ @default_queue || super
23
+ end
24
+
25
+ def self.define_queue(type, options = {})
26
+ type = type.to_s
27
+
28
+ options = {
29
+ :visibility_timeout => config[:default_visibility_timeout],
30
+ :default => false,
31
+ }.merge(options)
32
+
33
+ queue = self.new(type, options[:visibility_timeout])
34
+ self.queues ||= {}
35
+ self.queues[type] = queue
36
+
37
+ @default_queue = queue if options[:default]
38
+
39
+ queue
40
+ end
41
+
42
+ def self.get_queue(type, options = {})
43
+ type = type.to_s
44
+ (self.queues || {})[type] || super
45
+ end
46
+
47
+ def enqueue(message, options = {})
48
+ raise("enqueue expects a raw string") unless message.is_a?(String)
49
+ sqs_queue.send_message(message)
50
+ end
51
+
52
+ def poll(options = {}, &block)
53
+ options = {
54
+ :visibility_timeout => visibility_timeout,
55
+ :attributes => [ :sent_at, :receive_count, :first_received_at ],
56
+ :raise_exceptions => false,
57
+ }.merge(options)
58
+
59
+ message_handler = block || lambda do |definition, message|
60
+ definition.execute
61
+ end
62
+
63
+ sqs_queue.poll(options) do |message|
64
+ begin
65
+ raw_message = JSON.parse(message.body)
66
+ definition_class = JobDefinition.job_definition_class_for(raw_message['type'], raw_message['version'])
67
+ raise('no definition found') if !definition_class
68
+ definition = definition_class.new.from_json(message.body)
69
+ message_handler.call(definition, message)
70
+ rescue Exception => e
71
+ if options[:raise_exceptions]
72
+ raise e
73
+ else
74
+ JobQueue.config[:logger].error("unable to process message: #{e.message}")
75
+ JobQueue.config[:logger].error("message body: #{message.body}")
76
+ JobQueue.config[:logger].error(e.backtrace.join("\n "))
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ class << self
85
+ attr_accessor :queues
86
+ end
87
+
88
+ attr_accessor :sqs_queue, :visibility_timeout
89
+
90
+ def initialize(type, visibility_timeout)
91
+ sqs = ::AWS::SQS.new
92
+ self.sqs_queue = sqs.queues.create "#{self.class.config[:queue_prefix]}-#{type}-#{self.class.config[:environment]}"
93
+ self.visibility_timeout = visibility_timeout
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleJob
2
+ VERSION = '0.0.0'
3
+ end
data/lib/simple_job.rb ADDED
@@ -0,0 +1,7 @@
1
+ module SimpleJob
2
+
3
+ autoload :JobDefinition, 'simple_job/job_definition'
4
+ autoload :JobQueue, 'simple_job/job_queue'
5
+ autoload :SQSJobQueue, 'simple_job/sqs_job_queue'
6
+
7
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_job
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Dawson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-10 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activemodel
16
+ requirement: &70245578980360 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70245578980360
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70245578978120 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70245578978120
36
+ - !ruby/object:Gem::Dependency
37
+ name: aws-sdk
38
+ requirement: &70245578976320 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.2'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70245578976320
47
+ description: Contains libraries that support defining, queueing, and executing jobs.
48
+ email: daws23@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files:
52
+ - README.rdoc
53
+ - CHANGELOG.rdoc
54
+ - LICENSE.txt
55
+ files:
56
+ - lib/simple_job/job_definition.rb
57
+ - lib/simple_job/job_queue.rb
58
+ - lib/simple_job/sqs_job_queue.rb
59
+ - lib/simple_job/version.rb
60
+ - lib/simple_job.rb
61
+ - README.rdoc
62
+ - CHANGELOG.rdoc
63
+ - LICENSE.txt
64
+ homepage: https://github.com/daws/simple_job
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --main
69
+ - README.rdoc
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ segments:
79
+ - 0
80
+ hash: -2274642139086505235
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.10
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Support classes and modules for executable jobs.
93
+ test_files: []