rda-python-dsquasar 3.0.11__tar.gz → 3.0.12__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.11/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.12}/PKG-INFO +1 -1
  2. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/pyproject.toml +1 -1
  3. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/dsquasar.py +74 -42
  4. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  5. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/LICENSE +0 -0
  6. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/MANIFEST.in +0 -0
  7. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/README.md +0 -0
  8. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/setup.cfg +0 -0
  9. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/__init__.py +0 -0
  10. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  11. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/dsquasar.usg +0 -0
  12. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/dstacc.py +0 -0
  13. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/taccrec.py +0 -0
  14. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar/tacctar.py +0 -0
  15. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar.egg-info/SOURCES.txt +0 -0
  16. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.11 → rda_python_dsquasar-3.0.12}/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.11
3
+ Version: 3.0.12
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.11"
7
+ version = "3.0.12"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -94,8 +94,9 @@ class DsQuasar(PgCMD, PgSplit):
94
94
  # concurrent transfers, so extra upload processes just wait on it.
95
95
  self.MPBLIMIT = 5000 # tar file count per batch process for uploading
96
96
  self.MPBMAX = 4 # maximum number of batch processes for uploading
97
- self.MAXRUNTIME = 23*3600 # 23 hours; stop before the 24-hour PBS walltime
98
- self.ONEHOUR = 3600 # seconds; time headroom needed to finish before walltime
97
+ self.ONEHOUR = 3600 # seconds; headroom kept to report before the walltime
98
+ self.WALLTIME = 24*3600 # PBS walltime assumed, refreshed from PBS at start of a run
99
+ self.MAXRUNTIME = self.WALLTIME - self.ONEHOUR # cutoff to cache a progress report
99
100
  # a repeat submit normally blocks as a duplicate while the batch job is running. if
100
101
  # that job is running but barely progressing, an extra worker is submitted instead
101
102
  # (see pick_worker_slot). progress is measured as the fraction of the recorded
@@ -119,7 +120,7 @@ class DsQuasar(PgCMD, PgSplit):
119
120
  'dolock' : 1,
120
121
  'doemail' : 0,
121
122
  'starttime' : 0, # wall-clock start of the run, for the PBS walltime guard
122
- 'tardone' : 0, # tar files dispatched so far, for the finish-rate estimate
123
+ 'einfo' : 0, # set once a progress report is cached into dscheck.einfo
123
124
  'maxworkers' : self.MAXWORKERS, # -W, maximum concurrent workers per command
124
125
  'worker' : 1, # -w, this run's worker slot; >1 for an added extra worker
125
126
  'cmd' : None
@@ -195,6 +196,7 @@ class DsQuasar(PgCMD, PgSplit):
195
196
  def start_actions(self):
196
197
  self.cmdlog(self.PGBACK['cmd'])
197
198
  self.PGBACK['starttime'] = tm()
199
+ self.set_walltime_deadline()
198
200
  if self.sopts['u']:
199
201
  if self.sopts['a']: self.pglog("-u: Dataset IDs must be provided to Unlock datasets", self.LOGWRN)
200
202
  self.unlock_datasets()
@@ -236,9 +238,7 @@ class DsQuasar(PgCMD, PgSplit):
236
238
  if self.PGBACK['bckcnt']: title += "({})".format(self.PGBACK['bckcnt'])
237
239
  if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
238
240
  if self.PGLOG['DSCHECK']:
239
- tbl = "dscheck"
240
- cnd = "cindex = {}".format(self.PGLOG['DSCHECK']['cindex'])
241
- self.build_customized_email(tbl, "einfo", cnd, title, self.LOGWRN)
241
+ self.report_dscheck_email(title)
242
242
  else:
243
243
  self.pglog(title, self.LOGWRN|self.SNDEML)
244
244
  if self.PGBACK['pstep']: self.record_dscheck_status("D")
@@ -770,49 +770,83 @@ class DsQuasar(PgCMD, PgSplit):
770
770
  self.set_email("{}: Quit {} for {} Files of {}!".format(self.PGBACK['cmd'], amsg, bmsg, dmsg), self.EMLTOP)
771
771
  title = "dsquasar: Quit {} Error({})".format(amsg, self.PGBACK['errcnt'])
772
772
  if self.PGLOG['DSCHECK']:
773
- tbl = "dscheck"
774
- cnd = "cindex = {}".format(self.PGLOG['DSCHECK']['cindex'])
775
- self.build_customized_email(tbl, "einfo", cnd, title, self.LOGWRN)
773
+ self.report_dscheck_email(title)
776
774
  else:
777
775
  self.pglog(title, self.LOGWRN|self.SNDEML)
778
776
  if self.PGBACK['pstep']: self.record_dscheck_status("F")
779
777
  self.pgexit(0)
780
778
 
