rda-python-dsquasar 3.0.20__tar.gz → 3.0.22__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.20/src/rda_python_dsquasar.egg-info → rda_python_dsquasar-3.0.22}/PKG-INFO +1 -1
  2. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/pyproject.toml +1 -1
  3. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/dsquasar.py +60 -43
  4. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/dsquasar.usg +17 -2
  5. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22/src/rda_python_dsquasar.egg-info}/PKG-INFO +1 -1
  6. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/LICENSE +0 -0
  7. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/MANIFEST.in +0 -0
  8. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/README.md +0 -0
  9. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/setup.cfg +0 -0
  10. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/__init__.py +0 -0
  11. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/ds_quasar.py +0 -0
  12. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/dstacc.py +0 -0
  13. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/taccrec.py +0 -0
  14. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar/tacctar.py +0 -0
  15. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar.egg-info/SOURCES.txt +0 -0
  16. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar.egg-info/dependency_links.txt +0 -0
  17. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar.egg-info/entry_points.txt +0 -0
  18. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar.egg-info/requires.txt +0 -0
  19. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/src/rda_python_dsquasar.egg-info/top_level.txt +0 -0
  20. {rda_python_dsquasar-3.0.20 → rda_python_dsquasar-3.0.22}/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.20
3
+ Version: 3.0.22
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.20"
7
+ version = "3.0.22"
8
8
  authors = [
9
9
  { name="Zaihua Ji", email="zji@ucar.edu" },
10
10
  ]
@@ -23,7 +23,6 @@ import os
23
23
  import re
24
24
  import sys
25
25
  import time
26
- import signal
27
26
  from os import path as op
28
27
  from time import time as tm
29
28
  from rda_python_common.pg_cmd import PgCMD
@@ -124,6 +123,7 @@ class DsQuasar(PgCMD, PgSplit):
124
123
  'maxcnt' : 10,
125
124
  'dolock' : 1,
126
125
  'doemail' : 0,
126
+ 'locflag' : None, # -L, limit the datasets gathered to one dataset.locflag
127
127
  'starttime' : 0, # wall-clock start of the run, for the PBS walltime guard
128
128
  'einfo' : 0, # set once a progress report is cached into dscheck.einfo
129
129
  'eretry' : 0, # earliest retry time after a progress report failed to cache
@@ -145,7 +145,7 @@ class DsQuasar(PgCMD, PgSplit):
145
145
  argv = sys.argv[1:]
146
146
  for arg in argv:
147
147
  if re.match(r'^-(h|-help)$', arg, re.I): self.show_usage('dsquasar')
148
- 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)
149
149
  if ms:
150
150
  arg = ms.group(1)
151
151
  if arg == 'b':
@@ -153,7 +153,7 @@ class DsQuasar(PgCMD, PgSplit):
153
153
  elif arg in self.sopts:
154
154
  self.sopts[arg] = 1
155
155
  option = None
156
- elif 'AcdlmtwW'.find(arg) > -1:
156
+ elif 'AcdLlmtwW'.find(arg) > -1:
157
157
  option = arg
158
158
  if arg == 'd': self.bopts = []
159
159
  elif 'BD'.find(arg) > -1:
@@ -180,6 +180,10 @@ class DsQuasar(PgCMD, PgSplit):
180
180
  if not (arg == 'Y' or arg == 'N'): self.pglog(arg +": Lock Flag(-l) must be Y or N", self.LGWNEX)
181
181
  self.PGBACK['dolock'] = 1 if arg == 'Y' else 0
182
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
183
187
  elif option == 'W':
184
188
  self.PGBACK['maxworkers'] = int(arg)
185
189
  if self.PGBACK['maxworkers'] < 1: self.pglog(arg +": Maximum Workers(-W) must be > 0", self.LGWNEX)
@@ -203,7 +207,7 @@ class DsQuasar(PgCMD, PgSplit):
203
207
  self.cmdlog(self.PGBACK['cmd'])
204
208
  self.PGBACK['starttime'] = tm()
205
209
  self.set_walltime_deadline()
206
- self.catch_batch_termination()
210
+ self.catch_term_signal(self.batch_term_report)
207
211
  if self.sopts['u']:
208
212
  if self.sopts['a']: self.pglog("-u: Dataset IDs must be provided to Unlock datasets", self.LOGWRN)
209
213
  self.unlock_datasets()
@@ -852,42 +856,29 @@ class DsQuasar(PgCMD, PgSplit):
852
856
  self.PGLOG['CURBID'], self.seconds_to_string_time(self.WALLTIME),
853
857
  self.seconds_to_string_time(self.MAXRUNTIME)), self.LOGWRN)
