railway-ipc 0.1.3 → 0.1.4

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.tool-versions +1 -0
  4. data/Gemfile.lock +125 -1
  5. data/README.md +5 -0
  6. data/lib/railway_ipc.rb +28 -21
  7. data/lib/railway_ipc/base_message.pb.rb +21 -0
  8. data/lib/railway_ipc/consumer/consumer.rb +112 -0
  9. data/lib/railway_ipc/consumer/consumer_response_handlers.rb +14 -0
  10. data/lib/railway_ipc/errors.rb +1 -0
  11. data/lib/railway_ipc/handler.rb +2 -0
  12. data/lib/railway_ipc/handler_manifest.rb +10 -0
  13. data/lib/railway_ipc/handler_store.rb +21 -0
  14. data/lib/railway_ipc/models/consumed_message.rb +48 -0
  15. data/lib/railway_ipc/models/published_message.rb +27 -0
  16. data/lib/railway_ipc/null_message.rb +1 -1
  17. data/lib/railway_ipc/publisher.rb +9 -4
  18. data/lib/railway_ipc/rabbitmq/adapter.rb +93 -0
  19. data/lib/railway_ipc/rabbitmq/payload.rb +7 -3
  20. data/lib/railway_ipc/rpc/client/client.rb +104 -0
  21. data/lib/railway_ipc/rpc/client/client_response_handlers.rb +25 -0
  22. data/lib/railway_ipc/rpc/client/errors/timeout_error.rb +5 -0
  23. data/lib/railway_ipc/rpc/concerns/error_adapter_configurable.rb +13 -0
  24. data/lib/railway_ipc/rpc/concerns/message_observation_configurable.rb +18 -0
  25. data/lib/railway_ipc/rpc/concerns/publish_location_configurable.rb +13 -0
  26. data/lib/railway_ipc/rpc/rpc.rb +2 -0
  27. data/lib/railway_ipc/rpc/server/server.rb +89 -0
  28. data/lib/railway_ipc/rpc/server/server_response_handlers.rb +17 -0
  29. data/lib/railway_ipc/tasks/generate_migrations.rake +26 -0
  30. data/lib/railway_ipc/version.rb +1 -1
  31. data/priv/migrations/add_railway_ipc_consumed_messages.rb +19 -0
  32. data/priv/migrations/add_railway_ipc_published_messages.rb +18 -0
  33. data/railway_ipc.gemspec +25 -16
  34. metadata +123 -8
  35. data/lib/railway_ipc/client.rb +0 -87
  36. data/lib/railway_ipc/concerns/message_handling.rb +0 -118
  37. data/lib/railway_ipc/consumer.rb +0 -26
  38. data/lib/railway_ipc/rabbitmq/connection.rb +0 -55
  39. data/lib/railway_ipc/server.rb +0 -50
@@ -0,0 +1,10 @@
1
+ module RailwayIpc
2
+ class HandlerManifest
3
+ attr_reader :message, :handler
4
+
5
+ def initialize(message:, handler:)
6
+ @message = message
7
+ @handler = handler
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ require 'railway_ipc/handler_manifest'
2
+ module RailwayIpc
3
+ class HandlerStore
4
+ attr_reader :handler_map
5
+ def initialize
6
+ @handler_map = {}
7
+ end
8
+
9
+ def registered
10
+ handler_map.keys
11
+ end
12
+
13
+ def register(message:, handler:)
14
+ handler_map[message.to_s] = HandlerManifest.new(message: message, handler: handler)
15
+ end
16
+
17
+ def get(response_message)
18
+ handler_map[response_message]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ module RailwayIpc
2
+ 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
+ }
9
+
10
+ attr_reader :decoded_message
11
+ self.table_name = 'railway_ipc_consumed_messages'
12
+ self.primary_key = 'uuid'
13
+
14
+ validates :uuid, :status, presence: true
15
+ validates :status, inclusion: { in: STATUSES.values }
16
+
17
+ def processed?
18
+ self.status == STATUSES[:success]
19
+ end
20
+
21
+ def encoded_protobuf=(encoded_protobuf)
22
+ self.encoded_message = Base64.encode64(encoded_protobuf)
23
+ end
24
+
25
+ def decoded_message
26
+ @decoded_message ||= decode_message
27
+ end
28
+
29
+ private
30
+
31
+ def timestamp_attributes_for_create
32
+ super << :inserted_at
33
+ 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
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ module RailwayIpc
2
+ class PublishedMessage < ActiveRecord::Base
3
+ self.table_name = 'railway_ipc_published_messages'
4
+ self.primary_key = "uuid"
5
+
6
+ validates :uuid, :status, presence: true
7
+
8
+ def self.store_message(exchange_name, message)
9
+ 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
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def timestamp_attributes_for_create
24
+ super << :inserted_at
25
+ end
26
+ end
27
+ end
@@ -1,6 +1,6 @@
1
1
  module RailwayIpc
