hoodoo 1.0.5 → 1.1.0

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.
Files changed (27) hide show
  1. checksums.yaml +8 -8
  2. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +43 -25
  3. data/lib/hoodoo/discovery.rb +1 -1
  4. data/lib/hoodoo/services/discovery/discoverers/by_flux.rb +130 -0
  5. data/lib/hoodoo/services/discovery/results/for_amqp.rb +15 -21
  6. data/lib/hoodoo/services/discovery/results/for_local.rb +17 -0
  7. data/lib/hoodoo/services/middleware/amqp_log_message.rb +92 -154
  8. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +25 -50
  9. data/lib/hoodoo/services/middleware/middleware.rb +75 -25
  10. data/lib/hoodoo/services/services/service.rb +2 -2
  11. data/lib/hoodoo/version.rb +1 -1
  12. data/spec/services/discovery/discoverers/by_flux_spec.rb +134 -0
  13. data/spec/services/discovery/results/for_amqp_spec.rb +4 -7
  14. data/spec/services/discovery/results/for_local_spec.rb +4 -0
  15. data/spec/services/middleware/amqp_log_message_spec.rb +32 -34
  16. data/spec/services/middleware/amqp_log_writer_spec.rb +2 -5
  17. data/spec/services/middleware/middleware_exotic_communication_spec.rb +147 -143
  18. data/spec/services/middleware/middleware_logging_spec.rb +10 -10
  19. data/spec/services/middleware/middleware_multi_remote_spec.rb +1 -1
  20. data/spec/services/middleware/middleware_public_spec.rb +73 -18
  21. data/spec/services/middleware/middleware_spec.rb +4 -4
  22. data/spec/services/services/application_spec.rb +3 -3
  23. data/spec/spec_helper.rb +3 -8
  24. metadata +19 -7
  25. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +0 -66
  26. data/spec/alchemy/alchemy-amq.rb +0 -33
  27. data/spec/services/discovery/discoverers/by_consul_spec.rb +0 -29
@@ -13,33 +13,33 @@ module Hoodoo; module Services
13
13
  class Middleware
14
14
 
15
15
  # Log writer which sends structured messages to an AMQP-based queue via
16
- # the AlchemyAMQ gems. A Hoodoo::Logger::FastWriter subclass, since though
17
- # talking to the queue might be comparatively slow, Alchemy itself uses an
18
- # asynchronous Thread for this so there's no need to add another one for
19
- # this logger.
16
+ # the Alchemy Flux gem. A Hoodoo::Logger::FastWriter subclass, since though
17
+ # talking to the queue might be comparatively slow, Flux uses Event Machine
18
+ # for this so there's no need to add another Thread for this logger.
20
19
  #
21
- # See also Hoodoo::Logger and Hoodoo::Services::Middleware::AMQPLogMessage.
20
+ # See also Hoodoo::Logger.
22
21
  #
23
22
  class AMQPLogWriter < Hoodoo::Logger::FastWriter
24
23
 
25
24
  # Create an AMQP logger instance.
26
25
  #
27
- # +alchemy+:: The Alchemy endpoint to use for sending messages to the
28
- # AMQP-based queue.
26
+ # +alchemy+:: The Alchemy endpoint to use for sending messages to the
27
+ # AMQP-based queue.
29
28
  #
30
- # +queue_name+:: The queue name (as a String) to use. Optional. If
31
- # omitted, reads +ENV[ 'AMQ_LOGGING_ENDPOINT' ]+ or if
32
- # that is unset, defaults to +platform.logging+.
29
+ # +routing_key+:: The routing key (as a String) to use. Optional. If
30
+ # omitted, reads +ENV[ 'AMQ_LOGGING_ENDPOINT' ]+ or if
31
+ # that is unset, defaults to +platform.logging+.
33
32
  #
34
33
  # If you're running with Rack on top of Alchemy, then the +call+ method's