854
858
 
855
- # PBS kills a job that runs out of walltime with SIGTERM first and SIGKILL a few seconds
856
- # later (the MoM's kill_delay, 10 seconds by default), and the same pair is what qdel
857
- # sends. SIGTERM is the only warning there is, and nothing in the common library traps
858
- # it, so the run used to die on the spot with whatever it had to say unsaid. catching it
859
- # turns the last seconds into a report. only a batch run arms this: on the command line
860
- # SIGTERM must keep killing the process the way the user expects.
861
- def catch_batch_termination(self):
862
- if self.PGLOG['CURBID'] < 1: return
863
- signal.signal(signal.SIGTERM, self.batch_term_handler)
864
-
865
- # park a progress report and then die of the signal that was sent. this runs on borrowed
866
- # time, so it does only what it must: no tar queue counts (two database queries we may
867
- # not get to finish - the done count comes from memory), no dataset unlocking (the
868
- # dscheck daemon already cleans up after a dead pid). the default handler is restored
869
- # first so that a second signal, or the SIGKILL that follows, ends the run outright
870
- # instead of re-entering here should the report hang.
859
+ # park a progress report for a run PBS is about to kill. catch_term_signal() arms this
860
+ # for a batch run only, restores the default handler before calling it, and re-raises
861
+ # the signal afterwards, so all this has to do is build the report.
862
+ # it runs on borrowed time (SIGKILL follows SIGTERM by the MoM's kill_delay, 10 seconds
863
+ # by default), so it does only what it must: no tar queue counts (two database queries
864
+ # we may not get to finish - the done count comes from memory), no dataset unlocking
865
+ # (the dscheck daemon already cleans up after a dead pid).
871
866
  # the report may have to be written from inside an interrupted database call; that is
872
867
  # why it goes through report_dscheck_email, whose cache_customized_email falls back to
873
868
  # sending the mail directly when the UPDATE fails.
874
- def batch_term_handler(self, signum, frame):
875
- signal.signal(signum, signal.SIG_DFL)
876
- # a forked child shares the parent's dscheck record: it must not report for the run
877
- if self.PGSIG['PPID'] > 1: os._exit(1)
878
- if not self.PGBACK['einfo'] and self.PGBACK['doemail'] and self.PGLOG['DSCHECK']:
879
- etime = self.seconds_to_string_time(int(tm() - self.PGBACK['starttime']))
880
- amsg = self.action_message()
881
- dmsg = self.batch_done_count()
882
- wmsg = self.seconds_to_string_time(self.WALLTIME)
883
- self.pglog(self.INDENT + "{}: Terminated by signal {} after {} of the {} PBS walltime, {}".format(
884
- amsg, signum, etime, wmsg, dmsg), self.LOGACT)
885
- self.set_email("{}: {} terminated after {} of the {} PBS walltime, {}!".format(
886
- self.PGBACK['cmd'], amsg, etime, wmsg, dmsg), self.EMLTOP)
887
- title = "dsquasar: {} terminated ({})".format(amsg, dmsg)
888
- if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
889
- self.report_dscheck_email(title, 1)
890
- os.kill(os.getpid(), signum)
869
+ def batch_term_report(self, signum, frame):
870
+ if self.PGBACK['einfo'] or not (self.PGBACK['doemail'] and self.PGLOG['DSCHECK']): return
871
+ etime = self.seconds_to_string_time(int(tm() - self.PGBACK['starttime']))
872
+ amsg = self.action_message()
873
+ dmsg = self.batch_done_count()
874
+ wmsg = self.seconds_to_string_time(self.WALLTIME)
875
+ self.pglog(self.INDENT + "{}: Terminated by signal {} after {} of the {} PBS walltime, {}".format(
876
+ amsg, signum, etime, wmsg, dmsg), self.LOGACT)
877
+ self.set_email("{}: {} terminated after {} of the {} PBS walltime, {}!".format(
878
+ self.PGBACK['cmd'], amsg, etime, wmsg, dmsg), self.EMLTOP)
879
+ title = "dsquasar: {} terminated ({})".format(amsg, dmsg)
880
+ if self.PGBACK['errcnt']: title += " Error({})".format(self.PGBACK['errcnt'])
881
+ self.report_dscheck_email(title, 1)
891
882
 
892
883
  # email a report for a run under dscheck control. the progress report is parked in
893
884
  # dscheck.einfo and the final report is sent right away. the run holds the check lock
@@ -932,9 +923,10 @@ class DsQuasar(PgCMD, PgSplit):
932
923
  # even if PBS kills the job at the walltime. the final report follows and drops the
933
924
  # parked one if the run does finish in time. the live email buffers are saved and put
