rda-python-dsquasar 3.0.21__tar.gz → 3.0.23__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.21/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.23}/PKG-INFO +1 -1
  2. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/pyproject.toml +1 -1
  3. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/dsquasar.py +61 -17
  4. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/dsquasar.usg +20 -4
  5. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/dstacc.py +13 -5
  6. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  7. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/LICENSE +0 -0
  8. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/MANIFEST.in +0 -0
  9. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/README.md +0 -0
  10. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/setup.cfg +0 -0
  11. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/__init__.py +0 -0
  12. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  13. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/taccrec.py +0 -0
  14. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar/tacctar.py +0 -0
  15. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar.egg-info/SOURCES.txt +0 -0
  16. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.21 → rda_python_dsquasar-3.0.23}/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.21
3
+ Version: 3.0.23
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.21"
7
+ version = "3.0.23"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -105,7 +105,7 @@ class DsQuasar(PgCMD, PgSplit):
105
105
  # dscheck work done, which is action independent because dcount counts GDEX files
106
106
  # for -A 3 but tar files for -A 2/-A 4.
107
107
  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
108
+ self.WORKGRACE = 2*3600 # 2 hours before a running job is judged on progress
109
109
  self.MINWPROJ = 0.9 # projected done fraction at the walltime that counts as finishing
