rda-python-dsquasar 3.0.1__tar.gz → 3.0.6__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.6/MANIFEST.in +1 -0
  2. {rda_python_dsquasar-3.0.1/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.6}/PKG-INFO +1 -1
  3. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/pyproject.toml +1 -1
  4. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar/dsquasar.py +368 -125
  5. rda_python_dsquasar-3.0.6/src/rda_python_dsquasar/dsquasar.usg +146 -0
  6. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  7. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar.egg-info/SOURCES.txt +2 -0
  8. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/LICENSE +0 -0
  9. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/README.md +0 -0
  10. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/setup.cfg +0 -0
  11. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar/__init__.py +0 -0
  12. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  13. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar/dstacc.py +0 -0
  14. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar/taccrec.py +0 -0
  15. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar/tacctar.py +0 -0
  16. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.1 → rda_python_dsquasar-3.0.6}/test/test_dsquasar.py +0 -0
@@ -0,0 +1 @@
1
+ include src/rda_python_dsquasar/*.usg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.1
3
+ Version: 3.0.6
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.1"
7
+ version = "3.0.6"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -12,7 +12,7 @@
12
12
  # to gather Web and Saved bfiles, tar them into larger (> 2GB) bfiles, and
13
13
  # back them up onto Quasar Backup Servers, at the Globus end points of
14
14
  # NCAR GDEX Quasar and/or NCAR GDEX Quasar Drdata, for Backup and/or
15
- # desaster recovery copies, respectively. The file tarring and backing up
15
+ # disaster recovery copies, respectively. The file tarring and backing up
16
16
  # processes are completed in utility program dsarch.
17
17
  #
18
18
  # Github : https://github.com/NCAR/rda-python-dsquasar.git
@@ -84,6 +84,25 @@ class DsQuasar(PgCMD, PgSplit):
84
84
  self.ONESIZE = 20*self.PGLOG['ONEGBS'] # 20GB, minimal file size to tar a single file
85
85
  self.TFCOUNT = 100 # if file count is greater, use self.MINSIZE for tar file
86
86
  self.SUBLMTS = 2000 # file count limit for a sub-group
87
+ # tarring (TARACT) forks one child per tar file, so its parallel unit is the tar
88
+ # file: use a small per-process limit and a high cap to spread the work widely.
89
+ self.MPTLIMIT = 200 # tar file count per batch process for tarring
90
+ self.MPTMAX = 12 # maximum number of batch processes for tarring
91
+ # uploading (BCKACT) forks one child per BCKSIZE transfer batch (~BCKSIZE/TARSIZE
92
+ # tar files each), so far fewer parallel units than tar files: use a large
93
+ # per-process limit and a low cap to avoid over-reserving cpus.
94
+ self.MPBLIMIT = 900 # tar file count per batch process for uploading
95
+ self.MPBMAX = 4 # maximum number of batch processes for uploading
96
+ self.MAXRUNTIME = 23*3600 # 23 hours; stop before the 24-hour PBS walltime
97
+ self.ONEHOUR = 3600 # seconds; time headroom needed to finish before walltime
98
+ # a repeat submit normally blocks as a duplicate while the batch job is running. if
99
+ # that job is running but barely progressing, an extra worker is submitted instead
100
+ # (see pick_worker_slot). progress is measured as the fraction of the recorded
101
+ # dscheck work done, which is action independent because dcount counts GDEX files
102
+ # for -A 3 but tar files for -A 2/-A 4.
103
+ self.MAXWORKERS = 2 # default maximum concurrent workers per command (-W)
104
+ self.WORKGRACE = 6*3600 # 6 hours before a running job is judged on progress
105
+ self.MINWDONE = 0.01 # done fraction after WORKGRACE that counts as progressing
87
106
  self.PGBACK = {
88
107
  'workdir' : "{}/{}/quasar_backup".format(self.PGLOG['GDEXWORK'], self.PGLOG['COMMONUSER']),
89
108
  'mproc' : 1,
@@ -97,6 +116,10 @@ class DsQuasar(PgCMD, PgSplit):
97
116
  'maxcnt' : 10,
98
117
  'dolock' : 1,
99
118
  'doemail' : 0,
119
+ 'starttime' : 0, # wall-clock start of the run, for the PBS walltime guard
120
+ 'tardone' : 0, # tar files dispatched so far, for the finish-rate estimate
121
+ 'maxworkers' : self.MAXWORKERS, # -W, maximum concurrent workers per command
122
+ 'worker' : 1, # -w, this run's worker slot; >1 for an added extra worker
100
123
  'cmd' : None
101
124
  }
102
125
  self.dsids = []
@@ -105,13 +128,15 @@ class DsQuasar(PgCMD, PgSplit):
105
128
  self.dstart = 0
106
129
 
107
130
  # read in command line parameters
108
- def read_parameters(self):
131
+ def read_parameters(self):
109
132
  self.PGLOG['LOGFILE'] = "dsquasar.log" # set different log file
133
+ self.set_help_path(__file__)
110
134
  self.dssdb_dbname()
111
135
  option = None
112
136
  argv = sys.argv[1:]
113
137
  for arg in argv:
114
- ms = re.match(r'^-(a|b|c|d|e|E|l|m|n|t|u|A|B|D)$', arg)
138
+ if re.match(r'^-(h|-help)$', arg, re.I): self.show_usage('dsquasar')
139
+ ms = re.match(r'^-(a|b|c|d|e|E|l|m|n|t|u|w|A|B|D|W)$', arg)
115
140
  if ms:
116
141
  arg = ms.group(1)
117
142
  if arg == 'b':
@@ -119,7 +144,7 @@ class DsQuasar(PgCMD, PgSplit):
119
144
  elif arg in self.sopts:
120
145
  self.sopts[arg] = 1
121
146
  option = None
122
- elif 'Acdlmt'.find(arg) > -1:
147
+ elif 'AcdlmtwW'.find(arg) > -1:
123
148
  option = arg
124
149
  if arg == 'd': self.bopts = []
125
150
  elif 'BD'.find(arg) > -1:
@@ -145,36 +170,28 @@ class DsQuasar(PgCMD, PgSplit):
145
170
  if not (arg == 'Y' or arg == 'N'): self.pglog(arg +": Lock Flag(-l) must be Y or N", self.LGWNEX)
146
171
  self.PGBACK['dolock'] = 1 if arg == 'Y' else 0
147
172
  option = None
173
+ elif option == 'W':
174
+ self.PGBACK['maxworkers'] = int(arg)
175
+ if self.PGBACK['maxworkers'] < 1: self.pglog(arg +": Maximum Workers(-W) must be > 0", self.LGWNEX)
176
+ option = None
177
+ elif option == 'w':
178
+ self.PGBACK['worker'] = int(arg)
179
+ if self.PGBACK['worker'] < 1: self.pglog(arg +": Worker Slot(-w) must be > 0", self.LGWNEX)
180
+ option = None
148
181
  else:
149
182
  self.add_to_dsids(arg)
150
183
  else:
151
184
  self.pglog(arg + ": Value without leading option", self.LGWNEX)
152
- if not (self.sopts['a'] or self.dsids):
153
- print("dsquasar [-b] [(-a|-t DatasetIDs)] [-c ChangeDays] [-(e|E)] [-(B|D)] [-A ActionBits] \\")
154
- print(" [-l (Y|N)] [-m ProcessCount] [-n] [-u] [-d [HostName] [TryCount]")
155
- print(" Option -a - gather files of all datasets for Quasar Backup, or")
156
- print(" Option -t - specify dataset IDs to backup files, wildcard % is allowed")
157
- print(" Option -b - turn the background mode on; no screen display")
158
- print(" Option -c - change days to work with option -A 1 to rebackup changed files")
159
- print(" Option -d - turn the delay mode on for running as a batch job")
160
- print(" Option -e - send an email to specialist for statistics")
161
- print(" Option -E - send an email to specialist for detail statistics")
162
- print(" Option -B - Quasar Backup only for the provided datasets, or")
163
- print(" Option -D - Quasar Backup&Drdata; default to both B & D")
164
- print(" Option -A - Action bits, 1 - create dsarch input files; 2 - build tarfiles;")
165
- print(" 4 - Transfer Quasar backup files; 8 - Check Backup Files;")
166
- print(" 16 - Dump Backup Statistics; default to 3 for 1+2 actions,")
167
- print(" and the valid values are 1,2,3,4,6,7,8,16")
168
- print(" Option -l - flag to check&lock dataset before backup processes, defaults to Y")
169
- print(" Option -m - number of concurrent processes, defaults to 1")
170
- print(" Option -n - gather and show the available file counts with no backup actions")
171
- print(" Option -u - to clean up backup locks for provided dataset IDs")
172
- sys.exit(0)
185
+ if not (self.sopts['a'] or self.dsids): self.show_usage('dsquasar')
186
+ # an even worker slot walks the datasets from the other end so that it and the odd
187
+ # slot before it meet in the middle instead of fighting over the same dataset locks
188
+ if self.reverse_dsids(): self.dsids.reverse()
173
189
  self.PGBACK['cmd'] = "dsquasar {}".format(' '.join(argv))
174
190
 
175
191
  # do quasar backup process
176
192
  def start_actions(self):
177
193
  self.cmdlog(self.PGBACK['cmd'])
194
+ self.PGBACK['starttime'] = tm()
178
195
  if self.sopts['u']:
179
196
  if self.sopts['a']: self.pglog("-u: Dataset IDs must be provided to Unlock datasets", self.LOGWRN)
180
197
  self.unlock_datasets()
@@ -331,21 +348,148 @@ class DsQuasar(PgCMD, PgSplit):
331
348
  self.backup_dataset_tarfiles(dsfiles, 'B')
332
349
  self.backup_dataset_tarfiles(dsfiles, 'D')
333
350
 
351
+ # size up the number of batch processes to reserve, sizing tarring and uploading
352
+ # independently because their parallel work units differ. tarring's unit is the tar
353
+ # file (one child per tar): new infiles CINACT will create this run (estimated from
354
+ # the total bytes of GDEX files ready to back up, ~one tar per TARSIZE) plus existing
355
+ # status 'N' infile records to build (TARACT), scaled one process per MPTLIMIT tar
356
+ # files, capped at MPTMAX. uploading's unit is the BCKSIZE transfer batch (one child
357
+ # per ~BCKSIZE/TARSIZE tar files): existing status 'T' tar records to transfer
358
+ # (BCKACT), scaled one process per MPBLIMIT tar files, capped at MPBMAX. the phases
359
+ # run sequentially in one run and share a single reserved ncpus, so return the max of
360
+ # the two counts. create-infile only (-A 1) is not a batch action (see NBACTS) so it
361
+ # never reaches here and stays single process.
362
+ def batch_process_count(self, acts):
363
+ mproc = 1
364
+ tcnt = 0
365
+ if acts&self.CINACT:
366
+ sizes = [0]
367
+ if self.gather_dataset_files(None, False, sizes):
368
+ tcnt += max(1, (sizes[0] + self.TARSIZE - 1)//self.TARSIZE)
369
+ if acts&self.TARACT: tcnt += self.batch_tar_count('N')
370
+ if tcnt > self.MPTLIMIT:
371
+ tproc = (tcnt + self.MPTLIMIT - 1)//self.MPTLIMIT
372
+ if tproc > self.MPTMAX: tproc = self.MPTMAX
373
+ if tproc > mproc: mproc = tproc
374
+ if acts&self.BCKACT:
375
+ bcnt = self.batch_tar_count('T')
376
+ if bcnt > self.MPBLIMIT:
377
+ bproc = (bcnt + self.MPBLIMIT - 1)//self.MPBLIMIT
378
+ if bproc > self.MPBMAX: bproc = self.MPBMAX
379
+ if bproc > mproc: mproc = bproc
380
+ return mproc
381
+
382
+ # count the bfile tar records of a given status honoring backflag/dataset scope
383
+ def batch_tar_count(self, status):
384
+ bcnd = "status = '{}'".format(status)
385
+ if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
386
+ if self.dsids:
387
+ tcnt = 0
388
+ for dsid in self.dsids:
389
+ tcnt += self.pgget('bfile', '', "dsid = '{}' AND {}".format(dsid, bcnd), self.LGWNEX)
390
+ return tcnt
391
+ return self.pgget('bfile', '', bcnd, self.LGWNEX)
392
+
393
+ # look up the dscheck record registered for a worker's argv, using the same lookup key
394
+ # init_dscheck builds
395
+ def lookup_worker_dscheck(self, argv):
396
+ dargv = self.argv_to_string(argv, 0, "Process in Delayed Mode")
397
+ dargx = None
398
+ if len(dargv) > 100:
399
+ dargx = dargv[100:]
400
+ dargv = dargv[0:100]
401
+ return self.get_dscheck("dsquasar", dargv, self.PGBACK['workdir'], self.PGLOG['CURUID'], dargx, self.LOGWRN)
402
+
403
+ # a running batch job counts as stalled once it has run longer than WORKGRACE and has
404
+ # still completed no more than MINWDONE of its recorded work; anything further along
405
+ # than that is progressing and is left alone. the done fraction dcount/fcount is used
406
+ # instead of a raw count because dcount counts GDEX files for -A 3 but tar files for
407
+ # -A 2/-A 4. a job that is not locked, not started yet, or whose process is gone is not
408
+ # stalled: init_dscheck already restarts a dead one.
409
+ def worker_progress_stalled(self, pgrec):
410
+ if not (pgrec['pid'] and pgrec['stttime']): return False
411
+ if self.check_host_pid(pgrec['lockhost'], pgrec['pid']) <= 0: return False
412
+ if (tm() - pgrec['stttime']) < self.WORKGRACE: return False
413
+ done = pgrec['dcount']/pgrec['fcount'] if pgrec['fcount'] else 0
414
+ return done <= self.MINWDONE
415
+
416
+ # whether more than one worker may run the current action. two workers stay off each
417
+ # other's files only through dataset locking, so the action must lock what it works on:
418
+ # creating input files and building tar files do (backup_dataset_files and
419
+ # backup_dataset_infiles lock each dsid), but transferring (BCKACT) walks the status 'T'
420
+ # records by bid without locking, so two workers would transfer the same tar files twice.
421
+ def extra_workers_allowed(self):
422
+ return bool(self.PGBACK['dolock'] and not self.PGBACK['action']&self.BCKACT)
423
+
424
+ # pick the worker slot to submit into. slot 1 uses the invocation argv unchanged so a
425
+ # repeat submit still blocks as a duplicate. when a slot is held by a running job that
426
+ # is stalled, the next slot is tried instead, and appending -w N to the argv gives that
427
+ # extra worker its own dscheck record and PBS job. returns the slot and the dscheck
428
+ # record already registered for it, if any.
429
+ def pick_worker_slot(self):
430
+ if self.PGBACK['worker'] > 1: # an explicit -w N forces that slot
431
+ return (self.PGBACK['worker'], self.lookup_worker_dscheck(sys.argv[1:]))
432
+ pgrec = self.lookup_worker_dscheck(sys.argv[1:])
433
+ if not pgrec or not self.worker_progress_stalled(pgrec): return (1, pgrec)
434
+ # worker 1 is running but stalled: submit an extra worker into the first slot with no
435
+ # dscheck record yet. a slot that is already registered is left alone, so an extra
436
+ # worker still waiting to be started by the dscheck daemon is never taken over and
437
+ # run here instead, outside PBS.
438
+ maxw = self.PGBACK['maxworkers'] if self.extra_workers_allowed() else 1
439
+ for slot in range(2, maxw + 1):
440
+ if not self.lookup_worker_dscheck(sys.argv[1:] + ['-w', str(slot)]):
441
+ self.pglog("Worker 1 is running with {} of {} done: adding worker {}".format(
442
+ pgrec['dcount'], pgrec['fcount'], slot), self.LOGWRN)
443
+ return (slot, None)
444
+ return (1, pgrec) # no free slot; block on worker 1 as usual
445
+
334
446
  # start none daemon and intialize dscheck counts
335
447
  def start_dsquasar_none_daemon(self, act, fcnt = 0):
336
448
  acts = self.PGBACK['action']
449
+ cact = ('A' if acts < 10 else '') + str(acts)
337
450
  if self.bopts != None:
338
451
  dsid = self.dsids[0] if len(self.dsids) == 1 else ''
339
452
  if not re.match(r'^[a-z]\d{6}$', dsid): dsid = ''
340
- cact = ('A' if acts < 10 else '') + str(acts)
341
- if act == self.STTACT or acts&self.NBACTS != acts:
342
- self.set_one_boption('qoptions', '-l walltime=24:00:00', 1)
453
+ # only the initial delayed submit (not yet under PBS) sets qoptions for the batch
454
+ # job the dscheck daemon will start; size up multi-processing from the detail file
455
+ # count (unless an explicit -m N was given) and reserve matching PBS cpus. a
456
+ # command-line or the PBS batch run does not set qoptions.
457
+ if self.PGLOG['CURBID'] < 1 and (act == self.STTACT or acts&self.NBACTS != acts):
458
+ # pick the worker slot first; slot 1 keeps the invocation argv so a repeat
459
+ # submit still blocks as a duplicate, while a stalled run hands the next slot
460
+ # its own -w N argv. only size up multi-processing - which checks the backup
461
+ # files - when the chosen slot has no dscheck record on file yet, so a
462
+ # duplicate submit blocks without checking the backup files.
463
+ (slot, pgrec) = self.pick_worker_slot()
464
+ if slot > 1:
465
+ self.PGBACK['worker'] = slot
466
+ sys.argv.extend(['-w', str(slot)])
467
+ self.PGBACK['cmd'] += " -w {}".format(slot)
468
+ if not pgrec:
469
+ qoptions = '-l walltime=24:00:00'
470
+ if acts&self.NBACTS != acts:
471
+ if self.PGBACK['mproc'] < 2: self.PGBACK['mproc'] = self.batch_process_count(acts)
472
+ if self.PGBACK['mproc'] > 1:
473
+ qoptions += ",select=1:ncpus={0}:mem={0}gb".format(self.PGBACK['mproc'])
474
+ self.set_one_boption('qoptions', qoptions, 1)
343
475
  self.init_dscheck(0, '', "dsquasar", dsid, cact, self.PGBACK['workdir'],
344
476
  self.PGLOG['CURUID'], self.bopts, self.LOGWRN)
345
- if self.PGBACK['mproc'] > 1: self.PGBACK['mproc'] = 1
477
+ # on the PBS batch run, take the process count from the reserved ncpus in qoptions
478
+ # instead of recomputing it
479
+ if self.PGLOG['DSCHECK'] and self.PGBACK['mproc'] < 2 and acts&self.NBACTS != acts:
480
+ pgrec = self.pgget('dscheck', 'qoptions',
481
+ "cindex = {}".format(self.PGLOG['DSCHECK']['cindex']), self.LGEREX)
482
+ if pgrec and pgrec['qoptions']:
483
+ ms = re.search(r'ncpus=(\d+)', pgrec['qoptions'])
484
+ if ms: self.PGBACK['mproc'] = int(ms.group(1))
485
+ if self.PGBACK['mproc'] > 1 and acts&self.NBACTS == acts: self.PGBACK['mproc'] = 1
346
486
  elif self.PGBACK['mproc'] > 1 and acts&self.NBACTS == acts:
347
487
  self.PGBACK['mproc'] = 1
348
- self.start_none_daemon('dsquasar', '', self.PGLOG['CURUID'], self.PGBACK['mproc'], 60, 1)
488
+ # tag the daemon string with the action, and with the worker slot for an extra worker,
489
+ # so that concurrent workers can be told apart in the log
490
+ dact = cact
491
+ if self.PGBACK['worker'] > 1: dact += "-w{}".format(self.PGBACK['worker'])
492
+ self.start_none_daemon('dsquasar', dact, self.PGLOG['CURUID'], self.PGBACK['mproc'], 60, 1)
349
493
  if self.PGLOG['DSCHECK']:
350
494
  if act == self.STTACT:
351
495
  fcnt = self.gather_dataset_bckfiles(None, False)
@@ -409,13 +553,17 @@ class DsQuasar(PgCMD, PgSplit):
409
553
  dmsg = list(bfiles)[0] if dcnt == 1 else "{} datasets".format(dcnt)
410
554
  self.pglog("{}: {} for {} ...".format(amsg, bmsg, dmsg), self.WARNLG)
411
555
  qinfo = {'backflag' : backflag, 'bid' : self.current_bid(), 'dsids' : [], 'fcnt' : 0,
412
- 'size' : 0, 'infiles' : [], 'instr' : '', 'qdsids' : [],
556
+ 'size' : 0, 'infiles' : [], 'instr' : '', 'qdsids' : [], 'abids' : [],
413
557
  'dslocks' : [], 'qfcnt' : 0, 'qsize' : 0, 'qcnt' : 0}
414
558
  for dsid in bfiles:
415
559
  if len(qinfo['dsids']) == self.DSCNT: self.process_one_backup_file(qinfo, True, False)
416
560
  if self.PGBACK['dolock'] and dsid not in qinfo['dslocks']:
417
561
  if self.lock_dataset(dsid, 1, self.LOGERR) < 1: continue
418
562
  qinfo['dslocks'].append(dsid)
563
+ if not self.regather_dataset_files(bfiles, dsid, backflag):
564
+ self.lock_dataset(dsid, 0, self.LGEREX)
565
+ qinfo['dslocks'].remove(dsid)
566
+ continue
419
567
  fcnt = bfiles[dsid]['scount']
420
568
  if fcnt > 0:
421
569
  self.process_backup_files(qinfo, dsid, fcnt, bfiles[dsid]['srecs'], 'S')
@@ -427,6 +575,9 @@ class DsQuasar(PgCMD, PgSplit):
427
575
  self.lock_dataset(dsid, 0, self.LGEREX)
428
576
  qinfo['dslocks'].remove(dsid)
429
577
  qinfo['dsids'] = []
578
+ if self.PGBACK['mproc'] > 1:
579
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
580
+ self.confirm_quasar_counts(qinfo, qinfo['abids'], "status = 'T'") # recount confirmed backups from RDADB
430
581
  if qinfo['dslocks']:
431
582
  for dsid in qinfo['dslocks']:
432
583
  self.lock_dataset(dsid, 0, self.LGEREX) # unlock the locked datasets
@@ -453,6 +604,44 @@ class DsQuasar(PgCMD, PgSplit):
453
604
  msg = "{}: {}({}) GDEX {}file{} of {} for next {}".format(amsg, fcnt, ssize, cmsg, s, dmsg, bmsg)
454
605
  self.pglog(self.INDENT + msg, self.LOGACT)
455
606
 
607
+ # the file list was gathered before this dataset was locked, so it can be hours old and
608
+ # name files another worker has backed up since. re-gather it now that the dataset is
609
+ # locked and no other worker can add backup files for it, and replace the stale list.
610
+ # returns the file count left to back up for the given backup flag.
611
+ def regather_dataset_files(self, bfiles, dsid, backflag):
612
+ pgrec = self.pgget("dataset", "backflag", "dsid = '{}'".format(dsid), self.LGWNEX)
613
+ if not pgrec: return 0
614
+ dsfiles = {'B' : {}, 'D' : {}}
615
+ self.get_dataset_files(dsid, dsfiles, pgrec['backflag'])
616
+ if dsid not in dsfiles[backflag]:
617
+ self.pglog("{}: no {} file left to back up, skip".format(dsid, self.BACKMSG[backflag]), self.DTLACT)
618
+ return 0
619
+ bfiles[dsid] = dsfiles[backflag][dsid]
620
+ return bfiles[dsid]['scount'] + bfiles[dsid]['wcount']
621
+
622
+ # lock the other datasets whose files share one tar file, so that no other worker gathers
623
+ # or tars their files while this tar file is being claimed. datasets already locked by this
624
+ # run, including the primary one, are left alone. returns the datasets locked here, to be
625
+ # unlocked once the tar file is dispatched, or None if one of them is held by another
626
+ # worker, in which case nothing new is left locked and the tar file must wait for a later run
627
+ def lock_backup_datasets(self, qinfo, dsids):
628
+ dslocks = []
629
+ if not self.PGBACK['dolock']: return dslocks
630
+ for dsid in dsids:
631
+ if dsid in qinfo['dslocks']: continue
632
+ if self.lock_dataset(dsid, 1, self.LOGERR) < 1:
633
+ self.unlock_backup_datasets(qinfo, dslocks)
634
+ return None
635
+ qinfo['dslocks'].append(dsid)
636
+ dslocks.append(dsid)
637
+ return dslocks
638
+
639
+ # unlock the given datasets and drop them from the lock list tracked for cleanup on quit
640
+ def unlock_backup_datasets(self, qinfo, dslocks):
641
+ for dsid in dslocks:
642
+ self.lock_dataset(dsid, 0, self.LOGERR)
643
+ if dsid in qinfo['dslocks']: qinfo['dslocks'].remove(dsid)
644
+
456
645
  # get the current bid for adding bfile
457
646
  def current_bid(self):
458
647
  pgrec = self.pgget("bfile", "max(bid) mid", '', self.LGEREX)
@@ -475,16 +664,36 @@ class DsQuasar(PgCMD, PgSplit):
475
664
  bmsg = self.BACKMSG[backflag]
476
665
  s = 's' if bcnt > 1 else ''
477
666
  self.pglog("{}: {} {} file{}...".format(amsg, bcnt, bmsg, s), self.WARNLG)
478
- qinfo = {'backflag' : backflag, 'bid' : 0, 'dsids' : [], 'fcnt' : 0, 'size' : 0,
479
- 'infiles' : [], 'instr' : '', 'qdsids' : [], 'qfcnt' : 0, 'qsize' : 0, 'qcnt' : 0}
667
+ qinfo = {'backflag' : backflag, 'bid' : 0, 'dsids' : [], 'fcnt' : 0, 'size' : 0, 'abids' : [],
668
+ 'infiles' : [], 'instr' : '', 'qdsids' : [], 'dslocks' : [], 'qfcnt' : 0,
669
+ 'qsize' : 0, 'qcnt' : 0}
480
670
  for dsid in bfiles:
481
- if self.PGBACK['dolock'] and self.lock_dataset(dsid, 1, self.LOGERR) < 1: continue
671
+ if self.PGBACK['dolock']:
672
+ if self.lock_dataset(dsid, 1, self.LOGERR) < 1: continue
673
+ qinfo['dslocks'].append(dsid)
482
674
  for bid in bfiles[dsid]:
675
+ # the status 'N' records were gathered before this dataset was locked, so they
676
+ # can be hours old; confirm each one is still untarred before spending a tar on
677
+ # it. holding the dataset lock makes this check race free against a second
678
+ # worker, which cannot hold the same dsid at the same time.
679
+ if not self.pgget('bfile', '', "bid = {} AND status = 'N'".format(bid), self.LGEREX):
680
+ self.pglog("{}-{}: no longer waiting to be tarred, skip".format(dsid, bid), self.DTLACT)
681
+ continue
483
682
  qinfo['bid'] = bid
484
683
  binfo = bfiles[dsid][bid]
485
684
  for bkey in binfo: qinfo[bkey] = binfo[bkey]
685
+ # a tar file can hold files of multiple datasets; lock the other ones too before
686
+ # tarring, and release them again as soon as the tar file is dispatched
687
+ dslocks = self.lock_backup_datasets(qinfo, binfo['dsids'])
688
+ if dslocks is None:
689
+ self.pglog("{}-{}: another dataset of the tar file is locked, skip".format(dsid, bid), self.DTLACT)
690
+ continue
486
691
  self.process_one_backup_file(qinfo, False)
487
- if self.PGBACK['dolock']: self.lock_dataset(dsid, 0, self.LOGERR)
692
+ self.unlock_backup_datasets(qinfo, dslocks)
693
+ if self.PGBACK['dolock']: self.unlock_backup_datasets(qinfo, [dsid])
694
+ if self.PGBACK['mproc'] > 1:
695
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
696
+ self.confirm_quasar_counts(qinfo, qinfo['abids'], "status = 'T'") # recount confirmed backups from RDADB
488
697
  qcnt = qinfo['qcnt']
489
698
  if qcnt > 0:
490
699
  self.PGBACK['bckcnt'] += qcnt
@@ -522,6 +731,9 @@ class DsQuasar(PgCMD, PgSplit):
522
731
  if qinfo['size'] < self.BCKSIZE: continue
523
732
  self.transfer_quasar_tarfiles(qinfo)
524
733
  if qinfo['size'] > 0: self.transfer_quasar_tarfiles(qinfo)
734
+ if self.PGBACK['mproc'] > 1:
735
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
736
+ self.confirm_quasar_counts(qinfo, list(bfiles), "status = 'A'") # recount confirmed backups from RDADB
525
737
  qcnt = qinfo['qcnt']
526
738
  if qcnt > 0:
527
739
  self.PGBACK['bckcnt'] += qcnt
@@ -562,11 +774,79 @@ class DsQuasar(PgCMD, PgSplit):
562
774
  if self.PGBACK['pstep']: self.record_dscheck_status("F")
563
775
  self.pgexit(0)
564
776
 
777
+ # guard the -A 3 (Create Input&Tar, build status 'N' infiles) and -A 4 (Transfer,
778
+ # send status 'T' tars) PBS batch jobs against the 24-hour walltime: once past
779
+ # MAXRUNTIME, if the tar files still to process cannot finish within the last hour at
780
+ # the rate achieved so far, send a progress email and stop cleanly so the report is
781
+ # not lost to a PBS timeout; the remaining records are left for the next scheduled run
782
+ # to resume. a no-op for command-line runs, other actions, or before the cutoff.
783
+ def check_batch_deadline(self, qinfo):
784
+ act = self.PGBACK['action']
785
+ if self.PGLOG['CURBID'] < 1 or act not in (self.CTACTS, self.BCKACT): return
786
+ elapsed = tm() - self.PGBACK['starttime']
787
+ if elapsed < self.MAXRUNTIME: return
788
+ status, work = ('N', 'build') if act == self.CTACTS else ('T', 'transfer')
789
+ remaining = self.batch_tar_count(status)
790
+ if remaining < 1: return
791
+ done = self.PGBACK['tardone']
792
+ need = remaining*elapsed/done if done > 0 else elapsed
793
+ if need <= self.ONEHOUR: return # enough time left to finish before the walltime
794
+ if self.PGBACK['mproc'] > 1: self.check_child(None, 0, self.LOGWRN, 1) # wait all children
795
+ if qinfo and qinfo.get('dslocks'):
796
+ for dsid in qinfo['dslocks']: self.lock_dataset(dsid, 0, self.LGEREX)
797
+ etime = self.seconds_to_string_time(int(elapsed))
798
+ amsg = self.ACTMSG[act]
799
+ 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)
800
+ self.pglog(self.INDENT + msg, self.LOGACT)
801
+ if self.PGBACK['doemail']:
802
+ bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
803
+ self.set_email("{}: {} - stopped early before the PBS walltime with {} {} tar file(s) remaining!".format(self.PGBACK['cmd'], amsg, remaining, bmsg), self.EMLTOP)
804
+ title = "dsquasar: {} stopped early ({} remaining)".format(amsg, remaining)
805
+ if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
806
+ if self.PGLOG['DSCHECK']:
807
+ self.build_customized_email("dscheck", "einfo", "cindex = {}".format(self.PGLOG['DSCHECK']['cindex']), title, self.LOGWRN)
808
+ else:
809
+ self.pglog(title, self.LOGWRN|self.SNDEML)
810
+ if self.PGBACK['pstep']: self.record_dscheck_status("D")
811
+ self.pgexit(0)
812
+
813
+ # recompute the confirmed backup counts from RDADB after all child processes
814
+ # finished, so a multi-process summary reflects succeeded (not just started) work
815
+ def confirm_quasar_counts(self, qinfo, bids, dcnd):
816
+ qinfo['qcnt'] = qinfo['qfcnt'] = qinfo['qsize'] = 0
817
+ qinfo['qdsids'] = []
818
+ if not bids: return
819
+ cnd = "{} AND bid IN ({})".format(dcnd, ','.join(str(bid) for bid in bids))
820
+ pgrecs = self.pgmget('bfile', "dsid, dsids, data_size size, (scount + wcount) fcnt", cnd, self.LGEREX)
821
+ bcnt = len(pgrecs['dsid']) if pgrecs else 0
822
+ for i in range(bcnt):
823
+ qinfo['qcnt'] += 1
824
+ qinfo['qfcnt'] += pgrecs['fcnt'][i]
825
+ qinfo['qsize'] += pgrecs['size'][i]
826
+ dsid = pgrecs['dsid'][i]
827
+ if dsid not in qinfo['qdsids']: qinfo['qdsids'].append(dsid)
828
+ if pgrecs['dsids'][i]:
829
+ for did in pgrecs['dsids'][i].split(','):
830
+ if did and did not in qinfo['qdsids']: qinfo['qdsids'].append(did)
831
+
832
+ # dsarch reports "<dsid>-<file>: <type> file backed up to /<dsid>/<bfile> by <date>" for
833
+ # each file to tar that is already backed up. our own record is the duplicate to drop only
834
+ # when those files point at some other bfile; a report naming our own bfile means this
835
+ # record is the one already backed up, so it must be kept.
836
+ def duplicate_backup_file(self, dsid, qfile):
837
+ ours = "/{}/{}".format(dsid, qfile)
838
+ isdup = False
839
+ for ms in re.finditer(r'file backed up to (\S+) by ', self.PGLOG['SYSERR']):
840
+ if ms.group(1) == ours: return False
841
+ isdup = True
842
+ return isdup
843
+
565
844
  # backup one Quasar Backup or Backup&Drdata from one or multiple inputs and,
566
845
  # reset the quasar backup dict
567
846
  def process_one_backup_file(self, qinfo, addback, keepid = False):
568
847
  ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
569
848
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
849
+ self.check_batch_deadline(qinfo)
570
850
  dsids = qinfo['dsids']
571
851
  dcnt = len(dsids)
572
852
  if dcnt == 0: return
@@ -580,6 +860,7 @@ class DsQuasar(PgCMD, PgSplit):
580
860
  else:
581
861
  bid = qinfo['bid']
582
862
  qfile = qinfo['bfile']
863
+ if 'abids' in qinfo: qinfo['abids'].append(bid) # track dispatched bids for confirmed recount
583
864
  stat = 1
584
865
  if self.PGBACK['action'] > self.CINACT:
585
866
  s = 's' if fcnt > 1 else ''
@@ -599,9 +880,9 @@ class DsQuasar(PgCMD, PgSplit):
599
880
  stat = self.pgsystem(cmd, self.ERRACT, 325) # 256 + 64 + 4 + 1
600
881
  if stat:
601
882
  for infile in qinfo['infiles']: self.delete_local_file(infile)
602
- elif re.search(r'file backed up to', self.PGLOG['SYSERR']):
883
+ elif self.duplicate_backup_file(dsid, qfile):
603
884
  if self.pgdel('bfile', f"bid = {bid}"):
604
- self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplicattion", self.DTLACT)
885
+ self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplication", self.DTLACT)
605
886
  for infile in qinfo['infiles']: self.delete_local_file(infile)
606
887
  sys.exit(0) # stop child process
607
888
  else:
@@ -612,9 +893,9 @@ class DsQuasar(PgCMD, PgSplit):
612
893
  stat = self.pgsystem(cmd, self.ERRACT, 325) # 256 + 64 + 4 + 1
613
894
  if stat:
614
895
  for infile in qinfo['infiles']: self.delete_local_file(infile)
615
- elif re.search(r'file backed up to', self.PGLOG['SYSERR']):
896
+ elif self.duplicate_backup_file(dsid, qfile):
616
897
  if self.pgdel('bfile', f"bid = {bid}"):
617
- self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplicattion", self.DTLACT)
898
+ self.pglog(f"{dsid}-{qfile}: backup tarfile deleted for duplication", self.DTLACT)
618
899
  for infile in qinfo['infiles']: self.delete_local_file(infile)
619
900
  else:
620
901
  self.PGBACK['errcnt'] += 1
@@ -623,12 +904,13 @@ class DsQuasar(PgCMD, PgSplit):
623
904
  if stat:
624
905
  # reset qinfo after quasar backup
625
906
  qinfo['qcnt'] += 1
907
+ self.PGBACK['tardone'] += 1 # cumulative dispatched tars for the walltime finish-rate
626
908
  qinfo['qfcnt'] += fcnt
627
909
  qinfo['qsize'] += fsize
628
910
  for dsid in dsids:
629
911
  if dsid not in qinfo['qdsids']: qinfo['qdsids'].append(dsid)
630
912
  if addback:
631
- lastdsid = dsids.pop() if keepid else None
913
+ lastdsid = dsids.pop() if keepid else None
632
914
  for dsid in dsids: # unlock all datasets but the last one
633
915
  if dsid in qinfo['dslocks']:
634
916
  self.lock_dataset(dsid, 0, self.LGEREX)
@@ -645,6 +927,7 @@ class DsQuasar(PgCMD, PgSplit):
645
927
  def transfer_quasar_tarfiles(self, qinfo):
646
928
  ccnt = self.check_child(None, 0, self.LOGWRN, -1) if self.PGBACK['mproc'] > 1 else 0
647
929
  if self.PGSIG['QUIT']: self.quit_dsquasar(qinfo)
930
+ self.check_batch_deadline(qinfo)
648
931
  # prepare for backup one tar file
649
932
  dsids = qinfo['dsids']
650
933
  bids = qinfo['bids']
@@ -675,10 +958,10 @@ class DsQuasar(PgCMD, PgSplit):
675
958
  dstat = bstat = -1
676
959
  if ccnt == 0 or self.PGSIG['PPID'] > 1:
677
960
  if bflg == 'D':
678
- dstat = self.quasar_multiple_trasnfer(tofiles, fromfiles, 'gdex-quasar-drdata', 'gdex-glade', self.ERRACT)
961
+ dstat = self.quasar_multiple_transfer(tofiles, fromfiles, 'gdex-quasar-drdata', 'gdex-glade', self.ERRACT)
679
962
  if not dstat: self.pglog("Error Quaser Drdata for " + fmsg, self.ERRACT|self.LOGERR)
680
- bstat = self.quasar_multiple_trasnfer(tofiles, fromfiles, 'gdex-quasar', 'gdex-glade', self.ERRACT)
681
- if not dstat: self.pglog("Error Quaser Backup for " + fmsg, self.ERRACT|self.LOGERR)
963
+ bstat = self.quasar_multiple_transfer(tofiles, fromfiles, 'gdex-quasar', 'gdex-glade', self.ERRACT)
964
+ if not bstat: self.pglog("Error Quaser Backup for " + fmsg, self.ERRACT|self.LOGERR)
682
965
  if dstat == self.FINISH: dstat = self.check_globus_finished(tofiles[0], 'gdex-quasar-drdata', self.ERRACT|self.NOWAIT)
683
966
  if bstat == self.FINISH: bstat = self.check_globus_finished(tofiles[0], 'gdex-quasar', self.ERRACT|self.NOWAIT)
684
967
  if dstat and bstat:
@@ -698,6 +981,7 @@ class DsQuasar(PgCMD, PgSplit):
698
981
  if dstat and bstat:
699
982
  # reset qinfo after quasar backup
700
983
  qinfo['qcnt'] += bcnt
984
+ self.PGBACK['tardone'] += bcnt # cumulative transferred tars for the walltime finish-rate
701
985
  qinfo['qfcnt'] += fcnt
702
986
  qinfo['qsize'] += fsize
703
987
  for dsid in dsids:
@@ -777,7 +1061,7 @@ class DsQuasar(PgCMD, PgSplit):
777
1061
  return {'wcount' : 0, 'wrecs' : [], 'scount' : 0, 'srecs' : []}
778
1062
 
779
1063
  # get all files need to be backed up for a given dataset ID
780
- def get_dataset_files(self, dsid, dsfiles, backflag, logact = 0):
1064
+ def get_dataset_files(self, dsid, dsfiles, backflag, logact = 0, sizes = None):
781
1065
  dcnd = "dsid = '{}'".format(dsid)
782
1066
  fopt = self.get_backup_options(dsid, dcnd)
783
1067
  if not fopt:
@@ -817,6 +1101,7 @@ class DsQuasar(PgCMD, PgSplit):
817
1101
  self.pglog("{}-G{}: Found {} Files ({})".format(dsid, gidx, rcnt, rmsg), self.LOGWRN)
818
1102
 
819
1103
  fcnt = cnts['B'][0] + cnts['D'][0]
1104
+ if sizes is not None: sizes[0] += cnts['B'][1] + cnts['D'][1]
820
1105
  if dsfiles:
821
1106
  cmsg = 'Changed ' if self.PGBACK['chgdays'] else ''
822
1107
  for bflg in dsfiles:
@@ -836,7 +1121,6 @@ class DsQuasar(PgCMD, PgSplit):
836
1121
 
837
1122
  # get web/saved files to backup for a given group condition and file option
838
1123
  def get_group_files(self, dsid, gcnd, dcnd, bfiles, fopt, infiles, cnts):
839
- rcnt = 0
840
1124
  if self.PGBACK['chgdays'] > 0:
841
1125
  return self.get_group_changed_files(dsid, gcnd, dcnd, bfiles, fopt, infiles, cnts)
842
1126
  else:
@@ -982,28 +1266,37 @@ class DsQuasar(PgCMD, PgSplit):
982
1266
  def get_backup_options(self, dsid, dcnd):
983
1267
  fopt = 0
984
1268
  bcnd = "= 0" if self.PGBACK['chgdays'] < 1 else "> 0"
985
- # if self.pgget('sfile', 'sid', "{} AND bid {}".format(dcnd, bcnd), self.LGWNEX): fopt |= self.SOPT
986
1269
  if self.pgget_wfile(dsid, 'wid', "bid " + bcnd, self.LGWNEX): fopt |= self.WOPT
987
1270
  return fopt
988
1271
 
1272
+ # dataset ordering for gathering work; even worker slots start from the high end (d999999)
1273
+ # and odd ones from the low end, so that a pair of workers meets in the middle instead of
1274
+ # fighting over the same dataset locks
1275
+ def dsid_order(self):
1276
+ return " ORDER BY dsid DESC" if self.reverse_dsids() else " ORDER BY dsid"
1277
+
1278
+ # whether this worker slot walks the datasets from the high end
1279
+ def reverse_dsids(self):
1280
+ return self.PGBACK['worker']%2 == 0
1281
+
989
1282
  # gather all available dataset ids to backup data files
990
- def gather_dataset_files(self, dsfiles, unlock = True):
1283
+ def gather_dataset_files(self, dsfiles, unlock = True, sizes = None):
991
1284
  fcnt = 0
1285
+ # warn per named dataset with no files; stay silent when scanning all datasets
1286
+ logact = self.LOGWRN if self.dsids else 0
992
1287
  if self.dsids:
1288
+ pgrecs = []
993
1289
  for dsid in self.dsids:
994
- dcnd = "dsid = '{}'".format(dsid)
995
- pgrec = self.pgget("dataset", "dsid, backflag, pid", dcnd, self.LGWNEX)
996
- if pgrec:
997
- if unlock and pgrec['pid'] and self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
998
- fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], self.LOGWRN)
1290
+ pgrec = self.pgget("dataset", "dsid, backflag, pid", "dsid = '{}'".format(dsid), self.LGWNEX)
1291
+ if pgrec: pgrecs.append(pgrec)
999
1292
  else:
1000
- dcnd = "ORDER BY dsid"
1001
- pgrecs = self.pgmget("dataset", "dsid, backflag, pid", dcnd, self.LGWNEX)
1002
- dcnt = len(pgrecs['dsid']) if pgrecs else 0
1003
- for i in range(dcnt):
1004
- dsid = pgrecs['dsid'][i]
1005
- if unlock and pgrecs['pid'][i] and self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
1006
- fcnt += self.get_dataset_files(dsid, dsfiles, pgrecs['backflag'][i])
1293
+ mrecs = self.pgmget("dataset", "dsid, backflag, pid", self.dsid_order().strip(), self.LGWNEX)
1294
+ dcnt = len(mrecs['dsid']) if mrecs else 0
1295
+ pgrecs = [self.onerecord(mrecs, i) for i in range(dcnt)]
1296
+ for pgrec in pgrecs:
1297
+ dsid = pgrec['dsid']
1298
+ if unlock and pgrec['pid'] and self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
1299
+ fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact, sizes)
1007
1300
  if dsfiles:
1008
1301
  s = 's' if fcnt > 1 else ''
1009
1302
  bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
@@ -1013,28 +1306,21 @@ class DsQuasar(PgCMD, PgSplit):
1013
1306
 
1014
1307
  # gather all available backup file records with status N to backup
1015
1308
  def gather_dataset_infiles(self, dsfiles):
1016
- icnt = fcnt = cnt = 0
1309
+ icnt = fcnt = 0
1017
1310
  bcnd = "status = 'N'"
1018
- ocnd = " ORDER BY dsid"
1019
1311
  flds = "bid, dsid, bfile, type, data_size size, (scount + wcount) fcnt, dsids, note"
1020
1312
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
1021
1313
  if self.dsids:
1022
- for dsid in self.dsids:
1023
- dcnd = "dsid = '{}' AND {}".format(dsid, bcnd)
1024
- pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1025
- bcnt = len(pgrecs['bid']) if pgrecs else 0
1026
- for i in range(bcnt):
1027
- cnt = self.get_backup_infile(dsfiles, self.onerecord(pgrecs, i))
1028
- if cnt:
1029
- icnt +=1
1030
- fcnt += cnt
1314
+ cnds = ["dsid = '{}' AND {}".format(dsid, bcnd) for dsid in self.dsids]
1031
1315
  else:
1032
- pgrecs = self.pgmget("bfile", flds, bcnd+ocnd, self.LGWNEX)
1316
+ cnds = [bcnd + self.dsid_order()]
1317
+ for dcnd in cnds:
1318
+ pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1033
1319
  bcnt = len(pgrecs['bid']) if pgrecs else 0
1034
1320
  for i in range(bcnt):
1035
1321
  cnt = self.get_backup_infile(dsfiles, self.onerecord(pgrecs, i))
1036
1322
  if cnt:
1037
- icnt +=1
1323
+ icnt += 1
1038
1324
  fcnt += cnt
1039
1325
  if dsfiles:
1040
1326
  s = 's' if icnt > 1 else ''
@@ -1045,28 +1331,21 @@ class DsQuasar(PgCMD, PgSplit):
1045
1331
  # gather all available backup file records with status T to backup
1046
1332
  #
1047
1333
  def gather_dataset_tarfiles(self, dsfiles):
1048
- tcnt = fcnt = cnt = 0
1334
+ tcnt = fcnt = 0
1049
1335
  bcnd = "status = 'T'"
1050
- ocnd = " ORDER BY dsid"
1051
1336
  flds = "bid, dsid, bfile, type, data_size size, (scount + wcount) fcnt, note"
1052
1337
  if self.PGBACK['backflag']: bcnd += " AND type = '{}'".format(self.PGBACK['backflag'])
1053
1338
  if self.dsids:
1054
- for dsid in self.dsids:
1055
- dcnd = "dsid = '{}' AND {}".format(dsid, bcnd)
1056
- pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1057
- bcnt = len(pgrecs['bid']) if pgrecs else 0
1058
- for i in range(bcnt):
1059
- cnt = self.get_backup_tarfile(dsfiles, self.onerecord(pgrecs, i))
1060
- if cnt:
1061
- tcnt +=1
1062
- fcnt += cnt
1339
+ cnds = ["dsid = '{}' AND {}".format(dsid, bcnd) for dsid in self.dsids]
1063
1340
  else:
1064
- pgrecs = self.pgmget("bfile", flds, bcnd+ocnd, self.LGWNEX)
1341
+ cnds = [bcnd + self.dsid_order()]
1342
+ for dcnd in cnds:
1343
+ pgrecs = self.pgmget("bfile", flds, dcnd, self.LGWNEX)
1065
1344
  bcnt = len(pgrecs['bid']) if pgrecs else 0
1066
1345
  for i in range(bcnt):
1067
1346
  cnt = self.get_backup_tarfile(dsfiles, self.onerecord(pgrecs, i))
1068
1347
  if cnt:
1069
- tcnt +=1
1348
+ tcnt += 1
1070
1349
  fcnt += cnt
1071
1350
  s = 's' if tcnt > 1 else ''
1072
1351
  bmsg = self.BACKMSG[self.PGBACK['backflag']] if self.PGBACK['backflag'] else 'backup'
@@ -1092,7 +1371,6 @@ class DsQuasar(PgCMD, PgSplit):
1092
1371
  if self.PGBACK['action']&self.CINACT: ret = fcnt
1093
1372
  if not dsfiles: return ret
1094
1373
  dsids = [dsid]
1095
- # if not (pgrec['note'] or self.rebuild_file_note(pgrec, dsids)): return 0
1096
1374
  if not pgrec['note']:
1097
1375
  return self.pglog(bfile + ": Miss note for N backup file", self.ERRACT)
1098
1376
  infiles = []
@@ -1147,7 +1425,6 @@ class DsQuasar(PgCMD, PgSplit):
1147
1425
  if self.PGBACK['action']&self.CINACT: ret = fcnt
1148
1426
  if not dsfiles: return ret
1149
1427
  dsids = [dsid]
1150
- # if not (pgrec['note'] or self.rebuild_file_note(pgrec, dsids)): return 0
1151
1428
  if not pgrec['note']:
1152
1429
  return self.pglog(bfile + ": Miss note for T backup file", self.ERRACT)
1153
1430
  ftype = None
@@ -1347,6 +1624,9 @@ class DsQuasar(PgCMD, PgSplit):
1347
1624
  binfo = bfiles[bid]
1348
1625
  for bkey in binfo: qinfo[bkey] = binfo[bkey]
1349
1626
  self.process_one_quasar_pathfile(qinfo)
1627
+ if self.PGBACK['mproc'] > 1:
1628
+ self.check_child(None, 0, self.LOGWRN, 1) # wait all child processes done
1629
+ self.confirm_quasar_counts(qinfo, list(bfiles), "bfile LIKE 'G%/%.tar'") # recount confirmed renames from RDADB
1350
1630
  qcnt = qinfo['qcnt']
1351
1631
  if qcnt > 0:
1352
1632
  s = 's' if qcnt > 1 else ''
@@ -1671,43 +1951,6 @@ class DsQuasar(PgCMD, PgSplit):
1671
1951
  dsfiles[pgrec['type']][pgrec['bid']] = {'note' : pgrec['note'], 'fcnt' : pgrec['fcnt'], 'size' : pgrec['size'],
1672
1952
  'date' : pgrec['date_modified'], 'time' : pgrec['time_modified']}
1673
1953
 
1674
- # rebuild missed file note
1675
- def rebuild_file_note(self, pgrec, dsids):
1676
- bid = pgrec['bid']
1677
- fields = 'file, dsid, type, data_size, checksum'
1678
- bfiles = {dsids[0] : {'S' : [], 'W' : []}}
1679
- pgrecs = self.pgmget('sfile', 's' + fields, "bid = {} ORDER BY dsid, sfile".format(bid), self.LGEREX)
1680
- cnt = len(pgrecs['sfile']) if pgrecs else 0
1681
- for i in range(cnt):
1682
- dsid = pgrecs['dsid'][i]
1683
- if dsid not in dsids:
1684
- dsids.append(dsid)
1685
- bfiles[dsid] = {'S' : [], 'W' : []}
1686
- bfiles[dsid]['S'].append([pgrecs['sfile'][i], pgrecs['type'][i], pgrecs['data_size'][i], pgrecs['checksum'][i]])
1687
- pgrecs = self.pgmget('wfile', 'w' + fields, "bid = {} ORDER BY dsid, wfile".format(bid), self.LGEREX)
1688
- cnt = len(pgrecs['wfile']) if pgrecs else 0
1689
- for i in range(cnt):
1690
- dsid = pgrecs['dsid'][i]
1691
- if dsid not in dsids:
1692
- dsids.append(dsid)
1693
- bfiles[dsid] = {'S' : [], 'W' : []}
1694
- bfiles[dsid]['W'].append([pgrecs['wfile'][i], pgrecs['type'][i], pgrecs['data_size'][i], pgrecs['checksum'][i]])
1695
- # create note
1696
- note = ''
1697
- for dsid in bfiles:
1698
- for ftype in bfiles[dsid]:
1699
- files = bfiles[dsid][ftype]
1700
- if not files: continue
1701
- note += "<{}_{}_{}.txt>\n".format(dsid, ftype, bid)
1702
- note += "DS<=>{}\n{}F<:>{}T<:>SZ<:>MC<:>\n".format(dsid, ftype, ftype)
1703
- for flist in files:
1704
- note += "{}<:>{}<:>{}<:>{}<:>\n".format(flist[0], flist[1], flist[2], flist[3])
1705
- if note:
1706
- self.pgexec("UPDATE bfile SET note = '{}' WHERE bid = {}".format(note, bid), self.LGEREX)
1707
- pgrec['note'] = note
1708
- return 1
1709
- return 0
1710
-
1711
1954
  # dump statistics for GDEX files available to create input files
1712
1955
  def dump_dataset_files(self, dsfiles, backflag):
1713
1956
  bfiles = dsfiles[backflag]
@@ -1922,7 +2165,7 @@ class DsQuasar(PgCMD, PgSplit):
1922
2165
  pcnt += 1
1923
2166
  return pcnt
1924
2167
 
1925
- # main function to excecute this script
2168
+ # main function to execute this script
1926
2169
  def main():
1927
2170
  object = DsQuasar()
1928
2171
  object.read_parameters()
@@ -0,0 +1,146 @@
1
+
2
+ Name: dsquasar - back up RDA/GDEX data archives onto the Quasar backup servers
3
+
4
+ Check the Backup flags set in dataset.backflag and/or dsgroup.backflag of RDADB
5
+ to gather Web and Saved files, tar them into larger (>= 2GB) backup files, and
6
+ back them up onto the NCAR GDEX Quasar Globus end points, for a Backup copy
7
+ (gdex-quasar) and/or a disaster-recovery Drdata copy (gdex-quasar-drdata). The
8
+ file tarring and transferring is performed through the utility program dsarch.
9
+
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]
13
+
14
+ Either -a or -t must be given (usage is displayed otherwise). dsquasar runs
15
+ from the Quasar backup work directory; it changes there automatically.
16
+
17
+
18
+ DATASET SELECTION
19
+
20
+ -a Gather files of all datasets that have a Backup flag set.
21
+
22
+ -t DatasetIDs
23
+ One or more dataset IDs whose files are backed up. The SQL wildcard
24
+ '%' is allowed and is expanded against the dataset table. Ignored,
25
+ with a warning, when -a is also given.
26
+
27
+
28
+ BACKUP SCOPE
29
+
30
+ -B Quasar Backup only (endpoint gdex-quasar) for the given datasets.
31
+
32
+ -D Quasar Backup & Drdata (gdex-quasar and gdex-quasar-drdata).
33
+
34
+ -B and -D are mutually exclusive; the default (neither given) backs up
35
+ to both the Backup and the Drdata endpoints.
36
+
37
+ -c ChangeDays
38
+ Used with -A 1 to re-back up files that changed within the given
39
+ number of days. Must be greater than 0.
40
+
41
+
42
+ ACTIONS
43
+
44
+ -A ActionBits
45
+ Bit flags selecting the actions to perform; the valid values are
46
+ 1, 2, 3, 4, 6, 7, 8, 16 and it defaults to 3 (actions 1 + 2):
47
+
48
+ 1 - create the dsarch input Save/Web file lists and add the
49
+ backup file (bfile) placeholder records (status 'N').
50
+ 2 - build tar files (each >= 5GB where possible) from the Saved/
51
+ Web files listed in the input files (status 'N' -> 'T').
52
+ 4 - transfer the built tar files (batched to >= 90GB per Globus
53
+ transfer) to the Quasar backup end points (status 'T' -> 'A').
54
+ 8 - check file sizes between RDADB and the Quasar servers, and
55
+ reset status to 'N' when they differ.
56
+ 16 - dump backup statistics for files backed up and files that are
57
+ ready to be backed up.
58
+
59
+ Combined values: 3 = 1+2, 6 = 2+4, 7 = 1+2+4.
60
+
61
+
62
+ RUN CONTROL
63
+
64
+ -b Turn on background mode; no screen display.
65
+
66
+ -d [HostName] [TryCount]
67
+ Turn on delay mode to run as a batch job. A dscheck record is created
68
+ and the dscheck daemon later submits the job to PBS. On the initial
69
+ submit dsquasar sizes multi-processing from the detail file count and
70
+ reserves matching PBS cpus (walltime + select=1:ncpus=N:mem=Ngb) in the
71
+ dscheck qoptions; the invocation arguments are left unchanged so a
72
+ repeat submit with the same arguments still blocks as a duplicate.
73
+
74
+ -l (Y|N)
75
+ Whether to check and lock a dataset before backing it up. Defaults
76
+ to Y.
77
+
78
+ -m ProcessCount
79
+ Number of concurrent child processes. Defaults to 1. In delay mode
80
+ the process count travels through the reserved PBS cpus rather than
81
+ the command line.
82
+
83
+ -n Gather and show the available file counts only; no backup actions are
84
+ performed.
85
+
86
+ -W MaxWorkers
87
+ Maximum number of concurrent batch workers for the same command.
88
+ Defaults to 2. A repeat delayed submit normally blocks while the
89
+ batch job is running; if that job has run for more than 6 hours and
90
+ has still completed no more than 1% of its recorded work, it is
91
+ treated as stalled and an extra worker is submitted instead, up to
92
+ this limit. Extra workers rely on dataset locking to stay off each
93
+ other's files, so this is pinned to 1 with -l N, and also for the
94
+ transfer action (-A 4, 6, 7), which claims tar files without locking.
95
+
96
+ -w WorkerSlot
97
+ Worker slot number for this run, added automatically to an extra
98
+ worker's command line so it gets its own dscheck record. Not
99
+ normally set by hand. Even slots walk the datasets in descending
100
+ dsid order (from d999999) and odd slots in ascending order, so that a
101
+ pair of workers meets in the middle instead of contending for the
102
+ same dataset locks.
103
+
104
+ -u Clean up (unlock) backup locks for the given dataset IDs. Dataset IDs
105
+ must be provided (not valid with -a).
106
+
107
+
108
+ EMAIL
109
+
110
+ -e Send an email to the specialist with backup statistics.
111
+
112
+ -E Send an email to the specialist with detailed backup statistics.
113
+
114
+
115
+ MISCELLANEOUS
116
+
117
+ -h Display this help document.
118
+
119
+
120
+ EXAMPLES
121
+
122
+ 1. Back up (create input files and tar files for) all flagged datasets:
123
+
124
+ dsquasar -a -A 3
125
+
126
+ 2. Transfer built tar files of one dataset to both Quasar end points:
127
+
128
+ dsquasar -t d123456 -A 4
129
+
130
+ 3. Show the counts of files ready to back up for a dataset, without acting:
131
+
132
+ dsquasar -t d123456 -n
133
+
134
+ 4. Dump backup statistics for all datasets by email:
135
+
136
+ dsquasar -a -A 16 -e
137
+
138
+ 5. Queue a delayed PBS batch job to create input files and tar files for all
139
+ datasets in the background (as run from cron); multi-processing is sized
140
+ automatically from the file count:
141
+
142
+ dsquasar -a -A 3 -e -b -d PBS
143
+
144
+ 6. Unlock backup locks left on datasets:
145
+
146
+ dsquasar -t d123456 -u
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.1
3
+ Version: 3.0.6
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
@@ -1,9 +1,11 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  src/rda_python_dsquasar/__init__.py
5
6
  src/rda_python_dsquasar/ds_quasar.py
6
7
  src/rda_python_dsquasar/dsquasar.py
8
+ src/rda_python_dsquasar/dsquasar.usg
7
9
  src/rda_python_dsquasar/dstacc.py
8
10
  src/rda_python_dsquasar/taccrec.py
9
11
  src/rda_python_dsquasar/tacctar.py