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