+2010-06-22 Daigo Moriwaki <daigo at debian dot org>
+
+ * [shogi-server]
+ - A new command line option:
+ --floodgate-names GameStringA[,GameStringB[,...]]
+ where a game string should be a valid game name such as
+ floodgate-900-0.
+ .
+ Note: Without this option, no floodgate games are started. If
+ you want floodgate-900-0 to run, which was default enabled in
+ previous versions, you need to spefify the game name in this new
+ option.
+ - Floodgate time configuration file:
+ You need to set starting times of floodgate groups in
+ configuration files under the top directory. Each floodgat
+ e group requires a correspoding configuration file named
+ "<game_name>.conf". The file will be re-read once just after a
+ game starts.
+ .
+ For example, a floodgate-3600-30 game group requires
+ floodgate-3600-30.conf. However, for floodgate-900-0 and
+ floodgate-3600-0, which were default enabled in previous
+ versions, configuration files are optional if you are happy with
+ defualt time settings.
+ File format is:
+ Line format:
+ # This is a comment line
+ DoW Time
+ ...
+ where
+ DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
+ "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
+ "Friday" | "Saturday"
+ Time := HH:MM
+
+ For example,
+ Sat 13:00
+ Sat 22:00
+ Sun 13:00
+
2010-06-01 Daigo Moriwaki <daigo at debian dot org>
* [shogi-server]
--- /dev/null
+## This is a sample configuration file for a Floodgate game, setting
+## starting times.
+##
+## This is a comment line
+Sat 13:00
+## This is a comment line
+Sun 13:00
require 'shogi_server'
require 'shogi_server/config'
require 'shogi_server/util'
+require 'shogi_server/league/floodgate_thread.rb'
require 'tempfile'
#################################################
specify filename for logging process ID
--daemon dir
run as a daemon. Log files will be put in dir.
+ --floodgate-games game_A[,...]
+ enable Floodgate with various game names.
--player-log-dir dir
log network messages for each player. Log files
will be put in the dir.
options = Hash::new
parser = GetoptLong.new(
["--daemon", GetoptLong::REQUIRED_ARGUMENT],
+ ["--floodgate-games", GetoptLong::REQUIRED_ARGUMENT],
["--pid-file", GetoptLong::REQUIRED_ARGUMENT],
["--player-log-dir", GetoptLong::REQUIRED_ARGUMENT])
parser.quiet = true
end
end
+ if $options["floodgate-games"]
+ names = $options["floodgate-games"].split(",")
+ new_names =
+ names.select do |name|
+ ShogiServer::League::Floodgate::game_name?(name)
+ end
+ if names.size != new_names.size
+ $stderr.puts "Found a wrong Floodgate game: %s" % [names.join(",")]
+ exit 6
+ end
+ $options["floodgate-games"] = new_names
+ end
+
if $options["floodgate-history"]
$stderr.puts "WARNING: --floodgate-history has been deprecated."
$options["floodgate-history"] = nil
end
end
-def floodgate_reload_log(leagues)
- floodgate = leagues.min {|a,b| a.next_time <=> b.next_time}
- diff = floodgate.next_time - Time.now
- log_message("Floodgate reloaded. The next match will start at %s in %d seconds" %
- [floodgate.next_time, diff])
-end
-
-def setup_floodgate(game_names)
- return Thread.start(game_names) do |game_names|
- Thread.pass
- leagues = game_names.collect do |game_name|
- ShogiServer::League::Floodgate.new($league,
- {:game_name => game_name})
- end
- leagues.delete_if do |floodgate|
- ret = false
- unless floodgate.next_time
- log_error("Unsupported game name: %s" % floodgate.game_name)
- ret = true
- end
- ret
- end
- if leagues.empty?
- log_error("No valid Floodgate game names found")
- return # exit from this thread
- end
- floodgate_reload_log(leagues)
-
- while (true)
- begin
- floodgate = leagues.min {|a,b| a.next_time <=> b.next_time}
- diff = floodgate.next_time - Time.now
- if diff > 0
- floodgate_reload_log(leagues) if $DEBUG
- sleep(diff/2)
- next
- end
- next_array = leagues.collect do |floodgate|
- if (floodgate.next_time - Time.now) > 0
- [floodgate.game_name, floodgate.next_time]
- else
- log_message("Starting Floodgate games...: %s" % [floodgate.game_name])
- $league.reload
- floodgate.match_game
- floodgate.charge
- [floodgate.game_name, floodgate.next_time] # next_time has been updated
- end
- end
- $mutex.synchronize do
- log_message("Reloading source...")
- ShogiServer.reload
- end
- # Regenerate floodgate instances after ShogiServer.realod
- leagues = next_array.collect do |game_name, next_time|
- floodgate = ShogiServer::League::Floodgate.new($league,
- {:game_name => game_name,
- :next_time => next_time})
- end
- floodgate_reload_log(leagues)
- rescue Exception => ex
- # ignore errors
- log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
- end
- end
- end
-end
-
def main
$options = parse_command_line
config[:ServerType] = WEBrick::Daemon if $options["daemon"]
config[:Logger] = $logger
- fg_thread = nil
+ setup_floodgate = nil
config[:StartCallback] = Proc.new do
srand
end
setup_watchdog_for_giant_lock
$league.setup_players_database
- fg_thread = setup_floodgate(["floodgate-900-0", "floodgate-3600-0"])
+ setup_floodgate = ShogiServer::SetupFloodgate.new($options["floodgate-games"])
+ setup_floodgate.start
end
config[:StopCallback] = Proc.new do
["INT", "TERM"].each do |signal|
trap(signal) do
server.shutdown
- fg_thread.kill if fg_thread
+ setup_floodgate.kill
end
end
unless (RUBY_PLATFORM.downcase =~ /mswin|mingw|cygwin|bccwin/)
+require 'shogi_server/util'
+require 'date'
require 'thread'
require 'ostruct'
require 'pathname'
end
end # class method
- attr_reader :next_time, :league, :game_name
+ # @next_time is updated if and only if charge() was called
+ #
+ attr_reader :next_time
+ attr_reader :league, :game_name
def initialize(league, hash={})
@league = league
@next_time = hash[:next_time] || nil
@game_name = hash[:game_name] || "floodgate-900-0"
- charge
+ charge if @next_time.nil?
end
def game_name?(str)
class << self
def factory(game_name)
ret = nil
+ conf_file_name = File.join($topdir, "#{game_name}.conf")
+
if $DEBUG
ret = NextTimeGenerator_Debug.new
+ elsif File.exists?(conf_file_name)
+ lines = IO.readlines(conf_file_name)
+ ret = NextTimeGeneratorConfig.new(lines)
elsif game_name == "floodgate-900-0"
ret = NextTimeGenerator_Floodgate_900_0.new
elsif game_name == "floodgate-3600-0"
end
end
+ # Schedule the next time from configuration files.
+ #
+ # Line format:
+ # # This is a comment line
+ # DoW Time
+ # ...
+ # where
+ # DoW := "Sun" | "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" |
+ # "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" |
+ # "Friday" | "Saturday"
+ # Time := HH:MM
+ #
+ # For example,
+ # Sat 13:00
+ # Sat 22:00
+ # Sun 13:00
+ #
+ class NextTimeGeneratorConfig
+
+ # Constructor.
+ # Read configuration contents.
+ #
+ def initialize(lines)
+ @lines = lines
+ end
+
+ def call(now=Time.now)
+ if now.kind_of?(Time)
+ now = ::ShogiServer::time2datetime(now)
+ end
+ candidates = []
+ # now.cweek 1-53
+ # now.cwday 1(Monday)-7
+ @lines.each do |line|
+ if %r!^\s*(\w+)\s+(\d{1,2}):(\d{1,2})! =~ line
+ dow, hour, minute = $1, $2.to_i, $3.to_i
+ dow_index = ::ShogiServer::parse_dow(dow)
+ next if dow_index.nil?
+ next unless (0..23).include?(hour)
+ next unless (0..59).include?(minute)
+ time = DateTime::commercial(now.year, now.cweek, dow_index, hour, minute) rescue next
+ time += 7 if time <= now
+ candidates << time
+ end
+ end
+ candidates.map! {|dt| ::ShogiServer::datetime2time(dt)}
+ return candidates.empty? ? nil : candidates.min
+ end
+ end
+
+ # Schedule the next time for floodgate-900-0: each 30 minutes
+ #
class NextTimeGenerator_Floodgate_900_0
def call(now)
- # each 30 minutes
if now.min < 30
return Time.mktime(now.year, now.month, now.day, now.hour, 30)
else
end
end
+ # Schedule the next time for floodgate-3600-0: each 2 hours (odd hour)
+ #
class NextTimeGenerator_Floodgate_3600_0
def call(now)
- # each 2 hours (odd hour)
return Time.mktime(now.year, now.month, now.day, now.hour) + ((now.hour%2)+1)*3600
end
end
+ # Schedule the next time for debug: each 30 seconds.
+ #
class NextTimeGenerator_Debug
def call(now)
- # for test, each 30 seconds
if now.sec < 30
return Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
else
--- /dev/null
+require 'shogi_server'
+require 'shogi_server/league/floodgate'
+
+module ShogiServer
+
+ class SetupFloodgate
+ # Constructor.
+ # @param game_names an array of game name strings
+ #
+ def initialize(game_names)
+ @game_names = game_names
+ @thread = nil
+ end
+
+ # Return the most recent Floodgate instance
+ #
+ def next_league(leagues)
+ floodgate = leagues.min {|a,b| a.next_time <=> b.next_time}
+ return floodgate
+ end
+
+ def floodgate_reload_log(leagues)
+ floodgate = next_league(leagues)
+ diff = floodgate.next_time - Time.now
+ log_message("Floodgate reloaded. The next match will start at %s in %d seconds" %
+ [floodgate.next_time, diff])
+ end
+
+ def mk_leagues
+ leagues = @game_names.collect do |game_name|
+ ShogiServer::League::Floodgate.new($league,
+ {:game_name => game_name})
+ end
+ leagues.delete_if do |floodgate|
+ ret = false
+ unless floodgate.next_time
+ log_error("Unsupported game name: %s" % floodgate.game_name)
+ ret = true
+ end
+ ret
+ end
+ if leagues.empty?
+ log_error("No valid Floodgate game names found")
+ return [] # will exit the thread
+ end
+ floodgate_reload_log(leagues)
+ return leagues
+ end
+
+ def wait_next_floodgate(floodgate)
+ diff = floodgate.next_time - Time.now
+ if diff > 0
+ floodgate_reload_log(leagues) if $DEBUG
+ sleep(diff/2)
+ return true
+ end
+ return false
+ end
+
+ def reload_shogi_server
+ $mutex.synchronize do
+ log_message("Reloading source...")
+ ShogiServer.reload
+ end
+ end
+
+ def start_games(floodgate)
+ log_message("Starting Floodgate games...: %s" % [floodgate.game_name])
+ $league.reload
+ floodgate.match_game
+ end
+
+ # Regenerate floodgate instances from next_array for the next matches.
+ # @param next_array array of [game_name, next_time]
+ #
+ def regenerate_leagues(next_array)
+ leagues = next_array.collect do |game_name, next_time|
+ floodgate = ShogiServer::League::Floodgate.new($league,
+ {:game_name => game_name,
+ :next_time => next_time})
+ end
+ floodgate_reload_log(leagues)
+ return leagues
+ end
+
+ def start
+ return nil if @game_names.nil? || @game_names.empty?
+
+ log_message("Set up floodgate games: %s" % [@game_names.join(",")])
+ @thread = Thread.start(@game_names) do |game_names|
+ Thread.pass
+ leagues = mk_leagues
+ if leagues.nil? || leagues.empty?
+ return # exit from this thread
+ end
+
+ while (true)
+ begin
+ floodgate = next_league(leagues)
+ next if wait_next_floodgate(floodgate)
+
+ next_array = leagues.collect do |floodgate|
+ if (floodgate.next_time - Time.now) > 0
+ [floodgate.game_name, floodgate.next_time]
+ else
+ start_games(floodgate)
+ floodgate.charge # updates next_time
+ [floodgate.game_name, floodgate.next_time]
+ end
+ end
+
+ reload_shogi_server
+
+ # Regenerate floodgate instances after ShogiServer.realod
+ leagues = regenerate_leagues(next_array)
+ rescue Exception => ex
+ # ignore errors
+ log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
+ end
+ end # infinite loop
+
+ return @thread
+ end # Thread
+
+ end # def start
+
+ def kill
+ @thread.kill if @thread
+ end
+
+ end # class SetupFloodgate
+
+end # module ShogiServer
require 'TC_floodgate'
require 'TC_floodgate_history'
require 'TC_floodgate_next_time_generator'
+require 'TC_floodgate_thread.rb'
require 'TC_functional'
require 'TC_game'
require 'TC_game_result'
require 'test/unit'
require 'shogi_server'
require 'shogi_server/league/floodgate'
+require 'ftools'
$topdir = File.expand_path File.dirname(__FILE__)
+class TestNextTimeGenerator < Test::Unit::TestCase
+ def setup
+ @game_name = "floodgate-3600-0"
+ @config_path = File.join($topdir, "#{@game_name}.conf")
+ end
+
+ def teardown
+ if File.exist? @config_path
+ FileUtils.rm @config_path
+ end
+ end
+
+ def test_assure_file_does_not_exist
+ assert !File.exist?(@config_path)
+ end
+
+ def test_factory_from_config_file
+ # no config file
+ assert !File.exist?(@config_path)
+ assert_instance_of ShogiServer::League::Floodgate::NextTimeGenerator_Floodgate_3600_0,
+ ShogiServer::League::Floodgate::NextTimeGenerator.factory(@game_name)
+
+ # there is a config file
+ FileUtils.touch(@config_path)
+ assert_instance_of ShogiServer::League::Floodgate::NextTimeGeneratorConfig,
+ ShogiServer::League::Floodgate::NextTimeGenerator.factory(@game_name)
+ end
+end
+
class TestNextTimeGenerator_900_0 < Test::Unit::TestCase
def setup
@next = ShogiServer::League::Floodgate::NextTimeGenerator_Floodgate_900_0.new
end
end
+class TestNextTimeGeneratorConfig < Test::Unit::TestCase
+ def setup
+ end
+
+ def test_read
+ now = DateTime.new(2010, 6, 10, 21, 20, 15) # Thu
+ assert_equal DateTime.parse("10-06-2010 21:20:15"), now
+
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Thu 22:00"
+ assert_instance_of Time, ntc.call(now)
+ assert_equal Time.parse("10-06-2010 22:00"), ntc.call(now)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Thu 22:15"
+ assert_equal Time.parse("10-06-2010 22:15"), ntc.call(now)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Fri 22:00"
+ assert_equal Time.parse("11-06-2010 22:00"), ntc.call(now)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Sat 22:00"
+ assert_equal Time.parse("12-06-2010 22:00"), ntc.call(now)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Sun 22:00"
+ assert_equal Time.parse("13-06-2010 22:00"), ntc.call(now)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Mon 22:00"
+ assert_equal Time.parse("14-06-2010 22:00"), ntc.call(now)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Thu 20:00"
+ assert_equal Time.parse("17-06-2010 20:00"), ntc.call(now)
+ end
+
+ def test_read_time
+ now = Time.mktime(2010, 6, 10, 21, 20, 15)
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Thu 22:00"
+ assert_instance_of Time, ntc.call(now)
+ end
+
+ def test_read_change
+ now = DateTime.new(2010, 6, 10, 21, 59, 59) # Thu
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Thu 22:00"
+ assert_equal Time.parse("10-06-2010 22:00"), ntc.call(now)
+
+ now = DateTime.new(2010, 6, 10, 22, 0, 0) # Thu
+ ntc = ShogiServer::League::Floodgate::NextTimeGeneratorConfig.new "Thu 22:00"
+ assert_equal Time.parse("17-06-2010 22:00"), ntc.call(now)
+ end
+end
--- /dev/null
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require 'test/unit'
+require 'ostruct'
+$topdir = File.expand_path File.dirname(__FILE__)
+require 'shogi_server'
+require 'shogi_server/league/floodgate'
+require 'shogi_server/league/floodgate_thread'
+require 'test/mock_log_message'
+
+class MySetupFloodgate < ShogiServer::SetupFloodgate
+ def initialize(game_names)
+ super
+ @is_reload_shogi_server = false
+ @is_start_games = false
+ end
+ attr_reader :is_reload_shogi_server, :is_start_games
+
+ def reload_shogi_server
+ @is_reload_shogi_server = true
+ end
+
+ def start_games(floodgate)
+ @is_start_games = floodgate
+ end
+end
+
+class TestSetupFloodgate < Test::Unit::TestCase
+ def setup
+ game_names = %w(floodgate-900-0 floodgate-3600-0)
+ @sf = MySetupFloodgate.new game_names
+ end
+
+ def test_initialize_empty
+ sf = ShogiServer::SetupFloodgate.new []
+ thread = sf.start
+ assert_nil thread
+ end
+
+ def test_mk_leagues
+ leagues = @sf.mk_leagues
+ assert_equal 2, leagues.size
+ assert_equal "floodgate-900-0", leagues[0].game_name
+ assert_equal "floodgate-3600-0", leagues[1].game_name
+ end
+
+ def test_next_league
+ fa = OpenStruct.new
+ now = Time.now
+ fa.next_time = now
+ fb = OpenStruct.new
+ fb.next_time = now + 1
+ assert_equal fa.next_time, @sf.next_league([fa]).next_time
+ assert_equal fa.next_time, @sf.next_league([fa,fb]).next_time
+ assert_equal fa.next_time, @sf.next_league([fb,fa]).next_time
+ end
+
+ def test_wait_next_floodgate
+ f = OpenStruct.new
+ f.next_time = Time.now + 1;
+ assert @sf.wait_next_floodgate f
+ f.next_time = Time.now - 1;
+ assert(!@sf.wait_next_floodgate(f))
+ end
+
+ def test_regenerate_leagues
+ game_names = %w(floodgate-900-0 floodgate-3600-0)
+ now = Time.now
+ next_array = [["floodgate-900-0", now+100], ["floodgate-3600-0", now+200]]
+ objs = @sf.regenerate_leagues(next_array)
+ assert_equal 2, objs.size
+ assert_instance_of ShogiServer::League::Floodgate, objs[0]
+ assert_instance_of ShogiServer::League::Floodgate, objs[1]
+ end
+
+ def test_start
+ def @sf.mk_leagues
+ ret = []
+ now = Time.now
+ ret << ShogiServer::League::Floodgate.new($league,
+ {:game_name => "floodgate-900-0",
+ :next_time => (now-100)})
+ ret << ShogiServer::League::Floodgate.new($league,
+ {:game_name => "floodgate-3600-0",
+ :next_time => (now-200)})
+ ret
+ end
+ thread = @sf.start
+ sleep 1
+ assert_instance_of Thread, thread
+ assert_equal("floodgate-3600-0", @sf.is_start_games.game_name)
+ end
+end