pokan-cluster 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +65 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +65 -0
- data/lib/pokan-cluster.rb +247 -0
- data/lib/pokan-cluster/autorunner.rb +34 -0
- data/lib/pokan-cluster/config_defaults.rb +21 -0
- data/lib/pokan-cluster/configuration.rb +77 -0
- data/lib/pokan-cluster/log_messages.rb +48 -0
- data/lib/pokan-cluster/prepared_string.rb +59 -0
- data/lib/pokan-cluster/version.rb +7 -0
- data/lib/server_configuration.rb +119 -0
- data/pokan.log +10 -0
- data/spec/pokan-cluster/autorunner_spec.rb +14 -0
- data/spec/pokan-cluster/configuration_spec.rb +44 -0
- data/spec/pokan-cluster/prepared_string_spec.rb +34 -0
- data/spec/pokan-cluster_spec.rb +64 -0
- data/spec/spec_helper.rb +4 -0
- data/test_server.rb +17 -0
- metadata +110 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
group :development do
|
4
|
+
gem "rspec"
|
5
|
+
gem "guard-rspec"
|
6
|
+
gem "bundler"
|
7
|
+
gem "jeweler"
|
8
|
+
gem "pry"
|
9
|
+
end
|
10
|
+
|
11
|
+
gem "pokan", "~> 0.1.0rc2"
|
12
|
+
gem "host"
|
13
|
+
gem "yagni", "~> 1.2"
|
14
|
+
gem "logging"
|
15
|
+
|
16
|
+
#TODO: remove this group
|
17
|
+
group :local do
|
18
|
+
gem "redis"
|
19
|
+
gem "eventmachine"
|
20
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (0.9.8)
|
5
|
+
diff-lcs (1.1.3)
|
6
|
+
eventmachine (0.12.10)
|
7
|
+
git (1.2.5)
|
8
|
+
guard (0.8.4)
|
9
|
+
thor (~> 0.14.6)
|
10
|
+
guard-rspec (0.4.5)
|
11
|
+
guard (>= 0.4.0)
|
12
|
+
host (0.1)
|
13
|
+
jeweler (1.6.4)
|
14
|
+
bundler (~> 1.0)
|
15
|
+
git (>= 1.2.5)
|
16
|
+
rake
|
17
|
+
json (1.6.1)
|
18
|
+
little-plugger (1.1.2)
|
19
|
+
logging (1.6.1)
|
20
|
+
little-plugger (>= 1.1.2)
|
21
|
+
method_source (0.6.7)
|
22
|
+
ruby_parser (>= 2.3.1)
|
23
|
+
pokan (0.1.0rc2)
|
24
|
+
eventmachine
|
25
|
+
eventmachine
|
26
|
+
json
|
27
|
+
redis
|
28
|
+
redis
|
29
|
+
pry (0.9.7.3)
|
30
|
+
coderay (~> 0.9.8)
|
31
|
+
method_source (~> 0.6.7)
|
32
|
+
ruby_parser (>= 2.3.1)
|
33
|
+
slop (~> 2.1.0)
|
34
|
+
rake (0.9.2)
|
35
|
+
redis (2.2.2)
|
36
|
+
rspec (2.6.0)
|
37
|
+
rspec-core (~> 2.6.0)
|
38
|
+
rspec-expectations (~> 2.6.0)
|
39
|
+
rspec-mocks (~> 2.6.0)
|
40
|
+
rspec-core (2.6.4)
|
41
|
+
rspec-expectations (2.6.0)
|
42
|
+
diff-lcs (~> 1.1.2)
|
43
|
+
rspec-mocks (2.6.0)
|
44
|
+
ruby_parser (2.3.1)
|
45
|
+
sexp_processor (~> 3.0)
|
46
|
+
sexp_processor (3.0.7)
|
47
|
+
slop (2.1.0)
|
48
|
+
thor (0.14.6)
|
49
|
+
yagni (1.2)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
bundler
|
56
|
+
eventmachine
|
57
|
+
guard-rspec
|
58
|
+
host
|
59
|
+
jeweler
|
60
|
+
logging
|
61
|
+
pokan (~> 0.1.0rc2)
|
62
|
+
pry
|
63
|
+
redis
|
64
|
+
rspec
|
65
|
+
yagni (~> 1.2)
|
data/Guardfile
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
guard 'rspec', :cli => '--color --format doc' do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/pokan-cluster.rb}) { |m| "spec/pokan-cluster_spec.rb" }
|
4
|
+
watch(%r{^lib/pokan-cluster/(.+)\.rb$}) { |m| "spec/pokan-cluster/#{m[1]}_spec.rb" }
|
5
|
+
watch("spec/spec_helper.rb") { "spec" }
|
6
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Renato Mascarenhas
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= pokan-cluster
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Contributing to pokan-cluster
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
9
|
+
* Fork the project
|
10
|
+
* Start a feature/bugfix branch
|
11
|
+
* Commit and push until you are happy with your contribution
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2011 rmascarenhas. See LICENSE.txt for
|
18
|
+
further details.
|
19
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path('lib/pokan-cluster/version')
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'bundler'
|
7
|
+
begin
|
8
|
+
Bundler.setup(:default, :development)
|
9
|
+
rescue Bundler::BundlerError => e
|
10
|
+
$stderr.puts e.message
|
11
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
12
|
+
exit e.status_code
|
13
|
+
end
|
14
|
+
require 'rake'
|
15
|
+
|
16
|
+
require 'jeweler'
|
17
|
+
Jeweler::Tasks.new do |gem|
|
18
|
+
gem.name = "pokan-cluster"
|
19
|
+
gem.version = Pokan::Cluster::VERSION
|
20
|
+
|
21
|
+
gem.homepage = "http://github.com/rmascarenhas/pokan-cluster"
|
22
|
+
gem.license = "MIT"
|
23
|
+
|
24
|
+
gem.summary = %Q{pokan-cluster is a set of neat, handy functionalities built
|
25
|
+
on top of pokan.}
|
26
|
+
|
27
|
+
gem.description = <<-END
|
28
|
+
pokan-cluster adds a handy set of functionalities on top of pokan, everyone's
|
29
|
+
favorite Ruby Gossip protocol implementation. Logging, easy YAML configuration
|
30
|
+
file, workload information of peers in your network (so that you can write a
|
31
|
+
simple load balancer) are among the features pokan-cluster has to offer!
|
32
|
+
Take a look at it and contribute! (see our Github page).
|
33
|
+
END
|
34
|
+
gem.email = "renato.mascosta@gmail.com"
|
35
|
+
gem.authors = ["Fabio Lima Pereira", "Rafael Regis do Prado", "Renato Mascarenhas"]
|
36
|
+
|
37
|
+
gem.add_dependency 'pokan'
|
38
|
+
gem.add_dependency 'yagni'
|
39
|
+
gem.add_dependency 'host'
|
40
|
+
|
41
|
+
end
|
42
|
+
Jeweler::RubygemsDotOrgTasks.new
|
43
|
+
|
44
|
+
require 'rspec/core'
|
45
|
+
require 'rspec/core/rake_task'
|
46
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
47
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
48
|
+
end
|
49
|
+
|
50
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
51
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
52
|
+
spec.rcov = true
|
53
|
+
end
|
54
|
+
|
55
|
+
task :default => :spec
|
56
|
+
|
57
|
+
require 'rake/rdoctask'
|
58
|
+
Rake::RDocTask.new do |rdoc|
|
59
|
+
version = Pokan::Cluster::VERSION
|
60
|
+
|
61
|
+
rdoc.rdoc_dir = 'rdoc'
|
62
|
+
rdoc.title = "pokan-cluster #{version}"
|
63
|
+
rdoc.rdoc_files.include('README*')
|
64
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
65
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
require 'pokan'
|
2
|
+
require 'yell/hash'
|
3
|
+
require 'logging'
|
4
|
+
|
5
|
+
module Pokan
|
6
|
+
|
7
|
+
require_relative 'pokan-cluster/configuration'
|
8
|
+
require_relative 'pokan-cluster/prepared_string'
|
9
|
+
require_relative 'pokan-cluster/autorunner'
|
10
|
+
require_relative 'pokan-cluster/log_messages'
|
11
|
+
require_relative 'pokan-cluster/config_defaults'
|
12
|
+
require_relative 'pokan-cluster/version'
|
13
|
+
|
14
|
+
# Cluster module, to be included on all classes that want to implement
|
15
|
+
# a custom Gossip server. Including this module enables the developer to
|
16
|
+
# customize the configuration file location (and format, as long as she
|
17
|
+
# provided a parser for that format). The user can also pass manually
|
18
|
+
# the values for the configuration options. However, this is not the
|
19
|
+
# recommended way, of course.
|
20
|
+
#
|
21
|
+
# Example
|
22
|
+
#
|
23
|
+
# class MyCustomGossiper
|
24
|
+
# include Pokan::Cluster
|
25
|
+
#
|
26
|
+
# config { JSON.parse(CONFIG_PATH) }
|
27
|
+
#
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The `config` method expects a hash containing values for network connection,
|
31
|
+
# seed address, log information, and others. If you want to see an example of a
|
32
|
+
# valid configuration hash, you can parse the following YAML (recommened config
|
33
|
+
# format for pokan-cluster):
|
34
|
+
#
|
35
|
+
# pokan:
|
36
|
+
# address: "10.10.10.8" # your IP address
|
37
|
+
#
|
38
|
+
# connect:
|
39
|
+
# tcp: 4444
|
40
|
+
# udp: 8787 # ports that will be binded
|
41
|
+
# epoll: true # Linux machine?
|
42
|
+
#
|
43
|
+
# gossip:
|
44
|
+
# every: 2 # seconds
|
45
|
+
# share: "cpu,memory,load" # these are the valid options (separated by comma).
|
46
|
+
#
|
47
|
+
# seed: "10.10.10.2" # will be contacted on startup
|
48
|
+
#
|
49
|
+
# log:
|
50
|
+
# enabled: true # always good!
|
51
|
+
# level: "info"
|
52
|
+
# file: "/path/to/my/log" # default is STDOUT
|
53
|
+
#
|
54
|
+
# Optionally, you can directly pass a file, using the `config_file` method. However,
|
55
|
+
# only the YAML format is currently supported
|
56
|
+
#
|
57
|
+
# class MyCustomGossiper
|
58
|
+
# include Pokan::Cluster
|
59
|
+
#
|
60
|
+
# config_file = '/path/to/my/config.yml'
|
61
|
+
# end
|
62
|
+
module Cluster
|
63
|
+
|
64
|
+
|
65
|
+
# When a server includes the Pokan::Cluster module, it gains access to its
|
66
|
+
# class methods, allowing the user to set configuration and behavior of the
|
67
|
+
# server. It is also configured to automatically run when the server class
|
68
|
+
# is loaded.
|
69
|
+
def self.included(klass)
|
70
|
+
klass.extend ClassMethods
|
71
|
+
Autorunner.register_server(klass)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Automatically starts registered servers.
|
75
|
+
at_exit {
|
76
|
+
unless $! || Pokan::Cluster::Autorunner.run?
|
77
|
+
Pokan::Cluster::Autorunner.run
|
78
|
+
end
|
79
|
+
}
|
80
|
+
|
81
|
+
module ClassMethods
|
82
|
+
|
83
|
+
attr_accessor :address, :tcp_port, :udp_port, :epoll, :gossip_interval,
|
84
|
+
:seed, :logging, :log_level, :log_file
|
85
|
+
|
86
|
+
# Pretty names
|
87
|
+
alias_method :epoll?, :epoll
|
88
|
+
alias_method :logging?, :logging
|
89
|
+
|
90
|
+
# Loads the server configuration, expecting a valid config hash in a block.
|
91
|
+
# This allows you to use whatever configuration format, as long as you can
|
92
|
+
# transform it to a valid pokan hash.
|
93
|
+
def config(&block)
|
94
|
+
@server || load!(block.call)
|
95
|
+
initialize_log
|
96
|
+
delegate
|
97
|
+
end
|
98
|
+
|
99
|
+
# Convenience method, allowing the user to set the file containing the
|
100
|
+
# configuration in the YAML format. In the future, maybe add builtin support
|
101
|
+
# for other different formats.
|
102
|
+
def config_file(path, options={})
|
103
|
+
config { YAML.load(File.read path) }
|
104
|
+
end
|
105
|
+
|
106
|
+
# Disables log for the gossip server
|
107
|
+
def disable_log
|
108
|
+
@logging = false
|
109
|
+
end
|
110
|
+
|
111
|
+
# Sets the seed address and redis port, if specified
|
112
|
+
#
|
113
|
+
# class Server
|
114
|
+
# include Pokan::Cluster
|
115
|
+
#
|
116
|
+
# gossip_with '10.10.10.3', :redis => 6549
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# If no Redis port is specified, +6379+ is assumed (as it is the default
|
120
|
+
# Redis port).
|
121
|
+
def gossip_with(address, options)
|
122
|
+
update_autorunner_option(:gossip_with, address)
|
123
|
+
update_autorunner_option(:redis, options[:redis] || DEFAULTS[:redis])
|
124
|
+
end
|
125
|
+
|
126
|
+
def method_missing(meth, *args, &block)
|
127
|
+
@delegator.send(meth, *args, &block)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# Sets variables according to the hash passed.
|
133
|
+
def load!(config_hash)
|
134
|
+
config = Configuration.new(config_hash)
|
135
|
+
@address = config.get('pokan.address')
|
136
|
+
@server = Pokan::Server.new(@address)
|
137
|
+
|
138
|
+
update_ivs(config)
|
139
|
+
config_autorunner
|
140
|
+
end
|
141
|
+
|
142
|
+
def update_ivs(config)
|
143
|
+
@tcp_port = config.get('pokan.connect.tcp') || DEFAULTS[:tcp_port]
|
144
|
+
@udp_port = config.get('pokan.connect.udp') || DEFAULTS[:udp_port]
|
145
|
+
@epoll = config.get('pokan.connect.epoll') || DEFAULTS[:epoll]
|
146
|
+
@gossip_interval = config.get('pokan.gossip.every') || DEFAULTS[:gossip_interval]
|
147
|
+
@seed = config.get('pokan.seed.address')
|
148
|
+
@redis = config.get('pokan.seed.redis') || DEFAULTS[:redis]
|
149
|
+
@logging = config.get('pokan.log.enabled') || DEFAULTS[:logging]
|
150
|
+
@log_level = config.get('pokan.log.level').to_sym || DEFAULTS[:level]
|
151
|
+
@log_file = config.get('pokan.log.file')
|
152
|
+
end
|
153
|
+
|
154
|
+
def config_autorunner
|
155
|
+
options = {}
|
156
|
+
options.merge!({ :gossip_with => @seed }) unless @seed.nil?
|
157
|
+
options.merge!({ :redis => @redis }) unless @redis.nil?
|
158
|
+
|
159
|
+
Autorunner.options = options
|
160
|
+
end
|
161
|
+
|
162
|
+
def update_autorunner_option(key, value)
|
163
|
+
opt = Autorunner.options
|
164
|
+
opt[key] = value
|
165
|
+
|
166
|
+
Autorunner.options = opt
|
167
|
+
end
|
168
|
+
|
169
|
+
# Adds callbacks for logging on the happening of certain events, such
|
170
|
+
# as new keys being defined, changed, gossip messages, etc.
|
171
|
+
def initialize_log
|
172
|
+
@log_initialized ||= false
|
173
|
+
|
174
|
+
if logging? && !@log_initialized
|
175
|
+
setup_logging
|
176
|
+
add_log_callbacks
|
177
|
+
end
|
178
|
+
|
179
|
+
@log_initialized = true
|
180
|
+
end
|
181
|
+
|
182
|
+
# requires and sets up the `logging` library.
|
183
|
+
def setup_logging
|
184
|
+
@logger = Logging.logger['pokan-cluster']
|
185
|
+
appenders = [Logging.appenders.stdout]
|
186
|
+
appenders << Logging.appenders.file(@log_file) if @log_file
|
187
|
+
|
188
|
+
@logger.add_appenders *appenders
|
189
|
+
@logger.level = @log_level
|
190
|
+
end
|
191
|
+
|
192
|
+
# adds callbacks on the server instance so that some events are logged
|
193
|
+
def add_log_callbacks
|
194
|
+
@server.on(:start) do |address, tcp_port, udp_port, epoll, gossip_interval, seed_address, seed_port|
|
195
|
+
@logger.debug LOG_MESSAGES[:start][:debug].with(address, tcp_port, udp_port, epoll,
|
196
|
+
gossip_interval, seed_address, @log_level.to_s, @log_file).string
|
197
|
+
|
198
|
+
@logger.info LOG_MESSAGES[:start][:info].with(address).string
|
199
|
+
end
|
200
|
+
|
201
|
+
@server.before(:sync) do |address, port|
|
202
|
+
@logger.debug LOG_MESSAGES[:before_sync][:debug].with(address, port).string
|
203
|
+
@logger.info LOG_MESSAGES[:before_sync][:info].string
|
204
|
+
end
|
205
|
+
|
206
|
+
@server.after(:sync) do |time_elapsed|
|
207
|
+
@logger.debug LOG_MESSAGES[:after_sync][:debug].with(time_elapsed).string
|
208
|
+
@logger.info LOG_MESSAGES[:after_sync][:info].string
|
209
|
+
end
|
210
|
+
|
211
|
+
@server.on(:new_key) do |key, value|
|
212
|
+
@logger.debug LOG_MESSAGES[:new_key][:debug].with(key.to_s, value).string
|
213
|
+
end
|
214
|
+
|
215
|
+
@server.before(:gossip) do |address, port, digest_length|
|
216
|
+
@logger.debug LOG_MESSAGES[:before_gossip][:debug].with(address, digest_length).string
|
217
|
+
@logger.info LOG_MESSAGES[:before_gossip][:info].with(address).string
|
218
|
+
end
|
219
|
+
|
220
|
+
@server.after(:gossip) do
|
221
|
+
@logger.debug LOG_MESSAGES[:after_gossip][:debug].string
|
222
|
+
end
|
223
|
+
|
224
|
+
@server.on(:shutdown) do
|
225
|
+
@logger.info LOG_MESSAGES[:shutdown]
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
# Initializes the delegator instance so that unknown methods will
|
232
|
+
# be delegated to the server instance, providing a better API for
|
233
|
+
# developers who want to customize their server behavior.
|
234
|
+
def delegate
|
235
|
+
require 'delegate'
|
236
|
+
@delegator = DelegateClass(Pokan::Server).new(@server)
|
237
|
+
end
|
238
|
+
|
239
|
+
def prepend(a_string, to_string)
|
240
|
+
(a_string.to_s + to_string.to_s).to_sym
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pokan
|
2
|
+
|
3
|
+
module Cluster
|
4
|
+
|
5
|
+
# Class used for automatically run a server that include Pokan::Cluster
|
6
|
+
# module.
|
7
|
+
class Autorunner
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :server
|
11
|
+
attr_accessor :options
|
12
|
+
end
|
13
|
+
|
14
|
+
# Registers a new server, that will be started when the method
|
15
|
+
# Pokan::Cluster::Autorunner.run gets called.
|
16
|
+
def self.register_server(server_class)
|
17
|
+
@server = server_class
|
18
|
+
end
|
19
|
+
|
20
|
+
# Checks if we have any server to start
|
21
|
+
def self.run?
|
22
|
+
@server.respond_to? :start
|
23
|
+
end
|
24
|
+
|
25
|
+
# Calls the method `start` on every registered server
|
26
|
+
def self.run
|
27
|
+
@server.start(@options)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Pokan
|
2
|
+
module Cluster
|
3
|
+
|
4
|
+
# Default values for configuration, specifying:
|
5
|
+
# * TCP and UDP ports
|
6
|
+
# * epoll set to false
|
7
|
+
# * logging enabled to `STDOUT`
|
8
|
+
# * gossip interval: 1 second
|
9
|
+
|
10
|
+
DEFAULTS = {
|
11
|
+
:tcp_port => 5555,
|
12
|
+
:udp_port => 5555,
|
13
|
+
:epoll => false,
|
14
|
+
:gossip_interval => 1,
|
15
|
+
:redis => 6379,
|
16
|
+
:logging => true,
|
17
|
+
:level => :info
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# lib/pokan-cluster/configuration.rb
|
2
|
+
|
3
|
+
module Pokan
|
4
|
+
module Cluster
|
5
|
+
|
6
|
+
# Class used to hold configuration options for anything.
|
7
|
+
# It inherits from Yell::Hash, so that the options are can be passed
|
8
|
+
# in a usual Ruby hash. Optionally, you can pass it a string containing
|
9
|
+
# your data and an object which responds to the method +parse+,
|
10
|
+
# which will take the data string and build the corresponding hash.
|
11
|
+
#
|
12
|
+
# The options passed can be accessed later via the +get+ method, used in the
|
13
|
+
# example above (see further details in Pokan::Cluster::Configuration#get)
|
14
|
+
#
|
15
|
+
# Usage
|
16
|
+
#
|
17
|
+
# # If we wanted to use a YAML configuration file...
|
18
|
+
# # and our file was the following:
|
19
|
+
# # pokan:
|
20
|
+
# # cluster: "configuration"
|
21
|
+
#
|
22
|
+
# class YAMLParser
|
23
|
+
# def self.parse(data)
|
24
|
+
# YAML.load(data)
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# c = Pokan::Cluster::Configuration.new(File.read('config.yaml'), YAMLParser)
|
29
|
+
# c.get('pokan.cluster') #=> "configuration"
|
30
|
+
class Configuration < Yell::Hash
|
31
|
+
|
32
|
+
# Creates a new Pokan::Cluster::Configuration object, containing the
|
33
|
+
# data passed. It can be either a usual Ruby hash or a String with your
|
34
|
+
# data, as long as you provide a parser which responds to the method
|
35
|
+
# +parse+ taking the string as parameter and returning the corresponding
|
36
|
+
# hash.
|
37
|
+
def initialize(data, parser=nil)
|
38
|
+
case data
|
39
|
+
when Object::Hash
|
40
|
+
@data = data
|
41
|
+
when String
|
42
|
+
@data = load_data(data, parser)
|
43
|
+
end
|
44
|
+
|
45
|
+
super(@data)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Loads the data in case a string was passed in the initialization.
|
49
|
+
# It actually just calls the method +parse+ on the parser object
|
50
|
+
def load_data(data, parser)
|
51
|
+
parser.parse(data)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Main method, used to query values from your configuration. It accepts
|
55
|
+
# as a parameter a string containing the "path" to reach the data you
|
56
|
+
# want.
|
57
|
+
#
|
58
|
+
# h = { :max_connections => "10", :proto => { :tcp => true } }
|
59
|
+
# c = Pokan::Cluster::Configuration.new(h)
|
60
|
+
# c.get('max_connections') #=> "10"
|
61
|
+
# c.get('proto.tcp') #=> "true"
|
62
|
+
def get(query)
|
63
|
+
step = nil # contains consequent results of our query
|
64
|
+
path = query.split '.'
|
65
|
+
|
66
|
+
path.each { |param| step = send(param.to_sym) }
|
67
|
+
|
68
|
+
step
|
69
|
+
rescue NoMethodError
|
70
|
+
nil
|
71
|
+
ensure
|
72
|
+
reload
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Pokan
|
2
|
+
module Cluster
|
3
|
+
|
4
|
+
def self.m(string)
|
5
|
+
PreparedString.new(string)
|
6
|
+
end
|
7
|
+
|
8
|
+
LOG_MESSAGES = {
|
9
|
+
:start => { :debug => m("Address: %p\nTCP port: %p\nUDP port: %p\n
|
10
|
+
Using epoll: %p\nGossip interval: %p seconds\n
|
11
|
+
Seed address: %p\n\n
|
12
|
+
Log level is %p\n. Logging to %p\n"),
|
13
|
+
|
14
|
+
:info => m('Server is starting at %p...')
|
15
|
+
},
|
16
|
+
|
17
|
+
:before_sync => { :debug => m("Starting Redis sync with seed on address %p, port %p"),
|
18
|
+
:info => m("Syncing databases...")
|
19
|
+
},
|
20
|
+
|
21
|
+
:after_sync => { :debug => m("Databases synced in %p seconds"),
|
22
|
+
:info => m("Done.")
|
23
|
+
},
|
24
|
+
|
25
|
+
:new_key => { :debug => m("Saving new key-value pair: %p:%p") },
|
26
|
+
|
27
|
+
:new_peer => { :debug => m("New peer detected at address %p:%p"),
|
28
|
+
:info => m("New peer: %p") },
|
29
|
+
|
30
|
+
:key_changed => { :debug => m("Key %p changing from %p to %p") },
|
31
|
+
|
32
|
+
:key_removed => { :debug => m("Key %p was removed"),
|
33
|
+
:info => m("Peer at addres %p just left")
|
34
|
+
},
|
35
|
+
|
36
|
+
:before_gossip => { :debug => m("Preparing digest message for gossiping with %p (length: %p bytes)"),
|
37
|
+
:info => m("Sending gossip message to randomly selected peer (%p)")
|
38
|
+
},
|
39
|
+
|
40
|
+
:after_gossip => { :debug => m("Gossip message sent") },
|
41
|
+
|
42
|
+
:shutdown => m('Server is finishing...')
|
43
|
+
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Pokan
|
2
|
+
|
3
|
+
module Cluster
|
4
|
+
|
5
|
+
# PreparedString is a class representing strings with marks, to be
|
6
|
+
# substituted later with specific values.
|
7
|
+
#
|
8
|
+
# Example
|
9
|
+
#
|
10
|
+
# p = PreparedString.new('Hi %p, my name is %p')
|
11
|
+
# p.with('John', 'James').string #=> "Hi John, my name is James"
|
12
|
+
# p.string #=> "Hi %p, my name is %p"
|
13
|
+
# p.with('John').string #=> "Hi John, my name is %p"
|
14
|
+
#
|
15
|
+
# Useful for log messages.
|
16
|
+
class PreparedString
|
17
|
+
|
18
|
+
# Creates a new instance of your prepared string as the first argument.
|
19
|
+
# You can specify the token to be substituted when using PreparedString#with
|
20
|
+
# as the second argument. Default is `%p`
|
21
|
+
def initialize(string, token='%p')
|
22
|
+
@original, @token = string, token
|
23
|
+
@current = copy @original
|
24
|
+
@changed = false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieves the string content for the prepared string. If a substitution had
|
28
|
+
# already been done (using PreparedString#with), then the substitued string
|
29
|
+
# will be returned. Otherwise, the original string is returned.
|
30
|
+
def string
|
31
|
+
r_string = @changed ? @current : @original
|
32
|
+
@changed = false
|
33
|
+
@current = copy @original
|
34
|
+
|
35
|
+
r_string
|
36
|
+
end
|
37
|
+
|
38
|
+
# Allows you to specify the strings to be placed in the tokens
|
39
|
+
def with(*args)
|
40
|
+
@changed = true
|
41
|
+
|
42
|
+
args.each { |token|
|
43
|
+
@current.sub!(@token, token.to_s)
|
44
|
+
}
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def copy(obj)
|
52
|
+
obj.dup
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# lib/pokan-cluster/server_configuration.rb
|
2
|
+
module Pokan
|
3
|
+
|
4
|
+
##
|
5
|
+
# Class used to store and manage server configuration.
|
6
|
+
# Should not be used by user code, just by the Pokan::Cluster
|
7
|
+
# class. It loads a YAML configuration file located in the user
|
8
|
+
# home directory or in the +.config+ directory or in +/etc/pokan.yml+.
|
9
|
+
# Optionally, a different config file can be passed.
|
10
|
+
#
|
11
|
+
# === Usage
|
12
|
+
# s = Pokan::ServerConfiguration.new("pokan.yml")
|
13
|
+
# s.tcp_port #=> 8888 (depending on your configuration options)
|
14
|
+
#
|
15
|
+
# A general configuration file has the following skeleton:
|
16
|
+
#
|
17
|
+
# pokan:
|
18
|
+
# connection:
|
19
|
+
# udp: 88885
|
20
|
+
# tcp: 7777
|
21
|
+
#
|
22
|
+
# seed: "10.10.10.8"
|
23
|
+
#
|
24
|
+
# log:
|
25
|
+
# enabled: true
|
26
|
+
# log_file: "/var/log/pokan.log"
|
27
|
+
class ServerConfiguration
|
28
|
+
|
29
|
+
YAML_ROOT = 'pokan'
|
30
|
+
|
31
|
+
# Creates a new instance of Pokan::ServerConfiguration.
|
32
|
+
# You can pass an optional parameter indicating the location
|
33
|
+
# of the configuration file (must be a valid YAML file)
|
34
|
+
#
|
35
|
+
# If no config file is passed, we first look if there is a +.pokan.yml+
|
36
|
+
# file in the user's home directory. If there isn't, then we search
|
37
|
+
# for a +.config/pokan/pokan.yml+. If none of these are found, then
|
38
|
+
# we set the default values (see Pokan::ServerConfiguration#set_defaults
|
39
|
+
# for further details)
|
40
|
+
def initialize(config_file=nil)
|
41
|
+
@config_file = config_file || search_config
|
42
|
+
|
43
|
+
@config_file.nil? ? set_defaults : load_config(@config_file)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Searches for a valid YAML configuration file in +.pokan.yml+
|
47
|
+
# located in the user's home directory or in +.config/pokan/pokan.yml'+,
|
48
|
+
# or in +/etc/pokan.yml+.
|
49
|
+
# Returns the file name, if found, or +nil+ if no configuration file was
|
50
|
+
# found.
|
51
|
+
def search_config
|
52
|
+
home_file = File.absolute_path '~/.pokan.yml'
|
53
|
+
config_file = File.absolute_path '~/.config/pokan/pokan.yml'
|
54
|
+
etc_file = '/etc/pokan.yml'
|
55
|
+
|
56
|
+
[home_file, config_file, etc_file].each do |config|
|
57
|
+
return config if File.exists? config
|
58
|
+
end
|
59
|
+
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns +true+ if a configuration file was found, or if the user
|
64
|
+
# explicitly passed a file.
|
65
|
+
def has_config_file?
|
66
|
+
!@config_file.nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the absolute path of the configuration file, if any.
|
70
|
+
def file_path
|
71
|
+
@config_file
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns +true+ if we are logging activities on the server
|
75
|
+
def logging?
|
76
|
+
@logging
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_config(config_file)
|
80
|
+
raise "YAML file not found: #{config_file}" unless File.exists? config_file
|
81
|
+
|
82
|
+
@data = YAML.load_file(config_file)
|
83
|
+
define_methods_for(@data[YAML_ROOT].keys)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Sets the default values for all the attributes that can be set
|
87
|
+
# with the configuration file. These are:
|
88
|
+
# * TCP port: 5555
|
89
|
+
# * UDP port: 5555
|
90
|
+
# * no seeds information
|
91
|
+
# * logger enabled
|
92
|
+
# * log_file ./pokan.log
|
93
|
+
def set_defaults
|
94
|
+
@tcp_port = 5555
|
95
|
+
@udp_port = 5555
|
96
|
+
@seeds = []
|
97
|
+
@logging = true
|
98
|
+
@log_file = './pokan.log'
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Defines methods in the singleton class for every key in the +keys+
|
104
|
+
# parameter
|
105
|
+
def define_methods_for(keys)
|
106
|
+
keys.each do |k|
|
107
|
+
singleton_class.send(:define_method, k) do
|
108
|
+
@data[YAML_ROOT][k]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def singleton_class
|
114
|
+
class << self; self; end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
data/pokan.log
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
INFO pokan-cluster : New peer detected at address role
|
2
|
+
INFO pokan-cluster : Sending gossip message to randomly selected peer ()
|
3
|
+
INFO pokan-cluster : New peer detected at address role
|
4
|
+
INFO pokan-cluster : Server is starting at 192.168.229.197...
|
5
|
+
INFO pokan-cluster : New peer detected at address role
|
6
|
+
INFO pokan-cluster : Server is starting at 192.168.229.197...
|
7
|
+
INFO pokan-cluster : New peer detected at address role
|
8
|
+
INFO pokan-cluster : Server is starting at 192.168.229.197...
|
9
|
+
INFO pokan-cluster : Sending gossip message to randomly selected peer (10.10.10.8)
|
10
|
+
INFO pokan-cluster : Server is starting at 192.168.229.197...
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe Pokan::Cluster::Autorunner do
|
4
|
+
|
5
|
+
subject { Pokan::Cluster::Autorunner }
|
6
|
+
|
7
|
+
it 'should register classes that include Pokan::Cluster' do
|
8
|
+
class DummyServer
|
9
|
+
include Pokan::Cluster
|
10
|
+
end
|
11
|
+
|
12
|
+
subject.server.should eq DummyServer
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe Pokan::Cluster::Configuration do
|
4
|
+
|
5
|
+
let(:yaml) {
|
6
|
+
<<-END
|
7
|
+
connection:
|
8
|
+
udp: 8888
|
9
|
+
tcp: 8888
|
10
|
+
|
11
|
+
seed:
|
12
|
+
address: "10.10.10.8"
|
13
|
+
redis: 6379
|
14
|
+
|
15
|
+
log:
|
16
|
+
enabled: true
|
17
|
+
log_file: "/var/log/pokan"
|
18
|
+
END
|
19
|
+
}
|
20
|
+
|
21
|
+
subject {
|
22
|
+
parser = double('parser')
|
23
|
+
parser.stub! :parse => YAML.load(yaml)
|
24
|
+
Pokan::Cluster::Configuration.new(yaml, parser)
|
25
|
+
}
|
26
|
+
|
27
|
+
it 'should retrieve all data' do
|
28
|
+
subject.get('connection.udp').should eq 8888
|
29
|
+
subject.get('connection.tcp').should eq 8888
|
30
|
+
|
31
|
+
subject.get('seed.address').should eq "10.10.10.8"
|
32
|
+
subject.get('seed.redis').should eq 6379
|
33
|
+
|
34
|
+
subject.get('log.enabled').should be_true
|
35
|
+
subject.get('log.log_file').should eq "/var/log/pokan"
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should return nil on non existent configuration' do
|
39
|
+
subject.get('log.levell').should be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe Pokan::Cluster::PreparedString do
|
4
|
+
|
5
|
+
let(:string) {
|
6
|
+
'A: %p, B: %p C:%p'
|
7
|
+
}
|
8
|
+
|
9
|
+
let(:prepared_string) {
|
10
|
+
Pokan::Cluster::PreparedString.new(string)
|
11
|
+
}
|
12
|
+
|
13
|
+
it 'should return the given string when no substitution is made' do
|
14
|
+
prepared_string.string.should eq string
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return semi substituted string' do
|
18
|
+
prepared_string.with('dummy').string.should eq 'A: dummy, B: %p C:%p'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should return fully substituted string' do
|
22
|
+
prepared_string.with('dummy', 'yummy', 'mummy').string.should eq 'A: dummy, B: yummy C:mummy'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should return correctly substituted string when more arguments are passed' do
|
26
|
+
prepared_string.with('dummy', 'yummy', 'mummy', 'a', 'b', 'c').string.should eq 'A: dummy, B: yummy C:mummy'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should return initial string after changes' do
|
30
|
+
prepared_string.with('dummy').string.should eq 'A: dummy, B: %p C:%p'
|
31
|
+
prepared_string.string.should eq string
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe Pokan::Cluster do
|
4
|
+
|
5
|
+
subject {
|
6
|
+
class MyPokan
|
7
|
+
include Pokan::Cluster
|
8
|
+
|
9
|
+
config {
|
10
|
+
YAML.parse(<<-END
|
11
|
+
pokan:
|
12
|
+
|
13
|
+
address: "10.10.10.7"
|
14
|
+
|
15
|
+
connect:
|
16
|
+
tcp: 8888
|
17
|
+
udp: 8888
|
18
|
+
epoll: true
|
19
|
+
|
20
|
+
seed:
|
21
|
+
address: "10.10.10.1"
|
22
|
+
redis: 6379
|
23
|
+
|
24
|
+
gossip:
|
25
|
+
every: 2
|
26
|
+
|
27
|
+
log:
|
28
|
+
enabled: true
|
29
|
+
level: "info"
|
30
|
+
file: "pokan.log"
|
31
|
+
END
|
32
|
+
).to_ruby
|
33
|
+
}
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
context 'when all options are passed' do
|
40
|
+
|
41
|
+
it 'should properly initialize instance variables' do
|
42
|
+
subject.address.should eq '10.10.10.7'
|
43
|
+
|
44
|
+
subject.tcp_port.should eq 8888
|
45
|
+
subject.udp_port.should eq 8888
|
46
|
+
subject.epoll.should be_true
|
47
|
+
|
48
|
+
subject.seed.should eq '10.10.10.1'
|
49
|
+
|
50
|
+
subject.gossip_interval.should eq 2
|
51
|
+
|
52
|
+
subject.logging.should be_true
|
53
|
+
subject.log_level.should eq :info
|
54
|
+
subject.log_file.should eq 'pokan.log'
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should delegate to the server instance' do
|
59
|
+
subject.use_epoll.should be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/test_server.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'pokan-cluster'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
class TestServer
|
5
|
+
include Pokan::Cluster
|
6
|
+
|
7
|
+
config { YAML.parse_file('/home/renato/pokan.yml').to_ruby }
|
8
|
+
|
9
|
+
after :sync do
|
10
|
+
puts "Synced!"
|
11
|
+
end
|
12
|
+
|
13
|
+
after :gossip do |addr, port|
|
14
|
+
puts "Gossiped with #{addr}:#{port}"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pokan-cluster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Fabio Lima Pereira
|
9
|
+
- Rafael Regis do Prado
|
10
|
+
- Renato Mascarenhas
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2011-11-14 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: pokan
|
18
|
+
requirement: &4783360 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *4783360
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yagni
|
29
|
+
requirement: &4782660 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *4782660
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: host
|
40
|
+
requirement: &4781960 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
type: :runtime
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *4781960
|
49
|
+
description: ! " pokan-cluster adds a handy set of functionalities on top of pokan,
|
50
|
+
everyone's\n favorite Ruby Gossip protocol implementation. Logging, easy YAML
|
51
|
+
configuration\n file, workload information of peers in your network (so that
|
52
|
+
you can write a\n simple load balancer) are among the features pokan-cluster
|
53
|
+
has to offer!\n Take a look at it and contribute! (see our Github page).\n"
|
54
|
+
email: renato.mascosta@gmail.com
|
55
|
+
executables: []
|
56
|
+
extensions: []
|
57
|
+
extra_rdoc_files: []
|
58
|
+
files:
|
59
|
+
- .document
|
60
|
+
- .rspec
|
61
|
+
- Gemfile
|
62
|
+
- Gemfile.lock
|
63
|
+
- Guardfile
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.rdoc
|
66
|
+
- Rakefile
|
67
|
+
- lib/pokan-cluster.rb
|
68
|
+
- lib/pokan-cluster/autorunner.rb
|
69
|
+
- lib/pokan-cluster/config_defaults.rb
|
70
|
+
- lib/pokan-cluster/configuration.rb
|
71
|
+
- lib/pokan-cluster/log_messages.rb
|
72
|
+
- lib/pokan-cluster/prepared_string.rb
|
73
|
+
- lib/pokan-cluster/version.rb
|
74
|
+
- lib/server_configuration.rb
|
75
|
+
- pokan.log
|
76
|
+
- spec/pokan-cluster/autorunner_spec.rb
|
77
|
+
- spec/pokan-cluster/configuration_spec.rb
|
78
|
+
- spec/pokan-cluster/prepared_string_spec.rb
|
79
|
+
- spec/pokan-cluster_spec.rb
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
- test_server.rb
|
82
|
+
homepage: http://github.com/rmascarenhas/pokan-cluster
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
hash: 1258949825256661048
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.8.10
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: pokan-cluster is a set of neat, handy functionalities built on top of pokan.
|
110
|
+
test_files: []
|