rda-python-dsquasar 3.0.23__tar.gz → 3.0.25__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.23/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.25}/PKG-INFO +1 -1
  2. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/pyproject.toml +1 -1
  3. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/dsquasar.py +160 -42
  4. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/dsquasar.usg +15 -12
  5. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/dstacc.py +26 -1
  6. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  7. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/LICENSE +0 -0
  8. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/MANIFEST.in +0 -0
  9. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/README.md +0 -0
  10. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/setup.cfg +0 -0
  11. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/__init__.py +0 -0
  12. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  13. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/taccrec.py +0 -0
  14. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar/tacctar.py +0 -0
  15. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar.egg-info/SOURCES.txt +0 -0
  16. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.23 → rda_python_dsquasar-3.0.25}/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.23
3
+ Version: 3.0.25
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.23"
7
+ version = "3.0.25"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -83,11 +83,18 @@ 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
89
91
  # file: use a small per-process limit and a high cap to spread the work widely.
90
- self.MPTLIMIT = 200 # tar file count per batch process for tarring
92
+ self.MPTLIMIT = 200 # tar file count per batch process for tarring on Glade
93
+ # a tar file of an object store dataset has every member staged down from Boreas and
94
+ # checksummed first, so it takes many times longer than a Glade one: give it a
95
+ # process per far fewer tar files. this is the only way to speed up a backlog held
96
+ # in one dataset, since a second worker cannot share the dataset lock.
97
+ self.MPOLIMIT = 20 # tar file count per batch process for tarring on the object store
91
98
  self.MPTMAX = 12 # maximum number of batch processes for tarring
92
99
  # uploading (BCKACT) forks one child per BCKSIZE transfer batch (~BCKSIZE/TARSIZE
93
100
  # tar files each), so far fewer parallel units than tar files: use a large
@@ -105,8 +112,9 @@ class DsQuasar(PgCMD, PgSplit):
105
112
  # dscheck work done, which is action independent because dcount counts GDEX files
106
113
  # for -A 3 but tar files for -A 2/-A 4.
107
114
  self.MAXWORKERS = 2 # default maximum concurrent workers per command (-W)
