Skip to content

Commit 3db0cf5

Browse files
authored
Don't create logger on --skip_solve and explicitly close logger (#1371)
Avoids 'too many open files' error with large simulations.
1 parent d4cc65f commit 3db0cf5

3 files changed

Lines changed: 82 additions & 28 deletions

File tree

gridpath/common_functions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,33 @@ def flush(self):
453453
self.terminal.flush()
454454
self.log_file.flush()
455455

456+
def close(self):
457+
"""
458+
Close the log file to release the file descriptor.
459+
Critical for preventing "too many open files" errors.
460+
"""
461+
if hasattr(self, "log_file") and self.log_file and not self.log_file.closed:
462+
self.log_file.close()
463+
464+
def __del__(self):
465+
"""
466+
Ensure log file is closed when object is garbage collected
467+
"""
468+
self.close()
469+
470+
def __enter__(self):
471+
"""
472+
Support context manager protocol
473+
"""
474+
return self
475+
476+
def __exit__(self, exc_type, exc_val, exc_tb):
477+
"""
478+
Close log file when exiting context manager
479+
"""
480+
self.close()
481+
return False
482+
456483

457484
def string_from_time(datetime_string):
458485
"""

gridpath/run_end_to_end.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,9 @@ def main(args=None):
455455
print("Done. Run finished on {}.".format(end_time))
456456

457457
# If logging, we need to return sys.stdout to original (i.e. stop writing
458-
# to log file)
458+
# to log file) and close the log file to release file descriptor
459459
if parsed_args.log:
460+
logger.close()
460461
sys.stdout = stdout_original
461462
sys.stderr = stderr_original
462463

gridpath/run_scenario.py

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from csv import reader, writer
2626
import datetime
2727
import dill
28+
import gc
2829
import json
2930
from multiprocessing import get_context, Manager
3031
import os.path
@@ -210,7 +211,43 @@ def run_optimization_for_subproblem_stage(
210211
Return the objective function (Total_Cost) value; only used in testing mode
211212
212213
"""
213-
# If directed to do so, log optimization run
214+
# Determine whether to skip this optimization before creating logger to
215+
# avoid the logging overhead and opening too many files for large
216+
# simulations
217+
skip_solve = False
218+
if parsed_arguments.incomplete_only:
219+
termination_condition_file = os.path.join(
220+
scenario_directory,
221+
weather_iteration_directory,
222+
hydro_iteration_directory,
223+
availability_iteration_directory,
224+
subproblem_directory,
225+
stage_directory,
226+
"results",
227+
"termination_condition.txt",
228+
)
229+
if os.path.isfile(termination_condition_file):
230+
with open(termination_condition_file, "r") as f:
231+
termination_condition = f.read()
232+
if not parsed_arguments.quiet:
233+
print(
234+
f"Subproblem stage {subproblem_directory} "
235+
f"{stage_directory} "
236+
f"previously solved with termination condition "
237+
f"**{termination_condition}**. Skipping solve."
238+
)
239+
skip_solve = True
240+
if not parsed_arguments.quiet:
241+
print(
242+
f"Skipping {weather_iteration_directory}/{hydro_iteration_directory}/"
243+
f"{availability_iteration_directory}/{subproblem_directory}/{stage_directory} "
244+
f"(already solved)"
245+
)
246+
# Force garbage collection to release file descriptor immediately
247+
gc.collect()
248+
return None # Exit early without creating logger
249+
250+
# If directed to do so, log optimization run (only if actually solving)
214251
if parsed_arguments.log:
215252
logs_directory = create_logs_directory_if_not_exists(
216253
scenario_directory,
@@ -238,31 +275,6 @@ def run_optimization_for_subproblem_stage(
238275
sys.stdout = logger
239276
sys.stderr = logger
240277

241-
# Determine whether to skip this optimization
242-
skip_solve = False
243-
if parsed_arguments.incomplete_only:
244-
termination_condition_file = os.path.join(
245-
scenario_directory,
246-
weather_iteration_directory,
247-
hydro_iteration_directory,
248-
availability_iteration_directory,
249-
subproblem_directory,
250-
stage_directory,
251-
"results",
252-
"termination_condition.txt",
253-
)
254-
if os.path.isfile(termination_condition_file):
255-
with open(termination_condition_file, "r") as f:
256-
termination_condition = f.read()
257-
if not parsed_arguments.quiet:
258-
print(
259-
f"Subproblem stage {subproblem_directory} "
260-
f"{stage_directory} "
261-
f"previously solved with termination condition "
262-
f"**{termination_condition}**. Skipping solve."
263-
)
264-
skip_solve = True
265-
266278
if not skip_solve:
267279
# If directed, set temporary file directory to be the logs directory
268280
# In conjunction with --keepfiles, this will write the solver solution
@@ -395,10 +407,14 @@ def run_optimization_for_subproblem_stage(
395407
)
396408

397409
# If logging, we need to return sys.stdout to original (i.e. stop writing
398-
# to log file)
410+
# to log file) and close the log file to release file descriptor
399411
if parsed_arguments.log:
412+
logger.close()
400413
sys.stdout = stdout_original
401414
sys.stderr = stderr_original
415+
# Explicitly delete logger reference and force garbage collection
416+
del logger
417+
gc.collect()
402418

403419
# Return the objective function value (in the testing suite, the value
404420
# gets checked against the expected value, but this is the only place
@@ -446,6 +462,8 @@ def run_optimization_for_subproblem(
446462
multi_stage,
447463
parsed_arguments,
448464
)
465+
# Force garbage collection after each stage to release file descriptors
466+
gc.collect()
449467

450468

451469
def run_optimization_for_subproblem_pool(pool_datum):
@@ -551,6 +569,10 @@ def solve_sequentially(
551569
parsed_arguments=parsed_arguments,
552570
objective_values=objective_values,
553571
)
572+
# Force garbage collection after each subproblem to release file descriptors
573+
gc.collect()
574+
# Force garbage collection after each availability iteration
575+
gc.collect()
554576

555577
return objective_values
556578

@@ -915,6 +937,10 @@ def save_results(
915937
dynamic_components=dynamic_components,
916938
verbose=parsed_arguments.verbose,
917939
)
940+
941+
# Force garbage collection to release file descriptors immediately
942+
# This prevents "too many open files" errors when processing many iterations
943+
gc.collect()
918944
# If solver status is not ok, don't export results and print some
919945
# messages for the user
920946
else:

0 commit comments

Comments
 (0)