Skip to content

Commit f3e4ef3

Browse files
authored
fix: Clean up temp CSV in AthenaQuery.as_dataframe (v2) (#6345)
AthenaQuery.as_dataframe() downloaded the Athena query result to <tempdir>/<execution_id>.csv and never removed it, so every query left a file behind and repeated queries could fill the local disk. Wrap the download and read_csv in try/finally and remove the temporary file afterwards, including when the download or parsing fails. Cleanup is best-effort: an OSError is logged as a warning and does not affect the returned DataFrame. Fixes #5100
1 parent 08b6fde commit f3e4ef3

2 files changed

Lines changed: 76 additions & 8 deletions

File tree

‎src/sagemaker/feature_store/feature_group.py‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ def get_query_execution(self) -> Dict[str, Any]:
149149
def as_dataframe(self, **kwargs) -> DataFrame:
150150
"""Download the result of the current query and load it into a DataFrame.
151151
152+
The query result is downloaded to a temporary local CSV file, which is removed
153+
after it has been loaded (or if downloading/loading fails).
154+
152155
Args:
153156
**kwargs (object): key arguments used for the method pandas.read_csv to be able to
154157
have a better tuning on data. For more info read:
@@ -168,15 +171,27 @@ def as_dataframe(self, **kwargs) -> DataFrame:
168171
output_filename = os.path.join(
169172
tempfile.gettempdir(), f"{self._current_query_execution_id}.csv"
170173
)
171-
self.sagemaker_session.download_athena_query_result(
172-
bucket=self._result_bucket,
173-
prefix=self._result_file_prefix,
174-
query_execution_id=self._current_query_execution_id,
175-
filename=output_filename,
176-
)
174+
try:
175+
self.sagemaker_session.download_athena_query_result(
176+
bucket=self._result_bucket,
177+
prefix=self._result_file_prefix,
178+
query_execution_id=self._current_query_execution_id,
179+
filename=output_filename,
180+
)
181+
182+
kwargs.pop("delimiter", None)
183+
return pd.read_csv(filepath_or_buffer=output_filename, delimiter=",", **kwargs)
184+
finally:
185+
_remove_temp_file(output_filename)
186+
177187

178-
kwargs.pop("delimiter", None)
179-
return pd.read_csv(filepath_or_buffer=output_filename, delimiter=",", **kwargs)
188+
def _remove_temp_file(path: str) -> None:
189+
"""Best-effort removal of a temporary file; never raises."""
190+
try:
191+
if os.path.exists(path):
192+
os.remove(path)
193+
except OSError as e:
194+
logger.warning("Failed to remove temporary query result file %s: %s", path, e)
180195

181196

182197
@attr.s

‎tests/unit/sagemaker/feature_store/test_feature_group.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,3 +1628,56 @@ def test_athena_query_as_dataframe_query_running(sagemaker_session_mock, query):
16281628
with pytest.raises(RuntimeError) as error:
16291629
query.as_dataframe()
16301630
assert "Current query query_id is still being executed" in str(error)
1631+
1632+
1633+
def _write_query_result_csv(**kwargs):
1634+
with open(kwargs["filename"], "w") as f:
1635+
f.write("col\n1\n2\n3\n")
1636+
1637+
1638+
def _prepare_succeeded_query(sagemaker_session_mock, query):
1639+
sagemaker_session_mock.get_query_execution.return_value = {
1640+
"QueryExecution": {"Status": {"State": "SUCCEEDED"}}
1641+
}
1642+
sagemaker_session_mock.download_athena_query_result.side_effect = _write_query_result_csv
1643+
query._current_query_execution_id = "query_id"
1644+
query._result_bucket = "bucket"
1645+
query._result_file_prefix = "prefix"
1646+
1647+
1648+
def test_athena_query_as_dataframe_removes_temp_file(sagemaker_session_mock, query, tmp_path):
1649+
_prepare_succeeded_query(sagemaker_session_mock, query)
1650+
expected_file = tmp_path / "query_id.csv"
1651+
1652+
with patch("tempfile.gettempdir", Mock(return_value=str(tmp_path))):
1653+
df = query.as_dataframe()
1654+
1655+
download_kwargs = sagemaker_session_mock.download_athena_query_result.call_args[1]
1656+
assert df["col"].tolist() == [1, 2, 3]
1657+
assert download_kwargs["filename"] == str(expected_file)
1658+
assert not expected_file.exists()
1659+
1660+
1661+
@patch("pandas.read_csv", Mock(side_effect=ValueError("bad csv")))
1662+
def test_athena_query_as_dataframe_removes_temp_file_when_read_fails(
1663+
sagemaker_session_mock, query, tmp_path
1664+
):
1665+
_prepare_succeeded_query(sagemaker_session_mock, query)
1666+
1667+
with patch("tempfile.gettempdir", Mock(return_value=str(tmp_path))):
1668+
with pytest.raises(ValueError, match="bad csv"):
1669+
query.as_dataframe()
1670+
1671+
assert not (tmp_path / "query_id.csv").exists()
1672+
1673+
1674+
def test_athena_query_as_dataframe_cleanup_failure_does_not_raise(
1675+
sagemaker_session_mock, query, tmp_path
1676+
):
1677+
_prepare_succeeded_query(sagemaker_session_mock, query)
1678+
1679+
with patch("tempfile.gettempdir", Mock(return_value=str(tmp_path))):
1680+
with patch("os.remove", Mock(side_effect=PermissionError("file in use"))):
1681+
df = query.as_dataframe()
1682+
1683+
assert len(df) == 3

0 commit comments

Comments
 (0)