rda-python-dsquasar 3.0.19__tar.gz → 3.0.21__tar.gz

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.
Files changed (20) hide show
  1. {rda_python_dsquasar-3.0.19/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.21}/PKG-INFO +1 -1
  2. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/pyproject.toml +1 -1
  3. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/dsquasar.py +109 -32
  4. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  5. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/LICENSE +0 -0
  6. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/MANIFEST.in +0 -0
  7. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/README.md +0 -0
  8. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/setup.cfg +0 -0
  9. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/__init__.py +0 -0
  10. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  11. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/dsquasar.usg +0 -0
  12. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/dstacc.py +0 -0
  13. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/taccrec.py +0 -0
  14. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar/tacctar.py +0 -0
  15. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar.egg-info/SOURCES.txt +0 -0
  16. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.19 → rda_python_dsquasar-3.0.21}/test/test_dsquasar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.19
3
+ Version: 3.0.21
4
4
  Summary: RDA Python package to backup and recover RDA data archives to and from GLOBUS Quasar backup server
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-dsquasar
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rda_python_dsquasar"
7
- version = "3.0.19"
7
+ version = "3.0.21"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -96,6 +96,7 @@ class DsQuasar(PgCMD, PgSplit):
96
96
  self.MPBLIMIT = 5000 # tar file count per batch process for uploading
97
97
  self.MPBMAX = 4 # maximum number of batch processes for uploading
98
98
  self.ONEHOUR = 3600 # seconds; headroom kept to report before the walltime
99
+ self.ERETRY = 600 # seconds to wait before retrying a failed progress report
99
100
  self.WALLTIME = 24*3600 # PBS walltime assumed, refreshed from PBS at start of a run
100
101
  self.MAXRUNTIME = self.WALLTIME - self.ONEHOUR # cutoff to cache a progress report
101
102
  # a repeat submit normally blocks as a duplicate while the batch job is running. if
@@ -124,6 +125,7 @@ class DsQuasar(PgCMD, PgSplit):
124
125
  'doemail' : 0,
125
126
  'starttime' : 0, # wall-clock start of the run, for the PBS walltime guard
126
127
  'einfo' : 0, # set once a progress report is cached into dscheck.einfo
128
+ 'eretry' : 0, # earliest retry time after a progress report failed to cache
127
129
  'maxworkers' : self.MAXWORKERS, # -W, maximum concurrent workers per command
128
130
  'worker' : 1, # -w, this run's worker slot; >1 for an added extra worker
129
131
  'cmd' : None
@@ -200,6 +202,7 @@ class DsQuasar(PgCMD, PgSplit):
200
202
  self.cmdlog(self.PGBACK['cmd'])
201
203
  self.PGBACK['starttime'] = tm()
202
204
  self.set_walltime_deadline()
205
+ self.catch_term_signal(self.batch_term_report)
203
206
  if self.sopts['u']:
204
207
  if self.sopts['a']: self.pglog("-u: Dataset IDs must be provided to Unlock datasets", self.LOGWRN)
205
208
  self.unlock_datasets()
@@ -238,7 +241,7 @@ class DsQuasar(PgCMD, PgSplit):
238
241
  self.PGLOG['ERRCNT'] or self.PGBACK['einfo']):
239
242
  self.PGBACK['doemail'] = 0
240
243
  if self.PGBACK['doemail']:
241
- amsg = self.ACTMSG[self.PGBACK['action']]
244
+ amsg = self.action_message()
242
245
  bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
243
246
  dcnt = len(self.dsids)
244
247
  dmsg = self.dsids[0] if dcnt == 1 else "{} datasets".format(dcnt if dcnt > 1 else 'All')
@@ -791,7 +794,7 @@ class DsQuasar(PgCMD, PgSplit):
791
794
  # a second report, drop the progress report parked in dscheck.einfo by the parent, and
792
795
  # mark the shared check record failed while the parent is still working
793
796
  if self.PGSIG['PPID'] > 1: sys.exit(1)
794
- if self.PGBACK['mproc'] > 1: self.check_child(None, 0, self.LOGWRN, 1)
797
+ self.wait_all_children()
795
798
  if qinfo:
796
799
  if 'dslocks' in qinfo and qinfo['dslocks']:
797
800
  for dsid in qinfo['dslocks']: self.lock_dataset(dsid, 0, self.LGEREX)
@@ -799,7 +802,7 @@ class DsQuasar(PgCMD, PgSplit):
799
802
  dcnt = len(qinfo['qdsids'])
800
803
  fcnt = qinfo['qfcnt']
801
804
  ssize = self.format_float_value(qinfo['qsize'])
