servicy 0.0.3 → 0.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: caf0064f89f13aec57352cff7a92d144db60444c
4
- data.tar.gz: 7b95d789be54127997ed6ea51d709fd18910efe8
3
+ metadata.gz: d9048f3bb1f1521a29ceeb800dd7b3eb5149c9b7
4
+ data.tar.gz: 1c474e01e90d51469893fe19be368004cc4654d8
5
5
  SHA512:
6
- metadata.gz: 0b3af8c73e8b8a5d2a61eef42c89dbef44b528a199e38436ff4ce1a54613d776a4f95fab796c73a736d9321b20917949d891b313ab66ab2cfcf86c011e3a729f
7
- data.tar.gz: b4d288ad7756cd24814b5d57de1a15b8f50b1497fde40dc662ec1811d42127746337875afc3a2d6bb3da5417853729c4ff933710c81a84ce0cd1cc070188b1a1
6
+ metadata.gz: 7eb75dfafdc14c501aeb551579d839991aa312b3d554cadcc1efb410f1c9636651f9d6a8516b88415fe0739b867f242c95fd5e0adec9c067941d95c094e54d9b
7
+ data.tar.gz: 4986b4d40c49b4bb5b78f427fb5ccb25da068cb22bb225c09ca4adc245a8805af3f6ce62acb1f5b44153599a5cb740064a01700a172e9c6796ac23a5a964b126
data/README.md CHANGED
@@ -245,6 +245,27 @@ with the provided query. In the last two queries, additional error information
245
245
  may be provided, but should be considered for human-use only; ie, showing an
246
246
  error message to a user.
247
247
 
