railway-ipc 0.1.6 → 2.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -3
  3. data/CHANGELOG.md +61 -0
  4. data/Gemfile +2 -2
  5. data/README.md +2 -2
  6. data/Rakefile +10 -4
  7. data/bin/console +3 -3
  8. data/bin/rspec +29 -0
  9. data/bin/rubocop +29 -0
  10. data/lib/railway_ipc.rb +9 -11
  11. data/lib/railway_ipc/Rakefile +2 -0
  12. data/lib/railway_ipc/consumer/consumer.rb +33 -73
  13. data/lib/railway_ipc/consumer/process_incoming_message.rb +111 -0
  14. data/lib/railway_ipc/errors.rb +9 -1
  15. data/lib/railway_ipc/handler.rb +17 -3
  16. data/lib/railway_ipc/handler_store.rb +5 -2
  17. data/lib/railway_ipc/incoming_message.rb +51 -0
  18. data/lib/railway_ipc/logger.rb +32 -26
  19. data/lib/railway_ipc/models/consumed_message.rb +40 -35
  20. data/lib/railway_ipc/models/published_message.rb +11 -9
  21. data/lib/railway_ipc/publisher.rb +77 -3
  22. data/lib/railway_ipc/rabbitmq/adapter.rb +23 -15
  23. data/lib/railway_ipc/rabbitmq/payload.rb +9 -4
  24. data/lib/railway_ipc/railtie.rb +2 -0
  25. data/lib/railway_ipc/responder.rb +10 -3
  26. data/lib/railway_ipc/response.rb +4 -1
  27. data/lib/railway_ipc/rpc/client/client.rb +43 -18
  28. data/lib/railway_ipc/rpc/client/client_response_handlers.rb +2 -0
  29. data/lib/railway_ipc/rpc/client/errors/timeout_error.rb +2 -0
  30. data/lib/railway_ipc/rpc/concerns/error_adapter_configurable.rb +2 -0
  31. data/lib/railway_ipc/rpc/concerns/message_observation_configurable.rb +2 -0
  32. data/lib/railway_ipc/rpc/concerns/publish_location_configurable.rb +2 -0
  33. data/lib/railway_ipc/rpc/rpc.rb +2 -0
  34. data/lib/railway_ipc/rpc/server/server.rb +25 -7
  35. data/lib/railway_ipc/rpc/server/server_response_handlers.rb +2 -0
  36. data/lib/railway_ipc/tasks/generate_migrations.rake +16 -16
  37. data/lib/railway_ipc/tasks/start_consumers.rake +3 -1
  38. data/lib/railway_ipc/tasks/start_servers.rake +3 -1
  39. data/lib/railway_ipc/unhandled_message_error.rb +2 -0
  40. data/lib/railway_ipc/unknown_message.pb.rb +18 -0
  41. data/lib/railway_ipc/version.rb +3 -1
  42. data/priv/migrations/add_railway_ipc_consumed_messages.rb +5 -3
  43. data/priv/migrations/add_railway_ipc_published_messages.rb +3 -1
  44. data/railway_ipc.gemspec +34 -30
  45. metadata +65 -66
  46. data/.rspec +0 -3
  47. data/.travis.yml +0 -7
  48. data/Gemfile.lock +0 -186
  49. data/lib/railway_ipc/base_message.pb.rb +0 -21
  50. data/lib/railway_ipc/consumer/consumer_response_handlers.rb +0 -14
  51. data/lib/railway_ipc/handler_manifest.rb +0 -10
  52. data/lib/railway_ipc/null_handler.rb +0 -7
  53. data/lib/railway_ipc/null_message.rb +0 -7
@@ -1 +1,9 @@
1
- class RailwayIpc::InvalidProtobuf < StandardError; end
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/ClassAndModuleChildren
4
+ class RailwayIpc::Error < StandardError; end
5
+ class RailwayIpc::InvalidProtobuf < RailwayIpc::Error; end
6
+ class RailwayIpc::FailedToStoreOutgoingMessage < RailwayIpc::Error; end
7
+ class RailwayIpc::IncomingMessage::ParserError < RailwayIpc::Error; end
8
+ class RailwayIpc::IncomingMessage::InvalidMessage < RailwayIpc::Error; end
9
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  class Handler
3
5
  class << self
@@ -9,15 +11,27 @@ module RailwayIpc
9
11
  end
10
12
 
11
13
  def handle(message)