35
- # +env+ parameter containing the Rack environment should have a key of
36
- # +rack.alchemy+ or +alchemy+ with a value that can be assigned to the
37
- # +alchemy+ parameter. The logger will then use the active Alchemy
38
- # service to send messages to its configured named queue.
34
+ # +env+ parameter containing the Rack environment _MUST_ have a key of
35
+ # +alchemy.service+ with a value that's the AlchemyFlux::Service instance
36
+ # handling queue communication. This is assigned to the +alchemy+
37
+ # parameter. The logger will then use this active Alchemy service to send
38
+ # messages to its configured routing key.
39
39
  #
40
- def initialize( alchemy, queue_name = nil )
41
- @alchemy = alchemy
42
- @queue_name = queue_name || ENV[ 'AMQ_LOGGING_ENDPOINT' ] || 'platform.logging'
40
+ def initialize( alchemy, routing_key = nil )
41
+ @alchemy = alchemy
42
+ @routing_key = routing_key || ENV[ 'AMQ_LOGGING_ENDPOINT' ] || 'platform.logging'
43
43
  end
44
44
 
45
45
  # Custom implementation of the Hoodoo::Logger::WriterMixin#report
@@ -63,19 +63,15 @@ module Hoodoo; module Services
63
63
  # the log payload.
64
64
  #
65
65
  def report( level, component, code, data )
66
- return if @alchemy.nil? || defined?( Hoodoo::Services::Middleware::AMQPLogMessage ).nil?
66
+ return if @alchemy.nil?
67
67
 
68
68
  # Take care with Symbol keys in 'data' vs string keys in e.g. 'Session'.
69
69
 
70
70
  data[ :id ] ||= Hoodoo::UUID.generate()
71
71
 
72
- interaction_id = data[ :interaction_id ]
73
- session = data[ :session ] || {}
72
+ session = data[ :session ] || {}
73
+ message = Hoodoo::Services::Middleware::AMQPLogMessage.new( {
74
74
 
75
- caller_id = session[ 'caller_id' ]
76
- identity = ( session[ 'identity' ] || {} ).to_h
77
-
78
- message = Hoodoo::Services::Middleware::AMQPLogMessage.new(
79
75
  :id => data[ :id ],
80
76
  :level => level,
81
77
  :component => component,
@@ -84,34 +80,13 @@ module Hoodoo; module Services
84
80
 
85
81
  :data => data,
86
82
 
87
- :interaction_id => interaction_id,
88
- :caller_id => caller_id,
89
- :identity => identity,
90
-
91
- :routing_key => @queue_name,
92
- )
83
+ :interaction_id => data[ :interaction_id ],
84
+ :caller_id => session[ 'caller_id' ],
85
+ :identity => ( session[ 'identity' ] || {} ).to_h
93
86
 
94
- # Broken services can attempt to log invalid datatypes such as
95
- # BigDecimal, which Msgpack can't serialize. At the time of writing,
96
- # Alchemy AMQ would thus encounter a serialization exception *within
97
- # its EventMachine sending loop*, so in a different *native* thread.
98
- # This exception could not be caught by Ruby / Hoodoo in "this"
99
- # native thread, so instead the uncaught exception kills the service.
100
- # Although Alchemy could catch that itself, there is no way for it to
101
- # report the failure to Hoodoo in a way that a log reporting caller
102
- # could understand; it's async and in the wrong native thread anyway.
103
- #
104
- # Solution: In non-production environments only (due to the potential
105
- # performance hit), manually serialize the message before sending it.
106
- # If this call blows up, the Hoodoo exception handler will catch it
107
- # and report the result as a properly formed 500, including the
108
- # Msgpack (or other exception) details on exactly what was wrong.
109
- #
110
- unless Hoodoo::Services::Middleware.environment.production?
111
- message.serialize()
112
- end
87
+ } ).to_h()
113
88
 
114
- @alchemy.send_message( message )
89
+ @alchemy.send_message_to_queue( @routing_key, message )
115
90
  end
116
91
  end
117
92
 
@@ -53,7 +53,7 @@ module Hoodoo; module Services
53
53
  #
54
54
  # The middleware adds a STDERR stream writer logger by default and an AMQP
55
55
  # log writer on the first Rack +call+ should the Rack environment provide an