248
+ Notes on design of your objects
249
+ ===============================
250
+
251
+ See [here](http://martinfowler.com/articles/distributed-objects-microservices.html).
252
+ It's a good article. The upshot is that, while hiding weather or not an object
253
+ is remote is good for programmer burden (ie, it reduces it), it's bad for API
254
+ design.
255
+
256
+ Servicy's goal is to make it simple for you to build microservices in Ruby that
257
+ can either run locally or remotely without the developer having to worry about
258
+ or care which. That said, as the developer of a service, it is _your_
259
+ responsibility to make sure that you don't make services that suck (tm).
260
+
261
+ Don't build your objects to have iterators that require multiple-traversals
262
+ over the wire. Don't build objects that lazy-evaluate things. Don't build
263
+ objects that themselves rely on other services that are poorly built. Etc.
264
+
265
+ Do eat your own dogfood, and use your own toilet paper. If it hurts for you to
266
+ use it, with all your insider knowledge and perspective, it is downright agony
267
+ for other people.
268
+
248
269
  TODO
249
270
  ====
250
271
 
@@ -253,5 +274,8 @@ Things left to do:
253
274
  1. In the command-line tool, there is some kind of bug where the registration
254
275
  message never returns back to the client. This causes registration to hang
255
276
  forever...
256
- 1. There are a number of just kind of ugly-code things around that I should sort out.
257
- 1. Probably some other things that I'm forgetting.
277
+ 1. I should sit down and really think out code structure and refactor where
278
+ needed. Things are getting a little difficult to follow if you are jumping
279
+ around between files a bunch.
280
+ 1. I need to cut down on the number of latency and heartbeat entries that are
281
+ saved. It gets a bit out of hand.
data/Servicy.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: servicy 0.0.3 ruby lib
5
+ # stub: servicy 0.0.5 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "servicy"
9
- s.version = "0.0.3"
9
+ s.version = "0.0.5"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Thomas Luce"]
14
- s.date = "2014-08-11"
14
+ s.date = "2014-08-15"
15
15
  s.description = "A service registration and discovery framework, as a server and client library."
16
16
  s.email = "thomas.luce@gmail.com"
17
17
  s.executables = ["servicy"]
@@ -25,6 +25,9 @@ Gem::Specification.new do |s|
25
25
  "bin/servicy",
26
26
  "lib/api.rb",
27
27
  "lib/client.rb",
28
+ "lib/configurable.rb",
29
+ "lib/formats.rb",
30
+ "lib/formats/json.rb",
28
31
  "lib/hash.rb",
29
32
  "lib/load_balancer.rb",
30
33
  "lib/load_balancer/random.rb",
@@ -36,6 +39,7 @@ Gem::Specification.new do |s|
36
39
  "lib/servicy.rb",
37
40
  "lib/transport/in_memory_transport.rb",
38
41
  "lib/transport/messages.rb",
42
+ "lib/transport/null_transport.rb",
39
43
  "lib/transport/tcp_transport.rb",
40
44
  "lib/transports.rb",
41
45
  "test.rb"
@@ -43,5 +47,17 @@ Gem::Specification.new do |s|
43
47
  s.homepage = "https://github.com/thomasluce/servicy"
44
48
  s.rubygems_version = "2.2.2"
45
49
  s.summary = "A service registration and discovery framework"
50
+
51
+ if s.respond_to? :specification_version then
52
+ s.specification_version = 4
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<contraction>, [">= 0"])
56
+ else
57
+ s.add_dependency(%q<contraction>, [">= 0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<contraction>, [">= 0"])
61
+ end
46
62
  end
47
63
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.5
data/lib/api.rb CHANGED
@@ -1,14 +1,8 @@
1
- # TODO: Investigate this kind of thing using Delegator to see if I can get
2
- # a speed boost. Each exported method would have to be dynamically defined
3
- # in the API class to do contract checking and then pass that on to the
4
- # delegated object. I don't, however, know if that's actually faster than
5
- # using method_missing. Investigate
6
1
  module Servicy
7
2
  class API
8
- def self.create(klass, method_descriptions)
9
- @klass = klass
10
- method_descriptions = { class: method_descriptions[:class].inject({}) { |h, info| h[info[:method].to_sym] = info; h },
11
- instance: method_descriptions[:instance].inject({}) { |h, info| h[info[:method].to_sym] = info; h }
3
+ def self.create(klass, mds)
4
+ method_descriptions = { class: mds[:class].inject({}) { |h, info| h[info[:method].to_sym] = info; h },
5
+ instance: mds[:instance].inject({}) { |h, info| h[info[:method].to_sym] = info; h }
12
6
  }
13
7
 
14
8
  # We inherit from API here so that in the future we can add functionality
@@ -16,27 +10,42 @@ module Servicy
16
10
  # This may change to Client at some point... Who
17
11
  # knows.
18
12
  magic_class = Class.new(Servicy::API) do
19
- @@base_class = klass
20
- @@method_descriptions = method_descriptions
21
-
22
13
  def initialize(*args)
23
- @instance = @@base_class.new(*args)
14
+ @instance = self.class.const_get('BASE_CLASS').new(*args)
24
15
  end
25
16
 
26
- def method_descriptions
27
- @@method_descriptions
17
+ def self.set_remote(client)
18
+ @remote = client
19
+ end
20
+ def self.remote?
21
+ @remote
22
+ end
23
+ def self.client
24
+ @remote
28
25
  end
29
26
 
27
+ # TODO: I'm thinking that I might be able to do this with
28
+ # #define_method's instead. That will spead things up aroud 2-5x.
29
+
30
30
  # The magic happens in the two method_missing methods.
31
31
  # Meta-programming-averse; gaze upon my works, ye haughty, and despair!
32
32
  def method_missing(method, *args, &block)
33
33
  # Verify the contract
34
- data = method_descriptions[:instance][method]
35
- raise NoMethodError.new("#{@klass.to_s} does not expose an API endpoint called, #{method.to_s}") unless data
34
+ data = self.class.const_get('METHOD_DESCRIPTIONS')[:instance][method]
35
+ raise NoMethodError.new("#{self.class.const_get('BASE_CLASS')} does not expose an API endpoint called, #{method.to_s}") unless data
36
36
  data[:contract].valid_args?(*args)
37
37
 
38
38
  # Dispatch the result
39
- result = @instance.send(method, *args, &block)
39
+ result = nil
40
+ if self.class.remote?
41
+ params = data[:contract].params.each_with_index.inject({}) do |h, (param, index)|
42
+ h[param.to_s] = args[index]
43
+ h
44
+ end
45
+ result = self.class.client.transport.remote_request(method, params)
46
+ else
47
+ result = @instance.send(method, *args, &block)
48
+ end
40
49
 
41
50
  # Check the result
42
51
  data[:contract].valid_return?(*args, result)
@@ -47,17 +56,26 @@ module Servicy
47
56
 
48
57
  # Make sure that we can respond to the things we actually do respond to.
49
58
  def respond_to?(method)
50
- method_descriptions[:instance].include?(method) || super
59
+ self.class.const_get('METHOD_DESCRIPTIONS')[:instance].include?(method) || super
51
60
  end
52
61
 
53
62
  def self.method_missing(method, *args, &block)
54
63
  # Verify the contract
55
- data = @@method_descriptions[:class][method]
56
- raise NoMethodError.new("#{@klass.to_s} does not expose an API endpoint called, #{method.to_s}") unless data
64
+ data = self.const_get('METHOD_DESCRIPTIONS')[:class][method]
65
+ raise NoMethodError.new("#{self.const_get('BASE_CLASS')} does not expose an API endpoint called, #{method.to_s}") unless data
57
66
  data[:contract].valid_args?(*args)
58
67
 
59
68
  # Dispatch the result
60
- result = @@base_class.send(method, *args, &block)
69
+ result = nil
70
+ if remote?
71
+ params = data[:contract].params.each_with_index.inject({}) do |h, (param, index)|
72
+ h[param.to_s] = args[index]
73
+ h
74
+ end
75
+ result = client.transport.remote_request(method, params)
76
+ else
77
+ result = self.const_get('BASE_CLASS').send(method, *args, &block)
78
+ end
61
79
 
62
80
  # Check the result
63
81
  data[:contract].valid_return?(*args, result)
@@ -67,14 +85,14 @@ module Servicy
67
85
  end
68
86
 
69
87
  def self.respond_to?(method)
70
- @@method_descriptions[:class].include?(method) || super
88
+ const_get('METHOD_DESCRIPTIONS')[:class].include?(method) || super
71
89
  end
72
90
 
73
91
  # Send back either all the documentation, or just for a particular
74
92
  # method.
75
93
  def self.docs(method=nil)
76
94
  if method.nil?
77
- return @@method_descriptions.map do |(type, methods)|
95
+ return const_get('METHOD_DESCRIPTIONS').map do |(type, methods)|
78
96
  methods.values.map { |v| v[:docs] }
79
97
  end.flatten
80
98
  else
@@ -83,7 +101,7 @@ module Servicy
83
101
  search_in = method[0] == '.' ? [:class] : (method[0] == '#' ? [:instance] : search_in)
84
102
  end
85
103
  search_in.each do |type|
86
- @@method_descriptions[type].each do |(name,stuff)|
104
+ const_get('METHOD_DESCRIPTIONS')[type].each do |(name,stuff)|
87
105
  return stuff[:docs] if method.to_s =~ /(\.|#)?#{name.to_s}$/
88
106
  end
89
107
  end
@@ -95,6 +113,8 @@ module Servicy
95
113
  klass.constants(false).each do |const|
96
114
  magic_class.const_set(const, klass.const_get(const))
97
115
  end
116
+ magic_class.const_set('BASE_CLASS', klass)
117
+ magic_class.const_set('METHOD_DESCRIPTIONS', method_descriptions)
98
118
 
99
119
  # Give it a good name
100
120
  class_name = "#{klass.to_s}API"
@@ -117,10 +137,5 @@ module Servicy
117
137
 
118
138
  { name: name, host: 'localhost', port: port, version: version, heartbeat_port: heartbeat_port }
119
139
  end
120
-
121
- # Sets the remote configuration options
122
- def set_remote_configuration(config)
123
- @remote_config = config
124
- end
125
140
  end
126
141
  end
data/lib/client.rb CHANGED
@@ -12,23 +12,35 @@ module Servicy
12
12
 
13
13
  # Create a new Client instance
14
14
  def initialize(transport=nil)
15
- @transport = transport || Transport.InMemory.new
15
+ @transport = transport || Servicy.client.transport
16
+ end
17
+
18
+ # Return weather or not you are connected to a remote servicy server
19
+ def connected?
20
+ @transport.send(Servicy::Transport::Message.query(name: 'com.servicy.not.a.real.service')) rescue return false
21
+ true
16
22
  end
17
23
 
18
24
  # Register a service with the service discovery portal found at the other
19
25
  # end of @transport
20
26
  def register_service(args)
27
+ if !args.is_a?(Hash) && args.ancestors.include?(Servicy::API)
28
+ args = args.search_query
29
+ end
30
+
21
31
  if args.is_a?(Hash)
22
32
  @transport.send(Servicy::Transport::Message.registration(Servicy::Service.new(args)))
23
33
  else
24
- # TODO: Check if it's an API, and if so register with that.
34
+ raise ArgumentError.new("Don't know how to register type, #{args.inspect}")
25
35
  end
26
36
  end
27
37
 
28
38
  # Find a single service matching a query. This will cause the server to use
29
39
  # whatever configured load-balancing mechanism is configured for it.
30
40
  def find_service(args)
31
- service = @transport.send(Servicy::Transport::Message.query_one(args)).services.first
41
+ result = @transport.send(Servicy::Transport::Message.query_one(args))
42
+ services = result && result.services
43
+ service = services && services.first
32
44
  service && Servicy::Service.new(service)
33
45
  end
34
46
 
@@ -79,18 +91,6 @@ module Servicy
79
91
  API.create klass, data
80
92
  end
81
93
 
82
- # Ask the server at the other end of transport for a provider for the
83
- # provided API object, and configure the API object to use the remote
84
- # service.
85
- def self.find_service_provider_for(api, transport)
86
- client = Client.new(transport)
87
- service = client.find_service(api.search_query)
88
- raise ServiceNotFoundError.new(api.search_query) unless service
89
-
90
- api.set_remote_configuration(service.to_h)
91
- api
92
- end
93
-
94
94
  private
95
95
 
96
96
  # Get the contents of a file that contains a method, along with the line
@@ -0,0 +1,19 @@
1
+ class Configurable
2
+ def initialize
3
+ @stuff = {}
4
+ end
5
+
6
+ def method_missing(name, *args, &block)
7
+ if name.to_s =~ /=$/
8
+ @stuff[name.to_s.sub(/=$/, '')] = args.first
9
+ return
10
+ end
11
+
12
+ return @stuff[name] if @stuff[name]
13
+ @stuff[name] = Configurable.new
14
+ end
15
+
16
+ def nil?
17
+ true
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ module Servicy
4
+ module Formats
5
+ class JSON < Servicy::Format
6
+ class << self
7
+ def format(things)
8
+ things.to_json
9
+ end
10
+
11
+ def unformat(things)
12
+ return nil if things == "null"
13
+ return ::JSON.parse(things) rescue ::JSON.parse("[#{things.inspect}]").first
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/formats.rb ADDED
@@ -0,0 +1,36 @@
1
+ # Formats decide how a thing is encoded over the wire, regardless of what
2
+ # transport protocol is used. So, if you want to send protobufs over HTTP go
3
+ # nuts, dude.
4
+ module Servicy
5
+ # This represents an abstract class that should be overwritten for individual
6
+ # formats.
7
+ class Format
8
+ class << self
9
+ # @param [Hash] things is a key-value pairing where the values are the
10
+ # things to be encoded. You can call this recursively for nested objects.
11
+ def format(things)
12
+ raise NotImplementedError.new
13
+ end
14
+
15
+ # Turn a formatted thing back into a native representation
16
+ def unformat(thing)
17
+ raise NotImplementedError.new
18
+ end
19
+ end
20
+ end
21
+
22
+ module Formats
23
+ def self.method_missing(name, *args, &block)
24
+ c = Servicy::Formats.const_get(name)
25
+ return c if c && c.ancestors.include?(Servicy::Format)
26
+ rescue
27
+ super
28
+ end
29
+ end
30
+ end
31
+
32
+ # Load all the formatters
33
+ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'formats', '*.rb'))].each do |file|
34
+ next if file.start_with?('.') || File.directory?(file)
35
+ require File.expand_path(file)
36
+ end
data/lib/server/server.rb CHANGED
@@ -9,52 +9,47 @@ module Servicy
9
9
  # TODO: I should really make this take an options hash, but that will
10
10
  # require changing a lot of shit...
11
11
  def initialize(transport=nil, load_balancer=nil, logger_stream=nil, config_file=nil)
12
- @transport = transport || InMemoryTransport.new
12
+ @transport = transport || Servicy.config.server.transport
13
13
  @services = [] # All services
14
14
  @deactivated_services = [] # Dead services
15
- @load_balancer = load_balancer || RoundRobinLoadBalancer.new
16
- logger_stream ||= File.open(File.join('/var', 'log', 'servicy_server.log'), 'a')
17
- @logger = Logger.new(logger_stream)
18
- @config_file = config_file || File.expand_path("~/.servicy.conf")
15
+ @load_balancer = load_balancer || Servicy.config.server.load_balancer
16
+ @config_file = config_file || Servicy.config.server.config_file
19
17
  start_heartbeat_process
20
18
  end
21
19
 
22
20
  # Starts the {Servicy::Transport} listening (whatever that means for the
23
21
  # implementation), gets messages, handles them, and sends replies.
24
- # TODO: I think I might need to invert this so that it tells the transport
25
- # to start, and passes a block to the start method that handles the
26
- # messages... That way I can have the transport fork when a connection
27
- # comes in, handle it, and clean up the fork when it's done, allowing
28
- # everything to keep chugging along.
29
22
  def go!
30
23
  transport.start do |message|
31
- @logger.info "Received message, #{message.inspect}"
24
+ Servicy.logger.info "Received message, #{message.inspect}"
32
25
  last_service_count = @services.length
33
26
 
34
- result = nil
35
- if message.message == 'registration'
27
+ result = case message.message
28
+ when 'registration'
36
29
  self.register message.service
37
- result = Transport::Message.success
38
- elsif message.message == 'query'
30
+ Transport::Message.success
31
+ when 'query'
39
32
  if message.only_one
40
33
  temp = [self.find_one(message.query)].compact
41
34
  else
42
35
  temp = self.find(message.query)
43
36
  end
44
- result = Transport::Message.services temp
45
- elsif message.message == 'stats'
37
+ Transport::Message.services temp
38
+ when 'stats'
46
39
  # Gather stats about the running system, and return it
47
40
  # TODO: Other stats?
48
- result = Transport::Message.statistics(num_services: last_service_count,
41
+ Transport::Message.statistics(num_services: last_service_count,
49
42
  uptimes: uptimes,
50
43
  latencies: latencies
51
44
  )
52
45
  else
53
- result = Transport::Message.error("Bad message type, #{message.inspect}")
46
+ Transport::Message.error("Bad message type, #{message.inspect}")
54
47
  end
55
48
 
56
49
  if @services.length > last_service_count
50
+ Servicy.logger.debug("Saving to #{@config_file}")
57
51
  self.save!(@config_file)
52
+ Servicy.logger.debug("Saved")
58
53
  last_service_count = @services.length
59
54
  end
60
55
  result
@@ -107,13 +102,17 @@ module Servicy
107
102
  # tests and README.md for examples.
108
103
  def register(args)
109
104
  service = Service.new(args)
110
- @logger.info "Registering service, #{service.inspect}..."
105
+ Servicy.logger.info "Registering service, #{service}..."
111
106
  if service.up?
112
- @logger.info "Service, #{service}, up; registered."
113
- @services << service
107
+ if @services.include? service
108
+ Servicy.logger.info "Service, #{service}, already exists. Not duplicating"
109
+ else
110
+ @services << service
111
+ Servicy.logger.info "Service, #{service}, up; registered."
112
+ end
114
113
  return service
115
114
  else
116
- @logger.info "Service, #{service}, down; not registered."
115
+ Servicy.logger.info "Service, #{service}, down; not registered."
117
116
  return false
118
117
  end
119
118
  end
@@ -122,7 +121,8 @@ module Servicy
122
121
  # active list of services that are available to searches, etc.
123
122
  # @param [{Service}] service the service to de-register
124
123
  def unregister(service)
125
- @logger.info "De-registering service, #{service}"
124
+ return if @deactivated_services.include?(service)
125
+ Servicy.logger.info "De-registering service, #{service}"
126
126
  @deactivated_services << service
127
127
  end
128
128
 
@@ -131,7 +131,7 @@ module Servicy
131
131
  # @param [{Service}] service The service to remove from deactivated list
132
132
  def reregister(service)
133
133
  if @deactivated_services.delete(service)
134
- @logger.info "Re-registering service, #{service}"
134
+ Servicy.logger.info "Re-registering service, #{service}"
135
135
  end
136
136
  end
137
137
 
@@ -160,16 +160,53 @@ module Servicy
160
160
  end
161
161
 
162
162
  # Find one or more services based on some search criteria.
163
- # TODO: Give more documentation about this here.
163
+ # When finding things, you can search by any single or combination of name,
164
+ # version, and api definition. When searching for name:
165
+ #
166
+ # find(name: 'com.foobar.baz')
167
+ #
168
+ # ... or for wild-cards if you don't care who provides a thing
169
+ #
170
+ # find(name: '.baz')
171
+ #
172
+ # For searching by version, you can search by absolute version number,
173
+ # minimum, maximum, or a range (by passing min and max together):
174
+ #
175
+ # # Absolute version
176
+ # find(version: '1.2.4-p123')
177
+ #
178
+ # # Min and max as range
179
+ # find(min_version: '1.2.0', max_version: '2.0.0')
180
+ #
181
+ # All version numbers must follow the format:
182
+ #
183
+ # (major).(minor).(revision)(-patch)?
184
+ #
185
+ # And are compared to one another by converting the version to numbers and
186
+ # comparing those numbers. The version happens by:
187
+ #
188
+ # (major * 1000) + (minor * 100) + (revision * 10) + (1/patch)
189
+ #
190
+ # Searching by api can be used to search for a service that provides a
191
+ # certain interface, even if you don't know what that interface is called.
192
+ # For example, if you don't know what your authorization service is called,
193
+ # but you know that it takes 2 strings (username and password) and returns
194
+ # either false or a User object, you can search by:
195
+ #
196
+ # find(api: '.login/2[String,String] -> User|nil')
197
+ #
198
+ # NOTE The query syntax listed here isn't working currently. See
199
+ # {Servicy::ServiceSearcher} for information on how to actually search by
200
+ # api in the current version.
164
201
  def find(args={})
165
- @logger.info "Finding #{args.inspect}..."
202
+ Servicy.logger.info "Finding #{args.inspect}..."
166
203
 
167
204
  searcher = Servicy::ServiceSearcher.new(services - deactivated_services)
168
205
  searcher.filter_by_name args[:name]
169
206
  searcher.filter_by_versions args[:min_version], args[:max_version], args[:version]
170
207
  searcher.filter_by_api args[:api]
171
208
 
172
- @logger.info "Found #{searcher.services.inspect}"
209
+ Servicy.logger.info "Found #{searcher.services.length} providers for #{args.inspect}"
173
210
  searcher.services
174
211
  end
175
212
 
data/lib/server.rb CHANGED
@@ -3,6 +3,5 @@ require 'json'
3
3
  require 'transports'
4
4
  require 'transport/in_memory_transport'
5
5
  require 'load_balancer'
6
- require 'logger'
7
6
  require 'server/service_searcher'
8
7
  require 'server/server'
data/lib/service.rb CHANGED
@@ -4,7 +4,7 @@ module Servicy
4
4
  class Service
5
5
  include Comparable
6
6
  attr_reader :name, :version, :host, :port, :heartbeat_port, :protocol,
7
- :api, :heartbeat_check_rate, :latencies, :heartbeats
7
+ :api, :heartbeat_check_rate, :latencies, :heartbeats, :no_heartbeat
8
8
  attr_accessor :heartbeat_last_check
9
9
 
10
10
  NAME_REGEX = /[^\.]+\.([^\.]+\.?)+/
@@ -50,9 +50,14 @@ module Servicy
50
50
  @heartbeat_port = args[:heartbeat_port]
51
51
  @api = args[:api] || {}
52
52
  @heartbeat_check_rate = args[:heartbeat_check_rate] || 1
53
+ @transport = transport_from_class_or_string(args[:protocol] || Servicy.config.client.transport)
54
+ if @transport
55
+ args[:protocol] = @transport.protocol_string # This is to make sure that it loads correctly after saving to disk.
56
+ end
53
57
  @heartbeat_last_check = 0
54
58
  @latencies = []
55
59
  @heartbeats = []
60
+ @no_heartbeat = !!args[:no_heartbeat]
56
61
  end
57
62
 
58
63
  # Get a configuration value
@@ -99,12 +104,13 @@ module Servicy
99
104
  def self.version_as_number(version)
100
105
  parts = version.split('.')
101
106
  parts.last.gsub(/\D/, '')
102
- parts[0].to_i * 1000 + parts[1].to_i * 100 + parts[2].to_i * 10 + (parts[3] || 0)
107
+ parts[0].to_i * 1000 + parts[1].to_i * 100 + parts[2].to_i * 10 + (1 / (parts[3] || 1).to_f)
103
108
  end
104
109
 
105
110
  # Check weather or not a service is up, based on connecting to the hearbeat
106
111
  # port, and reading a single byte.
107
112
  def up?
113
+ return true if skip_heartbeat?
108
114
  t1 = Time.now
109
115
  s = TCPSocket.new(host, heartbeat_port)
110
116
  Timeout.timeout(5) do
@@ -161,11 +167,37 @@ module Servicy
161
167
  api[method_type] && api[method_type].select { |a| a[:name] == method }.first
162
168
  end
163
169
 
170
+ # Make a call to a remote end-point.
171
+ def remote_call(method, *args)
172
+ # TODO: think about handling errors in a sane way
173
+
174
+ # Encode things for the transports formatter
175
+ args = @transport.format(args)
176
+
177
+ # Make the call
178
+ @transport.remote_request(method, args)
179
+
180
+ # Decode things coming back
181
+ @transport.unformat(result)
182
+ end
183
+
164
184
  private
165
185
 
186
+ def skip_heartbeat?
187
+ !!no_heartbeat
188
+ end
189
+
190
+ def transport_from_class_or_string(transport)
191
+ transport.is_a?(Servicy::Transport) ? transport : transport_from_string(transport)
192
+ end
193
+
194
+ def transport_from_string(transport)
195
+ Servicy::Transport.all.select { |t| t.protocol_string == transport }.first
196
+ end
197
+
166
198
  # These are just to keep us from filling up memory.
167
199
  def record_latency(t1, t2)
168
- @latencies << t2 - t1
200
+ @latencies << t2.to_i - t1.to_i
169
201
  @latencies = @latencies[0...10] if @latencies.length > 10
170
202
  end
171
203
 
data/lib/servicy.rb CHANGED
@@ -1,9 +1,163 @@
1
1
  $: << File.expand_path(File.join(File.dirname(__FILE__)))
2
2
 
3
- # NOTE: You will almost never need both of these things. In all likeliness, you
4
- # will want to do something like `require 'servicy/client'` yourself. This is
5
- # here mostly as a convinience to rspec.
6
3
  require 'hash'
4
+ require 'formats'
7
5
  require 'server'
8
6
  require 'client'
9
7
  require 'api'
8
+ require 'configurable'
9
+ require 'logger'
10
+
11
+ module Servicy
12
+ # Get a logger instance that we can do something useful with.
13
+ def self.logger
14
+ @logger ||= begin
15
+ stream = config.server.logger_stream
16
+ stream = stream || File.open(File.join('/var', 'log', 'servicy_server.log'), 'a')
17
+ Logger.new(stream)
18
+ end
19
+ end
20
+
21
+ # After this method gets done running, you either have the state of the world
22
+ # exactly the same as it was, or your object is replaced in the global Object
23
+ # scope with a new object that will proxy methods to a remote service
24
+ # provider.
25
+ #
26
+ # To define a service provider is a bit more work, and less auto-magic
27
+ # because the exact details of the infrastructure and architecture of your
28
+ # setup will be very different from others. So Servicy doesn't make
29
+ # assumptions about how you want to do things. That said, it's relatively
30
+ # simple:
31
+ #
32
+ # # In some server class
33
+ # Servicy::Server.new(...).go!
34
+ #
35
+ # # In some service provider, possibly on a different box
36
+ # api = Servicy::Client.create_api_for(MyServiceClass)
37
+ # Servicy::Client.register_service(api)
38
+ # FIXME: @__api, and it's ilk, get overridden in the even that there is more
39
+ # than one.
40
+ def self.included(mod)
41
+ make_service_listener(mod)
42
+
43
+ # Attempt to find the service
44
+ client = Servicy::Client.new(config.server.transport)
45
+ raise 'go to bottom' unless client.connected?
46
+
47
+ # Create a new api object
48
+ @__api = Servicy::Client.create_api_for(mod)
49
+ port = Servicy.config.send(mod.to_s.to_sym).port
50
+ Servicy.logger.debug("Chose port, #{port} for service, #{mod.to_s}")
51
+ @__api.const_set("PORT", port) unless port.nil?
52
+ # TODO: for the other things as well
53
+
54
+ @__service = client.find_service(@__api.search_query)
55
+ raise 'go to bottom' unless @__service
56
+
57
+ # If all went well, then we can wrap the object to call to the remote
58
+ # service.
59
+ client = Servicy::Client.new(config.client.transport.class.new(port: port))
60
+ @__api.set_remote(client)
61
+
62
+ # ... and delegate that ish. We do it by killing off the original object,
63
+ # and replacing it with our own.
64
+ Object.send(:remove_const, mod.name.to_sym)
65
+ Object.const_set(mod.name.to_sym, @__api)
66
+ rescue => e
67
+ # Something went wrong, just give up silently, but don't do anything
68
+ # else; use this object as a regular-ass object. This is to allow you to
69
+ # use your API objects as normal objects in an environment where the
70
+ # object is bundled as part of a gem (,say, ) and there is no remote
71
+ # handler for it: use it locally
72
+ end
73
+
74
+ def self.make_service_listener(mod)
75
+ # If we didn't manage to connect to a remote client, then we are, by
76
+ # definition, the provider. We should start the transports api_server, and
77
+ # dispatch requests here.
78
+ mod.send(:define_singleton_method, :start_servicy_server) do
79
+ begin
80
+ Thread.new do
81
+ begin
82
+ port = Servicy.config.send(self.to_s.to_sym).port || Servicy.config.server.port
83
+ transport = Servicy.config.service.transport
84
+ transport = transport.nil? ? Servicy.config.server.transport : transport
85
+ client = Servicy::Client.new(transport.class.new(port: port))
86
+
87
+ client.transport.start_api do |request|
88
+ method = nil
89
+ begin
90
+ method = self.method(request.method_name.to_sym)
91
+ rescue
92
+ method = self.instance_method(request.method_name.to_sym)
93
+ method = method.bind(self.new)
94
+ end
95
+ things = transport.unformat(request.args)
96
+ args = method.parameters.map do |param|
97
+ things[param.last.to_s]
98
+ end
99
+
100
+ Servicy.logger.info "Calling #{method} with #{args.inspect}"
101
+ begin
102
+ result = method.call(*args)
103
+ Servicy::Transport::Message.api_response(result)
104
+ rescue => e
105
+ Servicy::Transport::Message.error(e.to_s + "\n" + e.backtrace.join("\n"))
106
+ end
107
+ end
108
+
109
+ client.transport.stop
110
+ rescue => e
111
+ Servicy::Transport::Message.error(e.to_s + "\n" + e.backtrace.join("\n"))
112
+ end
113
+ end
114
+
115
+ Servicy.logger.info "Creating #{self.to_s} api"
116
+ api = Servicy::Client.create_api_for(self)
117
+ port = Servicy.config.send(mod.to_s.to_sym).port
118
+ port = port.nil? ? Servicy.config.server.port : port
119
+ api.const_set 'PORT', port
120
+ # TODO: the other things, too
121
+ Servicy.logger.debug("Chose port, #{port} for service, #{mod.to_s}")
122
+
123
+ server_port = Servicy.config.server.port
124
+ transport = Servicy.config.server.transport
125
+ client = Servicy::Client.new(transport.class.new(port: server_port))
126
+ Servicy.logger.info "Registering api, #{api.to_s}"
127
+ client.register_service api
128
+ rescue => e
129
+ Servicy.logger.error "An error occurred with the service: #{e.to_s}\n#{e.backtrace.join("\n")}"
130
+ end
131
+ end
132
+ rescue => e
133
+ # Again, just let it die silently. There is no reason to make things not
134
+ # work locally.
135
+ Servicy.logger.error(e)
136
+ end
137
+
138
+ # Use this method to configure Servicy for other services
139
+ def self.configure(&block)
140
+ conf = Configurable.new
141
+
142
+ # Give some reasonable defaults
143
+ conf.server.host = 'localhost'
144
+ conf.server.transport = Servicy::Transport.InMemory.new
145
+ conf.server.load_balancer = Servicy::RoundRobinLoadBalancer.new
146
+ conf.server.logger_stream = File.open(File.join('/var', 'log', 'servicy_server.log'), 'a')
147
+ conf.server.config_file = File.expand_path("~/.servicy.conf")
148
+ # To set something for a given object, you would do something like:
149
+ # conf.transport.port = 1234
150
+
151
+ conf.client.transport = Servicy::Transport.InMemory.new
152
+ conf.transport.format = Servicy::Formats.JSON
153
+
154
+ conf = yield conf if block_given?
155
+
156
+ # Set the options
157
+ @_config = conf
158
+ end
159
+
160
+ def self.config
161
+ @_config ||= configure
162
+ end
163
+ end
@@ -33,7 +33,7 @@ module Servicy
33
33
 
34
34
  def socket_path
35
35
  @socket_path ||= begin
36
- file = Tempfile.new(@config[:path] || 'servicy-in-memory-transport')
36
+ file = Tempfile.new(@config[:path] || Servicy.config.transport.path || 'servicy-in-memory-transport')
37
37
  file.close
38
38
  path = file.path
39
39
  file.delete
@@ -15,7 +15,8 @@ module Servicy
15
15
  if stuff.is_a?(Hash)
16
16
  @struct = stuff
17
17
  else
18
- @struct = JSON.parse(stuff)
18
+ # TODO: should this be a formatter that I can swap out?
19
+ @struct = JSON.parse(stuff || stuff.to_s)
19
20
  end
20
21
  end
21
22
 
@@ -117,6 +118,25 @@ module Servicy
117
118
  })
