job_reactor 0.5.0.beta2

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.
data/README.markdown ADDED
@@ -0,0 +1,88 @@
1
+ JobReactor
2
+ ==========
3
+
4
+ JobReactor is a library for creating and processing background jobs.
5
+ It is asynchronous client-server distributed system based on [EventMachine][0].
6
+
7
+ Quick start
8
+ ===========
9
+ Coming soon, see examples
10
+
11
+ Main features
12
+ =============
13
+ 1. Client-server architecture
14
+ -----------------------------
15
+ You can run as many distributors and working nodes as you need. You are free to choose the strategy.
16
+ If you have many background tasks from each part of your application you can use, for example, 3 distributors (one in each process) and 10 working nodes.
17
+ If you don't have many jobs you can leave only one node which will be connected to 3 distributors.
18
+ 2. High scalability
19
+ -------------------
20
+ Nodes and distributors are connected via TCP. So, you can run them on any machine you can connect to.
21
+ Nodes may use different storages or the same one. So, you can store vitally important jobs in relational database and
22
+ simple insignificant jobs in memory.
23
+ And more: your nodes may create jobs for others nodes and communicate with each other. See page [advance usage].
24
+ 3. Full job control
25
+ -------------------
26
+ You can add callback and errbacks to the job which will be called on the node.
27
+ You also can add 'success feedback' and 'error feedback' which will be called in your main application.
28
+ When job is done on remote node, your application will receive the result inside corespondent 'feedback'.
29
+ If error occur in the job you can see it in errbacks and do what you want.
30
+ Inside the job you can get information about when it starts, which node execute job and etc.
31
+ You also can add some arguments to the job on-the-fly which will be used in the subsequent callbacks and errbacks. See [advance usage].
32
+ 4. Reliability
33
+ --------------
34
+ You can run additional nodes and stop any nodes on-the-fly.
35
+ Distributor is smart enough to send jobs to another node if someone is stopped or crashed.
36
+ If no nodes are connected to distributor it will keep jobs in memory and send them when nodes start.
37
+ If node is stopped or crashed it will retry stored jobs after start.
38
+ 5. EventMachine available
39
+ -------------------------
40
+ Remember, your jobs will be run inside EventMachine reactor! You can easily use the power of async nature of EventMachine.
41
+ Use asynchronous [http requests], [websockets], [etc.], [etc.], and [etc]. See page [advance usage].
42
+ 6. Deferred and periodic jobs
43
+ -----------------------------
44
+ You can use deferred jobs which will run 'after' some time or 'run_at' given time.
45
+ You can create periodic jobs which will run every given time period and cancel them on condition.
46
+ 7. No polling
47
+ -------------
48
+ There is no storage polling. Absolutely. When node receives job (no matter instant, periodic or deferred) there will be EventMachine timer created
49
+ which will start job at the right time.
50
+ 8. Job retrying
51
+ --------------
52
+ If job fails it will be retried. You can choose global retrying strategy or manage separate jobs.
53
+ 9. Predefined nodes
54
+ -------------------
55
+ You can specify node for jobs, so they will be executed in that node environment. And you can specify which node is forbidden for the job.
56
+ If no nodes are specified distributor will try to send the job to the first free node.
57
+ 10. Node based priorities
58
+ -----------------------
59
+ There are no priorities like in Delayed::Job or Stalker. Bud there are flexible node-based priorities.
60
+ You can specify the node which should execute the job. You can reserve several nodes for high priority jobs.
61
+
62
+
63
+
64
+ The main parts of JobReactor are:
65
+ ---------------------------------
66
+ JobReactor module for creating jobs.
67
+ Distributor module for 'distributing' jobs between working nodes.
68
+ Node object for job processing.
69
+ #TODO
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+ How it works
79
+ ------------
80
+ #TODO
81
+
82
+
83
+
84
+
85
+
86
+ Links:
87
+ ------
88
+ [0]: http://rubyeventmachine.com/
@@ -0,0 +1,40 @@
1
+ # TODO comment it
2
+ module JobReactor
3
+ module Distributor
4
+ class Client < EM::Connection
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def name
10
+ @name
11
+ end
12
+
13
+ def lock
14
+ @lock = true
15
+ end
16
+
17
+ def unlock
18
+ @lock = false
19
+ end
20
+
21
+ def locked?
22
+ @lock
23
+ end
24
+
25
+ def available?
26
+ !locked?
27
+ end
28
+
29
+ def receive_data(data)
30
+ self.unlock if data == 'ok'
31
+ end
32
+
33
+ def unbind
34
+ JR::Logger.log "#{@name} disconnected"
35
+ close_connection
36
+ JobReactor::Distributor.connections.delete(self)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ # TODO comment it
2
+ module JobReactor
3
+ module Distributor
4
+ class Server < EM::Connection
5
+
6
+ def post_init
7
+ JR::Logger.log 'Begin node handshake'
8
+ end
9
+
10
+ def receive_data(data)
11
+ data = Marshal.load(data)
12
+ if data[:node_info]
13
+ node_info = data[:node_info]
14
+ JR::Logger.log "Receive data from node: #{data[:node_info]}"
15
+ JobReactor::Distributor.nodes << node_info
16
+ connection = EM.connect(*node_info[:server], Client, node_info[:name])
17
+ JobReactor::Distributor.connections << connection
18
+ elsif data[:success]
19
+ JR.run_succ_feedback(data[:success])
20
+ send_data('ok')
21
+ elsif data[:error]
22
+ JR.run_err_feedback(data[:error])
23
+ send_data('ok')
24
+ end
25
+
26
+ data
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,92 @@
1
+ require 'job_reactor/distributor/client'
2
+ require 'job_reactor/distributor/server'
3
+
4
+ module JobReactor
5
+ module Distributor
6
+ extend self
7
+
8
+ def host
9
+ @@host
10
+ end
11
+
12
+ def port
13
+ @@port
14
+ end
15
+
16
+ def nodes
17
+ @@nodes ||= []
18
+ end
19
+
20
+ # Contains connections pool
21
+ def connections
22
+ @@connections ||= []
23
+ end
24
+
25
+ #Starts distributor on given hast and port
26
+
27
+ def start(host, port)
28
+ @@host = host
29
+ @@port = port
30
+ EM.start_server(host, port, JobReactor::Distributor::Server, [host, port])
31
+ JR::Logger.log "Distributor listens #{host}:#{port}"
32
+ #EM.add_periodic_timer(5) do
33
+ # JR::Logger.log('Available nodes: ' << JR::Distributor.connections.map(&:name).join(' '))
34
+ #end
35
+ end
36
+
37
+ # Tries to find available node connection
38
+ # If it is distributor will send marshalled data
39
+ # If get_connection returns nil distributor will try again after 1 second
40
+ #
41
+ def send_data_to_node(hash)
42
+ connection = get_connection(hash)
43
+ if connection
44
+ data = Marshal.dump(hash)
45
+ connection.send_data(data)
46
+ connection.lock
47
+ else
48
+ EM.next_tick do
49
+ send_data_to_node(hash)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Looks for available connection.
57
+ # If job hash specified node, tries check if the node is available.
58
+ # If not, returns nil or tries to find any other free node if :always_use_specified_node == true
59
+ # If job hasn't any specified node, methods return any available connection or nil (and will be launched again in one second)
60
+
61
+ def get_connection(hash)
62
+ check_node_pool
63
+ if hash['node']
64
+ node_connection = connections.select{ |con| con.name == hash['node'] && con.name != hash['not_node']}.first
65
+ JR::Logger.log("WARNING: Node #{hash['node']} is not available") unless node_connection
66
+ if node_connection.try(:available?)
67
+ node_connection
68
+ else
69
+ JR.config[:always_use_specified_node] ? nil : connections.select{ |con| con.available? && con.name != hash['not_node'] }.first
70
+ end
71
+ else
72
+ connections.select{ |con| con.available? && con.name != hash['not_node'] }.first
73
+ end
74
+ end
75
+
76
+ # Checks node poll. If it is empty will fail after :when_node_pull_is_empty_will_raise_exception_after seconds
77
+ # The distributor will fail when number of timers raise to EM.get_max_timers which if default 100000 for the majority system
78
+ # To exit earlier may be useful for error detection
79
+ #
80
+ def check_node_pool
81
+ if connections.size == 0
82
+ JR::Logger.log 'Warning: Node pool is empty'
83
+ EM::Timer.new(JR.config[:when_node_pull_is_empty_will_raise_exception_after]) do
84
+ if connections.size == 0
85
+ raise JobReactor::NodePoolIsEmpty
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,31 @@
1
+ # Names are informative
2
+ # TODO
3
+ module JobReactor
4
+ def self.config
5
+ @@config ||= {}
6
+ end
7
+ end
8
+
9
+ JR = JobReactor
10
+
11
+ JR.config[:job_directory] = 'reactor_jobs'
12
+ JR.config[:max_attempt] = 10
13
+ JR.config[:retry_multiplier] = 5
14
+ JR.config[:retry_jobs_at_start] = true
15
+ JR.config[:merge_job_itself_to_args] = true
16
+ JR.config[:log_job_processing] = true
17
+ JR.config[:always_use_specified_node] = false #will send job to another node if specified node is not available
18
+ JR.config[:remove_done_jobs] = true
19
+ JR.config[:remove_cancelled_jobs] = true
20
+ JR.config[:when_node_pull_is_empty_will_raise_exception_after] = 3600
21
+
22
+ JR.config[:redis_host] = 'localhost'
23
+ JR.config[:redis_port] = 6379
24
+
25
+ #TODO next releases with rails support
26
+ #JR.config[:active_record_adapter] = 'mysql2'
27
+ #JR.config[:active_record_database] = 'em'
28
+ #JR.config[:active_record_user] = ''
29
+ #JR.config[:active_record_password] = ''
30
+ #JR.config[:active_record_table_name] = 'reactor_jobs'
31
+ #JR.config[:use_custom_active_record_connection] = true
@@ -0,0 +1,23 @@
1
+ module JobReactor
2
+
3
+ # The purpose of exceptions is in their names
4
+ # TODO
5
+
6
+ class NoJobsDefined < RuntimeError
7
+ end
8
+ class NoSuchJob < RuntimeError
9
+ end
10
+ class CancelJob < RuntimeError
11
+ end
12
+ class NodePoolIsEmpty < RuntimeError
13
+ end
14
+ class NoSuchNode < RuntimeError
15
+ end
16
+ class LostConnection < RuntimeError
17
+ end
18
+ class SchedulePeriodicJob < RuntimeError
19
+ end
20
+
21
+ end
22
+
23
+
@@ -0,0 +1,39 @@
1
+ # parse jobs defined in the JR.config[:job_directory]
2
+ # build hash of the following structure:
3
+ # {"job_1" => {
4
+ # job: Proc,
5
+ # callbacks: [
6
+ # ["first_callback", Proc],
7
+ # ["second_callback", Proc]
8
+ # ],
9
+ # errbacks: [
10
+ # ["first_errback", Proc],
11
+ # ["second_errback", Proc]
12
+ # ]
13
+ # },
14
+ # "job_2" => {
15
+ # job: Proc,
16
+ # callbacks: [[]],
17
+ # errbacks: [[]]
18
+ # }
19
+ # }
20
+ # Names of callbacks and errbacks are optional and may be used just for description
21
+
22
+ module JobReactor
23
+ extend self
24
+
25
+ def job(name, &block)
26
+ JR.jobs.merge!(name => { job: block })
27
+ end
28
+
29
+ def job_callback(name, callback_name = 'noname', &block)
30
+ JR.jobs[name].merge!(callbacks: []) unless JR.jobs[name][:callbacks]
31
+ JR.jobs[name][:callbacks] << [callback_name, block]
32
+ end
33
+
34
+ def job_errback(name, errback_name = 'noname', &block)
35
+ JR.jobs[name].merge!(errbacks: []) unless JR.jobs[name][:errbacks]
36
+ JR.jobs[name][:errbacks] << [errback_name, block]
37
+ end
38
+
39
+ end
@@ -0,0 +1,25 @@
1
+ # Storages implement simple functionality.
2
+ # There are four methods should be implemented:
3
+ # save(hash).
4
+ # load(hash).
5
+ # destroy(hash).
6
+ # jobs_for(name).
7
+ # The last one is used when node is restarting to retry saved jobs.
8
+ # The storage may not be thread safe, because each node manage it own jobs and don't now anything about others.
9
+
10
+ # Defines storages for lazy loading
11
+
12
+ # TODO 'NEXT RELEASE'
13
+ # require 'active_record'
14
+ # class JobReactor::ActiveRecordStorage < ::ActiveRecord::Base; end
15
+
16
+ module JobReactor::MemoryStorage; end
17
+ module JobReactor::RedisStorage; end
18
+
19
+ module JobReactor
20
+ STORAGES = {
21
+ #'active_record_storage' => JobReactor::ActiveRecordStorage,
22
+ 'memory_storage' => JobReactor::MemoryStorage,
23
+ 'redis_storage' => JobReactor::RedisStorage
24
+ }
25
+ end
@@ -0,0 +1,208 @@
1
+ # The core.
2
+ # Gives API to parse jobs, send them to node using distributor, and make them for node.
3
+
4
+ require 'job_reactor/job_reactor/config'
5
+ require 'job_reactor/job_reactor/job_parser'
6
+ require 'job_reactor/job_reactor/exceptions'
7
+ require 'job_reactor/job_reactor/storages'
8
+
9
+ module JobReactor
10
+
11
+ # Yes, we monkeypatched Ruby core class.
12
+ # Now all hashes hash EM::Deferrable callbacks and errbacks.
13
+ # It is just for simplicity.
14
+ # It's cool use 'job = {}' instead 'job = JobHash.new.
15
+ # We are ready to discuss it and change.
16
+ #
17
+ Hash.send(:include, EM::Deferrable)
18
+
19
+ class << self
20
+
21
+ # Accessors to jobs.
22
+ #
23
+ def jobs
24
+ @@jobs ||= { }
25
+ end
26
+
27
+ # Ready flag.
28
+ # @@ready is true when block is called inside EM reactor.
29
+ #
30
+ def ready!
31
+ @@ready = true
32
+ end
33
+
34
+ def ready?
35
+ (@@ready ||= false) && EM.reactor_running?
36
+ end
37
+
38
+ # Requires storage
39
+ # Creates and start node.
40
+ #
41
+ def start_node(opts)
42
+ parse_jobs
43
+ require_storage!(opts)
44
+ node = Node.new(opts)
45
+ node.start
46
+ end
47
+
48
+ def start_distributor(host, port)
49
+ JR::Distributor.start(host, port)
50
+ end
51
+
52
+ def succ_feedbacks
53
+ @@callbacks ||= { }
54
+ end
55
+
56
+ def err_feedbacks
57
+ @@errbacks ||= { }
58
+ end
59
+
60
+ # Here is the only method user can call inside the application (excepts start-up methods, of course).
61
+ # You have to specify job_name and optionally its args and opts.
62
+ # The method set initial arguments and send job to distributor which will send it to node.
63
+ # Options are :after and :period (for deferred and periodic jobs), and :node to specify the preferred node to launch job.
64
+ # Use :always_use_specified_node option to be sure that job will launched in the specified node.
65
+ # Job itself is a hash with the following keys:
66
+ # name, args, make_after, last_error, run_at, failed_at, attempt, period, node, not_node, status, distributor, on_success, on_error.
67
+ # TODO examples.
68
+ #
69
+ def enqueue(name, args = { }, opts = { }, success_proc = nil, error_proc = nil)
70
+ hash = { 'name' => name, 'args' => args, 'attempt' => 0, 'status' => 'new' }
71
+
72
+ hash.merge!('period' => opts[:period]) if opts[:period]
73
+ opts[:after] = (opts[:run_at] - Time.now) if opts[:run_at]
74
+ hash.merge!('make_after' => (opts[:after] || 0))
75
+
76
+ hash.merge!('node' => opts[:node]) if opts[:node]
77
+ hash.merge!('not_node' => opts[:not_node]) if opts[:not_node]
78
+
79
+ hash.merge!('distributor' => "#{JR::Distributor.host}:#{JR::Distributor.port}")
80
+
81
+ add_succ_feedbacks!(hash, success_proc) if success_proc
82
+ add_err_feedbacks!(hash, error_proc) if error_proc
83
+
84
+ JR::Distributor.send_data_to_node(hash)
85
+ end
86
+
87
+
88
+ # This method is used by node (Node#schedule).
89
+ # It makes job from hash by calling callback and errback methods.
90
+ #
91
+ # The strategy is the following:
92
+ # First and last callback (add_start_callback) are informational.
93
+ # Second is the proc specified in JR.job method.
94
+ # Third and ... are the procs specified in job_callbacks.
95
+ #
96
+ # Then errbacks are attached.
97
+ # They are called when error occurs in callbacks.
98
+ # The last errback raise exception again to return job back to node workflow.
99
+ # See Node#do_job method to better understand how this works.
100
+ #
101
+ def make(hash) #new job is a Hash
102
+ raise NoSuchJob unless jr_job = JR.jobs[hash['name']]
103
+
104
+ job = hash
105
+ add_start_callback(job) if JR.config[:log_job_processing]
106
+ job.callback(&jr_job[:job])
107
+
108
+ jr_job[:callbacks].each do |callback|
109
+ job.callback(&callback[1])
110
+ end if jr_job[:callbacks]
111
+
112
+ add_last_callback(job) if JR.config[:log_job_processing]
113
+
114
+ add_start_errback(job) if JR.config[:log_job_processing]
115
+
116
+ jr_job[:errbacks].each do |errback|
117
+ job.errback(&errback[1])
118
+ end if jr_job[:errbacks]
119
+
120
+ add_complete_errback(job) if JR.config[:log_job_processing]
121
+
122
+ job
123
+ end
124
+
125
+ # Runs success callbacks with job args
126
+ #
127
+ def run_succ_feedback(data)
128
+ proc = data[:do_not_delete] ? succ_feedbacks[data[:callback_id]] : succ_feedbacks.delete(data[:callback_id])
129
+ proc.call(data[:args]) if proc
130
+ end
131
+
132
+ # Runs error callbacks with job args
133
+ # Exception class is in args[:error]
134
+ #
135
+ def run_err_feedback(data)
136
+ proc = err_feedbacks.delete(data[:errback_id])
137
+ proc.call(data[:args]) if proc
138
+ end
139
+
140
+ private
141
+
142
+ # Requires storage and change opts[:storage] to the constant
143
+ #
144
+ def require_storage!(opts)
145
+ require "job_reactor/storages/#{opts[:storage]}"
146
+ opts[:storage] = STORAGES[opts[:storage]]
147
+ end
148
+
149
+ # Loads all *.rb files in the :job_directory folder
150
+ # See job_reactor/job_parser to understand how job hash is built
151
+ #
152
+ def parse_jobs
153
+ JR.config[:job_directory] += '/**/*.rb'
154
+ Dir[JR.config[:job_directory]].each {|file| load file }
155
+ end
156
+
157
+ # Adds success callback which will launch when node reports success
158
+ #
159
+ def add_succ_feedbacks!(hash, callback)
160
+ distributor = "#{JR::Distributor.host}:#{JR::Distributor.port}"
161
+ feedback_id = "#{distributor}_#{Time.now.utc.to_f}"
162
+ succ_feedbacks.merge!(feedback_id => callback)
163
+ hash.merge!('on_success' => feedback_id)
164
+ end
165
+
166
+ # Adds error callback which will launch when node reports error
167
+ #
168
+ def add_err_feedbacks!(hash, errback)
169
+ distributor = "#{JR::Distributor.host}:#{JR::Distributor.port}"
170
+ feedback_id = "#{distributor}_#{Time.now.utc.to_f}"
171
+ err_feedbacks.merge!(feedback_id => errback)
172
+ hash.merge!('on_error' => feedback_id)
173
+ end
174
+
175
+ # Logs the beginning.
176
+ #
177
+ def add_start_callback(job)
178
+ job.callback do
179
+ JR::Logger.log_event(:start, job)
180
+ end
181
+ end
182
+
183
+ # Logs the completing
184
+ #
185
+ def add_last_callback(job)
186
+ job.callback do
187
+ JR::Logger.log_event(:complete, job)
188
+ end
189
+ end
190
+
191
+ # Logs the beginning or error cycle.
192
+ #
193
+ def add_start_errback(job)
194
+ job.errback do
195
+ JR::Logger.log_event(:error, job)
196
+ end
197
+ end
198
+
199
+ # Logs the end of error cycle
200
+ #
201
+ def add_complete_errback(job)
202
+ job.errback do
203
+ JR::Logger.log_event(:error_complete, job)
204
+ end
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,57 @@
1
+ module JobReactor
2
+ module Logger
3
+ ################
4
+ # To set output stream
5
+ @@logger_method = :puts
6
+
7
+ def self.stdout=(value)
8
+ if value.is_a?(Symbol) && value == :rails_logger
9
+ @@stdout = Rails.logger
10
+ @@logger_method = :info
11
+ else
12
+ @@stdout = value
13
+ @@logger_method = :puts
14
+ end
15
+ end
16
+
17
+ def self.stdout
18
+ @@stdout ||= $stdout
19
+ end
20
+
21
+ #################
22
+ # Is checked in dev_log
23
+
24
+ @@development = false
25
+
26
+ def self.development=(value)
27
+ @@development = !!value
28
+ end
29
+
30
+ #################
31
+
32
+ # Log message to output stream
33
+ #
34
+ def self.log(msg)
35
+ stdout.public_send(@@logger_method, '-'*100)
36
+ stdout.public_send(@@logger_method, msg)
37
+ end
38
+
39
+ # Build string for job event and log it
40
+ #
41
+ def self.log_event(event, job)
42
+ log("Log: #{event} #{job['name']}")
43
+ end
44
+
45
+ # Log if JR::Logger.development is set to true
46
+ #
47
+ def self.dev_log(msg)
48
+ log(msg) if development?
49
+ end
50
+
51
+ # Is JR::Logger.development set to true?
52
+ #
53
+ def self.development?
54
+ @@development
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ module JobReactor
2
+ class Node
3
+ class Client < EM::Connection
4
+
5
+ def initialize(node, distributor)
6
+ @node = node
7
+ @distributor = distributor
8
+ end
9
+
10
+ def post_init
11
+ JR::Logger.log("Searching for distributor: #{@distributor.join(' ')} ...")
12
+ end
13
+
14
+ def lock
15
+ @lock = true
16
+ end
17
+
18
+ def unlock
19
+ @lock = false
20
+ end
21
+
22
+ def locked?
23
+ @lock
24
+ end
25
+
26
+ def available?
27
+ !locked?
28
+ end
29
+
30
+ def receive_data(data)
31
+ self.unlock if data == 'ok'
32
+ end
33
+
34
+ # Sends node credentials to distributor.
35
+ #
36
+ def connection_completed
37
+ JR::Logger.log('Begin distributor handshake')
38
+ data = {node_info: {name: @node.config[:name], server: @node.config[:server]} }
39
+ data = Marshal.dump(data)
40
+ send_data(data)
41
+ end
42
+
43
+ # Tries to connect.
44
+ #
45
+ def unbind
46
+ EM::Timer.new(1) do
47
+ @node.connect_to(@distributor)
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ module JobReactor
2
+ class Node
3
+ class Server < EM::Connection
4
+
5
+ #Need to know the storage to call save method on it
6
+ #Need to now node name to send it to the distributor
7
+ #
8
+ def initialize(node, storage)
9
+ @storage = storage
10
+ @node = node
11
+ end
12
+
13
+ #Ok, node is connected and ready to work
14
+ #
15
+ def post_init
16
+ JR::Logger.log("#{@node.name} ready to work")
17
+ end
18
+
19
+ # It is the place where job life cycle begins.
20
+ # This method:
21
+ # -receives data from distributor;
22
+ # -saves them in storage;
23
+ # -returns 'ok' to unlock node connection;
24
+ # -and schedules job;
25
+ #
26
+ def receive_data(data)
27
+ hash = Marshal.load(data)
28
+ JR::Logger.log("#{@node.name} received job: #{hash}")
29
+ hash.merge!('node' => @node.name)
30
+ @storage.save(hash) do |hash|
31
+ @node.schedule(hash)
32
+ end
33
+ send_data('ok')
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,222 @@
1
+ require 'job_reactor/node/server'
2
+ require 'job_reactor/node/client'
3
+ module JobReactor
4
+ class Node
5
+
6
+ def initialize(opts)
7
+ @config = { storage: opts[:storage], name: opts[:name], server: opts[:server], distributors: opts[:distributors]}
8
+ end
9
+
10
+ def config
11
+ @config
12
+ end
13
+
14
+ # Config accessors.
15
+ #
16
+ [:storage, :name, :server, :distributors].each do |method|
17
+ define_method(method) do
18
+ config[method]
19
+ end
20
+ end
21
+
22
+ # Store distributor connection instances.
23
+ #
24
+ def connections
25
+ @connections ||= {}
26
+ end
27
+
28
+ # Retrying jobs if any,
29
+ # starts server and tries to connect to distributors.
30
+ #
31
+ def start
32
+ retry_jobs if JR.config[:retry_jobs_at_start]
33
+ EM.start_server(*self.config[:server], Server, self, self.storage)
34
+ self.config[:distributors].each do |distributor|
35
+ connect_to(distributor)
36
+ end
37
+ end
38
+
39
+ # Connects to distributor.
40
+ # This method is public, because it is called by client when connection interrupt.
41
+ #
42
+ def connect_to(distributor)
43
+ if connections[distributor]
44
+ JR::Logger.log 'Searching for distributors ...'
45
+ connections[distributor].reconnect(*distributor)
46
+ else
47
+ connections.merge!(distributor => EM.connect(*distributor, Client, self, distributor))
48
+ end
49
+ end
50
+
51
+ # The method is called by node server.
52
+ # It makes a job and run do_job.
53
+ #
54
+ def schedule(hash)
55
+ EM::Timer.new(hash['make_after']) do #Of course, we can start job immediately (unless it is 'after' job), but we let EM take care about it. Maybe there is another job is ready to start
56
+ self.storage.load(hash) do |hash|
57
+ if job = JR.make(hash) #If we decide fail silently. See JR.make
58
+ do_job(job)
59
+ else
60
+ #TODO Do nothing or raise exception ????
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # Calls succeed on deferrable object.
69
+ # When job (or it's callbacks) fails, errbacks are launched.
70
+ # If errbacks fails job will be relaunched.
71
+ #
72
+ # You can see custom exception 'CancelJob''.
73
+ # You can use it to change normal execution.
74
+ #
75
+ def do_job(job)
76
+ job['run_at'] = Time.now
77
+ job['status'] = 'in progress'
78
+ storage.save(job) do |job|
79
+ begin
80
+ args = job['args'].merge(JR.config[:merge_job_itself_to_args] ? {:job_itself => job.dup} : {})
81
+ job.succeed(args)
82
+ job['args'] = args
83
+ job_completed(job)
84
+ rescue JobReactor::CancelJob
85
+ cancel_job(job)
86
+ rescue Exception => e
87
+ rescue_job(e, job)
88
+ end
89
+ end
90
+ end
91
+
92
+ # Reports success to distributor if should do it.
93
+ # If job is 'periodic' job schedule it again.
94
+ # Sets status completed or removes job from storage.
95
+ #
96
+ def job_completed(job)
97
+ report_success(job) if job['on_success']
98
+ if job['period'] && job['period'].to_i > 0
99
+ job['status'] = 'queued'
100
+ job['make_after'] = job['period']
101
+ job['args'].delete(:job_itself)
102
+ storage.save(job) { |job| schedule(job) }
103
+ else
104
+ if JR.config[:remove_done_jobs]
105
+ storage.destroy(job)
106
+ else
107
+ job['status'] = 'complete'
108
+ storage.save(job)
109
+ end
110
+ end
111
+ end
112
+
113
+ #Lanches job errbacks
114
+ #
115
+ def rescue_job(e, job)
116
+ begin
117
+ job['failed_at'] = Time.now #Save error info
118
+ job['last_error'] = e
119
+ job['status'] = 'error'
120
+ self.storage.save(job) do |job|
121
+ begin
122
+ args = job['args'].merge!(:error => e).merge(JR.config[:merge_job_itself_to_args] ? { :job_itself => job.dup } : { })
123
+ job.fail(args) #Fire errbacks. You can access error in you errbacks (args[:error])
124
+ job['args'] = args
125
+ complete_rescue(job)
126
+ rescue JobReactor::CancelJob
127
+ cancel_job(job) #If it was cancelled we destroy it or set status 'cancelled'
128
+ rescue Exception => e #Recsue Exceptions in errbacks
129
+ job['args'].merge!(:errback_error => e)
130
+ complete_rescue(job)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ #Tryes again or report error
137
+ #
138
+ def complete_rescue(job)
139
+ if job['attempt'].to_i < JobReactor.config[:max_attempt] - 1
140
+ try_again(job)
141
+ else
142
+ report_error(job) if job['on_error']
143
+ end
144
+ end
145
+
146
+ # Cancels job. Remove or set 'cancelled status'
147
+ #
148
+ def cancel_job(job)
149
+ report_error(job) if job['on_error']
150
+ if JR.config[:remove_cancelled_jobs]
151
+ storage.destroy(job)
152
+ else
153
+ job['status'] = 'cancelled'
154
+ storage.save(job)
155
+ end
156
+ end
157
+
158
+ # try_again has special condition for periodic jobs.
159
+ # They will be rescheduled after period time.
160
+ #
161
+ def try_again(job)
162
+ job['attempt'] += 1
163
+ if job['period'] && job['period'] > 0
164
+ job['make_after'] = job['period']
165
+ else
166
+ job['make_after'] = job['attempt'] * JobReactor.config[:retry_multiplier]
167
+ end
168
+ job['args'].delete(:job_itself)
169
+ self.storage.save(job) do |job|
170
+ self.schedule(job)
171
+ end
172
+ end
173
+
174
+ # Retries jobs.
175
+ # Runs only once when node starts.
176
+ #
177
+ def retry_jobs
178
+ storage.jobs_for(name) do |job_to_retry|
179
+ job_to_retry['args'].merge!(:retrying => true)
180
+ try_again(job_to_retry) if job_to_retry
181
+ end
182
+ end
183
+
184
+ # Reports success to node, sends jobs args
185
+ #
186
+ def report_success(job)
187
+ host, port = job['distributor'].split(':')
188
+ port = port.to_i
189
+ distributor = self.connections[[host, port]]
190
+ data = {:success => { callback_id: job['on_success'], args: job['args']}}
191
+ data[:success].merge!(do_not_delete: true) if job['period'] && job['period'].to_i > 0
192
+ data = Marshal.dump(data)
193
+ send_data_to_distributor(distributor, data)
194
+ end
195
+
196
+ # Reports error to node, sends jobs args.
197
+ # Exception class is merged to args.
198
+ #
199
+ def report_error(job)
200
+ host, port = job['distributor'].split(':')
201
+ port = port.to_i
202
+ distributor = self.connections[[host, port]]
203
+ data = {:error => { errback_id: job['on_error'], args: job['args']}}
204
+ data = Marshal.dump(data)
205
+ send_data_to_distributor(distributor, data)
206
+ end
207
+
208
+ # Sends data to distributor
209
+ #
210
+ def send_data_to_distributor(distributor, data)
211
+ if distributor.locked?
212
+ EM.next_tick do
213
+ send_data_to_distributor(distributor, data)
214
+ end
215
+ else
216
+ distributor.send_data(data)
217
+ distributor.lock
218
+ end
219
+ end
220
+
221
+ end
222
+ end
@@ -0,0 +1,39 @@
1
+ # TODO comment it
2
+ module JobReactor
3
+ module MemoryStorage
4
+
5
+ @@storage = { }
6
+
7
+ class << self
8
+ def storage
9
+ @@storage
10
+ end
11
+
12
+ def load(hash, &block)
13
+ hash = storage[hash['id']]
14
+ hash_copy = { }
15
+ hash.each { |k, v| hash_copy.merge!(k => v) }
16
+ block.call(hash_copy) if block_given?
17
+ end
18
+
19
+ def save(hash, &block)
20
+ unless (hash['id'])
21
+ id = Time.now.to_f.to_s
22
+ hash.merge!('id' => id)
23
+ end
24
+ storage.merge!(hash['id'] => hash)
25
+
26
+ block.call(hash) if block_given?
27
+ end
28
+
29
+ def destroy(hash)
30
+ storage.delete(hash['id'])
31
+ end
32
+
33
+ def jobs_for(name, &block) #No persistance
34
+ nil
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,76 @@
1
+ # TODO comment it
2
+ require 'em-redis'
3
+
4
+ module JobReactor
5
+ module RedisStorage
6
+ @@storage = EM::Protocols::Redis.connect(host: JobReactor.config[:redis_host], port: JobReactor.config[:redis_port])
7
+ ATTRS = %w(id name args last_error run_at failed_at attempt period make_after status distributor on_success on_error)
8
+
9
+ class << self
10
+
11
+ def storage
12
+ @@storage
13
+ end
14
+
15
+ def load(hash, &block)
16
+ key = "#{hash['node']}_#{hash['id']}"
17
+ hash_copy = {'node' => hash['node']} #need new object, because old one has been 'failed'
18
+
19
+ storage.hmget(key, *ATTRS) do |record|
20
+ ATTRS.each_with_index do |attr, i|
21
+ hash_copy[attr] = record[i]
22
+ end
23
+ ['attempt', 'period', 'make_after'].each do |attr|
24
+ hash_copy[attr] = hash_copy[attr].to_i
25
+ end
26
+ hash_copy['args'] = Marshal.load(hash_copy['args'])
27
+
28
+ block.call(hash_copy) if block_given?
29
+ end
30
+ end
31
+
32
+
33
+ def save(hash, &block)
34
+ hash.merge!('id' => Time.now.to_f.to_s) unless hash['id']
35
+ key = "#{hash['node']}_#{hash['id']}"
36
+ args, hash['args'] = hash['args'], Marshal.dump(hash['args'])
37
+
38
+ storage.hmset(key, *ATTRS.map{|attr| [attr, hash[attr]]}.flatten) do
39
+ hash['args'] = args
40
+
41
+ block.call(hash) if block_given?
42
+ end
43
+ end
44
+
45
+ def destroy(hash)
46
+ storage.del("#{hash['node']}_#{hash['id']}")
47
+ end
48
+
49
+ def destroy_all_jobs_for(name)
50
+ pattern = "*#{name}_*"
51
+ storage.del(*storage.keys(pattern))
52
+ end
53
+
54
+ def jobs_for(name, &block)
55
+ pattern = "*#{name}_*"
56
+ storage.keys(pattern) do |keys|
57
+ keys.each do |key|
58
+ hash = {}
59
+ storage.hget(key, 'id') do |id|
60
+ hash['id'] = id
61
+ hash['node'] = name
62
+ self.load(hash) do |hash|
63
+ if hash['status'] != 'complete' && hash['status'] != 'cancelled' && hash['attempt'].to_i < JobReactor.config[:max_attempt]
64
+ block.call(hash)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,44 @@
1
+ require 'eventmachine'
2
+ require 'job_reactor/job_reactor'
3
+ require 'job_reactor/logger'
4
+ require 'job_reactor/node'
5
+ require 'job_reactor/distributor'
6
+
7
+
8
+ # JobReactor initialization process.
9
+ # Parses jobs, runs EventMachine reactor and call given block inside reactor.
10
+ # The ::run method run EM in Thread to do not prevent execution of application.
11
+ # The ::wait_em_and_run is for using JobReactor with
12
+ # applications already have EventMachine inside and run it at start. Server Thin, for example.
13
+ # The run! method is for using JobReactor as standalone application. Advanced usage. For example you wand use node with distributor in one process
14
+ #
15
+ module JobReactor
16
+ extend self
17
+
18
+ def run(&block)
19
+ Thread.new do
20
+ if EM.reactor_running?
21
+ block.call if block_given?
22
+ JR.ready!
23
+ else
24
+ EM.run do
25
+ block.call if block_given?
26
+ JR.ready!
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def run!(&block)
33
+ if EM.reactor_running?
34
+ block.call if block_given?
35
+ JR.ready!
36
+ else
37
+ EM.run do
38
+ block.call if block_given?
39
+ JR.ready!
40
+ end
41
+ end
42
+ end
43
+
44
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: job_reactor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0.beta2
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Anton Mishchuk
9
+ - Andrey Rozhkovskiy
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-06-01 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ requirement: &83843190 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *83843190
26
+ - !ruby/object:Gem::Dependency
27
+ name: em-redis
28
+ requirement: &83842960 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *83842960
37
+ description: ! " JobReactor is a library for creating and processing background
38
+ jobs.\n It is client-server distributed system based on EventMachine.\n"
39
+ email: anton.mishchuk@gmial.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - lib/job_reactor.rb
45
+ - lib/job_reactor/job_reactor.rb
46
+ - lib/job_reactor/node.rb
47
+ - lib/job_reactor/storages/redis_storage.rb
48
+ - lib/job_reactor/storages/memory_storage.rb
49
+ - lib/job_reactor/distributor/client.rb
50
+ - lib/job_reactor/distributor/server.rb
51
+ - lib/job_reactor/node/client.rb
52
+ - lib/job_reactor/node/server.rb
53
+ - lib/job_reactor/job_reactor/job_parser.rb
54
+ - lib/job_reactor/job_reactor/storages.rb
55
+ - lib/job_reactor/job_reactor/exceptions.rb
56
+ - lib/job_reactor/job_reactor/config.rb
57
+ - lib/job_reactor/distributor.rb
58
+ - lib/job_reactor/logger.rb
59
+ - README.markdown
60
+ homepage: http://github.com/antonmi/job_reactor
61
+ licenses: []
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>'
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.1
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 1.8.6
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Simple, powerful and high scalable job queueing and background workers system
84
+ based on EventMachine
85
+ test_files: []