934
925
  # back, so the final report still carries everything logged before the cutoff.
935
- # PBS sends no catchable warning before the kill - SIGTERM is not trapped and SIGKILL
936
- # cannot be - so this parked report is the only thing standing between a killed job and
937
- # a run that is never heard from. every action submitted to PBS is guarded, not just the
926
+ # batch_term_report covers the kill itself, but only the seconds of it: SIGKILL cannot be
927
+ # caught, and a process stuck in uninterruptible I/O may never run the handler. this parked
928
+ # report is what stands between a killed job and a run that is never heard from, so it is
929
+ # kept as the first line of defence. every action submitted to PBS is guarded, not just the
938
930
  # tar and transfer ones: -A 16 and the hidden -A 128 are submitted with the same 24 hour
939
931
  # walltime and used to be excluded, so they died silently.
940
932
  # the tar queue depths are only reported for the actions they describe. both depths are
@@ -1482,13 +1474,29 @@ class DsQuasar(PgCMD, PgSplit):
1482
1474
  fcnt = 0
1483
1475
  # warn per named dataset with no files; stay silent when scanning all datasets
1484
1476
  logact = self.LOGWRN if self.dsids else 0
1477
+ flds = "dsid, backflag, pid, lockhost"
1478
+ # -L splits the work by where the data sits: an Object(O) dataset has to be staged
1479
+ # down from the object store and checksummed before it can be tarred, while a Glade(G)
1480
+ # one is only stat'ed, so the two classes run at wildly different rates and are better
1481
+ # off in separate runs than sharing one walltime.
1482
+ # G is matched as "not O" rather than "= G" on purpose: dataset.locflag is only a
1483
+ # summary of the per-file flags (set_dataset_locflag writes it only when every public
1484
+ # web file agrees, and leaves the old value otherwise), so it can hold a stale value.
1485
+ # as "not O" the two halves stay exhaustive and no dataset falls through both runs
1486
+ lcnd = ''
1487
+ if self.PGBACK['locflag'] == 'O':
1488
+ lcnd = "locflag = 'O'"
1489
+ elif self.PGBACK['locflag']:
1490
+ lcnd = "locflag <> 'O'"
1485
1491
  if self.dsids:
1486
1492
  pgrecs = []
1487
1493
  for dsid in self.dsids:
1488
- pgrec = self.pgget("dataset", "dsid, backflag, pid", "dsid = '{}'".format(dsid), self.LGWNEX)
1494
+ dcnd = "dsid = '{}'".format(dsid)
1495
+ if lcnd: dcnd += " AND " + lcnd
1496
+ pgrec = self.pgget("dataset", flds, dcnd, self.LGWNEX)
1489
1497
  if pgrec: pgrecs.append(pgrec)
1490
1498
  else:
1491
- mrecs = self.pgmget("dataset", "dsid, backflag, pid", self.dsid_order().strip(), self.LGWNEX)
1499
+ mrecs = self.pgmget("dataset", flds, (lcnd + self.dsid_order()).strip(), self.LGWNEX)
1492
1500
  dcnt = len(mrecs['dsid']) if mrecs else 0
1493
1501
  pgrecs = [self.onerecord(mrecs, i) for i in range(dcnt)]
1494
1502
  for pgrec in pgrecs:
@@ -1501,7 +1509,16 @@ class DsQuasar(PgCMD, PgSplit):
1501
1509
  # main runs take new files (bid = 0), so it shares no record with the worker holding
1502
1510
  # the lock and must not skip the dataset for it
1503
1511
  if unlock and pgrec['pid'] and not self.PGBACK['chgdays']:
1504
- if self.lock_dataset(dsid, 0, self.LOGACT) < 1: continue
1512
+ # quiet unlock: a live holder is the normal case for a second worker, and the
1513
+ # library wording for it ("Cannot Unlock") reads like a failure. report the skip
1514
+ # here instead, in terms of the worker that is still on the dataset
1515
+ lstat = self.lock_dataset(dsid, 0, 0)
1516
+ if lstat < 1:
1517
+ # -1 is the live holder; 0 means the record went away or the db errored,
1518
+ # which lock_dataset has already logged as an error
1519
+ if lstat < 0:
1520
+ self.pglog("{}: skipped, still in use by {}/{}".format(dsid, pgrec['pid'], pgrec['lockhost']), self.LOGACT)
1521
+ continue
1505
1522
  fcnt += self.get_dataset_files(dsid, dsfiles, pgrec['backflag'], logact, sizes)
1506
1523
  if dsfiles:
1507
1524
  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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_dsquasar
3
- Version: 3.0.20
3
+ Version: 3.0.22
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