110
110
  self.PGBACK = {
111
111
  'workdir' : "{}/{}/quasar_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
@@ -123,6 +123,7 @@ class DsQuasar(PgCMD, PgSplit):
123
123
  'maxcnt' : 10,
124
124
  'dolock' : 1,
125
125
  'doemail' : 0,
126
+ 'locflag' : None, # -L, limit the datasets gathered to one dataset.locflag
126
127
  'starttime' : 0, # wall-clock start of the run, for the PBS walltime guard
127
128
  'einfo' : 0, # set once a progress report is cached into dscheck.einfo
128
129
  'eretry' : 0, # earliest retry time after a progress report failed to cache
@@ -144,7 +145,7 @@ class DsQuasar(PgCMD, PgSplit):
144
145
  argv = sys.argv[1:]
145
146
  for arg in argv:
146
147
  if re.match(r'^-(h|-help)$', arg, re.I): self.show_usage('dsquasar')
147
- ms = re.match(r'^-(a|b|c|d|e|E|l|m|n|t|u|w|A|B|D|W)$', arg)
148
+ ms = re.match(r'^-(a|b|c|d|e|E|l|m|n|t|u|w|A|B|D|L|W)$', arg)
148
149
  if ms:
149
150
  arg = ms.group(1)
150
151
  if arg == 'b':
@@ -152,7 +153,7 @@ class DsQuasar(PgCMD, PgSplit):
152
153
  elif arg in self.sopts:
153
154
  self.sopts[arg] = 1
154
155
  option = None
155
- elif 'AcdlmtwW'.find(arg) > -1:
156
+ elif 'AcdLlmtwW'.find(arg) > -1:
156
157
  option = arg
157
158
  if arg == 'd': self.bopts = []
158
159
  elif 'BD'.find(arg) > -1:
@@ -179,6 +180,10 @@ class DsQuasar(PgCMD, PgSplit):
179
180
  if not (arg == 'Y' or arg == 'N'): self.pglog(arg +": Lock Flag(-l) must be Y or N", self.LGWNEX)
180
181
  self.PGBACK['dolock'] = 1 if arg == 'Y' else 0
181
182
  option = None
183
+ elif option == 'L':
184
+ if not (arg == 'G' or arg == 'O'): self.pglog(arg +": Location Flag(-L) must be G or O", self.LGWNEX)
185
+ self.PGBACK['locflag'] = arg
186
+ option = None
182
187
  elif option == 'W':
183
188
  self.PGBACK['maxworkers'] = int(arg)
184
189
  if self.PGBACK['maxworkers'] < 1: self.pglog(arg +": Maximum Workers(-W) must be > 0", self.LGWNEX)
@@ -426,8 +431,9 @@ class DsQuasar(PgCMD, PgSplit):
426
431
  dargv = dargv[0:100]
427
432
  return self.get_dscheck("dsquasar", dargv, self.PGBACK['workdir'], self.PGLOG['CURUID'], dargx, self.LOGWRN)
428
433
 
429
- # a running batch job counts as stalled once it has run longer than WORKGRACE and its
430
- # progress so far projects to less than MINWPROJ of its recorded work by the walltime:
434
+ # a running batch job counts as stalled once it has run longer than WORKGRACE, has
435
+ # recorded some work done, and its progress so far projects to less than MINWPROJ of
436
+ # its recorded work by the walltime:
431
437
  # such a job cannot finish this attempt however long it is left alone, so an extra
432
438
  # worker is what helps it, not more retries.
433
439
  # the test is on the RATE, not on an absolute done fraction as it first was. a fixed
@@ -442,11 +448,15 @@ class DsQuasar(PgCMD, PgSplit):
442
448
  # runs this and set_walltime_deadline reads the granted walltime back in the batch job.
443
449
  # when the daemon caps it to a shorter queue limit the projection is optimistic, which
444
450
  # errs towards leaving the job alone.
451
+ # a job that has recorded nothing done is still in its gather phases, which read as a
452
+ # zero rate; it is left for the next submit rather than judged. that guard is what lets
453
+ # WORKGRACE stay short: an extra worker can only be added by a cron submit, every 4
454
+ # hours, so a 6 hour grace kept it out of the first 6 to 10 hours of every attempt.
445
455
  def worker_progress_stalled(self, pgrec):
446
456
  if not (pgrec['pid'] and pgrec['stttime']): return False
447
457
  if self.check_host_pid(pgrec['lockhost'], pgrec['pid']) <= 0: return False
448
458
  elapsed = tm() - pgrec['stttime']
449
- if elapsed < self.WORKGRACE: return False
459
+ if elapsed < self.WORKGRACE or not pgrec['dcount']: return False
450
460
  done = pgrec['dcount']/pgrec['fcount'] if pgrec['fcount'] else 0
451
461
  return (done*self.WALLTIME/elapsed) < self.MINWPROJ
452
462
 
@@ -918,9 +928,10 @@ class DsQuasar(PgCMD, PgSplit):
918
928
  # even if PBS kills the job at the walltime. the final report follows and drops the
919
929
  # parked one if the run does finish in time. the live email buffers are saved and put
920
930
  # 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
931
+ # batch_term_report covers the kill itself, but only the seconds of it: SIGKILL cannot be
932
+ # caught, and a process stuck in uninterruptible I/O may never run the handler. this parked
933
+ # report is what stands between a killed job and a run that is never heard from, so it is
934
+ # kept as the first line of defence. every action submitted to PBS is guarded, not just the
924
935
  # tar and transfer ones: -A 16 and the hidden -A 128 are submitted with the same 24 hour
925
936
  # walltime and used to be excluded, so they died silently.
926
937
  # the tar queue depths are only reported for the actions they describe. both depths are
@@ -1392,11 +1403,19 @@ class DsQuasar(PgCMD, PgSplit):
1392
1403
  ofile = self.join_paths(dsid, fname)
1393
1404
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
1394
1405
  if not info: return False
1395
- lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
1396
- self.object_copy_local(lfile, ofile, bucket, self.ERRACT)
1397
- cinfo = self.check_local_file(lfile, 32, self.ERRACT)
1398
- if not cinfo: return False
1399
- if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
1406
+ # an object still matching its record on size and time, with its checksum
1407
+ # recorded already, has nothing to evaluate: downloading it only to md5 it
1408
+ # again costs a transfer per file, and dsarch downloads it anyway to tar it.
1409
+ # a directory key such as a zarr store has no checksum, so it is always fetched
1410
+ if not (info['isfile'] and pgrec['checksum'] and info['data_size'] == pgrec['data_size'] and
1411
+ self.cmptime(info['date_modified'], info['time_modified'],
1412
+ pgrec['date_modified'], pgrec['time_modified']) == 0):
1413
+ lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
1414
+ # pass info on so the object is not listed a second time
1415
+ if not self.object_copy_local(lfile, ofile, bucket, self.ERRACT, info): return False
1416
+ cinfo = self.check_local_file(lfile, 32, self.ERRACT)
1417
+ if not cinfo: return False
1418
+ if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
1400
1419
  lname = 'OBS'
1401
1420
  else:
1402
1421
  lfile = "{}/{}/".format(chome, dsid)
@@ -1468,13 +1487,29 @@ class DsQuasar(PgCMD, PgSplit):
1468
1487
  fcnt = 0
1469
1488
  # warn per named dataset with no files; stay silent when scanning all datasets
1470
1489
  logact = self.LOGWRN if self.dsids else 0
1490
+ flds = "dsid, backflag, pid, lockhost"
1491
+ # -L splits the work by where the data sits: an Object(O) dataset has to be staged
1492
+ # down from the object store and checksummed before it can be tarred, while a Glade(G)
1493
+ # one is only stat'ed, so the two classes run at wildly different rates and are better
1494
+ # off in separate runs than sharing one walltime.
1495
+ # G is matched as "not O" rather than "= G" on purpose: dataset.locflag is only a
1496
+ # summary of the per-file flags (set_dataset_locflag writes it only when every public
1497
+ # web file agrees, and leaves the old value otherwise), so it can hold a stale value.
1498
+ # as "not O" the two halves stay exhaustive and no dataset falls through both runs
1499
+ lcnd = ''
1500
+ if self.PGBACK['locflag'] == 'O':
1501
+ lcnd = "locflag = 'O'"
1502
+ elif self.PGBACK['locflag']:
1503
+ lcnd = "locflag <> 'O'"
1471
1504
  if self.dsids:
1472
1505
  pgrecs = []
1473
1506
  for dsid in self.dsids:
1474
- pgrec = self.pgget("dataset", "dsid, backflag, pid", "dsid = '{}'".format(dsid), self.LGWNEX)
1507
+ dcnd = "dsid = '{}'".format(dsid)
1508
+ if lcnd: dcnd += " AND " + lcnd
1509
+ pgrec = self.pgget("dataset", flds, dcnd, self.LGWNEX)
1475
1510
  if pgrec: pgrecs.append(pgrec)
1476
1511
  else:
1477
- mrecs = self.pgmget("dataset", "dsid, backflag, pid", self.dsid_order().strip(), self.LGWNEX)
1512
+ mrecs = self.pgmget("dataset", flds, (lcnd + self.dsid_order()).strip(), self.LGWNEX)
1478
1513
  dcnt = len(mrecs['dsid']) if mrecs else 0
1479
1514
  pgrecs = [self.onerecord(mrecs, i) for i in range(dcnt)]
1480
1515
  for pgrec in pgrecs:
@@ -1487,7 +1522,16 @@ class DsQuasar(PgCMD, PgSplit):
1487
1522
  # main runs take new files (bid = 0), so it shares no record with the worker holding
1488
1523
  # the lock and must not skip the dataset for it
1489
1524
  if unlock and pgrec['pid'] and not self.PGBACK['chgdays']:
1490
- if self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
1525
+ # quiet unlock: a live holder is the normal case for a second worker, and the
1526
+ # library wording for it ("Cannot Unlock") reads like a failure. report the skip
1527
+ # here instead, in terms of the worker that is still on the dataset
1528
+ lstat = self.lock_dataset(dsid, 0, 0)
1529
+ if lstat < 1:
1530
+ # -1 is the live holder; 0 means the record went away or the db errored,
1531
+ # which lock_dataset has already logged as an error
1532
+ if lstat < 0:
1533
+ self.pglog("{}: skipped, still in use by {}/{}".format(dsid, pgrec['pid'], pgrec['lockhost']), self.LOGACT)
1534
+ continue
1491
1535
  fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact, sizes)