12
- RailwayIpc.logger.info(message, 'Handling message')
14
+ RailwayIpc.logger.info('Handling message', log_message_options(message))
13
15
  response = self.class.block.call(message)
14
16
  if response.success?
15
- RailwayIpc.logger.info(message, 'Successfully handled message')
17
+ RailwayIpc.logger.info('Successfully handled message', log_message_options(message))
16
18
  else
17
- RailwayIpc.logger.error(message, 'Failed to handle message')
19
+ RailwayIpc.logger.error('Failed to handle message', log_message_options(message))
18
20
  end
19
21
 
20
22
  response
21
23
  end
24
+
25
+ private
26
+
27
+ def log_message_options(message)
28
+ {
29
+ feature: 'railway_ipc_consumer',
30
+ protobuf: {
31
+ type: message.class.name,
32
+ data: message
33
+ }
34
+ }
35
+ end
22
36
  end
23
37
  end
@@ -1,7 +1,10 @@
1
- require 'railway_ipc/handler_manifest'
1
+ # frozen_string_literal: true
2
+
2
3
  module RailwayIpc
4
+ HandlerManifest = Struct.new(:message, :handler)
3
5
  class HandlerStore
4
6
  attr_reader :handler_map
7
+
5
8
  def initialize
6
9
  @handler_map = {}
7
10
  end
@@ -11,7 +14,7 @@ module RailwayIpc
11
14
  end
12
15
 
13
16
  def register(message:, handler:)
14
- handler_map[message.to_s] = HandlerManifest.new(message: message, handler: handler)
17
+ handler_map[message.to_s] = HandlerManifest.new(message, handler)
15
18
  end
16
19
 
17
20
  def get(response_message)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailwayIpc
4
+ class IncomingMessage
5
+ attr_reader :type, :payload, :parsed_payload, :errors
6
+
7
+ def initialize(payload)
8
+ @parsed_payload = JSON.parse(payload)
9
+ @type = parsed_payload['type']
10
+ @payload = payload
11
+ @errors = {}
12
+ rescue JSON::ParserError => e
13
+ raise RailwayIpc::IncomingMessage::ParserError.new(e)
14
+ end
15
+
16
+ def uuid
17
+ decoded.uuid
18
+ end
19
+
20
+ def user_uuid
21
+ decoded.user_uuid
22
+ end
23
+
24
+ def correlation_id
25
+ decoded.correlation_id
26
+ end
27
+
28
+ def valid?
29
+ errors[:uuid] = 'uuid is required' unless uuid.present?
30
+ errors[:correlation_id] = 'correlation_id is required' unless correlation_id.present?
31
+ errors.none?
32
+ end
33
+
34
+ def decoded
35
+ @decoded ||=
36
+ begin
37
+ protobuf_msg = Base64.decode64(parsed_payload['encoded_message'])
38
+ decoder = Kernel.const_get(type)
39
+ decoder.decode(protobuf_msg)
40
+ rescue Google::Protobuf::ParseError => e
41
+ raise RailwayIpc::IncomingMessage::ParserError.new(e)
42
+ rescue NameError
43
+ RailwayIpc::Messages::Unknown.decode(protobuf_msg)
44
+ end
45
+ end
46
+
47
+ def stringify_errors
48
+ errors.values.join(', ')
49
+ end
50
+ end
51
+ end
@@ -1,35 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
4
+ # Custom logger that accepts a `device`, `level`, and `formatter`.
5
+ # `formatter` can be any object that responds to `call`; a
6
+ # `Logger::Formatter` is used if the argument is not provided.
7
+ #
8
+ # Here is an example formatter that uses `Oj` to format structured log
9
+ # messages:
10
+ #
11
+ # require 'oj'
12
+ # OjFormatter = proc do |severity, datetime, progname, data|
13
+ # data.merge!(
14
+ # name: progname,
15
+ # timestamp: datetime,
16
+ # severity: severity
17
+ # )
18
+ # Oj.dump(data, { mode: :compat, time_format: :xmlschema })
19
+ # end
20
+ #
21
+ # logger = RailwayIpc::Logger.new(STDOUT, Logger::INFO, OjFormatter)
22
+ #
2
23
  class Logger
3
-
4
- attr_reader :logger
5
-
6
- def initialize(logger)
7
- @logger = logger
24
+ def initialize(device=STDOUT, level=::Logger::INFO, formatter=nil)
25
+ @logger = ::Logger.new(device)
26
+ @logger.level = level
27
+ @logger.formatter = formatter if formatter
8
28
  end
