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