rda-python-dsquasar 3.0.22__tar.gz → 3.0.24__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.22/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.24}/PKG-INFO +1 -1
  2. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/pyproject.toml +1 -1
  3. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/dsquasar.py +48 -10
  4. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/dsquasar.usg +3 -2
  5. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/dstacc.py +39 -6
  6. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  7. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/LICENSE +0 -0
  8. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/MANIFEST.in +0 -0
  9. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/README.md +0 -0
  10. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/setup.cfg +0 -0
  11. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/__init__.py +0 -0
  12. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  13. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/taccrec.py +0 -0
  14. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar/tacctar.py +0 -0
  15. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar.egg-info/SOURCES.txt +0 -0
  16. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.22 → rda_python_dsquasar-3.0.24}/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.22
3
+ Version: 3.0.24
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.22"
7
+ version = "3.0.24"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -83,6 +83,8 @@ class DsQuasar(PgCMD, PgSplit):
83
83
  self.TARSIZE = 5*self.PGLOG['ONEGBS'] # 5GB, tarfile size limit
84
84
  self.MINSIZE = self.PGLOG['TWOGBS'] # 2GB, minimal tarfile size
85
85
  self.ONESIZE = 20*self.PGLOG['ONEGBS'] # 20GB, minimal file size to tar a single file
86
+ self.MISSLOG = 10 # files named in the log per dataset when missing from storage
87
+ self.MISSCNT = 0 # files of the current dataset missing from storage
86
88
  self.TFCOUNT = 100 # if file count is greater, use self.MINSIZE for tar file
87
89
  self.SUBLMTS = 2000 # file count limit for a sub-group
88
90
  # tarring (TARACT) forks one child per tar file, so its parallel unit is the tar
@@ -105,7 +107,7 @@ class DsQuasar(PgCMD, PgSplit):
105
107
  # dscheck work done, which is action independent because dcount counts GDEX files
106
108
  # for -A 3 but tar files for -A 2/-A 4.
107
109
  self.MAXWORKERS = 2 # default maximum concurrent workers per command (-W)
108
- self.WORKGRACE = 6*3600 # 6 hours before a running job is judged on progress
110
+ self.WORKGRACE = 2*3600 # 2 hours before a running job is judged on progress
109
111
  self.MINWPROJ = 0.9 # projected done fraction at the walltime that counts as finishing
