moleculer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +44 -0
  5. data/.travis.yml +23 -0
  6. data/.yardopts +1 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +57 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +59 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/examples/benchmark_server.rb +21 -0
  16. data/examples/client-server/client.rb +13 -0
  17. data/examples/client-server/server.rb +25 -0
  18. data/lib/moleculer/broker.rb +318 -0
  19. data/lib/moleculer/configuration.rb +109 -0
  20. data/lib/moleculer/context.rb +24 -0
  21. data/lib/moleculer/errors/action_not_found.rb +6 -0
  22. data/lib/moleculer/errors/invalid_action_response.rb +11 -0
  23. data/lib/moleculer/errors/local_node_already_registered.rb +6 -0
  24. data/lib/moleculer/errors/node_not_found.rb +6 -0
  25. data/lib/moleculer/errors/transporter_already_started.rb +6 -0
  26. data/lib/moleculer/node.rb +105 -0
  27. data/lib/moleculer/packets/base.rb +47 -0
  28. data/lib/moleculer/packets/disconnect.rb +10 -0
  29. data/lib/moleculer/packets/discover.rb +10 -0
  30. data/lib/moleculer/packets/event.rb +37 -0
  31. data/lib/moleculer/packets/heartbeat.rb +18 -0
  32. data/lib/moleculer/packets/info.rb +97 -0
  33. data/lib/moleculer/packets/req.rb +57 -0
  34. data/lib/moleculer/packets/res.rb +43 -0
  35. data/lib/moleculer/packets.rb +25 -0
  36. data/lib/moleculer/registry.rb +325 -0
  37. data/lib/moleculer/serializers/json.rb +23 -0
  38. data/lib/moleculer/serializers.rb +10 -0
  39. data/lib/moleculer/service/action.rb +60 -0
  40. data/lib/moleculer/service/base.rb +117 -0
  41. data/lib/moleculer/service/event.rb +50 -0
  42. data/lib/moleculer/service/remote.rb +80 -0
  43. data/lib/moleculer/service.rb +2 -0
  44. data/lib/moleculer/support/hash_util.rb +48 -0
  45. data/lib/moleculer/support/log_proxy.rb +67 -0
  46. data/lib/moleculer/support/open_struct.rb +15 -0
  47. data/lib/moleculer/support/string_util.rb +25 -0
  48. data/lib/moleculer/support.rb +4 -0
  49. data/lib/moleculer/transporters/redis.rb +207 -0
  50. data/lib/moleculer/transporters.rb +22 -0
  51. data/lib/moleculer/version.rb +3 -0
  52. data/lib/moleculer.rb +103 -0
  53. data/moleculer.gemspec +50 -0
  54. metadata +238 -0
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require_relative "registry"
6
+ require_relative "transporters"
7
+ require_relative "support"
8
+
9
+ module Moleculer
10
+ ##
11
+ # The Broker is the primary component of Moleculer. It handles action, events, and communication with remote nodes.
12
+ # Only a single broker should be run for any given process, and it is automatically started when Moleculer::start or
13
+ # Moleculer::run is called.
14
+ class Broker
15
+ include Moleculer::Support
16
+ extend Forwardable
17
+ attr_reader :config
18
+
19
+ def_delegators :@config, :node_id, :heartbeat_interval, :services, :service_prefix
20
+
21
+ ##
22
+ # @param config [Moleculer::Config] the broker configuration
23
+ def initialize(config)
24
+ @config = config
25
+
26
+ @config.broker = self
27
+
28
+ @logger = @config.logger.get_child("[BROKER]")
29
+ @registry = Registry.new(@config)
30
+ @transporter = Transporters.for(@config.transporter).new(@config)
31
+ @contexts = Concurrent::Map.new
32
+ end
33
+
34
+ ##
35
+ # Call the provided action.
36
+ #
37
+ # @param action_name [String] the action to call.
38
+ # @param params [Hash] the params with which to call the action
39
+ # @param meta [Hash] the metadata of the request
40
+ #
41
+ # @return [Hash] the return result of the action call
42
+ def call(action_name, params, meta: {}, node_id: nil, timeout: Moleculer.config.timeout)
43
+ action = node_id ? @registry.fetch_action_for_node_id(action_name, node_id) : @registry.fetch_action(action_name)
44
+
45
+ context = Context.new(
46
+ broker: self,
47
+ action: action,
48
+ params: params,
49
+ meta: meta,
50
+ timeout: timeout,
51
+ )
52
+
53
+ future = Concurrent::Promises.resolvable_future
54
+
55
+ @contexts[context.id] = {
56
+ context: context,
57
+ called_at: Time.now,
58
+ future: future,
59
+ }
60
+
61
+ action.execute(context, self)
62
+
63
+ future.value!(context.timeout)
64
+ end
65
+
66
+ def emit(event_name, payload)
67
+ @logger.debug("emitting event '#{event_name}'")
68
+ events = @registry.fetch_events_for_emit(event_name)
69
+
70
+ events.each { |e| e.execute(payload, self) }
71
+ end
72
+
73
+ def run
74
+ self_read, self_write = IO.pipe
75
+
76
+ %w[INT TERM].each do |sig|
77
+ trap sig do
78
+ self_write.puts(sig)
79
+ end
80
+ end
81
+
82
+ begin
83
+ start
84
+
85
+ while (readable_io = IO.select([self_read]))
86
+ signal = readable_io.first[0].gets.strip
87
+ handle_signal(signal)
88
+ end
89
+ rescue Interrupt
90
+ stop
91
+ end
92
+ end
93
+
94
+ def start
95
+ @logger.info "starting"
96
+ @transporter.connect
97
+ register_local_node
98
+ start_subscribers
99
+ publish_discover
100
+ publish_info
101
+ start_heartbeat
102
+ self
103
+ end
104
+
105
+ def stop
106
+ @logger.info "stopping"
107
+ publish(:disconnect)
108
+ @transporter.disconnect
109
+ exit 0
110
+ end
111
+
112
+ def wait_for_services(*services)
113
+ until (services = @registry.missing_services(*services)) && services.empty?
114
+ @logger.info "waiting for services '#{services.join(', ')}'"
115
+ sleep 0.1
116
+ end
117
+ end
118
+
119
+ def local_node
120
+ @registry.local_node
121
+ end
122
+
123
+ ##
124
+ # Processes an incoming message and passes it to the appropriate channel for handling
125
+ #
126
+ # @param [String] channel the channel in which the message came in on
127
+ # @param [Hash] message the raw deserialized message
128
+ def process_message(channel, message)
129
+ subscribers[channel] << Packets.for(channel.split(".")[1]).new(message) if subscribers[channel]
130
+ rescue StandardError => e
131
+ @logger.error e
132
+ end
133
+
134
+ def process_response(packet)
135
+ context = @contexts.delete(packet.id)
136
+ context[:future].fulfill(packet.data)
137
+ end
138
+
139
+ def process_event(packet)
140
+ @logger.debug("processing event '#{packet.event}'")
141
+ events = @registry.fetch_events_for_node_id(packet.event, node_id)
142
+
143
+ events.each { |e| e.execute(packet.data) }
144
+ rescue StandardError => e
145
+ @logger.error e
146
+ end
147
+
148
+ def process_request(packet)
149
+ @logger.debug "processing request #{packet.id}"
150
+ action = @registry.fetch_action_for_node_id(packet.action, node_id)
151
+ node = @registry.fetch_node(packet.sender)
152
+
153
+ context = Context.new(
154
+ id: packet.id,
155
+ broker: self,
156
+ action: action,
157
+ params: packet.params,
158
+ meta: packet.meta,
159
+ timeout: @config.timeout,
160
+ )
161
+
162
+ response = action.execute(context, self)
163
+
164
+ publish_res(
165
+ id: context.id,
166
+ success: true,
167
+ data: response,
168
+ error: {},
169
+ meta: context.meta,
170
+ stream: false,
171
+ node: node,
172
+ )
173
+ end
174
+
175
+ private
176
+
177
+ def handle_signal(sig)
178
+ case sig
179
+ when "INT", "TERM"
180
+ raise Interrupt
181
+ end
182
+ end
183
+
184
+ def publish(packet_type, message = {})
185
+ packet = Packets.for(packet_type).new(message)
186
+ @transporter.publish(packet)
187
+ end
188
+
189
+ def publish_event(event_data)
190
+ publish_to_node(:event, event_data.delete(:node), event_data)
191
+ end
192
+
193
+ def publish_heartbeat
194
+ publish(:heartbeat)
195
+ end
196
+
197
+ ##
198
+ # Publishes the discover packet
199
+ def publish_discover
200
+ publish(:discover)
201
+ end
202
+
203
+ def publish_info(node_id = nil)
204
+ return publish(:info, @registry.local_node.as_json) unless node_id
205
+
206
+ node = @registry.safe_fetch_node(node_id) || node_id
207
+ publish_to_node(:info, node, @registry.local_node.as_json)
208
+ end
209
+
210
+ def publish_req(request_data)
211
+ publish_to_node(:req, request_data.delete(:node), request_data)
212
+ end
213
+
214
+ def publish_res(response_data)
215
+ publish_to_node(:res, response_data.delete(:node), response_data)
216
+ end
217
+
218
+ def publish_to_node(packet_type, node, message = {})
219
+ packet = Packets.for(packet_type).new(message.merge(node: node))
220
+ @transporter.publish(packet)
221
+ end
222
+
223
+ def register_local_node
224
+ @logger.info "registering #{services.length} local services"
225
+ node = Node.new(
226
+ node_id: node_id,
227
+ services: services,
228
+ local: true,
229
+ )
230
+ @registry.register_node(node)
231
+ end
232
+
233
+ def register_or_update_remote_node(info_packet)
234
+ node = Node.from_remote_info(info_packet)
235
+ @registry.register_node(node)
236
+ end
237
+
238
+ def register_local_services
239
+ services.each do |service|
240
+ register_service(service)
241
+ end
242
+ end
243
+
244
+ def register_service(service)
245
+ @registry.register_local_service(service)
246
+ end
247
+
248
+ def start_heartbeat
249
+ Concurrent::TimerTask.new(execution_interval: heartbeat_interval) do
250
+ publish_heartbeat
251
+ @registry.expire_nodes
252
+ end.execute
253
+ end
254
+
255
+ def start_subscribers
256
+ subscribe_to_info
257
+ subscribe_to_res
258
+ subscribe_to_req
259
+ subscribe_to_events
260
+ subscribe_to_discover
261
+ subscribe_to_disconnect
262
+ subscribe_to_heartbeat
263
+ end
264
+
265
+ def subscribe_to_events
266
+ subscribe("MOL.EVENT.#{node_id}") do |packet|
267
+ process_event(packet)
268
+ end
269
+ end
270
+
271
+ def subscribe_to_info
272
+ subscribe("MOL.INFO.#{node_id}") do |packet|
273
+ register_or_update_remote_node(packet)
274
+ end
275
+ subscribe("MOL.INFO") do |packet|
276
+ register_or_update_remote_node(packet)
277
+ end
278
+ end
279
+
280
+ def subscribe_to_res
281
+ subscribe("MOL.RES.#{node_id}") do |packet|
282
+ process_response(packet)
283
+ end
284
+ end
285
+
286
+ def subscribe_to_req
287
+ subscribe("MOL.REQ.#{node_id}") do |packet|
288
+ process_request(packet)
289
+ end
290
+ end
291
+
292
+ def subscribe_to_discover
293
+ subscribe("MOL.DISCOVER") do |packet|
294
+ publish_info(packet.sender) unless packet.sender == node_id
295
+ end
296
+ subscribe("MOL.DISCOVER.#{node_id}") do |packet|
297
+ publish_info(packet.sender)
298
+ end
299
+ end
300
+
301
+ def subscribe_to_heartbeat
302
+ subscribe("MOL.HEARTBEAT") do |packet|
303
+ node = @registry.safe_fetch_node(packet.sender)
304
+ node.beat
305
+ end
306
+ end
307
+
308
+ def subscribe_to_disconnect
309
+ subscribe("MOL.DISCONNECT") do |packet|
310
+ @registry.remove_node(packet.sender)
311
+ end
312
+ end
313
+
314
+ def subscribe(channel, &block)
315
+ @transporter.subscribe(channel, &block)
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,109 @@
1
+ module Moleculer
2
+ ##
3
+ # Handles Moleculer configuration
4
+ # @private
5
+ class Configuration
6
+ ##
7
+ # @private
8
+ class ServiceList
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ @services = []
12
+ end
13
+
14
+ def <<(service)
15
+ service.broker = @configuration.broker
16
+ @services << service
17
+ end
18
+
19
+ def length
20
+ @services.length
21
+ end
22
+
23
+ def first
24
+ @services.first
25
+ end
26
+
27
+ def map(&block)
28
+ @services.map(&block)
29
+ end
30
+
31
+ def include?(service)
32
+ @services.include?(service)
33
+ end
34
+ end
35
+
36
+ private_constant :ServiceList
37
+
38
+ class << self
39
+ attr_reader :accessors
40
+
41
+ private
42
+
43
+ def config_accessor(attribute, default = nil, &block)
44
+ @accessors ||= {}
45
+ @accessors[attribute.to_sym] = { default: default, block: block }
46
+
47
+ class_eval <<-method
48
+ def #{attribute}
49
+ @#{attribute} ||= default_for("#{attribute}".to_sym)
50
+ end
51
+ method
52
+
53
+ instance_eval do
54
+ attr_writer attribute.to_sym
55
+ end
56
+ end
57
+ end
58
+
59
+ config_accessor :log_file
60
+ config_accessor :log_level, :debug
61
+ config_accessor :logger do |c|
62
+ logger = Ougai::Logger.new(c.log_file || STDOUT)
63
+ logger.formatter = Ougai::Formatters::Readable.new("MOL")
64
+ logger.level = c.log_level
65
+ Moleculer::Support::LogProxy.new(logger)
66
+ end
67
+ config_accessor :heartbeat_interval, 5
68
+ config_accessor :timeout, 5
69
+ config_accessor :transporter, "redis://localhost"
70
+ config_accessor :serializer, :json
71
+ config_accessor :node_id, "#{Socket.gethostname.downcase}-#{Process.pid}"
72
+ config_accessor :service_prefix
73
+
74
+ attr_accessor :broker
75
+
76
+ def initialize(options={})
77
+ options.each do |option, value|
78
+ send("#{option}=".to_sym, value)
79
+ end
80
+ end
81
+
82
+ def services
83
+ @services ||= ServiceList.new(self)
84
+ end
85
+
86
+ def services=(array)
87
+ @services = ServiceList.new(self)
88
+ array.each { |s| @services << s}
89
+ end
90
+
91
+ private
92
+
93
+ def accessors
94
+ self.class.accessors
95
+ end
96
+
97
+ def default_for(attribute)
98
+ accessors[attribute][:default] || (has_block_for?(attribute) ? block_for(attribute).call(self) : nil)
99
+ end
100
+
101
+ def block_for(attribute)
102
+ accessors[attribute][:block]
103
+ end
104
+
105
+ def has_block_for?(attribute)
106
+ accessors[attribute][:block] && true
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,24 @@
1
+ module Moleculer
2
+ class Context
3
+ attr_reader :request_id,
4
+ :action,
5
+ :params,
6
+ :meta,
7
+ :level,
8
+ :timeout,
9
+ :id
10
+
11
+ def initialize(broker:, action:, params:, meta:, parent_id: nil, level: 1, timeout:, id: nil)
12
+ @id = id ? id : SecureRandom.uuid
13
+ @broker = broker
14
+ @action = action
15
+ @request_id = SecureRandom.uuid
16
+ @parent_id = parent_id
17
+ @params = params
18
+ @meta = meta
19
+ @level = level
20
+ @timeout = timeout
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ module Moleculer
2
+ module Errors
3
+ class ActionNotFound < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module Moleculer
2
+ module Errors
3
+ ##
4
+ # Raised when an action does not return a hash
5
+ class InvalidActionResponse < StandardError
6
+ def initialize(response)
7
+ super "Action must return a Hash, instead it returned a #{response.class.name}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Moleculer
2
+ module Errors
3
+ class LocalNodeAlreadyRegistered < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Moleculer
2
+ module Errors
3
+ class NodeNotFound < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Moleculer
2
+ module Errors
3
+ class TransporterAlreadyStarted < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Moleculer
6
+ ##
7
+ # Nodes are a representation of communicating apps within the same event bus.
8
+ # A node is something that emits/listens to events within the bus and
9
+ # communicates accordingly.
10
+ class Node
11
+ class << self
12
+ def from_remote_info(info_packet)
13
+ new(
14
+ services: info_packet.services,
15
+ node_id: info_packet.sender,
16
+ )
17
+ end
18
+ end
19
+
20
+ attr_reader :id,
21
+ :services
22
+
23
+ def initialize(options = {})
24
+ @id = options.fetch(:node_id)
25
+ @local = options.fetch(:local, false)
26
+ @hostname = options.fetch(:hostname, Socket.gethostname)
27
+
28
+ svcs = options.fetch(:services)
29
+ # TODO: move this up to from_remote_info
30
+ svcs.map! { |service| Service.from_remote_info(service, self) } if svcs.first.is_a? Hash
31
+ @services = Hash[svcs.map { |s| [s.service_name, s] }]
32
+ end
33
+
34
+ def register_service(service)
35
+ @services[service.name] = service
36
+ end
37
+
38
+ ##
39
+ # @return [Hash] returns a key, value mapping of available actions on this node
40
+ # TODO: refactor this into a list object
41
+ def actions
42
+ unless @actions
43
+ @actions = {}
44
+
45
+ @services.each_value { |s| s.actions.each { |key, value| @actions[key] = value } }
46
+ end
47
+ @actions
48
+ end
49
+
50
+ ##
51
+ # @return [Hash] returns a key value mapping of events on this node
52
+ # TODO: refactor this into a list object
53
+ def events
54
+ unless @events
55
+ @events = {}
56
+ @services.each_value do |s|
57
+ s.events.each do |key, value|
58
+ @events[key] ||= []
59
+ @events[key] << value
60
+ end
61
+ end
62
+ end
63
+ @events
64
+ end
65
+
66
+ ##
67
+ # @return [Time] the time of the last heartbeat, or Time#now if the node is local.
68
+ def last_heartbeat_at
69
+ return Time.now if local?
70
+
71
+ @last_heartbeat_at || Time.now
72
+ end
73
+
74
+ ##
75
+ # Updates the last heartbeat to Time#now
76
+ def beat
77
+ @last_heartbeat_at = Time.now
78
+ end
79
+
80
+ def local?
81
+ @local
82
+ end
83
+
84
+ def as_json
85
+ {
86
+ config: {},
87
+ seq: 1,
88
+ ipList: [],
89
+ hostname: @hostname,
90
+ services: @services.values.map(&:as_json),
91
+ client: client_attrubutes
92
+ }
93
+ end
94
+
95
+ private
96
+
97
+ def client_attrubutes
98
+ {
99
+ type: "Ruby",
100
+ version: Moleculer::VERSION,
101
+ lang_version: RUBY_VERSION
102
+ }
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,47 @@
1
+ require_relative "../support"
2
+
3
+ module Moleculer
4
+ module Packets
5
+ ##
6
+ # @abstract Subclass for packet types.
7
+ class Base
8
+ include Support
9
+ ##
10
+ # The protocol version
11
+ attr_reader :ver
12
+
13
+ ##
14
+ # The sender of the packet
15
+ attr_reader :sender
16
+
17
+ def self.packet_name
18
+ name.split("::").last.upcase
19
+ end
20
+
21
+ ##
22
+ # @param data [Hash] the raw packet data
23
+ # @options data [String] :ver the protocol version, defaults to `'3'`
24
+ # @options data [String] :sender the packet sender, defaults to `Moleculer#node_id`
25
+ def initialize(data = {})
26
+ @ver = HashUtil.fetch(data, :ver, "3")
27
+ @sender = HashUtil.fetch(data, :sender, Moleculer.config.node_id)
28
+ end
29
+
30
+ ##
31
+ # The publishing topic for the packet. This is used to publish packets to the moleculer network. Override as
32
+ # needed.
33
+ #
34
+ # @return [String] the pub/sub topic to publish to
35
+ def topic
36
+ "MOL.#{self.class.packet_name}"
37
+ end
38
+
39
+ def as_json
40
+ {
41
+ ver: ver,
42
+ sender: sender,
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ require_relative "base"
2
+
3
+ module Moleculer
4
+ module Packets
5
+ ##
6
+ # Represents a DISCONNECT packet
7
+ class Disconnect < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative "base"
2
+
3
+ module Moleculer
4
+ module Packets
5
+ ##
6
+ # Represents a DISCOVER packet
7
+ class Discover < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ require_relative "base"
2
+
3
+ module Moleculer
4
+ module Packets
5
+ ##
6
+ # Represents a EVENT packet
7
+ class Event < Base
8
+ attr_reader :event,
9
+ :data,
10
+ :broadcast,
11
+ :groups
12
+
13
+ def initialize(data)
14
+ super(data)
15
+
16
+ @event = HashUtil.fetch(data, :event)
17
+ @data = HashUtil.fetch(data, :data)
18
+ @broadcast = HashUtil.fetch(data, :broadcast)
19
+ @groups = HashUtil.fetch(data, :groups, [])
20
+ @node = HashUtil.fetch(data, :node, nil)
21
+ end
22
+
23
+ def as_json
24
+ super.merge(
25
+ event: @event,
26
+ data: @data,
27
+ broadcast: @broadcast,
28
+ groups: @groups,
29
+ )
30
+ end
31
+
32
+ def topic
33
+ "#{super}.#{@node.id}"
34
+ end
35
+ end
36
+ end
37
+ end