moleculer 0.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 +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