2
2
  class NullMessage
3
- def self.decode(message)
3
+ def self.decode(_message)
4
4
  self.new
5
5
  end
6
6
  end
@@ -1,4 +1,5 @@
1
1
  require 'singleton'
2
+
2
3
  module RailwayIpc
3
4
  class Publisher < Sneakers::Publisher
4
5
  include ::Singleton
@@ -12,14 +13,18 @@ module RailwayIpc
12
13
  @exchange_name
13
14
  end
14
15
 
15
-
16
16
  def initialize
17
17
  super(exchange: self.class.exchange_name, exchange_type: :fanout)
18
18
  end
19
19
 
20
- def publish(message)
21
- RailwayIpc.logger.info(message, "Publishing message")
22
- super(RailwayIpc::Rabbitmq::Payload.encode(message))
20
+ def publish(message, published_message_store=RailwayIpc::PublishedMessage)
21
+ RailwayIpc.logger.info(message, 'Publishing message')
22
+ result = super(RailwayIpc::Rabbitmq::Payload.encode(message))
23
+ published_message_store.store_message(self.class.exchange_name, message)
24
+ result
25
+ rescue RailwayIpc::InvalidProtobuf => e
26
+ RailwayIpc.logger.error(message, 'Invalid protobuf')
27
+ raise e
23
28
  end
24
29
  end
25
30
  end
@@ -0,0 +1,93 @@
1
+ module RailwayIpc
2
+ module Rabbitmq
3
+ class Adapter
4
+ class TimeoutError < StandardError;
5
+ end
6
+ extend Forwardable
7
+ attr_reader :connection, :exchange, :exchange_name, :queue, :queue_name, :channel
8
+ def_delegators :connection,
9
+ :automatically_recover?,
10
+ :connected?,
11
+ :host,
12
+ :logger,
13
+ :pass,
14
+ :port,
15
+ :user
16
+
17
+ def initialize(amqp_url: ENV["RAILWAY_RABBITMQ_CONNECTION_URL"], exchange_name:, queue_name: '', options: {})
18
+ @queue_name = queue_name
19
+ @exchange_name = exchange_name
20
+ settings = AMQ::Settings.parse_amqp_url(amqp_url)
21
+ @connection = Bunny.new({
22
+ host: settings[:host],
23
+ user: settings[:user],
24
+ pass: settings[:pass],
25
+ port: settings[:port],
26
+ automatic_recovery: false,
27
+ logger: RailwayIpc.bunny_logger}.merge(options)
28
+ )
29
+ end
30
+
31
+ def publish(message, options = {})
32
+ exchange.publish(message, options) if exchange
33
+ end
34
+
35
+ def reply(message, from)
36
+ channel.default_exchange.publish(message, routing_key: from)
37
+ end
38
+
39
+ def subscribe(&block)
40
+ queue.subscribe(&block)
41
+ end
42
+
43
+ def check_for_message(timeout: 10, time_elapsed: 0, &block)
44
+ raise TimeoutError.new if time_elapsed >= timeout
45
+
46
+ block ||= ->(result) { result }
47
+
48
+ result = queue.pop
49
+ return block.call(*result) if result.compact.any?
50
+
51
+ sleep 1
52
+ check_for_message(timeout: timeout, time_elapsed: time_elapsed + 1, &block)
53
+ end
54
+
55
+ def connect
56
+ connection.start
57
+ @channel = connection.channel
58
+ self
59
+ end
60
+
61
+ def disconnect
62
+ channel.close
63
+ connection.close
64
+ self
65
+ end
66
+
67
+ def create_exchange(strategy: :fanout, options: {durable: true})
68
+ @exchange = Bunny::Exchange.new(connection.channel, :fanout, exchange_name, options)
69
+ self
70
+ end
71
+
72
+ def delete_exchange
73
+ exchange.delete if exchange
74
+ self
75
+ end
76
+
77
+ def create_queue(options = {durable: true})
78
+ @queue = @channel.queue(queue_name, options)
79
+ self
80
+ end
81
+
82
+ def bind_queue_to_exchange
83
+ queue.bind(exchange)
84
+ self
85
+ end
86
+
87
+ def delete_queue
88
+ queue.delete if queue
89
+ self
90
+ end
91
+ end
92
+ end
93
+ end
@@ -5,7 +5,11 @@ module RailwayIpc
5
5
 