110
112
  self.PGBACK = {
111
113
  'workdir' : "{}/{}/quasar_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
@@ -431,8 +433,9 @@ class DsQuasar(PgCMD, PgSplit):
431
433
  dargv = dargv[0:100]
432
434
  return self.get_dscheck("dsquasar", dargv, self.PGBACK['workdir'], self.PGLOG['CURUID'], dargx, self.LOGWRN)
433
435
 
434
- # a running batch job counts as stalled once it has run longer than WORKGRACE and its
435
- # progress so far projects to less than MINWPROJ of its recorded work by the walltime:
436
+ # a running batch job counts as stalled once it has run longer than WORKGRACE, has
437
+ # recorded some work done, and its progress so far projects to less than MINWPROJ of
438
+ # its recorded work by the walltime:
436
439
  # such a job cannot finish this attempt however long it is left alone, so an extra
437
440
  # worker is what helps it, not more retries.
438
441
  # the test is on the RATE, not on an absolute done fraction as it first was. a fixed
@@ -447,11 +450,15 @@ class DsQuasar(PgCMD, PgSplit):
447
450
  # runs this and set_walltime_deadline reads the granted walltime back in the batch job.
448
451
  # when the daemon caps it to a shorter queue limit the projection is optimistic, which
449
452
  # errs towards leaving the job alone.
453
+ # a job that has recorded nothing done is still in its gather phases, which read as a
454
+ # zero rate; it is left for the next submit rather than judged. that guard is what lets
455
+ # WORKGRACE stay short: an extra worker can only be added by a cron submit, every 4
456
+ # hours, so a 6 hour grace kept it out of the first 6 to 10 hours of every attempt.
450
457
  def worker_progress_stalled(self, pgrec):
451
458
  if not (pgrec['pid'] and pgrec['stttime']): return False
452
459
  if self.check_host_pid(pgrec['lockhost'], pgrec['pid']) <= 0: return False
453
460
  elapsed = tm() - pgrec['stttime']
454
- if elapsed < self.WORKGRACE: return False
461
+ if elapsed < self.WORKGRACE or not pgrec['dcount']: return False
455
462
  done = pgrec['dcount']/pgrec['fcount'] if pgrec['fcount'] else 0
456
463
  return (done*self.WALLTIME/elapsed) < self.MINWPROJ
457
464
 
@@ -1220,6 +1227,7 @@ class DsQuasar(PgCMD, PgSplit):
1220
1227
  fd = None
1221
1228
  fcate = filetype.lower()
1222
1229
  tname = fcate + 'file'
1230
+ self.MISSCNT = 0 # files missing from their storage location
1223
1231
  for i in range(fcnt):
1224
1232
  self.check_batch_deadline()
1225
1233
  pgrec = recs[i]
@@ -1244,6 +1252,12 @@ class DsQuasar(PgCMD, PgSplit):
1244
1252
  fd = None
1245
1253
  self.process_one_backup_file(qinfo, True, (fcnt-i) > 1)
1246
1254
  if fd: fd.close()
1255
+ mcnt = self.MISSCNT
1256
+ if mcnt:
1257
+ # one line per dataset reaches the email; the first file names are in the log
1258
+ s = 's' if mcnt > 1 else ''
1259
+ ftype = 'Saved' if fcate == 's' else 'Web'
1260
+ self.pglog("{}: {} of {} {} file{} missing from storage, not backed up".format(dsid, mcnt, fcnt, ftype, s), self.LOGACT)
1247
1261
 
1248
1262
  # initialize dataset backup dict structure
1249
1263
  @staticmethod
@@ -1388,7 +1402,21 @@ class DsQuasar(PgCMD, PgSplit):
1388
1402
  rcnt += 1
1389
1403
  return rcnt
1390
1404
 
1391
- # evaluate file stat on disk/object storage and modify RDADB record if not match
1405
+ # log a GDEX file whose record waits for backup but whose data is not where its
1406
+ # locflag says. the check functions return None for a missing file without logging
1407
+ # it, so such a file used to be skipped silently, run after run. only the first
1408
+ # MISSLOG files of a dataset are named, so a dataset missing thousands of files does
1409
+ # not flood the log every run. returns None, which tells the caller the file is
1410
+ # missing rather than failed
1411
+ def missing_file_stat(self, dsid, cate, fname, path):
1412
+ self.MISSCNT += 1
1413
+ if self.MISSCNT <= self.MISSLOG:
1414
+ self.pglog("{}-{}-{}: Missing {}".format(dsid, cate.upper(), fname, path), self.LOGWRN)
1415
+ return None
1416
+
1417
+ # evaluate file stat on disk/object storage and modify RDADB record if not match.
1418
+ # returns True to back the file up, False when it cannot be checked, and None
1419
+ # when it is missing from its storage location
1392
1420
  def evaluate_file_stat(self, dsid, cate, pgrec):
1393
1421
  tname = cate + 'file'
1394
1422
  fname = pgrec[tname]
@@ -1397,18 +1425,28 @@ class DsQuasar(PgCMD, PgSplit):
1397
1425
  bucket = 'gdex-decsdata' if cate == 's' else self.PGLOG['OBJCTBKT']
1398
1426
  ofile = self.join_paths(dsid, fname)
1399
1427
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
1428
+ if info is None: return self.missing_file_stat(dsid, cate, fname, "{}:{}".format(bucket, ofile))
1400
1429
  if not info: return False
1401
- lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
1402
- self.object_copy_local(lfile, ofile, bucket, self.ERRACT)
1403
- cinfo = self.check_local_file(lfile, 32, self.ERRACT)
1404
- if not cinfo: return False
1405
- if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
1430
+ # an object still matching its record on size and time, with its checksum
1431
+ # recorded already, has nothing to evaluate: downloading it only to md5 it
1432
+ # again costs a transfer per file, and dsarch downloads it anyway to tar it.
1433
+ # a directory key such as a zarr store has no checksum, so it is always fetched
1434
+ if not (info['isfile'] and pgrec['checksum'] and info['data_size'] == pgrec['data_size'] and
1435
+ self.cmptime(info['date_modified'], info['time_modified'],
1436
+ pgrec['date_modified'], pgrec['time_modified']) == 0):
1437
+ lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
1438
+ # pass info on so the object is not listed a second time
1439
+ if not self.object_copy_local(lfile, ofile, bucket, self.ERRACT, info): return False
1440
+ cinfo = self.check_local_file(lfile, 32, self.ERRACT)
1441
+ if not cinfo: return False
1442
+ if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
1406
1443
  lname = 'OBS'
1407
1444
  else:
1408
1445
  lfile = "{}/{}/".format(chome, dsid)
1409
1446
  if cate == 's': lfile += pgrec['type'] + '/'
1410
1447
  lfile += fname
1411
1448
  info = self.check_local_file(lfile, 33, self.ERRACT)
1449
+ if info is None: return self.missing_file_stat(dsid, cate, fname, lfile)
1412
1450
  if not info: return False
1413
1451
  lname = 'DSK'
1414
1452
  record = {}
@@ -108,8 +108,9 @@
108
108
  -W MaxWorkers
109
109
  Maximum number of concurrent batch workers for the same command.
110
110
  Defaults to 2. A repeat delayed submit normally blocks while the
111
- batch job is running; if that job has run for more than 6 hours and
112
- has still completed no more than 1% of its recorded work, it is
111
+ batch job is running; if that job has run for more than 2 hours,
112
+ has recorded some work done, and at its rate so far would finish
113
+ less than 90% of its recorded work within the walltime, it is
113
114
  treated as stalled and an extra worker is submitted instead, up to
114
115
  this limit. Extra workers rely on dataset locking to stay off each
115
116
  other's files, so this is pinned to 1 with -l N, and also for the
@@ -79,6 +79,8 @@ class DsTACC(PgCMD, PgSplit):
79
79
  self.TARSIZE = 1024*self.PGLOG['ONEGBS'] # 1TB, tarfile size limit
80
80
  self.MINSIZE = 512*self.PGLOG['ONEGBS'] # .5TB, minimal tarfile size
81
81
  self.ONESIZE = 20*self.PGLOG['ONEGBS'] # 20GB, minimal file size to tar a single file
82
+ self.MISSLOG = 10 # files named in the log per dataset when missing from storage
83
+ self.MISSCNT = 0 # files of the current dataset missing from storage
82
84
  self.TFCOUNT = 1000000 # if file count is greater, use self.MINSIZE for tar file
83
85
  self.PGBACK = {
84
86
  'workdir' : "{}/{}/tacc_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
@@ -731,6 +733,7 @@ class DsTACC(PgCMD, PgSplit):
731
733
  fd = None
732
734
  fcate = filetype.lower()
733
735
  tname = fcate + 'file'
736
+ self.MISSCNT = 0 # files missing from their storage location
734
737
  for i in range(fcnt):
735
738
  pgrec = recs[i]
736
739
  if not self.evaluate_file_stat(dsid, fcate, pgrec): continue
@@ -754,6 +757,12 @@ class DsTACC(PgCMD, PgSplit):
754
757
  fd = None
755
758
  self.process_one_backup_file(qinfo, True, (fcnt-i) > 1)
756
759
  if fd: fd.close()
760
+ mcnt = self.MISSCNT
761
+ if mcnt:
762
+ # one line per dataset reaches the email; the first file names are in the log
763
+ s = 's' if mcnt > 1 else ''
764
+ ftype = 'Saved' if fcate == 's' else 'Web'
765
+ self.pglog("{}: {} of {} {} file{} missing from storage, not backed up".format(dsid, mcnt, fcnt, ftype, s), self.LOGACT)
757
766
 
758
767
  # initialize dataset backup dict structure
759
768
  @staticmethod
@@ -851,7 +860,21 @@ class DsTACC(PgCMD, PgSplit):
851
860
  rcnt += 1
852
861
  return rcnt
853
862
 
854
- # evaluate file stat on disk/object storage and modify RDADB record if not match
863
+ # log a GDEX file whose record waits for backup but whose data is not where its
864
+ # locflag says. the check functions return None for a missing file without logging
865
+ # it, so such a file used to be skipped silently, run after run. only the first
866
+ # MISSLOG files of a dataset are named, so a dataset missing thousands of files does
867
+ # not flood the log every run. returns None, which tells the caller the file is
868
+ # missing rather than failed
869
+ def missing_file_stat(self, dsid, cate, fname, path):
870
+ self.MISSCNT += 1
871
+ if self.MISSCNT <= self.MISSLOG:
872
+ self.pglog("{}-{}-{}: Missing {}".format(dsid, cate.upper(), fname, path), self.LOGWRN)
873
+ return None
874
+
875
+ # evaluate file stat on disk/object storage and modify RDADB record if not match.
876
+ # returns True to back the file up, False when it cannot be checked, and None
877
+ # when it is missing from its storage location
855
878
  def evaluate_file_stat(self, dsid, cate, pgrec):
856
879
  tname = cate + 'file'
857
880
  fname = pgrec[tname]
@@ -860,18 +883,28 @@ class DsTACC(PgCMD, PgSplit):
860
883
  bucket = 'gdex-decsdata' if cate == 's' else self.PGLOG['OBJCTBKT']
861
884
  ofile = self.join_paths(dsid, fname)
862
885
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
886
+ if info is None: return self.missing_file_stat(dsid, cate, fname, "{}:{}".format(bucket, ofile))
863
887
  if not info: return False
864
- lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
865
- self.object_copy_local(lfile, ofile, bucket, self.ERRACT)
866
- cinfo = self.check_local_file(lfile, 32, self.ERRACT)
867
- if not cinfo: return False
868
- if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
888
+ # an object still matching its record on size and time, with its checksum
889
+ # recorded already, has nothing to evaluate: downloading it only to md5 it
890
+ # again costs a transfer per file, and dsarch downloads it anyway to tar it.
891
+ # a directory key such as a zarr store has no checksum, so it is always fetched
892
+ if not (info['isfile'] and pgrec['checksum'] and info['data_size'] == pgrec['data_size'] and
893
+ self.cmptime(info['date_modified'], info['time_modified'],
894
+ pgrec['date_modified'], pgrec['time_modified']) == 0):
895
+ lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
896
+ # pass info on so the object is not listed a second time
897
+ if not self.object_copy_local(lfile, ofile, bucket, self.ERRACT, info): return False
898
+ cinfo = self.check_local_file(lfile, 32, self.ERRACT)
899
+ if not cinfo: return False
900
+ if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
869
901
  lname = 'OBS'
870
902
  else:
871
903
  lfile = "{}/{}/".format(chome, dsid)
872
904
  if cate == 's': lfile += pgrec['type'] + '/'
873
905
  lfile += fname
874
906
  info = self.check_local_file(lfile, 33, self.ERRACT)
907
+ if info is None: return self.missing_file_stat(dsid, cate, fname, lfile)
875
908
  if not info: return False
876
909
  lname = 'DSK'
877
910
  record = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.22
3
+ Version: 3.0.24
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