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.
@@ -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
@@ -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