Skip to content

Commit f555e71

Browse files
authored
Keep log file of failed downloads + add exceptions to the log (#182)
* copy log file to the temp dir before removing project directory if download fails * address review * codestyle * add active waiting test * log errors when project download fails (refs #156) * write traceback to the log check that traceback present in the log in test * formatting * address review
1 parent 8d00585 commit f555e71

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

mergin/client_pull.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import shutil
1717
import tempfile
1818
import typing
19+
import traceback
1920

2021
import concurrent.futures
2122

@@ -55,6 +56,7 @@ def __init__(
5556
self.mp = mp # MerginProject instance
5657
self.is_cancelled = False
5758
self.project_info = project_info # parsed JSON with project info returned from the server
59+
self.failure_log_file = None # log file, copied from the project directory if download fails
5860

5961
def dump(self):
6062
print("--- JOB ---", self.total_size, "bytes")
@@ -99,12 +101,25 @@ def _cleanup_failed_download(directory, mergin_project=None):
99101
If a download job fails, there will be the newly created directory left behind with some
100102
temporary files in it. We want to remove it because a new download would fail because
101103
the directory already exists.
104+
105+
Returns path to the client log file or None if log file does not exist.
102106
"""
103107
# First try to get the Mergin Maps project logger and remove its handlers to allow the log file deletion
104108
if mergin_project is not None:
105109
mergin_project.remove_logging_handler()
106110

111+
# keep log file as it might contain useful debug info
112+
log_file = os.path.join(directory, ".mergin", "client-log.txt")
113+
dest_path = None
114+
115+
if os.path.exists(log_file):
116+
tmp_file = tempfile.NamedTemporaryFile(prefix="mergin-", suffix=".txt", delete=False)
117+
tmp_file.close()
118+
dest_path = tmp_file.name
119+
shutil.copyfile(log_file, dest_path)
120+
107121
shutil.rmtree(directory)
122+
return dest_path
108123

109124

110125
def download_project_async(mc, project_path, directory, project_version=None):
@@ -184,7 +199,11 @@ def download_project_is_running(job):
184199
"""
185200
for future in job.futures:
186201
if future.done() and future.exception() is not None:
187-
_cleanup_failed_download(job.directory, job.mp)
202+
job.mp.log.error(
203+
"Error while downloading project: " + "".join(traceback.format_exception(future.exception()))
204+
)
205+
job.mp.log.info("--- download aborted")
206+
job.failure_log_file = _cleanup_failed_download(job.directory, job.mp)
188207
raise future.exception()
189208
if future.running():
190209
return True
@@ -206,7 +225,11 @@ def download_project_finalize(job):
206225
# make sure any exceptions from threads are not lost
207226
for future in job.futures:
208227
if future.exception() is not None:
209-
_cleanup_failed_download(job.directory, job.mp)
228+
job.mp.log.error(
229+
"Error while downloading project: " + "".join(traceback.format_exception(future.exception()))
230+
)
231+
job.mp.log.info("--- download aborted")
232+
job.failure_log_file = _cleanup_failed_download(job.directory, job.mp)
210233
raise future.exception()
211234

212235
job.mp.log.info("--- download finished")

mergin/test/test_client.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@
99
import pytz
1010
import sqlite3
1111
import glob
12+
import time
1213

1314
from .. import InvalidProject
1415
from ..client import MerginClient, ClientError, MerginProject, LoginError, decode_token_data, TokenError, ServerType
1516
from ..client_push import push_project_async, push_project_cancel
17+
from ..client_pull import (
18+
download_project_async,
19+
download_project_wait,
20+
download_project_finalize,
21+
download_project_is_running,
22+
download_project_cancel,
23+
)
1624
from ..utils import (
1725
generate_checksum,
1826
get_versions_with_file_changes,
@@ -2214,3 +2222,44 @@ def test_download_files(mc: MerginClient):
22142222

22152223
with pytest.raises(ClientError, match=f"No \\[non_existing\\.file\\] exists at version v3"):
22162224
mc.download_files(project_dir, [f_updated, "non_existing.file"], version="v3")
2225+
2226+
2227+
def test_download_failure(mc):
2228+
test_project = "test_download_failure"
2229+
project = API_USER + "/" + test_project
2230+
project_dir = os.path.join(TMP_DIR, test_project)
2231+
download_dir = os.path.join(TMP_DIR, "download", test_project)
2232+
2233+
cleanup(mc, project, [project_dir, download_dir])
2234+
# prepare local project
2235+
shutil.copytree(TEST_DATA_DIR, project_dir)
2236+
2237+
# create remote project
2238+
mc.create_project_and_push(test_project, directory=project_dir)
2239+
2240+
# download project async
2241+
with pytest.raises(IsADirectoryError):
2242+
job = download_project_async(mc, project, download_dir)
2243+
os.makedirs(os.path.join(download_dir, "base.gpkg.0"))
2244+
download_project_wait(job)
2245+
download_project_finalize(job)
2246+
2247+
assert job.failure_log_file is not None
2248+
with open(job.failure_log_file, "r", encoding="utf-8") as f:
2249+
content = f.read()
2250+
assert "Traceback" in content
2251+
2252+
# active waiting
2253+
remove_folders([download_dir])
2254+
job = download_project_async(mc, project, download_dir)
2255+
os.makedirs(os.path.join(download_dir, "base.gpkg.0"))
2256+
with pytest.raises(IsADirectoryError):
2257+
while True:
2258+
assert download_project_is_running(job)
2259+
2260+
download_project_cancel(job) # to clean up things
2261+
2262+
assert job.failure_log_file is not None
2263+
with open(job.failure_log_file, "r", encoding="utf-8") as f:
2264+
content = f.read()
2265+
assert "Traceback" in content

0 commit comments

Comments
 (0)