118
119
  end
119
120
 
121
+ # Make a remote API call.
122
+ # @param [String] method The method to call on the remote implementor.
123
+ # @param [Hash{String => Object}] args The arguments to pass to the method.
124
+ # These are expected to be pre-formatted by an appropriate Formatter
125
+ def self.api(method, args)
126
+ new({
127
+ message: 'api',
128
+ method_name: method,
129
+ args: args
130
+ })
131
+ end
132
+
133
+ def self.api_response(result)
134
+ new({
135
+ message: 'api_response',
136
+ result: result
137
+ })
138
+ end
139
+
120
140
  # Allow comparing of messages. This doesn't make for useful sorting, at
121
141
  # the moment, but does make it easy to know if two things are saying the
122
142
  # same thing.
@@ -0,0 +1,22 @@
1
+ require 'transports'
2
+
3
+ # A transport which does nothing
4
+ module Servicy
5
+ # This is a simple transport implementation that is to be used primarily for
6
+ # testing, so that I don't have to have a bunch of servers running just to
7
+ # run rspec...
8
+ class NilTransport < Servicy::Transport
9
+ def send(message)
10
+ nil
11
+ end
12
+
13
+ def start
14
+ while true
15
+ end
16
+ end
17
+
18
+ def stop
19
+ nil
20
+ end
21
+ end
22
+ end
@@ -7,44 +7,65 @@ module Servicy
7
7
  # file" business works out well...
