a13g 0.1.0.beta3 → 0.1.0.beta4
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/Rakefile +53 -0
- data/a13g-0.1.0.beta3.gem +0 -0
- data/a13g.gemspec +100 -0
- data/examples/consumer.rb +16 -0
- data/examples/multiple_connections.rb +26 -0
- data/examples/producer.rb +11 -0
- data/examples/simple_project/README +3 -0
- data/examples/simple_project/Rakefile +6 -0
- data/examples/simple_project/config/broker.yml +10 -0
- data/examples/simple_project/lib/consumers/first_consumer.rb +7 -0
- data/examples/simple_project/lib/consumers/second_consumer.rb +9 -0
- data/examples/simple_project/lib/simple_project.rb +20 -0
- data/lib/a13g.rb +68 -0
- data/lib/a13g/adapters.rb +45 -0
- data/lib/a13g/adapters/abstract_adapter.rb +330 -0
- data/lib/a13g/adapters/stomp_adapter.rb +163 -0
- data/lib/a13g/adapters/test_adapter.rb +102 -0
- data/lib/a13g/base.rb +448 -0
- data/lib/a13g/command.rb +69 -0
- data/lib/a13g/consumer.rb +129 -0
- data/lib/a13g/destination.rb +22 -0
- data/lib/a13g/errors.rb +60 -0
- data/lib/a13g/listener.rb +190 -0
- data/lib/a13g/message.rb +68 -0
- data/lib/a13g/producer.rb +107 -0
- data/lib/a13g/railtie.rb +4 -0
- data/lib/a13g/recipes.rb +31 -0
- data/lib/a13g/subscription.rb +123 -0
- data/lib/a13g/support/logger.rb +194 -0
- data/lib/a13g/support/utils.rb +25 -0
- data/lib/a13g/utils.rb +25 -0
- data/lib/a13g/version.rb +10 -0
- data/spec/a13g_spec.rb +74 -0
- data/spec/config/broker.yml +4 -0
- data/spec/dconfig/broker.yml +4 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- metadata +50 -4
data/lib/a13g/command.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'daemons'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module A13g
|
6
|
+
class Command
|
7
|
+
##
|
8
|
+
# Number of instances to create.
|
9
|
+
#
|
10
|
+
attr_accessor :worker_count
|
11
|
+
|
12
|
+
def initialize(args) # :nodoc:
|
13
|
+
@options = { :quiet => true, :pid_dir => "#{A13g::Base.path.root}/tmp/pids" }
|
14
|
+
@worker_count = 1
|
15
|
+
@monitor = false
|
16
|
+
@args = OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: script/messaging [options] start|stop|restart|run"
|
18
|
+
opts.on('-h', '--help', 'Show this message') { puts opts; exit 1 }
|
19
|
+
opts.on('-n', '--number_of_workers=workers', "Number of unique workers to spawn") {|worker_count| @worker_count = worker_count.to_i rescue 1 }
|
20
|
+
opts.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') {|dir| @options[:pid_dir] = dir }
|
21
|
+
opts.on('-i', '--identifier=n', 'A numeric identifier for the worker.') {|n| @options[:identifier] = n }
|
22
|
+
opts.on('-m', '--monitor', 'Start monitor process.') { @monitor = true }
|
23
|
+
end.parse!(args)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Runs A13g daemons.
|
28
|
+
#
|
29
|
+
def daemonize!
|
30
|
+
dir = @options[:pid_dir]
|
31
|
+
Dir.mkdir(dir) unless File.exists?(dir)
|
32
|
+
|
33
|
+
if @worker_count > 1 && @options[:identifier]
|
34
|
+
raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
|
35
|
+
elsif @worker_count == 1 && @options[:identifier]
|
36
|
+
process_name = "messaging.#{@options[:identifier]}"
|
37
|
+
run_process(process_name, dir)
|
38
|
+
else
|
39
|
+
worker_count.times do |worker_index|
|
40
|
+
process_name = worker_count == 1 ? "messaging" : "messaging.#{worker_index}"
|
41
|
+
run_process(process_name, dir)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
##
|
49
|
+
# Creates single daemonized process.
|
50
|
+
#
|
51
|
+
def run_process(process_name, dir)
|
52
|
+
Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
|
53
|
+
run process_name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Runs single a13g worker.
|
59
|
+
#
|
60
|
+
def run(worker_name = nil)
|
61
|
+
Dir.chdir(A13g::Base.path.root)
|
62
|
+
A13g::Listener.start!
|
63
|
+
#rescue => e
|
64
|
+
# Rails.logger.fatal e
|
65
|
+
# STDERR.puts e.message
|
66
|
+
# exit 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module A13g
|
2
|
+
# Message consumers are classes where received messages are processed. You
|
3
|
+
# can define there how message should be served.
|
4
|
+
#
|
5
|
+
# === Subscribing and processing messages
|
6
|
+
#
|
7
|
+
# Each consumer can subscribe only one destination (queue or topic). Because
|
8
|
+
# of security reasons it's not possible to listen many destinations by single
|
9
|
+
# consumer.
|
10
|
+
#
|
11
|
+
# class MyConsumer < A13g::Consumer
|
12
|
+
# subscribe "/queue/MyQueue", :ack => :client
|
13
|
+
#
|
14
|
+
# def on_message(message)
|
15
|
+
# if message.body == 'Abort me!'
|
16
|
+
# abort!
|
17
|
+
# else
|
18
|
+
# print message.body
|
19
|
+
# ack! message
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# === Handling errors
|
25
|
+
#
|
26
|
+
# There is also opportunity to define own error handler. Usually it's defined
|
27
|
+
# in ApplicationConsumer.
|
28
|
+
#
|
29
|
+
# class ApplicationConsumer < A13g::Consumer
|
30
|
+
# def on_error(err)
|
31
|
+
# raise err
|
32
|
+
# logger.error err
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# === Multiple connections
|
37
|
+
#
|
38
|
+
# You can also select connection context in your consumer. Thanks to it
|
39
|
+
# each consumer can work with different server.
|
40
|
+
#
|
41
|
+
# A13g.setup(:first, :adapter => 'stomp', :host => 'first.host.com')
|
42
|
+
# A13g.setup(:second, :adapter => 'stomp', :host => 'second.host.com')
|
43
|
+
#
|
44
|
+
# class FirstConsumer < A13g::Consumer
|
45
|
+
# context :first
|
46
|
+
# # ...
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# class SecondConsumer < A13g::Consumer
|
50
|
+
# context :second
|
51
|
+
# # ...
|
52
|
+
# end
|
53
|
+
class Consumer < Base
|
54
|
+
include Singleton
|
55
|
+
include Producer
|
56
|
+
|
57
|
+
# This destination will be subscribed by consumer
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
cattr_reader :subscription
|
61
|
+
@@subscription = nil
|
62
|
+
|
63
|
+
class << self
|
64
|
+
# Assigns specified destination to consumer. Destination can be assigned
|
65
|
+
# to consumer only once.
|
66
|
+
#
|
67
|
+
# @param [String] destination
|
68
|
+
# destination name
|
69
|
+
# @param [Hash] headers
|
70
|
+
# subscription headers
|
71
|
+
# @param [Array] args
|
72
|
+
# additional arguments
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def subscribe(destination, headers={}, *args)
|
76
|
+
unless @@subscription
|
77
|
+
@@subscription = Subscription.create(instance, destination, headers, connection, *args)
|
78
|
+
else
|
79
|
+
Subscription.all.delete(self.name)
|
80
|
+
@@subscription = nil
|
81
|
+
subscribe(destination, headers, *args)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# This action should be implemented in your own processor. It will be
|
87
|
+
# executed when subsriber will receive message.
|
88
|
+
#
|
89
|
+
# @param [A13g::Message] message
|
90
|
+
# received message
|
91
|
+
#
|
92
|
+
# @api public
|
93
|
+
def on_message(message)
|
94
|
+
raise NotImplementedError, "Implement the `on_message` method in your own consumer class"
|
95
|
+
end
|
96
|
+
|
97
|
+
# This action should be implemented in your own processor. It will be
|
98
|
+
# executed when some exception will be caught while receiving message.
|
99
|
+
#
|
100
|
+
# @param [Exception, StandardError] err
|
101
|
+
# catched error
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
def on_error(err)
|
105
|
+
raise err
|
106
|
+
end
|
107
|
+
|
108
|
+
# Processing message to `on_message` or `on_error`.
|
109
|
+
#
|
110
|
+
# @param [A13g::Message] message
|
111
|
+
# received message
|
112
|
+
#
|
113
|
+
# @api public
|
114
|
+
def process!(message)
|
115
|
+
logger.info("#{self.class.name} is processing message #{message.id}")
|
116
|
+
on_message(message)
|
117
|
+
rescue Object => err
|
118
|
+
begin
|
119
|
+
on_error(err)
|
120
|
+
rescue AbortMessage => ex
|
121
|
+
raise ex
|
122
|
+
rescue IgnoreMessage => ex
|
123
|
+
raise ex
|
124
|
+
rescue Object => ex
|
125
|
+
logger.error("#{self.class.name}: error in `on_error`, will propagate no further: #{ex.message}")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module A13g
|
2
|
+
# It's recommended having a destination object to represent each subscribed
|
3
|
+
# destination.
|
4
|
+
class Destination
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
# Constructor.
|
8
|
+
#
|
9
|
+
# @param [String] name
|
10
|
+
# name of destination
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
def initialize(name)
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s # :nodoc:
|
18
|
+
"<A13g::Destination @name='#{name}'>"
|
19
|
+
end
|
20
|
+
alias :inspect :to_s
|
21
|
+
end
|
22
|
+
end
|
data/lib/a13g/errors.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module A13g
|
2
|
+
# Generic a13g exception class.
|
3
|
+
MessagingError = Class.new(StandardError)
|
4
|
+
|
5
|
+
# Raised when default connection context is not specified.
|
6
|
+
MissingDefaultContext = Class.new(MessagingError)
|
7
|
+
|
8
|
+
# Raised when connection to the broker could not been established
|
9
|
+
# (for example when <tt>connection=</tt> is given a nil object).
|
10
|
+
ConnectionNotEstablished = Class.new(MessagingError)
|
11
|
+
|
12
|
+
# Raised when destination for message is not specified.
|
13
|
+
NoDestinationError = Class.new(MessagingError)
|
14
|
+
|
15
|
+
# Error raised when invalid adapter were specified in configuration.
|
16
|
+
AdapterNotSpecified = Class.new(ArgumentError)
|
17
|
+
|
18
|
+
# Error raised when adapter wasn't found.
|
19
|
+
AdapterNotFound = Class.new(LoadError)
|
20
|
+
|
21
|
+
# Error raised when configuration file wasn't found.
|
22
|
+
ConfigurationFileNotFound = Class.new(MessagingError)
|
23
|
+
|
24
|
+
# Used to indicate that the processing for a thread shoud complete.
|
25
|
+
StopProcessing = Class.new(Interrupt)
|
26
|
+
|
27
|
+
# Used to indicate that the processing on a message should cease,
|
28
|
+
# and the message should be returned back to the broker as best it can be.
|
29
|
+
AbortMessage = Class.new(MessagingError)
|
30
|
+
|
31
|
+
# Used when message should be ignored by dispatcher.
|
32
|
+
IgnoreMessage = Class.new(MessagingError)
|
33
|
+
|
34
|
+
# Raised when destination is defined in gateway more than once.
|
35
|
+
DestinationAlreadyDefined = Class.new(MessagingError)
|
36
|
+
|
37
|
+
# Raised when destination specified for sending message wasn't subscribed.
|
38
|
+
DestinationNotDefinedError = Class.new(ArgumentError)
|
39
|
+
|
40
|
+
# Raised when destination is subscribed by more than one consumer.
|
41
|
+
class DestinationAlreadySubscribedError < MessagingError
|
42
|
+
def message
|
43
|
+
"More than one consumer can't subcribe the same destination"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Error raised when consumer has defined more than one subscription.
|
48
|
+
class TooManySubscriptionsError < MessagingError
|
49
|
+
def message
|
50
|
+
"Each consumer can subscribe only one destination"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Error raised when consumer has assigned more than one context.
|
55
|
+
class ContextAlreadyDefinedError < MessagingError
|
56
|
+
def message
|
57
|
+
"Each consumer can use only one connection context"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module A13g
|
2
|
+
# Listener creates receiving loops for active connections. Receiving loops are
|
3
|
+
# running in separated threads and dispatches all received messages to
|
4
|
+
# related consumer.
|
5
|
+
class Listener
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# @see A13g::Listener#start!
|
10
|
+
# @api public
|
11
|
+
def start!
|
12
|
+
instance.start!
|
13
|
+
end
|
14
|
+
|
15
|
+
# @see @see A13g::Listener#stop!
|
16
|
+
# @api public
|
17
|
+
def stop!
|
18
|
+
instance.stop!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :logger
|
23
|
+
|
24
|
+
# @api public
|
25
|
+
def initialize
|
26
|
+
@logger = Base.logger
|
27
|
+
@dispatcher_mutex = Mutex.new
|
28
|
+
@connection_threads = {}
|
29
|
+
@running = true
|
30
|
+
@exiting = false
|
31
|
+
end
|
32
|
+
|
33
|
+
# Starts message listerers with receiving loop for each active connection.
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def start!
|
37
|
+
logger.debug "Initializing subscriptions..."
|
38
|
+
Thread.new do
|
39
|
+
Base.create_subscriptions
|
40
|
+
begin
|
41
|
+
Base.contexts.values.each do |context|
|
42
|
+
@connection_threads[context] = Thread.new do
|
43
|
+
receiving_loop(context.connection) if context.connection
|
44
|
+
end
|
45
|
+
end
|
46
|
+
prevent_clean_exit
|
47
|
+
rescue Interrupt
|
48
|
+
logger.error "\n<<Interrupt received>>"
|
49
|
+
rescue Object => ex
|
50
|
+
logger.error "#{ex.class.name}: #{ex.message}\n\t#{ex.backtrace.join("\n\t")}"
|
51
|
+
raise ex
|
52
|
+
ensure
|
53
|
+
logger.debug "Cleaning up..."
|
54
|
+
stop!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# It cleaning up all active subscriptions and dispatching all received but
|
60
|
+
# not processed messages. It's also closing all active connections with brokers.
|
61
|
+
#
|
62
|
+
# @api public
|
63
|
+
def stop!
|
64
|
+
@running = false
|
65
|
+
dispatching = true
|
66
|
+
while dispatching
|
67
|
+
dispatching = false
|
68
|
+
@connection_threads.each do |name, thread|
|
69
|
+
if thread[:message] && thread[:connection]
|
70
|
+
dispatching = true
|
71
|
+
message = thread[:message]
|
72
|
+
connection = thread[:connection]
|
73
|
+
if thread.alive?
|
74
|
+
connection.logger.error "Waiting on thread `#{name}` to finish processing last message..."
|
75
|
+
else
|
76
|
+
connection.logger.error "Starting thread `#{name}` to finish processing last message..."
|
77
|
+
thread.exit
|
78
|
+
thread = Thread.start do
|
79
|
+
begin
|
80
|
+
Thread.current[:connection] = connection
|
81
|
+
Thread.current[:message] = message
|
82
|
+
dispatch(message, connection)
|
83
|
+
ensure
|
84
|
+
Thread.current[:message] = nil
|
85
|
+
Thread.current[:connection] = nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
else
|
90
|
+
thread.raise StopProcessing if thread.alive?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
Base.teardown
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Protection for "clean" exit. This method makes sure, that all threads
|
100
|
+
# have been completed.
|
101
|
+
#
|
102
|
+
# @api private
|
103
|
+
def prevent_clean_exit
|
104
|
+
trap('TERM') { raise Interrupt }
|
105
|
+
while @running
|
106
|
+
living = false
|
107
|
+
@connection_threads.each {|name, thread| living ||= thread.alive? }
|
108
|
+
@running = living
|
109
|
+
return logger.error("All threads have died...") unless @running
|
110
|
+
sleep 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Receives messages from specified connection in loop, and it's sending
|
115
|
+
# redirects received messages to dispatcher.
|
116
|
+
#
|
117
|
+
# @param [A13g::Adapters::AbstracAdapter] connection
|
118
|
+
# connection object
|
119
|
+
#
|
120
|
+
# @api private
|
121
|
+
def receiving_loop(connection)
|
122
|
+
Thread.current[:connection] = connection
|
123
|
+
while @running
|
124
|
+
begin
|
125
|
+
Thread.current[:message] = nil
|
126
|
+
Thread.current[:message] = message = connection.receive
|
127
|
+
rescue StopProcessing
|
128
|
+
connection.logger.error "Processing Stopped in thread[#{name}]: receive interrupted, will process last message if already received"
|
129
|
+
rescue Object => ex
|
130
|
+
connection.logger.error "Exception from connection.receive in thread[#{name}]: #{ex.message}\n#{ex.backtrace.join("\n\t")}"
|
131
|
+
ensure
|
132
|
+
if message = Thread.current[:message]
|
133
|
+
Thread.current[:message] = nil
|
134
|
+
dispatch(message, connection)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
Thread.pass
|
138
|
+
end
|
139
|
+
connection.logger.error "Receive loop terminated in thread[#{name}]"
|
140
|
+
end
|
141
|
+
|
142
|
+
# It processes message in synchronized thread. If received message come
|
143
|
+
# from queue/topic subscribed by one of consumers, then will be dispatched
|
144
|
+
# by proper one, otherwise message will be signed as unreceived.
|
145
|
+
#
|
146
|
+
# @param [A13g::Message] message
|
147
|
+
# received message
|
148
|
+
# @param [A13g::Adapters::AbstracAdapter] connection
|
149
|
+
# connection object
|
150
|
+
#
|
151
|
+
# @api private
|
152
|
+
def dispatch(message, connection)
|
153
|
+
@dispatcher_mutex.synchronize do
|
154
|
+
begin
|
155
|
+
case message.command
|
156
|
+
when 'ERROR'
|
157
|
+
connection.logger.error("<< (#{connection.url}#{message.destination.name}): ERROR #{message.headers['message']}")
|
158
|
+
when 'MESSAGE'
|
159
|
+
connection.logger.info("<< "+Utils.format_message("#{connection.url}#{message.destination.name}", message.body, message.headers))
|
160
|
+
abort, ignore, processed = false, false, false
|
161
|
+
if subscription = Subscription.find(message.destination.name, connection)
|
162
|
+
begin
|
163
|
+
processed = true
|
164
|
+
return subscription.consumer.process!(message)
|
165
|
+
rescue AbortMessage
|
166
|
+
abort = true
|
167
|
+
connection.logger.info(">> (#{connection.url}#{message.destination.name}): ABORT #{message.id}")
|
168
|
+
return connection.unreceive(message, subscription.headers)
|
169
|
+
rescue IgnoreMessage
|
170
|
+
ignore = true
|
171
|
+
connection.logger.info(">> (#{connection.url}#{message.destination.name}): IGNORE #{message.id}")
|
172
|
+
return
|
173
|
+
ensure
|
174
|
+
if !abort && !ignore && subscription.headers[:ack] == 'auto'
|
175
|
+
message.ack!
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
else
|
180
|
+
connection.logger.error("<< (#{connection.url}#{message.destination.name}): Unknown message command: #{message.command}")
|
181
|
+
end
|
182
|
+
rescue Object => ex
|
183
|
+
connection.logger.error "Dispatch exception: #{ex}"
|
184
|
+
connection.logger.error ex.backtrace.join("\n\t")
|
185
|
+
raise ex
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|