gorg_service 4.1.0 → 5.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fc3822b0c00f7ba359527dcaa1245707ca309c0d
4
- data.tar.gz: b8f61e1735b52a535df86432317d4981765663ce
3
+ metadata.gz: 23984c4b8e4322a1423a2d6458c7025303da38b7
4
+ data.tar.gz: 6c3e2e694dc143d926742bab066b31448808b4b5
5
5
  SHA512:
6
- metadata.gz: a60aecc1ffd07385e7dec4620456b436b4d64227de4fcae6669f0a1d8637a119e472f7701c6222aa72272b7701a54e661eb4a38f7b07442cee8e82cae0f76f63
7
- data.tar.gz: 863da64b36760eee6a28c637d61ef2a558ea1d00e86bed8792da8bb435a6ee99b7383834cde05bc98beab27e0ce9ed851e43195b756a8635ba8e67fa6de0009a
6
+ metadata.gz: 268e2a10c35eec58d78d6a177d05d53a648ff62e955323419649673fbfa18c99de4dd2705793bc2d18ad8169d1ba83cab2d0f5d7c7d3b583bcd210a91016ef09
7
+ data.tar.gz: dff239c0d3c80884cf6f8adf47b54c97160ed801c870ea5dffec578dd7dda07fc449a159a1f2a4a36f5d059370de1d7ab3d534090470f6997b41cf9598c6c60e
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2016 Alexandre Narbonne
3
+ Copyright (c) 2016 Société des ingénieurs Arts et Métiers
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -45,8 +45,7 @@ GorgService.configure do |c|
45
45
  # c.rabbitmq_vhost = "/"
46
46
  #
47
47
  #
48
- # c.rabbitmq_queue_name = c.application_name
49
- # c.rabbitmq_exchange_name = "exchange"
48
+ # c.rabbitmq_event_exchange_name = "exchange"
50
49
  #
51
50
  # time before trying again on softfail in milliseconds (temporary error)
52
51
  # c.rabbitmq_deferred_time = 1800000 # 30min
@@ -58,15 +57,6 @@ GorgService.configure do |c|
58
57
  # Central logging is disable if nil
59
58
  # c.log_routing_key = nil
60
59
  #
61
- # Routing hash
62
- # map routing_key of received message with MessageHandler
63
- # exemple:
64
- # c.message_handler_map={
65
- # "some.routing.key" => MyMessageHandler,
66
- # "Another.routing.key" => OtherMessageHandler,
67
- # "third.routing.key" => MyMessageHandler,
68
- # }
69
- c.message_handler_map= {} #TODO : Set my routing hash
70
60
 
71
61
  end
72
62
  ```
@@ -80,19 +70,19 @@ my_service.run
80
70
  ```
81
71
  ### Routing and MessageHandler
82
72
  When running, GorgService act as a consumer on Gadz.org RabbitMQ network.
83
- It bind its queue on the main exchange and subscribes to routing keys defines in `message_handler_map`
73
+ It bind its queue on the main exchange and subscribes to routing keys with `listen_to`
84
74
 
85
75
  Each received message will be routed to the corresponding `MessageHandler`. AMQP wildcards are supported.The first key to match the incoming routing key will be used.
86
76
 
87
- A `MessageHandler` is a kind of controller. This is where you put the message is processed.
77
+ A `MessageHandler` is a kind of controller. This is where you the message is processed.
88
78
  A `MessageHandler` expect a `GorgService::Message` as param of its `initializer`method.
89
79
 
90
80
  Here is an exemple `MessageHandler` :
91
81
  ```ruby
92
- require 'json'
93
- require 'json-schema' #Checkout https://github.com/ruby-json-schema/json-schema
94
82
 
95
- class ExampleMessageHandler < GorgService::MessageHandler
83
+ class ExampleMessageHandler < GorgService::Consumer::MessageHandler::EventHandler
84
+
85
+ listen_to "event.user.updated"
96
86
 
97
87
  EXPECTED_SCHEMA = {
98
88
  "type" => "object",
@@ -102,22 +92,25 @@ class ExampleMessageHandler < GorgService::MessageHandler
102
92
  }
103
93
  }
104
94
 
105
- def initialize(msg)
106
- data=msg.data
107
- begin
108
- JSON::Validator.validate!(EXPECTED_SCHEMA, data)
109
- rescue JSON::Schema::ValidationError => e
110
- #This message can't be processed, it will be discarded
111
- raise_hardfail("Invalid message",e)
112
- end
113
-
95
+ def validate
96
+ message.validate_with(EXPECTED_SCHEMA)
97
+ end
98
+
99
+ def process
114
100
  begin
115
- MyAPI.send(msg.data)
116
- rescue MyAPI::UnavailableConnection => e
117
- # This message can be processed but external resources
118
- # are not available at this moment, retry later
119
- raise_softfail("Can't connect to MyAPI",e)
120
- end
101
+ if example=Example.update(message.data[:user_id])
102
+ producer=GorgServiceProducer.new
103
+ event=GorgService::Message.new(
104
+ event: "event.example.updated",
105
+ data: {example_id: example.id}
106
+ )
107
+ producer.publish_message(event)
108
+ else
109
+ raise_hardfail("Unable to update example")
110
+ end
111
+ rescue ApiConnectionError => e
112
+ raise_softfail("Unable to connect to API",error: e)
113
+ end
121
114
  end
122
115
  end
123
116
  ```
