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,163 @@
1
+ module A13g
2
+ module Adapters
3
+ class StompAdapter < AbstractAdapter
4
+ def initialize(config, logger)
5
+ unless defined? Stomp
6
+ begin
7
+ require_library_or_gem('stomp')
8
+ rescue LoadError
9
+ $stderr.puts '!!! Please install the stomp gem and try again: gem install stomp.'
10
+ raise
11
+ end
12
+ end
13
+
14
+ config.symbolize_keys
15
+
16
+ config = {
17
+ :login => '',
18
+ :passcode => '',
19
+ :host => 'localhost',
20
+ :port => config[:ssl] ? 61612 : 61613,
21
+ :reliable => true,
22
+ :dead_letter_queue => '/queue/DLQ',
23
+ :client_id => nil,
24
+ :max_retry => 0,
25
+ :max_reconnect_delay => 30.0,
26
+ :timeout => -1,
27
+ :ssl => false
28
+ }.merge(config)
29
+
30
+ super(config, logger)
31
+
32
+ @connection_options = {
33
+ :hosts => [
34
+ {
35
+ :login => @config[:login],
36
+ :passcode => @config[:passcode],
37
+ :host => @config[:host],
38
+ :port => @config[:port],
39
+ :ssl => @config[:ssl],
40
+ :reliable => @config[:reliable],
41
+ },],
42
+ :initial_reconnect_delay => 0.01,
43
+ :max_reconnect_delay => @config[:max_reconnect_delay],
44
+ :use_exponential_back_off => true,
45
+ :back_off_multiplier => 2,
46
+ :max_reconnect_attempts => @config[:max_retry],
47
+ :randomize => false,
48
+ :backup => false,
49
+ :timeout => @config[:timeout]
50
+ }
51
+
52
+ connect
53
+ end
54
+
55
+ def adapter_name
56
+ 'Stomp'.freeze
57
+ end
58
+
59
+ def url
60
+ "stomp://#{@config[:host]}:#{@config[:port]}" if @config
61
+ end
62
+
63
+ def active?
64
+ if @connection.respond_to?(:open?)
65
+ @connection.open?
66
+ else
67
+ false
68
+ end
69
+ end
70
+
71
+ def reconnect!
72
+ disconnect!
73
+ connect
74
+ end
75
+
76
+ def disconnect!(headers={})
77
+ if active? && @connection.respond_to?(:disconnect)
78
+ @connection.disconnect(headers)
79
+ end
80
+ end
81
+
82
+ def ack(message, headers={})
83
+ if message
84
+ ack_headers = message.headers.has_key?(:transaction) ? message.headers[:transaction] : {}
85
+ headers = ack_headers.merge(headers)
86
+ result = raw_connection.ack(message.headers['message-id'], headers)
87
+ run_callbacks(:after_ack, [message, headers])
88
+ result
89
+ end
90
+ end
91
+
92
+ def begin(name, headers={})
93
+ result = raw_connection.begin(name, headers)
94
+ run_callbacks(:after_begin_transaction, [name, headers])
95
+ result
96
+ end
97
+
98
+ def commit(name, headers={})
99
+ result = raw_connection.commit(name, headers)
100
+ run_callbacks(:after_commit_transaction, [name, headers])
101
+ result
102
+ end
103
+
104
+ def abort(name, headers={})
105
+ result = raw_connection.abort(name, headers)
106
+ run_callbacks(:after_abort_transaction, [name, headers])
107
+ result
108
+ end
109
+
110
+ def subscribe(destination, headers={}, sub_id=nil)
111
+ result = raw_connection.subscribe(destination, headers, sub_id)
112
+ run_callbacks(:after_subscribe, [destination, headers, sub_id])
113
+ result
114
+ end
115
+
116
+ def unsubscribe(destination, headers={}, sub_id=nil)
117
+ result = raw_connection.unsubscribe(destination, headers, sub_id=nil)
118
+ run_callbacks(:after_unsubscribe, [destination, headers, sub_id])
119
+ result
120
+ end
121
+
122
+ def publish(destination, message, headers={})
123
+ headers = { :persistent => true }.merge(headers)
124
+ result = raw_connection.publish(destination, message, headers)
125
+ run_callbacks(:after_publish, [destination, message, headers])
126
+ result
127
+ end
128
+
129
+ def receive
130
+ raw_message = raw_connection.receive
131
+ message = Message.new(raw_message, raw_message.headers['message-id'],
132
+ raw_message.headers, raw_message.body, raw_message.command, self)
133
+ run_callbacks(:after_receive, [message])
134
+ message
135
+ end
136
+
137
+ def unreceive(message, headers={})
138
+ defaults = { :dead_letter_queue => @config[:dead_letter_queue] }
139
+ headers = headers.merge(defaults)
140
+ result = raw_connection.unreceive(message, headers)
141
+ run_callbacks(:after_receive, [message, headers])
142
+ result
143
+ end
144
+
145
+ def client_ack?(message)
146
+ raw_connection.client_ack?(message)
147
+ end
148
+
149
+ protected
150
+
151
+ def connect
152
+ if @connection = Stomp::Connection.new(@connection_options)
153
+ logger.debug "Stomp connection with broker at #{url} has been established"
154
+ else
155
+ verify!
156
+ end
157
+ end
158
+ end
159
+
160
+ register(:stomp, StompAdapter)
161
+ end
162
+ end
163
+
@@ -0,0 +1,102 @@
1
+ module A13g
2
+ module Adapters
3
+ class TestAdapter < AbstractAdapter
4
+
5
+ def initialize(config, logger)
6
+ super(config, logger)
7
+ @receiving_queue = []
8
+ connect
9
+ end
10
+
11
+ def adapter_name
12
+ 'Test'.freeze
13
+ end
14
+
15
+ def url
16
+ ''
17
+ end
18
+
19
+ def active?
20
+ @active ||= false
21
+ end
22
+
23
+ def reconnect!
24
+ disconnect!
25
+ connect
26
+ end
27
+
28
+ def disconnect!(headers={})
29
+ @active = false if active?
30
+ end
31
+
32
+ def ack(message, headers={})
33
+ if message
34
+ run_callbacks(:after_ack, [message, headers])
35
+ true
36
+ end
37
+ end
38
+
39
+ def begin(name, headers={})
40
+ run_callbacks(:after_begin_transaction, [name, headers])
41
+ true
42
+ end
43
+
44
+ def commit(name, headers={})
45
+ run_callbacks(:after_commit_transaction, [name, headers])
46
+ true
47
+ end
48
+
49
+ def abort(name, headers={})
50
+ run_callbacks(:after_abort_transaction, [name, headers])
51
+ true
52
+ end
53
+
54
+ def subscribe(destination, headers={}, sub_id=nil)
55
+ run_callbacks(:after_subscribe, [destination, headers, sub_id])
56
+ true
57
+ end
58
+
59
+ def unsubscribe(destination, headers={}, sub_id=nil)
60
+ run_callbacks(:after_unsubscribe, [destination, headers, sub_id])
61
+ true
62
+ end
63
+
64
+ def publish(destination, message, headers={})
65
+ headers = {
66
+ 'message-id' => 'TEST',
67
+ 'destination' => destination.to_s
68
+ }.merge(headers)
69
+ msg = OpenStruct.new(
70
+ :body => message,
71
+ :headers => headers,
72
+ :command => 'MESSAGE'
73
+ )
74
+ @receiving_queue << Message.new(msg, msg.headers['message-id'],
75
+ msg.headers, message, msg.command, self)
76
+ run_callbacks(:after_publish, [destination, message, headers])
77
+ true
78
+ end
79
+
80
+ def receive
81
+ if message = @receiving_queue.shift
82
+ run_callbacks(:after_receive, [message])
83
+ message
84
+ end
85
+ end
86
+
87
+ def unreceive(message, headers={})
88
+ run_callbacks(:after_receive, [message, headers])
89
+ end
90
+
91
+ protected
92
+
93
+ def connect
94
+ @active = true
95
+ logger.debug "Test connection has been established"
96
+ end
97
+ end
98
+
99
+ register(:test, TestAdapter)
100
+ end
101
+ end
102
+
@@ -0,0 +1,448 @@
1
+ require 'ostruct'
2
+
3
+ module A13g
4
+ # It describes connection during setup.
5
+ class ConnectionSpecification
6
+ attr_reader :config, :adapter
7
+ def initialize(config, adapter)
8
+ @config, @adapter = config, adapter
9
+ end
10
+ end
11
+
12
+ # It keeps context informations about connection and related consumers.
13
+ class ConnectionContext
14
+ attr_reader :name, :related_classes, :connection
15
+
16
+ # Constructor.
17
+ #
18
+ # @param [Symbol] name
19
+ # context name
20
+ # @param [A13g::Adapters::AbstractAdapter] connection
21
+ # related connection
22
+ # @param [Array] related classess
23
+ # list of consumers woring in this context
24
+ #
25
+ # @api public
26
+ def initialize(name, connection, related_classes=[])
27
+ @name, @connection, @related_classes = name, connection, related_classes
28
+ @related_classes << Base if name == :default
29
+ end
30
+
31
+ def to_s # :nodoc:
32
+ name
33
+ end
34
+ end
35
+
36
+ class Base
37
+ # Returns broker configuration for current connection and environment.
38
+ #
39
+ # For example, the following broker.yml...
40
+ #
41
+ # development:
42
+ # adapter: stomp
43
+ # host: localhost
44
+ # port: 61613
45
+ #
46
+ # ...would result in A13g::Base#config on development environment
47
+ # to look like this:
48
+ #
49
+ # {
50
+ # 'development' => {
51
+ # 'adapter' => 'stomp'
52
+ # 'host' => 'localhost'
53
+ # 'port' => 61613
54
+ # }
55
+ # }
56
+ #
57
+ # @api public
58
+ cattr_reader :configurations
59
+ @@configurations = HashWithIndifferentAccess.new
60
+
61
+ # @api public
62
+ cattr_accessor :logger, :instance_writer => false
63
+ @@logger = nil
64
+
65
+ # @api public
66
+ cattr_accessor :environment, :instance_writer => false
67
+ @@environment = nil
68
+
69
+ # @api public
70
+ cattr_reader :ready
71
+ @@ready = false
72
+
73
+ # Keeps list of connection contexts.
74
+ #
75
+ # @api public
76
+ cattr_reader :contexts
77
+ @@contexts = HashWithIndifferentAccess.new
78
+
79
+ # List of contexts related with consumers.
80
+ #
81
+ # @api public
82
+ cattr_reader :contextuals
83
+ @@contextuals = {}
84
+
85
+ # @api public
86
+ cattr_reader :path
87
+ @@path = nil
88
+
89
+ # Returns the connection currently associated with the class.
90
+ #
91
+ # @return [A13g::Adapters::AbstractAdapter]
92
+ # currenct connection
93
+ #
94
+ # @api public
95
+ def connection
96
+ self.class.connection
97
+ end
98
+
99
+ class << self
100
+ # Returns all pathes to important directories and files.
101
+ #
102
+ # You can define following pathes when your project structure is different
103
+ # than standard Ruby on Rails / Merb / Padrino application.
104
+ #
105
+ # A13g::Base.path.app # => path to application
106
+ # A13g::Base.path.config # => path to broker configuration file
107
+ # A13g::Base.path.consumers # => path to consumers directory
108
+ #
109
+ # There is also shortcut: A13g#path.
110
+ #
111
+ # @see A13g#path
112
+ #
113
+ # @return [Struct(:root, :log, :config, :consumers)]
114
+ # important directories pathes
115
+ #
116
+ # @api public
117
+ def path
118
+ unless @@path
119
+ @@path = OpenStruct.new
120
+ @@path.root ||= APP_ROOT if defined?(APP_ROOT)
121
+ @@path.root ||= RACK_ROOT if defined?(RACK_ROOT)
122
+ @@path.root ||= RAILS_ROOT if defined?(RAILS_ROOT)
123
+ @@path.root ||= Rails.root if defined?(Rails)
124
+ @@path.root ||= "./"
125
+ @@path.config ||= File.join(@@path.root.to_s, 'config')
126
+ @@path.consumers ||= File.join(@@path.root.to_s, 'app', 'consumers')
127
+ end
128
+ @@path
129
+ end
130
+
131
+ # Current environment.
132
+ #
133
+ # @return [String]
134
+ # env name
135
+ #
136
+ # @api public
137
+ def environment
138
+ unless @environment
139
+ @@environment ||= APP_ENV.to_s if defined?(APP_ENV)
140
+ @@environment ||= RACK_ENV.to_s if defined?(RACK_ENV)
141
+ @@environment ||= Rails.env.to_s if defined?(Rails)
142
+ @@environment ||= 'development'
143
+ end
144
+ @@environment
145
+ end
146
+
147
+ # Output logger for processor instance. Actually it's global logger for
148
+ # whole A13g.
149
+ #
150
+ # @return [Logger]
151
+ #
152
+ # @api public
153
+ def logger
154
+ unless @@logger
155
+ @@logger ||= APP_LOGGER if defined?(APP_LOGGER)
156
+ @@logger ||= Merb::Logger if defined?(Merb::Logger)
157
+ @@logger ||= RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)
158
+ @@logger ||= Rails.logger if defined?(Rails) && !defined?(RAILS_DEFAULT_LOGGER)
159
+ @@logger ||= Logger.new(STDOUT)
160
+ end
161
+ @@logger
162
+ end
163
+
164
+ # Returns `true` if was successfully prepared to work.
165
+ #
166
+ # @return [Boolean]
167
+ # is A13g ready to work
168
+ #
169
+ # @api public
170
+ def ready?
171
+ @@ready
172
+ end
173
+
174
+ # Prepares configuration if not ready and creates connection in specified
175
+ # context.
176
+ #
177
+ # Here you have few examples how to setup A13g...
178
+ #
179
+ # # ...using configuration hash
180
+ # A13g::Base.setup(:default, :adapter => 'stomp', :host => 'localhost')
181
+ #
182
+ # # ...using configuration for specified environment
183
+ # A13g::Base.setup(:default, :development)
184
+ #
185
+ # # ...using default context and current environment configuration
186
+ # A13g::Base.setup() # or A13g::Base.setup(:default)
187
+ #
188
+ # # ...using shortcut
189
+ # A13g.setup(:default)
190
+ #
191
+ # @param [Context] context
192
+ # context name
193
+ # @param [Hash, Symbol, ConnectionSpecification] spec
194
+ # connection configuration
195
+ #
196
+ # @return [A13g::Adapters::AbstractAdapter]
197
+ # established connection
198
+ #
199
+ # @api public
200
+ def setup(context=:default, spec=nil)
201
+ unless ready?
202
+ path.freeze
203
+ environment.freeze
204
+ logger
205
+ if !spec.is_a?(Hash) && !spec.is_a?(ConnectionSpecification)
206
+ load_config_from_file
207
+ else
208
+ @@configurations[context] = HashWithIndifferentAccess.new
209
+ @@configurations[context][environment] = spec if spec.is_a?(Hash)
210
+ @@configurations[context][environment] = spec.config if spec.is_a?(ConnectionSpecification)
211
+ end
212
+ @@ready = true
213
+ end
214
+ establish_connection(context, spec)
215
+ end
216
+
217
+ # Clears all destinations and closes active connections.
218
+ #
219
+ # @api public
220
+ def teardown
221
+ destroy_subscriptions
222
+ clear_all_connections!
223
+ @@logger = nil
224
+ @@path = nil
225
+ @@environment = nil
226
+ @@contextuals = {}
227
+ @@configurations = HashWithIndifferentAccess.new
228
+ @@contexts = HashWithIndifferentAccess.new
229
+ @@ready = false
230
+ end
231
+
232
+ # Connection currently associated with the class or connection from
233
+ # specified context.
234
+ #
235
+ # @param [Symbol] name
236
+ # context name
237
+ #
238
+ # @return [A13g::Adapters::AbstractAdapter]
239
+ # found connection
240
+ #
241
+ # @api public
242
+ def connection(name=nil)
243
+ unless name.nil?
244
+ context = @@contexts[name]
245
+ if context
246
+ raise_connection_error(name) unless context.connection
247
+ return context.connection
248
+ end
249
+ else
250
+ if context
251
+ raise_connection_error(name) unless context.connection
252
+ return context.connection
253
+ else
254
+ return connection(:default)
255
+ end
256
+ end
257
+ raise_connection_error(:default)
258
+ end
259
+
260
+ # Defines connection context. For consumers can be called only once per class.
261
+ #
262
+ # @param [Symbol] name
263
+ # connection name
264
+ #
265
+ # @return [A13g::ConnectionContext]
266
+ # related context
267
+ #
268
+ # @api public
269
+ def context(name=nil)
270
+ if name.nil?
271
+ if @@contextuals[self]
272
+ return @@contextuals[self]
273
+ else
274
+ return @@contexts[:default]
275
+ end
276
+ end
277
+ unless @@contextuals[self]
278
+ if connection(name)
279
+ @@contexts[name].related_classes << self
280
+ @@contextuals[self] = @@contexts[name]
281
+ end
282
+ else
283
+ raise ContextAlreadyDefinedError
284
+ end
285
+ end
286
+
287
+ # Returns true if connected with broker is established and valid.
288
+ #
289
+ # @return [Boolean]
290
+ # connection state
291
+ #
292
+ # @api public
293
+ def connected?
294
+ connection && connection.active?
295
+ end
296
+
297
+ # Creates all defined subscriptions.
298
+ #
299
+ # @api public
300
+ def create_subscriptions
301
+ load_consumers
302
+ Subscription.all.each {|name, subscription| subscription.subscribe }
303
+ end
304
+
305
+ # Destroys all active subscriptions.
306
+ #
307
+ # @api public
308
+ def destroy_subscriptions
309
+ Subscription.all.each {|name, subscription| subscription.unsubscribe }
310
+ end
311
+
312
+ private
313
+
314
+ # Connects to broker using authorization specified in <tt>spec</tt> param.
315
+ #
316
+ # === Connection specification options
317
+ #
318
+ # * <tt>:adapter</tt> - You can use one of following: stomp, jms, wmq, test,
319
+ # beanstalk, asqs, realiable_msg
320
+ # * <tt>:username</tt> - User name for authentication
321
+ # * <tt>:password</tt> - Password for authentication
322
+ # * <tt>:host</tt> - Where broker is served
323
+ # * <tt>:port</tt> - Which port you want to connect
324
+ #
325
+ # There are also adapter specific options, like eg. <tt>:max_retry</tt>,
326
+ # or # <tt>:dead_letter_queue</tt> for stomp adapter.
327
+ #
328
+ # @see A13g#setup
329
+ #
330
+ # @api private
331
+ def establish_connection(context, spec=nil)
332
+ case spec
333
+ when nil
334
+ establish_connection(context, environment)
335
+ when ConnectionSpecification
336
+ remove_connection(name)
337
+ adapter = Adapters.load(spec.adapter)
338
+ conn = adapter.new(spec.config, logger)
339
+ @@contexts[context] = ConnectionContext.new(context, conn)
340
+ conn
341
+ when Symbol, String
342
+ if configurations[context] && configuration = configurations[context][spec.to_s]
343
+ establish_connection(context, configuration)
344
+ else
345
+ raise AdapterNotSpecified, "Broker in `#{spec}` environment is not configured"
346
+ end
347
+ when Hash
348
+ spec = spec.symbolize_keys
349
+ establish_connection unless spec.key?(:adapter)
350
+ establish_connection(context, ConnectionSpecification.new(spec, spec[:adapter]))
351
+ else
352
+ raise ArgumentError, "Unknown connection specification"
353
+ end
354
+ end
355
+
356
+ # @param [Symbol] name
357
+ # context name
358
+ #
359
+ # @api private
360
+ def raise_connection_error(name)
361
+ if name != :default
362
+ raise ConnectionNotEstablished, "Connection marked as `#{name}` is not established"
363
+ else
364
+ raise MissingDefaultContext, "There is not defined default connection context. Did you forgot about `:default`?"
365
+ end
366
+ nil
367
+ end
368
+
369
+ # Removes specified connection.
370
+ #
371
+ # @param [Symbol] name
372
+ # context name
373
+ #
374
+ # @api private
375
+ def remove_connection(context)
376
+ context = @@contexts.delete(context)
377
+ context.connection.disconnect! if context && context.connection
378
+ end
379
+
380
+ # Close all active connections.
381
+ #
382
+ # @api private
383
+ def clear_active_connections!
384
+ @@contexts.values.each do |context|
385
+ if c = context.connection
386
+ c.disconnect! if c.active?
387
+ end
388
+ end
389
+ end
390
+
391
+ # Close all connections.
392
+ #
393
+ # @api private
394
+ def clear_all_connections!
395
+ @@contexts.values.each do |context|
396
+ if c = context.connection
397
+ c.disconnect!
398
+ end
399
+ end
400
+ end
401
+
402
+ # Verify all connections.
403
+ #
404
+ # @api private
405
+ def verify_active_connections!
406
+ @@contexts.values.each do |context|
407
+ if c = context.connection
408
+ c.verify!
409
+ end
410
+ end
411
+ end
412
+
413
+ # Load configurations from file.
414
+ #
415
+ # @param [Symbol] context
416
+ # context name
417
+ #
418
+ # @return [HashWithIndifferentAccess]
419
+ # configuration hash
420
+ #
421
+ # @api private
422
+ def load_config_from_file(context=:default)
423
+ fname = context == :default ? "broker.yml" : "broker-#{context.to_s}.yml"
424
+ cpath = File.join(@@path.config, fname)
425
+ if File.exists?(cpath)
426
+ conf = YAML.load_file(cpath)
427
+ @@configurations[context] = HashWithIndifferentAccess.new(conf)
428
+ end
429
+ end
430
+
431
+ # Loads all available consumers.
432
+ #
433
+ # @api private
434
+ def load_consumers
435
+ if File.exists?(path.consumers)
436
+ begin
437
+ load File.join(path.consumers, 'application_consumer.rb')
438
+ rescue LoadError
439
+ # Do nothing... It is allowed to skip ApplicationConsumer.
440
+ end
441
+ Dir[File.join(path.consumers, '*_consumer.rb')].each do |f|
442
+ load f unless f =~ /\/application_consumer.rb\Z/
443
+ end
444
+ end
445
+ end
446
+ end
447
+ end
448
+ end