8
8
  class TCPTransport < Servicy::Transport
9
9
  HELLO = "Servicy says HI!"
10
+
10
11
  def send(message)
12
+ Servicy.logger.debug("Connecting")
11
13
  socket = TCPSocket.new host, port
12
14
  hello = socket.gets
13
15
  raise "Not a servicy server" unless hello.strip == HELLO
14
- socket.send message.body + "\n", 0
15
- # FIXME: For some reason, this stalls when using the command-line client.
16
- # Wireshark isn't helpful while I'm on the vpn, so I will have to fix
17
- # this later.
18
- message = Message.new(socket.gets)
16
+ Servicy.logger.debug("Sending #{message.body}")
17
+ socket.puts message.body
18
+ response = socket.gets
19
+ Servicy.logger.debug("Received #{response}")
20
+ message = Message.new(response)
19
21
  socket.close
20
22
  message
21
23
  end
22
24
 
23
25
  def start(&block)
24
26
  @server = TCPServer.open(port)
25
- while s = @server.accept
26
- Thread.new do
27
- s.puts HELLO
28
- message = Message.new s.gets
29
- result = yield message
30
- s.puts result.body
31
- s.close
27
+
28
+ loop do
29
+ Thread.start(@server.accept) do |client|
30
+ client.puts HELLO
31
+ message = Message.new client.gets
32
+ Servicy.logger.debug("Got #{message.body}")
33
+ result = block.call(message)
34
+ Servicy.logger.debug("Replying #{result.body}")
35
+ client.puts result.body
36
+ client.close
37
+ Servicy.logger.debug("Closed connection")
32
38
  end