1492
1536
  if dsfiles:
1493
1537
  s = 's' if fcnt > 1 else ''
@@ -8,8 +8,8 @@
8
8
  file tarring and transferring is performed through the utility program dsarch.
9
9
 
10
10
  Usage: dsquasar [-b] [(-a | -t DatasetIDs)] [-c ChangeDays] [-(e|E)] \
11
- [-(B|D)] [-A ActionBits] [-l (Y|N)] [-m ProcessCount] \
12
- [-n] [-u] [-d [HostName] [TryCount]] [-h]
11
+ [-(B|D)] [-L (G|O)] [-A ActionBits] [-l (Y|N)] \
12
+ [-m ProcessCount] [-n] [-u] [-d [HostName] [TryCount]] [-h]
13
13
 
14
14
  Either -a or -t must be given (usage is displayed otherwise). dsquasar runs
15
15
  from the Quasar backup work directory; it changes there automatically.
@@ -24,6 +24,21 @@
24
24
  '%' is allowed and is expanded against the dataset table. Ignored,
25
25
  with a warning, when -a is also given.
26
26
 
27
+ -L (G|O)
28
+ Limit the run by dataset.locflag: O takes the datasets on the object
29
+ store, G takes everything else. Applies to -a and to -t alike. The
30
+ two classes back up at very different rates: an O dataset has every
31
+ file staged down from the object store and checksummed before it can
32
+ be tarred, while a G dataset is only stat'ed. Running them in one job
33
+ lets the slow class eat the whole walltime, so give them separate
34
+ runs, e.g. one submit with -L G and another with -L O. Defaults to
35
+ all datasets.
36
+
37
+ -L G and -L O together always cover every dataset, so a pair of runs
38
+ loses nothing. The split is by dataset, though, and dataset.locflag
39
+ is only a summary of the per-file flags, so a dataset holding files of
40
+ both kinds is backed up entirely by whichever run claims it.
41
+
27
42
 