802
- amsg = self.ACTMSG[self.PGBACK['action']]
805
+ amsg = self.action_message()
803
806
  bmsg = self.BACKMSG[qinfo['backflag']]
804
807
  dmsg = qinfo['qdsids'][0] if dcnt == 1 else "{} datasets".format(dcnt)
805
808
  msg = "Quit {}: {} {} files for {}({}) files of {}".format(amsg, qcnt, bmsg, fcnt, ssize, dmsg)
@@ -815,6 +818,13 @@ class DsQuasar(PgCMD, PgSplit):
815
818
  if self.PGBACK['pstep']: self.record_dscheck_status("F")
816
819
  self.pgexit(0)
817
820
 
821
+ # ACTMSG carries no wording for the hidden actions (-A 32/64), so name those by number
822
+ # rather than raise a KeyError: a report that cannot be worded is still a report that
823
+ # has to go out, and crashing while building it loses the run as silently as a kill does
824
+ def action_message(self, act = None):
825
+ if act is None: act = self.PGBACK['action']
826
+ return self.ACTMSG[act] if act in self.ACTMSG else "Action {}".format(act)
827
+
818
828
  # a batch run normally gets the full 24 hours asked for at submit time, but the dscheck
819
829
  # daemon caps that to the maximum of the PBS queue it lands in - only 6 hours for the
820
830
  # default queue - and to any planned system down. read the granted walltime back from
@@ -830,13 +840,41 @@ class DsQuasar(PgCMD, PgSplit):
830
840
  self.PGLOG['CURBID'], self.seconds_to_string_time(self.WALLTIME)), self.LOGWRN)
831
841
  return
832
842
  wtime = 3600*int(ms.group(1)) + 60*int(ms.group(2)) + (int(ms.group(3)) if ms.group(3) else 0)
833
- if wtime <= self.ONEHOUR: return # too short to keep the reporting headroom
843
+ if wtime <= self.ONEHOUR: # too short to keep the reporting headroom
844
+ self.pglog("PBS Job {} walltime {} is too short to report progress ahead of; assume {}".format(
845
+ self.PGLOG['CURBID'], self.seconds_to_string_time(wtime),
846
+ self.seconds_to_string_time(self.WALLTIME)), self.LOGWRN)
847
+ return
834
848
  self.WALLTIME = wtime
835
849
  self.MAXRUNTIME = wtime - self.ONEHOUR
836
850
  self.pglog("PBS Job {} walltime {}: report progress after {}".format(
837
851
  self.PGLOG['CURBID'], self.seconds_to_string_time(self.WALLTIME),
838
852
  self.seconds_to_string_time(self.MAXRUNTIME)), self.LOGWRN)
839
853
 
854
+ # park a progress report for a run PBS is about to kill. catch_term_signal() arms this
855
+ # for a batch run only, restores the default handler before calling it, and re-raises
856
+ # the signal afterwards, so all this has to do is build the report.
857
+ # it runs on borrowed time (SIGKILL follows SIGTERM by the MoM's kill_delay, 10 seconds
858
+ # by default), so it does only what it must: no tar queue counts (two database queries
859
+ # we may not get to finish - the done count comes from memory), no dataset unlocking
860
+ # (the dscheck daemon already cleans up after a dead pid).
861
+ # the report may have to be written from inside an interrupted database call; that is
862
+ # why it goes through report_dscheck_email, whose cache_customized_email falls back to
863
+ # sending the mail directly when the UPDATE fails.
864
+ def batch_term_report(self, signum, frame):
865
+ if self.PGBACK['einfo'] or not (self.PGBACK['doemail'] and self.PGLOG['DSCHECK']): return
866
+ etime = self.seconds_to_string_time(int(tm() - self.PGBACK['starttime']))
867
+ amsg = self.action_message()
868
+ dmsg = self.batch_done_count()
869
+ wmsg = self.seconds_to_string_time(self.WALLTIME)
870
+ self.pglog(self.INDENT + "{}: Terminated by signal {} after {} of the {} PBS walltime, {}".format(
871
+ amsg, signum, etime, wmsg, dmsg), self.LOGACT)
872
+ self.set_email("{}: {} terminated after {} of the {} PBS walltime, {}!".format(
873
+ self.PGBACK['cmd'], amsg, etime, wmsg, dmsg), self.EMLTOP)
874
+ title = "dsquasar: {} terminated ({})".format(amsg, dmsg)
875
+ if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
876
+ self.report_dscheck_email(title, 1)
877
+
840
878
  # email a report for a run under dscheck control. the progress report is parked in
841
879
  # dscheck.einfo and the final report is sent right away. the run holds the check lock