33
39
  end
34
40
  end
41
+ alias_method :start_api, :start
35
42
 
36
43
  def stop
37
44
  @server && @server.close
38
45
  end
39
46
 
47
+ # This makes a request as an actual API consumer over the wire, and returns
48
+ # the results.
49
+ def remote_request(name, args)
50
+ args = format(args)
51
+ response = send(Message.api(name, args))
52
+ if response.struct['error']
53
+ raise response.error
54
+ else
55
+ results = unformat(response.result)
56
+ end
57
+ end
58
+
40
59
  private
41
60
 
42
61
  def host
43
- @config[:host]
62
+ host = Servicy.config.client.host
63
+ host = host.nil? ? 'localhost' : host
64
+ @config[:host] || host
44
65
  end
45
66
 
46
67
  def port
47
- @config[:port]
68
+ @config[:port] || Servicy.config.transport.port
48
69
  end
49
70
  end
50
71
  end
data/lib/transports.rb CHANGED
@@ -16,7 +16,7 @@ module Servicy
16
16
 
17
17
  # Override this method to send a message from {Client} to {Server}
18
18
  def send(messge)
19
- raise 'Not implemented'
19
+ raise NotImplementedError.new
20
20
  end
21
21
 
22
22
  # Override this method to yield a message when received on the {Server}. It
