gorg_service 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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