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,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