railway-ipc 0.1.3 → 0.1.4

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