Browse Source

[master] Merge branch 'trac2574'

JINMEI Tatuya 12 years ago
parent
commit
cf78a00658

+ 42 - 13
src/bin/loadzone/loadzone.py.in

@@ -25,6 +25,7 @@ from isc.datasrc import *
 import isc.util.process
 import isc.util.process
 import isc.log
 import isc.log
 from isc.log_messages.loadzone_messages import *
 from isc.log_messages.loadzone_messages import *
+from datetime import timedelta
 
 
 isc.util.process.rename()
 isc.util.process.rename()
 
 
@@ -87,7 +88,6 @@ class LoadZoneRunner:
     '''
     '''
     def __init__(self, command_args):
     def __init__(self, command_args):
         self.__command_args = command_args
         self.__command_args = command_args
-        self.__loaded_rrs = 0
         self.__interrupted = False # will be set to True on receiving signal
         self.__interrupted = False # will be set to True on receiving signal
 
 
         # system-wide log configuration.  We need to configure logging this
         # system-wide log configuration.  We need to configure logging this
@@ -103,8 +103,9 @@ class LoadZoneRunner:
                                           [{"output": "stderr",
                                           [{"output": "stderr",
                                             "destination": "console"}]}]}
                                             "destination": "console"}]}]}
 
 
-        # These are essentially private, and defined as "protected" for the
+        # These are essentially private, but defined as "protected" for the
         # convenience of tests inspecting them
         # convenience of tests inspecting them
+        self._loaded_rrs = 0
         self._zone_class = None
         self._zone_class = None
         self._zone_name = None
         self._zone_name = None
         self._zone_file = None
         self._zone_file = None
@@ -113,6 +114,10 @@ class LoadZoneRunner:
         self._log_severity = 'INFO'
         self._log_severity = 'INFO'
         self._log_debuglevel = 0
         self._log_debuglevel = 0
         self._report_interval = LOAD_INTERVAL_DEFAULT
         self._report_interval = LOAD_INTERVAL_DEFAULT
+        self._start_time = None
+        # This one will be used in (rare) cases where we want to allow tests to
+        # fake time.time()
+        self._get_time = time.time
 
 
         self._config_log()
         self._config_log()
 
 
@@ -200,16 +205,37 @@ class LoadZoneRunner:
         logger.info(LOADZONE_SQLITE3_USING_DEFAULT_CONFIG, default_db_file)
         logger.info(LOADZONE_SQLITE3_USING_DEFAULT_CONFIG, default_db_file)
         return '{"database_file": "' + default_db_file + '"}'
         return '{"database_file": "' + default_db_file + '"}'
 
 
-    def _report_progress(self, loaded_rrs):
+    def _report_progress(self, loaded_rrs, progress, dump=True):
         '''Dump the current progress report to stdout.
         '''Dump the current progress report to stdout.
 
 
         This is essentially private, but defined as "protected" for tests.
         This is essentially private, but defined as "protected" for tests.
+        Normally dump is True, but tests will set it False to get the
+        text to be reported.  Tests may also fake self._get_time (which
+        is set to time.time() by default) and self._start_time for control
+        time related conditions.
 
 
         '''
         '''
-        elapsed = time.time() - self.__start_time
-        sys.stdout.write("\r" + (80 * " "))
-        sys.stdout.write("\r%d RRs loaded in %.2f seconds" %
-                         (loaded_rrs, elapsed))
+        elapsed = self._get_time() - self._start_time
+        speed = int(loaded_rrs / elapsed) if elapsed > 0 else 0
+        etc = None            # calculate estimated time of completion
+        if progress != ZoneLoader.PROGRESS_UNKNOWN:
+            etc = (1 - progress) * (elapsed / progress)
+
+        # Build report text
+        report_txt = '\r%d RRs' % loaded_rrs
+        if progress != ZoneLoader.PROGRESS_UNKNOWN:
+            report_txt += ' (%.1f%%)' % (progress * 100)
+        report_txt += ' in %s, %d RRs/sec' % \
+            (str(timedelta(seconds=int(elapsed))), speed)
+        if etc is not None:
+            report_txt += ', %s ETC' % str(timedelta(seconds=int(etc)))
+
+        # Dump or return the report text.
+        if dump:
+            sys.stdout.write("\r" + (80 * " "))
+            sys.stdout.write(report_txt)
+        else:
+            return report_txt
 
 
     def _do_load(self):
     def _do_load(self):
         '''Main part of the load logic.
         '''Main part of the load logic.
@@ -230,7 +256,7 @@ class LoadZoneRunner:
                             self._zone_class)
                             self._zone_class)
             loader = ZoneLoader(datasrc_client, self._zone_name,
             loader = ZoneLoader(datasrc_client, self._zone_name,
                                 self._zone_file)
                                 self._zone_file)