108
- self.WORKGRACE = 2*3600 # 2 hours before a running job is judged on progress
109
- self.MINWPROJ = 0.9 # projected done fraction at the walltime that counts as finishing
115
+ self.WORKGRACE = 3600 # 1 hour before a running job is judged on progress
116
+ self.GATHGRACE = 2*3600 # 2 hours before a job with nothing done yet counts as stalled
117
+ self.MINWPROJ = 1.5 # projected done fraction at the walltime that counts as finishing
110
118
  self.PGBACK = {
111
119
  'workdir' : "{}/{}/quasar_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
112
120
  'mproc' : 1,
@@ -200,6 +208,7 @@ class DsQuasar(PgCMD, PgSplit):
200
208
  # an even worker slot walks the datasets from the other end so that it and the odd
201
209
  # slot before it meet in the middle instead of fighting over the same dataset locks
202
210
  if self.reverse_dsids(): self.dsids.reverse()
211
+ self.order_dsids()
203
212
  self.PGBACK['cmd'] = "dsquasar {}".format(' '.join(argv))
204
213
 
205
214
  # do quasar backup process
@@ -273,6 +282,18 @@ class DsQuasar(PgCMD, PgSplit):
273
282
  dsid = self.format_dataset_id(dsid)
274
283
  if dsid not in self.dsids: self.dsids.append(dsid)
275
284
 
285
+ # put the -t datasets on the object store first for an odd worker slot and the ones on
286
+ # Glade first for an even one, keeping the given (or worker-reversed) order within each
287
+ # class; see dsid_order for why
288
+ def order_dsids(self):
289
+ if len(self.dsids) < 2: return
290
+ dcnd = "locflag = 'O' AND dsid IN ('{}')".format("', '".join(self.dsids))
291
+ pgrecs = self.pgmget('dataset', 'dsid', dcnd, self.LGWNEX)
292
+ if not pgrecs: return
293
+ odsids = set(pgrecs['dsid'])
294
+ reverse = self.reverse_dsids()
295
+ self.dsids.sort(key = lambda dsid: (dsid in odsids) == reverse)
296
+
276
297
  # Check backup and file information and dump the backup statistics#
277
298
  def dump_statistics_action(self):
278
299
  self.start_dsquasar_none_daemon(self.STTACT)
@@ -373,7 +394,8 @@ class DsQuasar(PgCMD, PgSplit):
373
394
  # file (one child per tar): new infiles CINACT will create this run (estimated from
374
395
  # the total bytes of GDEX files ready to back up, ~one tar per TARSIZE) plus existing
375
396
  # status 'N' infile records to build (TARACT), scaled one process per MPTLIMIT tar
376
- # files, capped at MPTMAX. uploading's unit is the BCKSIZE transfer batch (one child
397
+ # files on Glade or one per MPOLIMIT tar files on the object store, the two summed and
398
+ # capped at MPTMAX. uploading's unit is the BCKSIZE transfer batch (one child
377
399
  # per ~BCKSIZE/TARSIZE tar files): existing status 'T' tar records to transfer
378
400
  # (BCKACT), scaled one process per MPBLIMIT tar files, capped at MPBMAX. the phases
379
401
  # run sequentially in one run and share a single reserved ncpus, so return the max of
@@ -381,16 +403,34 @@ class DsQuasar(PgCMD, PgSplit):
381
403
  # never reaches here and stays single process.
382
404
  def batch_process_count(self, acts):
383
405
  mproc = 1
384
- tcnt = 0
406
+ gcnt = ocnt = 0 # tar files on Glade and on the object store
407
+ # an extra worker cannot touch the datasets another live process holds, typically the
408
+ # big object store dataset worker 1 is on, so it leaves them out of its sizing as its
409
+ # real gather will; counting them reserved worker 1's cpus again for the Glade work
410
+ # left. the gather's unlock pass skips a live holder and frees a dead one's lock.
411
+ # worker 1 keeps counting everything: a held dataset is usually released well within
412
+ # its 24 hours, and checking each holder costs an rdaps call per dataset
413
+ extra = self.PGBACK['worker'] > 1
385
414
  if acts&self.CINACT:
386
- sizes = [0]
387
- if self.gather_dataset_files(None, False, sizes):
388
- tcnt += max(1, (sizes[0] + self.TARSIZE - 1)//self.TARSIZE)
389
- if acts&self.TARACT: tcnt += self.batch_tar_count('N')
390
- if tcnt > self.MPTLIMIT:
391
- tproc = (tcnt + self.MPTLIMIT - 1)//self.MPTLIMIT
392
- if tproc > self.MPTMAX: tproc = self.MPTMAX
393
- if tproc > mproc: mproc = tproc
415
+ sizes = [0, 0] # bytes on Glade and on the object store
416
+ if self.gather_dataset_files(None, extra, sizes):
417
+ gcnt += (sizes[0] + self.TARSIZE - 1)//self.TARSIZE
418
+ ocnt += (sizes[1] + self.TARSIZE - 1)//self.TARSIZE
419
+ if not (gcnt or ocnt): gcnt = 1
420
+ if acts&self.TARACT:
421
+ # a dataset lock left by a dead process also drops out here, though the gather
422
+ # would free it; that undercounts by a stale lock's infiles at most
423
+ xcnd = " AND dsid NOT IN (SELECT dsid FROM dataset WHERE pid > 0)" if extra else ''
424
+ ncnt = self.batch_tar_count('N', None, xcnd)
425
+ oncnt = self.batch_tar_count('N', 'O', xcnd) if ncnt else 0
426
+ gcnt += ncnt - oncnt
427
+ ocnt += oncnt
428
+ # ceil(gcnt/MPTLIMIT + ocnt/MPOLIMIT): one process per MPTLIMIT Glade tar files plus
429
+ # one per MPOLIMIT object store tar files, rounded up once over the sum
430
+ tunit = self.MPTLIMIT*self.MPOLIMIT
431
+ tproc = (gcnt*self.MPOLIMIT + ocnt*self.MPTLIMIT + tunit - 1)//tunit
432
+ if tproc > self.MPTMAX: tproc = self.MPTMAX
433
+ if tproc > mproc: mproc = tproc
394
434
  if acts&self.BCKACT:
395
435
  bcnt = self.batch_tar_count('T')
396
436
  if bcnt > self.MPBLIMIT:
@@ -399,10 +439,25 @@ class DsQuasar(PgCMD, PgSplit):
399
439
  if bproc > mproc: mproc = bproc
400
440
  return mproc
401
441
 
402
- # count the bfile tar records of a given status honoring backflag/dataset scope
403
- def batch_tar_count(self, status):
442
+ # the -L scope of a bfile condition, or the scope of the given locflag. bfile has no
443
+ # locflag, so a record is matched through its dataset with the same "O" / "not O" split
444
+ # gather_dataset_files uses, keeping the two halves exhaustive. without it an -L O and
445
+ # an -L G run that overlap each tar, transfer and report the tar records of both halves
446
+ def bfile_locflag_condition(self, locflag = None):
447
+ if not locflag: locflag = self.PGBACK['locflag']
448
+ if not locflag: return ''
449
+ lnot = '' if locflag == 'O' else 'NOT '
450
+ return " AND dsid {}IN (SELECT dsid FROM dataset WHERE locflag = 'O')".format(lnot)
451
+
452
+ # count the bfile tar records of a given status honoring backflag/dataset/locflag scope;
453
+ # a locflag given narrows the count further to that half, inside any -L scope, and xcnd
454
+ # is any extra condition to AND on
455
+ def batch_tar_count(self, status, locflag = None, xcnd = ''):
404
456
  bcnd = "status = '{}'".format(status)
405
457
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
458
+ bcnd += self.bfile_locflag_condition()
459
+ if locflag: bcnd += self.bfile_locflag_condition(locflag)
460
+ bcnd += xcnd
406
461
  if self.dsids:
407
462
  tcnt = 0
408
463
  for dsid in self.dsids:
@@ -431,11 +486,16 @@ class DsQuasar(PgCMD, PgSplit):
431
486
  dargv = dargv[0:100]
432
487
  return self.get_dscheck("dsquasar", dargv, self.PGBACK['workdir'], self.PGLOG['CURUID'], dargx, self.LOGWRN)
433
488
 
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:
437
- # such a job cannot finish this attempt however long it is left alone, so an extra
438
- # worker is what helps it, not more retries.
489
+ # a running batch job counts as stalled once it has run longer than WORKGRACE and its
490
+ # progress so far projects to less than MINWPROJ of its recorded work by the walltime,
491
+ # or once it has run longer than GATHGRACE with nothing recorded done at all:
492
+ # such a job cannot finish this attempt comfortably however long it is left alone, so an
493
+ # extra worker is what helps it, not more retries.
494
+ # MINWPROJ is above 1 on purpose: the first worker starts on the slow object store
495
+ # datasets and an extra one on the fast Glade ones (see dsid_order), so a second worker
496
+ # is wanted whenever the first is not well on its way to finishing, not only when it is
497
+ # sure to miss the walltime. 1.5 leaves alone only a job on pace to finish within about
498
+ # two thirds of the walltime.
439
499
  # the test is on the RATE, not on an absolute done fraction as it first was. a fixed
440
500
  # 1% threshold only catches a job that is nearly frozen, and reads a job that needs
441
501
  # weeks as progressing: the real case was 2% done after 13H35M of a 24 hour walltime,
@@ -448,15 +508,19 @@ class DsQuasar(PgCMD, PgSplit):
448
508
  # runs this and set_walltime_deadline reads the granted walltime back in the batch job.
449
509
  # when the daemon caps it to a shorter queue limit the projection is optimistic, which
450
510
  # 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.
511
+ # a job that has recorded nothing done may still be in its gather phases, which read as
512
+ # a zero rate, so it gets the longer GATHGRACE before it is judged. but a job that has
513
+ # recorded nothing after that long, typically one downloading its first object store
514
+ # dataset (dcount is only flushed every pstep files), is the slowest case of all and
515
+ # must not be left alone forever. the graces stay short because an extra worker can
516
+ # only be added by a cron submit, every 4 hours: a 6 hour grace kept it out of the first
517
+ # 6 to 10 hours of every attempt.
455
518
  def worker_progress_stalled(self, pgrec):
456
519
  if not (pgrec['pid'] and pgrec['stttime']): return False
457
520
  if self.check_host_pid(pgrec['lockhost'], pgrec['pid']) <= 0: return False
458
521
  elapsed = tm() - pgrec['stttime']
459
- if elapsed < self.WORKGRACE or not pgrec['dcount']: return False
522
+ if elapsed < self.WORKGRACE: return False
523
+ if not pgrec['dcount']: return elapsed >= self.GATHGRACE
460
524
  done = pgrec['dcount']/pgrec['fcount'] if pgrec['fcount'] else 0
461
525
  return (done*self.WALLTIME/elapsed) < self.MINWPROJ
462
526
 
@@ -607,7 +671,7 @@ class DsQuasar(PgCMD, PgSplit):
607
671
  self.check_batch_deadline()
608
672
  if len(qinfo['dsids']) == self.DSCNT: self.process_one_backup_file(qinfo, True, False)
609
673
  if self.PGBACK['dolock'] and dsid not in qinfo['dslocks']:
610
- if self.lock_dataset(dsid, 1, self.LOGERR) < 1: continue
674
+ if self.lock_work_dataset(dsid) < 1: continue
611
675
  qinfo['dslocks'].append(dsid)
612
676
  if not self.regather_dataset_files(bfiles, dsid, backflag):
613
677
  self.lock_dataset(dsid, 0, self.LGEREX)
@@ -673,12 +737,27 @@ class DsQuasar(PgCMD, PgSplit):
673
737
  # run, including the primary one, are left alone. returns the datasets locked here, to be
674
738
  # unlocked once the tar file is dispatched, or None if one of them is held by another
675
739
  # worker, in which case nothing new is left locked and the tar file must wait for a later run
740
+ # lock a dataset before working on it. a dataset held by another live process is the
741
+ # normal case once a second worker or the other backup action runs, and the library
742
+ # wording for it ("Locked by N/PBS: Cannot Relock") was logged as an ERROR, which counted
743
+ # in the error count and the error email although the dataset is just skipped. lock
744
+ # quietly and report the skip as information instead, naming the holder; a database
745
+ # error is still logged as an error by lock_dataset. returns what lock_dataset returns:
746
+ # 1 locked, -1 held by another process, 0 no dataset record or a database error
747
+ def lock_work_dataset(self, dsid, logact = None):
748
+ lstat = self.lock_dataset(dsid, 1, 0)
749
+ if lstat < 0:
750
+ pgrec = self.pgget('dataset', 'pid, lockhost', "dsid = '{}'".format(dsid), self.LOGERR)
751
+ holder = "{}/{}".format(pgrec['pid'], pgrec['lockhost']) if pgrec else 'another process'
752
+ self.pglog("{}: skipped, locked by {}".format(dsid, holder), self.LOGACT if logact is None else logact)
753
+ return lstat
754
+
676
755
  def lock_backup_datasets(self, qinfo, dsids):
677
756
  dslocks = []
678
757
  if not self.PGBACK['dolock']: return dslocks
679
758
  for dsid in dsids:
680
759
  if dsid in qinfo['dslocks']: continue
681
- if self.lock_dataset(dsid, 1, self.LOGERR) < 1:
760
+ if self.lock_work_dataset(dsid, self.DTLACT) < 1:
682
761
  self.unlock_backup_datasets(qinfo, dslocks)
683
762
  return None
684
763
  qinfo['dslocks'].append(dsid)
@@ -718,7 +797,7 @@ class DsQuasar(PgCMD, PgSplit):
718
797
  'qsize' : 0, 'qcnt' : 0}
719
798
  for dsid in bfiles:
720
799
  if self.PGBACK['dolock']:
721
- if self.lock_dataset(dsid, 1, self.LOGERR) < 1: continue
800
+ if self.lock_work_dataset(dsid) < 1: continue
722
801
  qinfo['dslocks'].append(dsid)
723
802
  for bid in bfiles[dsid]:
724
803
  self.check_batch_deadline()
@@ -1225,6 +1304,7 @@ class DsQuasar(PgCMD, PgSplit):
1225
1304
  fd = None
1226
1305
  fcate = filetype.lower()
1227
1306
  tname = fcate + 'file'
1307
+ self.MISSCNT = 0 # files missing from their storage location
1228
1308
  for i in range(fcnt):
1229
1309
  self.check_batch_deadline()
1230
1310
  pgrec = recs[i]
@@ -1249,6 +1329,12 @@ class DsQuasar(PgCMD, PgSplit):
1249
1329
  fd = None
1250
1330
  self.process_one_backup_file(qinfo, True, (fcnt-i) > 1)
1251
1331
  if fd: fd.close()
1332
+ mcnt = self.MISSCNT
1333
+ if mcnt:
1334
+ # one line per dataset reaches the email; the first file names are in the log
1335
+ s = 's' if mcnt > 1 else ''
1336
+ ftype = 'Saved' if fcate == 's' else 'Web'
1337
+ self.pglog("{}: {} of {} {} file{} missing from storage, not backed up".format(dsid, mcnt, fcnt, ftype, s), self.LOGACT)
1252
1338
 
1253
1339
  # initialize dataset backup dict structure
1254
1340
  @staticmethod
@@ -1393,7 +1479,21 @@ class DsQuasar(PgCMD, PgSplit):
1393
1479
  rcnt += 1
1394
1480
  return rcnt
1395
1481
 
1396
- # evaluate file stat on disk/object storage and modify RDADB record if not match
1482
+ # log a GDEX file whose record waits for backup but whose data is not where its
1483
+ # locflag says. the check functions return None for a missing file without logging
1484
+ # it, so such a file used to be skipped silently, run after run. only the first
1485
+ # MISSLOG files of a dataset are named, so a dataset missing thousands of files does
1486
+ # not flood the log every run. returns None, which tells the caller the file is
1487
+ # missing rather than failed
1488
+ def missing_file_stat(self, dsid, cate, fname, path):
1489
+ self.MISSCNT += 1
1490
+ if self.MISSCNT <= self.MISSLOG:
1491
+ self.pglog("{}-{}-{}: Missing {}".format(dsid, cate.upper(), fname, path), self.LOGWRN)
1492
+ return None
1493
+
1494
+ # evaluate file stat on disk/object storage and modify RDADB record if not match.
1495
+ # returns True to back the file up, False when it cannot be checked, and None
1496
+ # when it is missing from its storage location
1397
1497
  def evaluate_file_stat(self, dsid, cate, pgrec):
1398
1498
  tname = cate + 'file'
1399
1499
  fname = pgrec[tname]
@@ -1402,6 +1502,7 @@ class DsQuasar(PgCMD, PgSplit):
1402
1502
  bucket = 'gdex-decsdata' if cate == 's' else self.PGLOG['OBJCTBKT']
1403
1503
  ofile = self.join_paths(dsid, fname)
1404
1504
  info = self.check_object_file(ofile, bucket, 1, self.ERRACT)
1505
+ if info is None: return self.missing_file_stat(dsid, cate, fname, "{}:{}".format(bucket, ofile))
1405
1506
  if not info: return False
1406
1507
  # an object still matching its record on size and time, with its checksum
1407
1508
  # recorded already, has nothing to evaluate: downloading it only to md5 it
@@ -1422,6 +1523,7 @@ class DsQuasar(PgCMD, PgSplit):
1422
1523
  if cate == 's': lfile += pgrec['type'] + '/'
1423
1524
  lfile += fname
1424
1525
  info = self.check_local_file(lfile, 33, self.ERRACT)
1526
+ if info is None: return self.missing_file_stat(dsid, cate, fname, lfile)
1425
1527
  if not info: return False
1426
1528
  lname = 'DSK'
1427
1529
  record = {}
@@ -1472,11 +1574,20 @@ class DsQuasar(PgCMD, PgSplit):
1472
1574
  if self.pgget_wfile(dsid, 'wid', "bid " + bcnd, self.LGWNEX): fopt |= self.WOPT
1473
1575
  return fopt
1474
1576
 
1475
- # dataset ordering for gathering work; even worker slots start from the high end (d999999)
1476
- # and odd ones from the low end, so that a pair of workers meets in the middle instead of
1477
- # fighting over the same dataset locks
1478
- def dsid_order(self):
1479
- return " ORDER BY dsid DESC" if self.reverse_dsids() else " ORDER BY dsid"
1577
+ # dataset ordering for gathering work. an odd worker slot starts on the datasets on the
1578
+ # object store and an even one on the datasets on Glade: an Object(O) dataset has every
1579
+ # file staged down from Boreas and checksummed before it can be tarred, while a Glade
1580
+ # one is only stat'ed, so the slow downloads get the most time from the first worker
1581
+ # while an added second worker clears the fast Glade work. an even slot walks the exact
1582
+ # reverse of an odd one (Glade from d999999 down, then the object store from d999999
1583
+ # down), so that a pair of workers meets in the middle instead of fighting over the same
1584
+ # dataset locks. dataset.locflag is matched as "O" / "not O" (a NULL locflag counts as
1585
+ # Glade), the same split -L uses. bfile has no locflag, so its records are matched
1586
+ # through their dataset
1587
+ def dsid_order(self, bfile = False):
1588
+ ocnd = "dsid IN (SELECT dsid FROM dataset WHERE locflag = 'O')" if bfile else "locflag = 'O'"
1589
+ dord = " DESC" if self.reverse_dsids() else ""
1590
+ return " ORDER BY CASE WHEN {} THEN 0 ELSE 1 END{}, dsid{}".format(ocnd, dord, dord)
1480
1591
 
1481
1592
  # whether this worker slot walks the datasets from the high end
1482
1593
  def reverse_dsids(self):
@@ -1487,11 +1598,9 @@ class DsQuasar(PgCMD, PgSplit):
1487
1598
  fcnt = 0
1488
1599
  # warn per named dataset with no files; stay silent when scanning all datasets
1489
1600
  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.
1601
+ flds = "dsid, backflag, locflag, pid, lockhost"
1602
+ # -L limits the run to where the data sits. it is optional: without it an odd worker
1603
+ # slot starts on the object store datasets and an even one on Glade (see dsid_order).
1495
1604
  # G is matched as "not O" rather than "= G" on purpose: dataset.locflag is only a
1496
1605
  # summary of the per-file flags (set_dataset_locflag writes it only when every public
1497
1606
  # web file agrees, and leaves the old value otherwise), so it can hold a stale value.
@@ -1532,7 +1641,14 @@ class DsQuasar(PgCMD, PgSplit):
1532
1641
  if lstat < 0:
1533
1642
  self.pglog("{}: skipped, still in use by {}/{}".format(dsid, pgrec['pid'], pgrec['lockhost']), self.LOGACT)
1534
1643
  continue
1535
- fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact, sizes)
1644
+ if sizes is None:
1645
+ fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact)
1646
+ else:
1647
+ # sizes holds the bytes ready on Glade and on the object store apart, since a
1648
+ # tar file of each takes a very different time to build (see MPOLIMIT)
1649
+ dsizes = [0]
1650
+ fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact, dsizes)
1651
+ sizes[1 if pgrec['locflag'] == 'O' else 0] += dsizes[0]
1536
1652
  if dsfiles:
1537
1653
  s = 's' if fcnt > 1 else ''
1538
1654
  bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
@@ -1546,10 +1662,11 @@ class DsQuasar(PgCMD, PgSplit):
1546
1662
  bcnd = "status = 'N'"
1547
1663
  flds = "bid, dsid, bfile, type, data_size size, (scount + wcount) fcnt, dsids, note"
1548
1664
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
1665
+ bcnd += self.bfile_locflag_condition()
1549
1666
  if self.dsids:
1550
1667
  cnds = ["dsid = '{}' AND {}".format(dsid, bcnd) for dsid in self.dsids]
1551
1668
  else:
1552
- cnds = [bcnd + self.dsid_order()]
1669
+ cnds = [bcnd + self.dsid_order(True)]
1553
1670
  for dcnd in cnds:
1554
1671
  pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1555
1672
  bcnt = len(pgrecs['bid']) if pgrecs else 0
@@ -1572,10 +1689,11 @@ class DsQuasar(PgCMD, PgSplit):
1572
1689
  bcnd = "status = 'T'"
1573
1690
  flds = "bid, dsid, bfile, type, data_size size, (scount + wcount) fcnt, note"
1574
1691
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
1692
+ bcnd += self.bfile_locflag_condition()
1575
1693
  if self.dsids:
1576
1694
  cnds = ["dsid = '{}' AND {}".format(dsid, bcnd) for dsid in self.dsids]
1577
1695
  else:
1578
- cnds = [bcnd + self.dsid_order()]
1696
+ cnds = [bcnd + self.dsid_order(True)]
1579
1697
  for dcnd in cnds:
1580
1698
  pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1581
1699
  bcnt = len(pgrecs['bid']) if pgrecs else 0
@@ -29,10 +29,10 @@
29
29
  store, G takes everything else. Applies to -a and to -t alike. The
30
30
  two classes back up at very different rates: an O dataset has every
31
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.
32
+ be tarred, while a G dataset is only stat'ed. -L is optional: without
33
+ it the first worker (and a run without -w) starts on the O datasets,
34
+ so the slow downloads get the most time, and a second worker (-w 2)
35
+ starts on the G datasets. Defaults to all datasets.
36
36
 
37
37
  -L G and -L O together always cover every dataset, so a pair of runs
38
38
  loses nothing. The split is by dataset, though, and dataset.locflag
@@ -97,9 +97,11 @@
97
97
  -m ProcessCount
98
98
  Number of concurrent child processes. Defaults to 1. When -m is not
99
99
  given, a delayed backup run sizes the count from the number of tar
100
- files it expects to handle and reserves matching PBS cpus, so in delay
101
- mode the process count normally travels through the reserved cpus
102
- rather than the command line. An explicit -m overrides that sizing,
100
+ files it expects to handle (one process per 200 tar files on Glade
101
+ and per 20 on the object store, whose files are downloaded first, up
102
+ to 12) and reserves matching PBS cpus, so in delay mode the process
103
+ count normally travels through the reserved cpus rather than the
104
+ command line. An explicit -m overrides that sizing,
103
105
  and -m 1 forces a single process.