56
- # Alchemy endpoint (see the AlchemyAMQ gem).
56
+ # Alchemy endpoint (see the Alchemy Flux gem).
57
57
  #
58
58
  class Middleware
59
59
 
@@ -211,10 +211,37 @@ module Hoodoo; module Services
211
211
  # Are we running on the queue, else (implied) a local HTTP server?
212
212
  #
213
213
  def self.on_queue?
214
- q = ENV[ 'AMQ_ENDPOINT' ]
214
+ q = ENV[ 'AMQ_URI' ]
215
215
  q.nil? == false && q.empty? == false
216
216
  end
217
217
 
218
+ # Return a service 'name' derived from the service's collection of
219
+ # declared resources. The name will be the same across any instances of
220
+ # the service that implement the same resources. This can be used for
221
+ # e.g. AMQP-based queue-named operations, that want to target the same
222
+ # resource collection regardless of instance.
223
+ #
224
+ # This method will not work unless the middleware has parsed the set
225
+ # of service interface declarations (during instance initialisation).
226
+ # If a least one middleware instance has already been created, it is
227
+ # safe to call.
228
+ #
229
+ def self.service_name
230
+ @@service_name
231
+ end
232
+
233
+ # For a given resource name and version, return the _de_ _facto_ routing
234
+ # path based on version and name with no modifications.
235
+ #
236
+ # +resource+:: Resource name for the endpoint, e.g. +:Purchase+.
237
+ # String or symbol.
238
+ #
239
+ # +version+:: Implemented version of the endpoint. Integer.
240
+ #
241
+ def self.de_facto_path_for( resource, version )
242
+ "/#{ version }/#{ resource }"
243
+ end
244
+
218
245
  # Access the middleware's logging instance. Call +report+ on this to make
219
246
  # structured log entries. See Hoodoo::Logger::WriterMixin#report along
220
247
  # with Hoodoo::Logger for other calls you can use.
@@ -338,7 +365,8 @@ module Hoodoo; module Services
338
365
  # of the endpoints.
339
366
  #
340
367
  def self.flush_services_for_test
341
- @@services = []
368
+ @@services = []
369
+ @@service_name = nil
342
370
 
343
371
  ObjectSpace.each_object( self ) do | middleware_instance |
344
372
  discoverer = middleware_instance.instance_variable_get( '@discoverer' )
@@ -383,7 +411,8 @@ module Hoodoo; module Services
383
411
  # actions Set of symbols naming allowed actions
384
412
  # implementation Hoodoo::Services::Implementation subclass *instance* to
385
413
  # use on match
386
-
414
+ #
415
+ #
387
416
  @@services = service_container.component_interfaces.map do | interface |
388
417
 
389
418
  if interface.nil? || interface.endpoint.nil? || interface.implementation.nil?
@@ -395,6 +424,13 @@ module Hoodoo; module Services
395
424
  #
396
425
  interfaces_have_public_methods() unless interface.public_actions.empty?
397
426
 
427
+ # There are two routes to an implementation - one via the custom path
428
+ # given through its 'endpoint' declaration, the other a de facto path
429
+ # determined from the unmodified version and resource name. Both lead
430
+ # to the same implementation instance.
431
+ #
432
+ implementation_instance = interface.implementation.new
433
+
398
434
  # Regexp explanation:
399
435
  #
400
436
  # Match "/", the version text, "/", the endpoint text, then either
@@ -402,17 +438,34 @@ module Hoodoo; module Services
402
438
  # everything else. Match data index 1 will be whatever character (if
403
439
  # any) followed after the endpoint ("/" or ".") while index 2 contains
404
440
  # everything else.
441
+ #
442
+ custom_path = "/v#{ interface.version }/#{ interface.endpoint }"
443
+ custom_regexp = /\/v#{ interface.version }\/#{ interface.endpoint }(\.|\/|$)(.*)/
444
+
445
+ # Same as above, but for the de facto routing.
446
+ #
447
+ de_facto_path = self.class.de_facto_path_for( interface.resource, interface.version )
448
+ de_facto_regexp = /\/#{ interface.version }\/#{ interface.resource }(\.|\/|$)(.*)/
405
449
 
