servicy 0.0.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: caf0064f89f13aec57352cff7a92d144db60444c
4
+ data.tar.gz: 7b95d789be54127997ed6ea51d709fd18910efe8
5
+ SHA512:
6
+ metadata.gz: 0b3af8c73e8b8a5d2a61eef42c89dbef44b528a199e38436ff4ce1a54613d776a4f95fab796c73a736d9321b20917949d891b313ab66ab2cfcf86c011e3a729f
7
+ data.tar.gz: b4d288ad7756cd24814b5d57de1a15b8f50b1497fde40dc662ec1811d42127746337875afc3a2d6bb3da5417853729c4ff933710c81a84ce0cd1cc070188b1a1
data/README.md ADDED
@@ -0,0 +1,257 @@
1
+ Servicy
2
+ =======
3
+
4
+ A service registration and discovery platform in Ruby for SOA.
5
+
6
+ The goal of Servicy is to provide a simple daemon which allows services in an
7
+ SOA application configuration to register themselves by name and/or API, and
8
+ allow service consumers to find instances of services to use, and be relatively
9
+ sure of up-time, routing, etc.
10
+
11
+ Servicy could possibly used as a router as well, although that may be out of
12
+ scope we will see.
13
+
14
+ General architecture ideas
15
+ ==========================
16
+
17
+ Servicy would be broken into a few pieces:
18
+
19
+ 1. A daemon which can handle registration and query requests
20
+ 2. An in-memory database of services to facilitate queries
21
+ 3. A persistent cache of services to allow for fast restarts
22
+ 4. (possibly) A service/API router
23
+ 5. Reporting service to report on service health and usage.
24
+ 6. Client library for service discovery and connection
25
+ 7. Service library for registration, API discovery and building
26
+
27
+ One big issue that will have to be resolved is version dependencies. If there
28
+ are two versions of a service, each with a different public API, then Servicy
29
+ should know about the differences and send back to a service requester the
30
+ correct version based on their requirements. For this, Servicy should have a
31
+ way of discovering and/or services defining their API to Servicy.
32
+
33
+ Each service should also be able to define multiple instances, and Servicy
34
+ should have multiple strategies for choosing instances of a given service. For
35
+ example, round-robin, load-based (would require machine-level meta-data to be
36
+ reported by the services), even requests, etc. To start with, a simple
37
+ round-robin scheme will likely be the only implementation. Furthermore, Servicy
38
+ should have knowledge of if a service is up or down, and automatically remove
39
+ from its list of available providers, or add it back in as needed.
40
+
41
+ Also, Servicy should maintain statistics on usage for services and be able to
42
+ report that information back to a user. Stats should include number of
43
+ discoveries, cache hits/misses, service up-times, provider failures, and, if it
44
+ works as a router, request latency. In the event that I end up _not_ making it
45
+ a router (seems likely at this point), it should still report latency for
46
+ service discovery and query operations that require back-and-forth with the
47
+ service.
48
+
49
+ Protocol ideas
50
+ ==============
51
+
52
+ Servicy is protocol agnostic, and the exact transport mechanism used to
53
+ communicate between client and server doesn't matter. The data is sent as JSON
54
+ underneath it all (for now; I may even change things to be format agnostic in
55
+ the future). You can see examples of the JSON by looking in the
56
+ Servicy::Transport::Message class.
57
+
58
+ Service registration
59
+ --------------------
60
+
61
+ Service registration allows for a service to declare that they exist, how to
62
+ reach them, and what functionality they provide. Services are named using a
63
+ Java-style, inverted domain name. For example:
64
+
65
+ ```
66
+ com.mycompany.users.authentication
67
+ ```
68
+
69
+ might be the name of a user authentication service provided internally. This
70
+ allows for services to be broken up by what other services they interact with,
71
+ as well as be easy to read for users browsing the registry.
72
+
73
+ Beyond the name, a service must provide some other information for its
74
+ registry. This includes:
75
+
76
+ 1. Host to connect to
77
+ 2. Port on the host to connect to
78
+ 3. Protocol used to communicate (HTTP, HTTPS, Thrift, etc.)
79
+ 4. Version number
80
+ 5. Heartbeat port
81
+
82
+ The host can be either an IP address or a fully-qualified hostname. In either
83
+ case it should be route-able in the context of the Servicy host. In other
84
+ words, if Servicy cannot connect with it, it won't add it to the registry. The
85
+ port should be an integer between 1 and 65535. The protocol can be anything,
86
+ however some standards are:
87
+
88
+ * HTTP - _Must_ use un-encrypted HTTP
89
+ * HTTPS - _Must_ use TLS over HTTP
90
+ * HTTP/S - Can use either HTTP or HTTPS
91
+ * Thrift
92
+ * TCP - Direct, telnet-like communication
93
+ * UDP - Direct connection, but don't expect replies
94
+
95
+ The protocol says nothing about what information needs to be sent or received
96
+ by a client consuming the service. It is assumed that if the client is looking
97
+ for the service, it knows how to use it once it is found, and will only use the
98
+ protocol information to select between possible options provided by the
99
+ consuming library. Protocols can also be provided as a list of possible
100
+ options, ordered with most preferred first. At some point in the future, there
101
+ may be a mechanism by which a service can describe itself in a more useful way,
102
+ and client libraries can use that information to dynamically build an internal
103
+ API.
104
+
105
+ The version number must follow the format:
106
+
107
+ ```
108
+ major.minor.revision-patch
109
+ ```
110
+
111
+ The "-patch" part can be omitted. For example:
112
+
113
+ ```
114
+ 1.0.2-p143
115
+ ```
116
+
117
+ The heartbeat port is a port that Servicy can connect to to ensure that the
118
+ service is still functioning. In many cases this will be the same port as the
119
+ primary connection port; Servicy does not attempt to use the heartbeat
120
+ connection for anything. It simply connects, and if successful, disconnects
121
+ again. For this reason, your services should be resilliant to this type of
122
+ traffic.
123
+
124
+ Along with the required information already listed, a service can provide some
125
+ optional information about the API that they provide. This information comes
126
+ with some pieces of information for each API endpoint or action provided. The
127
+ information is:
128
+
129
+ 1. Endpoint identifier
130
+ 2. Parameter information
131
+ 3. Return information
132
+ 4. (optional) Human-readable documentation.
133
+
134
+ The endpoint identifier can be anything, and is just a string. For example, in
135
+ an HTTP/S RESTful API, it may be something like:
136
+
137
+ ```
138
+ /api/v1/user/login
139
+ ```
140
+
141
+ The parameter information is information about each parameter that is expected
142
+ by this endpoint. This data is structured, and requires information about name,
143
+ data type, and weather or not the parameter is required.
144
+
145
+ The return information is simply a description of the data that should be
146
+ expected to return.
147
+
148
+ The documentation is an arbitrary-length text string which can be used as
149
+ documentation by a developer developing against the API.
150
+
151
+ Multiple service providers can provide the same service. In that case, they are
152
+ simply added to a pool of possible providers that is selected from using some
153
+ load-balancing strategy such as round-robin or random selection.
154
+
155
+ Any service provider that has registered will periodically get ICMP pinged by
156
+ Servicy. If a response does not come back, it will be removed from the pool of
157
+ providers. If a response does come back, Servicy will then attempt to connect
158
+ to the heartbeat port to determine if the service is still functioning on the
159
+ box. If it is not, the provider will be removed from the pool, otherwise it
160
+ will be left in. This two-step process is to facilitate better logging and
161
+ debugging of issues.
162
+
163
+ Service de-registration
164
+ -----------------------
165
+
166
+ A service can remove itself from the pool voluntarily be simply sending the
167
+ name, version, host, and port of the service to be removed. However, this is
168
+ not strictly needed as the periodic heartbeat check will notice if a service
169
+ goes away. These checks happen every second.
170
+
171
+ A note on security
172
+ ++++++++++++++++++
173
+
174
+ For the time-being, it is assumed that you will have Servicy and all the
175
+ services it interacts with installed on internal, secured infrastructure. As
176
+ such, Servicy takes a very trusting policy. However, that means that someone
177
+ with malintent and access to your network could de-register services, or
178
+ register a bunch of fake ones. Future versions of Servicy may attempt to
179
+ resolve this issue. We'll see...
180
+
181
+ Service discovery
182
+ -----------------
183
+
184
+ Once services are registered, a client needs to be able to discover them.
185
+ Generally, they will discover a service and hold on to that information for the
186
+ lifetime of their use, only requesting a new handle should the old one be lost.
187
+ However, there is no reason why they couldn't request a new one every time.
188
+
189
+ *NOTE* In the future, it may be possible for Servicy to act as a router by
190
+ simply returning connection information for itself, and doing blind
191
+ pass-through with some strategy to the other providers. In that case, the
192
+ policy that a client uses to cache or not cache their connection information
193
+ may have to change.
194
+
195
+ A service consumer can query using one or both of two different dimensions:
196
+
197
+ 1. API name
198
+ 2. API functionality
199
+
200
+ When using the name dimension, you can also query by version using either an
201
+ exact match, a list of preferences, a range, a minimum, or a maximum.
202
+
203
+ When querying using functionality, you query using one or more API method
204
+ fingerprints. The fingerprints are in the following format:
205
+
206
+ ```
207
+ method_name#arg-type,arg-type,...#return_type
208
+ ```
209
+
210
+ where method_name is the name of the method you would like to call (or service
211
+ end-point, or whatever. This is the same as the API endpoint in the service
212
+ functionality registration information.), the `arg-type` pairs are the name of
213
+ an argument (or an empty string in the case of un-named arguments), followed by
214
+ a dash, followed by the argument type, and the return_type being the data-type
215
+ for the return value.
216
+
217
+ For example, a query could look something like this:
218
+
219
+ ```
220
+ user_create#username-string,password-string#User
221
+ ```
222
+
223
+ Behind the scenes, Servicy will use this information to find all possible
224
+ services that can fulfill this request. This will include any service that can
225
+ perform this request but that may have more optional parameters. Type
226
+ information can also be provided as a list of types, in the case of API's that
227
+ can handle multiple types of information:
228
+
229
+ ```
230
+ user_create#username-string,password-string|signed_string#User
231
+ ```
232
+
233
+ The possible other types that the data can be are separated by a pipe, '|'.
234
+
235
+ Finally, the service consumer can provide a boolean flag indicating weather or
236
+ not they wish to receive API information about the returned providers, or if
237
+ they simply want connection information. If the flag is set to true, they will
238
+ get full API documentation and definitions along with connection information.
239
+
240
+ The query system will either provide a successful "error"-code, along with a
241
+ list of one or more service providers, a "not-found" error-code indicating that
242
+ the query was valid, but that there are no available services which meet the
243
+ requirements, or an "error" error-code that indicates that something was wrong
244
+ with the provided query. In the last two queries, additional error information
245
+ may be provided, but should be considered for human-use only; ie, showing an
246
+ error message to a user.
247
+
248
+ TODO
249
+ ====
250
+
251
+ Things left to do:
252
+
253
+ 1. In the command-line tool, there is some kind of bug where the registration
254
+ message never returns back to the client. This causes registration to hang
255
+ 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.
data/Servicy.gemspec ADDED
@@ -0,0 +1,47 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: servicy 0.0.3 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "servicy"
9
+ s.version = "0.0.3"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Thomas Luce"]
14
+ s.date = "2014-08-11"
15
+ s.description = "A service registration and discovery framework, as a server and client library."
16
+ s.email = "thomas.luce@gmail.com"
17
+ s.executables = ["servicy"]
18
+ s.extra_rdoc_files = [
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ "README.md",
23
+ "Servicy.gemspec",
24
+ "VERSION",
25
+ "bin/servicy",
26
+ "lib/api.rb",
27
+ "lib/client.rb",
28
+ "lib/hash.rb",
29
+ "lib/load_balancer.rb",
30
+ "lib/load_balancer/random.rb",
31
+ "lib/load_balancer/round_robin.rb",
32
+ "lib/server.rb",
33
+ "lib/server/server.rb",
34
+ "lib/server/service_searcher.rb",
35
+ "lib/service.rb",
36
+ "lib/servicy.rb",
37
+ "lib/transport/in_memory_transport.rb",
38
+ "lib/transport/messages.rb",
39
+ "lib/transport/tcp_transport.rb",
40
+ "lib/transports.rb",
41
+ "test.rb"
42
+ ]
43
+ s.homepage = "https://github.com/thomasluce/servicy"
44
+ s.rubygems_version = "2.2.2"
45
+ s.summary = "A service registration and discovery framework"
46
+ end
47
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.3
data/bin/servicy ADDED
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'ostruct'
5
+ require 'rubygems'
6
+ require 'servicy'
7
+ require 'transport/tcp_transport'
8
+ require 'pp'
9
+ require 'fileutils'
10
+
11
+ VERSION = File.read(File.expand_path(File.join(File.dirname(__FILE__), '..', 'VERSION')))
12
+
13
+ # The following couple of functions are taking from the daemons gem. I don't
14
+ # need all it's functionality, and it's configuration options don't give me
15
+ # what I want. Given that I only need very simple daemonizing options here,
16
+ # this will be fine.
17
+ def safefork
18
+ tryagain = true
19
+ while tryagain
20
+ tryagain = false
21
+ begin
22
+ if pid = fork
23
+ return pid
24
+ else
25
+ return false
26
+ end
27
+ rescue Errno::EWOULDBLOCK
28
+ sleep 5
29
+ tryagain = true
30
+ end
31
+ end
32
+ end
33
+
34
+ def redirect_io(logfile_name)
35
+ begin; STDIN.reopen "/dev/null"; rescue ::Exception; end
36
+
37
+ if logfile_name
38
+ begin
39
+ STDOUT.reopen logfile_name, "a"
40
+ File.chmod(0644, logfile_name)
41
+ STDOUT.sync = true
42
+ rescue ::Exception
43
+ begin; STDOUT.reopen "/dev/null"; rescue ::Exception; end
44
+ end
45
+ else
46
+ begin; STDOUT.reopen "/dev/null"; rescue ::Exception; end
47
+ end
48
+
49
+ begin; STDERR.reopen STDOUT; rescue ::Exception; end
50
+ STDERR.sync = true
51
+ end
52
+
53
+ def make_daemon(logfile_name=nil, pidfile_name=nil)
54
+ # Fork and exit from the parent
55
+ safefork and exit
56
+
57
+ # Detach from the controlling terminal
58
+ unless sess_id = Process.setsid
59
+ raise 'cannot detach from controlling terminal'
60
+ end
61
+
62
+ # Prevent the possibility of acquiring a controlling terminal
63
+ trap 'SIGHUP', 'IGNORE'
64
+ exit if pid = safefork
65
+
66
+ # Release old working directory
67
+ Dir.chdir "/"
68
+
69
+ # Make stdout and stderr go to our log if defined. /dev/null otherwise.
70
+ redirect_io(logfile_name)
71
+
72
+ # Split rand streams between spawning and daemonized process
73
+ srand
74
+
75
+ # Write a pidfile
76
+ File.open(pidfile_name, 'w') { |f| f.puts Process.pid }
77
+
78
+ # Set the service name so that ps makes sense.
79
+ $0 = 'servicy'
80
+
81
+ return sess_id
82
+ end
83
+
84
+ class ServicyOptionParser
85
+ def self.parse(command, command_command, args)
86
+ options = OpenStruct.new
87
+ options.port = 8000
88
+ options.daemonize = false
89
+ options.config_file = File.expand_path("~/.servicy.conf")
90
+ options.pid_file = File.expand_path("./servicy.pid")
91
+ options.log_file = File.expand_path('./servicy.log')
92
+ options.service_version = "1.0.0"
93
+ options.protocol = "HTTP/S"
94
+ options.server_host = "localhost"
95
+ options.command = %w(client server).include?(command) ? command : nil
96
+ options.command_command = %w(start stop status info register unregister search).include?(command_command) ? command_command : nil
97
+
98
+ OptionParser.new do |opts|
99
+ opts.banner = "Usage: servicy [command] [command command] [command options]"
100
+ opts.separator "Where command is one of: server, client"
101
+ opts.separator "And command-command is a command to send to either the client or the server."
102
+ opts.separator ""
103
+
104
+ opts.separator "Server commands:"
105
+ opts.separator " start - Start the server"
106
+ opts.separator " stop - Stop a running server"
107
+ opts.separator " status - Get weather or not a server is running"
108
+ opts.separator " info - Get info about a running server"
109
+
110
+ opts.separator ""
111
+ opts.separator "Server command options:"
112
+
113
+ opts.on '-p', '--port PORT', Integer, "Set the port for the server to listen on. Defaults to #{options.port}" do |port|
114
+ options.port = port
115
+ end
116
+
117
+ opts.on '-d', '--daemon', "Daemonize after starting server. Defaults to #{options.daemonize}" do
118
+ options.daemonize = true
119
+ end
120
+
121
+ opts.on '-f', '--config CONFIG_FILE', String, "Set the location for the service config file. Defaults to #{options.config_file}" do |file|
122
+ options.config_file = File.expand_path(file)
123
+ end
124
+
125
+ opts.on '-i', '--pid PID_FILE', String, "Set the PID file location. Defaults to #{options.pid_file}" do |file|
126
+ options.pid_file = File.expand_path(file)
127
+ end
128
+
129
+ opts.on '-l', '--logfile LOG_FILE', String, "Location of log file. Defaults to #{options.log_file}" do |file|
130
+ options.log_file = File.expand_path(file)
131
+ end
132
+
133
+ opts.separator ""
134
+ opts.separator "Client commands:"
135
+ opts.separator " register - Register a new service"
136
+ opts.separator " unregister - Un-register a registered service"
137
+ opts.separator " search - Search for a registered service"
138
+
139
+ opts.separator ""
140
+ opts.separator "Client command options:"
141
+
142
+ opts.on '-n', '--name NAME', String, "The name of the service" do |name|
143
+ options.service_name = name
144
+ end
145
+
146
+ opts.on '-h', '--host HOST', String, "The hostname or IP-address of the service" do |host|
147
+ options.service_host = host
148
+ end
149
+
150
+ opts.on '-H', '--server-host HOST', String, "The host where the service registrar can be found. Defaults to #{options.server_host}" do |host|
151
+ options.server_host = host
152
+ end
153
+
154
+ opts.on '-s', '--service-port PORT', Integer, "The port the service is on" do |port|
155
+ options.service_port = port
156
+ end
157
+
158
+ opts.on '-b', '--heartbeat-port PORT', Integer, "The port that the services' heartbeat service lives. Defaults to the service port" do |port|
159
+ options.heartbeat_port = port
160
+ end
161
+
162
+ opts.on '-e', '--service-version VERSION', String, "The version of the service. Defaults to #{options.service_version}" do |version|
163
+ options.version = version
164
+ end
165
+
166
+ opts.on '-r', '--protocol PROTO', String, "The protocol of the service. Defaults to #{options.protocol}" do |protocol|
167
+ options.protocol = protocol
168
+ end
169
+
170
+ opts.on '-a', '--api API', String, "API description for the service. See documentation for shorthand help." do |api|
171
+ options.api = api
172
+ end
173
+
174
+ opts.separator ""
175
+ opts.separator "Common options:"
176
+
177
+ opts.on "--help", "Show this message and exit" do
178
+ puts opts
179
+ exit
180
+ end
181
+
182
+ opts.on "-v", "--version", "Show current version and exit" do
183
+ puts VERSION
184
+ exit
185
+ end
186
+
187
+ opts.separator ""
188
+ opts.separator "Examples:"
189
+ opts.separator ""
190
+
191
+ opts.separator "Start a service discovery/registration server on port 3214"
192
+ opts.separator " servicy server start -p 3214"
193
+ opts.separator ""
194
+
195
+ opts.separator "Register a service at the server on 192.168.0.1, port 1234"
196
+ opts.separator " servicy client register -n com.foo.bar -p 1234 -H localhost -h localhost -s 1234"
197
+ opts.separator ""
198
+
199
+ opts.separator "Find a service"
200
+ opts.separator " servicy client search -H 192.168.0.1 -n 'com.bar.foo'"
201
+ opts.separator ""
202
+
203
+ if !command || !command_command
204
+ puts opts
205
+ exit
206
+ end
207
+ end.parse!
208
+
209
+ return options
210
+ end
211
+ end
212
+
213
+ class Command
214
+ def initialize(options)
215
+ @options = options
216
+ end
217
+
218
+ def method_missing(name, *args, &block)
219
+ @options[name]
220
+ end
221
+ end
222
+
223
+ class ServerCommand < Command
224
+ def start
225
+ transport = Servicy::TCPTransport.new(port: port)
226
+ if File.exists?(config_file)
227
+ server = Servicy::Server.load(config_file)
228
+ server.transport = transport
229
+ else
230
+ server = Servicy::Server.new(transport, nil, STDOUT, config_file)
231
+ end
232
+
233
+ make_daemon(log_file, pid_file) if daemonize
234
+ server.go!
235
+ end
236
+
237
+ def stop
238
+ # TODO: At some point in the future, this should send a shutdown command to
239
+ # give the server time to do its thing gracefully.
240
+ raise "Pid not found" if pid == 0 || !pid
241
+ Process.kill('TERM', pid)
242
+ FileUtils.rm(pid_file)
243
+ end
244
+
245
+ def info
246
+ client = Servicy::Client.new(Servicy::TCPTransport.new(port: port))
247
+ pp client.server_stats.struct
248
+ end
249
+
250
+ def status
251
+ Process.getpgid(pid)
252
+ puts "Server is running"
253
+ rescue
254
+ puts "Server is not running"
255
+ end
256
+
257
+ private
258
+
259
+ def pid
260
+ File.read(pid_file).strip.to_i
261
+ end
262
+ end
263
+
264
+ class ClientCommand < Command
265
+ def register
266
+ client.register_service({name: service_name,
267
+ host: service_host,
268
+ port: service_port,
269
+ heatbeat_port: heartbeat_port,
270
+ version: version,
271
+ protocol: protocol
272
+ })
273
+
274
+ # TODO: api parsing and registration. I want to get the search done for
275
+ # that first.
276
+ end
277
+
278
+ def unregister
279
+ end
280
+
281
+ def search
282
+ puts client.find_service(name: service_name,
283
+ host: service_host,
284
+ port: service_port,
285
+ heartbeat_port: heartbeat_port,
286
+ version: version,
287
+ protocol: protocol
288
+ ).as_json.to_json
289
+ end
290
+
291
+ private
292
+
293
+ def client
294
+ Servicy::Client.new(Servicy::TCPTransport.new(port: port))
295
+ end
296
+ end
297
+
298
+ if ARGV.first.to_s.start_with? "-"
299
+ ServicyOptionParser.parse(nil, nil, ARGV)
300
+ exit # Help or version
301
+ else
302
+ options = ServicyOptionParser.parse(ARGV.shift, ARGV.shift, ARGV)
303
+ klass = options.command == 'server' ? ServerCommand : ClientCommand
304
+ klass.new(options).send(options.command_command)
305
+ end