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,117 @@
1
+ require_relative "action"
2
+ require_relative "event"
3
+
4
+ module Moleculer
5
+ module Service
6
+ ##
7
+ # @abstract subclass to define a local service.
8
+ class Base
9
+ class << self
10
+ # @!attribute [rw] service_prefix
11
+ # @return [string] a prefix to add to the service name. The service_prefix is inherited from the parent class
12
+ # if it is not already defined in the current class.
13
+ attr_writer :service_prefix
14
+
15
+ ##
16
+ # The broker this service is attached to
17
+ attr_accessor :broker
18
+
19
+ def service_prefix
20
+ Moleculer.service_prefix
21
+ end
22
+
23
+ def node
24
+ broker.local_node
25
+ end
26
+
27
+ def broker
28
+ @broker || Moleculer.broker
29
+ end
30
+
31
+ ##
32
+ # Set the service_name to the provided name. If the node is local it will prefix the service name with the
33
+ # service prefix
34
+ #
35
+ # @param name [String] the name to which the service_name should be set
36
+ def service_name(name = nil)
37
+ @service_name = name if name
38
+
39
+ return "#{broker.service_prefix}.#{@service_name}" unless broker.service_prefix.nil?
40
+
41
+ @service_name
42
+ end
43
+
44
+ ##
45
+ # Defines an action on the service.
46
+ #
47
+ # @param name [String|Symbol] the name of the action.
48
+ # @param method [Symbol] the method to which the action maps.
49
+ # @param options [Hash] the action options.
50
+ # @option options [Boolean|Hash] :cache if true, will use default caching options, if a hash is provided caching
51
+ # options will reflect the hash.
52
+ # @option options [Hash] params list of param and param types. Can be used to coerce specific params to the
53
+ # provided type.
54
+ def action(name, method, options = {})
55
+ actions[action_name_for(name)] = Action.new(name, self, method, options)
56
+ end
57
+
58
+ ##
59
+ # Defines an event on the service.
60
+ #
61
+ # @param name [String|Symbol] the name of the event.
62
+ # @param method [Symbol] the method to which the event maps.
63
+ # @param options [Hash] event options.
64
+ # @option options [Hash] :group the group in which the event should belong, defaults to the service_name
65
+ def event(name, method, options = {})
66
+ events[name] = Event.new(name, self, method, options)
67
+ end
68
+
69
+ def actions
70
+ @actions ||= {}
71
+ end
72
+
73
+ def events
74
+ @events ||= {}
75
+ end
76
+
77
+ def action_name_for(name)
78
+ "#{service_name}.#{name}"
79
+ end
80
+ end
81
+
82
+ def initialize(broker)
83
+ @broker = broker
84
+ end
85
+
86
+ ##
87
+ # returns the action defined on the service class
88
+ # @see action
89
+ def actions
90
+ self.class.actions
91
+ end
92
+
93
+ ##
94
+ # returns the events defined on the service class
95
+ # @see events
96
+ def events
97
+ self.class.events
98
+ end
99
+
100
+ ##
101
+ # @return [Moleculer::Broker] the moleculer broker the service is attached to
102
+ def broker
103
+ self.class.broker
104
+ end
105
+
106
+ def self.as_json
107
+ {
108
+ name: service_name,
109
+ settings: {},
110
+ metadata: {},
111
+ actions: Hash[actions.values.map { |a| [a.name.to_sym, a.as_json] }],
112
+ events: Hash[events.values.map { |e| [e.name.to_sym, e.as_json] }],
113
+ }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "../errors/invalid_action_response"
2
+ require_relative "../support"
3
+ module Moleculer
4
+ module Service
5
+ ##
6
+ # Represents a service event.
7
+ class Event
8
+ # @!attribute [r] name
9
+ # @return [String] the name of the action
10
+ # @!attribute [r] service
11
+ # @return [Moleculer::Service] the service that this event is tied to
12
+ attr_reader :name, :service
13
+
14
+ ##
15
+ # @param name [String] the name of the action
16
+ # @param service [Moleculer::Service] the service to which the action belongs
17
+ # @param method [Symbol] the method which the event calls when executed
18
+ # @param options [Hash] the method options
19
+ # TODO: add ability to group events
20
+ def initialize(name, service, method, options = {})
21
+ @name = name
22
+ @service = service
23
+ @method = method
24
+ @service = service
25
+ @options = options
26
+ end
27
+
28
+ ##
29
+ # Executes the event
30
+ # @param data [Hash] the event data
31
+ def execute(data, broker)
32
+ @service.new(broker).public_send(@method, data)
33
+ end
34
+
35
+ ##
36
+ # @return [Moleculer::Node] the node of the service this event is tied to
37
+ def node
38
+ @service.node
39
+ end
40
+
41
+ ##
42
+ # @return [Hash] a hash representing this event as it would be in JSON
43
+ def as_json
44
+ {
45
+ name: name,
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ module Moleculer
2
+ ##
3
+ # @private
4
+ module Service
5
+ ##
6
+ # Creates a service instance from remote service info
7
+ #
8
+ # @param [Hash] service_info remote service information
9
+ def self.from_remote_info(service_info, service_node)
10
+ Class.new(Remote) do
11
+ service_name Support::HashUtil.fetch(service_info, :name)
12
+ fetch_actions(service_info)
13
+ fetch_events(service_info)
14
+ node service_node
15
+ end
16
+ end
17
+
18
+ ##
19
+ # Represents a remote service
20
+ class Remote < Base
21
+ class << self
22
+ def node(n = nil)
23
+ @node = n if n
24
+ @node
25
+ end
26
+
27
+ def service_name(name = nil)
28
+ @service_name = name if name
29
+
30
+ @service_name
31
+ end
32
+
33
+ private
34
+
35
+ def action_name_for(name)
36
+ name
37
+ end
38
+
39
+ def fetch_actions(service_info)
40
+ seq = 0
41
+ Support::HashUtil.fetch(service_info, :actions).values.each do |a|
42
+ next if Support::HashUtil.fetch(a, :name) =~ /^\$/
43
+
44
+ define_method("action_#{seq}".to_sym) do |ctx|
45
+ @broker.send(:publish_req,
46
+ id: ctx.id,
47
+ action: ctx.action.name,
48
+ params: ctx.params,
49
+ meta: ctx.meta,
50
+ timeout: ctx.timeout,
51
+ node: self.class.node,
52
+ request_id: ctx.request_id,
53
+ stream: false)
54
+ {}
55
+ end
56
+ action(Support::HashUtil.fetch(a, :name), "action_#{seq}".to_sym)
57
+ seq += 1
58
+ end
59
+ end
60
+
61
+ def fetch_events(service_info)
62
+ seq = 0
63
+ Support::HashUtil.fetch(service_info, :events).values.each do |a|
64
+ name = Support::HashUtil.fetch(a, :name)
65
+ define_method("event_#{seq}".to_sym) do |data|
66
+ @broker.send(:publish_event,
67
+ event: name,
68
+ data: data,
69
+ broadcast: false,
70
+ groups: [],
71
+ node: self.class.node)
72
+ end
73
+ event(name, "event_#{seq}".to_sym)
74
+ seq += 1
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "service/base"
2
+ require_relative "service/remote"
@@ -0,0 +1,48 @@
1
+ require_relative "string_util"
2
+
3
+ module Moleculer
4
+ module Support
5
+ ##
6
+ # A module of functional methods for working with hashes
7
+ module HashUtil
8
+ extend self
9
+ ##
10
+ # Works like fetch, but instead indifferently uses strings and symbols. It will try both snake case and camel
11
+ # case versions of the key.
12
+ #
13
+ # @param hash [Hash] the hash to fetch from
14
+ # @param key [Object] the key to fetch from the hash. This uses Hash#fetch internally, and if a string or synbol
15
+ # is passed the hash will use an indifferent fetch
16
+ # @param default [Object] the a fallback default if fetch fails. If not provided an exception will be raised if
17
+ # key cannot be found
18
+ #
19
+ # @return [Object] the value at the given key
20
+ def fetch(hash, key, default = :__no_default__)
21
+ return fetch_with_string(hash, key, default) if key.is_a?(String) || key.is_a?(Symbol)
22
+ return hash.fetch(key, default) if default != :__no_default__
23
+
24
+ hash.fetch(key)
25
+ end
26
+
27
+ private
28
+
29
+ def fetch_with_string(hash, key, default)
30
+ ret = get_camel(hash, key).nil? ? get_underscore(hash, key) : get_camel(hash, key)
31
+ return default if default != :__no_default__ && ret.nil?
32
+ raise KeyError, %(key not found: "#{key}") if ret.nil?
33
+
34
+ ret
35
+ end
36
+
37
+ def get_camel(hash, key)
38
+ camelized = StringUtil.camelize(key.to_s)
39
+ hash[camelized].nil? ? hash[camelized.to_sym] : hash[camelized]
40
+ end
41
+
42
+ def get_underscore(hash, key)
43
+ underscored = StringUtil.underscore(key.to_s)
44
+ hash[underscored].nil? ? hash[underscored.to_sym] : hash[underscored]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ require "ap"
2
+
3
+ module Moleculer
4
+ module Support
5
+ ##
6
+ # LogProxy acts as a bridge between multiple logger types.
7
+ # @private
8
+ class LogProxy
9
+ def initialize(logger, prefix = "")
10
+ @logger = logger
11
+ @prefix = prefix
12
+ end
13
+
14
+ ##
15
+ # @return [Moleculer::LogProxy] a prefixed child of the log proxy
16
+ def get_child(prefix)
17
+ self.class.new(@logger, prefix)
18
+ end
19
+
20
+ def fatal(*args)
21
+ @logger.fatal(parse_args(args.unshift(@prefix))) if @logger
22
+ end
23
+
24
+ def error(*args)
25
+ @logger.error(parse_args(args.unshift(@prefix))) if @logger
26
+ end
27
+
28
+ def warn(*args)
29
+ @logger.warn(parse_args(args.unshift(@prefix))) if @logger
30
+ end
31
+
32
+ def info(*args)
33
+ @logger.info(parse_args(args.unshift(@prefix))) if @logger
34
+ end
35
+
36
+ def debug(*args)
37
+ @logger.debug(parse_args(args.unshift(@prefix))) if @logger
38
+ end
39
+
40
+ def trace(*args)
41
+ return unless @logger
42
+
43
+ return @logger.trace(parse_args(args.unshift(@prefix))) if @logger.respond_to?(:trace)
44
+
45
+ @logger.debug(parse_args(args.unshift(@prefix)))
46
+ end
47
+
48
+ def level
49
+ @logger.level
50
+ end
51
+
52
+ private
53
+
54
+ def parse_args(args)
55
+ if args.last.is_a?(Hash)
56
+ args.each_index do |v|
57
+ args[v] = "\n" + args[v].ai + "\n" unless args[v].is_a?(String)
58
+ end
59
+ elsif args.last.is_a?(StandardError)
60
+ args = [args.slice(0, 1), args.last.message, "\n ", args.last.backtrace.join("\n ")].flatten
61
+ end
62
+
63
+ args.join(" ").strip
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ require "ostruct"
2
+
3
+ module Moleculer
4
+ module Support
5
+ ##
6
+ # An OpenStruct that supports camelized serialization for JSON
7
+ class OpenStruct < ::OpenStruct
8
+ ##
9
+ # @return [Hash] the object prepared for conversion to JSON for transmission
10
+ def as_json
11
+ Hash[to_h.map { |item| [StringUtil.camelize(item[0]), item[1]] }]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module Moleculer
2
+ module Support
3
+ ##
4
+ # A module of functional methods for working with strings.
5
+ module StringUtil
6
+ extend self
7
+ ##
8
+ # Converts a string to lowerCamelCase.
9
+ #
10
+ # @param term [String] the term to convert
11
+ def camelize(term)
12
+ new_term = term.gsub(/(?:^|_)([a-z])/) { $1.upcase }
13
+ new_term[0..1].downcase + new_term[2..-1]
14
+ end
15
+
16
+ ##
17
+ # Makes an underscored, lowercase form from the expression in the string.
18
+ #
19
+ # @param term[String] the word to convert into an underscored string
20
+ def underscore(term)
21
+ term.gsub(/(?<!^)[A-Z]/) { "_#$&" }.downcase
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "support/hash_util"
2
+ require_relative "support/string_util"
3
+ require_relative "support/open_struct"
4
+ require_relative "support/log_proxy"
@@ -0,0 +1,207 @@
1
+ require "redis"
2
+
3
+ # frozen_string_literal: true
4
+
5
+ module Moleculer
6
+ module Transporters
7
+ ##
8
+ # The Moleculer Redis transporter
9
+ class Redis
10
+ ##
11
+ # @private
12
+ # Represents the publisher connection
13
+ class Publisher
14
+ def initialize(config)
15
+ @uri = config.transporter
16
+ @logger = config.logger.get_child("[REDIS.TRANSPORTER.PUBLISHER]")
17
+ @serializer = Serializers.for(config.serializer).new(config)
18
+ end
19
+
20
+ ##
21
+ # Publishes the packet to the packet's topic
22
+ def publish(packet)
23
+ topic = packet.topic
24
+ @logger.trace "publishing packet to '#{topic}'", packet.as_json
25
+ connection.publish(topic, @serializer.serialize(packet))
26
+ end
27
+
28
+ ##
29
+ # Connects to redis
30
+ def connect
31
+ @logger.debug "connecting publisher client on '#{@uri}'"
32
+ connection
33
+ end
34
+
35
+ ##
36
+ # Disconnects from redis
37
+ def disconnect
38
+ @logger.debug "disconnecting publisher client"
39
+ connection.disconnect!
40
+ end
41
+
42
+ private
43
+
44
+ def connection
45
+ @connection ||= ::Redis.new(url: @uri)
46
+ end
47
+ end
48
+
49
+ ##
50
+ # Represents the subscriber connection
51
+ # @private
52
+ class Subscriber
53
+ ##
54
+ # Represents a subscription
55
+ class Subscription
56
+ def initialize(config:, channel:, block:)
57
+ @connection = ::Redis.new(url: config.transporter)
58
+ @channel = channel
59
+ @block = block
60
+ @logger = config.logger.get_child("[REDIS.TRANSPORTER.SUBSCRIPTION.#{channel}]")
61
+ @serializer = Serializers.for(config.serializer).new(config)
62
+ @node_id = config.node_id
63
+
64
+ # it is necessary to send some sort of message to signal the subscriber to disconnect and shutdown
65
+ # this is an internal message
66
+ reset_disconnect
67
+ end
68
+
69
+ ##
70
+ # Starts the subscriber
71
+ def connect
72
+ @thread = Thread.new do
73
+ begin
74
+ @logger.debug "starting subscription to '#{@channel}'"
75
+ @connection.subscribe(@channel) do |on|
76
+ on.unsubscribe do
77
+ unsubscribe
78
+ end
79
+
80
+ on.message do |_, message|
81
+ packet = process_message(message)
82
+ next unless packet
83
+
84
+ process_packet(packet)
85
+ end
86
+ end
87
+ rescue StandardError => error
88
+ @logger.fatal error
89
+ exit 1
90
+ end
91
+ end
92
+ self
93
+ end
94
+
95
+ def disconnect
96
+ @logger.debug "unsubscribing from '#{@channel}'"
97
+ redis = ::Redis.new(url: @uri)
98
+ redis.publish(@channel, @disconnect_hash.value)
99
+ redis.disconnect!
100
+ end
101
+
102
+ def reset_disconnect
103
+ @disconnect_hash ||= Concurrent::AtomicReference.new
104
+ @disconnect_hash.set("#{@node_id}.#{SecureRandom.hex}.disconnect")
105
+ end
106
+
107
+ private
108
+
109
+ def deserialize(message)
110
+ parsed = @serializer.deserialize(message)
111
+ return nil if parsed["sender"] == @node_id
112
+
113
+ parsed
114
+ end
115
+
116
+ def message_is_disconnect?(message)
117
+ message.split(".")[-1] == "disconnect"
118
+ end
119
+
120
+ def process_packet(packet)
121
+ return @connection.unsubscribe if packet == :disconnect
122
+
123
+ @logger.trace "received packet from #{packet.sender}:", packet.as_json
124
+
125
+ @block.call(packet)
126
+ rescue StandardError => error
127
+ @logger.error error
128
+ end
129
+
130
+ def process_message(message)
131
+ return :disconnect if message == @disconnect_hash
132
+
133
+ return nil if message_is_disconnect?(message)
134
+
135
+ packet_type = Packets.for(@channel.split(".")[1])
136
+
137
+ parsed = deserialize(message)
138
+
139
+ return nil unless parsed
140
+
141
+
142
+ packet_type.new(parsed)
143
+ rescue StandardError => error
144
+ @logger.error error
145
+ end
146
+
147
+ def unsubscribe
148
+ @logger.debug "disconnecting channel '#{@channel}'"
149
+ @connection.disconnect!
150
+ end
151
+ end
152
+
153
+ def initialize(config)
154
+ @config = config
155
+ @uri = config.transporter
156
+ @logger = config.logger.get_child("[REDIS.TRANSPORTER]")
157
+ @subscriptions = Concurrent::Array.new
158
+ end
159
+
160
+ def subscribe(channel, &block)
161
+ @logger.debug "subscribing to channel '#{channel}'"
162
+ @subscriptions << Subscription.new(
163
+ channel: channel,
164
+ block: block,
165
+ config: @config,
166
+ ).connect
167
+ end
168
+
169
+ def disconnect
170
+ @logger.debug "disconnecting subscriptions"
171
+ @subscriptions.each(&:disconnect)
172
+ end
173
+ end
174
+
175
+ def initialize(config)
176
+ @config = config
177
+ end
178
+
179
+ def subscribe(channel, &block)
180
+ subscriber.subscribe(channel, &block)
181
+ end
182
+
183
+ def publish(packet)
184
+ publisher.publish(packet)
185
+ end
186
+
187
+ def connect
188
+ publisher.connect
189
+ end
190
+
191
+ def disconnect
192
+ publisher.disconnect
193
+ subscriber.disconnect
194
+ end
195
+
196
+ private
197
+
198
+ def publisher
199
+ @publisher ||= Publisher.new(@config)
200
+ end
201
+
202
+ def subscriber
203
+ @subscriber ||= Subscriber.new(@config)
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,22 @@
1
+ require "uri"
2
+
3
+ module Moleculer
4
+ ##
5
+ # Transporters allow you to run services on multiple nodes. They provide the communication channels for other with
6
+ # other nodes handling the transfer of events, action calls and responses, ...etc.
7
+ module Transporters
8
+
9
+ ##
10
+ # Returns a new transporter for the provided transporter uri
11
+ #
12
+ # @param [String] uri the transporter uri
13
+ # @param [Moleculer::Broker] broker the broker instance
14
+ #
15
+ # @return [Moleculer::Transporters::Base] the transporter instance
16
+ def self.for(uri)
17
+ parsed = URI(uri)
18
+ require_relative("./transporters/#{parsed.scheme}")
19
+ const_get(parsed.scheme.split("_").map(&:capitalize).join)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Moleculer
2
+ VERSION = "0.1.0"
3
+ end