406
450
  Hoodoo::Services::Discovery::ForLocal.new(
407
451
  :resource => interface.resource,
408
452
  :version => interface.version,
409
- :base_path => "/v#{ interface.version }/#{ interface.endpoint }",
410
- :routing_regexp => /\/v#{ interface.version }\/#{ interface.endpoint }(\.|\/|$)(.*)/,
453
+ :base_path => custom_path,
454
+ :routing_regexp => custom_regexp,
455
+ :de_facto_base_path => de_facto_path,
456
+ :de_facto_routing_regexp => de_facto_regexp,
411
457
  :interface_class => interface,
412
- :implementation_instance => interface.implementation.new
458
+ :implementation_instance => implementation_instance
413
459
  )
460
+
414
461
  end
415
462
 
463
+ # Determine the service name from the resources above then announce
464
+ # the whole collection to any interested discovery engines.
465
+
466
+ sorted_resources = @@services.map() { | service | service.resource }.sort()
467
+ @@service_name = "service.#{ sorted_resources.join( '_' ) }"
468
+
416
469
  announce_presence_of( @@services )
417
470
  end
418
471
 
@@ -972,7 +1025,8 @@ module Hoodoo; module Services
972
1025
  # +env+:: Rack 'env' parameter from e.g. Rack's invocation of #call.
973
1026
  #
974
1027
  def enable_alchemy_logging_from( env )
975
- alchemy = env[ 'rack.alchemy' ]
1028
+ alchemy = env[ 'alchemy.service' ]
1029
+
976
1030
  unless alchemy.nil? || defined?( @@alchemy )
977
1031
  @@alchemy = alchemy
978
1032
  self.class.send( :add_queue_logging, @@alchemy ) unless @@alchemy.nil?
@@ -1343,22 +1397,13 @@ module Hoodoo; module Services
1343
1397
  end
1344
1398
  end
1345
1399
 
1346
- # Announce the presence of the service endpoints to known interested
1347
- # parties.
1400
+ # Announce the presence of the local resource endpoints in this service
1401
+ # to known interested parties.
1348
1402
  #
1349
1403
  # ONLY CALL AS PART OF INSTANCE CREATION (from #initialize).
1350
1404
  #
1351
- # +services+:: Array of Hashes describing service information.
1352
- #
1353
- # Hash keys/values are as follows:
1354
- #
1355
- # +regexp+:: A regular expression for a URI path which, if matched,
1356
- # means that this service endpoint is being called.
1357
- # +path+:: The endpoint path that the regexp would match, with
1358
- # leading "/" (e.g. "/v1/products")
1359
- # +interface+:: The Interface subclass for this endpoint.
1360
- # +implementation+:: The Implementation subclass instance for this
1361
- # endpoint.
1405
+ # +services+:: Array of Hoodoo::Services::Discovery::ForLocal instances
1406
+ # describing available resources in this local service.
1362
1407
  #
1363
1408
  def announce_presence_of( services )
1364
1409
 
@@ -1370,7 +1415,7 @@ module Hoodoo; module Services
1370
1415
  #
1371
1416
  # A class variable is wrong, as entirely new instances of the service
1372
1417
  # middleware might be stood up in one process and could potentially
1373
- # be handling different services. This is typically only the case for
1418
+ # be handling different resources. This is typically only the case for
1374
1419
  # running tests, but *might* happen elsewhere too. In any event, we
1375
1420
  # don't want announcements in one instance to pollute the discovery
1376
1421
  # data in another (especially the records of which services were
@@ -1378,14 +1423,15 @@ module Hoodoo; module Services
1378
1423
 
1379
1424
  if self.class.on_queue?
1380
1425
 
1381
- @discoverer ||= Hoodoo::Services::Discovery::ByConsul.new
1426
+ @discoverer ||= Hoodoo::Services::Discovery::ByFlux.new
1382
1427
 
1383
1428
  services.each do | service |
1384
1429
  interface = service.interface_class
1385
1430
 
1386
1431
  @discoverer.announce(
1387
1432
  interface.resource,
1388
- interface.version
1433
+ interface.version,
1434
+ { :services => services }
1389
1435
  )
