gmalamid-spinal 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 nutrun.com, Andy Kent, George Malamidis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,12 @@
1
+ --- It's still very early days and this is experimental code, read: "Here be dragons" ---
2
+
3
+ Spinal is lightweight middleware for bidirectional process distribution over TCP.
4
+ It comes in three parts all currently implemented in Ruby:
5
+
6
+ spinald - This is the server daemon, it manages connections and distributes requests. Servers are cluster-able in a share nothing style architecture.
7
+
8
+ Spinal Client Library - The client Library makes it super simple to delegate work to a Spinal Service, requests can be blocking or asynchronous.
9
+
10
+ Spinal Service Library - Use the service library to implement workers. As with servers, services are cluster-able and distributed.
11
+
12
+ See etc/demo for a simplified example using Rack as a client to a directory service.
@@ -0,0 +1,16 @@
1
+ require "rubygems"
2
+ require "rake/testtask"
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.pattern = "test/*_test.rb"
8
+ end
9
+
10
+ task(:check_gemspec) do
11
+ require 'rubygems/specification'
12
+ data = File.read('spinal.gemspec')
13
+ spec = nil
14
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
15
+ puts spec
16
+ end
@@ -0,0 +1,8 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'spinal', 'spinald')
2
+
3
+ Spinal.debug = true
4
+
5
+ EM.run do
6
+ trap(:INT) {EM.stop}
7
+ EM.start_server("localhost", 8888, Spinal::Spinald)
8
+ end
@@ -0,0 +1,22 @@
1
+ require "rubygems"
2
+ require "thin"
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..', '..', 'lib')
5
+
6
+ require "spinal/thin_deferrable_connection"
7
+ require "spinal/rack_remote_query"
8
+
9
+ Thin::Connection.send(:include, Spinal::ThinDeferrableConnection)
10
+
11
+ class Webapp
12
+ include Spinal::Rack::RemoteQuery
13
+
14
+ def call(env)
15
+ request = Rack::Request.new(env)
16
+ remote_query('wiretap', request.params.to_yaml) do |response|
17
+ [response.status, {'Content-Type' => 'text/plain'}, response.body]
18
+ end
19
+ end
20
+ end
21
+
22
+ Rack::Handler::Thin.run(Webapp.new, :Host => 'localhost', :Port => 4567)
@@ -0,0 +1,26 @@
1
+ Requests
2
+
3
+ Query a resource identified by URI
4
+ ========================
5
+ QUERY
6
+ address
7
+ ASCII payload
8
+ ___END___
9
+ ========================
10
+
11
+ Register Service with Spinal on resource identified by URI
12
+ ========================
13
+ REGISTER
14
+ address
15
+ host:port
16
+ ___END___
17
+ ========================
18
+
19
+
20
+ Responses
21
+ ========================
22
+ RESPONSE
23
+ [200|400|...]
24
+ ASCII body
25
+ ___END___
26
+ ========================
@@ -0,0 +1,28 @@
1
+ require "socket"
2
+ require "fcntl"
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..')
5
+
6
+ require "shared"
7
+
8
+ module Spinal
9
+ module Client
10
+ class Blocking
11
+ def initialize(host, port)
12
+ @socket = TCPSocket.new(host, port.to_i)
13
+ @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
14
+ end
15
+
16
+ def query(address, payload)
17
+ packet = Packet::Query.new(address, payload)
18
+ @socket.write(packet.raw)
19
+ packet_parser, resp = PacketParser.new, []
20
+ loop do
21
+ str = @socket.gets("\r\n").strip
22
+ packet_parser.parse(str)
23
+ return packet_parser.packet if packet_parser.has_packet?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+
4
+ $: << File.join(File.dirname(__FILE__), '..')
5
+
6
+ require "shared"
7
+
8
+ module Spinal
9
+ module Client
10
+ module Evented
11
+ def query(uri, payload = '', &response)
12
+ send_data(Packet::Query.new(uri, payload).raw)
13
+ @response = response
14
+ end
15
+
16
+ def receive_data(data)
17
+ packet_parser.parse(data)
18
+ packet_parser.each_packet { |packet| @response.call(packet) }
19
+ end
20
+
21
+ def packet_parser
22
+ @packet_parser ||= PacketParser.new
23
+ end
24
+
25
+ def self.connect(opts={})
26
+ client = nil
27
+ EM::safe_run(opts[:background]) do
28
+ client = EM.connect(opts[:host] || '0.0.0.0', opts[:port] || 8888, self)
29
+ end
30
+ client
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module EventMachine
37
+ def EventMachine::safe_run(background = nil, &block)
38
+ if EM::reactor_running?
39
+ EM::next_tick(&block)
40
+ sleep unless background
41
+ else
42
+ if background
43
+ $em_reactor_thread = Thread.new do
44
+ EM::run(&block)
45
+ end
46
+ else
47
+ EM::run(&block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ module Spinal
2
+ module Rack
3
+ module RemoteQuery
4
+ def remote_query(address, payload='', &response_callback)
5
+ @response = Spinal::ThinDeferrableConnection::DeferredResponse.new(&response_callback)
6
+ connection.send_data(Packet::Query.new(address, payload).raw)
7
+ @response
8
+ end
9
+
10
+ def receive_packet(packet)
11
+ @response.deliver(packet)
12
+ end
13
+
14
+ def connection
15
+ @connection ||= EM.connect('0.0.0.0', 8888, SpinalConnection, self)
16
+ end
17
+
18
+ module SpinalConnection
19
+ def initialize(client)
20
+ super
21
+ @client = client
22
+ end
23
+
24
+ def receive_data(data)
25
+ packet_parser.parse(data)
26
+ packet_parser.each_packet { |packet| @client.receive_packet(packet) }
27
+ end
28
+
29
+ def packet_parser
30
+ @packet_parser ||= PacketParser.new
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+
4
+ $: << File.dirname(__FILE__)
5
+
6
+ require "shared"
7
+ require "service/registration"
8
+ require "service/connection"
9
+ require "service/runner"
10
+ require "service/request"
11
+
12
+ module Spinal
13
+ module Service
14
+ def call(request)
15
+ raise "A Spinal::Service must implement the call(request) method"
16
+ end
17
+
18
+ module ClassMethods
19
+ def run_as(address, opts={})
20
+ opts[:address] = address
21
+ Runner.new(self.new, opts).run
22
+ end
23
+ end
24
+
25
+ def self.included(target)
26
+ target.extend(ClassMethods)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module Spinal
2
+ module Service
3
+ module Connection
4
+ attr_writer :service, :servers, :address, :host, :port
5
+
6
+ def receive_data(data)
7
+ packet_parser.parse(data)
8
+ packet_parser.each_packet do |packet|
9
+ request = Request.new(@service)
10
+ request.callback {|response| send_data(response)}
11
+ request.handle(packet)
12
+ end
13
+ end
14
+
15
+ def service
16
+ @service || proc {|req| "No service specified."}
17
+ end
18
+
19
+ def packet_parser
20
+ @packet_parser ||= PacketParser.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ module Spinal
2
+ module Registration
3
+ def receive_data(data)
4
+ packet_parser.parse(data)
5
+ packet_parser.each_packet do |packet|
6
+ @status = packet.status
7
+ end
8
+ end
9
+
10
+ def bind(servers, service_address, service_host, service_port)
11
+ servers.each do |server|
12
+ server_host, server_port = server.split(':')
13
+ conn = EM.connect(server_host, server_port.to_i, self)
14
+ packet = Packet::Register.new(service_address, service_host, service_port).raw
15
+ conn.send_data(packet)
16
+ end
17
+ end
18
+ module_function :bind
19
+
20
+ def unbind
21
+ if @status == StatusCodes::OK
22
+ puts "Registration complete!"
23
+ else
24
+ puts "[WARN] failed to register with spinald"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def packet_parser
31
+ @packet_parser ||= PacketParser.new
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module Spinal
2
+ module Service
3
+ class Request
4
+ include EM::Deferrable
5
+ attr_reader :service
6
+
7
+ def initialize(service)
8
+ @service = service
9
+ end
10
+
11
+ def handle(packet)
12
+ @packet = packet
13
+ EM.spawn do |request|
14
+ response = begin
15
+ [StatusCodes::OK, request.service.call(request.payload)]
16
+ rescue Spinal::Error => e
17
+ [e.code, e.message]
18
+ rescue Exception => e
19
+ [StatusCodes::SERVER_ERROR, e.message]
20
+ end
21
+ request.set_deferred_success(Packet::Response.new(*response).raw)
22
+ end.notify(self)
23
+ end
24
+
25
+ def payload
26
+ @packet.payload
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ module Spinal
2
+ module Service
3
+ class Runner
4
+ DEFAULT_OPTIONS = {
5
+ :host => "127.0.0.1",
6
+ :port => 5000,
7
+ :servers => ["127.0.0.1:8888"]
8
+ }
9
+
10
+ attr_reader :service
11
+
12
+ def initialize(service, options)
13
+ @service = service
14
+ @options = DEFAULT_OPTIONS.merge(options)
15
+ end
16
+
17
+ def run
18
+ trap(:INT) {EM.stop}
19
+ EM.run do
20
+ EM.start_server(host, port, Spinal::Service::Connection) do |t|
21
+ t.service,t.host, t.port = service, host, port
22
+ end
23
+ puts "#{service} (#{address}) listening on #{host}:#{port}"
24
+ Registration.bind(servers, address, host, port)
25
+ end
26
+ end
27
+
28
+ def host
29
+ @options[:host]
30
+ end
31
+
32
+ def servers
33
+ @options[:servers]
34
+ end
35
+
36
+ def address
37
+ @options[:address]
38
+ end
39
+
40
+ def port
41
+ @options[:port]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,6 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require "shared/packet"
4
+ require "shared/packet_parser"
5
+ require "shared/status_codes"
6
+ require "shared/error"
@@ -0,0 +1,12 @@
1
+ module Spinal
2
+ class Error < Exception
3
+ attr_reader :code
4
+ end
5
+
6
+ class NotFound < Error
7
+ def initialize(message)
8
+ super
9
+ @code = Spinal::StatusCodes::NOT_FOUND
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ module Spinal
2
+ module Packet
3
+ BOUNDARY = "___END___" unless defined?(BOUNDARY)
4
+ CR = "\r\n" unless defined?(CR)
5
+
6
+ def self.from_a(ary)
7
+ type = ary.shift
8
+ case type
9
+ when "QUERY" : Query.from_a(ary)
10
+ when "RESPONSE" : Response.from_a(ary)
11
+ when "REGISTER" : Register.from_a(ary)
12
+ else raise("Unrecognized packet type '#{type}'")
13
+ end
14
+ end
15
+
16
+ class Query
17
+ attr_reader :address, :payload
18
+
19
+ def initialize(address, payload)
20
+ @address, @payload = address, payload
21
+ end
22
+
23
+ def self.from_a(ary)
24
+ self.new(ary.shift, ary.join(CR))
25
+ end
26
+
27
+ def raw
28
+ ["QUERY", @address, @payload, "#{BOUNDARY}#{CR}"] * CR
29
+ end
30
+ end
31
+
32
+
33
+ class Response
34
+ attr_reader :status, :body
35
+
36
+ def initialize(status, body = '')
37
+ @status, @body = status.to_i, body
38
+ end
39
+
40
+ def self.from_a(ary)
41
+ self.new(ary.shift, ary.join(CR))
42
+ end
43
+
44
+ def raw
45
+ ["RESPONSE", @status, @body, "#{BOUNDARY}#{CR}"] * CR
46
+ end
47
+ end
48
+
49
+ class Register
50
+ attr_reader :address, :host, :port
51
+
52
+ def initialize(address, host, port)
53
+ @address, @host, @port = address, host, port.to_i
54
+ end
55
+
56
+ def self.from_a(ary)
57
+ address = ary.shift
58
+ host, port = ary.shift.split(":")
59
+ self.new(address, host, port)
60
+ end
61
+
62
+ def raw
63
+ ["REGISTER", @address, "#{@host}:#{@port}", "#{BOUNDARY}#{CR}"] * CR
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ module Spinal
2
+ class PacketParser
3
+ def initialize
4
+ @packets, @data = [], ''
5
+ end
6
+
7
+ def parse(packet)
8
+ @data << packet
9
+ tokens, remainder = [], ''
10
+ #FIXME: OUCH!! This will iterate through the entire payload/body...
11
+ @data.split(Packet::CR).each do |token|
12
+ unless token == Packet::BOUNDARY
13
+ tokens << token
14
+ remainder << "#{token}#{Packet::CR}"
15
+ else
16
+ @packets << Packet.from_a(tokens)
17
+ tokens, remainder = [], ''
18
+ end
19
+ end
20
+ @data = remainder
21
+ end
22
+
23
+ def each_packet
24
+ while packet = @packets.pop
25
+ yield packet
26
+ end
27
+ end
28
+
29
+ def packet
30
+ @packets.pop
31
+ end
32
+
33
+ def has_packet?
34
+ @packets.size == 1
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ module Spinal
2
+ module StatusCodes
3
+ OK = 200
4
+ NOT_FOUND = 404
5
+ UNKNOWN_RESOURCE = 418
6
+ SERVER_ERROR = 500
7
+ end
8
+ end
@@ -0,0 +1,118 @@
1
+ require "rubygems"
2
+ require "eventmachine"
3
+
4
+ $: << File.dirname(__FILE__)
5
+
6
+ require "shared"
7
+
8
+ module Spinal
9
+ #FIXME: Tmp hacks
10
+ def debug(str) puts str if @debug end
11
+ module_function :debug
12
+ def debug=(bool) @debug = bool end
13
+ module_function :debug=
14
+
15
+ # Keep tmp hacks above this line
16
+ # ==================================================
17
+
18
+ module Spinald
19
+
20
+ def receive_data(data)
21
+ Spinal.debug "Spinal received: #{data}"
22
+ packet_parser.parse(data)
23
+ packet_parser.each_packet { |packet| handle_request(packet) }
24
+ end
25
+
26
+ def handle_request(packet)
27
+ case packet.class.name
28
+ when 'Spinal::Packet::Query' : handle_query(packet)
29
+ when 'Spinal::Packet::Register' : handle_registration(packet)
30
+ else raise("Unknown packet type: #{packet.class.inspect}")
31
+ end
32
+ end
33
+
34
+ def handle_query(packet)
35
+ if resource = resources[packet.address]
36
+ resource.request(packet.raw) { |resp| send_data(resp) }
37
+ else
38
+ send_data(Packet::Response.new(StatusCodes::UNKNOWN_RESOURCE).raw)
39
+ end
40
+ end
41
+
42
+ def handle_registration(packet)
43
+ resources[packet.address] ||= Resource.new(packet.address)
44
+ resources[packet.address].add_service(packet.host, packet.port)
45
+ send_data(Packet::Response.new(StatusCodes::OK).raw)
46
+ close_connection_after_writing
47
+ end
48
+
49
+ def resources
50
+ @@resources ||= {}
51
+ end
52
+
53
+ def packet_parser
54
+ @packet_parser ||= PacketParser.new
55
+ end
56
+
57
+ class Resource
58
+ def initialize(address)
59
+ @address = address
60
+ @queue = []
61
+ @pool = ServiceConnectionPool.new
62
+ end
63
+
64
+ def add_service(host, port)
65
+ @pool.connect_service(host, port)
66
+ end
67
+
68
+ def request(data, &reply_callback)
69
+ if conn = @pool.reserve
70
+ conn.reply &reply_callback
71
+ conn.release do
72
+ @pool.release(conn)
73
+ if queued_request = @queue.pop
74
+ request(queued_request[0], &queued_request[1])
75
+ end
76
+ end
77
+ conn.send_data(data)
78
+ else
79
+ @queue.unshift([data, reply_callback])
80
+ end
81
+ end
82
+ end
83
+
84
+ class ServiceConnectionPool
85
+ def initialize
86
+ @connections = []
87
+ end
88
+
89
+ def connect_service(host, port)
90
+ @connections.unshift(EM.connect(host, port, ServiceConnection))
91
+ end
92
+
93
+ def reserve
94
+ @connections.pop
95
+ end
96
+
97
+ def release(conn)
98
+ @connections.unshift(conn)
99
+ end
100
+ end
101
+
102
+ module ServiceConnection
103
+ def receive_data(data)
104
+ Spinal.debug "ServiceConnection received: #{data}"
105
+ @reply.call(data)
106
+ @release.call
107
+ end
108
+
109
+ def reply(&cb)
110
+ @reply = cb
111
+ end
112
+
113
+ def release(&cb)
114
+ @release = cb
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,40 @@
1
+ $: << File.dirname(__FILE__)
2
+ require "shared"
3
+
4
+ module Spinal
5
+ module ThinDeferrableConnection
6
+ def process
7
+ if threaded?
8
+ @request.threaded = true
9
+ EventMachine.defer(method(:pre_process), method(:post_process))
10
+ else
11
+ @request.threaded = false
12
+ pre_process_result = pre_process
13
+ if pre_process_result.is_a?(DeferredResponse)
14
+ pre_process_result.post_process_callback { |res| post_process(res) }
15
+ else
16
+ post_process(pre_process_result)
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.included(target)
22
+ target.send(:remove_method, :process)
23
+ end
24
+
25
+ class DeferredResponse
26
+ def initialize(&response_callback)
27
+ @response_callback = response_callback
28
+ end
29
+
30
+ def post_process_callback(&post_process_callback)
31
+ @post_process_callback = post_process_callback
32
+ end
33
+
34
+ def deliver(packet)
35
+ result = @response_callback.call(packet)
36
+ @post_process_callback.call(result)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ GEMSPEC =Gem::Specification.new do |s|
2
+ s.name = 'spinal'
3
+ s.version = '0.0.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.rubyforge_project = "spinal"
6
+ s.summary, s.description = 'Lightweight middleware for bidirectional process distribution over TCP'
7
+ s.authors = 'Andy Kent, George Malamidis'
8
+ s.email = 'george@nutrun.com'
9
+ # s.homepage = 'http://spinal.rubyforge.org'
10
+ s.has_rdoc = false
11
+ s.rdoc_options += ['--quiet', '--title', 'Spinal', '--main', 'README']
12
+ s.extra_rdoc_files = ['README', 'COPYING']
13
+ s.files = [
14
+ "COPYING",
15
+ "Rakefile",
16
+ "README",
17
+ "spinal.gemspec",
18
+ "etc/demo/daemon.rb",
19
+ "etc/demo/service.rb",
20
+ "etc/demo/web.rb",
21
+ "etc/protocol.txt",
22
+ "lib/spinal/client/blocking.rb",
23
+ "lib/spinal/client/evented.rb",
24
+ "lib/spinal/rack_remote_query.rb",
25
+ "lib/spinal/service/connection.rb",
26
+ "lib/spinal/service/registration.rb",
27
+ "lib/spinal/service/request.rb",
28
+ "lib/spinal/service/runner.rb",
29
+ "lib/spinal/service.rb",
30
+ "lib/spinal/shared/error.rb",
31
+ "lib/spinal/shared/packet.rb",
32
+ "lib/spinal/shared/packet_parser.rb",
33
+ "lib/spinal/shared/status_codes.rb",
34
+ "lib/spinal/shared.rb",
35
+ "lib/spinal/spinald.rb",
36
+ "lib/spinal/thin_deferrable_connection.rb"
37
+ ]
38
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gmalamid-spinal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Kent, George Malamidis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: george@nutrun.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ - COPYING
25
+ files:
26
+ - COPYING
27
+ - Rakefile
28
+ - README
29
+ - spinal.gemspec
30
+ - etc/demo/daemon.rb
31
+ - etc/demo/service.rb
32
+ - etc/demo/web.rb
33
+ - etc/protocol.txt
34
+ - lib/spinal/client/blocking.rb
35
+ - lib/spinal/client/evented.rb
36
+ - lib/spinal/rack_remote_query.rb
37
+ - lib/spinal/service/connection.rb
38
+ - lib/spinal/service/registration.rb
39
+ - lib/spinal/service/request.rb
40
+ - lib/spinal/service/runner.rb
41
+ - lib/spinal/service.rb
42
+ - lib/spinal/shared/error.rb
43
+ - lib/spinal/shared/packet.rb
44
+ - lib/spinal/shared/packet_parser.rb
45
+ - lib/spinal/shared/status_codes.rb
46
+ - lib/spinal/shared.rb
47
+ - lib/spinal/spinald.rb
48
+ - lib/spinal/thin_deferrable_connection.rb
49
+ has_rdoc: false
50
+ homepage:
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --quiet
54
+ - --title
55
+ - Spinal
56
+ - --main
57
+ - README
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project: spinal
75
+ rubygems_version: 1.2.0
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: Lightweight middleware for bidirectional process distribution over TCP
79
+ test_files: []
80
+