a13g 0.1.0.beta3 → 0.1.0.beta4
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/.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
|