842
880
  # while it works, and the dscheck daemon only mails unlocked records, so a parked
@@ -857,8 +895,15 @@ class DsQuasar(PgCMD, PgSplit):
857
895
  ebuf = "From: {}\nTo: {}\n".format(sender, receiver)
858
896
  if self.PGLOG['CCDADDR']: ebuf += "Cc: {}\n".format(self.PGLOG['CCDADDR'])
859
897
  ebuf += "Subject: {}!\n\n{}\n".format(title, msg)
860
- self.PGBACK['einfo'] = 1 # report the progress only once per run
861
- return self.cache_customized_email("dscheck", "einfo", cnd, ebuf, self.LOGWRN)
898
+ # only a report that is actually parked (or sent directly by the fallback) counts
899
+ # as reported. marking it reported before the attempt turned a failed cache into a
900
+ # silent loss: nothing was parked, and the flag stopped every later attempt too
901
+ estat = self.cache_customized_email("dscheck", "einfo", cnd, ebuf, self.LOGWRN)
902
+ if estat:
903
+ self.PGBACK['einfo'] = 1 # report the progress only once per run
904
+ else:
905
+ self.PGBACK['eretry'] = tm() + self.ERETRY
906
+ return estat
862
907
  estat = self.build_customized_email("dscheck", "einfo", cnd, title, self.LOGWRN)
863
908
  # a failed send has already replaced the parked report via cache_customized_email()
864
909
  if estat == self.SUCCESS and self.PGBACK['einfo']:
@@ -868,40 +913,48 @@ class DsQuasar(PgCMD, PgSplit):
868
913
  self.pglog("Cannot clean the progress report of {}; it is emailed again".format(cnd), self.LOGWRN)
869
914
  return estat
870
915
 
871
- # guard the long PBS batch jobs (-A 2/3/4/6/7) against the walltime: once past MAXRUNTIME
872
- # the run keeps going, but a progress report is parked in dscheck.einfo so that
873
- # something is reported even if PBS kills the job at the walltime. the final report
874
- # follows and drops the parked one if the run does finish in time. the live email
875
- # buffers are saved and put back, so the final report still carries everything logged
876
- # before the cutoff.
877
- # both queue depths are reported rather than the current phase's, because -A 3 tars each
878
- # input file right after creating it, so its status 'N' count stays near zero while that
879
- # is the busy phase. they are worded as STATES ('left to build', 'tarred') and not as
880
- # work this run will do: only -A 4 and -A 6 transfer, so calling the status 'T' count
881
- # 'to transfer' in an -A 2 or -A 3 report claims work that run never performs.
882
- # a no-op for command-line runs, other actions, before the cutoff, or once a progress
883
- # report is cached.
916
+ # guard a long PBS batch job against the walltime: once past MAXRUNTIME the run keeps
917
+ # going, but a progress report is parked in dscheck.einfo so that something is reported
918
+ # even if PBS kills the job at the walltime. the final report follows and drops the
919
+ # parked one if the run does finish in time. the live email buffers are saved and put
920
+ # back, so the final report still carries everything logged before the cutoff.
921
+ # PBS sends no catchable warning before the kill - SIGTERM is not trapped and SIGKILL
922
+ # cannot be - so this parked report is the only thing standing between a killed job and
923
+ # a run that is never heard from. every action submitted to PBS is guarded, not just the
924
+ # tar and transfer ones: -A 16 and the hidden -A 128 are submitted with the same 24 hour
925
+ # walltime and used to be excluded, so they died silently.
926
+ # the tar queue depths are only reported for the actions they describe. both depths are
927
+ # reported rather than the current phase's, because -A 3 tars each input file right after
928
+ # creating it, so its status 'N' count stays near zero while that is the busy phase. they
929
+ # are worded as STATES ('left to build', 'tarred') and not as work this run will do: only
930
+ # -A 4 and -A 6 transfer, so calling the status 'T' count 'to transfer' in an -A 2 or
931
+ # -A 3 report claims work that run never performs.
932
+ # a no-op for command-line runs, before the cutoff, or once a progress report is cached.
884
933
  # called from the top of every per-item loop, not only where a tar is dispatched: a long
885
934
  # stretch of files that are skipped (already backed up, or not enough accumulated size to
886
935
  # tar yet) would otherwise walk past the cutoff without ever reaching the check, which is
887
936
  # how a 23h run reported nothing.
888
937
  def check_batch_deadline(self):
889
- act = self.PGBACK['action']
890
938
  if self.PGBACK['einfo'] or not (self.PGBACK['doemail'] and self.PGLOG['DSCHECK']): return