@@ -156,7 +149,6 @@ Error log structure :
156
149
 
157
150
  ## To Do
158
151
 
159
- - Internal logs using Logger
160
152
  - Allow disable JSON Schema Validation on incomming messages
161
153
  - Message and ErrorLog attributes validation
162
154
 
data/bin/console CHANGED
@@ -11,7 +11,7 @@ require "gorg_service"
11
11
  # Pry.start
12
12
 
13
13
  #You can add a file bin/env.rb to set convenience environment such as configuration
14
- env_path=File.join(File.dirname(__FILE__),"/env.rb")
14
+ env_path=File.expand_path("./env.rb",File.dirname(__FILE__))
15
15
  require env_path if File.file?(env_path)
16
16
 
17
17
  require "irb"
data/bin/worker CHANGED
@@ -11,7 +11,7 @@ require "gorg_service"
11
11
  # Pry.start
12
12
 
13
13
  #You should add a file bin/env.rb to set convenience environment such as configuration
14
- env_path=File.join(File.dirname(__FILE__),"/env.rb")
14
+ env_path=File.expand_path("./env.rb",File.dirname(__FILE__))
15
15
  require env_path if File.file?(env_path)
16
16
 
17
17
  GorgService.new.run
data/gorg_service.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_dependency 'bunny', '~> 2.2', '>= 2.2.2'
30
30
  spec.add_dependency 'json-schema', '~> 2.6'
31
- spec.add_dependency 'gorg_message_sender', '~> 1'
31
+ spec.add_dependency 'gorg_message_sender', '~> 1.4.2'
32
32
 
33
33
  spec.add_development_dependency "bundler", "~> 1.11"
34
34
  spec.add_development_dependency "rake", "~> 10.0"
@@ -21,8 +21,7 @@ class GorgService
21
21
  :application_id,
22
22
  :rabbitmq_host,
23
23
  :rabbitmq_port,
24
- :rabbitmq_queue_name,
25
- :rabbitmq_exchange_name,
24
+ :rabbitmq_event_exchange_name,
26
25
  :rabbitmq_deferred_time,
27
26
  :rabbitmq_max_attempts,
28
27
  :rabbitmq_user,
@@ -41,9 +40,8 @@ class GorgService
41
40
  @message_handler_map = {}
42
41
  @rabbitmq_host = "localhost"
43
42
  @rabbitmq_port = 5672
44
- @rabbitmq_queue_name = @application_id
45
43
  @rabbitmq_deferred_time = 1800000 #30 minutes
46
- @rabbitmq_exchange_name = "exchange"
44
+ @rabbitmq_event_exchange_name = "exchange"
47
45
  @rabbitmq_user = nil
48
46
  @rabbitmq_password = nil
49
47
  @rabbitmq_vhost = "/"
@@ -51,5 +49,20 @@ class GorgService
51
49
  @log_routing_key = nil
52
50
  @prefetch_count = 1
53
51
  end
52
+
53
+ def rabbitmq_queue_name=(value)
54
+ warn "[DEPRECATION] GorgService::Configuration : `rabbitmq_queue_name=` is deprecated and will be removed soon."
55
+ end
56
+
57
+ # Deprecated: please use rabbitmq_event_exchange_name instead
58
+ def rabbitmq_exchange_name
59
+ @rabbitmq_event_exchange_name
60
+ end
61
+
62
+ # Deprecated: please use rabbitmq_event_exchange_name instead
63
+ def rabbitmq_exchange_name=(v)
64
+ warn "[DEPRECATION] GorgService::Configuration : `rabbitmq_exchange_name` is deprecated. Please use `rabbitmq_event_exchange_name` instead."
65
+ @rabbitmq_event_exchange_name=v
66
+ end
54
67
  end
