From 7068995161daed3dfd6a92d20f9a7ae86955db62 Mon Sep 17 00:00:00 2001 From: Wilmer Uruchi Ticona Date: Thu, 2 Apr 2020 18:07:50 +0200 Subject: [PATCH] Developing issue #511. Added jobs structure persistence. --- .vscode/settings.json | 3 +- a29z_20200324_1027.pdfn | Bin 30604 -> 0 bytes autosubmit/database/db_structure.py | 153 ++++++++++ autosubmit/job/job_list.py | 438 +++++++++++++++++----------- autosubmit/job/job_utils.py | 27 +- 5 files changed, 442 insertions(+), 179 deletions(-) delete mode 100644 a29z_20200324_1027.pdfn create mode 100644 autosubmit/database/db_structure.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 97648ef47..e90a26767 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "restructuredtext.confPath": "${workspaceFolder}/docs/source" + "restructuredtext.confPath": "${workspaceFolder}/docs/source", + "python.pythonPath": "/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7" } \ No newline at end of file diff --git a/a29z_20200324_1027.pdfn b/a29z_20200324_1027.pdfn deleted file mode 100644 index 830d8fbd51b4978d420876ca927549b12e5b4020..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30604 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 XbnP7ec$bNuk% 0): + # print("Return structure") + return current_table_structure + else: + return None + else: + # pkl folder not found + raise Exception("pkl folder not found " + str(pkl_path)) + except Exception as exp: + print(traceback.format_exc()) + + +def create_connection(db_file): + """ + Create a database connection to the SQLite database specified by db_file. + :param db_file: database file name + :return: Connection object or None + """ + try: + conn = sqlite3.connect(db_file) + return conn + except: + return None + + +def create_table(conn, create_table_sql): + """ create a table from the create_table_sql statement + :param conn: Connection object + :param create_table_sql: a CREATE TABLE statement + :return: + """ + try: + c = conn.cursor() + c.execute(create_table_sql) + except Exception as e: + print(e) + + +def _get_exp_structure(path): + """ + Get all registers from experiment_status.\n + :return: row content: exp_id, name, status, seconds_diff + :rtype: 4-tuple (int, str, str, int) + """ + try: + conn = create_connection(path) + conn.text_factory = str + cur = conn.cursor() + cur.execute( + "SELECT e_from, e_to FROM experiment_structure") + rows = cur.fetchall() + return rows + except Exception as exp: + print(traceback.format_exc()) + return dict() + + +def save_structure(graph, exp_id, exp_path): + """ + Saves structure if path is valid + """ + pkl_path = os.path.join(exp_path, exp_id, "pkl") + if os.path.exists(pkl_path): + db_structure_path = os.path.join( + pkl_path, "structure_" + exp_id + ".db") + # with open(db_structure_path, "w"): + conn = create_connection(db_structure_path) + _delete_table_content(conn) + for u, v in graph.edges(): + # save + _create_edge(conn, u, v) + #print("Created edge " + str(u) + str(v)) + conn.commit() + else: + # pkl folder not found + raise Exception("pkl folder not found " + str(pkl_path)) + + +def _create_edge(conn, u, v): + """ + Create edge + """ + try: + sql = ''' INSERT INTO experiment_structure(e_from, e_to) VALUES(?,?) ''' + cur = conn.cursor() + cur.execute(sql, (u, v)) + # return cur.lastrowid + except sqlite3.Error as e: + print("Error on Insert : " + str(type(e).__name__)) + + +def _delete_table_content(conn): + """ + Deletes table content + """ + try: + sql = ''' DELETE FROM experiment_structure ''' + cur = conn.cursor() + cur.execute(sql) + conn.commit() + except sqlite3.Error as e: + print(traceback.format_exc()) + print("Error on Delete : " + str(type(e).__name__)) diff --git a/autosubmit/job/job_list.py b/autosubmit/job/job_list.py index 32869e539..02cbd8079 100644 --- a/autosubmit/job/job_list.py +++ b/autosubmit/job/job_list.py @@ -36,6 +36,7 @@ from autosubmit.job.job_utils import Dependency from autosubmit.job.job_common import Status, Type, bcolors from bscearth.utils.date import date2str, parse_date, sum_str_hours from autosubmit.job.job_packages import JobPackageSimple, JobPackageArray, JobPackageThread +import autosubmit.database.db_structure as DbStructure from networkx import DiGraph from autosubmit.job.job_utils import transitive_reduction @@ -48,7 +49,8 @@ class JobList: """ def __init__(self, expid, config, parser_factory, job_list_persistence): - self._persistence_path = os.path.join(config.LOCAL_ROOT_DIR, expid, "pkl") + self._persistence_path = os.path.join( + config.LOCAL_ROOT_DIR, expid, "pkl") self._update_file = "updated_list_" + expid + ".txt" self._failed_file = "failed_job_list_" + expid + ".pkl" self._persistence_file = "job_list_" + expid @@ -71,6 +73,7 @@ class JobList: self.packages_id = dict() self.job_package_map = dict() self.sections_checked = set() + @property def expid(self): """ @@ -96,7 +99,7 @@ class JobList: self._graph = value def generate(self, date_list, member_list, num_chunks, chunk_ini, parameters, date_format, default_retrials, - default_job_type, wrapper_type=None, wrapper_jobs=None,new=True, notransitive=False): + default_job_type, wrapper_type=None, wrapper_jobs=None, new=True, notransitive=False): """ Creates all jobs needed for the current workflow @@ -132,7 +135,8 @@ class JobList: jobs_parser = self._get_jobs_parser() - dic_jobs = DicJobs(self, jobs_parser, date_list, member_list, chunk_list, date_format, default_retrials) + dic_jobs = DicJobs(self, jobs_parser, date_list, member_list, + chunk_list, date_format, default_retrials) self._dic_jobs = dic_jobs priority = 0 @@ -141,9 +145,11 @@ class JobList: # jobs_data includes the name of the .our and .err files of the job in LOG_expid if not new: jobs_data = {str(row[0]): row for row in self.load()} - self._create_jobs(dic_jobs, jobs_parser, priority, default_job_type, jobs_data) + self._create_jobs(dic_jobs, jobs_parser, priority, + default_job_type, jobs_data) Log.info("Adding dependencies...") - self._add_dependencies(date_list, member_list, chunk_list, dic_jobs, jobs_parser, self.graph) + self._add_dependencies(date_list, member_list, + chunk_list, dic_jobs, jobs_parser, self.graph) Log.info("Removing redundant dependencies...") self.update_genealogy(new, notransitive) @@ -151,9 +157,9 @@ class JobList: job.parameters = parameters # Perhaps this should be done by default independent of the wrapper_type supplied - if wrapper_type == 'vertical-mixed': - self._ordered_jobs_by_date_member = self._create_sorted_dict_jobs(wrapper_jobs) - + if wrapper_type == 'vertical-mixed': + self._ordered_jobs_by_date_member = self._create_sorted_dict_jobs( + wrapper_jobs) @staticmethod def _add_dependencies(date_list, member_list, chunk_list, dic_jobs, jobs_parser, graph, option="DEPENDENCIES"): @@ -165,7 +171,8 @@ class JobList: continue dependencies_keys = jobs_parser.get(job_section, option).split() - dependencies = JobList._manage_dependencies(dependencies_keys, dic_jobs,job_section) + dependencies = JobList._manage_dependencies( + dependencies_keys, dic_jobs, job_section) for job in dic_jobs.get_jobs(job_section): num_jobs = 1 @@ -177,14 +184,14 @@ class JobList: dependencies, graph) @staticmethod - def _manage_dependencies(dependencies_keys, dic_jobs,job_section): + def _manage_dependencies(dependencies_keys, dic_jobs, job_section): dependencies = dict() for key in dependencies_keys: distance = None splits = None sign = None - if '-' not in key and '+' not in key and '*' not in key: + if '-' not in key and '+' not in key and '*' not in key: section = key else: if '-' in key: @@ -199,47 +206,61 @@ class JobList: if '[' in section: section_name = section[0:section.find("[")] - splits_section = int(dic_jobs.get_option(section_name, 'SPLITS', 0)) - splits = JobList._calculate_splits_dependencies(section, splits_section) + splits_section = int( + dic_jobs.get_option(section_name, 'SPLITS', 0)) + splits = JobList._calculate_splits_dependencies( + section, splits_section) section = section_name - dependency_running_type = dic_jobs.get_option(section, 'RUNNING', 'once').lower() + dependency_running_type = dic_jobs.get_option( + section, 'RUNNING', 'once').lower() delay = int(dic_jobs.get_option(section, 'DELAY', -1)) - select_chunks_opt = dic_jobs.get_option(job_section, 'SELECT_CHUNKS', None) + select_chunks_opt = dic_jobs.get_option( + job_section, 'SELECT_CHUNKS', None) selected_chunks = [] if select_chunks_opt is not None: if '*' in select_chunks_opt: sections_chunks = select_chunks_opt.split(' ') for section_chunk in sections_chunks: - info=section_chunk.split('*') + info = section_chunk.split('*') if info[0] in key: - for relation in range(1,len(info)): - auxiliar_relation_list=[] + for relation in range(1, len(info)): + auxiliar_relation_list = [] for location in info[relation].split('-'): auxiliar_chunk_list = [] location = location.strip('[').strip(']') if ':' in location: if len(location) == 3: - for chunk_number in range(int(location[0]),int(location[2])+1): - auxiliar_chunk_list.append(chunk_number) + for chunk_number in range(int(location[0]), int(location[2])+1): + auxiliar_chunk_list.append( + chunk_number) elif len(location) == 2: if ':' == location[0]: for chunk_number in range(0, int(location[1])+1): - auxiliar_chunk_list.append(chunk_number) + auxiliar_chunk_list.append( + chunk_number) elif ':' == location[1]: - for chunk_number in range(int(location[0])+1,len(dic_jobs._chunk_list)-1): - auxiliar_chunk_list.append(chunk_number) + for chunk_number in range(int(location[0])+1, len(dic_jobs._chunk_list)-1): + auxiliar_chunk_list.append( + chunk_number) elif ',' in location: for chunk in location.split(','): - auxiliar_chunk_list.append(int(chunk)) - elif re.match('^[0-9]+$',location): - auxiliar_chunk_list.append(int(location)) - auxiliar_relation_list.append(auxiliar_chunk_list) + auxiliar_chunk_list.append( + int(chunk)) + elif re.match('^[0-9]+$', location): + auxiliar_chunk_list.append( + int(location)) + auxiliar_relation_list.append( + auxiliar_chunk_list) selected_chunks.append(auxiliar_relation_list) if len(selected_chunks) >= 1: - dependency = Dependency(section, distance, dependency_running_type, sign, delay, splits,selected_chunks) #[]select_chunks_dest,select_chunks_orig + # []select_chunks_dest,select_chunks_orig + dependency = Dependency( + section, distance, dependency_running_type, sign, delay, splits, selected_chunks) else: - dependency = Dependency(section, distance, dependency_running_type, sign, delay, splits,[]) #[]select_chunks_dest,select_chunks_orig + # []select_chunks_dest,select_chunks_orig + dependency = Dependency( + section, distance, dependency_running_type, sign, delay, splits, []) dependencies[key] = dependency return dependencies @@ -271,30 +292,34 @@ class JobList: dependency) if skip: continue - chunk_relations_to_add=list() - if len(dependency.select_chunks_orig) > 0: # find chunk relation + chunk_relations_to_add = list() + if len(dependency.select_chunks_orig) > 0: # find chunk relation relation_indx = 0 while relation_indx < len(dependency.select_chunks_orig): if len(dependency.select_chunks_orig[relation_indx]) == 0 or job.chunk in dependency.select_chunks_orig[relation_indx] or job.chunk is None: chunk_relations_to_add.append(relation_indx) - relation_indx+=1 + relation_indx += 1 relation_indx -= 1 - if len(dependency.select_chunks_orig) <= 0 or job.chunk is None or len(chunk_relations_to_add) > 0 : #If doesn't contain select_chunks or running isn't chunk . ... - parents_jobs=dic_jobs.get_jobs(dependency.section, date, member, chunk) + # If doesn't contain select_chunks or running isn't chunk . ... + if len(dependency.select_chunks_orig) <= 0 or job.chunk is None or len(chunk_relations_to_add) > 0: + parents_jobs = dic_jobs.get_jobs( + dependency.section, date, member, chunk) for parent in parents_jobs: if dependency.delay == -1 or chunk > dependency.delay: if isinstance(parent, list): if job.split is not None: - parent = filter(lambda _parent: _parent.split == job.split, parent)[0] + parent = filter( + lambda _parent: _parent.split == job.split, parent)[0] else: if dependency.splits is not None: - parent = filter(lambda _parent: _parent.split in dependency.splits, parent) + parent = filter( + lambda _parent: _parent.split in dependency.splits, parent) if len(dependency.select_chunks_dest) <= 0 or parent.chunk is None: job.add_parent(parent) JobList._add_edge(graph, job, parent) else: - visited_parents=set() + visited_parents = set() for relation_indx in chunk_relations_to_add: if parent.chunk in dependency.select_chunks_dest[relation_indx] or len(dependency.select_chunks_dest[relation_indx]) == 0: if parent not in visited_parents: @@ -304,6 +329,7 @@ class JobList: JobList.handle_frequency_interval_dependencies(chunk, chunk_list, date, date_list, dic_jobs, job, member, member_list, dependency.section, graph) + @staticmethod def _calculate_dependency_metadata(chunk, chunk_list, member, member_list, date, date_list, dependency): skip = False @@ -400,20 +426,21 @@ class JobList: def _create_jobs(dic_jobs, parser, priority, default_job_type, jobs_data=dict()): for section in parser.sections(): Log.debug("Creating {0} jobs".format(section)) - dic_jobs.read_section(section, priority, default_job_type, jobs_data) + dic_jobs.read_section( + section, priority, default_job_type, jobs_data) priority += 1 def _create_sorted_dict_jobs(self, wrapper_jobs): """ Creates a sorting of the jobs whose job.section is in wrapper_jobs, according to the following filters in order of importance: - date, member, RUNNING, and chunk number; where RUNNING is defined in jobs_.conf for each section. + date, member, RUNNING, and chunk number; where RUNNING is defined in jobs_.conf for each section. If the job does not have a chunk number, the total number of chunks configured for the experiment is used. :param wrapper_jobs: User defined job types in autosubmit_,conf [wrapper] section to be wrapped. \n :type wrapper_jobs: String \n :return: Sorted Dictionary of Dictionary of List that represents the jobs included in the wrapping process. \n - :rtype: Dictionary Key: date, Value: (Dictionary Key: Member, Value: List of jobs that belong to the date, member, and are ordered by chunk number if it is a chunk job otherwise num_chunks from JOB TYPE (section) + :rtype: Dictionary Key: date, Value: (Dictionary Key: Member, Value: List of jobs that belong to the date, member, and are ordered by chunk number if it is a chunk job otherwise num_chunks from JOB TYPE (section) """ # Dictionary Key: date, Value: (Dictionary Key: Member, Value: List) dict_jobs = dict() @@ -423,21 +450,24 @@ class JobList: dict_jobs[date][member] = list() num_chunks = len(self._chunk_list) # Select only relevant jobs, those belonging to the sections defined in the wrapper - filtered_jobs_list = filter(lambda job: job.section in wrapper_jobs, self._job_list) + filtered_jobs_list = filter( + lambda job: job.section in wrapper_jobs, self._job_list) - filtered_jobs_fake_date_member, fake_original_job_map = self._create_fake_dates_members(filtered_jobs_list) + filtered_jobs_fake_date_member, fake_original_job_map = self._create_fake_dates_members( + filtered_jobs_list) - sections_running_type_map = dict() + sections_running_type_map = dict() for section in wrapper_jobs.split(" "): # RUNNING = once, as default. This value comes from jobs_.conf - sections_running_type_map[section] = self._dic_jobs.get_option(section, "RUNNING", 'once') - + sections_running_type_map[section] = self._dic_jobs.get_option( + section, "RUNNING", 'once') + for date in self._date_list: str_date = self._get_date(date) for member in self._member_list: # Filter list of fake jobs according to date and member, result not sorted at this point sorted_jobs_list = filter(lambda job: job.name.split("_")[1] == str_date and - job.name.split("_")[2] == member, filtered_jobs_fake_date_member) + job.name.split("_")[2] == member, filtered_jobs_fake_date_member) previous_job = sorted_jobs_list[0] @@ -445,7 +475,7 @@ class JobList: section_running_type = sections_running_type_map[previous_job.section] jobs_to_sort = [previous_job] - previous_section_running_type = None + previous_section_running_type = None # Index starts at 1 because 0 has been taken in a previous step for index in range(1, len(sorted_jobs_list) + 1): # If not last item @@ -457,10 +487,10 @@ class JobList: section_running_type = sections_running_type_map[job.section] # Test if RUNNING is different between sections, or if we have reached the last item in sorted_jobs_list if (previous_section_running_type != None and previous_section_running_type != section_running_type) \ - or index == len(sorted_jobs_list): + or index == len(sorted_jobs_list): - # Sorting by date, member, chunk number if it is a chunk job otherwise num_chunks from JOB TYPE (section) - # Important to note that the only differentiating factor would be chunk OR num_chunks + # Sorting by date, member, chunk number if it is a chunk job otherwise num_chunks from JOB TYPE (section) + # Important to note that the only differentiating factor would be chunk OR num_chunks jobs_to_sort = sorted(jobs_to_sort, key=lambda k: (k.name.split('_')[1], (k.name.split('_')[2]), (int(k.name.split('_')[3]) if len(k.name.split('_')) == 5 else num_chunks + 1))) @@ -484,7 +514,7 @@ class JobList: def _create_fake_dates_members(self, filtered_jobs_list): """ - Using the list of jobs provided, creates clones of these jobs and modifies names conditionted on job.date, job.member values (testing None). + Using the list of jobs provided, creates clones of these jobs and modifies names conditionted on job.date, job.member values (testing None). The purpose is that all jobs share the same name structure. :param filtered_jobs_list: A list of jobs of only those that comply with certain criteria, e.g. those belonging to a user defined job type for wrapping. \n @@ -508,7 +538,7 @@ class JobList: fake_job = copy.deepcopy(job) # Use previous values to modify name of fake job fake_job.name = fake_job.name.split('_', 1)[0] + "_" + self._get_date(date) + "_" \ - + member + "_" + fake_job.name.split("_", 1)[1] + + member + "_" + fake_job.name.split("_", 1)[1] # Filling list of fake jobs, only difference is the name filtered_jobs_fake_date_member.append(fake_job) # Mapping fake jobs to orignal ones @@ -561,7 +591,6 @@ class JobList: return self._date_list def get_member_list(self): - """ Get inner member list @@ -606,7 +635,7 @@ class JobList: """ return self._ordered_jobs_by_date_member - def get_completed(self, platform=None,wrapper=False): + def get_completed(self, platform=None, wrapper=False): """ Returns a list of completed jobs @@ -617,7 +646,7 @@ class JobList: """ completed_jobs = [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.COMPLETED] + job.status == Status.COMPLETED] if wrapper: return [job for job in completed_jobs if job.packed is False] @@ -634,13 +663,14 @@ class JobList: :rtype: list """ uncompleted_jobs = [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status != Status.COMPLETED] + job.status != Status.COMPLETED] if wrapper: return [job for job in uncompleted_jobs if job.packed is False] else: return uncompleted_jobs - def get_submitted(self, platform=None, hold =False , wrapper=False): + + def get_submitted(self, platform=None, hold=False, wrapper=False): """ Returns a list of submitted jobs @@ -651,17 +681,17 @@ class JobList: """ submitted = list() if hold: - submitted= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.SUBMITTED and job.hold == hold ] + submitted = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.SUBMITTED and job.hold == hold] else: - submitted= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.SUBMITTED ] + submitted = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.SUBMITTED] if wrapper: return [job for job in submitted if job.packed is False] else: return submitted - def get_running(self, platform=None,wrapper=False): + def get_running(self, platform=None, wrapper=False): """ Returns a list of jobs running @@ -670,13 +700,14 @@ class JobList: :return: running jobs :rtype: list """ - running= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.RUNNING] + running = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.RUNNING] if wrapper: return [job for job in running if job.packed is False] else: return running - def get_queuing(self, platform=None,wrapper=False): + + def get_queuing(self, platform=None, wrapper=False): """ Returns a list of jobs queuing @@ -685,13 +716,14 @@ class JobList: :return: queuedjobs :rtype: list """ - queuing= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.QUEUING] + queuing = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.QUEUING] if wrapper: return [job for job in queuing if job.packed is False] else: return queuing - def get_failed(self, platform=None,wrapper=False): + + def get_failed(self, platform=None, wrapper=False): """ Returns a list of failed jobs @@ -700,13 +732,14 @@ class JobList: :return: failed jobs :rtype: list """ - failed= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.FAILED] + failed = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.FAILED] if wrapper: return [job for job in failed if job.packed is False] else: return failed - def get_unsubmitted(self, platform=None,wrapper=False): + + def get_unsubmitted(self, platform=None, wrapper=False): """ Returns a list of unsummited jobs @@ -715,15 +748,15 @@ class JobList: :return: all jobs :rtype: list """ - unsubmitted= [job for job in self._job_list if (platform is None or job.platform == platform) and - ( job.status != Status.SUBMITTED and job.status != Status.QUEUING and job.status == Status.RUNNING and job.status == Status.COMPLETED ) ] + unsubmitted = [job for job in self._job_list if (platform is None or job.platform == platform) and + (job.status != Status.SUBMITTED and job.status != Status.QUEUING and job.status == Status.RUNNING and job.status == Status.COMPLETED)] if wrapper: return [job for job in unsubmitted if job.packed is False] else: return unsubmitted - def get_all(self, platform=None,wrapper=False): + def get_all(self, platform=None, wrapper=False): """ Returns a list of all jobs @@ -739,7 +772,7 @@ class JobList: else: return all - def get_ready(self, platform=None, hold=False , wrapper=False ): + def get_ready(self, platform=None, hold=False, wrapper=False): """ Returns a list of ready jobs @@ -749,14 +782,14 @@ class JobList: :rtype: list """ ready = [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.READY and job.hold is hold] + job.status == Status.READY and job.hold is hold] if wrapper: return [job for job in ready if job.packed is False] else: return ready - def get_waiting(self, platform=None,wrapper=False): + def get_waiting(self, platform=None, wrapper=False): """ Returns a list of jobs waiting @@ -765,8 +798,8 @@ class JobList: :return: waiting jobs :rtype: list """ - waiting_jobs= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.WAITING] + waiting_jobs = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.WAITING] if wrapper: return [job for job in waiting_jobs if job.packed is False] else: @@ -781,11 +814,11 @@ class JobList: :return: waiting jobs :rtype: list """ - waiting_jobs= [job for job in self._job_list if (job.platform.type == platform_type and - job.status == Status.WAITING)] + waiting_jobs = [job for job in self._job_list if (job.platform.type == platform_type and + job.status == Status.WAITING)] return waiting_jobs - def get_held_jobs(self,platform = None): + def get_held_jobs(self, platform=None): """ Returns a list of jobs in the platforms (Held) @@ -797,8 +830,7 @@ class JobList: return [job for job in self._job_list if (platform is None or job.platform == platform) and job.status == Status.HELD] - - def get_unknown(self, platform=None,wrapper=False): + def get_unknown(self, platform=None, wrapper=False): """ Returns a list of jobs on unknown state @@ -807,13 +839,14 @@ class JobList: :return: unknown state jobs :rtype: list """ - submitted= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.UNKNOWN] + submitted = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.UNKNOWN] if wrapper: return [job for job in submitted if job.packed is False] else: return submitted - def get_suspended(self, platform=None,wrapper=False): + + def get_suspended(self, platform=None, wrapper=False): """ Returns a list of jobs on unknown state @@ -822,12 +855,13 @@ class JobList: :return: unknown state jobs :rtype: list """ - suspended= [job for job in self._job_list if (platform is None or job.platform == platform) and - job.status == Status.SUSPENDED] + suspended = [job for job in self._job_list if (platform is None or job.platform == platform) and + job.status == Status.SUSPENDED] if wrapper: return [job for job in suspended if job.packed is False] else: return suspended + def get_in_queue(self, platform=None, wrapper=False): """ Returns a list of jobs in the platforms (Submitted, Running, Queuing, Unknown,Held) @@ -844,7 +878,8 @@ class JobList: return [job for job in in_queue if job.packed is False] else: return in_queue - def get_not_in_queue(self, platform=None,wrapper=False): + + def get_not_in_queue(self, platform=None, wrapper=False): """ Returns a list of jobs NOT in the platforms (Ready, Waiting) @@ -853,12 +888,13 @@ class JobList: :return: jobs not in platforms :rtype: list """ - not_queued= self.get_ready(platform) + self.get_waiting(platform) + not_queued = self.get_ready(platform) + self.get_waiting(platform) if wrapper: return [job for job in not_queued if job.packed is False] else: return not_queued - def get_finished(self, platform=None,wrapper=False): + + def get_finished(self, platform=None, wrapper=False): """ Returns a list of jobs finished (Completed, Failed) @@ -868,11 +904,12 @@ class JobList: :return: finished jobs :rtype: list """ - finished= self.get_completed(platform) + self.get_failed(platform) + finished = self.get_completed(platform) + self.get_failed(platform) if wrapper: return [job for job in finished if job.packed is False] else: return finished + def get_active(self, platform=None, wrapper=False): """ Returns a list of active jobs (In platforms queue + Ready) @@ -882,15 +919,17 @@ class JobList: :return: active jobs :rtype: list """ - active = self.get_in_queue(platform) + self.get_ready(platform=platform,hold=True) + self.get_ready(platform=platform,hold=False) - tmp = [job for job in active if job.hold and not job.status == Status.SUBMITTED and not job.status == Status.READY] - if len(tmp) == len(active): # IF only held jobs left without dependencies satisfied + active = self.get_in_queue(platform) + self.get_ready( + platform=platform, hold=True) + self.get_ready(platform=platform, hold=False) + tmp = [job for job in active if job.hold and not job.status == + Status.SUBMITTED and not job.status == Status.READY] + if len(tmp) == len(active): # IF only held jobs left without dependencies satisfied if len(tmp) != 0 and len(active) != 0: - Log.warning("Only Held Jobs active,Exiting Autosubmit (TIP: This can happen if suspended or/and Failed jobs are found on the workflow) ") + Log.warning( + "Only Held Jobs active,Exiting Autosubmit (TIP: This can happen if suspended or/and Failed jobs are found on the workflow) ") active = [] return active - def get_job_by_name(self, name): """ Returns the job that its name matches parameter name @@ -914,10 +953,10 @@ class JobList: jobs_by_id[job.id].append(job) return jobs_by_id - def get_in_ready_grouped_id(self, platform): - jobs=[] - [jobs.append(job) for job in jobs if (platform is None or job._platform.name is platform.name)] + jobs = [] + [jobs.append(job) for job in jobs if ( + platform is None or job._platform.name is platform.name)] jobs_by_id = dict() for job in jobs: @@ -993,7 +1032,8 @@ class JobList: """ Persists the job list """ - self._persistence.save(self._persistence_path, self._persistence_file, self._job_list) + self._persistence.save(self._persistence_path, + self._persistence_file, self._job_list) def update_from_file(self, store_change=True): """ @@ -1001,7 +1041,8 @@ class JobList: :param store_change: if True, renames the update file to avoid reloading it at the next iteration """ if os.path.exists(os.path.join(self._persistence_path, self._update_file)): - Log.info("Loading updated list: {0}".format(os.path.join(self._persistence_path, self._update_file))) + Log.info("Loading updated list: {0}".format( + os.path.join(self._persistence_path, self._update_file))) for line in open(os.path.join(self._persistence_path, self._update_file)): if line.strip() == '': continue @@ -1029,7 +1070,7 @@ class JobList: def parameters(self, value): self._parameters = value - def update_list(self, as_conf,store_change=True,fromSetStatus=False): + def update_list(self, as_conf, store_change=True, fromSetStatus=False): """ Updates job list, resetting failed jobs and changing to READY all WAITING jobs with all parents COMPLETED @@ -1053,62 +1094,76 @@ class JobList: retrials = job.retrials if job.fail_count <= retrials: - tmp = [parent for parent in job.parents if parent.status == Status.COMPLETED] + tmp = [ + parent for parent in job.parents if parent.status == Status.COMPLETED] if len(tmp) == len(job.parents): job.status = Status.READY job.packed = False save = True - Log.debug("Resetting job: {0} status to: READY for retrial...".format(job.name)) + Log.debug( + "Resetting job: {0} status to: READY for retrial...".format(job.name)) else: job.status = Status.WAITING save = True job.packed = False - Log.debug("Resetting job: {0} status to: WAITING for parents completion...".format(job.name)) + Log.debug( + "Resetting job: {0} status to: WAITING for parents completion...".format(job.name)) # if waiting jobs has all parents completed change its State to READY for job in self.get_completed(): if job.synchronize is not None: Log.debug('Updating SYNC jobs') - tmp = [parent for parent in job.parents if parent.status == Status.COMPLETED] + tmp = [ + parent for parent in job.parents if parent.status == Status.COMPLETED] if len(tmp) != len(job.parents): job.status = Status.WAITING save = True - Log.debug("Resetting sync job: {0} status to: WAITING for parents completion...".format(job.name)) + Log.debug( + "Resetting sync job: {0} status to: WAITING for parents completion...".format(job.name)) Log.debug('Update finished') Log.debug('Updating WAITING jobs') if not fromSetStatus: all_parents_completed = [] for job in self.get_waiting(): - tmp = [parent for parent in job.parents if parent.status == Status.COMPLETED] + tmp = [ + parent for parent in job.parents if parent.status == Status.COMPLETED] if job.parents is None or len(tmp) == len(job.parents): job.status = Status.READY job.hold = False - Log.debug("Setting job: {0} status to: READY (all parents completed)...".format(job.name)) + Log.debug( + "Setting job: {0} status to: READY (all parents completed)...".format(job.name)) if as_conf.get_remote_dependencies(): all_parents_completed.append(job.name) if as_conf.get_remote_dependencies(): - Log.debug('Updating WAITING jobs eligible for remote_dependencies') + Log.debug( + 'Updating WAITING jobs eligible for remote_dependencies') for job in self.get_waiting_remote_dependencies('slurm'.lower()): if job.name not in all_parents_completed: - tmp = [parent for parent in job.parents if ( (parent.status == Status.COMPLETED or parent.status == Status.QUEUING or parent.status == Status.RUNNING) and "setup" not in parent.name.lower() )] + tmp = [parent for parent in job.parents if ( + (parent.status == Status.COMPLETED or parent.status == Status.QUEUING or parent.status == Status.RUNNING) and "setup" not in parent.name.lower())] if len(tmp) == len(job.parents): job.status = Status.READY job.hold = True - Log.debug("Setting job: {0} status to: READY for be held (all parents queuing, running or completed)...".format(job.name)) + Log.debug( + "Setting job: {0} status to: READY for be held (all parents queuing, running or completed)...".format(job.name)) Log.debug('Updating Held jobs') if self.job_package_map: - held_jobs = [job for job in self.get_held_jobs() if ( job.id not in self.job_package_map.keys() ) ] - held_jobs += [wrapper_job for wrapper_job in self.job_package_map.values() if wrapper_job.status == Status.HELD ] + held_jobs = [job for job in self.get_held_jobs() if ( + job.id not in self.job_package_map.keys())] + held_jobs += [wrapper_job for wrapper_job in self.job_package_map.values() + if wrapper_job.status == Status.HELD] else: held_jobs = self.get_held_jobs() for job in held_jobs: - if self.job_package_map and job.id in self.job_package_map.keys(): # Wrappers and inner jobs + if self.job_package_map and job.id in self.job_package_map.keys(): # Wrappers and inner jobs hold_wrapper = False for inner_job in job.job_list: - valid_parents = [ parent for parent in inner_job.parents if parent not in job.job_list] - tmp = [parent for parent in valid_parents if parent.status == Status.COMPLETED ] + valid_parents = [ + parent for parent in inner_job.parents if parent not in job.job_list] + tmp = [ + parent for parent in valid_parents if parent.status == Status.COMPLETED] if len(tmp) < len(valid_parents): hold_wrapper = True job.hold = hold_wrapper @@ -1118,8 +1173,9 @@ class JobList: Log.debug( "Setting job: {0} status to: Queuing (all parents completed)...".format( job.name)) - else: # Non-wrapped jobs - tmp = [parent for parent in job.parents if parent.status == Status.COMPLETED] + else: # Non-wrapped jobs + tmp = [ + parent for parent in job.parents if parent.status == Status.COMPLETED] if len(tmp) == len(job.parents): job.hold = False Log.debug( @@ -1149,12 +1205,49 @@ class JobList: # Simplifying dependencies: if a parent is already an ancestor of another parent, # we remove parent dependency if not notransitive: - self.graph = transitive_reduction(self.graph) - for job in self._job_list: - children_to_remove = [child for child in job.children if child.name not in self.graph.neighbors(job.name)] - for child in children_to_remove: - job.children.remove(child) - child.parents.remove(job) + # Transitive reduction required + current_structure = None + try: + current_structure = DbStructure.get_structure( + self.expid, self._config.LOCAL_ROOT_DIR) + except Exception as exp: + pass + # print("Lengths : " + str(len(self._job_list)) + "\t" + + # str(len(current_structure.keys()))) + structure_valid = False + if ((current_structure) and (len(self._job_list) == len(current_structure.keys()))): + structure_valid = True + # print(current_structure.keys()) + # Structure exists and is valid, use it as a source of dependencies + for job in self._job_list: + if job.name not in current_structure.keys(): + structure_valid = False + continue + if structure_valid == True: + Log.info("Using existing valid structure.") + for job in self._job_list: + children_to_remove = [ + child for child in job.children if child.name not in current_structure[job.name]] + for child in children_to_remove: + job.children.remove(child) + child.parents.remove(job) + if structure_valid == False: + # Structure does not exist or it is not be updated, attempt to create it. + # print("Current: ") + # print(current_structure) + Log.info("Updating structure persistence...") + self.graph = transitive_reduction(self.graph) + for job in self._job_list: + children_to_remove = [ + child for child in job.children if child.name not in self.graph.neighbors(job.name)] + for child in children_to_remove: + job.children.remove(child) + child.parents.remove(job) + try: + DbStructure.save_structure( + self.graph, self.expid, self._config.LOCAL_ROOT_DIR) + except Exception as exp: + pass for job in self._job_list: if not job.has_parents() and new: @@ -1174,22 +1267,25 @@ class JobList: for job in self._job_list: show_logs = job.check_warnings if job.check.lower() == 'on_submission': - Log.info('Template {0} will be checked in running time'.format(job.section)) + Log.info( + 'Template {0} will be checked in running time'.format(job.section)) continue elif job.check.lower() != 'true': - Log.info('Template {0} will not be checked'.format(job.section)) + Log.info( + 'Template {0} will not be checked'.format(job.section)) continue else: if job.section in self.sections_checked: show_logs = False - if not job.check_script(as_conf, self.parameters,show_logs): + if not job.check_script(as_conf, self.parameters, show_logs): out = False self.sections_checked.add(job.section) if out: Log.result("Scripts OK") else: Log.warning("Scripts check failed") - Log.user_warning("Running after failed scripts check is at your own risk!") + Log.user_warning( + "Running after failed scripts check is at your own risk!") return out def _remove_job(self, job): @@ -1209,7 +1305,7 @@ class JobList: self._job_list.remove(job) - def rerun(self, chunk_list, notransitive=False,monitor=False): + def rerun(self, chunk_list, notransitive=False, monitor=False): """ Updates job list to rerun the jobs specified by chunk_list @@ -1221,14 +1317,17 @@ class JobList: Log.info("Adding dependencies...") dependencies = dict() for job_section in jobs_parser.sections(): - Log.debug("Reading rerun dependencies for {0} jobs".format(job_section)) + Log.debug( + "Reading rerun dependencies for {0} jobs".format(job_section)) # If does not has rerun dependencies, do nothing if not jobs_parser.has_option(job_section, "RERUN_DEPENDENCIES"): continue - dependencies_keys = jobs_parser.get(job_section, "RERUN_DEPENDENCIES").split() - dependencies = JobList._manage_dependencies(dependencies_keys, self._dic_jobs,job_section) + dependencies_keys = jobs_parser.get( + job_section, "RERUN_DEPENDENCIES").split() + dependencies = JobList._manage_dependencies( + dependencies_keys, self._dic_jobs, job_section) for job in self._job_list: job.status = Status.COMPLETED @@ -1244,7 +1343,7 @@ class JobList: for c in m['cs']: Log.debug("Chunk: " + c) chunk = int(c) - for job in [i for i in self._job_list if i.date == date and i.member == member and (i.chunk == chunk ) ]: + for job in [i for i in self._job_list if i.date == date and i.member == member and (i.chunk == chunk)]: if not job.rerun_only or chunk != previous_chunk + 1: job.status = Status.WAITING @@ -1256,7 +1355,7 @@ class JobList: for key in dependencies_keys: skip, (current_chunk, current_member, current_date) = JobList._calculate_dependency_metadata(chunk, member, date, - dependencies[key]) + dependencies[key]) if skip: continue @@ -1266,13 +1365,12 @@ class JobList: parent.status = Status.WAITING Log.debug("Parent: " + parent.name) - for job in [j for j in self._job_list if j.status == Status.COMPLETED]: if job.synchronize is None: self._remove_job(job) self.update_genealogy(notransitive=notransitive) - for job in [j for j in self._job_list if j.synchronize !=None]: + for job in [j for j in self._job_list if j.synchronize != None]: if job.status == Status.COMPLETED: job.status = Status.WAITING else: @@ -1299,9 +1397,9 @@ class JobList: self.update_genealogy(notransitive=notransitive) del self._dic_jobs - def print_with_status(self, statusChange = None, nocolor = False, existingList = None): + def print_with_status(self, statusChange=None, nocolor=False, existingList=None): """ - Returns the string representation of the dependency tree of + Returns the string representation of the dependency tree of the Job List :param statusChange: List of changes in the list, supplied in set status @@ -1316,11 +1414,13 @@ class JobList: # nocolor = True allJobs = self.get_all() if existingList is None else existingList # Header - result = (bcolors.BOLD if nocolor == False else '') + "## String representation of Job List [" + str(len(allJobs)) + "] " + result = (bcolors.BOLD if nocolor == False else '') + \ + "## String representation of Job List [" + str(len(allJobs)) + "] " if statusChange is not None: - result += "with " + (bcolors.OKGREEN if nocolor == False else '') + str(len(statusChange.keys())) + " Change(s) ##" + (bcolors.ENDC + bcolors.ENDC if nocolor == False else '') + result += "with " + (bcolors.OKGREEN if nocolor == False else '') + str(len(statusChange.keys()) + ) + " Change(s) ##" + (bcolors.ENDC + bcolors.ENDC if nocolor == False else '') else: - result += " ## " + result += " ## " # Find root root = None @@ -1331,8 +1431,9 @@ class JobList: print(root) # root exists if root is not None: - result += self._recursion_print(root, 0, visited, statusChange = statusChange, nocolor = nocolor) - else: + result += self._recursion_print(root, 0, visited, + statusChange=statusChange, nocolor=nocolor) + else: result += "\nCannot find root." return result @@ -1346,7 +1447,8 @@ class JobList: :rtype: String """ allJobs = self.get_all() - result = "## String representation of Job List [" + str(len(allJobs)) + "] ##" + result = "## String representation of Job List [" + str( + len(allJobs)) + "] ##" # Find root root = None @@ -1356,13 +1458,13 @@ class JobList: # root exists if root is not None: - result += self._recursion_print(root, 0) - else: + result += self._recursion_print(root, 0) + else: result += "\nCannot find root." return result - def _recursion_print(self, job, level, visited, statusChange = None, nocolor = False): + def _recursion_print(self, job, level, visited, statusChange=None, nocolor=False): """ Returns the list of children in a recursive way Traverses the dependency tree @@ -1372,32 +1474,38 @@ class JobList: """ result = "" if job.name not in visited: - visited.append(job.name) + visited.append(job.name) prefix = "" for i in range(level): - prefix += "| " + prefix += "| " # Prefix + Job Name - result = "\n"+ prefix + \ - (bcolors.BOLD + bcolors.CODE_TO_COLOR[job.status] if nocolor == False else '') + \ - job.name + \ - (bcolors.ENDC + bcolors.ENDC if nocolor == False else '') - if len(job._children) > 0: + result = "\n" + prefix + \ + (bcolors.BOLD + bcolors.CODE_TO_COLOR[job.status] if nocolor == False else '') + \ + job.name + \ + (bcolors.ENDC + bcolors.ENDC if nocolor == False else '') + if len(job._children) > 0: level += 1 children = job._children total_children = len(job._children) # Writes children number and status if color are not being showed result += " ~ [" + str(total_children) + (" children] " if total_children > 1 else " child] ") + \ - ("[" +Status.VALUE_TO_KEY[job.status] + "] " if nocolor == True else "") + ("[" + Status.VALUE_TO_KEY[job.status] + + "] " if nocolor == True else "") if statusChange is not None: # Writes change if performed - result += (bcolors.BOLD + bcolors.OKGREEN if nocolor == False else '') - result += (statusChange[job.name] if job.name in statusChange else "") - result += (bcolors.ENDC + bcolors.ENDC if nocolor == False else "") - + result += (bcolors.BOLD + + bcolors.OKGREEN if nocolor == False else '') + result += (statusChange[job.name] + if job.name in statusChange else "") + result += (bcolors.ENDC + + bcolors.ENDC if nocolor == False else "") + for child in children: # Continues recursion - result += self._recursion_print(child, level, visited, statusChange=statusChange, nocolor = nocolor) + result += self._recursion_print( + child, level, visited, statusChange=statusChange, nocolor=nocolor) else: - result += (" [" + Status.VALUE_TO_KEY[job.status] + "] " if nocolor == True else "") + result += (" [" + Status.VALUE_TO_KEY[job.status] + + "] " if nocolor == True else "") - return result \ No newline at end of file + return result diff --git a/autosubmit/job/job_utils.py b/autosubmit/job/job_utils.py index 14cc0030e..b73e21fba 100644 --- a/autosubmit/job/job_utils.py +++ b/autosubmit/job/job_utils.py @@ -26,16 +26,20 @@ from networkx import NetworkXError def transitive_reduction(graph): - if not is_directed_acyclic_graph(graph): - raise NetworkXError("Transitive reduction only uniquely defined on directed acyclic graphs.") - reduced_graph = DiGraph() - reduced_graph.add_nodes_from(graph.nodes()) - for u in graph: - u_edges = set(graph[u]) - for v in graph[u]: - u_edges -= {y for x, y in dfs_edges(graph, v)} - reduced_graph.add_edges_from((u, v) for v in u_edges) - return reduced_graph + try: + return networkx.algorithms.dag.transitive_reduction(graph) + except Exception as exp: + return None + # if not is_directed_acyclic_graph(graph): + # raise NetworkXError("Transitive reduction only uniquely defined on directed acyclic graphs.") + # reduced_graph = DiGraph() + # reduced_graph.add_nodes_from(graph.nodes()) + # for u in graph: + # u_edges = set(graph[u]) + # for v in graph[u]: + # u_edges -= {y for x, y in dfs_edges(graph, v)} + # reduced_graph.add_edges_from((u, v) for v in u_edges) + # return reduced_graph class Dependency(object): @@ -60,6 +64,3 @@ class Dependency(object): else: self.select_chunks_orig.append([]) - - - -- GitLab