-            self.__start_time = time.time()
+            self._start_time = time.time()
             if self._report_interval > 0:
             if self._report_interval > 0:
                 limit = self._report_interval
                 limit = self._report_interval
             else:
             else:
@@ -239,17 +265,20 @@ class LoadZoneRunner:
                 limit = LOAD_INTERVAL_DEFAULT
                 limit = LOAD_INTERVAL_DEFAULT
             while (not self.__interrupted and
             while (not self.__interrupted and
                    not loader.load_incremental(limit)):
                    not loader.load_incremental(limit)):
-                self.__loaded_rrs += self._report_interval
+                self._loaded_rrs += self._report_interval
                 if self._report_interval > 0:
                 if self._report_interval > 0:
-                    self._report_progress(self.__loaded_rrs)
+                    self._report_progress(self._loaded_rrs,
+                                          loader.get_progress())
             if self.__interrupted:
             if self.__interrupted:
                 raise LoadFailure('loading interrupted by signal')
                 raise LoadFailure('loading interrupted by signal')
 
 
             # On successful completion, add final '\n' to the progress
             # On successful completion, add final '\n' to the progress
             # report output (on failure don't bother to make it prettier).
             # report output (on failure don't bother to make it prettier).
             if (self._report_interval > 0 and
             if (self._report_interval > 0 and
-                self.__loaded_rrs >= self._report_interval):
+                self._loaded_rrs >= self._report_interval):
                 sys.stdout.write('\n')
                 sys.stdout.write('\n')
+            # record the final count of the loaded RRs for logging
+            self._loaded_rrs = loader.get_rr_count()
         except Exception as ex:
         except Exception as ex:
             # release any remaining lock held in the loader
             # release any remaining lock held in the loader
             loader = None
             loader = None
@@ -273,8 +302,8 @@ class LoadZoneRunner:
             self._set_signal_handlers()
             self._set_signal_handlers()
             self._parse_args()
             self._parse_args()
             self._do_load()
             self._do_load()
-            total_elapsed_txt = "%.2f" % (time.time() - self.__start_time)
-            logger.info(LOADZONE_DONE, self.__loaded_rrs, self._zone_name,
+            total_elapsed_txt = "%.2f" % (time.time() - self._start_time)
+            logger.info(LOADZONE_DONE, self._loaded_rrs, self._zone_name,
                         self._zone_class, total_elapsed_txt)
                         self._zone_class, total_elapsed_txt)
             return 0
             return 0
         except BadArgument as ex:
         except BadArgument as ex:

+ 3 - 8
src/bin/loadzone/loadzone_messages.mes

@@ -27,16 +27,11 @@ LOADZONE_ZONE_CREATED), but the loading operation has subsequently
 failed.  The newly created zone has been removed from the data source,
 failed.  The newly created zone has been removed from the data source,
 so that the data source will go back to the original state.
 so that the data source will go back to the original state.
 
 
-% LOADZONE_DONE Loaded (at least) %1 RRs into zone %2/%3 in %4 seconds
+% LOADZONE_DONE Loaded %1 RRs into zone %2/%3 in %4 seconds
 b10-loadzone has successfully loaded the specified zone.  If there was
 b10-loadzone has successfully loaded the specified zone.  If there was
 an old version of the zone in the data source, it is now deleted.
 an old version of the zone in the data source, it is now deleted.
-It also prints (a lower bound of) the number of RRs that have been loaded
-and the time spent for the loading.  Due to a limitation of the
-current implementation of the underlying library however, it cannot show the
-exact number of the loaded RRs; it's counted for every N-th RR where N
-is the value of the -i command line option.  So, for smaller zones that
-don't even contain N RRs, the reported value will be 0.  This will be
-improved in a future version.
+It also prints the number of RRs that have been loaded
+and the time spent for the loading.
 
 
 % LOADZONE_LOAD_ERROR Failed to load zone %1/%2: %3
 % LOADZONE_LOAD_ERROR Failed to load zone %1/%2: %3
 Loading a zone by b10-loadzone fails for some reason in the middle of
 Loading a zone by b10-loadzone fails for some reason in the middle of

+ 38 - 3
src/bin/loadzone/tests/loadzone_test.py

@@ -142,7 +142,7 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.__runner._datasrc_config = DATASRC_CONFIG
         self.__runner._datasrc_config = DATASRC_CONFIG
         self.__runner._report_interval = 1
         self.__runner._report_interval = 1
         self.__reports = []
         self.__reports = []
-        self.__runner._report_progress = lambda x: self.__reports.append(x)
+        self.__runner._report_progress = lambda x, _: self.__reports.append(x)
 
 
     def __check_zone_soa(self, soa_txt, zone_name=TEST_ZONE_NAME):
     def __check_zone_soa(self, soa_txt, zone_name=TEST_ZONE_NAME):
         """Check that the given SOA RR exists and matches the expected string
         """Check that the given SOA RR exists and matches the expected string
