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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f0ff2a01e8c008418b2e2197c0ee87271c349673
4
- data.tar.gz: 6fe0634414b78225c6c898da48d5a2793f96e0b1
3
+ metadata.gz: ab02aaee627c779f2426f107a2e48d4e876e14b0
4
+ data.tar.gz: c6429bf923f68a9f5cb13981fcf96377c4e24cc5
5
5
  SHA512:
6
- metadata.gz: 565e2ad3c3c35d84991b7cd2bd35bb326464506a8cc87a8c2e4b782d2b2808fa04d1d9a5a8466ddbd208718a169a148f69216e42f945326ffc77f5ce72b6f2b4
7
- data.tar.gz: b39cdbb74ed2f2df86b354c2f3855a025d780c529cf0cff22e9e71339c44f37744e9c0e298320b01fe3f01d6640a38ef99c0543d247c1ace35c2eada0bbb7d05
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
@@ -5,7 +5,7 @@ require 'yard'
5
5
 
6
6
  RSpec::Core::RakeTask.new("spec")
7
7
 
8
- Rubocop::RakeTask.new(:rubocop) do |task|
8
+ RuboCop::RakeTask.new(:rubocop) do |task|
9
9
  task.patterns = ['lib/**/*.rb']
10
10
  end
11
11
 
@@ -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]
@@ -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
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Serfx
4
- class RPCError < RuntimeError ; end
4
+ class RPCError < RuntimeError; end
5
5
  end
@@ -6,7 +6,6 @@ module Serfx
6
6
  # body.
7
7
  #
8
8
  class Response
9
-
10
9
  # Header is composed of two sub-parts
11
10
  # - Seq : an integer representing the original request
12
11
  # - Error: a string that represent whether the request made, was
@@ -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 limitations long running tasks can not be
10
- # orchestrated or invoked as serf handler directly.
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
- # AsynchJob address this by spawning the task as a background job,
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 3`; do echo $i; sleep 1; done'",
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, 'task_fire' do |event|
31
- # puts job.run
32
- # end
33
- #
34
- # on :query, 'task_check' do |event|
35
- # puts job.state_info.inspect
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
- # on :query, 'task_kill' do |event|
39
- # puts job.kill
40
- # end
62
+ # run
41
63
  #
42
- # on :query, 'task_reap' do |event|
43
- # puts job.reap
44
- # end
64
+ # Which can be managed via serf as:
45
65
  #
46
- # run
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 => e
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 exists? or command.nil?
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(command, out: stdout_file, err: stderr_file)
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
- _ , status = Process.wait2(child_pid)
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.exists?(state_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'){|f| f.write(JSON.generate(state))}
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
@@ -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=~/^SERF/}.each do | 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 => e
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 => e
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 (event.name === cbk.name)
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
@@ -2,5 +2,5 @@
2
2
  #
3
3
  # Provides version as a contsant for the serf gem
4
4
  module Serfx
5
- VERSION = '0.0.4'
5
+ VERSION = '0.0.5'
6
6
  end
data/spec/data/handler.rb CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
1
3
  require 'serfx/utils/handler'
2
4
 
3
5
  include Serfx::Utils::Handler
@@ -7,5 +9,4 @@ on :query, 'upcase' do |event|
7
9
  STDOUT.write(event.payload.upcase)
8
10
  end
9
11
  end
10
-
11
12
  run
@@ -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
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-05-31 00:00:00.000000000 Z
11
+ date: 2014-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack