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,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