railway-ipc 0.1.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -3
  3. data/CHANGELOG.md +50 -0
  4. data/Gemfile +2 -2
  5. data/README.md +1 -1
  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 +6 -4
  11. data/lib/railway_ipc/Rakefile +2 -0
  12. data/lib/railway_ipc/consumer/consumer.rb +31 -83
  13. data/lib/railway_ipc/consumer/process_incoming_message.rb +103 -0
  14. data/lib/railway_ipc/errors.rb +9 -1
  15. data/lib/railway_ipc/handler.rb +5 -6
  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 +4 -3
  19. data/lib/railway_ipc/models/consumed_message.rb +41 -28
  20. data/lib/railway_ipc/models/published_message.rb +11 -9
  21. data/lib/railway_ipc/publisher.rb +58 -1
  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 +6 -3
  26. data/lib/railway_ipc/response.rb +4 -1
  27. data/lib/railway_ipc/rpc/client/client.rb +27 -17
  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 +8 -3
  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 +62 -64
  46. data/.rspec +0 -3
  47. data/.tool-versions +0 -1
  48. data/.travis.yml +0 -7
  49. data/Gemfile.lock +0 -186
  50. data/lib/railway_ipc/base_message.pb.rb +0 -21
  51. data/lib/railway_ipc/consumer/consumer_response_handlers.rb +0 -14
  52. data/lib/railway_ipc/handler_manifest.rb +0 -10
  53. data/lib/railway_ipc/null_handler.rb +0 -7
  54. 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,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  class Handler
3
- include Sneakers::Worker
4
5
  class << self
5
6
  attr_reader :block
6
7
  end
@@ -10,14 +11,12 @@ module RailwayIpc
10
11
  end
11
12
 
12
13
  def handle(message)
13
- RailwayIpc.logger.info(message, "Handling message")
14
+ RailwayIpc.logger.info(message, 'Handling message')
14
15
  response = self.class.block.call(message)
15
16
  if response.success?
16
- RailwayIpc.logger.info(message, "Successfully handled message")
17
- ack!
17
+ RailwayIpc.logger.info(message, 'Successfully handled message')
18
18
  else
19
- RailwayIpc.logger.error(message, "Failed to handle message")
20
- ack!
19
+ RailwayIpc.logger.error(message, 'Failed to handle message')
21
20
  end
22
21
 
23
22
  response
@@ -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,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  class Logger
3
-
4
5
  attr_reader :logger
5
6
 
6
7
  def initialize(logger)
@@ -23,8 +24,8 @@ module RailwayIpc
23
24
  logger.error("[#{message_header(message)}] #{statement}")
24
25
  end
25
26
 
26
- def log_exception(e)
27
- logger.error(e)
27
+ def log_exception(exception)
28
+ logger.error(exception)
28
29
  end
29
30
 
30
31
  def message_header(message)
@@ -1,29 +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 }
16
-
17
- def processed?
18
- self.status == STATUSES[:success]
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
19
39
  end
20
40
 
21
- def encoded_protobuf=(encoded_protobuf)
22
- self.encoded_message = Base64.encode64(encoded_protobuf)
41
+ def update_with_lock(job)
42
+ with_lock('FOR UPDATE NOWAIT') do
43
+ job.run
44
+ self.status = job.status
45
+ save
46
+ end
23
47
  end
24
48
 
25
- def decoded_message
26
- @decoded_message ||= decode_message
49
+ def processed?
50
+ # rubocop:disable Style/RedundantSelf
51
+ self.status == STATUS_SUCCESS
52
+ # rubocop:enable Style/RedundantSelf
27
53
  end
28
54
 
29
55
  private
@@ -31,18 +57,5 @@ module RailwayIpc
31
57
  def timestamp_attributes_for_create
32
58
  super << :inserted_at
33
59
  end
34
-
35
- def decode_message
36
- begin
37
- message_class = Kernel.const_get(self.message_type)
38
- rescue NameError
39
- message_class = RailwayIpc::BaseMessage
40
- end
41
- message_class.decode(decoded_protobuf)
42
- end
43
-
44
- def decoded_protobuf
45
- Base64.decode64(self.encoded_message)
46
- end
47
60
  end
48
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,7 +21,11 @@ module RailwayIpc
18
21
  end