55
68
  end
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ class GorgService
5
+ class Consumer
6
+ #Common behavior of failling errors
7
+ class FailError < StandardError
8
+ attr_reader :error_raised
9
+
10
+ def initialize(message = nil, error_raised = nil)
11
+ @message = message
12
+ @error_raised = error_raised
13
+ end
14
+
15
+ def message
16
+ @message
17
+ end
18
+
19
+ def type
20
+ ""
21
+ end
22
+ end
23
+
24
+ #Softfail error : This message should be processed again later
25
+ class SoftfailError < FailError
26
+ def type
27
+ "softerror"
28
+ end
29
+ end
30
+
31
+ #Hardfail error : This message is not processable and will never be
32
+ class HardfailError < FailError
33
+ def type
34
+ "harderror"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "bunny"
5
+
6
+ class GorgService
7
+ class Consumer
8
+ class Listener
9
+
10
+ attr_accessor :consumer
11
+
12
+
13
+
14
+ def initialize(env: nil, max_attempts: 48, log_routing_key: nil)
15
+ @max_attempts=max_attempts.to_i
16
+ @log_routing_key=log_routing_key
17
+
18
+ @env=env
19
+ end
20
+
21
+ def listen
22
+ @consumer=@env.job_queue.subscribe(:manual_ack => true) do |delivery_info, properties, body|
23
+ #Log
24
+ routing_key=delivery_info[:routing_key]
25
+ GorgService.logger.info "Received message with routing key #{routing_key}"
26
+ GorgService.logger.debug "Message properties : #{properties.to_s}"
27
+ GorgService.logger.debug "Message payload : #{body.to_s[0...10000]}"
28
+
29
+ #Process
30
+ process_message(delivery_info, properties, body)
31
+
32
+ #Acknoledge
33
+ @env.ch.ack(delivery_info.delivery_tag)
34
+ end
35
+ end
36
+
37
+ def stop
38
+ @consumer.cancel
39
+ end
40
+
41
+ protected
42
+
43
+ def process_message(delivery_info, _properties, body)
44
+ message=nil
45
+ begin
46
+ #Parse message
47
+ message=Message.parse(delivery_info, _properties, body)
48
+
49
+ #Process message
50
+ incomming_message_error_count=message.errors.count
51
+ MessageRouter.new(message)
52
+ process_logging(message) if message.errors.count>incomming_message_error_count
53
+
54
+ rescue SoftfailError => e
55
+ process_softfail(e, message)
56
+ rescue HardfailError => e
57
+ process_hardfail(e, message)
58
+ end
59
+ end
60
+
61
+ def process_softfail(e, message)
62
+ message.log_error(e)
63
+ GorgService.logger.error "SOFTFAIL ERROR : #{e.message}"
64
+ if message.errors.count.to_i >= @max_attempts
65
+ GorgService.logger.info " DISCARD MESSAGE : #{message.errors.count} errors in message log"
66
+ process_hardfail(HardfailError.new("Too Much SoftError : This message reached the limit of softerror (max: #{@max_attempts})"), message)
67
+ else
68
+ send_to_deferred_queue(message)
69
+ end
70
+ end
71
+
72
+ def process_hardfail(e, message)
73
+ GorgService.logger.error "HARDFAIL ERROR : #{e.message}, #{e.error_raised&&e.error_raised.inspect}"
74
+ GorgService.logger.info " DISCARD MESSAGE"
75
+ if message
76
+ message.log_error(e)
77
+ process_logging(message)
78
+ end
79
+ end
80
+
81
+ def process_logging(message)
82
+ message.routing_key=@log_routing_key
83
+ GorgService::Producer.new.publish_message(message)
84
+ #RabbitmqProducer.new.send_raw(message.to_json, @log_routing_key, verbose: true) if @log_routing_key
85
+ end
86
+
87
+ def send_to_deferred_queue(msg)
88
+ if @env.delayed_queue_for msg.event
89
+ @env.delayed_in_exchange.publish(msg.to_json, :routing_key => msg.event)
90
+ GorgService.logger.info "DEFER MESSAGE : message sent to #{@env.delayed_in_exchange.name} with routing key #{msg.event}"
91
+ else
92
+ raise "DelayedQueueNotFound"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ class GorgService
4
+ class Consumer
5
+ module MessageHandler
6
+ class Base
7
+
8
+ def initialize(message)
9
+ @message=message
10
+
11
+ begin
12
+ validate
13
+ rescue GorgService::Message::DataValidationError => e
14
+ raise_hardfail("DataValidationError",error: e.errors)
15
+ end
16
+ process
17
+ end
18
+
19
+ def validate
20
+ GorgService.logger.warn "WARNING : No message schema validation in #{self.class.name}, implement it in #validate(message) "
21
+ end
22
+
23
+ def process
24
+ GorgService.logger.warn "WARNING : You must define your MessageHandler behavior in #process"
25
+ end
26
+
27
+ def message
28
+ @message
29
+ end
30
+ alias_method :msg, :message
31
+
32
+ def raise_hardfail(message, error: nil)
33
+ raise HardfailError.new(message, error)
34
+ end
35
+
36
+ def raise_softfail(message, error: nil)
37
+ raise SoftfailError.new(message, error)
38
+ end
39
+
40
+ class << self
41
+
42
+ def listen_to(routing_key)
43
+ MessageRouter.register_route(routing_key, self)
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ class GorgService
5
+ class Consumer
6
+ module MessageHandler
7
+ class EventHandler < Base
8
+
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ class GorgService
5
+ class Consumer
6
+ module MessageHandler
7
+ class ReplyHandler < Base
8
+
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ class GorgService
5
+ class Consumer
6
+ module MessageHandler
7
+ class RequestHandler < Base
8
+
9
+ def reply_with(data)
10
+ if message.expect_reply?
11
+
12
+ reply=GorgService::Message.new(
13
+ event: message.reply_routing_key,
14
+ data: data,
15
+ correlation_id: message.id,
16
+ type: "reply"
17
+ )
18
+
19
+ replier=GorgService::Producer.new
20
+ replier.publish_message(reply,exchange: message.reply_to)
21
+ end
22
+ end
23
+
24
+ def raise_hardfail(message, error: nil, data: nil)
25
+ reply_with({
26
+ status: 'hardfail',
27
+ error_message: message,
28
+ debug_message: error&&error.inspect,
29
+ error_data: data
30
+ })
31
+
32
+ super(message, error: error)
33
+ end
34
+
35
+ def raise_softfail(message, error: nil, data: nil)
36
+ reply_with({
37
+ status: 'softfail',
38
+ error_message: message,
39
+ debug_message: error&&error.inspect,
40
+ error_data: data
41
+ })
42
+ super(message, error: error)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ class GorgService
5
+ class Consumer
6
+ module MessageHandler
7
+
8
+ end
9
+ end
10
+ end
11
+
12
+ require "gorg_service/consumer/message_handler/base"
13
+ require "gorg_service/consumer/message_handler/event_handler"
14
+ require "gorg_service/consumer/message_handler/request_handler"
15
+ require "gorg_service/consumer/message_handler/reply_handler"
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ class GorgService
5
+ class Consumer
6
+ class MessageRouter
7
+
8
+ def initialize(message)
9
+ message_handler=self.class.message_handler_for message.routing_key
10
+ raise HardfailError.new("Routing error : No message handler found for this routing key") unless message_handler
11
+
12
+ message_handler.new(message)
13
+ end
14
+
15
+ class << self
16
+
17
+ def routes
18
+ @routes||={}
19
+ end
20
+
21
+ def register_route(routing_key, message_handler)
22
+ routes[routing_key]=message_handler
23
+ end
24
+
25
+ def listened_keys
26
+ routes.keys
27
+ end
28
+
29
+ def message_handler_for routing_key
30
+ @routes.each do |k, mh|
31
+ return mh if amqp_key_to_regex(k).match(routing_key)
32
+ end
33
+ false
34
+ end
35
+
36
+ private
37
+
38
+ def amqp_key_to_regex(key)
39
+ regex_base=key.gsub('.', '\.')
40
+ .gsub('*', '([a-zA-Z0-9\-_:]+)')
41
+ .gsub(/(\\\.)?#(\\\.)?/, '((\.)?[a-zA-Z0-9\-_:]*(\.)?)*')
42
+
43
+ /^#{regex_base}$/
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "gorg_service/consumer/message_router"
5
+ require "gorg_service/consumer/errors"
6
+ require "gorg_service/consumer/listener"
7
+ require "gorg_service/consumer/message_handler"
8
+
9
+ class GorgService
10
+ class Consumer
11
+
12
+ attr_accessor :environment
13
+
14
+ def initialize(environment: GorgService.environment)
15
+ @environment=environment
16
+ end
17
+
18
+ def listener
19
+ @listener ||= Listener.new(
20
+ env: environment,
21
+ max_attempts: GorgService.configuration.rabbitmq_max_attempts.to_i,
22
+ log_routing_key: GorgService.configuration.log_routing_key
23
+ )
24
+ end
25
+
26
+ def run
27
+ begin
28
+ self.start
29
+ puts " [*] Waiting for messages. To exit press CTRL+C"
30
+ loop do
31
+ sleep(1)
32
+ end
33
+ rescue SystemExit, Interrupt => _
34
+ self.stop
35
+ end
36
+ end
37
+
38
+ def start
39
+ listener.listen
40
+ end
41
+
42
+ def stop
43
+ listener.stop
44
+ end
45
+ end
46
+ end
@@ -11,8 +11,27 @@ require "gorg_service/message/error_log"
11
11
  class GorgService
12
12
  class Message
13
13
 
14
- attr_accessor :event
14
+ class DataValidationError < StandardError
15
+
16
+ attr_reader :errors
17
+
18
+ def initialize(errors)
19
+ @errors=errors
20
+ end
21
+ end
22
+
15
23
  attr_accessor :id
24
+ attr_accessor :reply_to
25
+ attr_accessor :correlation_id
26
+ attr_accessor :sender_id
27
+ attr_accessor :content_type
28
+ attr_accessor :content_encoding
29
+ attr_accessor :headers
30
+ attr_accessor :type
31
+
32
+ attr_accessor :event_id
33
+ attr_accessor :routing_key
34
+ attr_accessor :event
16
35
  attr_accessor :data
17
36
  attr_accessor :errors
18
37
  attr_accessor :creation_time
@@ -23,28 +42,56 @@ class GorgService
23
42
  end
24
43
 
25
44
 
26
- def initialize(id: generate_id, data: nil, event: nil, creation_time: DateTime.now.iso8601, sender: application_id , errors: [])
27
- @id=id
28
- @event=event
29
- @data=data
30
- @errors=errors&&errors.map{|e| e.is_a?(GorgService::Message::ErrorLog) ? e : GorgService::Message::ErrorLog.parse(e)}
31
- @creation_time=creation_time
32
- @sender= sender
45
+ def initialize(opts={})
46
+ ##Message payload params
47
+ @event_id= opts.fetch(:event_id,generate_id)
48
+ @errors= opts.fetch(:errors,nil)
49
+ @creation_time= opts.fetch(:creation_time,DateTime.now.iso8601)
50
+ @sender= opts.fetch(:sender,application_id)
51
+ @event= opts.fetch(:event,nil)
52
+ @data= opts.fetch(:data,nil)
53
+
54
+ #Message Attributes params
55
+ @routing_key= opts.fetch(:routing_key,event)
56
+ @id= opts.fetch(:id,generate_id)
57
+ @reply_to= opts.fetch(:reply_to,nil)
58
+ @correlation_id= opts.fetch(:correlation_id,nil)
59
+ @sender_id= opts.fetch(:sender_id,application_id)
60
+ @content_type= opts.fetch(:content_type,"application/json")
61
+ @content_encoding= opts.fetch(:content_encoding,"deflate")
62
+ @headers= opts.fetch(:headers,{})
63
+ @type= opts.fetch(:type,"event")
33
64
  end
34
65
 
35
- def to_h
36
- body={
37
- event_uuid: @id,
66
+ def body
67
+ _body={
68
+ event_uuid: @event_id,
38
69
  event_name: @event,
39
70
  event_sender_id: @sender,
40
71
  event_creation_time: @creation_time,
41
72
  data: @data,
42
73
  }
43
74
  if errors.any?
44
- body[:errors_count]=@errors.count
45
- body[:errors]=@errors.map{|e| e.to_h}
75
+ _body[:errors_count]=@errors.count
76
+ _body[:errors]=@errors.map{|e| e.to_h}
46
77
  end
47
- body
78
+ _body
79
+ end
80
+ alias_method :to_h, :body
81
+ alias_method :payload, :body
82
+
83
+ def properties
84
+ {
85
+ routing_key: routing_key,
86
+ reply_to: reply_to,
87
+ correlation_id: correlation_id,
88
+ content_type: content_type,
89
+ content_encoding: content_encoding,
90
+ headers: headers,
91
+ app_id: sender_id,
92
+ type: type,
93
+ message_id: id,
94
+ }
48
95
  end
49
96
 
50
97
  # Generate RabbitMQ message body
@@ -62,32 +109,60 @@ class GorgService
62
109
  errors<<e
63
110
  end
64
111
 
112
+ def reply_exchange
113
+ reply_to
114
+ end
115
+
116
+ def expect_reply?
117
+ !!reply_to
118
+ end
119
+
120
+ def reply_routing_key
121
+ event.sub('request','reply')
122
+ end
123
+
124
+ def validate_data_with(schema)
125
+ errors=JSON::Validator.fully_validate(schema, self.data)
126
+ if errors.any?
127
+ raise DataValidationError.new(errors)
128
+ else
129
+ return true
130
+ end
131
+ end
132
+
133
+
134
+
65
135
  ### Class methods
66
136
 
67
- # Parse RabbitMQ message body
68
- # @return Message
69
- # parsed message
70
- # Errors
71
- # Hardfail if un-parsable JSON body
72
- def self.parse_body(body)
137
+ def self.parse(delivery_info, properties, body)
73
138
  begin
74
139
  json_body=JSON.parse(body)
75
140
 
76
141
  JSON::Validator.validate!(GorgService::Message::JSON_SCHEMA,json_body)
77
142
 
78
- puts json_body["errors"].inspect
79
-
80
143
  msg=self.new(
81
- id: json_body["event_uuid"],
144
+ routing_key: delivery_info[:routing_key],
145
+ id: properties[:message_id],
146
+ reply_to: properties[:reply_to],
147
+ correlation_id: properties[:correlatio_to],
148
+ sender_id: properties[:app_id],
149
+ content_type: properties[:content_type],
150
+ content_encoding: properties[:content_encoding],
151
+ headers: properties[:header],
152
+ type: properties[:type],
153
+
154
+ event_id: json_body["event_uuid"],
82
155
  event: json_body["event_name"],
83
156
  data: convert_keys_to_sym(json_body["data"]),
84
157
  creation_time: json_body["event_creation_time"] && DateTime.parse(json_body["event_creation_time"]),
85
158
  sender: json_body["event_sender_id"],
86
159
  errors: json_body["errors"]&&json_body["errors"].map{|e| GorgService::Message::ErrorLog.parse(e)},
87
- )
160
+ )
88
161
  msg
89
162
  rescue JSON::ParserError => e
90
- raise GorgService::HardfailError.new(e), "Unprocessable message : Unable to parse JSON message body"
163
+ raise GorgService::Consumer::HardfailError.new("Unprocessable message : Unable to parse JSON message body", e)
164
+ rescue JSON::Schema::ValidationError => e
165
+ raise GorgService::Consumer::HardfailError.new("Invalid JSON : This message does not respect Gadz.org JSON Schema",e)
91
166
  end
92
167
  end
93
168
 
@@ -116,7 +191,7 @@ class GorgService
116
191
  GorgService.configuration.application_id
117
192
  end
118
193
 
119
-
194
+
120
195
 
121
196
  end
122
197
  end
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+
5
+ class GorgService
6
+ class Producer
7
+
8
+ attr_accessor :default_exchange
9
+ attr_accessor :environment
10
+
11
+ def initialize(environment_: GorgService.environment ,default_exchange_: nil)
12
+ self.environment=environment_
13
+ self.default_exchange= default_exchange_ || environment.event_exchange
14
+ end
15
+
16
+
17
+ def publish_message(message, exchange: default_exchange)
18
+ x=exchange.is_a?(Bunny::Exchange) ? exchange : environment.find_exchange_by_name(exchange)
19
+ GorgService.logger.info "Publish to #{x.name} - key : #{message.routing_key}"
20
+ GorgService.logger.debug "Message content : #{message.body.to_s[0...10000]}"
21
+
22
+ x.publish(message.to_json, message.properties)
23
+ end
24
+
25
+ end
26
+ end
@@ -1,10 +1,10 @@
1
1
  class GorgService
2
2
  class RabbitmqEnvBuilder
3
3
 
4
- def initialize(conn:nil,main_exchange:"", app_id:"", deferred_time: 10000, listened_routing_keys: [], prefetch: 1)
4
+ def initialize(conn:nil, event_exchange:"", app_id:"", deferred_time: 10000, listened_routing_keys: [], prefetch: 1)
5
5
  @_conn=conn
6
6
  @app_id=app_id
7
- @main_exchange_name=main_exchange
7
+ @event_exchange_name=event_exchange
8
8
  @deferred_time=deferred_time
9
9
  @delayed_queues={}
10
10
  @listened_routing_keys=listened_routing_keys
@@ -15,6 +15,7 @@ class GorgService
15
15
  @_conn.start unless @_conn.connected?
16
16
  @_conn
17
17
  end
18
+ alias_method :connection, :conn
18
19
 
19
20
  def ch
20
21
  unless (@_ch && @_ch.status == :open)
@@ -23,11 +24,21 @@ class GorgService
23
24
  end
24
25
  @_ch
25
26
  end
27
+ alias_method :channel, :ch
26
28
 
27
- def main_exchange
28
- ch.topic(@main_exchange_name, :durable => true)
29
+ def request_exchange
30
+ ch.topic("#{@app_id}.request", :durable => true)
29
31
  end
30
32
 
33
+ def reply_exchange
34
+ ch.topic("#{@app_id}.reply", :durable => true)
35
+ end
36
+
37
+ def event_exchange
38
+ ch.topic(@event_exchange_name, :durable => true)
39
+ end
40
+
41
+
31
42
  def delayed_in_exchange
32
43
  ch.topic("#{@app_id}_delayed_in_x", :durable => true)
33
44
  end
@@ -37,12 +48,14 @@ class GorgService
37
48
  end
38
49
 
39
50
  def job_queue
40
- GorgService.logger.debug @listened_routing_keys
51
+ GorgService.logger.debug "Listened keys :#{@listened_routing_keys}"
41
52
  q=ch.queue("#{@app_id}_job_q", :durable => true)
42
53
  q.bind delayed_out_exchange
43
54
  @listened_routing_keys.each do |rk|
44
- q.bind(main_exchange, :routing_key => rk)
55
+ q.bind(event_exchange, :routing_key => rk)
45
56
  end
57
+ q.bind(reply_exchange, :routing_key => "#")
58
+ q.bind(request_exchange, :routing_key => "#")
46
59
  q
47
60
  end
48
61
 
@@ -50,11 +63,29 @@ class GorgService
50
63
  @delayed_queues[routing_key]||= create_delayed_queue_for(routing_key)
51
64
  end
52
65
 
66
+ def find_exchange_by_name(name, type: 'topic', opts: {})
67
+ begin
68
+ ch.send(type,name,opts)
69
+ rescue Bunny::PreconditionFailed => e
70
+ regex=/PRECONDITION_FAILED - inequivalent arg '(?<arg>.*)' for exchange '(?<exchange>.*)' in vhost '(?<vhost>.*)': received '(?<our>.*)' but current is '(?<their>.*)'/
71
+ match=regex.match(e.message)
72
+
73
+ case match[:arg]
74
+ when "type"
75
+ find_exchange_by_name(name,type: match[:their],opts: opts)
76
+ else
77
+ find_exchange_by_name(name,type: type,opts: opts.merge({ match[:arg].to_sym => match[:their]}))
78
+ end
79
+ end
80
+ end
81
+
53
82
  private
54
83
 
55
84
  def set_logger
56
85
  x=ch.fanout("log", :durable => true)
57
- x.bind(main_exchange, :routing_key => "#")
86
+ x.bind(event_exchange, :routing_key => "#")
87
+ x.bind(reply_exchange, :routing_key => "#")
88
+ x.bind(request_exchange, :routing_key => "#")
58
89
  x.bind(delayed_in_exchange, :routing_key => "#")
59
90
  end
60
91
 
@@ -82,6 +113,4 @@ class GorgService
82
113
  end
83
114
 
84
115
  end
85
- end
86
-
87
- #ch.queue(test, durable: true, arguments: {'x-message-ttl' => 1000,'x-dead-letter-exchange' => "agoram_event_exchange",'x-dead-letter-routing-key' => "test",})
116
+ end
@@ -2,5 +2,5 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  class GorgService
5
- VERSION = "4.1.0"
5
+ VERSION = "5.0.0"
6
6
  end
data/lib/gorg_service.rb CHANGED
@@ -1,83 +1,43 @@
1
- require "gorg_message_sender"
1
+
2
2
  require "gorg_service/configuration"
3
3
  require "gorg_service/version"
4
- require "gorg_service/errors"
5
4
  require "gorg_service/rabbitmq_env_builder"
6
- require "gorg_service/listener"
7
5
  require "gorg_service/message"
8
- require "gorg_service/message_handler"
9
6
 
10
- #Duplicate GorgMessageSender to avoid configuration conflict
11
- RabbitmqProducer=GorgMessageSender.dup
7
+ require "gorg_service/consumer"
8
+ require "gorg_service/producer"
12
9
 
13
10
  class GorgService
14
- def initialize(listener: nil, bunny_session: nil, rabbitmq_env:nil)
15
-
16
- @bunny_session= bunny_session || Bunny.new(
17
- :hostname => GorgService.configuration.rabbitmq_host,
18
- :port => GorgService.configuration.rabbitmq_port,
19
- :user => GorgService.configuration.rabbitmq_user,
20
- :pass => GorgService.configuration.rabbitmq_password,
21
- :vhost => GorgService.configuration.rabbitmq_vhost
11
+ class << self
12
+
13
+ #Connection shared across Consumers and Producers (thread safe)
14
+ def connection
15
+ @bunny_session||=Bunny.new(
16
+ :hostname => GorgService.configuration.rabbitmq_host,
17
+ :port => GorgService.configuration.rabbitmq_port,
18
+ :user => GorgService.configuration.rabbitmq_user,
19
+ :pass => GorgService.configuration.rabbitmq_password,
20
+ :vhost => GorgService.configuration.rabbitmq_vhost
22
21
  )
22
+ @bunny_session.start unless @bunny_session.connected?
23
+ @bunny_session
24
+ end
23
25
 
24
- @env=rabbitmq_env || RabbitmqEnvBuilder.new(
25
- conn: @bunny_session,
26
- main_exchange: GorgService.configuration.rabbitmq_exchange_name,
27
- app_id:GorgService.configuration.application_id,
28
- deferred_time: GorgService.configuration.rabbitmq_deferred_time.to_i,
29
- listened_routing_keys: GorgService.configuration.message_handler_map.keys,
30
- prefetch:GorgService.configuration.prefetch_count,
31
- )
32
-
33
- @listener= listener || Listener.new(
34
- bunny_session: @bunny_session,
35
- message_handler_map:GorgService.configuration.message_handler_map,
36
- env: @env,
37
- max_attempts: GorgService.configuration.rabbitmq_max_attempts.to_i,
38
- log_routing_key: GorgService.configuration.log_routing_key
26
+ #Environment buidler. Don't share across threads since channels are not thread safe
27
+ def environment
28
+ RabbitmqEnvBuilder.new(
29
+ conn: connection,
30
+ event_exchange: GorgService.configuration.rabbitmq_event_exchange_name,
31
+ app_id: GorgService.configuration.application_id,
32
+ deferred_time: GorgService.configuration.rabbitmq_deferred_time.to_i,
33
+ listened_routing_keys: Consumer::MessageRouter.listened_keys,
34
+ prefetch: GorgService.configuration.prefetch_count,
39
35
  )
40
-
41
- RabbitmqProducer.configure do |c|
42
- # Id used to set the event_sender_id
43
- c.application_id = GorgService.configuration.application_id
44
-
45
- # RabbitMQ network and authentification
46
- c.host = GorgService.configuration.rabbitmq_host
47
- c.port = GorgService.configuration.rabbitmq_port
48
- c.vhost = GorgService.configuration.rabbitmq_vhost
49
- c.user = GorgService.configuration.rabbitmq_user
50
- c.password = GorgService.configuration.rabbitmq_password
51
-
52
- # Exchange configuration
53
- c.exchange_name = GorgService.configuration.rabbitmq_exchange_name
54
- c.durable_exchange= true
55
36
  end
56
37
 
57
- end
58
-
59
- def run
60
- begin
61
- self.start
62
- puts " [*] Waiting for messages. To exit press CTRL+C"
63
- loop do
64
- sleep(1)
65
- end
66
- rescue SystemExit, Interrupt => _
67
- self.stop
38
+ def logger
39
+ GorgService.configuration.logger
68
40
  end
69
- end
70
-
71
- def start
72
- @bunny_session.start
73
- @listener.listen
74
- end
75
-
76
- def stop
77
- @bunny_session.close
78
- end
79
41
 
80
- def self.logger
81
- GorgService.configuration.logger
82
42
  end
83
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gorg_service
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexandre Narbonne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-07 00:00:00.000000000 Z
11
+ date: 2017-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -50,14 +50,14 @@ dependencies:
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1'
53
+ version: 1.4.2
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1'
60
+ version: 1.4.2
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: bundler
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -163,12 +163,19 @@ files:
163
163
  - gorg_service.gemspec
164
164
  - lib/gorg_service.rb
165
165
  - lib/gorg_service/configuration.rb
166
- - lib/gorg_service/errors.rb
167
- - lib/gorg_service/listener.rb
166
+ - lib/gorg_service/consumer.rb
167
+ - lib/gorg_service/consumer/errors.rb
168
+ - lib/gorg_service/consumer/listener.rb
169
+ - lib/gorg_service/consumer/message_handler.rb
170
+ - lib/gorg_service/consumer/message_handler/base.rb
171
+ - lib/gorg_service/consumer/message_handler/event_handler.rb
172
+ - lib/gorg_service/consumer/message_handler/reply_handler.rb
173
+ - lib/gorg_service/consumer/message_handler/request_handler.rb
174
+ - lib/gorg_service/consumer/message_router.rb
168
175
  - lib/gorg_service/message.rb
169
176
  - lib/gorg_service/message/error_log.rb
170
177
  - lib/gorg_service/message/json_schema.rb
171
- - lib/gorg_service/message_handler.rb
178
+ - lib/gorg_service/producer.rb
172
179
  - lib/gorg_service/rabbitmq_env_builder.rb
173
180
  - lib/gorg_service/version.rb
174
181
  homepage: https://github.com/Zooip/gorg_service
@@ -1,37 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: utf-8
3
-
4
- class GorgService
5
-
6
- #Common behavior of failling errors
7
- class FailError < StandardError
8
- attr_reader :error_raised
9
-
10
- def initialize(message = nil, error_raised = nil)
11
- @message = message
12
- @error_raised = error_raised
13
- end
14
-
15
- def message
16
- @message
17
- end
18
-
19
- def type
20
- ""
21
- end
22
- end
23
-
24
- #Softfail error : This message should be processed again later
25
- class SoftfailError < FailError
26
- def type
27
- "softerror"
28
- end
29
- end
30
-
31
- #Hardfail error : This message is not processable and will never be
32
- class HardfailError < FailError
33
- def type
34
- "harderror"
35
- end
36
- end
37
- end
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: utf-8
3
-
4
- require "bunny"
5
-
6
- class GorgService
7
- class Listener
8
-
9
- def initialize(bunny_session: nil, env: nil, message_handler_map: {default: DefaultMessageHandler}, max_attempts: 48,log_routing_key:nil)
10
- @message_handler_map=message_handler_map
11
- @max_attempts=max_attempts.to_i
12
- @rmq_connection=bunny_session
13
- @log_routing_key=log_routing_key
14
-
15
- @env=env
16
- end
17
-
18
- def listen
19
- @env.job_queue.subscribe(:manual_ack => true) do |delivery_info, _properties, body|
20
- routing_key=delivery_info[:routing_key]
21
- GorgService.logger.info "Received message with routing key #{routing_key} containing : #{body}"
22
- process_message(body,routing_key)
23
- @env.ch.ack(delivery_info.delivery_tag)
24
- end
25
- end
26
-
27
- protected
28
-
29
- def rmq_connection
30
- @rmq_connection.start unless @rmq_connection.connected?
31
- @rmq_connection
32
- end
33
-
34
- def process_message(body,routing_key)
35
- message=nil
36
- incomming_message_error_count=0
37
- begin
38
- message_handler=message_handler_for routing_key
39
- raise HardfailError.new("Routing error : No message handler finded for this routing key") unless message_handler
40
-
41
- begin
42
- message=Message.parse_body(body)
43
- rescue JSON::ParserError => e
44
- raise HardfailError.new("JSON Parse error : Can't parse incoming message",e)
45
- rescue JSON::Schema::ValidationError => e
46
- raise HardfailError.new("Invalid JSON : This message does not respect Gadz.org JSON Schema",e)
47
- end
48
- incomming_message_error_count=message.errors.count
49
- message_handler.new(message)
50
- process_logging(message) if message.errors.count>incomming_message_error_count
51
- rescue SoftfailError => e
52
- process_softfail(e,message)
53
- rescue HardfailError => e
54
- process_hardfail(e,message)
55
- end
56
- end
57
-
58
- def process_softfail(e,message)
59
- message.log_error(e)
60
- GorgService.logger.error "SOFTFAIL ERROR : #{e.message}"
61
- if message.errors.count.to_i >= @max_attempts
62
- GorgService.logger.info " DISCARD MESSAGE : #{message.errors.count} errors in message log"
63
- process_hardfail(HardfailError.new("Too Much SoftError : This message reached the limit of softerror (max: #{@max_attempts})"),message)
64
- else
65
- send_to_deferred_queue(message)
66
- end
67
- end
68
-
69
- def process_hardfail(e,message)
70
- GorgService.logger.error "HARDFAIL ERROR : #{e.message}"
71
- GorgService.logger.info " DISCARD MESSAGE"
72
- if message
73
- message.log_error(e)
74
- process_logging(message)
75
- end
76
- end
77
-
78
- def process_logging(message)
79
- RabbitmqProducer.new.send_raw(message.to_json,@log_routing_key, verbose: true) if @log_routing_key
80
- end
81
-
82
- def send_to_deferred_queue(msg)
83
- if @env.delayed_queue_for msg.event
84
- @env.delayed_in_exchange.publish(msg.to_json, :routing_key => msg.event)
85
- GorgService.logger.info "DEFER MESSAGE : message sent to #{@env.delayed_in_exchange.name} with routing key #{msg.event}"
86
- else
87
- raise "DelayedQueueNotFound"
88
- end
89
- end
90
-
91
- def message_handler_for routing_key
92
- @message_handler_map.each do |k,mh|
93
- return mh if self.class.amqp_key_to_regex(k).match(routing_key)
94
- end
95
- end
96
-
97
- def self.amqp_key_to_regex(key)
98
- regex_base=key.gsub('.','\.')
99
- .gsub('*','([a-zA-Z0-9\-_:]+)')
100
- .gsub(/(\\\.)?#(\\\.)?/,'((\.)?[a-zA-Z0-9\-_:]*(\.)?)*')
101
-
102
- /^#{regex_base}$/
103
- end
104
-
105
- end
106
- end
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: utf-8
3
-
4
- class GorgService
5
- class MessageHandler
6
-
7
- def initialize(message)
8
- GorgService.logger.warning "WARNING : Defined your MessageHandler behavior in its 'initialize' method"
9
- end
10
-
11
- def raise_hardfail(message, error:nil)
12
- raise HardfailError.new(message, error)
13
- end
14
-
15
- def raise_softfail(message, error:nil)
16
- raise SoftfailError.new(message, error)
17
- end
18
-
19
- end
20
- end