-
Notifications
You must be signed in to change notification settings - Fork 10
Add Python 3.14 support #402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e315362
562c67c
8f479a6
d4a0929
62df9c7
6c9cf36
b4e9957
72ef3ac
a87b57c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,8 @@ dependencies: | |||||
| # Base | ||||||
| # ================= | ||||||
| - pip | ||||||
| - python >=3.11,<3.14 | ||||||
| - python >=3.11,<3.15 | ||||||
| - setuptools | ||||||
|
||||||
| - setuptools | |
| - setuptools >=65.0.0 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,10 +7,12 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import multiprocessing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os.path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import queue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sqlite3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import tarfile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
forsyth2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import traceback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import DefaultDict, List, Optional, Set, Tuple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -282,10 +284,10 @@ def extract_database( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if args.workers > 1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug("Running zstash {} with multiprocessing".format(cmd)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures = multiprocess_extract( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args.workers, matches, keep_files, keep, cache, cur, args | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args.workers, matches, keep_files, keep, cache, args | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures = extractFiles(matches, keep_files, keep, cache, cur, args) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| failures = extractFiles(matches, keep_files, keep, cache, args, None, cur) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Close database | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug("Closing index database") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -300,7 +302,6 @@ def multiprocess_extract( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keep_files: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keep_tars: Optional[bool], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache: str, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cur: sqlite3.Cursor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args: argparse.Namespace, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> List[FilesRow]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -314,12 +315,9 @@ def multiprocess_extract( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the processes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar_to_size_unsorted: DefaultDict[str, float] = collections.defaultdict(float) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db_row: FilesRow | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| size: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for db_row in matches: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar, size = db_row.tar, db_row.size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar_to_size_unsorted[tar] += size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Sort by the size. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar_to_size_unsorted[db_row.tar] += db_row.size | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar_to_size: collections.OrderedDict[str, float] = collections.OrderedDict( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sorted(tar_to_size_unsorted.items(), key=lambda x: x[1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -335,46 +333,52 @@ def multiprocess_extract( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # A min heap, of (work, worker_idx) tuples, work is the size of data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # that worker_idx needs to work on. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # We can efficiently get the worker with the least amount of work. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
forsyth2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| work_to_workers: List[Tuple[int, int]] = [(0, i) for i in range(num_workers)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heapq.heapify(workers_to_tars) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| work_to_workers: List[Tuple[float, int]] = [(0.0, i) for i in range(num_workers)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heapq.heapify(work_to_workers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was apparently a mistake that was fixed in this PR (see #426). That is, it should have always been |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Using a greedy approach, populate workers_to_tars. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, tar in enumerate(tar_to_size): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for tar in tar_to_size: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The worker with the least work should get the current largest amount of work. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_work: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_work: float | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| worker_idx: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_work, worker_idx = heapq.heappop(work_to_workers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_to_tars[worker_idx].add(tar) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Add this worker back to the heap, with the new amount of work. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| worker_tuple: Tuple[float, int] = (workers_work + tar_to_size[tar], worker_idx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # FIXME: error: Cannot infer type argument 1 of "heappush" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heapq.heappush(work_to_workers, worker_tuple) # type: ignore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heapq.heappush(work_to_workers, worker_tuple) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # For worker i, workers_to_matches[i] is a list of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # matches from the database for it to process. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_to_matches: List[List[FilesRow]] = [[] for _ in range(num_workers)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_idx: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for db_row in matches: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tar = db_row.tar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_idx: int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for workers_idx in range(len(workers_to_tars)): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if tar in workers_to_tars[workers_idx]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # This worker gets this db_row. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if db_row.tar in workers_to_tars[workers_idx]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_to_matches[workers_idx].append(db_row) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers_to_matches[workers_idx].append(db_row) | |
| workers_to_matches[workers_idx].append(db_row) | |
| # Once the appropriate worker is found, no need to check others. | |
| break |
Copilot
AI
Feb 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new multiprocessing.Manager() is created for the print monitor but never explicitly shut down. This can leave the manager server process running longer than necessary and can cause hangs on interpreter shutdown in some environments. After worker processes finish, call manager.shutdown() (and ideally join() worker processes) before returning from multiprocess_extract().
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a sleep(0.01) in the main process loop is good for reducing CPU usage while waiting for worker processes. However, this means the main process will check for failures at most 100 times per second. For very short-running jobs, this could add up to 10ms of latency before failures are detected. This is likely acceptable, but consider documenting this trade-off.
| pass | |
| pass | |
| # Sleep briefly to avoid busy-waiting while worker processes run. | |
| # This limits failure detection to ~100 checks per second (worst-case | |
| # ~10 ms latency for very short-running jobs), which is an intentional | |
| # trade-off for lower CPU usage in the main process. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another note from Claude, after mentioning "We routinely extract archives 10-20 TB in size. The default individual tar size is 256 GB."
With 40-80 tars running for hours each, this loop spins ~360,000 times per hour doing essentially nothing but checking is_alive(). That's harmless on its own, but consider: if a worker crashes without putting anything in failure_queue, the main process has no timeout mechanism. It will spin forever. At 10-20 TB job sizes, a silent worker death — due to OOM, a storage node going down, an HPSS timeout — is not a hypothetical.
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manager.shutdown() call may prevent proper cleanup of failures. This is called immediately after collecting failures from processes, but the PrintMonitor and ExtractWorker objects may still hold references to manager-created objects like locks and values. Consider moving the shutdown call after all cleanup is complete, or verify that all worker references to manager objects have been released. Additionally, verify that manager.shutdown() is the correct method - it may need to be manager.join() depending on the multiprocessing.Manager() implementation.
forsyth2 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter order change for extractFiles is not backwards compatible. Moving cur from position 4 to position 6 and making it optional, while adding multiprocess_worker before it, could break any external code that calls this function with positional arguments. While this might be an internal function, it's worth considering whether this breaking change is necessary or if it could be avoided by keeping the parameter order and just making cur optional with a default value of None.
| args: argparse.Namespace, | |
| multiprocess_worker: Optional[parallel.ExtractWorker] = None, | |
| cur: Optional[sqlite3.Cursor] = None, | |
| cur: Optional[sqlite3.Cursor] = None, | |
| args: Optional[argparse.Namespace] = None, | |
| multiprocess_worker: Optional[parallel.ExtractWorker] = None, |
Copilot
AI
Feb 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When cur is None, a new SQLite connection is opened for the worker, but it is only closed on the normal return path. If an exception escapes the main loop (e.g., HPSS retrieval failing after retries), the connection may leak. Wrap the main body in a try/finally that closes cur/con when close_db is true.
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential resource leak: If an exception occurs between lines 524-527 (when opening the database connection), the connection and cursor will not be properly closed because close_db is set to True on line 528, but if an exception happens before that line, the finally block or exception handler won't close the connection. Consider wrapping the connection opening in a try block or using a context manager to ensure proper cleanup.
| if cur is None: | |
| con: sqlite3.Connection = sqlite3.connect( | |
| get_db_filename(cache), detect_types=sqlite3.PARSE_DECLTYPES | |
| ) | |
| cur = con.cursor() | |
| close_db: bool = True | |
| else: | |
| close_db = False | |
| con: Optional[sqlite3.Connection] = None | |
| close_db: bool = False | |
| if cur is None: | |
| try: | |
| con = sqlite3.connect( | |
| get_db_filename(cache), detect_types=sqlite3.PARSE_DECLTYPES | |
| ) | |
| cur = con.cursor() | |
| close_db = True | |
| except Exception: | |
| if con is not None: | |
| con.close() | |
| raise |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The database cursor handling has been changed to open a new connection per worker process when running in parallel. This is necessary because sqlite3 connections and cursors are not thread-safe or process-safe. However, verify that all test cases pass with this change, particularly tests that involve concurrent database access, as this changes the connection model significantly.
| # Open database connection if not provided (parallel case) | |
| if cur is None: | |
| con: sqlite3.Connection = sqlite3.connect( | |
| get_db_filename(cache), detect_types=sqlite3.PARSE_DECLTYPES | |
| ) | |
| cur = con.cursor() | |
| close_db: bool = True | |
| else: | |
| close_db = False | |
| # Open database connection if not provided, or always in parallel. | |
| con: Optional[sqlite3.Connection] | |
| close_db: bool = False | |
| if multiprocess_worker is not None: | |
| # In parallel, each worker must use its own sqlite3 connection | |
| # because sqlite3 connections and cursors are not safe to share | |
| # between processes. | |
| con = sqlite3.connect( | |
| get_db_filename(cache), detect_types=sqlite3.PARSE_DECLTYPES | |
| ) | |
| cur = con.cursor() | |
| close_db = True | |
| elif cur is None: | |
| con = sqlite3.connect( | |
| get_db_filename(cache), detect_types=sqlite3.PARSE_DECLTYPES | |
| ) | |
| cur = con.cursor() | |
| close_db = True |
forsyth2 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tar extraction filter should use "data" instead of "tar" for Python 3.12+. The "tar" filter was deprecated in Python 3.14 and will be removed. According to Python documentation, "data" is the recommended filter that prevents path traversal attacks while being backwards compatible. This will ensure compatibility with Python 3.14 and future versions.
| tar.extract(tarinfo, filter="tar") | |
| tar.extract(tarinfo, filter="data") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This must be "tar" for tests to pass.
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tarfile.extract filter parameter was introduced in Python 3.12 to address security concerns. However, the "tar" filter is deprecated as of Python 3.14 (PEP 706). For Python 3.14+ compatibility, consider using filter="data" instead, which is the recommended secure filter. The "data" filter is safer and future-proof. Alternatively, explicitly handle different Python versions if the "tar" filter's behavior is required.
| tar.extract(tarinfo, filter="tar") | |
| tar.extract(tarinfo, filter="data") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We must use filter="tar" or else we get this test failure:
======================================================================
FAIL: test_hpss_none_fs_off (tests.integration.python_tests.group_by_workflow.test_cache_fs.TestCacheFs.test_hpss_none_fs_off)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/global/u1/f/forsyth/ez/zstash/tests/integration/python_tests/group_by_workflow/test_cache_fs.py", line 152, in test_hpss_none_fs_off
self.assertTrue(os.path.islink("dir2/file1_soft.txt")) # DIFFERENT from fs_on
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: False is not true
----------------------------------------------------------------------
Ran 1 test in 1.045s
FAILED (failures=1)
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The database connection cleanup at lines 742-744 only happens if the function exits normally. If an exception occurs in the try block (lines 629-712) or elsewhere in the function, the database connection will not be closed, leading to a resource leak. Consider wrapping the database operations in a try-finally block or using a context manager to ensure the connection is always closed when opened by this function.
Uh oh!
There was an error while loading. Please reload this page.