9
29
 
10
- def info(message, statement)
11
- logger.info("[#{message_header(message)}] #{statement}")
30
+ %w[fatal error warn info debug].each do |level|
31
+ define_method(level) do |message, data={}|
32
+ data.merge!(feature: 'railway_ipc') unless data.key?(:feature)
33
+ logger.send(level, data.merge(message: message))
34
+ end
12
35
  end
13
36
 
14
- def warn(message, statement)
15
- logger.warn("[#{message_header(message)}] #{statement}")
16
- end
17
-
18
- def debug(message, statement)
19
- logger.debug("[#{message_header(message)}] #{statement}")
20
- end
37
+ private
21
38
 
22
- def error(message, statement)
23
- logger.error("[#{message_header(message)}] #{statement}")
24
- end
25
-
26
- def log_exception(e)
27
- logger.error(e)
28
- end
29
-
30
- def message_header(message)
31
- log_statement = "message type: #{message.class}, uuid: #{message.uuid}, correlation_id: #{message.correlation_id}"
32
- message.respond_to?(:user_uuid) ? "#{log_statement}, user_uuid: #{message.user_uuid}" : log_statement
33
- end
39
+ attr_reader :logger
34
40
  end
35
41
  end
@@ -1,37 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  class ConsumedMessage < ActiveRecord::Base
3
- STATUSES = {
4
- success: 'success',
5
- processing: 'processing',
6
- unknown_message_type: 'unknown_message_type',
7
- failed_to_process: 'failed_to_process'
8
- }
5
+ STATUS_SUCCESS = 'success'
6
+ STATUS_PROCESSING = 'processing'
7
+ STATUS_IGNORED = 'ignored'
8
+ STATUS_UNKNOWN_MESSAGE_TYPE = 'unknown_message_type'
9
+ STATUS_FAILED_TO_PROCESS = 'failed_to_process'
10
+
11
+ VALID_STATUSES = [
12
+ STATUS_SUCCESS,
13
+ STATUS_PROCESSING,
14
+ STATUS_IGNORED,
15
+ STATUS_UNKNOWN_MESSAGE_TYPE,
16
+ STATUS_FAILED_TO_PROCESS
17
+ ].freeze
9
18
 
10
19
  attr_reader :decoded_message
20
+
11
21
  self.table_name = 'railway_ipc_consumed_messages'
12
- self.primary_key = 'uuid'
13
22
 
14
23
  validates :uuid, :status, presence: true
15
- validates :status, inclusion: { in: STATUSES.values }
24
+ validates :status, inclusion: { in: VALID_STATUSES }
25
+
26
+ def self.create_processing(consumer, incoming_message)
27
+ # rubocop:disable Style/RedundantSelf
28
+ self.create!(
29
+ uuid: incoming_message.uuid,
30
+ status: STATUS_PROCESSING,
31
+ message_type: incoming_message.type,
32
+ user_uuid: incoming_message.user_uuid,
33
+ correlation_id: incoming_message.correlation_id,
34
+ queue: consumer.queue_name,
35
+ exchange: consumer.exchange_name,
36
+ encoded_message: incoming_message.payload
37
+ )
38
+ # rubocop:enable Style/RedundantSelf
39
+ end
16
40
 
17
- def self.response_to_status(response)
18
- if response.success?
19
- STATUSES[:success]
20
- else
21
- STATUSES[:failed_to_process]
41
+ def update_with_lock(job)
42
+ with_lock('FOR UPDATE NOWAIT') do
43
+ job.run
44
+ self.status = job.status
45
+ save
22
46
  end
23
47
  end
24
48
 
25
49
  def processed?
26
- self.status == STATUSES[:success]
27
- end
28
-
29
- def encoded_protobuf=(encoded_protobuf)
30
- self.encoded_message = Base64.encode64(encoded_protobuf)
31
- end
32
-
33
- def decoded_message
34
- @decoded_message ||= decode_message
50
+ # rubocop:disable Style/RedundantSelf
51
+ self.status == STATUS_SUCCESS
52
+ # rubocop:enable Style/RedundantSelf
35
53
  end
36
54
 
37
55
  private
@@ -39,18 +57,5 @@ module RailwayIpc
39
57
  def timestamp_attributes_for_create
40
58
  super << :inserted_at
41
59
  end