781
- # guard the -A 3 (Create Input&Tar, build status 'N' infiles) and -A 4 (Transfer,
782
- # send status 'T' tars) PBS batch jobs against the 24-hour walltime: once past
783
- # MAXRUNTIME, if the tar files still to process cannot finish within the last hour at
784
- # the rate achieved so far, send a progress email and stop cleanly so the report is
785
- # not lost to a PBS timeout; the remaining records are left for the next scheduled run
786
- # to resume. a no-op for command-line runs, other actions, or before the cutoff.
787
- def check_batch_deadline(self, qinfo):
779
+ # a batch run normally gets the full 24 hours asked for at submit time, but the dscheck
780
+ # daemon caps that to the maximum of the PBS queue it lands in - only 6 hours for the
781
+ # default queue - and to any planned system down. read the granted walltime back from
782
+ # PBS so that the run reports its progress with an hour to spare whatever the limit
783
+ # turns out to be, and keep the assumed 24 hours for a command-line run or when PBS
784
+ # cannot tell us.
785
+ def set_walltime_deadline(self):
786
+ if self.PGLOG['CURBID'] < 1: return
787
+ stat = self.get_pbs_info(str(self.PGLOG['CURBID']), 0, self.LOGWRN)
788
+ ms = re.match(r'^(\d+):(\d+)(?::(\d+))?$', stat['ReqdTime']) if stat.get('ReqdTime') else None
789
+ if not ms:
790
+ self.pglog("Cannot read the PBS walltime of Job {}; assume {}".format(
791
+ self.PGLOG['CURBID'], self.seconds_to_string_time(self.WALLTIME)), self.LOGWRN)
792
+ return
793
+ wtime = 3600*int(ms.group(1)) + 60*int(ms.group(2)) + (int(ms.group(3)) if ms.group(3) else 0)
794
+ if wtime <= self.ONEHOUR: return # too short to keep the reporting headroom
795
+ self.WALLTIME = wtime
796
+ self.MAXRUNTIME = wtime - self.ONEHOUR
797
+ self.pglog("PBS Job {} walltime {}: report progress after {}".format(
798
+ self.PGLOG['CURBID'], self.seconds_to_string_time(self.WALLTIME),
799
+ self.seconds_to_string_time(self.MAXRUNTIME)), self.LOGWRN)
800
+
801
+ # email a report for a run under dscheck control. the report is normally sent right
802
+ # away, but is cached into dscheck.einfo instead if a progress report is cached there
803
+ # already, so that the final report replaces it; the dscheck daemon sends and clears
804
+ # einfo once the check record is unlocked.
805
+ def report_dscheck_email(self, title, cache = 0):
806
+ cnd = "cindex = {}".format(self.PGLOG['DSCHECK']['cindex'])
807
+ if not (cache or self.PGBACK['einfo']):
808
+ return self.build_customized_email("dscheck", "einfo", cnd, title, self.LOGWRN)
809
+ msg = self.get_email()
810
+ if not msg: return self.FAILURE
811
+ sender = self.PGLOG['CURUID'] + "@ucar.edu"
812
+ receiver = self.PGLOG['EMLADDR'] if self.PGLOG['EMLADDR'] else sender
813
+ if receiver.find(sender) < 0: self.add_carbon_copy(sender, 1)
814
+ ebuf = "From: {}\nTo: {}\n".format(sender, receiver)
815
+ if self.PGLOG['CCDADDR']: ebuf += "Cc: {}\n".format(self.PGLOG['CCDADDR'])
816
+ ebuf += "Subject: {}!\n\n{}\n".format(title, msg)
817
+ self.PGBACK['einfo'] = 1
818
+ return self.cache_customized_email("dscheck", "einfo", cnd, ebuf, self.LOGWRN)
819
+
820
+ # guard the long PBS batch jobs (-A 2/3/4/6) against the walltime: once past MAXRUNTIME
821
+ # the run keeps going, but a progress report is cached into dscheck.einfo so an email is
822
+ # still sent, by the dscheck daemon, if PBS kills the job at the walltime. the final
823
+ # report replaces it if the run does finish in time. the live email buffers are saved
824
+ # and put back, so the final report still carries everything logged before the cutoff.
825
+ # both queue depths are reported rather than the current phase's: -A 3 and -A 6 run the
826
+ # build and the transfer phase in one run, and -A 3 tars each input file right after
827
+ # creating it, so its status 'N' count stays near zero while that is the busy phase.
828
+ # a no-op for command-line runs, other actions, before the cutoff, or once a progress
829
+ # report is cached.
830
+ def check_batch_deadline(self):
788
831
  act = self.PGBACK['action']
789
- if self.PGLOG['CURBID'] < 1 or act not in (self.CTACTS, self.BCKACT): return
832
+ if self.PGBACK['einfo'] or not (self.PGBACK['doemail'] and self.PGLOG['DSCHECK']): return
833
+ if self.PGLOG['CURBID'] < 1 or act not in (self.TARACT, self.CTACTS, self.BCKACT, self.TBACTS): return
790
834
  elapsed = tm() - self.PGBACK['starttime']
