daemon_runner 0.2.2 → 0.3.0

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: 113e941352f0f027e74475522fbf103a2e2ab2b2
4
- data.tar.gz: c0fe5567cab12d9db836c618a52295a7a1e4e5d2
3
+ metadata.gz: d36f55746d2502664fad4104b312003f1b473b95
4
+ data.tar.gz: 6729f091a55d2ee3ebee5d0e72788589b88367c0
5
5
  SHA512:
6
- metadata.gz: a72c199d673e3edcb3af7ea87c307ea85325a76441eaa7aca735d76a30a6f4589e26cd87343fe5cbef7a20d10345d30da3bf830d3554d3d7620a3d7a9f5cdab9
7
- data.tar.gz: bd0fa329b8ebea978f1344e63eccf15cb416c3770f573d3fd73d0fb35cf2049c670400bd7212a694dcbd6e9226238f35cf2cf997c6481b298b6b71eb44d01642
6
+ metadata.gz: 6eaa76309f8b31a68ceb7ce508e7047c050fee14215f7330c28bf551f88c190bbcd37f3106aea7f595178441ab8560a5dcbffe852bf8a62fa0a75b515826a0d3
7
+ data.tar.gz: af1b9a90f5b429831c47b6df4091e175c493137d01b147bc9e43e1fce25e136167b0e64dade105b0884342e43ee25033ed8f8ade57fcbe22e27e4cb315a7a3b1
data/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
data/README.md CHANGED
@@ -40,6 +40,8 @@ In order to use this gem you must subclass `DaemonRunner::Client` and add a few
40
40
  * :error_sleep_time - Number of seconds to sleep before retying a failed task (**optional**, _default_: 5 seconds)
41
41
  * :post_task_sleep_time - Number of seconds to sleep after each task (**optional**, _default_: 1 seconds)
42
42
 
