kamisama 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 482b1702ced157fcca3c04db289c376857183807
4
+ data.tar.gz: 1f059faa1c65b3741ebd176f60e93658cfb7d0aa
5
+ SHA512:
6
+ metadata.gz: 036ead213535c5b0ccf332c88671cb3354431e21deeff00b6498a0eecde8b513d3db9e83d8753f66209bebe83ebff278f68ed4c53ed32f717ef6969608bc6f29
7
+ data.tar.gz: b316668f54aeee9358ff361c4851ab0d50de821f16147f5c5e30e9ef48309fe7a56a6eb8b5fc47c233a7083eae272fd0ea2f99b5063555f05a4e7f1fe4ab956a
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ vendor/bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,13 @@
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/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kamisama.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
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.
data/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # Kamisama
2
+
3
+ [![Build Status](https://semaphoreci.com/api/v1/renderedtext/kamisama/branches/master/badge.svg)](https://semaphoreci.com/renderedtext/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).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
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
data/bin/setup ADDED
@@ -0,0 +1,7 @@
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
data/kamisama.gemspec ADDED
@@ -0,0 +1,28 @@
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
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "ffi", "~> 1.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "sys-proctable"
28
+ end
data/lib/kamisama.rb ADDED
@@ -0,0 +1,94 @@
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
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,3 @@
1
+ class Kamisama
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kamisama
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Igor Šarčević
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sys-proctable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Start, monitor, and observe background worker processes, from Ruby.
84
+ email:
85
+ - igor@renderedtext.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - CODE_OF_CONDUCT.md
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - bin/console
98
+ - bin/setup
99
+ - kamisama.gemspec
100
+ - lib/kamisama.rb
101
+ - lib/kamisama/process_ctrl.rb
102
+ - lib/kamisama/respawn_limiter.rb
103
+ - lib/kamisama/task.rb
104
+ - lib/kamisama/version.rb
105
+ homepage: https://github.com/shiroyasha/kamisama
106
+ licenses:
107
+ - MIT
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.4.8
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Start, monitor, and observe background worker processes, from Ruby.
129
+ test_files: []