@@ -24,7 +24,12 @@ module Servicy
24
24
  # {Transport::Message} object, and send back the return value of the block
25
25
  # to the client.
26
26
  def start(&block)
27
- raise 'Not implemented'
27
+ raise NotImplementedError.new
28
+ end
29
+
30
+ # Same as start, but used in creation of API handlers.
31
+ def start_api(&block)
32
+ raise NotImplementedError.new
28
33
  end
29
34
 
30
35
  # Called when a transport is stopped
@@ -44,6 +49,39 @@ module Servicy
44
49
  rescue
45
50
  super
46
51
  end
52
+
53
+ def self.all
54
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
55
+ end
56
+
57
+ # Returns a printable string representing what protocol this would be used
58
+ # as. Override if you like.
59
+ def self.protocol_string
60
+ self.name.split("Transport").first
61
+ end
62
+
63
+ # @param [Hash] values Key-value pairs for things to encode.
64
+ def format(values)
65
+ formatter.format(values);
66
+ end
67
+
68
+ def unformat(values)
69
+ formatter.unformat(values)
70
+ end
71
+
72
+ # Method called to make a remote request of an API call. To be implemented
73
+ # per transport.
74
+ # @return [Object] the raw values coming back from the request, to be
75
+ # passed to the formatter for unformatting.
76
+ def remote_request(name, args)
77
+ raise NotImplementedError.new
78
+ end
79
+
80
+ private
81
+
82
+ def formatter
83
+ @formatter ||= @config['format'] || Servicy.config.transport.format
84
+ end
47
85
  end
