process_handler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1 @@
1
+ process_handler
@@ -0,0 +1 @@
1
+ 2.1
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'rufus-scheduler'
5
+ gem 'logasm'
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 3.1'
9
+ gem 'pry'
10
+ end
11
+
12
+ # Specify your gem's dependencies in process_handler.gemspec
13
+ gemspec
@@ -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
@@ -0,0 +1,66 @@
1
+ # ProcessHandler
2
+
3
+ [![Code Climate](https://codeclimate.com/github/salemove/process_handler/badges/gpa.svg)](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
+ ```
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task ci: :spec
7
+ task default: :spec
@@ -0,0 +1,3 @@
1
+ ---
2
+ IrresponsibleModule:
3
+ enabled: false
@@ -0,0 +1,6 @@
1
+ module Salemove
2
+ module ProcessHandler
3
+ end
4
+ end
5
+
6
+ require_relative 'process_handler/version'
@@ -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,5 @@
1
+ module Salemove
2
+ module ProcessHandler
3
+ VERSION = '0.1.0'
4
+ end
5
+ 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
@@ -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