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 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