villein 0.0.1

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: 9b6d36799ad957920fbb7b42d62b6e1bf148c70b
4
+ data.tar.gz: 375feb1e4a3da4b2febac8f08e3980a86916be03
5
+ SHA512:
6
+ metadata.gz: c24bfe7a7a228c061a27d2beb0d2a9ad7b218a0e63d0d28d21cd2a57a00a60b39932976898ee962e690bdf554c656140131aaddd75016c923e711dafdefe990a
7
+ data.tar.gz: c85805cc00f7f680fc75c674bf537a2b35552301c5bc631bd26058e0d39458dd096b5b83c9879ad379fbbd5241112292f4dace6f706f80cc1b595d9d298d16ca
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - "2.1.1"
5
+ - "2.0.0"
6
+ - "2.1.0"
7
+ - "ruby-head"
8
+
9
+ matrix:
10
+ allow_failures:
11
+ - rvm:
12
+ - "2.1.0"
13
+ - "ruby-head"
14
+ fast_finish: true
15
+ notifications:
16
+ email:
17
+ - travis-ci@sorah.jp
18
+ before_script:
19
+ - mkdir -p vendor/serf
20
+ - curl -o vendor/serf/serf.zip https://dl.bintray.com/mitchellh/serf/0.5.0_linux_amd64.zip
21
+ - unzip -d vendor/serf vendor/serf/serf.zip
22
+ - export PATH=$PWD/vendor/serf:$PATH
23
+ script: bundle exec rspec -fd ./spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in villein.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Shota Fukumori (sora_h)
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # Villein - Use `serf` from Ruby
2
+
3
+ Use [serf](https://www.serfdom.io/) from Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'villein'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install villein
18
+
19
+ ## Requirements
20
+
21
+ - Ruby 2.0.0+
22
+
23
+ ## Usage
24
+
25
+ ### Use for existing `serf agent`
26
+
27
+ ``` ruby
28
+ require 'villein'
29
+
30
+ # You have to tell RPC address and node name.
31
+ client = Villein::Client.new('localhost:7373', name: 'testnode')
32
+ ```
33
+
34
+ ### Start new `serf agent` then use
35
+
36
+ ``` ruby
37
+ require 'villein'
38
+
39
+ client = Villein::Agent.new
40
+
41
+ # Start the agent
42
+ client.start!
43
+
44
+ # Stop the agent
45
+ client.stop!
46
+
47
+ # Inspect the status
48
+ p client.running?
49
+ p client.stopped?
50
+
51
+ # You can specify many options to Agent.new...
52
+ # :node, :rpc_addr, :bind, :iface, :advertise, :discover,
53
+ # :config_file, :config_dir, :discover, :join, :snapshot, :encrypt, :profile,
54
+ # :protocol, :event_handlers, :replay, :tags, :tags_file, :log_level
55
+ ```
56
+
57
+ ### join and leave
58
+
59
+ ``` ruby
60
+ client.join('x.x.x.x')
61
+ client.join('x.x.x.x', replay: true)
62
+ client.leave()
63
+ client.force_leave('other-node')
64
+ ```
65
+
66
+ ### Sending user events
67
+
68
+ ``` ruby
69
+ # Send user event
70
+ client.event('my-event', 'payload')
71
+ client.event('my-event', 'payload', coalesce: true)
72
+ ```
73
+
74
+ ### Retrieve member list
75
+
76
+ ``` ruby
77
+ # Retrieve member list
78
+ client.members
79
+ # =>
80
+ # [
81
+ # {
82
+ # "name"=>"testnode", "addr"=>"192.168.101.157:7946", "port"=>7946,
83
+ # "tags"=>{}, "status"=>"alive",
84
+ # "protocol"=>{"max"=>4, "min"=>2, "version"=>4}
85
+ # }
86
+ # ]
87
+
88
+ # You can use some filters.
89
+ # The filters will be passed `serf members` command directly, so be careful
90
+ # to escape regexp-like strings!
91
+ client.members(name: 'foo')
92
+ client.members(status: 'alive')
93
+ client.members(tags: {foo: 'bar'})
94
+ ```
95
+
96
+ ### (Agent only) hook events
97
+
98
+ ``` ruby
99
+ agent = Villein::Agent.new
100
+ agent.start!
101
+
102
+ agent.on_member_join do |event|
103
+ p event # => #<Villein::Event>
104
+ p event.type # => 'member-join'
105
+ p event.self_name
106
+ p event.self_tags # => {"TAG1" => 'value1'}
107
+ p event.members # => [{name: "the-node", address:, tags: {tag1: 'value1'}}]
108
+ p event.user_event # => 'user'
109
+ p event.query_name
110
+ p event.ltime
111
+ p event.payload #=> "..."
112
+ end
113
+
114
+ agent.on_member_leave { |event| ... }
115
+ agent.on_member_failed { |event| ... }
116
+ agent.on_member_update { |event| ... }
117
+ agent.on_member_reap { |event| ... }
118
+ agent.on_user_event { |event| ... }
119
+ agent.on_query { |event| ... }
120
+
121
+ # Catch any events
122
+ agent.on_event { |event| p event }
123
+
124
+ # Catch the agent stop
125
+ agent.on_event { |status|
126
+ # `status` will be a Process::Status, on unexpectedly exits.
127
+ p status
128
+ }
129
+ ```
130
+
131
+ ## Advanced
132
+
133
+ TBD
134
+
135
+ ### Specifying location of `serf` command
136
+
137
+ ### Logging `serf agent`
138
+
139
+ ## FAQ
140
+
141
+ ### Why I have to tell node name?
142
+
143
+ ## Contributing
144
+
145
+ 1. Fork it ( https://github.com/sorah/villein/fork )
146
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
147
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
148
+ 4. Push to the branch (`git push origin my-new-feature`)
149
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rdoc/task'
4
+
5
+ RDoc::Task.new do |rdoc|
6
+ rdoc.main = "README.md"
7
+ rdoc.rdoc_files.include("README.md", "lib/**/*.rb")
8
+ rdoc.rdoc_dir = "#{__dir__}/doc"
9
+ end
@@ -0,0 +1,256 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+ require 'thread'
4
+ require 'villein/client'
5
+ require 'villein/event'
6
+
7
+ module Villein
8
+ ##
9
+ # Villein::Agent allows you to start new serf agent.
10
+ # Use this when you need to start and manage the serf agents from ruby process.
11
+ class Agent < Client
12
+ class AlreadyStarted < Exception; end
13
+ class NotRunning < Exception; end
14
+
15
+ EVENT_HANDLER_SH = File.expand_path(File.join(__dir__, '..', '..', 'misc', 'villein-event-handler'))
16
+
17
+ def initialize(serf: 'serf',
18
+ node: Socket.gethostname,
19
+ rpc_addr: '127.0.0.1:7373', bind: nil, iface: nil, advertise: nil,
20
+ config_file: nil, config_dir: nil,
21
+ discover: false, join: nil, snapshot: nil,
22
+ encrypt: nil, profile: nil, protocol: nil,
23
+ event_handlers: [], replay: nil,
24
+ tags: {}, tags_file: nil,
25
+ log_level: :info, log: File::NULL)
26
+ @serf = serf
27
+ @name = node
28
+ @rpc_addr = rpc_addr
29
+ @bind, @iface, @advertise = bind, iface, advertise
30
+ @config_file, @config_dir = config_file, config_dir
31
+ @discover, @join, @snapshot = discover, join, snapshot
32
+ @encrypt, @profile, @protocol = encrypt, profile, protocol
33
+ @custom_event_handlers, @replay = event_handlers, replay
34
+ @initial_tags, @tags_file = tags, tags_file
35
+ @log_level, @log = log_level, log
36
+ @hooks = {}
37
+
38
+ @pid, @exitstatus = nil, nil
39
+ @pid_lock = Mutex.new
40
+ end
41
+
42
+ attr_reader :pid, :exitstatus
43
+
44
+ ##
45
+ # Returns true when the serf agent has started
46
+ def started?
47
+ !!@pid
48
+ end
49
+
50
+ ##
51
+ # Returns true when the serf agent has started, but stopped for some reason.
52
+ # Use Agent#exitstatus to get <code>Process::Status</code> object.
53
+ def dead?
54
+ !!@exitstatus
55
+ end
56
+
57
+ ##
58
+ # Returns true when the serf agent is running (it has started and not dead yet).
59
+ def running?
60
+ started? && !dead?
61
+ end
62
+
63
+ # Start the serf agent.
64
+ def start!
65
+ raise AlreadyStarted if running?
66
+
67
+ @pid_lock.synchronize do
68
+ start_listening_events
69
+ start_process
70
+ start_watchdog
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Stop the serf agent.
76
+ # After +timeout_sec+ seconds elapsed, it will attempt to KILL if the agent is still running.
77
+ def stop!(timeout_sec = 10)
78
+ raise NotRunning unless running?
79
+
80
+ @pid_lock.synchronize do
81
+ Process.kill(:INT, @pid)
82
+
83
+ stop_watchdog
84
+ call_hooks 'stop', nil
85
+
86
+ kill_process(timeout_sec)
87
+
88
+ stop_listening_events
89
+
90
+ @pid = nil
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Add +at_exit+ hook to safely stop at exit of current ruby process.
96
+ # Note that +Kernel#.at_exit+ hook won't run when Ruby has crashed.
97
+ def auto_stop
98
+ at_exit { self.stop! }
99
+ end
100
+
101
+ %w(member_join member_leave member_failed member_update member_reap
102
+ user_event query stop event).each do |event|
103
+
104
+ define_method(:"on_#{event}") do |&block|
105
+ add_hook(event, block)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Command line arguments to start serf-agent.
111
+ def command
112
+ cmd = [@serf, 'agent']
113
+
114
+ cmd << ['-node', @name] if @name
115
+ cmd << '-replay' if @replay
116
+ cmd << '-discover' if @discover
117
+
118
+ @initial_tags.each do |key, val|
119
+ cmd << ['-tag', "#{key}=#{val}"]
120
+ end
121
+
122
+ cmd << [
123
+ '-event-handler',
124
+ [EVENT_HANDLER_SH, *event_listener_addr].join(' ')
125
+ ]
126
+
127
+ @custom_event_handlers.each do |handler|
128
+ cmd << ['-event-handler', handler]
129
+ end
130
+
131
+ %w(bind iface advertise config-file config-dir
132
+ encrypt join log-level profile protocol rpc-addr
133
+ snapshot tags-file).each do |key|
134
+
135
+ val = instance_variable_get("@#{key.gsub(/-/,'_')}")
136
+ cmd << ["-#{key}", val] if val
137
+ end
138
+
139
+ cmd.flatten.map(&:to_s)
140
+ end
141
+
142
+ private
143
+
144
+ def start_process
145
+ @exitstatus = nil
146
+
147
+ actual = -> { @pid = spawn(*command, out: @log, err: @log) }
148
+
149
+ if defined? Bundler
150
+ Bundler.with_clean_env(&actual)
151
+ else
152
+ actual.call
153
+ end
154
+ end
155
+
156
+ def kill_process(timeout_sec = 10)
157
+ begin
158
+ begin
159
+ timeout(timeout_sec) { Process.waitpid(@pid) }
160
+ rescue Timeout::Error
161
+ Process.kill(:KILL, @pid)
162
+ end
163
+ rescue Errno::ECHILD
164
+ end
165
+ end
166
+
167
+ def start_watchdog
168
+ return if @watchdog && @watchdog.alive?
169
+
170
+ @watchdog = Thread.new do
171
+ pid, @exitstatus = Process.waitpid2(@pid)
172
+ call_hooks(:stop, @exitstatus)
173
+ end
174
+ end
175
+
176
+ def stop_watchdog
177
+ @watchdog.kill if @watchdog && @watchdog.alive?
178
+ end
179
+
180
+ def event_listener_addr
181
+ raise "event listener not started [BUG]" unless @event_listener_server
182
+
183
+ addr = @event_listener_server.addr
184
+ [addr[-1], addr[1]]
185
+ end
186
+
187
+ def start_listening_events
188
+ return if @event_listener_thread
189
+
190
+ @event_listener_server = TCPServer.new('localhost', 0)
191
+ @event_listener_thread = Thread.new do
192
+ event_listener_loop
193
+ end
194
+ end
195
+
196
+ def stop_listening_events
197
+ if @event_listener_thread && @event_listener_thread.alive?
198
+ @event_listener_thread.kill
199
+ end
200
+
201
+ if @event_listener_server && !@event_listener_server.closed?
202
+ @event_listener_server.close
203
+ end
204
+
205
+ @event_listener_thread = nil
206
+ @event_listener_server = nil
207
+ end
208
+
209
+ def event_listener_loop
210
+ while sock = @event_listener_server.accept
211
+ Thread.new do
212
+ begin
213
+ buf = ""
214
+ loop do
215
+ socks, _, _ = IO.select([sock], nil, nil, 5)
216
+ break unless socks
217
+
218
+ socks[0].read_nonblock(1024, buf)
219
+ break if socks[0].eof?
220
+ end
221
+
222
+ handle_event buf
223
+ ensure
224
+ sock.close unless sock.closed?
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ def handle_event(json)
231
+ event_payload = JSON.parse(json)
232
+ event = Event.new(event_payload['env'], payload: event_payload['input'])
233
+
234
+ call_hooks event.type.gsub(/-/, '_'), event
235
+ call_hooks 'event', event
236
+ rescue JSON::ParserError
237
+ # do nothing
238
+ end
239
+
240
+ def hooks_for(name)
241
+ @hooks[name.to_s] ||= []
242
+ end
243
+
244
+ def call_hooks(name, *args)
245
+ hooks_for(name).each do |hook|
246
+ hook.call(*args)
247
+ end
248
+ nil
249
+ end
250
+
251
+ def add_hook(name, block)
252
+ hooks_for(name) << block
253
+ end
254
+ end
255
+ end
256
+
@@ -0,0 +1,107 @@
1
+ require 'json'
2
+ require 'villein/tags'
3
+
4
+ module Villein
5
+ ##
6
+ # Villein::Client allows you to order existing serf agent.
7
+ # You will need RPC address and agent name to command.
8
+ class Client
9
+ def initialize(rpc_addr, name: nil, serf: 'serf', silence: true)
10
+ @rpc_addr = rpc_addr
11
+ @name = name
12
+ @serf = serf
13
+ @silence = true
14
+ end
15
+
16
+ def silence?() !!@silence; end
17
+ attr_writer :silence
18
+
19
+ attr_reader :name, :rpc_addr, :serf
20
+
21
+ def event(name, payload, coalesce: true)
22
+ options = []
23
+
24
+ unless coalesce
25
+ options << '-coalesce=false'
26
+ end
27
+
28
+ call_serf 'event', *options, name, payload
29
+ end
30
+
31
+ def join(addr, replay: false)
32
+ options = []
33
+
34
+ if replay
35
+ options << '-replay'
36
+ end
37
+
38
+ call_serf 'join', *options, addr
39
+ end
40
+
41
+ def leave
42
+ call_serf 'leave'
43
+ end
44
+
45
+ def force_leave(node)
46
+ call_serf 'force-leave', node
47
+ end
48
+
49
+ def members(status: nil, name: nil, tags: {})
50
+ options = ['-format', 'json']
51
+
52
+ options.push('-status', status.to_s) if status
53
+ options.push('-name', name.to_s) if name
54
+
55
+ tags.each do |tag, val|
56
+ options.push('-tag', "#{tag}=#{val}")
57
+ end
58
+
59
+ json = IO.popen(['serf', 'members', "-rpc-addr=#{rpc_addr}", *options], 'r', &:read)
60
+ response = JSON.parse(json)
61
+
62
+ response["members"]
63
+ end
64
+
65
+ ##
66
+ # Returns Villein::Tags object for the current agent.
67
+ # Villein::Tags provides high-level API for tagging agents.
68
+ def tags
69
+ @tags ||= Tags.new(self)
70
+ end
71
+
72
+ ##
73
+ # Get tag from the agent.
74
+ # Using Villein::Client#tags method is recommended. It provides high-level API via +Villein::Tags+.
75
+ def get_tags
76
+ me = members(name: self.name)[0]
77
+ me["tags"]
78
+ end
79
+
80
+ ##
81
+ # Remove tag from the agent.
82
+ # Using Villein::Client#tags method is recommended. It provides high-level API via +Villein::Tags+.
83
+ def delete_tag(key)
84
+ call_serf 'tags', '-delete', key
85
+ end
86
+
87
+ ##
88
+ # Set tag to the agent.
89
+ # Using Villein::Client#tags method is recommended. It provides high-level API via +Villein::Tags+.
90
+ def set_tag(key, val)
91
+ call_serf 'tags', '-set', "#{key}=#{val}"
92
+ end
93
+
94
+ private
95
+
96
+ def call_serf(cmd, *args)
97
+ options = {}
98
+
99
+ if silence?
100
+ options[:out] = File::NULL
101
+ options[:err] = File::NULL
102
+ end
103
+
104
+ system @serf, cmd, "-rpc-addr=#{rpc_addr}", *args, options
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,73 @@
1
+ module Villein
2
+ class Event
3
+ MEMBERS_EVENT = %w(member-join member-leave member-failed member-update member-reap)
4
+
5
+ def initialize(env={}, payload: nil)
6
+ @type = env['SERF_EVENT']
7
+ @self_name = env['SERF_SELF_NAME']
8
+ @self_tags = Hash[env.select{ |k, v| /^SERF_TAG_/ =~ k }.map { |k, v| [k.sub(/^SERF_TAG_/, ''), v] }]
9
+ @user_event = env['SERF_USER_EVENT']
10
+ @query_name = env['SERF_QUERY_NAME']
11
+ @user_ltime = env['SERF_USER_LTIME']
12
+ @query_ltime = env['SERF_QUERY_LTIME']
13
+ @payload = payload
14
+ end
15
+
16
+ attr_reader :type, :self_name, :self_tags, :user_event, :query_name, :user_ltime, :query_ltime, :payload
17
+
18
+ def ltime
19
+ user_ltime || query_ltime
20
+ end
21
+
22
+ ##
23
+ # Parse and returns member list in Array<Hash> when available.
24
+ # Always return +nil+ if the event type is not +member-*+.
25
+ def members
26
+ return nil unless MEMBERS_EVENT.include?(type)
27
+ @members ||= begin
28
+ payload.each_line.map do |line|
29
+ name, address, _, tags_str = line.chomp.split(/\t/)
30
+ {name: name, address: address, tags: parse_tags(tags_str || '')}
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def parse_tags(str)
38
+ # "aa=b=,,c=d,e=f,g,h,i=j" => {"aa"=>"b=,", "c"=>"d", "e"=>"f,g,h", "i"=>"j"}
39
+ tokens = str.scan(/(.+?)([,=]|\z)/).flatten
40
+
41
+ pairs = []
42
+ stack = []
43
+
44
+ while token = tokens.shift
45
+ case token
46
+ when "="
47
+ stack << token
48
+ when ","
49
+ stack << token
50
+
51
+ if tokens.first != ',' && 2 <= stack.size
52
+ pairs << stack.dup
53
+ stack.clear
54
+ end
55
+ else
56
+ stack << token
57
+ end
58
+ end
59
+ pairs << stack.dup unless stack.empty?
60
+
61
+ pairs = pairs.inject([]) { |r, pair|
62
+ if !pair.find{ |_| _ == '='.freeze } && r.last
63
+ r.last.push(*pair)
64
+ r
65
+ else
66
+ r << pair
67
+ end
68
+ }
69
+ pairs.each(&:pop)
70
+ Hash[pairs.map{ |_| _.join.split(/=/,2) }]
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,57 @@
1
+ module Villein
2
+ class Tags
3
+ def initialize(client) # :nodoc:
4
+ @client = client
5
+ reload
6
+ end
7
+
8
+ ##
9
+ # Set tag of the agent.
10
+ def []=(key, value)
11
+ if value
12
+ key = key.to_s
13
+ value = value.to_s
14
+ @client.set_tag(key, value)
15
+ @tags[key] = value
16
+ else
17
+ self.delete key.to_s
18
+ end
19
+ end
20
+
21
+ ##
22
+ # Returns tag of the agent.
23
+ # Note that this method is cached, you have to call +reload+ method to flush them.
24
+ def [](key)
25
+ @tags[key.to_s]
26
+ end
27
+
28
+ ##
29
+ # Remove tag from the agent.
30
+ def delete(key)
31
+ key = key.to_s
32
+
33
+ @client.delete_tag key
34
+ @tags.delete key
35
+
36
+ nil
37
+ end
38
+
39
+ def inspect
40
+ "#<Villein::Tags #{@tags.inspect}>"
41
+ end
42
+
43
+ ##
44
+ # Returns +Hash+ of tags.
45
+ def to_h
46
+ # duping
47
+ Hash[@tags.map{ |k,v| [k,v] }]
48
+ end
49
+
50
+ ##
51
+ # Reload tags of the agent.
52
+ def reload
53
+ @tags = @client.get_tags
54
+ self
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Villein
2
+ VERSION = "0.0.1"
3
+ end
data/lib/villein.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "villein/version"
2
+
3
+ module Villein
4
+ # Your code goes here...
5
+ end
6
+
7
+ require 'villein/client'
8
+ require 'villein/agent'
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ exec ruby -rsocket -rjson -e'TCPSocket.new(ARGV[0], ARGV[1].to_i).puts({env: Hash[ENV.select{|k,v|/SERF/ =~ k}], input: $stdin.read}.to_json)' -- $*
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+ require 'villein/client'
3
+
4
+ require 'villein/agent'
5
+
6
+ describe Villein::Agent do
7
+ it "inherits Villein::Client" do
8
+ expect(described_class.ancestors).to include(Villein::Client)
9
+ end
10
+
11
+ let(:bind) { ENV["VILLEIN_TEST_BIND"] || "127.0.0.1:17946" }
12
+ let(:rpc_addr) { ENV["VILLEIN_TEST_RPC_ADDR"] || "127.0.0.1:17373" }
13
+
14
+ subject(:agent) { described_class.new(rpc_addr: rpc_addr, bind: bind) }
15
+
16
+ before do
17
+ @pids = []
18
+ allow(agent).to(receive(:spawn) { |*args|
19
+ pid = Kernel.spawn(*args)
20
+ @pids << pid
21
+ pid
22
+ })
23
+ end
24
+
25
+ after do
26
+ @pids.each do |pid|
27
+ begin
28
+ begin
29
+ timeout(5) { Process.waitpid(pid) }
30
+ rescue Timeout::Error
31
+ Process.kill(:KILL, pid)
32
+ end
33
+ rescue Errno::ECHILD, Errno::ESRCH
34
+ end
35
+ end
36
+ end
37
+
38
+ it "can start and stop workers" do
39
+ received = nil
40
+ agent.on_stop { |arg| received = [true, arg] }
41
+
42
+ agent.start!
43
+
44
+ expect(agent.dead?).to be_false
45
+ expect(agent.running?).to be_true
46
+ expect(agent.started?).to be_true
47
+ expect(agent.pid).to be_a(Fixnum)
48
+
49
+ agent.stop!
50
+
51
+ expect(agent.dead?).to be_false
52
+ expect(agent.running?).to be_false
53
+ expect(agent.started?).to be_false
54
+ expect(agent.pid).to be_nil
55
+
56
+ expect(received).to eq [true, nil]
57
+ end
58
+
59
+ it "can receive events" do
60
+ received1, received2 = nil, nil
61
+
62
+ agent.on_event { |e| received1 = e }
63
+ agent.on_member_join { |e| received2 = e }
64
+
65
+ agent.start!
66
+ 20.times { break if received1; sleep 0.1 }
67
+ agent.stop!
68
+
69
+ expect(received1).to be_a(Villein::Event)
70
+ expect(received2).to be_a(Villein::Event)
71
+ expect(received1.type).to eq 'member-join'
72
+ expect(received2.type).to eq 'member-join'
73
+ end
74
+
75
+ it "can handle unexpected stop" do
76
+ received = nil
77
+ agent.on_stop { |status| received = status }
78
+
79
+ agent.start!
80
+ Process.kill(:KILL, agent.pid)
81
+
82
+ 20.times { break if received; sleep 0.1 }
83
+ expect(agent.dead?).to be_true
84
+ expect(agent.running?).to be_false
85
+ expect(agent.started?).to be_true
86
+ expect(received).to be_a_kind_of(Process::Status)
87
+ end
88
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+ require 'villein/client'
3
+
4
+ describe Villein::Client do
5
+ let(:name) { 'the-node' }
6
+ subject(:client) { described_class.new('x.x.x.x:nnnn', name: name) }
7
+
8
+ def expect_serf(cmd, *args, retval: true, out: File::NULL)
9
+ expect(subject).to receive(:system) \
10
+ .with('serf', cmd, '-rpc-addr=x.x.x.x:nnnn', *args, out: out, err: out) \
11
+ .and_return(retval)
12
+ end
13
+
14
+ describe "#event" do
15
+ it "sends user event" do
16
+ expect_serf('event', 'test', 'payload')
17
+
18
+ client.event('test', 'payload')
19
+ end
20
+
21
+ context "with coalesce=false" do
22
+ it "sends user event with the option" do
23
+ expect_serf('event', '-coalesce=false', 'test', 'payload')
24
+
25
+ client.event('test', 'payload', coalesce: false)
26
+ end
27
+ end
28
+ end
29
+
30
+ describe "#join" do
31
+ it "attempts to join another node" do
32
+ expect_serf('join', 'y.y.y.y:nnnn')
33
+
34
+ client.join('y.y.y.y:nnnn')
35
+ end
36
+
37
+ context "with replay=true" do
38
+ it "attempts to join another node with replaying" do
39
+ expect_serf('join', '-replay', 'y.y.y.y:nnnn')
40
+
41
+ client.join('y.y.y.y:nnnn', replay: true)
42
+ end
43
+ end
44
+ end
45
+
46
+ describe "#leave" do
47
+ it "attempts to leave from cluster" do
48
+ expect_serf('leave')
49
+
50
+ client.leave
51
+ end
52
+ end
53
+
54
+ describe "#force_leave" do
55
+ it "attempts to remove member forcely" do
56
+ expect_serf('force-leave', 'the-node')
57
+
58
+ client.force_leave('the-node')
59
+ end
60
+ end
61
+
62
+ describe "#members" do
63
+ let(:json) { (<<-EOJ).gsub(/\n|\s+/,'') }
64
+ {"members":[{"name":"the-node","addr":"a.a.a.a:mmmm","port": 7948,
65
+ "tags":{"key":"val"},"status":"alive","protocol":{"max":4,"min":2,"version":4}}]}
66
+ EOJ
67
+
68
+ it "returns member list" do
69
+ allow(IO).to receive(:popen).with(%w(serf members -rpc-addr=x.x.x.x:nnnn -format json), 'r') \
70
+ .and_yield(double('io', read: json))
71
+
72
+ expect(client.members).to be_a_kind_of(Array)
73
+ expect(client.members[0]["name"]).to eq "the-node"
74
+ end
75
+
76
+ context "with status filter" do
77
+ it "returns member list" do
78
+ allow(IO).to receive(:popen).with(%w(serf members -rpc-addr=x.x.x.x:nnnn -format json -status alive), 'r') \
79
+ .and_yield(double('io', read: json))
80
+
81
+ client.members(status: :alive)
82
+ end
83
+ end
84
+
85
+ context "with name filter" do
86
+ it "returns member list" do
87
+ allow(IO).to receive(:popen).with(%w(serf members -rpc-addr=x.x.x.x:nnnn -format json -name node), 'r') \
88
+ .and_yield(double('io', read: json))
89
+
90
+ client.members(name: 'node')
91
+ end
92
+ end
93
+
94
+ context "with tag filter" do
95
+ it "returns member list" do
96
+ allow(IO).to receive(:popen).with(%w(serf members -rpc-addr=x.x.x.x:nnnn -format json -tag a=1 -tag b=2), 'r') \
97
+ .and_yield(double('io', read: json))
98
+
99
+ client.members(tags: {a: '1', b: '2'})
100
+ end
101
+ end
102
+ end
103
+
104
+ describe "#tags" do
105
+ before do
106
+ allow(client).to receive(:get_tags).and_return('a' => 'b')
107
+ end
108
+
109
+ it "returns Villein::Tags" do
110
+ expect(client.tags).to be_a(Villein::Tags)
111
+ expect(client.tags['a']).to eq 'b'
112
+ end
113
+
114
+ it "memoizes" do
115
+ expect(client.tags.__id__ == client.tags.__id__).to be_true
116
+ end
117
+ end
118
+
119
+ describe "#set_tag" do
120
+ it "sets tag" do
121
+ expect_serf('tags', '-set', 'newkey=newval')
122
+
123
+ client.set_tag('newkey', 'newval')
124
+ end
125
+ end
126
+
127
+ describe "#delete_tag" do
128
+ it "deletes tag" do
129
+ expect_serf('tags', '-delete', 'newkey')
130
+
131
+ client.delete_tag('newkey')
132
+ end
133
+ end
134
+
135
+ describe "#get_tags" do
136
+ subject(:tags) { client.get_tags }
137
+
138
+ it "retrieves using #member(name: )" do
139
+ json = (<<-EOJ).gsub(/\n|\s+/,'')
140
+ {"members":[{"name":"the-node","addr":"a.a.a.a:mmmm","port": 7948,
141
+ "tags":{"key":"val"},"status":"alive","protocol":{"max":4,"min":2,"version":4}}]}
142
+ EOJ
143
+
144
+ allow(IO).to receive(:popen).with(%w(serf members -rpc-addr=x.x.x.x:nnnn -format json -name the-node), 'r') \
145
+ .and_yield(double('io', read: json))
146
+
147
+ expect(tags).to be_a_kind_of(Hash)
148
+ expect(tags['key']).to eq 'val'
149
+ end
150
+
151
+ context "without name" do
152
+ let(:name) { nil }
153
+
154
+ it "raises error" do
155
+ expect { tags }.to raise_error
156
+ end
157
+ end
158
+ end
159
+ end
160
+
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+ require 'villein/event'
3
+
4
+ describe Villein::Event do
5
+ it "holds event variables" do
6
+ event = Villein::Event.new(
7
+ 'SERF_EVENT' => 'type',
8
+ 'SERF_SELF_NAME' => 'self_name',
9
+ 'SERF_TAG_key' => 'val',
10
+ 'SERF_TAG_key2' => 'val2',
11
+ 'SERF_USER_EVENT' => 'user_event',
12
+ 'SERF_QUERY_NAME' => 'query_name',
13
+ 'SERF_USER_LTIME' => 'user_ltime',
14
+ 'SERF_QUERY_LTIME' => 'query_ltime',
15
+ payload: 'payload',
16
+ )
17
+
18
+ expect(event.type).to eq 'type'
19
+ expect(event.self_name).to eq 'self_name'
20
+ expect(event.self_tags).to eq('key' => 'val', 'key2' => 'val2')
21
+ expect(event.user_event).to eq 'user_event'
22
+ expect(event.query_name).to eq 'query_name'
23
+ expect(event.user_ltime).to eq 'user_ltime'
24
+ expect(event.query_ltime).to eq 'query_ltime'
25
+ expect(event.payload).to eq 'payload'
26
+ end
27
+
28
+ describe "#ltime" do
29
+ it "returns user_ltime or query_ltime" do
30
+ expect(described_class.new('SERF_USER_LTIME' => nil, 'SERF_QUERY_LTIME' => '2').ltime).to eq '2'
31
+ expect(described_class.new('SERF_USER_LTIME' => '1', 'SERF_QUERY_LTIME' => nil).ltime).to eq '1'
32
+ expect(described_class.new('SERF_USER_LTIME' => '1', 'SERF_QUERY_LTIME' => '2').ltime).to eq '1'
33
+ end
34
+ end
35
+
36
+ describe "#members" do
37
+ context "when event is member-*" do
38
+ let(:payload) { "the-node\tX.X.X.X\t\tkey=val,a=b\nanother-node\tY.Y.Y.Y\t\tkey=val,a=b\n" }
39
+ subject(:event) { Villein::Event.new('SERF_EVENT' => 'member-join', payload: payload) }
40
+
41
+ it "parses member list" do
42
+ expect(event.members).to be_a_kind_of(Array)
43
+ expect(event.members.size).to eq 2
44
+ expect(event.members[0]).to eq(name: 'the-node', address: 'X.X.X.X', tags: {'key' => 'val', 'a' => 'b'})
45
+ expect(event.members[1]).to eq(name: 'another-node', address: 'Y.Y.Y.Y', tags: {'key' => 'val', 'a' => 'b'})
46
+ end
47
+
48
+ context "with confused tags" do
49
+ let(:payload) { "the-node\tX.X.X.X\t\taa=b=,,c=d,e=f,g,h,i=j\n" }
50
+
51
+ it "parses greedily" do
52
+ expect(event.members[0][:tags]).to eq("aa"=>"b=,", "c"=>"d", "e"=>"f,g,h", "i"=>"j")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
data/spec/tags_spec.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'villein/tags'
3
+
4
+ describe Villein::Tags do
5
+ let(:parent) { double('parent', get_tags: {'a' => '1'}) }
6
+ subject(:tags) { described_class.new(parent) }
7
+
8
+ describe "#[]" do
9
+ it "returns value for key in Symbol" do
10
+ expect(tags[:a]).to eq '1'
11
+ end
12
+
13
+ it "returns value for key in String" do
14
+ expect(tags['a']).to eq '1'
15
+ end
16
+ end
17
+
18
+ describe "#[]=" do
19
+ it "sets value for key in Symbol, using parent#set_tag" do
20
+ expect(parent).to receive(:set_tag).with('b', "1").and_return('1')
21
+
22
+ tags['b'] = 1
23
+
24
+ expect(tags['b']).to eq '1'
25
+ end
26
+
27
+ it "sets value for key in String, using parent#set_tag" do
28
+ expect(parent).to receive(:set_tag).with('b', "1").and_return('1')
29
+
30
+ tags[:b] = 1
31
+
32
+ expect(tags[:b]).to eq '1'
33
+ end
34
+
35
+ context "with nil" do
36
+ it "deletes key" do
37
+ expect(tags).to receive(:delete).with('b')
38
+ tags[:b] = nil
39
+ end
40
+ end
41
+ end
42
+
43
+ describe "#delete" do
44
+ it "deletes key, using parent#delete_tag" do
45
+ expect(parent).to receive(:delete_tag).with('a')
46
+ tags.delete :a
47
+ expect(tags[:a]).to be_nil
48
+ end
49
+ end
50
+
51
+ describe "#to_h" do
52
+ it "returns hash" do
53
+ expect(tags.to_h).to eq('a' => '1')
54
+ end
55
+ end
56
+
57
+ describe "#reload" do
58
+ it "retrieves latest tag using parent#get_tags" do
59
+ tags # init
60
+ allow(parent).to receive(:get_tags).and_return('new' => 'tag')
61
+
62
+ expect {
63
+ tags.reload
64
+ }.to change { tags['new'] } \
65
+ .from(nil).to('tag')
66
+ end
67
+ end
68
+ end
data/villein.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'villein/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "villein"
8
+ spec.version = Villein::VERSION
9
+ spec.authors = ["Shota Fukumori (sora_h)"]
10
+ spec.email = ["sorah@cookpad.com", "her@sorah.jp"]
11
+ spec.summary = %q{Use `serf` (serfdom.io) from Ruby.}
12
+ spec.description = %q{Use serf (http://www.serfdom.io/) from Ruby.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "rspec", "2.14.1"
22
+ spec.add_development_dependency "bundler", "~> 1.5"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rdoc"
25
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: villein
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Shota Fukumori (sora_h)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.14.1
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.14.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Use serf (http://www.serfdom.io/) from Ruby.
70
+ email:
71
+ - sorah@cookpad.com
72
+ - her@sorah.jp
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - lib/villein.rb
85
+ - lib/villein/agent.rb
86
+ - lib/villein/client.rb
87
+ - lib/villein/event.rb
88
+ - lib/villein/tags.rb
89
+ - lib/villein/version.rb
90
+ - misc/villein-event-handler
91
+ - spec/agent_spec.rb
92
+ - spec/client_spec.rb
93
+ - spec/event_spec.rb
94
+ - spec/spec_helper.rb
95
+ - spec/tags_spec.rb
96
+ - villein.gemspec
97
+ homepage: ''
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.2.2
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Use `serf` (serfdom.io) from Ruby.
121
+ test_files:
122
+ - spec/agent_spec.rb
123
+ - spec/client_spec.rb
124
+ - spec/event_spec.rb
125
+ - spec/spec_helper.rb
126
+ - spec/tags_spec.rb