Skip to content

Commit 87d3354

Browse files
GitHKAndrei Neagu
and
Andrei Neagu
authored
✨ dynamic-sidecar logs changes to input ports (#5999)
Co-authored-by: Andrei Neagu <[email protected]>
1 parent b5d82e0 commit 87d3354

File tree

3 files changed

+127
-12
lines changed

3 files changed

+127
-12
lines changed

packages/service-library/src/servicelib/file_utils.py

+53-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import asyncio
22
import hashlib
33
import shutil
4+
from contextlib import contextmanager
5+
from logging import Logger
46
from pathlib import Path
5-
from typing import Final, Protocol
7+
from typing import Final, Iterator, Protocol
68

79
# https://docs.python.org/3/library/shutil.html#shutil.rmtree
810
# https://docs.python.org/3/library/os.html#os.remove
@@ -60,10 +62,59 @@ async def create_sha256_checksum(
6062

6163
async def _eval_hash_async(
6264
async_stream: AsyncStream,
63-
hasher: "hashlib._Hash", # noqa: SLF001
65+
hasher: "hashlib._Hash",
6466
chunk_size: ByteSize,
6567
) -> str:
6668
while chunk := await async_stream.read(chunk_size):
6769
hasher.update(chunk)
6870
digest = hasher.hexdigest()
6971
return f"{digest}"
72+
73+
74+
def _get_file_properties(path: Path) -> tuple[float, int]:
75+
stats = path.stat()
76+
return stats.st_mtime, stats.st_size
77+
78+
79+
def _get_directory_snapshot(path: Path) -> dict[str, tuple[float, int]]:
80+
return {
81+
f"{p.relative_to(path)}": _get_file_properties(p)
82+
for p in path.rglob("*")
83+
if p.is_file()
84+
}
85+
86+
87+
@contextmanager
88+
def log_directory_changes(path: Path, logger: Logger, log_level: int) -> Iterator[None]:
89+
before: dict[str, tuple[float, int]] = _get_directory_snapshot(path)
90+
yield
91+
after: dict[str, tuple[float, int]] = _get_directory_snapshot(path)
92+
93+
after_keys: set[str] = set(after.keys())
94+
before_keys: set[str] = set(before.keys())
95+
common_keys = before_keys & after_keys
96+
97+
added_elements = after_keys - before_keys
98+
removed_elements = before_keys - after_keys
99+
content_changed_elements = {x for x in common_keys if before[x] != after[x]}
100+
101+
if added_elements or removed_elements or content_changed_elements:
102+
logger.log(log_level, "File changes in path: '%s'", f"{path}")
103+
if added_elements:
104+
logger.log(
105+
log_level,
106+
"Files added:\n%s",
107+
"\n".join([f"+ {x}" for x in sorted(added_elements)]),
108+
)
109+
if removed_elements:
110+
logger.log(
111+
log_level,
112+
"Files removed:\n%s",
113+
"\n".join([f"- {x}" for x in sorted(removed_elements)]),
114+
)
115+
if content_changed_elements:
116+
logger.log(
117+
log_level,
118+
"File content changed:\n%s",
119+
"\n".join([f"* {x}" for x in sorted(content_changed_elements)]),
120+
)

packages/service-library/tests/test_file_utils.py

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# pylint: disable=redefined-outer-name
22
# pylint: disable=unused-argument
33

4+
import logging
45
from pathlib import Path
56

67
import pytest
78
from faker import Faker
8-
from servicelib.file_utils import remove_directory
9+
from servicelib.file_utils import log_directory_changes, remove_directory
10+
11+
_logger = logging.getLogger(__name__)
912

1013

1114
@pytest.fixture
@@ -80,3 +83,60 @@ async def test_remove_not_existing_directory_rasing_error(
8083
await remove_directory(
8184
path=missing_path, only_children=only_children, ignore_errors=False
8285
)
86+
87+
88+
async def test_log_directory_changes(caplog: pytest.LogCaptureFixture, some_dir: Path):
89+
# directory cretion triggers no changes
90+
caplog.clear()
91+
with log_directory_changes(some_dir, _logger, logging.ERROR):
92+
(some_dir / "a-dir").mkdir(parents=True, exist_ok=True)
93+
assert "File changes in path" not in caplog.text
94+
assert "Files added:" not in caplog.text
95+
assert "Files removed:" not in caplog.text
96+
assert "File content changed" not in caplog.text
97+
98+
# files were added
99+
caplog.clear()
100+
with log_directory_changes(some_dir, _logger, logging.ERROR):
101+
(some_dir / "hoho").touch()
102+
assert "File changes in path" in caplog.text
103+
assert "Files added:" in caplog.text
104+
assert "Files removed:" not in caplog.text
105+
assert "File content changed" not in caplog.text
106+
107+
# files were removed
108+
caplog.clear()
109+
with log_directory_changes(some_dir, _logger, logging.ERROR):
110+
await remove_directory(path=some_dir)
111+
assert "File changes in path" in caplog.text
112+
assert "Files removed:" in caplog.text
113+
assert "Files added:" not in caplog.text
114+
assert "File content changed" not in caplog.text
115+
116+
# nothing changed
117+
caplog.clear()
118+
with log_directory_changes(some_dir, _logger, logging.ERROR):
119+
pass
120+
assert caplog.text == ""
121+
122+
# files added and removed
123+
caplog.clear()
124+
some_dir.mkdir(parents=True, exist_ok=True)
125+
(some_dir / "som_other_file").touch()
126+
with log_directory_changes(some_dir, _logger, logging.ERROR):
127+
(some_dir / "som_other_file").unlink()
128+
(some_dir / "som_other_file_2").touch()
129+
assert "File changes in path" in caplog.text
130+
assert "Files added:" in caplog.text
131+
assert "Files removed:" in caplog.text
132+
assert "File content changed" not in caplog.text
133+
134+
# file content changed
135+
caplog.clear()
136+
(some_dir / "file_to_change").touch()
137+
with log_directory_changes(some_dir, _logger, logging.ERROR):
138+
(some_dir / "file_to_change").write_text("ab")
139+
assert "File changes in path" in caplog.text
140+
assert "Files added:" not in caplog.text
141+
assert "Files removed:" not in caplog.text
142+
assert "File content changed" in caplog.text

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/modules/long_running_tasks.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from models_library.rabbitmq_messages import ProgressType, SimcorePlatformStatus
1212
from pydantic import PositiveInt
1313
from servicelib.fastapi.long_running_tasks.server import TaskProgress
14+
from servicelib.file_utils import log_directory_changes
1415
from servicelib.logging_utils import log_context
1516
from servicelib.progress_bar import ProgressBarData
1617
from servicelib.utils import logged_gather
@@ -476,15 +477,18 @@ async def task_ports_inputs_pull(
476477
),
477478
description="pulling inputs",
478479
) as root_progress:
479-
transferred_bytes = await nodeports.download_target_ports(
480-
nodeports.PortTypeName.INPUTS,
481-
mounted_volumes.disk_inputs_path,
482-
port_keys=port_keys,
483-
io_log_redirect_cb=functools.partial(
484-
post_sidecar_log_message, app, log_level=logging.INFO
485-
),
486-
progress_bar=root_progress,
487-
)
480+
with log_directory_changes(
481+
mounted_volumes.disk_inputs_path, _logger, logging.INFO
482+
):
483+
transferred_bytes = await nodeports.download_target_ports(
484+
nodeports.PortTypeName.INPUTS,
485+
mounted_volumes.disk_inputs_path,
486+
port_keys=port_keys,
487+
io_log_redirect_cb=functools.partial(
488+
post_sidecar_log_message, app, log_level=logging.INFO
489+
),
490+
progress_bar=root_progress,
491+
)
488492
await post_sidecar_log_message(
489493
app, "Finished pulling inputs", log_level=logging.INFO
490494
)

0 commit comments

Comments
 (0)