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