791
835
  if elapsed < self.MAXRUNTIME: return
792
- status, work = ('N', 'build') if act == self.CTACTS else ('T', 'transfer')
793
- remaining = self.batch_tar_count(status)
794
- if remaining < 1: return
795
- done = self.PGBACK['tardone']
796
- need = remaining*elapsed/done if done > 0 else elapsed
797
- if need <= self.ONEHOUR: return # enough time left to finish before the walltime
798
- if self.PGBACK['mproc'] > 1: self.check_child(None, 0, self.LOGWRN, 1) # wait all children
799
- if qinfo and qinfo.get('dslocks'):
800
- for dsid in qinfo['dslocks']: self.lock_dataset(dsid, 0, self.LGEREX)
836
+ tcnt = self.batch_tar_count('N')
837
+ bcnt = self.batch_tar_count('T')
801
838
  etime = self.seconds_to_string_time(int(elapsed))
802
839
  amsg = self.ACTMSG[act]
803
- msg = "{}: Stopped after {} with {} tar file(s) still to {} - not enough time to finish before the 24-hour PBS walltime; the next scheduled run will resume".format(amsg, etime, remaining, work)
840
+ bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
841
+ rmsg = "{} {} tar file(s) left to build and {} to transfer".format(tcnt, bmsg, bcnt)
842
+ msg = "{}: Still running after {} of the {} PBS walltime, with {}".format(amsg, etime, self.seconds_to_string_time(self.WALLTIME), rmsg)
804
843
  self.pglog(self.INDENT + msg, self.LOGACT)
805
- if self.PGBACK['doemail']:
806
- bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
807
- self.set_email("{}: {} - stopped early before the PBS walltime with {} {} tar file(s) remaining!".format(self.PGBACK['cmd'], amsg, remaining, bmsg), self.EMLTOP)
808
- title = "dsquasar: {} stopped early ({} remaining)".format(amsg, remaining)
809
- if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
810
- if self.PGLOG['DSCHECK']:
811
- self.build_customized_email("dscheck", "einfo", "cindex = {}".format(self.PGLOG['DSCHECK']['cindex']), title, self.LOGWRN)
812
- else:
813
- self.pglog(title, self.LOGWRN|self.SNDEML)
814
- if self.PGBACK['pstep']: self.record_dscheck_status("D")
815
- self.pgexit(0)
844
+ saved = {key : self.PGLOG[key] for key in ('EMLMSG', 'ERRMSG', 'ERRCNT', 'SUMMSG', 'PRGMSG')}
845
+ self.set_email("{}: {} still in progress after {}, with {}!".format(self.PGBACK['cmd'], amsg, etime, rmsg), self.EMLTOP)
846
+ title = "dsquasar: {} in progress ({} to build, {} to transfer)".format(amsg, tcnt, bcnt)
847
+ if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
848
+ self.report_dscheck_email(title, 1)
849
+ self.PGLOG.update(saved)
816
850
 
817
851
  # recompute the confirmed backup counts from RDADB after all child processes
818
852
  # finished, so a multi-process summary reflects succeeded (not just started) work
@@ -850,7 +884,7 @@ class DsQuasar(PgCMD, PgSplit):
850
884
  def process_one_backup_file(self, qinfo, addback, keepid = False):
851
885
  ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
852
886
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
853
- self.check_batch_deadline(qinfo)
887
+ self.check_batch_deadline()
854
888
  dsids = qinfo['dsids']
855
889
  dcnt = len(dsids)
856
890
  if dcnt == 0: return
@@ -908,7 +942,6 @@ class DsQuasar(PgCMD, PgSplit):
908
942
  if stat:
909
943
  # reset qinfo after quasar backup
910
944
  qinfo['qcnt'] += 1
911
- self.PGBACK['tardone'] += 1 # cumulative dispatched tars for the walltime finish-rate
912
945
  qinfo['qfcnt'] += fcnt
913
946
  qinfo['qsize'] += fsize
914
947
  for dsid in dsids:
@@ -931,7 +964,7 @@ class DsQuasar(PgCMD, PgSplit):
931
964
  def transfer_quasar_tarfiles(self, qinfo):
932
965
  ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
933
966
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
934
- self.check_batch_deadline(qinfo)
967
+ self.check_batch_deadline()
935
968
  # prepare for backup one tar file
936
969
  dsids = qinfo['dsids']
937
970
  bids = qinfo['bids']
@@ -985,7 +1018,6 @@ class DsQuasar(PgCMD, PgSplit):
985
1018
  if dstat and bstat:
986
1019
  # reset qinfo after quasar backup
987
1020
  qinfo['qcnt'] += bcnt
988
- self.PGBACK['tardone'] += bcnt # cumulative transferred tars for the walltime finish-rate
989
1021
  qinfo['qfcnt'] += fcnt
990
1022
  qinfo['qsize'] += fsize
991
1023
  for dsid in dsids:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.11
3
+ Version: 3.0.12
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