1390
1436
  end
1391
1437
 
@@ -1584,12 +1630,16 @@ module Hoodoo; module Services
1584
1630
  # then there's no matching endpoint; badly routed request; 404. If we
1585
1631
  # find many, raise an exception and rely on the exception handler to
1586
1632
  # send back a 500.
1633
+ #
1634
+ # We try the custom routing path first, then the de facto path, then
1635
+ # give up if neither match.
1587
1636
 
1588
1637
  uri_path = CGI.unescape( interaction.rack_request.path() )
1589
1638
 
1590
1639
  selected_path_data = nil
1591
1640
  selected_services = @@services.select do | service_data |
1592
- path_data = process_uri_path( uri_path, service_data.routing_regexp )
1641
+ path_data = process_uri_path( uri_path, service_data.routing_regexp ) ||
1642
+ process_uri_path( uri_path, service_data.de_facto_routing_regexp )
1593
1643
 
1594
1644
  if path_data.nil?
1595
1645
  false
@@ -102,7 +102,7 @@ module Hoodoo; module Services
102
102
  # +env+:: Rack environment (ignored).
103
103
  #
104
104
  def call( env )
105
- raise "Hoodoo::Services::Implementation subclasses should only be called through the middleware - add 'use Hoodoo::Services::Middleware' to (e.g.) config.ru"
105
+ raise "Hoodoo::Services::Service subclasses should only be called through the middleware - add 'use Hoodoo::Services::Middleware' to (e.g.) config.ru"
106
106
  end
107
107
 
108
108
  protected
@@ -126,7 +126,7 @@ module Hoodoo; module Services
126
126
  #
127
127
  classes.each do | klass |
128
128
  unless klass < Hoodoo::Services::Interface
129
- raise "Hoodoo::Services::Implementation::comprised_of expects Hoodoo::Services::Interface subclasses only - got '#{ klass }'"
129
+ raise "Hoodoo::Services::Service::comprised_of expects Hoodoo::Services::Interface subclasses only - got '#{ klass }'"
130
130
  end
131
131
  end
132
132
 
@@ -12,6 +12,6 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, ensure that the date in
13
13
  # "hoodoo.gemspec" is correct and run "bundle install" (or "update").
14
14
  #
15
- VERSION = '1.0.5'
15
+ VERSION = '1.1.0'
16
16
 
17
17
  end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hoodoo::Services::Discovery::ByFlux do
