Skip to content

Commit fd4acc6

Browse files
committed
[QC-1262]
different repo_cleaner must use a different file for the last run
1 parent a998e12 commit fd4acc6

3 files changed

Lines changed: 144 additions & 18 deletions

File tree

‎Framework/script/RepoCleaner/ReleaseNotes.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Release notes
22

3-
New
4-
-
3+
1.11
4+
- [QC-1262] - different repo_cleaner must use a different file for the last run
55

66
1.10
77
- Revive and clean up repocleaner tests

‎Framework/script/RepoCleaner/qcrepocleaner/o2-qc-repo-cleaner‎

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import argparse
2323
import importlib
2424
import logging
2525
import multiprocessing as mp
26+
import os
2627
import re
2728
import socket
2829
import sys
@@ -106,6 +107,8 @@ def parse_args():
106107
help='Do not check when was the last execution, run from timestamp 0.')
107108
parser.add_argument('--print-versions-details', dest='print_versions_details', action='store_true', default=False,
108109
help='Print extra details about the versions if enabled..')
110+
parser.add_argument('--instance-name', dest='instance_name', action='store', default=None,
111+
help='Instance name used to differentiate execution state files (timestamp and PID). If omitted, inferred from configuration.')
109112
args = parser.parse_args()
110113
dryable.set(args.dry_run)
111114
logging.info(args)
@@ -215,42 +218,79 @@ def find_matching_rules(rules, object_path):
215218
return result
216219

217220

218-
filepath = tempfile.gettempdir() + "/repoCleaner.txt"
221+
def get_instance_name(args=None):
222+
"""
223+
Determine the instance identifier based on args or config.
224+
"""
225+
if args is None:
226+
return "default"
227+
if getattr(args, 'instance_name', None):
228+
return args.instance_name
229+
if getattr(args, 'config_consul', None):
230+
items = args.config_consul.split(':')
231+
if len(items) >= 3:
232+
return Path(items[2]).stem
233+
if getattr(args, 'config_git', None):
234+
return "git"
235+
if getattr(args, 'config', None):
236+
config_stem = Path(args.config).stem
237+
if getattr(args, 'only_path', None) and args.only_path != "":
238+
sanitized_path = re.sub(r'[^a-zA-Z0-9_-]', '_', args.only_path.strip('/'))
239+
return f"{config_stem}_{sanitized_path}"
240+
return config_stem
241+
return "default"
242+
243+
244+
def get_timestamp_filepath(instance_name: str = None) -> str:
245+
"""
246+
Returns the path to the file where the timestamp of the last execution is stored for a given instance.
247+
"""
248+
if instance_name:
249+
sanitized = re.sub(r'[^a-zA-Z0-9_-]', '_', instance_name)
250+
return os.path.join(tempfile.gettempdir(), f"repoCleaner_{sanitized}.txt")
251+
else:
252+
return os.path.join(tempfile.gettempdir(), "repoCleaner.txt")
253+
254+
255+
filepath = get_timestamp_filepath()
219256
currentTimeStamp = int(time.time() * 1000)
220257

221258

222-
def get_ts_last_execution(ignore_last_execution: bool):
259+
def get_ts_last_execution(ignore_last_execution: bool, filepath_override: str = None):
223260
"""
224261
Returns the timestamp of the last execution.
225-
It is stored in a file in $TMP/repoCleaner.txt.
262+
It is stored in a file in $TMP/repoCleaner_<instance>.txt.
226263
:return: the timestamp of the last execution or 0 if it cannot find it.
227264
"""
228265
if ignore_last_execution:
229266
logging.info(f"Option ignore_last_execution set, we return 0 as timestamp.")
230267
return 0
231268

269+
target_filepath = filepath_override if filepath_override is not None else filepath
232270
try:
233-
f = open(filepath, "r")
271+
f = open(target_filepath, "r")
234272
except IOError:
235-
logging.info(f"File {filepath} not readable, we return 0 as timestamp.")
273+
logging.info(f"File {target_filepath} not readable, we return 0 as timestamp.")
236274
return 0
237275
timestamp = f.read()
238-
logging.info(f"Timestamp retrieved from {filepath}: {timestamp}")
276+
logging.info(f"Timestamp retrieved from {target_filepath}: {timestamp}")
239277
f.close()
240278
return timestamp
241279

242280

243-
def store_saved_timestamp():
281+
def store_saved_timestamp(filepath_override: str = None, timestamp: int = None):
244282
"""
245283
Store the timestamp we saved at the beginning of the execution of this script.
246284
"""
285+
target_filepath = filepath_override if filepath_override is not None else filepath
286+
ts = timestamp if timestamp is not None else currentTimeStamp
247287
try:
248-
f = open(filepath, "w+")
288+
f = open(target_filepath, "w+")
249289
except IOError:
250-
logging.error(f"Could not write the saved timestamp to {filepath}")
290+
logging.error(f"Could not write the saved timestamp to {target_filepath}")
251291
return
252-
f.write(str(currentTimeStamp))
253-
logging.info(f"Stored timestamp {currentTimeStamp} in {filepath}")
292+
f.write(str(ts))
293+
logging.info(f"Stored timestamp {ts} in {target_filepath}")
254294
f.close()
255295

256296

@@ -376,13 +416,15 @@ def process_object(object_path, rules, ccdb, args):
376416

377417

378418
def run(args, ccdb_url, rules):
419+
instance_name = get_instance_name(args)
420+
ts_filepath = get_timestamp_filepath(instance_name)
379421

380422
# Get list of objects from CCDB
381423
ccdb = Ccdb(ccdb_url, args.print_versions_details)
382424
ccdb.logger = logging.getLogger
383425
ccdb.set_adjustable_eov = args.set_adjustableEOV
384426
logging.info(f"ccdb.set_adjustable_eov: {ccdb.set_adjustable_eov}")
385-
paths = ccdb.get_objects_list(get_ts_last_execution(args.ignore_last_execution), args.only_path, args.only_path_no_subdir)
427+
paths = ccdb.get_objects_list(get_ts_last_execution(args.ignore_last_execution, ts_filepath), args.only_path, args.only_path_no_subdir)
386428
if args.only_path != '':
387429
paths = [item for item in paths if item is not None and item.startswith(args.only_path)]
388430
logging.debug(paths)
@@ -399,7 +441,7 @@ def run(args, ccdb_url, rules):
399441
message = datetime.today().strftime('%Y-%m-%d-%H:%M:%S')
400442
store_crucial_log(message + f" - End of the cleaner (total deleted: {ccdb.counter_deleted}, total updated: {ccdb.counter_validity_updated})")
401443
if not args.dry_run and not args.ignore_last_execution:
402-
store_saved_timestamp()
444+
store_saved_timestamp(ts_filepath)
403445

404446
# ****************
405447
# We start here !
@@ -413,8 +455,11 @@ def main():
413455
args = parse_args()
414456
logging.getLogger().setLevel(int(args.log_level))
415457

458+
instance_name = get_instance_name(args)
459+
pid_filename = f'o2-qc-repo-cleaner_{instance_name}.pid' if instance_name else 'o2-qc-repo-cleaner.pid'
460+
416461
try:
417-
with PIDFile(filename='o2-qc-repo-cleaner.pid'):
462+
with PIDFile(filename=pid_filename):
418463
ccdb_url, rules = read_config(args)
419464
run(args, ccdb_url, rules)
420465
except AlreadyRunningError:

‎Framework/script/RepoCleaner/tests/test_repoCleaner.py‎

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,23 @@ def import_path(path): # needed because o2-qc-repo-cleaner has no suffix
2121
sys.modules[module_name] = module
2222
return module
2323

24-
repoCleaner = import_path("../qcrepocleaner/o2-qc-repo-cleaner")
24+
script_dir = os.path.dirname(os.path.abspath(__file__))
25+
script_path = os.path.join(script_dir, "../qcrepocleaner/o2-qc-repo-cleaner")
26+
repoCleaner = import_path(script_path)
2527
parse_config = repoCleaner.parse_config
2628
Rule = repoCleaner.Rule
2729
find_matching_rules = repoCleaner.find_matching_rules
30+
get_instance_name = repoCleaner.get_instance_name
31+
get_timestamp_filepath = repoCleaner.get_timestamp_filepath
32+
get_ts_last_execution = repoCleaner.get_ts_last_execution
33+
store_saved_timestamp = repoCleaner.store_saved_timestamp
2834

2935

3036
class TestRepoCleaner(unittest.TestCase):
3137

3238
def test_parseConfig(self):
33-
args = parse_config("config-test.yaml")
39+
config_test_path = os.path.join(script_dir, "config-test.yaml")
40+
args = parse_config(config_test_path)
3441
self.assertEqual(args['ccdb_url'], CCDB_TEST_URL)
3542
rules = args['rules']
3643
self.assertEqual(len(rules), 3)
@@ -70,6 +77,80 @@ def test_findMatchingRule(self):
7077
rules.append(Rule('.*', '0', 'policyAll'))
7178
self.assertEqual(find_matching_rules(rules, 'task3/obj1')[0].policy, 'policyAll')
7279

80+
def test_get_instance_name(self):
81+
class DummyArgs:
82+
def __init__(self, **kwargs):
83+
self.__dict__.update(kwargs)
84+
85+
self.assertEqual(get_instance_name(None), 'default')
86+
self.assertEqual(get_instance_name(DummyArgs()), 'default')
87+
88+
# explicit instance name
89+
args = DummyArgs(instance_name='custom_inst', config='config.yaml')
90+
self.assertEqual(get_instance_name(args), 'custom_inst')
91+
92+
# config file
93+
args = DummyArgs(config='/path/to/my_cleaner_config.yaml')
94+
self.assertEqual(get_instance_name(args), 'my_cleaner_config')
95+
96+
# config with only_path
97+
args = DummyArgs(config='config.yaml', only_path='qc/ITS/MO')
98+
self.assertEqual(get_instance_name(args), 'config_qc_ITS_MO')
99+
100+
# consul config
101+
args = DummyArgs(config_consul='localhost:8500:cleaner-tpc.yaml')
102+
self.assertEqual(get_instance_name(args), 'cleaner-tpc')
103+
104+
# git config
105+
args = DummyArgs(config_git=True)
106+
self.assertEqual(get_instance_name(args), 'git')
107+
108+
def test_get_timestamp_filepath(self):
109+
default_path = get_timestamp_filepath()
110+
self.assertTrue(default_path.endswith('repoCleaner.txt'))
111+
112+
inst_path = get_timestamp_filepath('custom_inst')
113+
self.assertTrue(inst_path.endswith('repoCleaner_custom_inst.txt'))
114+
self.assertNotEqual(default_path, inst_path)
115+
116+
def test_instance_timestamp_isolation(self):
117+
inst1_file = get_timestamp_filepath('instance_1_test')
118+
inst2_file = get_timestamp_filepath('instance_2_test')
119+
120+
# Cleanup if leftover
121+
for f in [inst1_file, inst2_file]:
122+
if os.path.exists(f):
123+
os.remove(f)
124+
125+
try:
126+
# Initially no execution timestamp
127+
self.assertEqual(get_ts_last_execution(False, inst1_file), 0)
128+
self.assertEqual(get_ts_last_execution(False, inst2_file), 0)
129+
130+
# Store timestamp for instance 1
131+
ts1 = 1600000000000
132+
store_saved_timestamp(inst1_file, timestamp=ts1)
133+
134+
# Instance 1 has ts1, instance 2 is still 0
135+
self.assertEqual(int(get_ts_last_execution(False, inst1_file)), ts1)
136+
self.assertEqual(get_ts_last_execution(False, inst2_file), 0)
137+
138+
# Store timestamp for instance 2
139+
ts2 = 1700000000000
140+
store_saved_timestamp(inst2_file, timestamp=ts2)
141+
142+
# Both retain their own distinct timestamps
143+
self.assertEqual(int(get_ts_last_execution(False, inst1_file)), ts1)
144+
self.assertEqual(int(get_ts_last_execution(False, inst2_file)), ts2)
145+
146+
# Test ignore_last_execution option
147+
self.assertEqual(get_ts_last_execution(True, inst1_file), 0)
148+
self.assertEqual(get_ts_last_execution(True, inst2_file), 0)
149+
finally:
150+
for f in [inst1_file, inst2_file]:
151+
if os.path.exists(f):
152+
os.remove(f)
153+
73154

74155
if __name__ == '__main__':
75156
unittest.main()

0 commit comments

Comments
 (0)