43
+ * `schedule` - How often the task should run. See [Scheduling](#scheduling) below. (**optional**)
44
+
43
45
  ### Example
44
46
 
45
47
  ```ruby
@@ -102,6 +104,49 @@ service = MyService::Client.new(options)
102
104
  service.start!
103
105
  ```
104
106
 
107
+ ### Scheduling
108
+ Tasks can define the schedule that they run on and how their schedule is executed.
109
+ To do this, define a method named `schedule` with an array formatted as:
110
+
111
+ ```ruby
112
+ [:schedule_type, duration]
113
+ ```
114
+
115
+ For example, if you wanted your task to run every 5 minutes, you would do the
116
+ following:
117
+
118
+ ```ruby
119
+ #!/usr/bin/env ruby
120
+
121
+ class MyService
122
+ class Tasks
123
+ class Quiz
124
+ def schedule
125
+ [:every, '5m']
126
+ end
127
+
128
+ def run!(args)
129
+ puts args
130
+ args
131
+ end
132
+ end
133
+ end
134
+ end
135
+ ```
136
+
137
+ This would execute the `run!` method every 5 minutes. Duration can be defined as a
138
+ string in the `'<number>s/m/h/d/w'` format or as a number of seconds. Schedule types
139
+ are `:in, :at, :every, :cron, :interval`. See rufus-scheduler's
140
+ [README](https://github.com/jmettraux/rufus-scheduler#in-at-every-interval-cron)
141
+ for more information.
142
+
143
+ The default for tasks that don't explicitly define a schedule is
144
+
145
+ ```ruby
146
+ def schedule
147
+ [:interval, options[:loop_sleep_time]]
148
+ end
149
+ ```
105
150
 
106
151
  ## Development
107
152
 
@@ -20,9 +20,12 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency "logging", "~> 2.1"
22
22
  spec.add_dependency "mixlib-shellout", "~> 2.2"
23
+ spec.add_dependency "diplomat", "~> 1.0"
24
+ spec.add_dependency "rufus-scheduler", "~> 3.2"
23
25
 
24
26
  spec.add_development_dependency "bundler", "~> 1.12"
25
27
  spec.add_development_dependency "rake", "~> 10.0"
26
28
  spec.add_development_dependency "minitest", "~> 5.0"
27
29
  spec.add_development_dependency "yard", "~> 0.8.7"
30
+ spec.add_development_dependency "dev-consul", "~> 0.6.4"
28
31
  end
data/examples/example1.rb CHANGED
@@ -16,6 +16,10 @@ end
16
16
  class MyService
17
17
  class Tasks
18
18
  class Bar
19
+ def schedule
20
+ [:cron, '*/1 * * * *']
21
+ end
22
+
19
23
  def run!(name)
20
24
  puts name
21
25
  name
@@ -28,6 +32,10 @@ class MyService
28
32
  class Tasks
29
33
  class Baz
30
34
  class << self
35
+ def schedule
36
+ [:interval, 10]
37
+ end
38
+
31
39
  def run!(args)
32
40
  name = args[0]
33
41
  reason = args[1]
@@ -40,13 +48,28 @@ class MyService
40
48
  end
41
49
  end
42
50
 
51
+ class MyService
52
+ class Tasks
53
+ class Quiz
54
+ def schedule
55
+ [:interval, '30s']
56
+ end
57
+ def foo!(args)
58
+ puts 'Firing error'
59
+ sargs
60
+ end
61
+ end
62
+ end
63
+ end
64
+
43
65
  class MyService
44
66
  class Client < DaemonRunner::Client
45
67
  def tasks
46
68
  [
47
69
  [::MyService::Tasks::Foo.new, 'run!'],
48
70
  [::MyService::Tasks::Bar.new, 'run!', 'bar'],
49
- [::MyService::Tasks::Baz, 'run!', 'baz', 'because']
71
+ [::MyService::Tasks::Baz, 'run!', 'baz', 'because'],
72
+ [::MyService::Tasks::Quiz.new, 'foo!', 'blarg', 'assdg']
50
73
  ]
51
74
  end
52
75
  end
data/lib/daemon_runner.rb CHANGED
@@ -2,3 +2,4 @@ require_relative 'daemon_runner/version'
2
2
  require_relative 'daemon_runner/logger'
3
3
  require_relative 'daemon_runner/client'
4
4
  require_relative 'daemon_runner/shell_out'
5
+ require_relative 'daemon_runner/session'
@@ -1,3 +1,5 @@
1
+ require 'rufus-scheduler'
2
+
1
3
  module DaemonRunner
2
4
  class Client
3
5
  include Logger
@@ -9,6 +11,22 @@ module DaemonRunner
9
11
 
10
12
  def initialize(options)
11
13
  @options = options
14
+
15
+ # Set error handling
16
+ # @param [Rufus::Scheduler::Job] job job that raised the error
17
+ # @param [RuntimeError] error the body of the error
18
+ def scheduler.on_error(job, error)
19
+ error_sleep_time = job[:error_sleep_time]
20
+ logger = job[:logger]
21
+ task_id = job[:task_id]
22
+
23
+ logger.error "#{task_id}: #{error}"
24
+ logger.debug "#{task_id}: Suspending #{task_id} for #{error_sleep_time} seconds"
25
+ job.pause
26
+ sleep error_sleep_time
27
+ logger.debug "#{task_id}: Resuming #{task_id}"
28
+ job.resume
29
+ end
12
30
  end
13
31
 
14
32
  # Hook to allow initial setup tasks before running tasks.
@@ -33,7 +51,14 @@ module DaemonRunner
33
51
  This must be an array of methods for the runner to call'
34
52
  end
35
53
 
36
- # @return [Fixnum] Number of seconds to sleep between loop interations.
54
+ # @return [Array<Symbol, String/Fixnum>] Schedule tuple-like with the type of schedule and its timing.
55
+ def schedule
56
+ # The default type is an `interval` which trigger, execute and then trigger again after
57
+ # the interval has elapsed.
58
+ [:interval, loop_sleep_time]
59
+ end
60
+
61
+ # @return [Fixnum] Number of seconds to sleep between loop interactions.
37
62
  def loop_sleep_time
38
63
  return @loop_sleep_time unless @loop_sleep_time.nil?
39
64
  @loop_sleep_time = if options[:loop_sleep_time].nil?
@@ -68,22 +93,16 @@ module DaemonRunner
68
93
  def start!
69
94
  wait
70
95
 
71
- loop do # Loop on tasks
72
- logger.warn 'Tasks list is empty' if tasks.empty?
73
- tasks.each do |task|
74
- run_task(task)
75
- sleep post_task_sleep_time
76
- end
77
-
78
- sleep loop_sleep_time
96
+ logger.warn 'Tasks list is empty' if tasks.empty?
97
+ tasks.each do |task|
98
+ run_task(task)
99
+ sleep post_task_sleep_time
79
100
  end
80
101
 
81
- rescue StandardError => e
82
- # Don't exit the process if initialization fails.
83
- logger.error(e)
84
-
85
- sleep error_sleep_time
86
- retry
102
+ scheduler.join
103
+ rescue SystemExit, Interrupt
104
+ logger.info 'Shutting down'
105
+ scheduler.shutdown
87
106
  end
88
107
 
89
108
  private
@@ -103,30 +122,76 @@ module DaemonRunner
103
122
  end
104
123
 
105
124
  out[:method] = task[1]
125
+
126
+ out[:task_id] = if out[:instance].respond_to?(:task_id)
127
+ out[:instance].send(:task_id).to_s
128
+ else
129
+ "#{out[:class_name]}.#{out[:method]}"
130
+ end
131
+ raise ArgumentError, 'Invalid task id' if out[:task_id].nil? || out[:task_id].empty?
132
+
106
133
  out[:args] = task[2..-1].flatten
107
134
  out
108
135
  end
109
136
 
137
+ # @private
138
+ # @param [Class] instance an instance of the task class
139
+ # @return [Hash<Symbol, String>] schedule parsed in parts: Schedule type and timing
140
+ def parse_schedule(instance)
141
+ valid_types = [:in, :at, :every, :interval, :cron]
142
+ out = {}
143
+ task_schedule = if instance.respond_to?(:schedule)
144
+ instance.send(:schedule)
145
+ else
146
+ schedule
147
+ end
148
+
149
+ raise ArgumentError, 'Malformed schedule definition, should be [TYPE, DURATION]' if task_schedule.length < 2
150
+ raise ArgumentError, 'Invalid schedule type' unless valid_types.include?(task_schedule[0].to_sym)
151
+
152
+ out[:type] = task_schedule[0].to_sym
153
+ out[:schedule] = task_schedule[1]
154
+ out
155
+ end
156
+
110
157
  # @private
111
158
  # @param [Array<String, String, Array>] task to run
112
159
  # @return [String] output returned from task
113
160
  def run_task(task)
114
161
  parsed_task = parse_task(task)
115
162
  instance = parsed_task[:instance]
163
+ schedule = parse_schedule(instance)
116
164
  class_name = parsed_task[:class_name]
117
165
  method = parsed_task[:method]
118
166
  args = parsed_task[:args]
119
- log_line = "Running #{class_name}.#{method}"
120
- log_line += "(#{args})" unless args.empty?
121
- logger.debug log_line
122
-
123
- out = if args.empty?
124
- instance.send(method.to_sym)
125
- else
126
- instance.send(method.to_sym, args)
127
- end
128
- logger.debug "Got: #{out}"
129
- out
167
+ task_id = parsed_task[:task_id]
168
+
169
+ # Schedule the task
170
+ schedule_log_line = "#{task_id}: Scheduling job #{class_name}.#{method} as `:#{schedule[:type]}` type"
171
+ schedule_log_line += " with schedule: #{schedule[:schedule]}"
172
+ logger.debug schedule_log_line
173
+
174
+ scheduler.send(schedule[:type], schedule[:schedule], :overlap => false, :job => true) do |job|
175
+ log_line = "#{task_id}: Running #{class_name}.#{method}"
176
+ log_line += "(#{args})" unless args.empty?
177
+ logger.debug log_line
178
+
179
+ job[:error_sleep_time] = error_sleep_time
180
+ job[:logger] = logger
181
+ job[:task_id] = task_id
182
+
183
+ out = if args.empty?
184
+ instance.send(method.to_sym)
185
+ else
186
+ instance.send(method.to_sym, args)
187
+ end
188
+ logger.debug "#{task_id}: Got: #{out}"
189
+ end
190
+ end
191
+
192
+ # @return [Rufus::Scheduler] A scheduler instance
193
+ def scheduler
194
+ @scheduler ||= ::Rufus::Scheduler.new
130
195
  end
131
196
  end
132
197
  end
@@ -0,0 +1,125 @@
1
+ require 'diplomat'
2
+
3
+ module DaemonRunner
4
+ #
5
+ # Manage distributed locks with Consul
6
+ #
7
+ class Session
8
+ include Logger
9
+
10
+ class << self
11
+ attr_reader :session
12
+
13
+ def start(name, **options)
14
+ @session ||= Session.new(name, options).renew!
15
+ end
16
+
17
+
18
+ # Acquire a lock with the current session, or initialize a new session
19
+ #
20
+ # @param path [String] A path in the Consul key-value space to lock
21
+ # @param lock_session [Session] The Session instance to lock the lock to
22
+ # @return [Boolean] `true` if the lock was acquired
23
+ #
24
+ def lock(path)
25
+ Diplomat::Lock.acquire(path, session.id)
26
+ end
27
+
28
+ # Release a lock held by the current session
29
+ #
30
+ # @param path [String] A path in the Consul key-value space to release
31
+ # @param lock_session [Session] The Session instance that the lock was acquired with
32
+ #
33
+ def release(path)
34
+ Diplomat::Lock.release(path, session.id)
35
+ end
36
+ end
37
+
38
+ # Consul session ID
39
+ attr_reader :id
40
+
41
+ # Session name
42
+ attr_reader :name
43
+
44
+ # Period, in seconds, after which session expires
45
+ attr_reader :ttl
46
+
47
+ # Period, in seconds, that a session's locks will be
48
+ attr_reader :delay
49
+
50
+ # Behavior when a session is invalidated, can be set to either release or delete
51
+ attr_reader :behavior
52
+
53
+ # @param name [String] Session name
54
+ # @option options [Fixnum] ttl (15) Session TTL in seconds
55
+ # @option options [Fixnum] delay (15) Session release dealy in seconds
56
+ # @option options [String] behavior (release) Session release behavior
57
+ def initialize(name, **options)
58
+ logger.info('Initializing a Consul session')
59
+
60
+ @name = name
61
+ @ttl = options.fetch(:ttl, 15)
62
+ @delay = options.fetch(:delay, 15)
63
+ @behavior = options.fetch(:behavior, 'release')
64
+
65
+ init
66
+ end
67
+
68
+ # Check if there is an active renew thread
69
+ #
70
+ # @return [Boolean] `true` if the thread is alive
71
+ def renew?
72
+ @renew.is_a?(Thread) && @renew.alive?
73
+ end
74
+
75
+ # Create a thread to periodically renew the lock session
76
+ #
77
+ def renew!
78
+ return if renew?
79
+
80
+ @renew = Thread.new do
81
+ ## Wakeup every TTL/2 seconds and renew the session
82
+ loop do
83
+ sleep ttl / 2
84
+
85
+ begin
86
+ logger.debug(" - Renewing Consul session #{id}")
87
+ Diplomat::Session.renew(id)
88
+
89
+ rescue Faraday::ResourceNotFound
90
+ logger.warn("Consul session #{id} has expired!")
91
+
92
+ init
93
+ rescue StandardError => e
94
+ ## Keep the thread from exiting
95
+ logger.error(e)
96
+ end
97
+ end
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ # Stop the renew thread and destroy the session
104
+ #
105
+ def destroy!
106
+ @renew.kill if renew?
107
+ Diplomat::Session.destroy(id)
108
+ end
109
+
110
+ private
111
+
112
+ # Initialize a session and store it's ID
113
+ #
114
+ def init
115
+ @id = Diplomat::Session.create(
116
+ :Name => name,
117
+ :TTL => "#{ttl}s",
118
+ :LockDelay => "#{delay}s",
119
+ :Behavior => behavior
120
+ )
121
+
122
+ logger.info(" - Initialized a Consul session #{id}")
123
+ end
124
+ end
125
+ end
@@ -3,17 +3,30 @@ require 'mixlib/shellout'
3
3
  module DaemonRunner
4
4
  class ShellOut
5
5
  attr_reader :runner, :stdout
6
- attr_reader :command, :cwd, :timeout, :wait
6
+ attr_reader :command, :cwd, :timeout, :wait, :valid_exit_codes
7
+
8
+ # Wait for the process with the given pid to finish
9
+ # @param pid [Fixnum] the pid to wait on
10
+ # @param flags [Fixnum] flags to Process.wait2
11
+ # @return [Process::Status, nil] the process status or nil if no process was found
12
+ def self.wait2(pid = nil, flags = 0)
13
+ return nil if pid.nil?
14
+ Process.wait2(pid, flags)[1]
15
+ rescue Errno::ECHILD
16
+ nil
17
+ end
7
18
 
8
19
  # @param command [String] the command to run
9
20
  # @param cwd [String] the working directory to run the command
10
21
  # @param timeout [Fixnum] the command timeout
11
22
  # @param wait [Boolean] wheather to wait for the command to finish
12
- def initialize(command: nil, cwd: '/tmp', timeout: 15, wait: true)
23
+ # @param valid_exit_codes [Array<Fixnum>] exit codes that aren't flagged as failures
24
+ def initialize(command: nil, cwd: '/tmp', timeout: 15, wait: true, valid_exit_codes: [0])
13
25
  @command = command
14
26
  @cwd = cwd
15
27
  @timeout = timeout
16
28
  @wait = wait
29
+ @valid_exit_codes = valid_exit_codes
17
30
  end
18
31
 
19
32
  # Run command
@@ -27,6 +40,13 @@ module DaemonRunner
27
40
  end
28
41
  end
29
42
 
43
+ # Wait for the process to finish
44
+ # @param flags [Fixnum] flags to Process.wait2
45
+ # @return [Process::Status, nil] the process status or nil if no process was found
46
+ def wait2(flags = 0)
47
+ self.class.wait2(@pid, flags)
48
+ end
49
+
30
50
  private
31
51
 
32
52
  # Run a command and wait for it to finish
@@ -45,9 +65,10 @@ module DaemonRunner
45
65
  # @return [Fixnum] process id
46
66
  def run_and_detach
47
67
  log_r, log_w = IO.pipe
48
- Process.spawn(command, pgroup: true, err: :out, out: log_w)
68
+ @pid = Process.spawn(command, pgroup: true, err: :out, out: log_w)
49
69
  log_r.close
50
70
  log_w.close
71
+ @pid
51
72
  end
52
73
 
53
74
  # Validate command is defined before trying to start the command
@@ -74,6 +95,7 @@ module DaemonRunner
74
95
  # @return [Mixlib::ShellOut] client
75
96
  def runner
76
97
  @runner ||= Mixlib::ShellOut.new(command, :cwd => cwd, :timeout => timeout)
98
+ @runner.valid_exit_codes = valid_exit_codes
77
99
  end
78
100
  end
79
101
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daemon_runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Thompson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-12 00:00:00.000000000 Z
11
+ date: 2016-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logging
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: diplomat
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rufus-scheduler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.2'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: bundler
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,20 @@ dependencies:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
124
  version: 0.8.7
125
+ - !ruby/object:Gem::Dependency
126
+ name: dev-consul
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.6.4
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.6.4
97
139
  description:
98
140
  email:
99
141
  - Andrew_Thompson@rapid7.com
@@ -101,6 +143,7 @@ executables: []
101
143
  extensions: []
102
144
  extra_rdoc_files: []
103
145
  files:
146
+ - ".editorconfig"
104
147
  - ".gitignore"
105
148
  - ".travis.yml"
106
149
  - CODE_OF_CONDUCT.md
@@ -116,6 +159,7 @@ files:
116
159
  - lib/daemon_runner.rb
117
160
  - lib/daemon_runner/client.rb
118
161
  - lib/daemon_runner/logger.rb
162
+ - lib/daemon_runner/session.rb
119
163
  - lib/daemon_runner/shell_out.rb
120
164
  - lib/daemon_runner/version.rb
121
165
  homepage: https://github.com/rapid7/daemon_runner/