forkworker 0.0.1

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
+ SHA256:
3
+ metadata.gz: cbe42b40a14925ceef50b2ca98e2428eedf4385fbdbe24b83e8d7036ef478c6b
4
+ data.tar.gz: a9a150452ff9f4d2df1c7f7f31f5bc3d4247aec0977c1774ae897ed306021e05
5
+ SHA512:
6
+ metadata.gz: 3324c1e818b7d9fd7111cbed80e0ffa94712b0125581bb29d445561acb4a1f433ae0bf383c6290b79da6d9af196bfa0cd764b4ea434a1a2ed8a44e9075de4e57
7
+ data.tar.gz: 9e0e70f96f16b3f8b41953e372be5ad36509ba2cf877cb0dcf30eb8294b6eb9fc964d6682c55a532ad6cb6b1ac0d9d370829a7760e07e799b0156c3ed33ffedd
@@ -0,0 +1,7 @@
1
+ require "forkworker/logger"
2
+ require "forkworker/leader"
3
+ require "forkworker/worker"
4
+
5
+ module Forkworker
6
+ class NoMoreWork < StandardError; end
7
+ end
@@ -0,0 +1,162 @@
1
+ module Forkworker
2
+ class Leader
3
+ include Forkworker::Logger
4
+
5
+ def initialize(worker_num, pidfile: nil, setup_block: nil, prefork_block: nil, fork_block: nil, reporting_block: nil)
6
+ @wanted_number_of_workers = worker_num
7
+ @running = true
8
+ @worker_pids = []
9
+ @signals_received = []
10
+ @pidfile = pidfile
11
+
12
+ @setup_block = setup_block
13
+ @prefork_block = prefork_block
14
+ @fork_block = fork_block
15
+ @reporting_block = reporting_block
16
+
17
+ write_pid if @pidfile
18
+ end
19
+
20
+ def start!
21
+ traps
22
+ update_leader_title
23
+ @setup_block.call if @setup_block
24
+ spawn_missing_workers
25
+
26
+ gameloop = 1
27
+
28
+ until @worker_pids.dup.length == 0 && @running == false
29
+ sleep 0.25
30
+
31
+ # Handle actions
32
+ while(signal = @signals_received.shift)
33
+ case signal
34
+ when 'CHLD'
35
+ @worker_pids.dup.each do |wpid|
36
+ begin
37
+ wpid, _status = Process.waitpid(wpid, Process::WNOHANG)
38
+ @worker_pids.delete(wpid)
39
+ rescue Errno::ECHILD
40
+ end
41
+ end
42
+ when 'TTIN'
43
+ debug "-- handled #{signal}: wanted number of workers are now: #{@wanted_number_of_workers}"
44
+
45
+ @wanted_number_of_workers += 1
46
+ when 'TTOU'
47
+ unless @wanted_number_of_workers == 0
48
+ @wanted_number_of_workers -= 1
49
+ end
50
+
51
+ debug "-- handled #{signal}: wanted number of workers are now: #{@wanted_number_of_workers}"
52
+ when 'TERM'
53
+ debug "-- handled #{signal}"
54
+
55
+ @running = false
56
+ shutdown_each_worker(:TERM)
57
+ when 'QUIT'
58
+ debug "-- handled #{signal}"
59
+
60
+ @running = false
61
+ shutdown_each_worker(:QUIT)
62
+ end
63
+ end
64
+
65
+ # Spawn missing workers if we are not getting shut down
66
+ if @running
67
+ spawn_missing_workers
68
+ end
69
+
70
+ if gameloop % 20 == 0 && @reporting_block
71
+ @reporting_block.call
72
+ end
73
+
74
+ update_leader_title
75
+
76
+ gameloop += 1
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def spawn_missing_workers
83
+ begin
84
+ while (@worker_pids.length + 1) <= @wanted_number_of_workers
85
+ worker_data = if @prefork_block
86
+ @prefork_block.call
87
+ else
88
+ nil
89
+ end
90
+
91
+ if(pid = fork)
92
+ @worker_pids << pid
93
+ update_leader_title
94
+ else
95
+ Worker.new.work!(worker_data, &@fork_block)
96
+ end
97
+ end
98
+ rescue Forkworker::NoMoreWork
99
+ debug "-- No more work, so we're just finishing up running processes"
100
+ @running = false
101
+ end
102
+ end
103
+
104
+ def traps
105
+ # By trapping the :CHLD signal our process will be notified by the kernel when one of its children exits.
106
+ trap(:CHLD) do
107
+ @signals_received << 'CHLD'
108
+ end
109
+
110
+ trap(:TERM) do
111
+ @signals_received << 'TERM'
112
+ end
113
+
114
+ trap(:TTIN) do
115
+ @signals_received << 'TTIN'
116
+ end
117
+
118
+ trap(:TTOU) do
119
+ @signals_received << 'TTOU'
120
+ end
121
+
122
+ [:QUIT, :INT].each do |signal|
123
+ trap(signal) do
124
+ @signals_received << 'QUIT'
125
+ end
126
+ end
127
+ end
128
+
129
+ def update_leader_title
130
+ run_state = @running ? 'running' : 'shutting down'
131
+ $PROGRAM_NAME = "Leader ##{Process.pid} | #{run_state} | Workers=#{@worker_pids.length}/#{@wanted_number_of_workers}"
132
+ end
133
+
134
+ def shutdown_worker(signal, wpid)
135
+ begin
136
+ Process.kill(signal, wpid)
137
+ rescue Errno::ESRCH
138
+ end
139
+ end
140
+
141
+ def shutdown_each_worker(signal)
142
+ @worker_pids.dup.each { |wpid| shutdown_worker(signal, wpid) }
143
+ end
144
+
145
+ def write_pid
146
+ if File.exist?(@pidfile) && (pid = File.read(@pidfile))
147
+ begin
148
+ Process.getpgid(pid.to_i) # throws Errno::ESRCH if process with pid exists
149
+ debug "Program is already running on pid #{pid} specified in #{@pidfile}"
150
+ exit 1
151
+ rescue Errno::ESRCH
152
+ false
153
+ end
154
+ end
155
+
156
+ File.open(@pidfile, 'w') do |f|
157
+ f.write Process.pid
158
+ end
159
+ end
160
+
161
+ end
162
+ end
@@ -0,0 +1,7 @@
1
+ module Forkworker
2
+ module Logger
3
+ def debug(logline)
4
+ puts logline if ENV["DEBUG"] == "1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ module Forkworker
2
+ class Worker
3
+ include Forkworker::Logger
4
+
5
+ def work!(worker_data, &block)
6
+ @worker_data = worker_data
7
+ @running = true
8
+ update_title("spawned")
9
+ traps
10
+
11
+ instance_eval(&block)
12
+
13
+ exit(0)
14
+ end
15
+
16
+ private
17
+
18
+ def traps
19
+ trap(:TERM) do
20
+ @running = false
21
+ update_title
22
+ end
23
+ end
24
+
25
+ def update_title(status = nil)
26
+ @last_status = status if status
27
+ run_state = @running ? 'running' : 'shutting down'
28
+
29
+ $PROGRAM_NAME = "Worker ##{Process.pid} | #{run_state} | #{@last_status}"
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forkworker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kasper Grubbe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.9.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.9.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.13.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.13.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-remote
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.8
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.1.8
55
+ description: Forkworker lets you manage forking workloads easily
56
+ email: rubygems@kaspergrubbe.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/forkworker.rb
62
+ - lib/forkworker/leader.rb
63
+ - lib/forkworker/logger.rb
64
+ - lib/forkworker/worker.rb
65
+ homepage: https://rubygems.org/gems/forkworker
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.1.2
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Manage forking workloads with ease
88
+ test_files: []