6
6
  def self.encode(message)
7
7
  type = message.class.to_s
8
- message = Base64.encode64(message.class.encode(message))
8
+ begin
9
+ message = Base64.encode64(message.class.encode(message))
10
+ rescue NoMethodError
11
+ raise RailwayIpc::InvalidProtobuf.new("Message #{message} is not a valid protobuf")
12
+ end
9
13
  new(type, message).to_json
10
14
  end
11
15
 
@@ -23,8 +27,8 @@ module RailwayIpc
23
27
 
24
28
  def to_json
25
29
  {
26
- type: type,
27
- encoded_message: message
30
+ type: type,
31
+ encoded_message: message
28
32
  }.to_json
29
33
  end
30
34
  end
@@ -0,0 +1,104 @@
1
+ require "railway_ipc/rpc/client/client_response_handlers"
2
+ require "railway_ipc/rpc/concerns/publish_location_configurable"
3
+ require "railway_ipc/rpc/concerns/error_adapter_configurable"
4
+ require "railway_ipc/rpc/client/errors/timeout_error"
5
+
6
+ module RailwayIpc
7
+ class Client
8
+ attr_accessor :response_message, :request_message
9
+ attr_reader :rabbit_connection, :message
10
+ extend RailwayIpc::RPC::PublishLocationConfigurable
11
+ extend RailwayIpc::RPC::ErrorAdapterConfigurable
12
+
13
+ def self.request(message)
14
+ new(message).request
15
+ end
16
+
17
+ def self.handle_response(response_type)
18
+ RPC::ClientResponseHandlers.instance.register(response_type)
19
+ end
20
+
21
+ def initialize(request_message, opts = { automatic_recovery: false }, rabbit_adapter: RailwayIpc::Rabbitmq::Adapter)
22
+ @rabbit_connection = rabbit_adapter.new(exchange_name: self.class.exchange_name, options: opts)
23
+ @request_message = request_message
24
+ end
25
+
26
+ def request(timeout = 10)
27
+ setup_rabbit_connection
28
+ attach_reply_queue_to_message
29
+ publish_message
30
+ await_response(timeout)
31
+ response_message
32
+ end
33
+
34
+ def registered_handlers
35
+ RailwayIpc::RPC::ClientResponseHandlers.instance.registered
36
+ end
37
+
38
+ def process_payload(response)
39
+ decoded_payload = decode_payload(response)
40
+ case decoded_payload.type
41
+ when *registered_handlers
42
+ @message = get_message_class(decoded_payload).decode(decoded_payload.message)
43
+ RailwayIpc.logger.info(message, "Handling response")
44
+ RailwayIpc::Response.new(message, success: true)
45
+ else
46
+ @message = LearnIpc::ErrorMessage.decode(decoded_payload.message)
47
+ raise RailwayIpc::UnhandledMessageError, "#{self.class} does not know how to handle #{decoded_payload.type}"
48
+ end
49
+ end
50
+
51
+ def setup_rabbit_connection
52
+ rabbit_connection
53
+ .connect
54
+ .create_exchange
55
+ .create_queue(auto_delete: true, exclusive: true)
56
+ end
57
+
58
+ def await_response(timeout)
59
+ rabbit_connection.check_for_message(timeout: timeout) do |_, _, payload|
60
+ self.response_message = process_payload(payload)
61
+ end
62
+ rescue RailwayIpc::Rabbitmq::Adapter::TimeoutError
63
+ error = self.class.rpc_error_adapter_class.error_message(TimeoutError.new, self.request_message)
64
+ self.response_message = RailwayIpc::Response.new(error, success: false)
65
+ rescue StandardError
66
+ self.response_message = RailwayIpc::Response.new(message, success: false)
67
+ ensure
68
+ rabbit_connection.disconnect
69
+ end
70
+
71
+ private
72
+
73
+ def log_exception(e, payload)
74
+ RailwayIpc.logger.log_exception(
75
+ feature: "railway_consumer",
76
+ error: e.class,
77
+ error_message: e.message,
78
+ payload: decode_for_error(e, payload),
79
+ )
80
+ end
81
+
82
+ def get_message_class(decoded_payload)
83
+ RailwayIpc::RPC::ClientResponseHandlers.instance.get(decoded_payload.type)
84
+ end
85
+
86
+ def decode_payload(response)
87
+ RailwayIpc::Rabbitmq::Payload.decode(response)
88
+ end
89
+
90
+ def attach_reply_queue_to_message
91
+ request_message.reply_to = rabbit_connection.queue.name
92
+ end
93
+
94
+ def publish_message
95
+ RailwayIpc.logger.info(request_message, "Sending request")
96
+ rabbit_connection.publish(RailwayIpc::Rabbitmq::Payload.encode(request_message), routing_key: "")
97
+ end
98
+
99
+ def decode_for_error(e, payload)
100
+ return e.message unless payload
101
+ self.class.rpc_error_adapter_class.error_message(payload, self.request_message)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,25 @@
1
+ module RailwayIpc
2
+ module RPC
3
+ class ClientResponseHandlers
4
+ include Singleton
5
+
6
+ def registered
7
+ handler_map.keys
8
+ end
9
+
10
+ def register(response_message)
11
+ handler_map[response_message.to_s] = response_message
12
+ end
13
+
14
+ def get(response_message)
15
+ handler_map[response_message]
16
+ end
17
+
18
+ private
19
+
20
+ def handler_map
21
+ @handler_map ||= {}
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module RailwayIpc
2
+ class Client
3
+ class TimeoutError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ module RailwayIpc
2
+ module RPC
3
+ module ErrorAdapterConfigurable
4
+ def rpc_error_adapter(rpc_error_adapter)
5
+ @rpc_error_adapter = rpc_error_adapter
6
+ end
7
+
8
+ def rpc_error_adapter_class
9
+ @rpc_error_adapter
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module RailwayIpc
2
+ module RPC
3
+ module MessageObservationConfigurable
4
+ def listen_to(exchange:, queue:)
5
+ @exchange_name = exchange
6
+ @queue_name = queue
7
+ end
8
+
9
+ def queue_name
10
+ @queue_name
11
+ end
12
+
13
+ def exchange_name
14
+ @exchange_name
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module RailwayIpc
2
+ module RPC
3
+ module PublishLocationConfigurable
4
+ def publish_to(exchange:)
5
+ @exchange_name = exchange
6
+ end
7
+
8
+ def exchange_name
9
+ @exchange_name
10
+ end
11
+ end
12
+ end
13
+ end