@@ -175,6 +175,7 @@ class TestLoadZoneRunner(unittest.TestCase):
         # be 3 RRs
         # be 3 RRs
         self.assertEqual([1, 2, 3], self.__reports)
         self.assertEqual([1, 2, 3], self.__reports)
         self.__check_zone_soa(NEW_SOA_TXT)
         self.__check_zone_soa(NEW_SOA_TXT)
+        self.assertEqual(3, self.__runner._loaded_rrs)
 
 
     def test_load_update_skipped_report(self):
     def test_load_update_skipped_report(self):
         '''successful loading, with reports for every 2 RRs'''
         '''successful loading, with reports for every 2 RRs'''
@@ -182,6 +183,8 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.__runner._report_interval = 2
         self.__runner._report_interval = 2
         self.__runner._do_load()
         self.__runner._do_load()
         self.assertEqual([2], self.__reports)
         self.assertEqual([2], self.__reports)
+        # total RRs should still be set the actual value
+        self.assertEqual(3, self.__runner._loaded_rrs)
 
 
     def test_load_update_no_report(self):
     def test_load_update_no_report(self):
         '''successful loading, without progress reports'''
         '''successful loading, without progress reports'''
@@ -190,6 +193,36 @@ class TestLoadZoneRunner(unittest.TestCase):
         self.__runner._do_load()
         self.__runner._do_load()
         self.assertEqual([], self.__reports) # no report
         self.assertEqual([], self.__reports) # no report
         self.__check_zone_soa(NEW_SOA_TXT)   # but load is completed
         self.__check_zone_soa(NEW_SOA_TXT)   # but load is completed
+        self.assertEqual(3, self.__runner._loaded_rrs)
+
+    def test_report_progress(self):
+        '''Check the output format of report_progress.
+
+        For some simple scenario and minor corner cases.  We tweak the
+        start and current times so the test results will be predicatble.
+
+        '''
+        # 10 RRs in 10 sec, which is 25% of the entire zone.  it'll take
+        # 30 sec more
+        self.__runner._start_time = 10
+        self.__runner._get_time = lambda: 20
+        self.assertEqual('\r10 RRs (25.0%) in 0:00:10, 1 RRs/sec, ' +
+                         '0:00:30 ETC',
+                         self.__runner._report_progress(10, 0.25, False))
+
+        # start time == current time.  unlikely to happen in practice, but
+        # it shouldn't cause disruption.
+        self.__runner._get_time = lambda: 10
+        self.assertEqual('\r10 RRs (25.0%) in 0:00:00, 0 RRs/sec, ' +
+                         '0:00:00 ETC',
+                         self.__runner._report_progress(10, 0.25, False))
+
+        # progress is unknown
+        self.__runner._get_time = lambda: 20
+        unknown_progress = isc.datasrc.ZoneLoader.PROGRESS_UNKNOWN
+        self.assertEqual('\r10 RRs in 0:00:10, 1 RRs/sec',
+                         self.__runner._report_progress(10, unknown_progress,
+                                                        False))
 
 
     def test_create_and_load(self):
     def test_create_and_load(self):
         '''successful case to loading contents to a new zone (created).'''
         '''successful case to loading contents to a new zone (created).'''
@@ -275,7 +308,8 @@ class TestLoadZoneRunner(unittest.TestCase):
     def test_load_interrupted(self):
     def test_load_interrupted(self):
         '''Load attempt fails due to signal interruption'''
         '''Load attempt fails due to signal interruption'''
         self.__common_load_setup()
         self.__common_load_setup()
-        self.__runner._report_progress = lambda x: self.__interrupt_progress(x)
+        self.__runner._report_progress = \
+            lambda x, _: self.__interrupt_progress(x)
         # The interrupting _report_progress() will terminate the loading
         # The interrupting _report_progress() will terminate the loading
         # in the middle.  the number of reports is smaller, and the zone
         # in the middle.  the number of reports is smaller, and the zone
         # won't be changed.
         # won't be changed.
@@ -290,7 +324,8 @@ class TestLoadZoneRunner(unittest.TestCase):
 
 
         '''
         '''
         self.__common_load_setup()
         self.__common_load_setup()
-        self.__runner._report_progress = lambda x: self.__interrupt_progress(x)
+        self.__runner._report_progress = \
+            lambda x, _: self.__interrupt_progress(x)
         self.__runner._zone_name = Name('example.com')
         self.__runner._zone_name = Name('example.com')
         self.__runner._zone_file = ALT_NEW_ZONE_TXT_FILE
         self.__runner._zone_file = ALT_NEW_ZONE_TXT_FILE
         self.__check_zone_soa(None, zone_name=Name('example.com'))
         self.__check_zone_soa(None, zone_name=Name('example.com'))