42
-
43
- def decode_message
44
- begin
45
- message_class = Kernel.const_get(self.message_type)
46
- rescue NameError
47
- message_class = RailwayIpc::BaseMessage
48
- end
49
- message_class.decode(decoded_protobuf)
50
- end
51
-
52
- def decoded_protobuf
53
- Base64.decode64(self.encoded_message)
54
- end
55
60
  end
56
61
  end
@@ -1,20 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  class PublishedMessage < ActiveRecord::Base
3
5
  self.table_name = 'railway_ipc_published_messages'
4
- self.primary_key = "uuid"
6
+ self.primary_key = 'uuid'
5
7
 
6
8
  validates :uuid, :status, presence: true
7
9
 
8
10
  def self.store_message(exchange_name, message)
9
11
  encoded_message = RailwayIpc::Rabbitmq::Payload.encode(message)
10
- self.create(
11
- uuid: message.uuid,
12
- message_type: message.class.to_s,
13
- user_uuid: message.user_uuid,
14
- correlation_id: message.correlation_id,
15
- encoded_message: encoded_message,
16
- status: "sent",
17
- exchange: exchange_name
12
+ create!(
13
+ uuid: message.uuid,
14
+ message_type: message.class.to_s,
15
+ user_uuid: message.user_uuid,
16
+ correlation_id: message.correlation_id,
17
+ encoded_message: encoded_message,
18
+ status: 'sent',
19
+ exchange: exchange_name
18
20
  )
19
21
  end
20
22
 
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'singleton'
2
4
 
3
5
  module RailwayIpc
4
- class Publisher < Sneakers::Publisher
6
+ class SingletonPublisher < Sneakers::Publisher
5
7
  include ::Singleton
6
8
 
7
9
  def self.exchange(exchange)
@@ -10,6 +12,7 @@ module RailwayIpc
10
12
 
11
13
  def self.exchange_name
12
14
  raise 'Subclass must set the exchange' unless @exchange_name
15
+
13
16
  @exchange_name
14
17
  end
15
18
 
@@ -18,13 +21,84 @@ module RailwayIpc
18
21
  end
19
22
 
20
23
  def publish(message, published_message_store=RailwayIpc::PublishedMessage)
21
- RailwayIpc.logger.info(message, 'Publishing message')
24
+ RailwayIpc.logger.warn('DEPRECATED: Use new PublisherInstance class', log_message_options)
25
+ ensure_message_uuid(message)
26
+ ensure_correlation_id(message)
27
+ RailwayIpc.logger.info('Publishing message', log_message_options(message))
22
28
  result = super(RailwayIpc::Rabbitmq::Payload.encode(message))
23
29
  published_message_store.store_message(self.class.exchange_name, message)
24
30
  result
25
31
  rescue RailwayIpc::InvalidProtobuf => e
26
- RailwayIpc.logger.error(message, 'Invalid protobuf')
32
+ RailwayIpc.logger.error('Invalid protobuf', log_message_options(message))
33
+ raise e
34
+ end
35
+
36
+ private
37
+
38
+ def ensure_message_uuid(message)
39
+ message.uuid = SecureRandom.uuid if message.uuid.blank?
40
+ message
41
+ end
42
+
43
+ def ensure_correlation_id(message)
44
+ message.correlation_id = SecureRandom.uuid if message.correlation_id.blank?
45
+ message
46
+ end
47
+
48
+ def log_message_options(message=nil)
49
+ options = { feature: 'railway_ipc_publisher', exchange: self.class.exchange_name }
50
+ message.nil? ? options : options.merge(protobuf: { type: message.class, data: message })
51
+ end
52
+ end
53
+ end
54
+
55
+ module RailwayIpc
56
+ class Publisher < Sneakers::Publisher
57
+ attr_reader :exchange_name, :message_store
58
+
59
+ def initialize(opts={})
60
+ @exchange_name = opts.fetch(:exchange_name)
61
+ @message_store = opts.fetch(:message_store, RailwayIpc::PublishedMessage)
62
+ connection = opts.fetch(:connection, nil)
63
+ options = {
64
+ exchange: exchange_name,
65
+ connection: connection,
66
+ exchange_type: :fanout
67
+ }.compact
68
+ super(options)
69
+ end
70
+
71
+ # rubocop:disable Metrics/AbcSize
72
+ def publish(message)
73
+ message.uuid = SecureRandom.uuid if message.uuid.blank?
74
+ message.correlation_id = SecureRandom.uuid if message.correlation_id.blank?
75
+ RailwayIpc.logger.info('Publishing message', log_message_options(message))
76
+
77
+ stored_message = message_store.store_message(exchange_name, message)
78
+ super(RailwayIpc::Rabbitmq::Payload.encode(message))
79
+ rescue RailwayIpc::InvalidProtobuf => e
80
+ RailwayIpc.logger.error('Invalid protobuf', log_message_options(message))
81
+ raise e
82
+ rescue ActiveRecord::RecordInvalid => e
83
+ RailwayIpc.logger.error('Failed to store outgoing message', log_message_options(message))
84
+ raise RailwayIpc::FailedToStoreOutgoingMessage.new(e)
85
+ rescue StandardError => e
86
+ stored_message&.destroy
27
87
  raise e
28
88
  end
89
+ # rubocop:enable Metrics/AbcSize
90
+
91
+ private
92
+
93
+ def log_message_options(message)
94
+ {
95
+ feature: 'railway_ipc_publisher',
96
+ exchange: exchange_name,
97
+ protobuf: {
98
+ type: message.class,
99
+ data: message
100
+ }
101
+ }
102
+ end
29
103
  end
30
104
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  module Rabbitmq
3
5
  class Adapter
4
- class TimeoutError < StandardError;
5
- end
6
+ class TimeoutError < StandardError; end
6
7
  extend Forwardable
7
8
  attr_reader :connection, :exchange, :exchange_name, :queue, :queue_name, :channel
9
+
8
10
  def_delegators :connection,
9
11
  :automatically_recover?,
10
12
  :connected?,
@@ -14,24 +16,26 @@ module RailwayIpc
14
16
  :port,
15
17
  :user
16
18
 
17
- def initialize(amqp_url: ENV["RAILWAY_RABBITMQ_CONNECTION_URL"], exchange_name:, queue_name: '', options: {})
19
+ def initialize(amqp_url: ENV['RAILWAY_RABBITMQ_CONNECTION_URL'], exchange_name:, queue_name: '', options: {})
18
20
  @queue_name = queue_name
19
21
  @exchange_name = exchange_name
20
22
  settings = AMQ::Settings.parse_amqp_url(amqp_url)
21
23
  vhost = settings[:vhost] || '/'
22
24
  @connection = Bunny.new({
23
- host: settings[:host],
24
- user: settings[:user],
25
- pass: settings[:pass],
26
- port: settings[:port],
27
- vhost: vhost,
28
- automatic_recovery: false,
29
- logger: RailwayIpc.bunny_logger
30
- }.merge(options))
25
+ host: settings[:host],
26
+ user: settings[:user],
27
+ pass: settings[:pass],
28
+ port: settings[:port],
29
+ vhost: vhost,
30
+ automatic_recovery: false,
31
+ logger: RailwayIpc.logger
32
+ }.merge(options))
31
33
  end
