serfx 0.0.4 → 0.0.5
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 +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
|