serverengine 1.5.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,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