19
22
 
20
23
  def publish(message, published_message_store=RailwayIpc::PublishedMessage)
24
+ RailwayIpc.logger.logger.warn('DEPRECATED: Use new PublisherInstance class')
25
+ ensure_message_uuid(message)
26
+ ensure_correlation_id(message)
21
27
  RailwayIpc.logger.info(message, 'Publishing message')
28
+
22
29
  result = super(RailwayIpc::Rabbitmq::Payload.encode(message))
23
30
  published_message_store.store_message(self.class.exchange_name, message)
24
31
  result
@@ -26,5 +33,55 @@ module RailwayIpc
26
33
  RailwayIpc.logger.error(message, 'Invalid protobuf')
27
34
  raise e
28
35
  end
36
+
37
+ private
38
+
39
+ def ensure_message_uuid(message)
40
+ message.uuid = SecureRandom.uuid if message.uuid.blank?
41
+ message
42
+ end
43
+
44
+ def ensure_correlation_id(message)
45
+ message.correlation_id = SecureRandom.uuid if message.correlation_id.blank?
46
+ message
47
+ end
48
+ end
49
+ end
50
+
51
+ module RailwayIpc
52
+ class Publisher < Sneakers::Publisher
53
+ attr_reader :exchange_name, :message_store
54
+
55
+ def initialize(opts={})
56
+ @exchange_name = opts.fetch(:exchange_name)
57
+ @message_store = opts.fetch(:message_store, RailwayIpc::PublishedMessage)
58
+ connection = opts.fetch(:connection, nil)
59
+ options = {
60
+ exchange: exchange_name,
61
+ connection: connection,
62
+ exchange_type: :fanout
63
+ }.compact
64
+ super(options)
65
+ end
66
+
67
+ # rubocop:disable Metrics/AbcSize
68
+ def publish(message)
69
+ message.uuid = SecureRandom.uuid if message.uuid.blank?
70
+ message.correlation_id = SecureRandom.uuid if message.correlation_id.blank?
71
+ RailwayIpc.logger.info(message, 'Publishing message')
72
+
73
+ stored_message = message_store.store_message(exchange_name, message)
74
+ super(RailwayIpc::Rabbitmq::Payload.encode(message))
75
+ rescue RailwayIpc::InvalidProtobuf => e
76
+ RailwayIpc.logger.error(message, 'Invalid protobuf')
77
+ raise e
78
+ rescue ActiveRecord::RecordInvalid => e
79
+ RailwayIpc.logger.error(message, 'Failed to store outgoing message')
80
+ raise RailwayIpc::FailedToStoreOutgoingMessage.new(e)
81
+ rescue StandardError => e
82
+ stored_message&.destroy
83
+ raise e
84
+ end
85
+ # rubocop:enable Metrics/AbcSize
29
86
  end
30
87
  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.bunny_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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RailwayIpc
2
4
  module Rabbitmq
3
5
  class Payload
@@ -7,6 +9,7 @@ module RailwayIpc
7
9
  type = message.class.to_s
8
10
  begin
9
11
  message = Base64.encode64(message.class.encode(message))
12
+ # TODO: also need to rescue Google::Protobuf::TypeError
10
13
  rescue NoMethodError
11
14
  raise RailwayIpc::InvalidProtobuf.new("Message #{message} is not a valid protobuf")
12
15
  end
@@ -15,8 +18,8 @@ module RailwayIpc
15
18
 
16
19
  def self.decode(message)
17
20
  message = JSON.parse(message)
18
- type = message["type"]
19
- message = Base64.decode64(message["encoded_message"])
21
+ type = message['type']
22
+ message = Base64.decode64(message['encoded_message'])
20
23
  new(type, message)
21
24
  end
22
25
 
@@ -25,12 +28,14 @@ module RailwayIpc
25
28
  @message = message
26
29
  end
27
30
 
31
+ # rubocop:disable Lint/ToJSON
28
32
  def to_json
29
33
  {
30
- type: type,
31
- encoded_message: message
34
+ type: type,
35
+ encoded_message: message
32
36
  }.to_json
33
37
  end
38
+ # rubocop:enable Lint/ToJSON
34
39
  end
35
40
  end
36
41
  end