process_handler 0.1.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 +7 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +62 -0
- data/README.md +66 -0
- data/Rakefile +7 -0
- data/config.reek +3 -0
- data/lib/salemove/process_handler.rb +6 -0
- data/lib/salemove/process_handler/composite_process.rb +54 -0
- data/lib/salemove/process_handler/cron_process.rb +71 -0
- data/lib/salemove/process_handler/cron_process_monitor.rb +18 -0
- data/lib/salemove/process_handler/notifier_factory.rb +21 -0
- data/lib/salemove/process_handler/pivot_process.rb +108 -0
- data/lib/salemove/process_handler/process_monitor.rb +68 -0
- data/lib/salemove/process_handler/version.rb +5 -0
- data/process_handler.gemspec +23 -0
- data/spec/fixtures/composite_service.rb +44 -0
- data/spec/fixtures/cron_service.rb +15 -0
- data/spec/process_handler/composite_process_spec.rb +16 -0
- data/spec/process_handler/cron_process_spec.rb +86 -0
- data/spec/process_handler/pivot_process_spec.rb +137 -0
- data/spec/spec_helper.rb +30 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2530d6f931ed0ff1a7f617c7e04fdbf4be28a4ee
|
4
|
+
data.tar.gz: fb6d3c7f5e79fb988df866084dfc0ea116ca4c6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1781ed0ee1cf6fa992a64c1a0cb4bf49a4ed002346b8b705df2de9013ce0c8390c16249af2ed4e5da07f01030bc75060760caeaff32d9b8d1657086dd84e2c4a
|
7
|
+
data.tar.gz: 40e9b2d747eadfe6f423ca37667c802e79a441eecb7bbc7ffe5c06abc6d2dbdeb01cc6bd2e60d6e2333fa5625d6d8536a47a1633e83ce1d0ab512b0151035b3a
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
process_handler
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
process_handler (0.1.0)
|
5
|
+
airbrake
|
6
|
+
sucker_punch (~> 1.1)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
airbrake (4.1.0)
|
12
|
+
builder
|
13
|
+
multi_json
|
14
|
+
builder (3.2.2)
|
15
|
+
celluloid (0.15.2)
|
16
|
+
timers (~> 1.1.0)
|
17
|
+
coderay (1.1.0)
|
18
|
+
diff-lcs (1.2.5)
|
19
|
+
inflecto (0.0.2)
|
20
|
+
logasm (0.1.0)
|
21
|
+
inflecto
|
22
|
+
logstash-event (~> 1.2)
|
23
|
+
logstash-event (1.2.02)
|
24
|
+
method_source (0.8.2)
|
25
|
+
multi_json (1.10.1)
|
26
|
+
pry (0.9.12.6)
|
27
|
+
coderay (~> 1.0)
|
28
|
+
method_source (~> 0.8)
|
29
|
+
slop (~> 3.4)
|
30
|
+
rake (10.3.2)
|
31
|
+
rspec (3.1.0)
|
32
|
+
rspec-core (~> 3.1.0)
|
33
|
+
rspec-expectations (~> 3.1.0)
|
34
|
+
rspec-mocks (~> 3.1.0)
|
35
|
+
rspec-core (3.1.5)
|
36
|
+
rspec-support (~> 3.1.0)
|
37
|
+
rspec-expectations (3.1.2)
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
39
|
+
rspec-support (~> 3.1.0)
|
40
|
+
rspec-mocks (3.1.2)
|
41
|
+
rspec-support (~> 3.1.0)
|
42
|
+
rspec-support (3.1.1)
|
43
|
+
rufus-scheduler (3.0.9)
|
44
|
+
tzinfo
|
45
|
+
slop (3.4.7)
|
46
|
+
sucker_punch (1.2.1)
|
47
|
+
celluloid (~> 0.15.2)
|
48
|
+
thread_safe (0.3.4)
|
49
|
+
timers (1.1.0)
|
50
|
+
tzinfo (1.2.2)
|
51
|
+
thread_safe (~> 0.1)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
logasm
|
58
|
+
process_handler!
|
59
|
+
pry
|
60
|
+
rake
|
61
|
+
rspec (~> 3.1)
|
62
|
+
rufus-scheduler
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# ProcessHandler
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/salemove/process_handler)
|
4
|
+
|
5
|
+
ProcessHandler helps to spawn and manage services. There are multiple types of processes. Every process knows how to handle `SIGINT` and `SIGTERM` signals.
|
6
|
+
|
7
|
+
## PivotProcess
|
8
|
+
This process is used for services that need one or more threads and use the request-response model [Freddy](https://github.com/salemove/freddy).
|
9
|
+
|
10
|
+
Example of using pivot process:
|
11
|
+
```ruby
|
12
|
+
service = MyService.new
|
13
|
+
freddy = Freddy.new
|
14
|
+
|
15
|
+
process = Salemove::ProcessHandler::PivotProcess.new(freddy)
|
16
|
+
process.spawn(service)
|
17
|
+
```
|
18
|
+
|
19
|
+
### Service
|
20
|
+
If you want to use pivot process, then the given service must implement `call` method that takes `input` as an argument.
|
21
|
+
|
22
|
+
Example of a service:
|
23
|
+
```ruby
|
24
|
+
class Echo
|
25
|
+
def call(input)
|
26
|
+
result = # do something with input
|
27
|
+
{success: true, output: result} # return result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## CronProcess
|
34
|
+
This process allows a service to run recurringly either at times specified by a [cron expression](http://en.wikipedia.org/wiki/Cron#CRON_expression) or at a fixed time interval: "1" for seconds, "2h" for hours and "2d" for days.
|
35
|
+
|
36
|
+
Example of using cron process with cron expressions:
|
37
|
+
```ruby
|
38
|
+
service = MyService.new
|
39
|
+
|
40
|
+
# every five minutes between 7:00 and 7:55 on Mon to Fri
|
41
|
+
process = Salemove::ProcessHandler::CronProcess.new('0/5 7 * * 1-5')
|
42
|
+
process.spawn(service)
|
43
|
+
```
|
44
|
+
|
45
|
+
Example of using cron process with interval expressions:
|
46
|
+
```ruby
|
47
|
+
service = MyService.new
|
48
|
+
|
49
|
+
# every second hour
|
50
|
+
process = Salemove::ProcessHandler::CronProcess.new('2h')
|
51
|
+
process.spawn(service)
|
52
|
+
```
|
53
|
+
|
54
|
+
## Service
|
55
|
+
If you want to use a cron process, then you only must implement `call` method that does not take any arguments.
|
56
|
+
|
57
|
+
Example of a service:
|
58
|
+
```ruby
|
59
|
+
class MemWatcher
|
60
|
+
def call
|
61
|
+
result = `sysctl -a | grep 'hw.usermem'`
|
62
|
+
|
63
|
+
# e.g write this result to a file
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
data/Rakefile
ADDED
data/config.reek
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative 'process_monitor'
|
2
|
+
|
3
|
+
module Salemove
|
4
|
+
module ProcessHandler
|
5
|
+
|
6
|
+
def self.start_composite(&block)
|
7
|
+
CompositeProcess.new(&block).start
|
8
|
+
end
|
9
|
+
|
10
|
+
class CompositeProcess
|
11
|
+
|
12
|
+
def initialize(&block)
|
13
|
+
@process_spawners = []
|
14
|
+
@monitor = CompositeProcessMonitor.new
|
15
|
+
instance_eval &block if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(process, service)
|
19
|
+
@monitor.add process.process_monitor
|
20
|
+
@process_spawners << Proc.new { process.spawn service, blocking: false }
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
@process_spawners.each(&:call)
|
25
|
+
@monitor.start
|
26
|
+
block
|
27
|
+
end
|
28
|
+
|
29
|
+
def block
|
30
|
+
sleep 1 while @monitor.running?
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class CompositeProcessMonitor < ProcessMonitor
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@monitors = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def add(monitor)
|
42
|
+
@monitors << monitor
|
43
|
+
end
|
44
|
+
|
45
|
+
def stop
|
46
|
+
@monitors.each(&:stop)
|
47
|
+
sleep 1 while @monitors.any?(&:alive?)
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rufus-scheduler'
|
2
|
+
require_relative 'cron_process_monitor'
|
3
|
+
require_relative 'notifier_factory'
|
4
|
+
|
5
|
+
module Salemove
|
6
|
+
module ProcessHandler
|
7
|
+
|
8
|
+
class CronScheduler < Rufus::Scheduler
|
9
|
+
|
10
|
+
def initialize(exception_notifier, options)
|
11
|
+
super options
|
12
|
+
@exception_notifier = exception_notifier
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_error(job, error)
|
16
|
+
if @exception_notifier
|
17
|
+
@exception_notifier.notify_or_ignore(error, cgi_data: ENV.to_hash)
|
18
|
+
end
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
class CronProcess
|
25
|
+
|
26
|
+
attr_reader :process_monitor
|
27
|
+
|
28
|
+
def initialize(env: 'development',
|
29
|
+
notifier: nil,
|
30
|
+
notifier_factory: NotifierFactory,
|
31
|
+
scheduler_options: {})
|
32
|
+
@schedules = []
|
33
|
+
@exception_notifier = notifier_factory.get_notifier(env, notifier)
|
34
|
+
@scheduler = CronScheduler.new @exception_notifier, scheduler_options
|
35
|
+
@process_monitor = CronProcessMonitor.new(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [String] expression
|
39
|
+
# can either be a any cron expression like every five minutes: '5 * * * *'
|
40
|
+
# or interval like '1' for seconds, '2h' for hours and '2d' for days
|
41
|
+
def schedule(expression, params={}, overlap=false)
|
42
|
+
@schedules << { expression: expression, params: params, overlap: overlap }
|
43
|
+
end
|
44
|
+
|
45
|
+
def spawn(service, blocking: true)
|
46
|
+
@process_monitor.start
|
47
|
+
@schedules.each do |schedule|
|
48
|
+
spawn_schedule(service, schedule)
|
49
|
+
end
|
50
|
+
@scheduler.join if blocking
|
51
|
+
end
|
52
|
+
|
53
|
+
def spawn_schedule(service, expression:, params:, overlap:)
|
54
|
+
if params.empty?
|
55
|
+
@scheduler.repeat expression, {overlap: overlap} { service.call }
|
56
|
+
else
|
57
|
+
@scheduler.repeat expression, {overlap: overlap} { service.call(params) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def stop
|
62
|
+
#Separate thread to avoid Ruby 2.0+ trap context 'synchronize' exception
|
63
|
+
Thread.new do
|
64
|
+
@scheduler.shutdown(:wait)
|
65
|
+
@process_monitor.shutdown
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'process_monitor'
|
2
|
+
|
3
|
+
module Salemove
|
4
|
+
module ProcessHandler
|
5
|
+
class CronProcessMonitor < ProcessMonitor
|
6
|
+
|
7
|
+
def initialize(process)
|
8
|
+
@process = process
|
9
|
+
end
|
10
|
+
|
11
|
+
def stop
|
12
|
+
super
|
13
|
+
@process.stop
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'airbrake'
|
2
|
+
|
3
|
+
module Salemove
|
4
|
+
module ProcessHandler
|
5
|
+
class NotifierFactory
|
6
|
+
|
7
|
+
def self.get_notifier(env, conf)
|
8
|
+
if conf && conf[:type] == 'airbrake'
|
9
|
+
Airbrake.configure do |airbrake|
|
10
|
+
airbrake.async = true
|
11
|
+
airbrake.environment_name = env
|
12
|
+
airbrake.host = conf.fetch(:host)
|
13
|
+
airbrake.api_key = conf.fetch(:api_key)
|
14
|
+
end
|
15
|
+
Airbrake
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'benchmark'
|
3
|
+
require_relative 'process_monitor'
|
4
|
+
require_relative 'notifier_factory'
|
5
|
+
|
6
|
+
module Salemove
|
7
|
+
module ProcessHandler
|
8
|
+
class PivotProcess
|
9
|
+
|
10
|
+
attr_reader :process_monitor, :exception_notifier
|
11
|
+
|
12
|
+
def self.logger
|
13
|
+
@logger ||= Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.logger=(logger)
|
17
|
+
@logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(messenger,
|
21
|
+
env: 'development',
|
22
|
+
notifier: nil,
|
23
|
+
notifier_factory: NotifierFactory,
|
24
|
+
process_monitor: ProcessMonitor.new)
|
25
|
+
@messenger = messenger
|
26
|
+
@process_monitor = process_monitor
|
27
|
+
@exception_notifier = notifier_factory.get_notifier(env, notifier)
|
28
|
+
end
|
29
|
+
|
30
|
+
def spawn(service, blocking: true)
|
31
|
+
@process_monitor.start
|
32
|
+
|
33
|
+
@service_thread = ServiceSpawner.spawn(service, @messenger, @exception_notifier)
|
34
|
+
blocking ? wait_for_monitor : Thread.new { wait_for_monitor }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def wait_for_monitor
|
40
|
+
sleep 1 while @process_monitor.running?
|
41
|
+
@service_thread.shutdown
|
42
|
+
@service_thread.join
|
43
|
+
@process_monitor.shutdown
|
44
|
+
end
|
45
|
+
|
46
|
+
class ServiceSpawner
|
47
|
+
def self.spawn(service, messenger, exception_notifier)
|
48
|
+
new(service, messenger, exception_notifier).spawn
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(service, messenger, exception_notifier)
|
52
|
+
@service = service
|
53
|
+
@messenger = messenger
|
54
|
+
@exception_notifier = exception_notifier
|
55
|
+
end
|
56
|
+
|
57
|
+
def spawn
|
58
|
+
@messenger.respond_to(@service.class::QUEUE) do |input, handler|
|
59
|
+
response = handle_request(input)
|
60
|
+
|
61
|
+
if response.is_a?(Hash) && (response[:success] == false || response[:error])
|
62
|
+
handler.error(response)
|
63
|
+
else
|
64
|
+
handler.success(response)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def handle_request(input)
|
70
|
+
if input.has_key?(:ping)
|
71
|
+
{ success: true, pong: 'pong' }
|
72
|
+
else
|
73
|
+
delegate_to_service(input)
|
74
|
+
end
|
75
|
+
rescue => exception
|
76
|
+
handle_exception(exception, input)
|
77
|
+
end
|
78
|
+
|
79
|
+
def delegate_to_service(input)
|
80
|
+
result = benchmark(input) { @service.call(input) }
|
81
|
+
PivotProcess.logger.info "Result: #{result.inspect}"
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_exception(e, input)
|
86
|
+
PivotProcess.logger.error(e.inspect + "\n" + e.backtrace.join("\n"))
|
87
|
+
if @exception_notifier
|
88
|
+
@exception_notifier.notify_or_ignore(e, cgi_data: ENV.to_hash, parameters: input)
|
89
|
+
end
|
90
|
+
{ success: false, error: e.message }
|
91
|
+
end
|
92
|
+
|
93
|
+
def benchmark(input, &block)
|
94
|
+
type = input[:type] if input.is_a?(Hash)
|
95
|
+
result = nil
|
96
|
+
|
97
|
+
bm = Benchmark.measure { result = block.call }
|
98
|
+
if defined?(Logasm) && PivotProcess.logger.is_a?(Logasm)
|
99
|
+
PivotProcess.logger.debug "Execution time",
|
100
|
+
type: type, real: bm.real, user: bm.utime, system: bm.stime
|
101
|
+
end
|
102
|
+
|
103
|
+
result
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Salemove
|
2
|
+
module ProcessHandler
|
3
|
+
class ProcessMonitor
|
4
|
+
def start
|
5
|
+
init_signal_handlers
|
6
|
+
@state = :running
|
7
|
+
end
|
8
|
+
|
9
|
+
def stop
|
10
|
+
@state = :stopping if alive?
|
11
|
+
end
|
12
|
+
|
13
|
+
def shutdown
|
14
|
+
@state = :stopped
|
15
|
+
end
|
16
|
+
|
17
|
+
def running?
|
18
|
+
@state == :running
|
19
|
+
end
|
20
|
+
|
21
|
+
def alive?
|
22
|
+
@state != :stopped
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def init_signal_handlers
|
28
|
+
init_hup_signal
|
29
|
+
init_quit_signal
|
30
|
+
init_int_signal
|
31
|
+
init_term_signal
|
32
|
+
end
|
33
|
+
|
34
|
+
# Many daemons will reload their configuration files and reopen their
|
35
|
+
# logfiles instead of exiting when receiving this signal.
|
36
|
+
def init_hup_signal
|
37
|
+
trap :HUP do
|
38
|
+
puts 'SIGHUP: not implemented'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Interrupts a process. (The default action is to terminate gracefully).
|
43
|
+
def init_int_signal
|
44
|
+
trap :INT do
|
45
|
+
puts 'Exiting process gracefully!'
|
46
|
+
stop
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Terminates a process immediately.
|
51
|
+
def init_term_signal
|
52
|
+
trap :TERM do
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Terminates a process. This is different from both SIGKILL and SIGTERM
|
58
|
+
# in the sense that it generates a core dump of the process and also
|
59
|
+
# cleans up resources held up by a process. Like SIGINT, this can also
|
60
|
+
# be sent from the terminal as input characters.
|
61
|
+
def init_quit_signal
|
62
|
+
trap :QUIT do
|
63
|
+
exit
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'salemove/process_handler/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'process_handler'
|
8
|
+
spec.version = Salemove::ProcessHandler::VERSION
|
9
|
+
spec.authors = ['Indrek Juhkam']
|
10
|
+
spec.email = ['indrek@salemove.com']
|
11
|
+
spec.description = %q{This gem helps to monitor and manage processes}
|
12
|
+
spec.summary = %q{}
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'Private'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'airbrake'
|
22
|
+
spec.add_dependency 'sucker_punch', '~> 1.1' # for async airbrake notifications
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'salemove/process_handler'
|
2
|
+
require 'salemove/process_handler/composite_process'
|
3
|
+
require 'salemove/process_handler/cron_process'
|
4
|
+
require 'salemove/process_handler/pivot_process'
|
5
|
+
|
6
|
+
module Salemove
|
7
|
+
|
8
|
+
class EchoResultService
|
9
|
+
QUEUE = 'Dummy'
|
10
|
+
|
11
|
+
def call(params={})
|
12
|
+
puts "RESULT"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Messenger
|
17
|
+
def respond_to(*)
|
18
|
+
ResponderHandler.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ResponderHandler
|
23
|
+
|
24
|
+
def shutdown
|
25
|
+
end
|
26
|
+
|
27
|
+
def join
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
cron_process = ProcessHandler::CronProcess.new
|
33
|
+
cron_process.schedule('0.5')
|
34
|
+
cron_process.schedule('5', some: 'params')
|
35
|
+
|
36
|
+
ProcessHandler::PivotProcess.logger = Logger.new('/dev/null')
|
37
|
+
pivot_process = ProcessHandler::PivotProcess.new(Messenger.new)
|
38
|
+
|
39
|
+
ProcessHandler.start_composite do
|
40
|
+
add cron_process, EchoResultService.new
|
41
|
+
add pivot_process, EchoResultService.new
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'salemove/process_handler/cron_process'
|
2
|
+
|
3
|
+
module Salemove
|
4
|
+
|
5
|
+
class EchoResultService
|
6
|
+
def call
|
7
|
+
puts "RESULT"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
cron_process = ProcessHandler::CronProcess.new
|
12
|
+
cron_process.schedule('0.5')
|
13
|
+
cron_process.spawn(EchoResultService.new, blocking: true)
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'salemove/process_handler/composite_process'
|
3
|
+
|
4
|
+
describe ProcessHandler::CompositeProcess do
|
5
|
+
|
6
|
+
it 'can be gracefully stopped' do
|
7
|
+
result = run_and_signal_fixture(fixture: 'composite_service.rb', signal: 'INT', sleep_period: 1)
|
8
|
+
expect(result).to eq("RESULT\nExiting process gracefully!\n")
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'can be terminated' do
|
12
|
+
result = run_and_signal_fixture(fixture: 'composite_service.rb', signal: 'TERM', sleep_period: 1)
|
13
|
+
expect(result).to eq("RESULT\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'salemove/process_handler/cron_process'
|
3
|
+
|
4
|
+
describe ProcessHandler::CronProcess do
|
5
|
+
|
6
|
+
class AppenderService
|
7
|
+
|
8
|
+
def initialize(messages, queue)
|
9
|
+
@messages = messages
|
10
|
+
@queue = queue
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(name)
|
14
|
+
@messages << name
|
15
|
+
@queue << nil # unblock main thread
|
16
|
+
sleep 0.4 # simulate expense
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class ExceptionService
|
22
|
+
|
23
|
+
def initialize(queue)
|
24
|
+
@queue = queue
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
raise "A Runtimino Exceptino"
|
29
|
+
ensure
|
30
|
+
@queue << nil # unblock main thread
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'can be gracefully stopped' do
|
35
|
+
result = run_and_signal_fixture(fixture: 'cron_service.rb', signal: 'INT', sleep_period: 1)
|
36
|
+
expect(result).to eq("RESULT\nExiting process gracefully!\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'can be terminated' do
|
40
|
+
result = run_and_signal_fixture(fixture: 'cron_service.rb', signal: 'TERM', sleep_period: 1)
|
41
|
+
expect(result).to eq("RESULT\n")
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'scheduler' do
|
45
|
+
let(:process) { ProcessHandler::CronProcess.new(scheduler_options: {frequency: 0.1}) }
|
46
|
+
let(:messages) { [] }
|
47
|
+
let(:queue) { Queue.new }
|
48
|
+
|
49
|
+
it 'does not trigger 2 jobs at once' do
|
50
|
+
process.schedule('0.1', 'first')
|
51
|
+
process.schedule('0.4', 'second')
|
52
|
+
Thread.new do
|
53
|
+
process.spawn(AppenderService.new(messages, queue))
|
54
|
+
end
|
55
|
+
# block main thread until 3 schedules have run
|
56
|
+
(1..3).map { queue.pop }
|
57
|
+
expect(messages).to eq(['first', 'second', 'first'])
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'exception handler' do
|
63
|
+
let(:process) { ProcessHandler::CronProcess.new(params) }
|
64
|
+
let(:params) {{ env: 'test', notifier_factory: notifier_factory,
|
65
|
+
scheduler_options: {frequency: 0.1} }}
|
66
|
+
let(:notifier_factory) { double('NotifierFactory') }
|
67
|
+
let(:exception_notifier) { double('Airbrake') }
|
68
|
+
let(:queue) { Queue.new }
|
69
|
+
|
70
|
+
before(:each) do
|
71
|
+
allow(notifier_factory).to receive(:get_notifier) { exception_notifier }
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'notifies of exception' do
|
75
|
+
process.schedule('0.2')
|
76
|
+
expect(exception_notifier).to receive(:notify_or_ignore)
|
77
|
+
Thread.new do
|
78
|
+
process.spawn(ExceptionService.new(queue))
|
79
|
+
end
|
80
|
+
queue.pop
|
81
|
+
sleep 0.1
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'logasm'
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'salemove/process_handler/pivot_process'
|
4
|
+
|
5
|
+
class ResultService
|
6
|
+
QUEUE = 'Dummy'
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ProcessHandler::PivotProcess do
|
10
|
+
let(:monitor) { double('Monitor') }
|
11
|
+
let(:messenger) { double('Messenger') }
|
12
|
+
let(:handler) { double('Handler') }
|
13
|
+
let(:thread) { double('Thread') }
|
14
|
+
|
15
|
+
subject { process.spawn(service) }
|
16
|
+
let(:service) { ResultService.new }
|
17
|
+
|
18
|
+
let(:process) { ProcessHandler::PivotProcess.new(messenger, process_params) }
|
19
|
+
let(:process_params) {{ process_monitor: monitor , notifier_factory: notifier_factory, env: 'test' }}
|
20
|
+
let(:notifier_factory) { double('NotifierFactory') }
|
21
|
+
let(:responder) { double(shutdown: true, join: true) }
|
22
|
+
|
23
|
+
let(:input) {{}}
|
24
|
+
let(:result) { {success: true, result: 'RESULT'} }
|
25
|
+
|
26
|
+
let(:logger) { Logasm.new([]) }
|
27
|
+
|
28
|
+
def expect_monitor_to_behave
|
29
|
+
expect(monitor).to receive(:start)
|
30
|
+
expect(monitor).to receive(:running?) { false }
|
31
|
+
expect(monitor).to receive(:shutdown)
|
32
|
+
end
|
33
|
+
|
34
|
+
def expect_message
|
35
|
+
expect(messenger).to receive(:respond_to) {|destination, &callback|
|
36
|
+
callback.call(input, handler)
|
37
|
+
}.and_return(responder)
|
38
|
+
end
|
39
|
+
|
40
|
+
def expect_handler_thread_to_behave
|
41
|
+
allow(handler).to receive(:success) { thread }
|
42
|
+
allow(handler).to receive(:error) { thread }
|
43
|
+
expect(responder).to receive(:shutdown)
|
44
|
+
expect(responder).to receive(:join)
|
45
|
+
end
|
46
|
+
|
47
|
+
before do
|
48
|
+
ProcessHandler::PivotProcess.logger = logger
|
49
|
+
allow(notifier_factory).to receive(:get_notifier) { nil }
|
50
|
+
expect_monitor_to_behave
|
51
|
+
expect_message
|
52
|
+
expect_handler_thread_to_behave
|
53
|
+
allow(service).to receive(:call).with(input) { result }
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'when service responds correctly' do
|
57
|
+
|
58
|
+
it 'can be executed with logger' do
|
59
|
+
expect(handler).to receive(:success).with(result)
|
60
|
+
expect(service).to receive(:call).with(input)
|
61
|
+
subject()
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'when service responds with an error' do
|
67
|
+
let(:result) { { success: false, error: 'hey' } }
|
68
|
+
|
69
|
+
before do
|
70
|
+
expect(service).to receive(:call).with(input) { result }
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'acks the message properly' do
|
74
|
+
expect(handler).to receive(:error).with(result)
|
75
|
+
subject()
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
shared_examples 'an error_handler' do
|
80
|
+
|
81
|
+
it 'logs error' do
|
82
|
+
expect(logger).to receive(:error)
|
83
|
+
subject()
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'with exception_notifier' do
|
87
|
+
|
88
|
+
let(:exception_notifier) { double('Airbrake') }
|
89
|
+
|
90
|
+
before do
|
91
|
+
allow(notifier_factory).to receive(:get_notifier) { exception_notifier }
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'triggers exception_notifier' do
|
95
|
+
expect(exception_notifier).to receive(:notify_or_ignore)
|
96
|
+
subject()
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
describe 'when service raises exception' do
|
103
|
+
|
104
|
+
let(:result) { { success: false, error: exception } }
|
105
|
+
let(:exception) { "what an unexpected exception!" }
|
106
|
+
|
107
|
+
before do
|
108
|
+
expect(service).to receive(:call).with(input) { raise exception }
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'acks the message properly' do
|
112
|
+
expect(handler).to receive(:error).with(result)
|
113
|
+
subject()
|
114
|
+
end
|
115
|
+
|
116
|
+
it_behaves_like 'an error_handler'
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
describe 'when exception raises after service call' do
|
121
|
+
|
122
|
+
let(:result) { { success: false, output: exception } }
|
123
|
+
let(:exception) { "no no no ... no inspect for you!" }
|
124
|
+
|
125
|
+
before do
|
126
|
+
expect(result).to receive(:inspect) { raise exception }
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'still acks the message properly' do
|
130
|
+
subject()
|
131
|
+
end
|
132
|
+
|
133
|
+
it_behaves_like 'an error_handler'
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require 'pry' # for debugging
|
6
|
+
require 'rspec'
|
7
|
+
require 'salemove/process_handler'
|
8
|
+
|
9
|
+
include Salemove
|
10
|
+
|
11
|
+
RSpec.configure do |rspec_config|
|
12
|
+
rspec_config.filter_run focus: true
|
13
|
+
rspec_config.run_all_when_everything_filtered = true
|
14
|
+
|
15
|
+
def fixture_path(name)
|
16
|
+
File.join(File.dirname(__FILE__), "fixtures", name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_and_signal_fixture(fixture:, signal:, sleep_period:)
|
20
|
+
output_read, output_write = IO.pipe
|
21
|
+
|
22
|
+
pid = Process.spawn('ruby ' + fixture_path(fixture), out: output_write)
|
23
|
+
sleep sleep_period
|
24
|
+
Process.kill(signal, pid)
|
25
|
+
Process.wait2(pid)
|
26
|
+
output_write.close
|
27
|
+
|
28
|
+
output_read.read
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: process_handler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Indrek Juhkam
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: airbrake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sucker_punch
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
description: This gem helps to monitor and manage processes
|
42
|
+
email:
|
43
|
+
- indrek@salemove.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".ruby-gemset"
|
49
|
+
- ".ruby-version"
|
50
|
+
- Gemfile
|
51
|
+
- Gemfile.lock
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- config.reek
|
55
|
+
- lib/salemove/process_handler.rb
|
56
|
+
- lib/salemove/process_handler/composite_process.rb
|
57
|
+
- lib/salemove/process_handler/cron_process.rb
|
58
|
+
- lib/salemove/process_handler/cron_process_monitor.rb
|
59
|
+
- lib/salemove/process_handler/notifier_factory.rb
|
60
|
+
- lib/salemove/process_handler/pivot_process.rb
|
61
|
+
- lib/salemove/process_handler/process_monitor.rb
|
62
|
+
- lib/salemove/process_handler/version.rb
|
63
|
+
- process_handler.gemspec
|
64
|
+
- spec/fixtures/composite_service.rb
|
65
|
+
- spec/fixtures/cron_service.rb
|
66
|
+
- spec/process_handler/composite_process_spec.rb
|
67
|
+
- spec/process_handler/cron_process_spec.rb
|
68
|
+
- spec/process_handler/pivot_process_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: ''
|
71
|
+
licenses:
|
72
|
+
- Private
|
73
|
+
metadata: {}
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 2.2.2
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: ''
|
94
|
+
test_files:
|
95
|
+
- spec/fixtures/composite_service.rb
|
96
|
+
- spec/fixtures/cron_service.rb
|
97
|
+
- spec/process_handler/composite_process_spec.rb
|
98
|
+
- spec/process_handler/cron_process_spec.rb
|
99
|
+
- spec/process_handler/pivot_process_spec.rb
|
100
|
+
- spec/spec_helper.rb
|