villein 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +23 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +149 -0
- data/Rakefile +9 -0
- data/lib/villein/agent.rb +256 -0
- data/lib/villein/client.rb +107 -0
- data/lib/villein/event.rb +73 -0
- data/lib/villein/tags.rb +57 -0
- data/lib/villein/version.rb +3 -0
- data/lib/villein.rb +8 -0
- data/misc/villein-event-handler +2 -0
- data/spec/agent_spec.rb +88 -0
- data/spec/client_spec.rb +160 -0
- data/spec/event_spec.rb +57 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/tags_spec.rb +68 -0
- data/villein.gemspec +25 -0
- metadata +126 -0
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
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
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,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
|
data/lib/villein/tags.rb
ADDED
@@ -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
|
data/lib/villein.rb
ADDED
data/spec/agent_spec.rb
ADDED
@@ -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
|
data/spec/client_spec.rb
ADDED
@@ -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
|
+
|
data/spec/event_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|