servicy 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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