serverengine 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ #
2
+ # ServerEngine
3
+ #
4
+ # Copyright (C) 2012-2013 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module ServerEngine
19
+
20
+ module ClassMethods
21
+ def dump_uncaught_error(e)
22
+ STDERR.write "Unexpected error #{e}\n"
23
+ e.backtrace.each {|bt|
24
+ STDERR.write " #{bt}\n"
25
+ }
26
+ nil
27
+ end
28
+ end
29
+
30
+ extend ClassMethods
31
+
32
+ end
@@ -0,0 +1,3 @@
1
+ module ServerEngine
2
+ VERSION = "1.5.0"
3
+ end
@@ -0,0 +1,73 @@
1
+ #
2
+ # ServerEngine
3
+ #
4
+ # Copyright (C) 2012-2013 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module ServerEngine
19
+
20
+ class Worker
21
+ def initialize(server, worker_id)
22
+ @server = server
23
+ @config = server.config
24
+ @logger = @server.logger
25
+ @worker_id = worker_id
26
+ end
27
+
28
+ attr_reader :server, :worker_id
29
+ attr_accessor :config, :logger
30
+
31
+ def before_fork
32
+ end
33
+
34
+ def run
35
+ raise NoMethodError, "Worker#run method is not implemented"
36
+ end
37
+
38
+ def stop
39
+ end
40
+
41
+ def reload
42
+ end
43
+
44
+ def close
45
+ end
46
+
47
+ def install_signal_handlers
48
+ w = self
49
+ SignalThread.new do |st|
50
+ st.trap(Daemon::Signals::GRACEFUL_STOP) { w.stop }
51
+ st.trap(Daemon::Signals::IMMEDIATE_STOP, 'SIG_DFL')
52
+
53
+ st.trap(Daemon::Signals::GRACEFUL_RESTART) { w.stop }
54
+ st.trap(Daemon::Signals::IMMEDIATE_RESTART, 'SIG_DFL')
55
+
56
+ st.trap(Daemon::Signals::RELOAD) {
57
+ w.logger.reopen!
58
+ w.reload
59
+ }
60
+ st.trap(Daemon::Signals::DETACH) { w.stop }
61
+
62
+ st.trap(Daemon::Signals::DUMP) { Sigdump.dump }
63
+ end
64
+ end
65
+
66
+ def main
67
+ run
68
+ ensure
69
+ close
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,25 @@
1
+ require File.expand_path 'lib/serverengine/version', File.dirname(__FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "serverengine"
5
+ gem.version = ServerEngine::VERSION
6
+
7
+ gem.authors = ["Sadayuki Furuhashi"]
8
+ gem.email = ["frsyuki@gmail.com"]
9
+ gem.description = %q{A framework to implement robust multiprocess servers like Unicorn}
10
+ gem.summary = %q{ServerEngine - multiprocess server framework}
11
+ gem.homepage = "https://github.com/frsyuki/serverengine"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.require_paths = ["lib"]
17
+ gem.has_rdoc = false
18
+
19
+ gem.required_ruby_version = ">= 1.9.3"
20
+
21
+ gem.add_dependency("sigdump", ["~> 0.2.2"])
22
+
23
+ gem.add_development_dependency("rake", [">= 0.9.2"])
24
+ gem.add_development_dependency("rspec", ["~> 2.13.0"])
25
+ end
@@ -0,0 +1,58 @@
1
+
2
+ describe ServerEngine::BlockingFlag do
3
+ subject { BlockingFlag.new }
4
+
5
+ it 'set and reset' do
6
+ should_not be_set
7
+ subject.set!
8
+ should be_set
9
+ subject.reset!
10
+ should_not be_set
11
+ end
12
+
13
+ it 'set! and reset! return whether it toggled the state' do
14
+ subject.reset!.should == false
15
+ subject.set!.should == true
16
+ subject.set!.should == false
17
+ subject.reset!.should == true
18
+ end
19
+
20
+ it 'wait_for_set timeout' do
21
+ start = Time.now
22
+
23
+ subject.wait_for_set(0.01)
24
+ elapsed = Time.now - start
25
+
26
+ elapsed.should >= 0.01
27
+ end
28
+
29
+ it 'wait_for_reset timeout' do
30
+ subject.set!
31
+
32
+ start = Time.now
33
+
34
+ subject.wait_for_reset(0.01)
35
+ elapsed = Time.now - start
36
+
37
+ elapsed.should >= 0.01
38
+ end
39
+
40
+ it 'wait' do
41
+ start = Time.now
42
+ elapsed = nil
43
+
44
+ started = BlockingFlag.new
45
+ t = Thread.new do
46
+ started.set!
47
+ subject.wait_for_set(1)
48
+ elapsed = Time.now - start
49
+ end
50
+ started.wait_for_set
51
+
52
+ subject.set!
53
+ t.join
54
+
55
+ elapsed.should_not be_nil
56
+ elapsed.should < 0.5
57
+ end
58
+ end
@@ -0,0 +1,88 @@
1
+
2
+ describe ServerEngine::DaemonLogger do
3
+ before { FileUtils.mkdir_p("tmp") }
4
+ before { FileUtils.rm_f("tmp/se1.log") }
5
+ before { FileUtils.rm_f("tmp/se2.log") }
6
+
7
+ subject { DaemonLogger.new("tmp/se1.log", log_stdout: false, log_stderr: false) }
8
+
9
+ it 'reopen' do
10
+ subject.path = 'tmp/se2.log'
11
+ subject.reopen!
12
+ subject.warn "test"
13
+
14
+ File.read('tmp/se2.log').should =~ /test$/
15
+ end
16
+
17
+ it 'stderr hook 1' do
18
+ subject.hook_stderr!
19
+ STDERR.puts "test"
20
+
21
+ File.read('tmp/se1.log').should == "test\n"
22
+ end
23
+
24
+ it 'stderr hook 2' do
25
+ log = DaemonLogger.new("tmp/se1.log", log_stdout: false, log_stderr: true)
26
+ STDERR.puts "test"
27
+
28
+ File.read('tmp/se1.log').should == "test\n"
29
+ end
30
+
31
+ it 'stderr hook and reopen' do
32
+ subject.hook_stderr!
33
+ subject.path = 'tmp/se2.log'
34
+ subject.reopen!
35
+ STDERR.puts "test"
36
+
37
+ File.read('tmp/se2.log').should == "test\n"
38
+ end
39
+
40
+ it 'default level is debug' do
41
+ subject.debug 'debug'
42
+ File.read('tmp/se1.log').should =~ /debug$/
43
+ end
44
+
45
+ it 'level set by int' do
46
+ subject.level = Logger::FATAL
47
+ subject.level.should == Logger::FATAL
48
+
49
+ subject.level = Logger::ERROR
50
+ subject.level.should == Logger::ERROR
51
+
52
+ subject.level = Logger::WARN
53
+ subject.level.should == Logger::WARN
54
+
55
+ subject.level = Logger::INFO
56
+ subject.level.should == Logger::INFO
57
+
58
+ subject.level = Logger::DEBUG
59
+ subject.level.should == Logger::DEBUG
60
+ end
61
+
62
+ it 'level set by string' do
63
+ subject.level = 'fatal'
64
+ subject.level.should == Logger::FATAL
65
+
66
+ subject.level = 'error'
67
+ subject.level.should == Logger::ERROR
68
+
69
+ subject.level = 'warn'
70
+ subject.level.should == Logger::WARN
71
+
72
+ subject.level = 'info'
73
+ subject.level.should == Logger::INFO
74
+
75
+ subject.level = 'debug'
76
+ subject.level.should == Logger::DEBUG
77
+ end
78
+
79
+ it 'unknown level' do
80
+ lambda { subject.level = 'unknown' }.should raise_error(ArgumentError)
81
+ end
82
+
83
+ it 'stdout logger' do
84
+ STDOUT.should_not_receive(:reopen)
85
+ log = DaemonLogger.new(STDOUT)
86
+ log.debug "stdout logging test"
87
+ end
88
+ end
@@ -0,0 +1,46 @@
1
+
2
+ describe ServerEngine::Daemon do
3
+ include_context 'test server and worker'
4
+
5
+ it 'run and graceful stop' do
6
+ dm = Daemon.new(TestServer, TestWorker, daemonize: true, pid_path: "tmp/pid")
7
+ dm.main
8
+
9
+ test_state(:server_initialize).should == 1
10
+
11
+ pid = File.read('tmp/pid').to_i
12
+ wait_for_fork
13
+
14
+ Process.kill(:TERM, pid)
15
+ wait_for_stop
16
+
17
+ test_state(:server_stop_graceful).should == 1
18
+ test_state(:worker_stop).should == 1
19
+ test_state(:server_after_run).should == 1
20
+ end
21
+
22
+ it 'signals' do
23
+ dm = Daemon.new(TestServer, TestWorker, daemonize: true, pid_path: "tmp/pid")
24
+ dm.main
25
+
26
+ pid = File.read('tmp/pid').to_i
27
+ wait_for_fork
28
+
29
+ Process.kill(:USR2, pid)
30
+ wait_for_stop
31
+ test_state(:server_reload).should == 1
32
+
33
+ Process.kill(:USR1, pid)
34
+ wait_for_stop
35
+ test_state(:server_restart_graceful).should == 1
36
+
37
+ Process.kill(:HUP, pid)
38
+ wait_for_stop
39
+ test_state(:server_restart_immediate).should == 1
40
+
41
+ Process.kill(:QUIT, pid)
42
+ wait_for_stop
43
+ test_state(:server_stop_immediate).should == 1
44
+ end
45
+ end
46
+
@@ -0,0 +1,61 @@
1
+
2
+ describe ServerEngine::MultiWorkerServer do
3
+ include_context 'test server and worker'
4
+
5
+ [MultiThreadServer, MultiProcessServer].each do |impl_class|
6
+
7
+ it 'scale up' do
8
+ config = {:workers => 2}
9
+
10
+ s = impl_class.new(TestWorker) { config.dup }
11
+ t = Thread.new { s.main }
12
+
13
+ begin
14
+ wait_for_fork
15
+ test_state(:worker_run).should == 2
16
+
17
+ config[:workers] = 3
18
+ s.reload
19
+
20
+ wait_for_restart
21
+ test_state(:worker_run).should == 3
22
+
23
+ test_state(:worker_stop).should == 0
24
+
25
+ ensure
26
+ s.stop(true)
27
+ t.join
28
+ end
29
+
30
+ test_state(:worker_stop).should == 3
31
+ end
32
+
33
+ it 'scale down' do
34
+ config = {:workers => 2}
35
+
36
+ s = impl_class.new(TestWorker) { config.dup }
37
+ t = Thread.new { s.main }
38
+
39
+ begin
40
+ wait_for_fork
41
+ test_state(:worker_run).should == 2
42
+
43
+ config[:workers] = 1
44
+ s.restart(true)
45
+
46
+ wait_for_restart
47
+ test_state(:worker_run).should == 3
48
+
49
+ test_state(:worker_stop).should == 2
50
+
51
+ ensure
52
+ s.stop(true)
53
+ t.join
54
+ end
55
+
56
+ test_state(:worker_stop).should == 3
57
+ end
58
+
59
+ end
60
+ end
61
+
@@ -0,0 +1,118 @@
1
+
2
+ require 'pstore'
3
+
4
+ def reset_test_state
5
+ FileUtils.mkdir_p 'tmp'
6
+ FileUtils.rm_f 'tmp/state.pstore'
7
+ FileUtils.touch 'tmp/state.pstore'
8
+ end
9
+
10
+ def incr_test_state(key)
11
+ ps = PStore.new('tmp/state.pstore')
12
+ ps.transaction do
13
+ ps[key] ||= 0
14
+ ps[key] += 1
15
+ end
16
+ end
17
+
18
+ def test_state(key)
19
+ ps = PStore.new('tmp/state.pstore')
20
+ ps.transaction do
21
+ return ps[key] || 0
22
+ end
23
+ end
24
+
25
+ shared_context 'test server and worker' do
26
+ before { reset_test_state }
27
+
28
+ def wait_for_fork
29
+ sleep 0.2
30
+ end
31
+
32
+ def wait_for_stop
33
+ sleep 0.8
34
+ end
35
+
36
+ def wait_for_restart
37
+ sleep 1.5
38
+ end
39
+
40
+ module TestServer
41
+ def initialize
42
+ incr_test_state :server_initialize
43
+ end
44
+
45
+ def before_run
46
+ incr_test_state :server_before_run
47
+ end
48
+
49
+ def after_run
50
+ incr_test_state :server_after_run
51
+ end
52
+
53
+ def close
54
+ incr_test_state :server_close
55
+ end
56
+
57
+ def stop(stop_graceful)
58
+ incr_test_state :server_stop
59
+ if stop_graceful
60
+ incr_test_state :server_stop_graceful
61
+ else
62
+ incr_test_state :server_stop_immediate
63
+ end
64
+ super
65
+ end
66
+
67
+ def restart(stop_graceful)
68
+ incr_test_state :server_restart
69
+ if stop_graceful
70
+ incr_test_state :server_restart_graceful
71
+ else
72
+ incr_test_state :server_restart_immediate
73
+ end
74
+ super
75
+ end
76
+
77
+ def reload
78
+ incr_test_state :server_reload
79
+ super
80
+ end
81
+
82
+ def detach
83
+ incr_test_state :server_detach
84
+ super
85
+ end
86
+ end
87
+
88
+ module TestWorker
89
+ def initialize
90
+ incr_test_state :worker_initialize
91
+ @stop_flag = BlockingFlag.new
92
+ end
93
+
94
+ def before_fork
95
+ incr_test_state :worker_before_fork
96
+ end
97
+
98
+ def run
99
+ incr_test_state :worker_run
100
+ @stop_flag.wait(5.0)
101
+ @stop_flag.reset!
102
+ end
103
+
104
+ def stop
105
+ incr_test_state :worker_stop
106
+ @stop_flag.set!
107
+ end
108
+
109
+ def reload
110
+ incr_test_state :worker_reload
111
+ end
112
+
113
+ def close
114
+ incr_test_state :worker_close
115
+ end
116
+ end
117
+
118
+ end