├── .gitignore ├── .rspec ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── kamisama.gemspec ├── lib ├── kamisama.rb └── kamisama │ ├── process_ctrl.rb │ ├── respawn_limiter.rb │ ├── task.rb │ └── version.rb └── spec ├── kamisama_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | vendor/bundle 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in kamisama.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Igor Šarčević 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kamisama 2 | 3 | [![Build Status](https://semaphoreci.com/api/v1/shiroyasha/kamisama/branches/master/badge.svg)](https://semaphoreci.com/shiroyasha/kamisama) 4 | 5 | Start, monitor, and observe background worker processes, from Ruby. 6 | 7 | Based on [Unicorn](), [God](), and [Sidekiq](). 8 | 9 | # Usage 10 | 11 | Kamisama is useful for starting multiple background workers. For example, let's 12 | say that you have a background worker that crunches some data with periodic 13 | intervals. 14 | 15 | ``` ruby 16 | def worker 17 | loop do 18 | puts "Crunching data..." 19 | 20 | sleep 60 21 | end 22 | end 23 | ``` 24 | 25 | A usual way to run this task is to wrap it in a Rake task, and an upstart script 26 | to keep it running forever. This is pretty well until you have one process that 27 | you want to execute. However, if you want to run multiple processes, you need to 28 | introduce and manage multiple upstart configurations. One upstart script that 29 | acts like the master who manages your workers, and upstart scripts that describe 30 | your workers. 31 | 32 | This setup is cumbersome, hard to test, and managing different configurations 33 | for different environments (production, staging, development) can be outright 34 | frustrating. 35 | 36 | Kamisama is here to help, by abstracting away the issue of running and monitor 37 | multiple background workers. 38 | 39 | Let's run 17 instances of the above worker with Kamisama: 40 | 41 | ``` ruby 42 | def worker(worker_index) 43 | loop do 44 | puts "WORKER #{worker_index}: Crunching data..." 45 | 46 | sleep 60 47 | end 48 | end 49 | 50 | Kamisama.run(:instances => 17) { |index| worker(index) } 51 | ``` 52 | 53 | That's all! The above will start(fork) 17 processes on your machine, and restart 54 | them in case of failure. 55 | 56 | Keep in mind that you will still need to wrap Kamisama itself in a rake task 57 | and an Upstart script. 58 | 59 | ### Respawn limits 60 | 61 | Respawning workers is desirable in most cases, but we would still like to avoid 62 | rapid restarts of your workers in a short amount of time. Such rapid restarts 63 | can harm your system, and usually indicate that a serious issue is killing 64 | your workers. 65 | 66 | If the job is respawned more than `respawn_limit` times in `respawn_interval` 67 | seconds, Kamisama will considered this to be a deeper problem and will die. 68 | 69 | ``` ruby 70 | def worker(worker_index) 71 | loop do 72 | puts "WORKER #{worker_index}: Crunching data..." 73 | 74 | sleep 60 75 | end 76 | end 77 | 78 | config = { 79 | :instances => 17, 80 | :respawn_limit => 10, 81 | :respawn_interval => 60 82 | } 83 | 84 | Kamisama.run(config) { |index| worker(index) } 85 | ``` 86 | 87 | ## Signal control 88 | 89 | You can control your Kamisama process by sending kill signals to the running 90 | process. 91 | 92 | - [TERM](#term-signal) - terminates master process and all workers 93 | - [KILL](#kill-signal) - terminates master process and all workers 94 | - [TTIN](#ttin-signal) - spawns a new worker 95 | - [TTIN](#ttou-signal) - terminates a running worker 96 | 97 | #### TERM signal 98 | 99 | If you send a term signal to your Kamisama process, it will immediately 100 | shutdown. Following this, every children will be notified by the kernel that the 101 | master process has died with the TERM signal. 102 | 103 | For example, if you have the following processes: 104 | 105 | ``` bash 106 | 2000 - PID of master process 107 | 2001 - PID of first worker 108 | 2002 - PID of second worker 109 | 2003 - PID of third worker 110 | ``` 111 | 112 | Then when you send a "TERM" signal: 113 | 114 | ``` bash 115 | kill -TERM 2000 116 | ``` 117 | 118 | The master process `2000` will die immediately, and the workers processes 119 | (2001, 2002, 2003) will receive the `TERM` signal. 120 | 121 | #### KILL signal 122 | 123 | If you send a kill signal to your Kamisama process, it will immediately 124 | shutdown. Following this, every children will be notified by the kernel that the 125 | master process has dies with the TERM signal. 126 | 127 | For example, if you have the following processes: 128 | 129 | ``` bash 130 | 2000 - PID of master process 131 | 2001 - PID of first worker 132 | 2002 - PID of second worker 133 | 2003 - PID of third worker 134 | ``` 135 | 136 | Then when you send a "KILL" signal: 137 | 138 | ``` bash 139 | kill -9 2000 140 | ``` 141 | 142 | The master process `2000` will die immediately, and the workers processes 143 | (2001, 2002, 2003) will receive the `TERM` signal. 144 | 145 | #### TTIN signal 146 | 147 | If you send a ttin signal to your Kamisama process, it will spawn a new process. 148 | 149 | For example, if you have the following processes: 150 | 151 | ``` bash 152 | 2000 - PID of master process 153 | 2001 - PID of first worker 154 | 2002 - PID of second worker 155 | 2003 - PID of third worker 156 | ``` 157 | 158 | Then when you send a "TTIN" signal: 159 | 160 | ``` bash 161 | kill -TTIN 2000 162 | ``` 163 | 164 | The master process `2000` will spawn a new worker process. 165 | 166 | #### TTOU signal 167 | 168 | If you send a ttou signal to your Kamisama process, it will kill the oldest 169 | worker. 170 | 171 | For example, if you have the following processes: 172 | 173 | ``` bash 174 | 2000 - PID of master process 175 | 2001 - PID of first worker 176 | 2002 - PID of second worker 177 | 2003 - PID of third worker 178 | ``` 179 | 180 | Then when you send a "TTOU" signal: 181 | 182 | ``` bash 183 | kill -TTOU 2000 184 | ``` 185 | 186 | The master process `2000` will send a `TERM` signal to the process `2001`. 187 | 188 | *NOTE*: This will only work if you have more than one running processes. 189 | 190 | ## License 191 | 192 | The gem is available as open source under the terms of the 193 | [MIT License](http://opensource.org/licenses/MIT). 194 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "kamisama" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /kamisama.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'kamisama/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "kamisama" 8 | spec.version = Kamisama::VERSION 9 | spec.authors = ["Igor Šarčević"] 10 | spec.email = ["igor@renderedtext.com"] 11 | 12 | spec.summary = %q{Start, monitor, and observe background worker processes, from Ruby.} 13 | spec.description = %q{Start, monitor, and observe background worker processes, from Ruby.} 14 | spec.homepage = "https://github.com/shiroyasha/kamisama" 15 | spec.license = "MIT" 16 | 17 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 18 | # delete this section to allow pushing this gem to any host. 19 | if spec.respond_to?(:metadata) 20 | spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" 21 | else 22 | raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 23 | end 24 | 25 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "ffi", "~> 1.0" 31 | 32 | spec.add_development_dependency "bundler", "~> 1.10" 33 | spec.add_development_dependency "rake", "~> 10.0" 34 | spec.add_development_dependency "rspec" 35 | spec.add_development_dependency "sys-proctable" 36 | end 37 | -------------------------------------------------------------------------------- /lib/kamisama.rb: -------------------------------------------------------------------------------- 1 | class Kamisama 2 | require "kamisama/version" 3 | require "kamisama/process_ctrl" 4 | require "kamisama/task" 5 | require "kamisama/respawn_limiter" 6 | 7 | def self.run(options = {}, &block) 8 | new(options, &block).run 9 | end 10 | 11 | def initialize(options, &block) 12 | @block = block 13 | @instances = options.fetch(:instances) 14 | @respawn_limit = options.fetch(:respawn_limit, 3) 15 | @respawn_interval = options.fetch(:respawn_interval, 60) 16 | @monitor_sleep = 2 17 | 18 | @term_signal_received = false 19 | 20 | @respawn_limiter = Kamisama::RespawnLimiter.new(@respawn_limit, @respawn_interval) 21 | 22 | @tasks = [] 23 | end 24 | 25 | def run 26 | puts "[Kamisama Master] Process id: #{Process.pid}" 27 | puts "[Kamisama Master] Starting #{@instances} workers. \n" 28 | 29 | @instances.times { add_worker } 30 | 31 | handle_signals 32 | 33 | monitor 34 | end 35 | 36 | def handle_signals 37 | trap("TTIN") do 38 | @instances += 1 39 | end 40 | 41 | trap("TTOU") do 42 | # make sure that we always have at least one running worker 43 | if @instances > 1 44 | @instances -= 1 45 | end 46 | end 47 | 48 | trap("TERM") do 49 | @term_signal_received = true 50 | end 51 | end 52 | 53 | def add_worker 54 | puts "[Kamisama Master] #{Process.pid} Spawning new instance." 55 | 56 | @worker_index ||= 0 57 | @worker_index += 1 58 | 59 | task = Kamisama::Task.new(@worker_index, @block) 60 | task.start 61 | 62 | @tasks << task 63 | end 64 | 65 | def term_worker 66 | puts "[Kamisama Master] #{Process.pid} Terminating an instance." 67 | 68 | task = @tasks.shift 69 | task.terminate! 70 | end 71 | 72 | def monitor 73 | loop do 74 | break if @term_signal_received 75 | 76 | add_worker while @tasks.count < @instances 77 | term_worker while @tasks.count > @instances 78 | 79 | dead_tasks = @tasks.reject(&:alive?) 80 | 81 | dead_tasks.each do |task| 82 | @respawn_limiter.record! 83 | task.restart! 84 | end 85 | 86 | sleep(@monitor_sleep) 87 | end 88 | 89 | puts "[Kamisama Master] #{Process.pid} Terminating all instances" 90 | @tasks.each(&:terminate!) 91 | exit 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /lib/kamisama/process_ctrl.rb: -------------------------------------------------------------------------------- 1 | require "ffi" 2 | 3 | class Kamisama 4 | class ProcessCtrl 5 | SIGINT = 2 6 | SIGTERM = 15 7 | 8 | module LibC 9 | PR_SET_NAME = 15 10 | PR_SET_PDEATHSIG = 1 11 | 12 | extend FFI::Library 13 | ffi_lib "c" 14 | attach_function :prctl, [:int, :long, :long, :long, :long], :int 15 | end 16 | 17 | def self.set_process_name(process_name) 18 | # The process name is max 16 characters, so get the first 16, and if it is 19 | # less pad with spaces to avoid formatting wierdness 20 | process_name = "%-16.16s" % name 21 | 22 | LibC.prctl(LibC::PR_SET_NAME, process_name, 0, 0, 0) 23 | end 24 | 25 | def self.set_parent_death_signal(signal) 26 | case signal 27 | when :sigint 28 | LibC.prctl(LibC::PR_SET_PDEATHSIG, SIGINT, 0, 0, 0) 29 | when :sigterm 30 | LibC.prctl(LibC::PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0) 31 | else 32 | raise "Unrecognized signal '#{signal.inspect}'" 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/kamisama/respawn_limiter.rb: -------------------------------------------------------------------------------- 1 | class Kamisama::RespawnLimiter 2 | 3 | def initialize(respawn_limit, respawn_interval) 4 | @respawn_limit = respawn_limit 5 | @respawn_interval = respawn_interval 6 | 7 | @respawns = [] 8 | end 9 | 10 | def record! 11 | now = Time.now.to_i 12 | 13 | @respawns = @respawns.select { |timestamp| timestamp >= now - @respawn_interval } + [now] 14 | 15 | die_if_breached! 16 | end 17 | 18 | def calculate_respawn_count 19 | now = Time.now.to_i 20 | 21 | @respawns.count { |timestamp| timestamp > (now - @respawn_interval) } 22 | end 23 | 24 | def die_if_breached! 25 | respawn_count = calculate_respawn_count 26 | 27 | if respawn_count >= @respawn_limit 28 | puts "[Kamisama Master] Respawn count #{respawn_count} hit the limit of #{@respawn_limit} for the respawn interval of #{@respawn_interval} seconds." 29 | puts "[Kamisama Master] Terminating." 30 | 31 | exit(1) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/kamisama/task.rb: -------------------------------------------------------------------------------- 1 | class Kamisama 2 | class Task 3 | def initialize(task_index, block) 4 | @task_index = task_index 5 | @block = block 6 | end 7 | 8 | def start 9 | @pid = Process.fork do 10 | begin 11 | # receive sigterm when parent dies 12 | Kamisama::ProcessCtrl.set_parent_death_signal(:sigterm) 13 | 14 | log("Worker started. Hello!") 15 | 16 | @block.call(@task_index) 17 | rescue Exception => e 18 | # handle all exceptions, even system ones 19 | log("Shutting down... #{e.message}") 20 | exit 21 | ensure 22 | exit 23 | end 24 | end 25 | 26 | Process.detach(@pid) 27 | end 28 | 29 | def restart! 30 | puts "[Kamisama Master] Restarting Worker." 31 | @pid = nil 32 | start 33 | end 34 | 35 | def terminate! 36 | Process.kill("TERM", @pid) 37 | end 38 | 39 | def alive? 40 | Process.getpgid(@pid) 41 | true 42 | rescue Errno::ESRCH 43 | false 44 | end 45 | 46 | def log(message) 47 | puts "[WORKER #{@task_index}] #{message}" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/kamisama/version.rb: -------------------------------------------------------------------------------- 1 | class Kamisama 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/kamisama_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Kamisama do 4 | it "has a version number" do 5 | expect(Kamisama::VERSION).not_to be nil 6 | end 7 | 8 | describe "starting workers" do 9 | before do 10 | @pid = TestApp.start(:instances => 3) 11 | end 12 | 13 | it "starts multiple workers" do 14 | expect(SpecHelpers.child_count(@pid)).to eq(3) 15 | end 16 | 17 | after do 18 | TestApp.stop(@pid, :signal => "KILL") 19 | expect(SpecHelpers.child_count(@pid)).to eq(0) 20 | end 21 | end 22 | 23 | describe "restarting failed workers" do 24 | before do 25 | @pid = TestApp.start(:instances => 3) 26 | expect(SpecHelpers.child_count(@pid)).to eq(3) 27 | end 28 | 29 | it "restart failed worker" do 30 | children = SpecHelpers.child_pids(@pid) 31 | 32 | puts "Killing #{children.first}" 33 | Process.kill("TERM", children.first) 34 | 35 | # make sure that we actually killed a child 36 | sleep 1 37 | expect(SpecHelpers.child_count(@pid)).to eq(2) 38 | 39 | # wait for worker to respawn 40 | sleep 4 41 | expect(SpecHelpers.child_count(@pid)).to eq(3) 42 | end 43 | 44 | after do 45 | TestApp.stop(@pid, :signal => "KILL") 46 | expect(SpecHelpers.child_count(@pid)).to eq(0) 47 | end 48 | end 49 | 50 | describe "respawning" do 51 | before do 52 | @pid = TestApp.start(:instances => 3, :respawn_limit => 2, :respawn_interval => 10) 53 | expect(SpecHelpers.child_count(@pid)).to eq(3) 54 | sleep 3 55 | end 56 | 57 | it "obbeys the respawn count and respawn interval parameters" do 58 | Process.kill("TERM", SpecHelpers.child_pids(@pid).first) 59 | sleep 3 60 | Process.kill("TERM", SpecHelpers.child_pids(@pid).first) 61 | sleep 3 62 | 63 | expect(SpecHelpers.process_alive?(@pid)).to eq(false) 64 | end 65 | 66 | after do 67 | TestApp.stop(@pid, :signal => "KILL") if SpecHelpers.process_alive?(@pid) 68 | expect(SpecHelpers.child_count(@pid)).to eq(0) 69 | end 70 | end 71 | 72 | describe "increasing worker count with TTIN signal" do 73 | before do 74 | @pid = TestApp.start(:instances => 3) 75 | expect(SpecHelpers.child_count(@pid)).to eq(3) 76 | end 77 | 78 | it "adds a new worker" do 79 | 3.times do 80 | Process.kill("TTIN", @pid) 81 | sleep 1 82 | end 83 | 84 | sleep 2 85 | 86 | expect(SpecHelpers.child_count(@pid)).to eq(6) 87 | end 88 | 89 | after do 90 | TestApp.stop(@pid, :signal => "KILL") 91 | expect(SpecHelpers.child_count(@pid)).to eq(0) 92 | end 93 | end 94 | 95 | describe "decrease worker count with TTOU signal" do 96 | before do 97 | @pid = TestApp.start(:instances => 3) 98 | expect(SpecHelpers.child_count(@pid)).to eq(3) 99 | end 100 | 101 | it "removes running workers" do 102 | 2.times do 103 | Process.kill("TTOU", @pid) 104 | sleep 1 105 | end 106 | 107 | sleep 2 108 | 109 | expect(SpecHelpers.child_count(@pid)).to eq(1) 110 | end 111 | 112 | after do 113 | TestApp.stop(@pid, :signal => "KILL") 114 | expect(SpecHelpers.child_count(@pid)).to eq(0) 115 | end 116 | end 117 | 118 | describe "shutdown" do 119 | before do 120 | @pid = TestApp.start(:instances => 3) 121 | expect(SpecHelpers.child_count(@pid)).to eq(3) 122 | end 123 | 124 | it "removes running workers" do 125 | 2.times do 126 | Process.kill("TERM", @pid) 127 | sleep 1 128 | end 129 | 130 | sleep 2 131 | 132 | expect(SpecHelpers.child_count(@pid)).to eq(0) 133 | end 134 | 135 | after do 136 | TestApp.stop(@pid, :signal => "KILL") if SpecHelpers.process_alive?(@pid) 137 | expect(SpecHelpers.child_count(@pid)).to eq(0) 138 | end 139 | end 140 | 141 | end 142 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'kamisama' 3 | require "sys/proctable" 4 | 5 | module SpecHelpers 6 | module_function 7 | 8 | def child_count(pid) 9 | Sys::ProcTable.ps.select { |process| process.ppid == pid }.count 10 | end 11 | 12 | def child_pids(pid) 13 | Sys::ProcTable.ps.select { |process| process.ppid == pid }.map(&:pid) 14 | end 15 | 16 | def process_alive?(pid) 17 | Process.getpgid(pid) 18 | true 19 | rescue Errno::ESRCH 20 | false 21 | end 22 | end 23 | 24 | class TestApp 25 | 26 | def self.start(options) 27 | pid = Process.fork do 28 | Kamisama.run(options) do |index| 29 | worker(index) 30 | end 31 | exit 32 | end 33 | 34 | Process.detach(pid) 35 | 36 | # wait for children to spawn 37 | sleep 2 38 | pid 39 | end 40 | 41 | def self.worker(index) 42 | while true 43 | # puts "worker #{index} ... crunching data ..." 44 | sleep 2 45 | end 46 | end 47 | 48 | def self.stop(pid, options = {}) 49 | signal = options.fetch(:signal) 50 | 51 | puts "Stopping TestAPP with #{signal}" 52 | Process.kill(signal, pid) 53 | 54 | sleep 1 55 | end 56 | 57 | end 58 | --------------------------------------------------------------------------------