OSDN Git Service

* [shogi-server]
authordaigo <beatles@users.sourceforge.jp>
Fri, 2 Jul 2010 02:57:56 +0000 (11:57 +0900)
committerDaigo Moriwaki <daigo@debian.org>
Thu, 8 Jul 2010 12:54:22 +0000 (21:54 +0900)
  - A new command line option:
      --floodgate-names GameStringA[,GameStringB[,...]]
  - 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.

changelog
floodgate-0-240.conf.sample [new file with mode: 0644]
shogi-server
shogi_server/league/floodgate.rb
shogi_server/league/floodgate_thread.rb [new file with mode: 0644]
test/TC_ALL.rb
test/TC_floodgate_next_time_generator.rb
test/TC_floodgate_thread.rb [new file with mode: 0644]

index e2aef1b..1f596f4 100644 (file)
--- a/changelog
+++ b/changelog
@@ -1,3 +1,43 @@
+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]
diff --git a/floodgate-0-240.conf.sample b/floodgate-0-240.conf.sample
new file mode 100644 (file)
index 0000000..a9b0e2e
--- /dev/null
@@ -0,0 +1,7 @@
+## 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
index 7f6d068..580760a 100755 (executable)
@@ -33,6 +33,7 @@ $:.unshift File.dirname(__FILE__)
 require 'shogi_server'
 require 'shogi_server/config'
 require 'shogi_server/util'
+require 'shogi_server/league/floodgate_thread.rb'
 require 'tempfile'
 
 #################################################
@@ -74,6 +75,8 @@ OPTIONS
                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.
@@ -121,6 +124,7 @@ def parse_command_line
   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
@@ -174,6 +178,19 @@ def check_command_line
     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
@@ -284,73 +301,6 @@ def setup_watchdog_for_giant_lock
   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
@@ -372,7 +322,7 @@ def main
   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
   config[:Logger]     = $logger
 
-  fg_thread = nil
+  setup_floodgate = nil
 
   config[:StartCallback] = Proc.new do
     srand
@@ -381,7 +331,8 @@ def main
     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
@@ -395,7 +346,7 @@ def main
   ["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/)
index e3902de..f721d3a 100644 (file)
@@ -1,3 +1,5 @@
+require 'shogi_server/util'
+require 'date'
 require 'thread'
 require 'ostruct'
 require 'pathname'
@@ -21,13 +23,16 @@ class League
       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)
@@ -58,8 +63,13 @@ class League
       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"
@@ -70,9 +80,60 @@ class League
       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
@@ -81,16 +142,18 @@ class League
       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
diff --git a/shogi_server/league/floodgate_thread.rb b/shogi_server/league/floodgate_thread.rb
new file mode 100644 (file)
index 0000000..1b8ebc2
--- /dev/null
@@ -0,0 +1,133 @@
+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
index 9c2f290..934032c 100644 (file)
@@ -8,6 +8,7 @@ require 'TC_config'
 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'
index 03f17d3..519842a 100644 (file)
@@ -2,9 +2,39 @@ $:.unshift File.join(File.dirname(__FILE__), "..")
 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
@@ -87,3 +117,44 @@ class TestNextTimeGenerator_3600_0 < Test::Unit::TestCase
   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
diff --git a/test/TC_floodgate_thread.rb b/test/TC_floodgate_thread.rb
new file mode 100644 (file)
index 0000000..434f2b8
--- /dev/null
@@ -0,0 +1,92 @@
+$:.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