32
34
 
33
- def publish(message, options = {})
35
+ def publish(message, options={})
36
+ # rubocop:disable Style/SafeNavigation
34
37
  exchange.publish(message, options) if exchange
38
+ # rubocop:enable Style/SafeNavigation
35
39
  end
36
40
 
37
41
  def reply(message, from)
@@ -66,17 +70,19 @@ module RailwayIpc
66
70
  self
67
71
  end
68
72
 
69
- def create_exchange(strategy: :fanout, options: {durable: true})
70
- @exchange = Bunny::Exchange.new(connection.channel, :fanout, exchange_name, options)
73
+ def create_exchange(strategy: :fanout, options: { durable: true })
74
+ @exchange = Bunny::Exchange.new(connection.channel, strategy, exchange_name, options)
71
75
  self
72
76
  end
73
77
 
74
78
  def delete_exchange
79
+ # rubocop:disable Style/SafeNavigation
75
80
  exchange.delete if exchange
81
+ # rubocop:enable Style/SafeNavigation
76
82
  self
77
83
  end
78
84
 
79
- def create_queue(options = {durable: true})
85
+ def create_queue(options={ durable: true })
80
86
  @queue = @channel.queue(queue_name, options)
81
87
  self
82
88
  end
@@ -87,7 +93,9 @@ module RailwayIpc
87
93
  end
88
94
 
89
95
  def delete_queue
96
+ # rubocop:disable Style/SafeNavigation
90
97
  queue.delete if queue
98
+ # rubocop:enable Style/SafeNavigation
91
99
  self
92
100
  end
93
101
  end