28
43
  BACKUP SCOPE
29
44
 
@@ -93,8 +108,9 @@
93
108
  -W MaxWorkers
94
109
  Maximum number of concurrent batch workers for the same command.
95
110
  Defaults to 2. A repeat delayed submit normally blocks while the
96
- batch job is running; if that job has run for more than 6 hours and
97
- 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
98
114
  treated as stalled and an extra worker is submitted instead, up to
99
115
  this limit. Extra workers rely on dataset locking to stay off each
100
116
  other's files, so this is pinned to 1 with -l N, and also for the
@@ -861,11 +861,19 @@ class DsTACC(PgCMD, PgSplit):
861
861
  ofile = self.join_paths(dsid, fname)
862
862
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
863
863
  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']
864
+ # an object still matching its record on size and time, with its checksum
865
+ # recorded already, has nothing to evaluate: downloading it only to md5 it
866
+ # again costs a transfer per file, and dsarch downloads it anyway to tar it.
867
+ # a directory key such as a zarr store has no checksum, so it is always fetched
868
+ if not (info['isfile'] and pgrec['checksum'] and info['data_size'] == pgrec['data_size'] and
869
+ self.cmptime(info['date_modified'], info['time_modified'],
870
+ pgrec['date_modified'], pgrec['time_modified']) == 0):
871
+ lfile = "{}/{}/{}/{}".format(chome, self.PGLOG['BACKUPEP'], dsid, fname)
872
+ # pass info on so the object is not listed a second time
873
+ if not self.object_copy_local(lfile, ofile, bucket, self.ERRACT, info): return False
874
+ cinfo = self.check_local_file(lfile, 32, self.ERRACT)
875
+ if not cinfo: return False
876
+ if 'checksum' in cinfo: info['checksum'] = cinfo['checksum']
869
877
  lname = 'OBS'
870
878
  else:
871
879
  lfile = "{}/{}/".format(chome, dsid)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.21
3
+ Version: 3.0.23
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