891
- if self.PGLOG['CURBID'] < 1 or act not in (self.TARACT, self.CTACTS, self.BCKACT, self.TBACTS, self.CBACTS): return
939
+ if self.PGLOG['CURBID'] < 1: return # only a PBS batch run has a walltime to beat
892
940
  elapsed = tm() - self.PGBACK['starttime']
893
941
  if elapsed < self.MAXRUNTIME: return
894
- tcnt = self.batch_tar_count('N')
895
- bcnt = self.batch_tar_count('T')
942
+ # a report that could not be parked is retried, but not once per file: the counts
943
+ # below are database queries and the cutoff leaves a whole hour of them otherwise
944
+ if self.PGBACK['eretry'] and tm() < self.PGBACK['eretry']: return
945
+ act = self.PGBACK['action']
896
946
  etime = self.seconds_to_string_time(int(elapsed))
897
- amsg = self.ACTMSG[act]
898
- bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
947
+ amsg = self.action_message(act)
899
948
  dmsg = self.batch_done_count()
900
- rmsg = "{} {} tar file(s) left to build and {} tarred".format(tcnt, bmsg, bcnt)
901
- msg = "{}: Still running after {} of the {} PBS walltime, {}, with {}".format(amsg, etime, self.seconds_to_string_time(self.WALLTIME), dmsg, rmsg)
949
+ rmsg = ''
950
+ if act&self.CBACTS: # tar queue depths say nothing about the other actions
951
+ bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
952
+ rmsg = ", with {} {} tar file(s) left to build and {} tarred".format(
953
+ self.batch_tar_count('N'), bmsg, self.batch_tar_count('T'))
954
+ msg = "{}: Still running after {} of the {} PBS walltime, {}{}".format(amsg, etime, self.seconds_to_string_time(self.WALLTIME), dmsg, rmsg)
902
955
  self.pglog(self.INDENT + msg, self.LOGACT)
903
956
  saved = {key : self.PGLOG[key] for key in ('EMLMSG', 'ERRMSG', 'ERRCNT', 'SUMMSG', 'PRGMSG')}
904
- self.set_email("{}: {} still in progress after {}, {}, with {}!".format(self.PGBACK['cmd'], amsg, etime, dmsg, rmsg), self.EMLTOP)
957
+ self.set_email("{}: {} still in progress after {}, {}{}!".format(self.PGBACK['cmd'], amsg, etime, dmsg, rmsg), self.EMLTOP)
905
958
  title = "dsquasar: {} in progress ({})".format(amsg, dmsg)
906
959
  if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
907
960
  self.report_dscheck_email(title, 1)
@@ -921,6 +974,26 @@ class DsQuasar(PgCMD, PgSplit):
921
974
  self.show_wait_message(i, "{}: wait child processes".format(self.PGSIG['DSTR']), self.LOGWRN, 1)
922
975
  i += 1
923
976
 
977
+ # wait for a free child process slot, checking the walltime deadline between polls.
978
+ # check_child(..., -1) does the waiting inside its own loop too, breaking only once fewer
979
+ # than MPROC children are left running, so a parent whose children each tar many GB sits
980
+ # there for as long as every slot stays busy - well past the cutoff - while the deadline
981
+ # check right below the call is never reached. That is the other half of the problem
982
+ # wait_all_children() fixed: that one guarded the terminal wait, this one guards the far
983
+ # more frequent wait for a slot. Polling with dowait 0 keeps check_child's own cadence and
984
+ # wait message, and returns the free slot count exactly as check_child(..., -1) does.
985
+ def wait_child_slot(self):
986
+ if self.PGBACK['mproc'] < 2: return 0
987
+ i = 0
988
+ while True:
989
+ self.check_batch_deadline()
990
+ pcnt = self.check_child(None, 0, self.LOGWRN, 0)
991
+ ccnt = self.PGSIG['MPROC'] - pcnt
992
+ if ccnt > 0: return ccnt
993
+ self.show_wait_message(i, "{}: wait {}/{} child processes".format(
994
+ self.PGSIG['DSTR'], pcnt, self.PGSIG['MPROC']), self.LOGWRN, 1)
995
+ i += 1
996
+
924
997
  # recompute the confirmed backup counts from RDADB after all child processes
925
998
  # finished, so a multi-process summary reflects succeeded (not just started) work
926
999
  def confirm_quasar_counts(self, qinfo, bids, dcnd):
@@ -955,7 +1028,7 @@ class DsQuasar(PgCMD, PgSplit):
955
1028
  # backup one Quasar Backup or Backup&Drdata from one or multiple inputs and,
