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