48
86
  end
49
87
 
data/test.rb CHANGED
@@ -1,74 +1,100 @@
1
1
  require './lib/servicy'
2
2
 
3
- class Foo
4
- def initialize(thing)
5
- @thing = thing
6
- end
7
-
8
- # Do some foobar work
9
- # @param [String] a
10
- # @param [Integer] b
11
- def foobar(a,b)
12
- p a
13
- p b
14
- end
15
-
16
- # Do something on the class
17
- # @return [Hash{Integer => Array<String>}]
18
- def self.barbaz
19
- puts "OH MY GOD!"
20
- { 1 => ['foo', 'bar'] }
21
- end
3
+ Servicy.configure do |config|
4
+ config.server.transport = Servicy::Transport.TCP.new(port: 1234)
5
+ config
22
6
  end
23
7
 
24
- # Create a new class that wraps foo. It will be called FooAPI, and will behave
25
- # just like foo, but with contract checking. It will be the place that in the
26
- # future we extend to behave more like an API
27
- api = Servicy::Client.create_api_for Foo
28
- # p api
29
-
30
- # Instance-y-stuff
31
- foo = api.new(:something)
32
- foo.foobar('string',1)
8
+ server = Servicy::Server.new
9
+ Thread.new { server.go! }
33
10
 
34
- # Class-y stuff
35
- api.barbaz
36
-
37
- # Now we should get some explosions for failing to comply with the contracts
38
- begin
39
- foo.foobar(:not_a_string, 'or an integer')
40
- rescue => e
41
- puts e
11
+ class Foobar
42
12
  end
13
+ api = Servicy::Client.create_api_for(Foobar)
43
14
 
44
- ###############################################################################
45
- # Making a working api with an api object. This is just some ideas to outline
46
- # how I would like the interface to look, not really any working code just yet.
47
- #
48
-
49
- # Finding a registered service and using a remote service instead of a local
50
- # object. This would take the API object, gather information about it, make the
51
- # query to the service discovery portal, and return a new api model (that is
52
- # the same specifications as the original) that talks with the remote service
53
- # instead of the local object, raising an error if something goes wrong.
54
- transport = Servicy::Transport.TCP.new(port: 1234)
55
- api = Servicy::Client.find_service_provider_for(api, transport)
56
-
57
- # This should also be able to work with non-API objects given to it. In the
58
- # case of a library that you don't have installed locally.
59
- service = Servicy::Client.find_service(name: 'foobar') # This alredy exists
60
- api = service.api
15
+ server.register(api.search_query)
61
16
 
62
- # Starting a server with a given client/api object. Ideally, you can start
63
- # multipl servers.
64
- api.start_server('HTTP', 80)
65
- api.start_server('HTTPS', 443)
66
- api.start_server('TCP', 1235)
17
+ class Foobar
18
+ # @param [String] a
19
+ def foo(a)
20
+ puts "If you see this, something went wrong"
21
+ end
67
22
 
68
- # Maybe it would be better to do it with Transport objects...
69
- t = Transport.tcp.new(1235)
70
- api.start_server t
23
+ include Servicy
24
+ end
71
25
 
72
- # Combining registration and server starting
73
- api.register_with('127.0.0.1:1234', Transport.TCP.new(1235))
26
+ f = Foobar.new
27
+ p f
74
28
 
29
+ # class Foo
30
+ # def initialize(thing)
31
+ # @thing = thing
32
+ # end
33
+ #
34
+ # # Do some foobar work
35
+ # # @param [String] a
36
+ # # @param [Integer] b
37
+ # def foobar(a,b)
38
+ # p a
39
+ # p b
40
+ # end
41
+ #
42
+ # # Do something on the class
43
+ # # @return [Hash{Integer => Array<String>}]
44
+ # def self.barbaz
45
+ # puts "OH MY GOD!"
46
+ # { 1 => ['foo', 'bar'] }
47
+ # end
48
+ # end
49
+ #
50
+ # # Create a new class that wraps foo. It will be called FooAPI, and will behave
51
+ # # just like foo, but with contract checking. It will be the place that in the
52
+ # # future we extend to behave more like an API
53
+ # api = Servicy::Client.create_api_for Foo
54
+ # # p api
55
+ #
56
+ # # Instance-y-stuff
57
+ # foo = api.new(:something)
58
+ # foo.foobar('string',1)
59
+ #
60
+ # # Class-y stuff
61
+ # api.barbaz
62
+ #
63
+ # # Now we should get some explosions for failing to comply with the contracts
64
+ # begin
65
+ # foo.foobar(:not_a_string, 'or an integer')
66
+ # rescue => e
67
+ # puts e
68
+ # end
69
+ #
70
+ # ###############################################################################
71
+ # # Making a working api with an api object. This is just some ideas to outline
72
+ # # how I would like the interface to look, not really any working code just yet.
73
+ # #
74
+ #
75
+ # # Finding a registered service and using a remote service instead of a local
76
+ # # object. This would take the API object, gather information about it, make the
77
+ # # query to the service discovery portal, and return a new api model (that is
78
+ # # the same specifications as the original) that talks with the remote service
79
+ # # instead of the local object, raising an error if something goes wrong.
80
+ # transport = Servicy::Transport.TCP.new(port: 1234)
81
+ # api = Servicy::Client.find_service_provider_for(api, transport)
82
+ #
83
+ # # This should also be able to work with non-API objects given to it. In the
84
+ # # case of a library that you don't have installed locally.
85
+ # service = Servicy::Client.find_service(name: 'foobar') # This alredy exists
86
+ # api = service.api
87
+ #
88
+ # # Starting a server with a given client/api object. Ideally, you can start
89
+ # # multipl servers.
90
+ # api.start_server('HTTP', 80)
91
+ # api.start_server('HTTPS', 443)
92
+ # api.start_server('TCP', 1235)
93
+ #
94
+ # # Maybe it would be better to do it with Transport objects...
95
+ # t = Transport.tcp.new(1235)
96
+ # api.start_server t
97
+ #
98
+ # # Combining registration and server starting
99
+ # api.register_with('127.0.0.1:1234', Transport.TCP.new(1235))
100
+ #
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servicy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Luce
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-11 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2014-08-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: contraction
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: A service registration and discovery framework, as a server and client
14
28
  library.
15
29
  email: thomas.luce@gmail.com
@@ -25,6 +39,9 @@ files:
25
39
  - bin/servicy
26
40
  - lib/api.rb
27
41
  - lib/client.rb
42
+ - lib/configurable.rb
43
+ - lib/formats.rb
44
+ - lib/formats/json.rb
28
45
  - lib/hash.rb
29
46
  - lib/load_balancer.rb
30
47
  - lib/load_balancer/random.rb
@@ -36,6 +53,7 @@ files:
36
53
  - lib/servicy.rb
37
54
  - lib/transport/in_memory_transport.rb
38
55
  - lib/transport/messages.rb
56
+ - lib/transport/null_transport.rb
39
57
  - lib/transport/tcp_transport.rb
40
58
  - lib/transports.rb
41
59
  - test.rb