956
1029
  # reset the quasar backup dict
957
1030
  def process_one_backup_file(self, qinfo, addback, keepid = False):
958
- ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
1031
+ ccnt = self.wait_child_slot()
959
1032
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
960
1033
  self.check_batch_deadline()
961
1034
  dsids = qinfo['dsids']
@@ -1035,7 +1108,7 @@ class DsQuasar(PgCMD, PgSplit):
1035
1108
  # Transfer multiple tarfiles to Quasar Backup or Backup&Drdata, and
1036
1109
  # reset the quasar backup dict
1037
1110
  def transfer_quasar_tarfiles(self, qinfo):
1038
- ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
1111
+ ccnt = self.wait_child_slot()
1039
1112
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
1040
1113
  self.check_batch_deadline()
1041
1114
  # prepare for backup one tar file
@@ -1623,6 +1696,7 @@ class DsQuasar(PgCMD, PgSplit):
1623
1696
  qinfo = {'backflag' : backflag, 'bid' : 0, 'dsid' : None, 'size' : 0, 'bfile' : None,
1624
1697
  'bqfiles' : {}, 'dqfiles' : {}, 'qdsids' : [], 'qcnt' : 0, 'ncnt' : 0}
1625
1698
  for bid in bfiles:
1699
+ self.check_batch_deadline()
1626
1700
  qinfo['bid'] = bid
1627
1701
  binfo = bfiles[bid]
1628
1702
  if qinfo['dsid'] and binfo['dsid'] != qinfo['dsid']:
@@ -1741,12 +1815,13 @@ class DsQuasar(PgCMD, PgSplit):
1741
1815
  qinfo = {'backflag' : backflag, 'bid' : 0, 'dsids' : [], 'fcnt' : 0, 'size' : 0,
1742
1816
  'bfile' : None, 'pfile' : None, 'qdsids' : [], 'qfcnt' : 0, 'qsize' : 0, 'qcnt' : 0}
1743
1817
  for bid in bfiles:
1818
+ self.check_batch_deadline()
1744
1819
  qinfo['bid'] = bid
1745
1820
  binfo = bfiles[bid]
1746
1821
  for bkey in binfo: qinfo[bkey] = binfo[bkey]
1747
1822
  self.process_one_quasar_pathfile(qinfo)
1748
1823
  if self.PGBACK['mproc'] > 1:
1749
- self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
1824
+ self.wait_all_children() # wait all child processes done
1750
1825
  self.confirm_quasar_counts(qinfo, list(bfiles), "bfile LIKE 'G%/%.tar'") # recount confirmed renames from RDADB
1751
1826
  qcnt = qinfo['qcnt']
1752
1827
  if qcnt > 0:
@@ -1761,7 +1836,7 @@ class DsQuasar(PgCMD, PgSplit):
1761
1836
  # backup one Quasar Backup or Backup&Drdata from one or multiple inputs and,
1762
1837
  # reset the quasar backup dict
1763
1838
  def process_one_quasar_pathfile(self, qinfo):
1764
- ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
1839
+ ccnt = self.wait_child_slot()
1765
1840
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
1766
1841
  # prepare for backup one tar file
1767
1842
  dsids = qinfo['dsids']
@@ -1883,6 +1958,7 @@ class DsQuasar(PgCMD, PgSplit):
1883
1958
  qinfo = {'backflag' : backflag, 'bid' : 0, 'dsids' : [], 'fcnt' : 0, 'size' : 0,
1884
1959
  'qdsids' : [], 'qfcnt' : 0, 'qsize' : 0, 'qcnt' : 0}
1885
1960
  for bid in bfiles:
1961
+ self.check_batch_deadline()
1886
1962
  qinfo['bid'] = bid
1887
1963
  binfo = bfiles[bid]
1888
1964
  for bkey in binfo: qinfo[bkey] = binfo[bkey]
@@ -1981,6 +2057,7 @@ class DsQuasar(PgCMD, PgSplit):
1981
2057
  qinfo = {'dsids' : [], 'fcnt' : 0, 'mcnt' : 0, 'fsize' : 0, 'msize' : 0}
1982
2058
  qcnt = 0
1983
2059
  for bid in bfiles:
2060
+ self.check_batch_deadline()
1984
2061
  qcnt += self.process_one_quasar_mcsfile(bid, bfiles[bid], qinfo)
1985
2062
  if qcnt > 0:
1986
2063
  s = 's' if qcnt > 1 else ''
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.19
3
+ Version: 3.0.21
4
4
  Summary: RDA Python package to backup and recover RDA data archives to and from GLOBUS Quasar backup server
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-dsquasar