104
106
 
105
107
  -n Gather and show the available file counts only; no backup actions are
@@ -108,11 +110,12 @@
108
110
  -W MaxWorkers
109
111
  Maximum number of concurrent batch workers for the same command.
110
112
  Defaults to 2. A repeat delayed submit normally blocks while the
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
114
- treated as stalled and an extra worker is submitted instead, up to
115
- this limit. Extra workers rely on dataset locking to stay off each
113
+ batch job is running; if that job has run for more than 1 hour and
114
+ at its rate so far would not finish its recorded work within about
115
+ two thirds of the walltime (less than 150% of it by the walltime),
116
+ or has run for more than 2 hours with nothing recorded done yet, it
117
+ is treated as stalled and an extra worker is submitted instead, up
118
+ to this limit. Extra workers rely on dataset locking to stay off each
116
119
  other's files, so this is pinned to 1 with -l N, and also for the
117
120
  transfer action (-A 4, 6, 7), which claims tar files without locking.
118
121
 
@@ -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,6 +883,7 @@ 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
888
  # an object still matching its record on size and time, with its checksum
865
889
  # recorded already, has nothing to evaluate: downloading it only to md5 it
@@ -880,6 +904,7 @@ class DsTACC(PgCMD, PgSplit):
880
904
  if cate == 's': lfile += pgrec['type'] + '/'
881
905
  lfile += fname
882
906
  info = self.check_local_file(lfile, 33, self.ERRACT)
907
+ if info is None: return self.missing_file_stat(dsid, cate, fname, lfile)
883
908
  if not info: return False
884
909
  lname = 'DSK'
885
910
  record = {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.23
3
+ Version: 3.0.25
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