daemon_runner 0.2.2 → 0.3.0

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