serfx 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +94 -0
- data/Rakefile +1 -1
- data/lib/serfx/commands.rb +8 -8
- data/lib/serfx/connection.rb +5 -7
- data/lib/serfx/exceptions.rb +1 -1
- data/lib/serfx/response.rb +0 -1
- data/lib/serfx/utils/async_job.rb +59 -32
- data/lib/serfx/utils/handler.rb +6 -9
- data/lib/serfx/version.rb +1 -1
- data/spec/data/handler.rb +2 -1
- data/spec/serfx/client_spec.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab02aaee627c779f2426f107a2e48d4e876e14b0
|
4
|
+
data.tar.gz: c6429bf923f68a9f5cb13981fcf96377c4e24cc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58f1c41cfcd50252d7618e8370a37d86432647d8897c41cb677bbc17c125e19b6b716a264bbedafe3813900b84a6157db4822f7a346eb829ec0d994f4a040694
|
7
|
+
data.tar.gz: 644b79b040b7a4cfbe507b1264d2709622a747dcd4b6f5ba8ed8d2af18067703652748e135452649f82401267456a667eae246e5e9df68d9da805edd1633b3ef
|
data/README.md
CHANGED
@@ -48,6 +48,100 @@ conn.query('foo', 'bar') do |response|
|
|
48
48
|
end
|
49
49
|
```
|
50
50
|
|
51
|
+
## Writing custom handlers
|
52
|
+
|
53
|
+
serf agents can be configured to invoke an executable script when an user event is received.
|
54
|
+
|
55
|
+
`Serfx::Utils::Handler` module provides a set of helper methods to ease writing ruby based serf event handlers. It wraps the data passed via serf into a convenient `SerfEvent` object, as well as provides observer like API where callbacks can be registered based on event name and type.
|
56
|
+
|
57
|
+
For example, following script will respond to any qury event named 'upcase' and return the uppercase version of the original query event's payload
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
require 'serfx/utils/handler'
|
61
|
+
|
62
|
+
include Serfx::Utils::Handler
|
63
|
+
|
64
|
+
on :query, 'upcase' do |event|
|
65
|
+
STDOUT.write(event.payload.upcase)
|
66
|
+
end
|
67
|
+
|
68
|
+
run
|
69
|
+
```
|
70
|
+
|
71
|
+
Assuming this event handler is configured with `upcase` user event (-event-handler 'query:upcase=/path/to/handler'), it can be used as:
|
72
|
+
|
73
|
+
```sh
|
74
|
+
serf query -no-ack upcase foo
|
75
|
+
Response from 'node1': FOO
|
76
|
+
```
|
77
|
+
|
78
|
+
## Managing long running tasks via serf handlers
|
79
|
+
|
80
|
+
Serf event handler invocations are blocking calls. i.e. serf
|
81
|
+
will not process any other event when a handler invocation is
|
82
|
+
in progress. Due to this, long running tasks should not be
|
83
|
+
invoked as serf handler directly.
|
84
|
+
|
85
|
+
AsyncJob helps buildng serf handlers that involve long running commands.
|
86
|
+
It starts the command in background, allowing handler code to
|
87
|
+
return immediately. It does double fork where the first child process is
|
88
|
+
detached (attached to init as parent process) and and the target long
|
89
|
+
running task is spawned as a second child process. This allows the first
|
90
|
+
child process to wait and reap the output of actual long running task.
|
91
|
+
|
92
|
+
The first child process updates a state file before spawing
|
93
|
+
the long ranning task(state='invoking'), during the lon running task
|
94
|
+
execution (state='running') and after the spawned process' return
|
95
|
+
(state='finished'). This state file provides a convenient way to
|
96
|
+
query the current state of an AsyncJob.
|
97
|
+
|
98
|
+
AsyncJob porvide four methods to manage jobs. AsyncJob#start will
|
99
|
+
start the task. Once started, `AyncJob#state_info` can be used to check
|
100
|
+
whether the job is still running or finished. One started a job can be
|
101
|
+
either in 'running' state or in 'finished' state. `AsyncJob#reap`
|
102
|
+
is used for deleting the state file once the task is finished.
|
103
|
+
An AsyncJob can be killed, if its in running state, using the
|
104
|
+
`AsyncJob#kill` method. A new AyncJob can not be started unless previous
|
105
|
+
AsyncJob with same name/state file is reaped.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
require 'serfx/utils/async_job'
|
109
|
+
require 'serfx/utils/handler'
|
110
|
+
|
111
|
+
include Serfx::Utils::Handler
|
112
|
+
|
113
|
+
job = Serfx::Utils::AsyncJob.new(
|
114
|
+
name: "bash_test"
|
115
|
+
command: "bash -c 'for i in `seq 1 300`; do echo $i; sleep 1; done'",
|
116
|
+
state: '/opt/serf/states/long_task'
|
117
|
+
)
|
118
|
+
|
119
|
+
on :query, 'bash_test' do |event|
|
120
|
+
case event.payload
|
121
|
+
when 'start'
|
122
|
+
puts job.start
|
123
|
+
when 'kill'
|
124
|
+
puts job.kill
|
125
|
+
when 'reap'
|
126
|
+
puts job.reap
|
127
|
+
when 'check'
|
128
|
+
puts job.state_info
|
129
|
+
else
|
130
|
+
puts 'failed'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
run
|
135
|
+
```
|
136
|
+
Assuming this handler is configured with `bash_test` query events (-event-handler query:bash_test=/path/to/handler), it can be used as:
|
137
|
+
|
138
|
+
```sh
|
139
|
+
serf query bash_test start
|
140
|
+
serf query bash_test check # check if job is running or finished
|
141
|
+
serf query bash_test reap # delete a finished job's state file
|
142
|
+
serf query bash_test kill
|
143
|
+
```
|
144
|
+
|
51
145
|
## Specifying connection details
|
52
146
|
By default Serfx will try to connect to localhost at port 7373 (serf agent's default RPC port). Both `Serfx::Connection#new` as well as `Serfx.connect` accepts a hash specifying connection options i.e host, port, encryption, which can be used to specify non-default values.
|
53
147
|
|
data/Rakefile
CHANGED
data/lib/serfx/commands.rb
CHANGED
@@ -8,7 +8,6 @@ module Serfx
|
|
8
8
|
# Implements all of Serf's rpc commands using
|
9
9
|
# Serfx::Connection#request method
|
10
10
|
module Commands
|
11
|
-
|
12
11
|
# performs initial hanshake of an RPC session. Handshake has to be the
|
13
12
|
# first command to be invoked during an RPC session.
|
14
13
|
#
|
@@ -43,13 +42,13 @@ module Serfx
|
|
43
42
|
end
|
44
43
|
|
45
44
|
# force a failed node to leave the cluster
|
46
|
-
#
|
45
|
+
#
|
47
46
|
# @param node [String] name of the failed node
|
48
47
|
# @return [Response]
|
49
48
|
def force_leave(node)
|
50
49
|
request(:force_leave, 'Node' => node)
|
51
50
|
end
|
52
|
-
|
51
|
+
|
53
52
|
# join an existing cluster.
|
54
53
|
#
|
55
54
|
# @param existing [Array] an array of existing serf agents
|
@@ -58,7 +57,7 @@ module Serfx
|
|
58
57
|
def join(existing, replay = false)
|
59
58
|
request(:join, 'Existing' => existing, 'Replay' => replay)
|
60
59
|
end
|
61
|
-
|
60
|
+
|
62
61
|
# obtain the list of existing members
|
63
62
|
#
|
64
63
|
# @return [Response]
|
@@ -71,12 +70,13 @@ module Serfx
|
|
71
70
|
# @param tags [Array] an array of tags for filter
|
72
71
|
# @param status [Boolean] filter members based on their satatus
|
73
72
|
# @param name [String] filter based on exact name or pattern.
|
73
|
+
#
|
74
74
|
# @return [Response]
|
75
75
|
def members_filtered(tags, status = 'alive', name = nil)
|
76
76
|
filter = {
|
77
77
|
'Tags' => tags,
|
78
78
|
'Status' => status
|
79
|
-
|
79
|
+
}
|
80
80
|
filter['Name'] = name unless name.nil?
|
81
81
|
request(:members_filtered, filter)
|
82
82
|
end
|
@@ -114,7 +114,7 @@ module Serfx
|
|
114
114
|
end
|
115
115
|
[res, t]
|
116
116
|
end
|
117
|
-
|
117
|
+
|
118
118
|
# monitor is similar to the stream command, but instead of events it
|
119
119
|
# subscribes the channel to log messages from the agent
|
120
120
|
#
|
@@ -123,12 +123,12 @@ module Serfx
|
|
123
123
|
def monitor(loglevel = 'debug')
|
124
124
|
request(:monitor, 'LogLevel' => loglevel.upcase)
|
125
125
|
end
|
126
|
-
|
126
|
+
|
127
127
|
# stop is used to stop either a stream or monitor
|
128
128
|
def stop(sequence_number)
|
129
129
|
tcp_send(:stop, 'Stop' => sequence_number)
|
130
130
|
end
|
131
|
-
|
131
|
+
|
132
132
|
# leave is used trigger a graceful leave and shutdown of the current agent
|
133
133
|
#
|
134
134
|
# @return [Response]
|
data/lib/serfx/connection.rb
CHANGED
@@ -34,7 +34,7 @@ module Serfx
|
|
34
34
|
remove_key: [:header, :body],
|
35
35
|
list_keys: [:header, :body],
|
36
36
|
stats: [:header, :body]
|
37
|
-
|
37
|
+
}
|
38
38
|
|
39
39
|
include Serfx::Commands
|
40
40
|
extend Forwardable
|
@@ -69,7 +69,6 @@ module Serfx
|
|
69
69
|
def unpacker
|
70
70
|
@unpacker ||= MessagePack::Unpacker.new(socket)
|
71
71
|
end
|
72
|
-
|
73
72
|
# read data from tcp socket and pipe it through msgpack unpacker for
|
74
73
|
# deserialization
|
75
74
|
#
|
@@ -77,7 +76,7 @@ module Serfx
|
|
77
76
|
def read_data
|
78
77
|
unpacker.read
|
79
78
|
end
|
80
|
-
|
79
|
+
|
81
80
|
# takes raw RPC command name and an optional request body
|
82
81
|
# and convert them to msgpack encoded data and then send
|
83
82
|
# over tcp
|
@@ -91,7 +90,7 @@ module Serfx
|
|
91
90
|
header = {
|
92
91
|
'Command' => command.to_s.gsub('_', '-'),
|
93
92
|
'Seq' => seq
|
94
|
-
|
93
|
+
}
|
95
94
|
Log.info("#{__method__}|Header: #{header.inspect}")
|
96
95
|
buff = MessagePack::Buffer.new
|
97
96
|
buff << header.to_msgpack
|
@@ -101,16 +100,15 @@ module Serfx
|
|
101
100
|
@requests[seq] = { header: header, ack?: false }
|
102
101
|
seq
|
103
102
|
end
|
104
|
-
|
103
|
+
|
105
104
|
# checks if the RPC response header has `error` field popular or not
|
106
105
|
# raises [RPCError] exception if error string is not empty
|
107
|
-
#
|
106
|
+
#
|
108
107
|
# @param header [Hash] RPC response header as hash
|
109
108
|
def check_rpc_error!(header)
|
110
109
|
fail RPCError, header['Error'] unless header['Error'].empty?
|
111
110
|
end
|
112
111
|
|
113
|
-
|
114
112
|
# read data from the tcp socket. and convert it to a [Response] object
|
115
113
|
#
|
116
114
|
# @param command [String] RPC command name for which response will be read
|
data/lib/serfx/exceptions.rb
CHANGED
data/lib/serfx/response.rb
CHANGED
@@ -3,49 +3,71 @@
|
|
3
3
|
require 'json'
|
4
4
|
module Serfx
|
5
5
|
module Utils
|
6
|
-
|
7
6
|
# Serf event handler invocations are blocking calls. i.e. serf
|
8
7
|
# will not process any other event when a handler invocation is
|
9
|
-
# in progress. Due to this
|
10
|
-
#
|
8
|
+
# in progress. Due to this, long running tasks should not be
|
9
|
+
# invoked as serf handler directly.
|
10
|
+
#
|
11
|
+
# AsyncJob helps buildng serf handlers that involve long running commands.
|
12
|
+
# It starts the command in background, allowing handler code to
|
13
|
+
# return immediately. It does double fork where the first child process is
|
14
|
+
# detached (attached to init as parent process) and and the target long
|
15
|
+
# running task is spawned as a second child process. This allows the first
|
16
|
+
# child process to wait and reap the output of actual long running task.
|
17
|
+
#
|
18
|
+
# The first child process updates a state file before spawing
|
19
|
+
# the long ranning task(state='invoking'), during the lon running task
|
20
|
+
# execution (state='running') and after the spawned process' return
|
21
|
+
# (state='finished'). This state file provides a convenient way to
|
22
|
+
# query the current state of an AsyncJob.
|
23
|
+
#
|
24
|
+
# AsyncJob porvide four methods to manage jobs. AsyncJob#start will
|
25
|
+
# start the task. Once started, AyncJob#state_info can be used to check
|
26
|
+
# whether the job is still running or finished. One started a job can be
|
27
|
+
# either in 'running' state or in 'finished' state. AsyncJob#reap
|
28
|
+
# is used for deleting the state file once the task is finished.
|
29
|
+
# An AsyncJob can be killed, if its in running state, using the
|
30
|
+
# AsyncJob#kill method. A new AyncJob can not be started unless previous
|
31
|
+
# AsyncJob with same name/state file is reaped.
|
11
32
|
#
|
12
|
-
#
|
13
|
-
# allowing the handler code to return immediately. It does double fork
|
14
|
-
# where the first child process is detached (attached to init as parent
|
15
|
-
# process) and spawn the second child process with the target,
|
16
|
-
# long running task. This allows the parent process to wait and reap the
|
17
|
-
# output of target task and save it in disk so that it can be exposed
|
18
|
-
# via other serf events
|
33
|
+
# Following is an example of writing a serf handler using AsyncJob.
|
19
34
|
#
|
20
35
|
# @example
|
21
36
|
# require 'serfx/utils/async_job'
|
22
37
|
# require 'serfx/utils/handler'
|
23
38
|
#
|
39
|
+
# include Serfx::Utils::Handler
|
40
|
+
#
|
24
41
|
# job = Serfx::Utils::AsyncJob.new(
|
25
42
|
# name: "bash_test"
|
26
|
-
# command: "bash -c 'for i in `seq 1
|
43
|
+
# command: "bash -c 'for i in `seq 1 300`; do echo $i; sleep 1; done'",
|
27
44
|
# state: '/opt/serf/states/long_task'
|
28
45
|
# )
|
29
46
|
#
|
30
|
-
# on :query, '
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
47
|
+
# on :query, 'bash_test' do |event|
|
48
|
+
# case event.payload
|
49
|
+
# when 'start'
|
50
|
+
# puts job.start
|
51
|
+
# when 'kill'
|
52
|
+
# puts job.kill
|
53
|
+
# when 'reap'
|
54
|
+
# puts job.reap
|
55
|
+
# when 'check'
|
56
|
+
# puts job.state_info
|
57
|
+
# else
|
58
|
+
# puts 'failed'
|
59
|
+
# end
|
36
60
|
# end
|
37
61
|
#
|
38
|
-
#
|
39
|
-
# puts job.kill
|
40
|
-
# end
|
62
|
+
# run
|
41
63
|
#
|
42
|
-
#
|
43
|
-
# puts job.reap
|
44
|
-
# end
|
64
|
+
# Which can be managed via serf as:
|
45
65
|
#
|
46
|
-
#
|
66
|
+
# serf query bash_test start
|
67
|
+
# serf query bash_test check # check if job is running or finished
|
68
|
+
# serf query bash_test reap # delete a finished job's state file
|
69
|
+
# serf query bash_test kill
|
47
70
|
class AsyncJob
|
48
|
-
|
49
71
|
attr_reader :command, :state_file, :stdout_file, :stderr_file
|
50
72
|
|
51
73
|
# @param opts [Hash] specify the job details
|
@@ -71,7 +93,7 @@ module Serfx
|
|
71
93
|
begin
|
72
94
|
Process.kill(sig, state_info['pid'].to_i)
|
73
95
|
'success'
|
74
|
-
rescue Exception
|
96
|
+
rescue Exception
|
75
97
|
'failed'
|
76
98
|
end
|
77
99
|
else
|
@@ -102,7 +124,6 @@ module Serfx
|
|
102
124
|
end
|
103
125
|
end
|
104
126
|
|
105
|
-
|
106
127
|
# start a background daemon and spawn another process to run specified
|
107
128
|
# command. writes back state information in the state file
|
108
129
|
# after spawning daemon process (state=invoking), after spawning the
|
@@ -111,7 +132,7 @@ module Serfx
|
|
111
132
|
#
|
112
133
|
# @return [String] 'success' if task is started
|
113
134
|
def start
|
114
|
-
if
|
135
|
+
if exist? || command.nil?
|
115
136
|
return 'failed'
|
116
137
|
end
|
117
138
|
pid = fork do
|
@@ -124,11 +145,15 @@ module Serfx
|
|
124
145
|
}
|
125
146
|
write_state(state)
|
126
147
|
begin
|
127
|
-
child_pid = Process.spawn(
|
148
|
+
child_pid = Process.spawn(
|
149
|
+
command,
|
150
|
+
out: stdout_file,
|
151
|
+
err: stderr_file
|
152
|
+
)
|
128
153
|
state[:pid] = child_pid
|
129
154
|
state[:status] = 'running'
|
130
155
|
write_state(state)
|
131
|
-
_
|
156
|
+
_, status = Process.wait2(child_pid)
|
132
157
|
state[:exitstatus] = status.exitstatus
|
133
158
|
state[:status] = 'finished'
|
134
159
|
rescue Errno::ENOENT => e
|
@@ -159,13 +184,15 @@ module Serfx
|
|
159
184
|
#
|
160
185
|
# @return [TrueClass, FalseClass] true if the task exists, else false
|
161
186
|
def exists?
|
162
|
-
File.
|
187
|
+
File.exist?(state_file)
|
163
188
|
end
|
164
189
|
|
165
190
|
# writes a hash as json in the state_file
|
166
191
|
# @param [Hash] state represented as a hash, to be written
|
167
192
|
def write_state(state)
|
168
|
-
File.open(state_file, 'w')
|
193
|
+
File.open(state_file, 'w') do |f|
|
194
|
+
f.write(JSON.generate(state))
|
195
|
+
end
|
169
196
|
end
|
170
197
|
end
|
171
198
|
end
|
data/lib/serfx/utils/handler.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
module Serfx
|
2
2
|
module Utils
|
3
|
-
|
4
3
|
# helper module to for serf custom handlers
|
5
4
|
#
|
6
5
|
# serf agents can be configured to invoke an executable
|
@@ -22,22 +21,20 @@ module Serfx
|
|
22
21
|
# end
|
23
22
|
# run
|
24
23
|
module Handler
|
25
|
-
|
26
24
|
# when serf agent invokes a handler it passes the event payload
|
27
25
|
# through STDIN. while event metadata such as event type, name etc
|
28
26
|
# is passed as a set of environment variables.
|
29
27
|
# [SerfEvent] encapsulates such event.
|
30
28
|
class SerfEvent
|
31
|
-
|
32
29
|
attr_reader :environment, :payload, :type, :name
|
33
30
|
|
34
31
|
# @param env [Hash] environment
|
35
32
|
# @param stdin [IO] stadard input stream for the event
|
36
33
|
def initialize(env = ENV, stdin = STDIN)
|
37
|
-
@environment ={}
|
34
|
+
@environment = {}
|
38
35
|
@payload = nil
|
39
36
|
@name = nil
|
40
|
-
env.keys.select{|k|k
|
37
|
+
env.keys.select { |k| k =~ /^SERF/ }.each do | k|
|
41
38
|
@environment[k] = env[k].strip
|
42
39
|
end
|
43
40
|
@type = @environment['SERF_EVENT']
|
@@ -46,13 +43,13 @@ module Serfx
|
|
46
43
|
@name = @environment['SERF_QUERY_NAME']
|
47
44
|
begin
|
48
45
|
@payload = stdin.read_nonblock(4096).strip
|
49
|
-
rescue Errno::EAGAIN
|
46
|
+
rescue Errno::EAGAIN, EOFError
|
50
47
|
end
|
51
48
|
when 'user'
|
52
49
|
@name = @environment['SERF_USER_EVENT']
|
53
50
|
begin
|
54
51
|
@payload = stdin.read_nonblock(4096).strip
|
55
|
-
rescue Errno::EAGAIN
|
52
|
+
rescue Errno::EAGAIN, EOFError
|
56
53
|
end
|
57
54
|
end
|
58
55
|
end
|
@@ -72,7 +69,7 @@ module Serfx
|
|
72
69
|
event = SerfEvent.new
|
73
70
|
callbacks[event.type.downcase.to_sym].each do |cbk|
|
74
71
|
if cbk.name
|
75
|
-
cbk.block.call(event) if
|
72
|
+
cbk.block.call(event) if event.name === cbk.name
|
76
73
|
else
|
77
74
|
cbk.block.call(event)
|
78
75
|
end
|
@@ -84,7 +81,7 @@ module Serfx
|
|
84
81
|
SerfCallback = Struct.new(:name, :block)
|
85
82
|
|
86
83
|
def callbacks
|
87
|
-
@_callbacks ||= Hash.new{|h,k| h[k] = []}
|
84
|
+
@_callbacks ||= Hash.new { |h, k| h[k] = [] }
|
88
85
|
end
|
89
86
|
end
|
90
87
|
end
|
data/lib/serfx/version.rb
CHANGED
data/spec/data/handler.rb
CHANGED
data/spec/serfx/client_spec.rb
CHANGED
@@ -133,12 +133,16 @@ describe Serfx do
|
|
133
133
|
keys = @conn.list_keys.body['Keys'].keys
|
134
134
|
expect(keys).to include('QHOYjmYlxSCBhdfiolhtDQ==')
|
135
135
|
@conn.install_key('Ih6cZqutM33tMdoFo1iNyw==')
|
136
|
+
sleep 2
|
136
137
|
keys = @conn.list_keys.body['Keys'].keys
|
137
138
|
expect(keys).to include('Ih6cZqutM33tMdoFo1iNyw==')
|
139
|
+
sleep 2
|
138
140
|
@conn.use_key('Ih6cZqutM33tMdoFo1iNyw==')
|
139
141
|
new_keys = @conn.list_keys.body['Keys'].keys
|
142
|
+
sleep 2
|
140
143
|
expect(new_keys.first).to eq('Ih6cZqutM33tMdoFo1iNyw==')
|
141
144
|
@conn.remove_key('QHOYjmYlxSCBhdfiolhtDQ==')
|
145
|
+
sleep 2
|
142
146
|
final_keys = @conn.list_keys.body['Keys'].keys
|
143
147
|
expect(final_keys.first).to_not include('QHOYjmYlxSCBhdfiolhtDQ==')
|
144
148
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serfx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rnjib Dey
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|