4
+
5
+ after :all do
6
+ Hoodoo::Services::Middleware.flush_services_for_test()
7
+ end
8
+
9
+ context 'without a mocked service name it' do
10
+ before :each do
11
+ @d = described_class.new
12
+ allow( Hoodoo::Services::Middleware ).to receive( :service_name ).and_return( 'service.Version' )
13
+ end
14
+
15
+ it 'announces' do
16
+ result = @d.announce( 'Version', '2' ) # Intentional string use
17
+
18
+ expect( result ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
19
+ expect( result.resource ).to eq( :Version )
20
+ expect( result.version ).to eq( 2 )
21
+ expect( result.routing_path ).to eq( '/2/Version')
22
+ end
23
+
24
+ it 'discovers local' do
25
+ @d.announce( :Version, 2 )
26
+ result = @d.discover( 'Version', '2' )
27
+
28
+ expect( result ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
29
+ expect( result.resource ).to eq( :Version )
30
+ expect( result.version ).to eq( 2 )
31
+ expect( result.routing_path ).to eq( '/2/Version')
32
+ end
33
+
34
+ it 'discovers remote' do
35
+ result = @d.discover( 'External', 1 )
36
+
37
+ expect( result ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
38
+ expect( result.resource ).to eq( :External )
39
+ expect( result.version ).to eq( 1 )
40
+ expect( result.routing_path ).to eq( '/1/External')
41
+ end
42
+ end
43
+
44
+ context 'with a real service list it' do
45
+
46
+ class FluxDiscovererImplementationA < Hoodoo::Services::Implementation; end
47
+ class FluxDiscovererImplementationB < Hoodoo::Services::Implementation; end
48
+ class FluxDiscovererImplementationC < Hoodoo::Services::Implementation; end
49
+
50
+ class FluxDiscovererInterfaceA < Hoodoo::Services::Interface
51
+ interface :InterfaceA do
52
+ endpoint :a_interfaces, FluxDiscovererImplementationA
53
+ version 1
54
+ end
55
+ end
56
+
57
+ class FluxDiscovererInterfaceB < Hoodoo::Services::Interface
58
+ interface :InterfaceB do
59
+ endpoint :b_interfaces, FluxDiscovererImplementationA
60
+ version 2
61
+ end
62
+ end
63
+
64
+ class FluxDiscovererInterfaceC < Hoodoo::Services::Interface
65
+ interface :InterfaceC do
66
+ endpoint :c_interfaces, FluxDiscovererImplementationA
67
+ version 3
68
+ end
69
+ end
70
+
71
+ # Intentionally not in alphabetical order, to check for reordering in
72
+ # service name / queue name environment variable later.
73
+ #
74
+ class FluxDiscovererService < Hoodoo::Services::Service
75
+ comprised_of FluxDiscovererInterfaceB,
76
+ FluxDiscovererInterfaceA,
77
+ FluxDiscovererInterfaceC
78
+ end
79
+
80
+ before :each do
81
+ Hoodoo::Services::Middleware.flush_services_for_test()
82
+ @d = described_class.new
83
+ end
84
+
85
+ it 'announces and sets environment variables' do
86
+ ENV.delete( 'ALCHEMY_SERVICE_NAME' )
87
+ ENV.delete( 'ALCHEMY_RESOURCE_PATHS' )
88
+
89
+ expect_any_instance_of( Hoodoo::Services::Middleware ).to receive( :announce_presence_of ) do | instance, services |
90
+ result = @d.announce( :InterfaceA, '1', { :services => services } )
91
+
92
+ expect( result ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
93
+ expect( result.resource ).to eq( :InterfaceA )
94
+ expect( result.version ).to eq( 1 )
95
+ expect( result.routing_path ).to eq( '/1/InterfaceA')
96
+ end
97
+
98
+ Hoodoo::Services::Middleware.new( FluxDiscovererService.new )
99
+
100
+ expect( ENV[ 'ALCHEMY_SERVICE_NAME' ] ).to eq( 'service.InterfaceA_InterfaceB_InterfaceC' )
101
+ expect( ENV[ 'ALCHEMY_RESOURCE_PATHS' ].split( ',' ) ).to match_array(
102
+ %w{ /v1/a_interfaces /1/InterfaceA /v2/b_interfaces /2/InterfaceB /v3/c_interfaces /3/InterfaceC }
103
+ )
104
+ end
105
+
106
+ it 'discovers local' do
107
+ expect_any_instance_of( Hoodoo::Services::Middleware ).to receive( :announce_presence_of ) do | instance, services |
108
+ @d.announce( :InterfaceC, 3, { :services => services } )
109
+ result = @d.discover( 'InterfaceC', '3' )
110
+
111
+ expect( result ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
112
+ expect( result.resource ).to eq( :InterfaceC )
113
+ expect( result.version ).to eq( 3 )
114
+ expect( result.routing_path ).to eq( '/3/InterfaceC')
115
+ end
116
+
117
+ Hoodoo::Services::Middleware.new( FluxDiscovererService.new )
118
+ end
119
+
120
+ it 'discovers remote' do
121
+ expect_any_instance_of( Hoodoo::Services::Middleware ).to receive( :announce_presence_of ) do | instance, services |
122
+ result = @d.discover( 'External', 1 )
123
+
124
+ expect( result ).to be_a( Hoodoo::Services::Discovery::ForAMQP )
125
+ expect( result.resource ).to eq( :External )
126
+ expect( result.version ).to eq( 1 )
127
+ expect( result.routing_path ).to eq( '/1/External')
128
+ end
129
+
130
+ Hoodoo::Services::Middleware.new( FluxDiscovererService.new )
131
+ end
132
+
133
+ end
134
+ end