shogi-server source
修订版 | 57e00845d62e520576f71c7ed968af274cc5da50 (tree) |
---|---|
时间 | 2008-10-02 00:03:50 |
作者 | beatles <beatles@b8c6...> |
Commiter | beatles |
Under debugging
@@ -20,6 +20,9 @@ | ||
20 | 20 | |
21 | 21 | $:.unshift File.dirname(__FILE__) |
22 | 22 | require 'shogi_server' |
23 | +require 'shogi_server/board' # not autoloaded | |
24 | +require 'shogi_server/player' | |
25 | +require 'shogi_server/game' | |
23 | 26 | |
24 | 27 | ################################################# |
25 | 28 | # MAIN |
@@ -32,7 +35,7 @@ def gets_safe(socket, timeout=nil) | ||
32 | 35 | return :timeout |
33 | 36 | end |
34 | 37 | rescue Exception => ex |
35 | - log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") | |
38 | + log_error("gets_safe: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") | |
36 | 39 | return :exception |
37 | 40 | end |
38 | 41 |
@@ -113,20 +116,21 @@ def write_pid_file(file) | ||
113 | 116 | end |
114 | 117 | |
115 | 118 | def mutex_watchdog(mutex, sec) |
119 | + sec = 1 if sec < 1 | |
120 | + queue = [] | |
116 | 121 | while true |
117 | - begin | |
118 | - timeout(sec) do | |
119 | - begin | |
120 | - mutex.lock | |
121 | - ensure | |
122 | - mutex.unlock | |
123 | - end | |
122 | + if mutex.try_lock | |
123 | + queue.clear | |
124 | + mutex.unlock | |
125 | + else | |
126 | + queue.push(Object.new) | |
127 | + if queue.size > sec | |
128 | + # timeout | |
129 | + log_error("mutex watchdog timeout: %d sec" % [sec]) | |
130 | + queue.clear | |
124 | 131 | end |
125 | - sleep(sec) | |
126 | - rescue TimeoutError | |
127 | - log_error("mutex watchdog timeout") | |
128 | - exit(1) | |
129 | 132 | end |
133 | + sleep(1) | |
130 | 134 | end |
131 | 135 | end |
132 | 136 |
@@ -182,7 +186,40 @@ def setup_watchdog_for_giant_lock | ||
182 | 186 | end |
183 | 187 | end |
184 | 188 | |
189 | +def setup_floodgate | |
190 | + Thread.start do | |
191 | + Thread.pass | |
192 | + floodgate = ShogiServer::League::Floodgate.new(LEAGUE) | |
193 | + log_message("Flooddgate reloaded. The next match will start at %s." % | |
194 | + [floodgate.next_time]) | |
195 | + | |
196 | + while (true) | |
197 | + begin | |
198 | + sleep(10) | |
199 | + next if Time.now < floodgate.next_time | |
200 | + LEAGUE.reload | |
201 | + floodgate.match_game | |
202 | + floodgate.charge | |
203 | + next_time = floodgate.next_time | |
204 | + $mutex.synchronize do | |
205 | + Dependencies.clear | |
206 | + end | |
207 | + floodgate = ShogiServer::League::Floodgate.new(LEAGUE, next_time) | |
208 | + log_message("Flooddgate reloaded. The next match will start at %s." % | |
209 | + [floodgate.next_time]) | |
210 | + rescue Exception => ex | |
211 | + # ignore errors | |
212 | + log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}") | |
213 | + end | |
214 | + end | |
215 | + end | |
216 | +end | |
217 | + | |
185 | 218 | def main |
219 | + | |
220 | + [ShogiServer::League::Floodgate, ShogiServer::Pairing].each do |klass| | |
221 | + Dependencies.unloadable klass | |
222 | + end | |
186 | 223 | |
187 | 224 | setup_watchdog_for_giant_lock |
188 | 225 |
@@ -219,12 +256,16 @@ def main | ||
219 | 256 | config[:Port] = port |
220 | 257 | config[:ServerType] = WEBrick::Daemon if $options["daemon"] |
221 | 258 | config[:Logger] = $logger |
222 | - if $options["pid-file"] | |
223 | - pid_file = File.expand_path($options["pid-file"]) | |
224 | - config[:StartCallback] = Proc.new do | |
225 | - write_pid_file(pid_file) | |
259 | + | |
260 | + config[:StartCallback] = Proc.new do | |
261 | + if $options["pid-file"] | |
262 | + write_pid_file($options["pid-file"]) | |
226 | 263 | end |
227 | - config[:StopCallback] = Proc.new do | |
264 | + setup_floodgate | |
265 | + end | |
266 | + | |
267 | + config[:StopCallback] = Proc.new do | |
268 | + if $options["pid-file"] | |
228 | 269 | FileUtils.rm(pid_file, :force => true) |
229 | 270 | end |
230 | 271 | end |
@@ -234,12 +275,13 @@ def main | ||
234 | 275 | trap(signal) do |
235 | 276 | LEAGUE.shutdown |
236 | 277 | server.shutdown |
278 | + # TODO Shutdown Floodgate's thread | |
237 | 279 | end |
238 | 280 | end |
239 | 281 | trap("HUP") do |
240 | 282 | LEAGUE.shutdown |
241 | 283 | Dependencies.clear |
242 | - LEAGUE.restart | |
284 | + # TODO Restart Floodgate's thread | |
243 | 285 | end |
244 | 286 | $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] |
245 | 287 | log_message("server started [Revision: #{ShogiServer::Revision}]") |
@@ -276,6 +318,10 @@ if ($0 == __FILE__) | ||
276 | 318 | TCPSocket.do_not_reverse_lookup = true |
277 | 319 | Thread.abort_on_exception = $DEBUG ? true : false |
278 | 320 | |
321 | + begin | |
279 | 322 | LEAGUE = ShogiServer::League::new |
280 | - main | |
323 | + main | |
324 | + rescue Exception => ex | |
325 | + log_error("main: #{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}") | |
326 | + end | |
281 | 327 | end |
@@ -20,19 +20,36 @@ | ||
20 | 20 | module ShogiServer # for a namespace |
21 | 21 | |
22 | 22 | class Board |
23 | - def initialize | |
23 | + def initialize(move_count=0) | |
24 | 24 | @sente_hands = Array::new |
25 | 25 | @gote_hands = Array::new |
26 | 26 | @history = Hash::new(0) |
27 | 27 | @sente_history = Hash::new(0) |
28 | 28 | @gote_history = Hash::new(0) |
29 | 29 | @array = [[], [], [], [], [], [], [], [], [], []] |
30 | - @move_count = 0 | |
30 | + @move_count = move_count | |
31 | 31 | @teban = nil # black => true, white => false |
32 | 32 | end |
33 | 33 | attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history, :teban |
34 | 34 | attr_reader :move_count |
35 | 35 | |
36 | + def deep_copy | |
37 | + # return Marshal.load(Marshal.dump(self)) | |
38 | + board = Board.new(self.move_count) | |
39 | + board.sente_hands = self.sente_hands.clone | |
40 | + board.gote_hands = self.gote_hands.clone | |
41 | + board.history = self.history.clone | |
42 | + board.history.default = 0 | |
43 | + board.sente_history = self.sente_history.clone | |
44 | + board.sente_history.default = 0 | |
45 | + board.gote_history = self.gote_history.clone | |
46 | + board.gote_history.default = 0 | |
47 | + board.array = [] | |
48 | + self.array.each {|a| board.array.push(a.clone)} | |
49 | + board.teban = self.teban | |
50 | + return board | |
51 | + end | |
52 | + | |
36 | 53 | def initial |
37 | 54 | PieceKY::new(self, 1, 1, false) |
38 | 55 | PieceKE::new(self, 2, 1, false) |
@@ -82,10 +99,12 @@ class Board | ||
82 | 99 | |
83 | 100 | if ((x0 == 0) || (y0 == 0)) |
84 | 101 | piece = have_piece?(hands, name) |
85 | - return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece? | |
102 | + return :illegal if (piece == nil || ! piece.move_to?(x1, y1, name)) | |
86 | 103 | piece.move_to(x1, y1) |
87 | 104 | else |
88 | - return :illegal if (! @array[x0][y0].move_to?(x1, y1, name)) # TODO null check? | |
105 | + if (@array[x0][y0] == nil || !@array[x0][y0].move_to?(x1, y1, name)) | |
106 | + return :illegal | |
107 | + end | |
89 | 108 | if (@array[x0][y0].name != name) # promoted ? |
90 | 109 | @array[x0][y0].promoted = true |
91 | 110 | end |
@@ -169,7 +188,7 @@ class Board | ||
169 | 188 | |
170 | 189 | ## case: rival_ou is moving |
171 | 190 | rival_ou.movable_grids.each do |(cand_x, cand_y)| |
172 | - tmp_board = Marshal.load(Marshal.dump(self)) | |
191 | + tmp_board = deep_copy | |
173 | 192 | s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente) |
174 | 193 | raise "internal error" if (s != true) |
175 | 194 | if (! tmp_board.checkmated?(! sente)) # good move |
@@ -197,7 +216,7 @@ class Board | ||
197 | 216 | end |
198 | 217 | end |
199 | 218 | names.map! do |name| |
200 | - tmp_board = Marshal.load(Marshal.dump(self)) | |
219 | + tmp_board = deep_copy | |
201 | 220 | s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente) |
202 | 221 | if s == :illegal |
203 | 222 | s # result |
@@ -380,7 +399,7 @@ class Board | ||
380 | 399 | return :illegal # can't put on existing piece |
381 | 400 | end |
382 | 401 | |
383 | - tmp_board = Marshal.load(Marshal.dump(self)) | |
402 | + tmp_board = deep_copy | |
384 | 403 | return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal) |
385 | 404 | return :oute_kaihimore if (tmp_board.checkmated?(sente)) |
386 | 405 | tmp_board.update_sennichite(sente) |
@@ -364,6 +364,7 @@ class Game | ||
364 | 364 | end |
365 | 365 | |
366 | 366 | move_status = @board.handle_one_move(str, @sente == @current_player) |
367 | + # log_debug("move_status: %s for %s's %s" % [move_status, @sente == @current_player ? "BLACK" : "WHITE", str]) | |
367 | 368 | |
368 | 369 | if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status) |
369 | 370 | @fh.printf("'ILLEGAL_MOVE(%s)\n", str) |
@@ -17,9 +17,6 @@ | ||
17 | 17 | ## along with this program; if not, write to the Free Software |
18 | 18 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
19 | 19 | |
20 | -#require 'shogi_server/league/floodgate' | |
21 | -#require 'shogi_server/league/persistent' | |
22 | - | |
23 | 20 | module ShogiServer # for a namespace |
24 | 21 | |
25 | 22 | ###################################################### |
@@ -33,8 +30,6 @@ class League | ||
33 | 30 | @players = Hash::new |
34 | 31 | @event = nil |
35 | 32 | @dir = File.dirname(__FILE__) |
36 | - @floodgate = Floodgate.new(self) | |
37 | - #@floodgate.run | |
38 | 33 | end |
39 | 34 | attr_accessor :players, :games, :event, :dir |
40 | 35 |
@@ -44,12 +39,6 @@ class League | ||
44 | 39 | @persistent.save(player) |
45 | 40 | end |
46 | 41 | end |
47 | - @floodgate.shutdown | |
48 | - end | |
49 | - | |
50 | - def restart | |
51 | - @floodgate = Floodgate.new(self) | |
52 | - #@floodgate.run | |
53 | 42 | end |
54 | 43 | |
55 | 44 | # this should be called just after instanciating a League object. |
@@ -8,48 +8,30 @@ class League | ||
8 | 8 | end |
9 | 9 | end |
10 | 10 | |
11 | - def initialize(league) | |
11 | + attr_reader :next_time, :league | |
12 | + | |
13 | + def initialize(league, next_time=nil) | |
12 | 14 | @league = league |
13 | - @next_time = nil | |
15 | + @next_time = next_time | |
14 | 16 | charge |
15 | 17 | end |
16 | 18 | |
17 | - def run | |
18 | - @thread = Thread.new do | |
19 | - Thread.pass | |
20 | - while (true) | |
21 | - begin | |
22 | - sleep(10) | |
23 | - next if Time.now < @next_time | |
24 | - @league.reload | |
25 | - match_game | |
26 | - charge | |
27 | - rescue Exception => ex | |
28 | - # ignore errors | |
29 | - log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}") | |
30 | - end | |
31 | - end | |
32 | - end | |
33 | - end | |
34 | - | |
35 | - def shutdown | |
36 | - @thread.kill if @thread | |
37 | - end | |
38 | - | |
39 | - # private | |
40 | - | |
41 | 19 | def charge |
42 | 20 | now = Time.now |
43 | - # if now.min < 30 | |
44 | - # @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30) | |
45 | - # else | |
46 | - # @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600 | |
47 | - # end | |
48 | - # for test | |
49 | - if now.sec < 30 | |
50 | - @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30) | |
21 | + unless $DEBUG | |
22 | + # each 30 minutes | |
23 | + if now.min < 30 | |
24 | + @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30) | |
25 | + else | |
26 | + @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600 | |
27 | + end | |
51 | 28 | else |
52 | - @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60 | |
29 | + # for test, each 30 seconds | |
30 | + if now.sec < 30 | |
31 | + @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30) | |
32 | + else | |
33 | + @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60 | |
34 | + end | |
53 | 35 | end |
54 | 36 | end |
55 | 37 |
@@ -17,6 +17,8 @@ | ||
17 | 17 | ## along with this program; if not, write to the Free Software |
18 | 18 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
19 | 19 | |
20 | +require 'monitor' | |
21 | + | |
20 | 22 | module ShogiServer # for a namespace |
21 | 23 | |
22 | 24 | class BasicPlayer |
@@ -110,7 +112,9 @@ class Player < BasicPlayer | ||
110 | 112 | @sente = nil |
111 | 113 | @socket_buffer = [] |
112 | 114 | @main_thread = Thread::current |
113 | - @write_queue = Queue.new | |
115 | + @write_queue = [] | |
116 | + @wq_mon = Monitor.new | |
117 | + @wq_cond = @wq_mon.new_cond | |
114 | 118 | @player_logger = nil |
115 | 119 | start_write_thread |
116 | 120 | end |
@@ -186,16 +190,23 @@ class Player < BasicPlayer | ||
186 | 190 | while !@socket.closed? |
187 | 191 | str = "" |
188 | 192 | begin |
189 | - begin | |
190 | - timeout(5) do | |
191 | - str = @write_queue.deq | |
193 | + timeout_flg = false | |
194 | + @wq_mon.synchronize do | |
195 | + if @write_queue.empty? | |
196 | + if @wq_cond.wait(15) | |
197 | + #timeout | |
198 | + timeout_flg = true | |
199 | + # log_debug("write_queue health check timeout") | |
200 | + end | |
192 | 201 | end |
193 | - if str == nil | |
194 | - break | |
202 | + if !timeout_flg && !@write_queue.empty? | |
203 | + str = @write_queue.shift | |
204 | + # log_debug("Dequeued %s from %s's write_queue." % [str, @name]) | |
195 | 205 | end |
196 | - rescue TimeoutError | |
197 | - next | |
198 | - end | |
206 | + end # synchronize | |
207 | + next if timeout_flg | |
208 | + break if str == nil # exit | |
209 | + | |
199 | 210 | if r = select(nil, [@socket], nil, 20) |
200 | 211 | r[1].first.write(str) |
201 | 212 | log(:info, :out, str) |
@@ -216,7 +227,12 @@ class Player < BasicPlayer | ||
216 | 227 | # Note that sending a message is included in the giant lock. |
217 | 228 | # |
218 | 229 | def write_safe(str) |
219 | - @write_queue.enq str | |
230 | + @wq_mon.synchronize do | |
231 | + # log_debug("Enqueuing %s..." % [str]) | |
232 | + @write_queue.push(str) | |
233 | + # log_debug("Enqueued %s into %s's write_queue." % [str, @name]) | |
234 | + @wq_cond.broadcast | |
235 | + end | |
220 | 236 | end |
221 | 237 | |
222 | 238 | def to_s |
@@ -0,0 +1,84 @@ | ||
1 | +# queue = Queue.new | |
2 | +# timeout(5) do | |
3 | +# queue.deq | |
4 | +# end | |
5 | +# | |
6 | +# is not good since not all of stdlib is safe with respect to | |
7 | +# asynchronous exceptions. | |
8 | +# This class is a safe implementation. | |
9 | +# See: http://www.ruby-forum.com/topic/107864 | |
10 | +# | |
11 | + | |
12 | +require 'thread' | |
13 | +require 'monitor' | |
14 | + | |
15 | +module ShogiServer | |
16 | + | |
17 | +class TimeoutQueue | |
18 | + def initialize | |
19 | + @lock = Mutex.new | |
20 | + @messages = [] | |
21 | + @readers = [] | |
22 | + end | |
23 | + | |
24 | + def enq(msg) | |
25 | + @lock.synchronize do | |
26 | + unless @readers.empty? | |
27 | + @readers.pop << msg | |
28 | + else | |
29 | + @messages.push msg | |
30 | + end | |
31 | + end | |
32 | + end | |
33 | + | |
34 | + # | |
35 | + # @param timeout | |
36 | + # @return nil if timeout | |
37 | + # | |
38 | + def deq(timeout=5) | |
39 | + timeout_thread = nil | |
40 | + mon = nil | |
41 | + empty_cond = nil | |
42 | + | |
43 | + begin | |
44 | + reader = nil | |
45 | + @lock.synchronize do | |
46 | + unless @messages.empty? | |
47 | + # fast path | |
48 | + return @messages.shift | |
49 | + else | |
50 | + reader = Queue.new | |
51 | + @readers.push reader | |
52 | + if timeout | |
53 | + mon = Monitor.new | |
54 | + empty_cond = mon.new_cond | |
55 | + | |
56 | + timeout_thread = Thread.new do | |
57 | + mon.synchronize do | |
58 | + if empty_cond.wait(timeout) | |
59 | + # timeout | |
60 | + @lock.synchronize do | |
61 | + @readers.delete reader | |
62 | + reader << nil | |
63 | + end | |
64 | + else | |
65 | + # timeout_thread was waked up before timeout | |
66 | + end | |
67 | + end | |
68 | + end # thread | |
69 | + end | |
70 | + end | |
71 | + end | |
72 | + # either timeout or writer will send to us | |
73 | + return reader.shift | |
74 | + ensure | |
75 | + # (try to) clean up timeout thread | |
76 | + if timeout_thread | |
77 | + mon.synchronize { empty_cond.signal } | |
78 | + Thread.pass | |
79 | + end | |
80 | + end | |
81 | + end | |
82 | +end | |
83 | + | |
84 | +end |