Skip to content

Commit 6c1383a

Browse files
authored
Graceful handling of download cancel/interruption (#4146)
1 parent b98c281 commit 6c1383a

File tree

15 files changed

+365
-118
lines changed

15 files changed

+365
-118
lines changed

libmamba/include/mamba/core/error_handling.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ namespace mamba
6363
using base_type = mamba_error;
6464
using error_list_t = std::vector<mamba_error>;
6565

66-
explicit mamba_aggregated_error(error_list_t&& error_list);
66+
explicit mamba_aggregated_error(error_list_t&& error_list, bool with_bug_report_info = true);
6767

6868
const char* what() const noexcept override;
6969

70+
bool has_only_error(mamba_error_code code) const;
71+
7072
private:
7173

7274
error_list_t m_error_list;
7375
mutable std::string m_aggregated_message;
74-
static constexpr const char* m_base_message = "Multiple errors occurred:\n";
76+
bool m_with_bug_report_message = true;
7577
};
7678

7779
/********************************

libmamba/include/mamba/core/logging.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,7 @@ namespace mamba
955955

956956
static void activate_buffer();
957957
static void deactivate_buffer();
958+
static bool is_buffer_enabled();
958959
static void print_buffer(std::ostream& ostream);
959960

960961
private:

libmamba/include/mamba/download/mirror.hpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,11 @@ namespace mamba::download
7474
std::size_t running_transfers = 0;
7575
std::size_t successful_transfers = 0;
7676
std::size_t failed_transfers = 0;
77+
std::size_t stopped_transfers = 0;
7778
};
7879

80+
static_assert(std::default_initializable<MirrorStats>);
81+
7982
// A Mirror represents a location from where an asset can be downloaded.
8083
// It handles the generation of required requests to get the asset, and
8184
// provides some statistics about its usage.
@@ -97,9 +100,11 @@ namespace mamba::download
97100
request_generator_list
98101
get_request_generators(const std::string& url_path, const std::string& spec_sha256) const;
99102

103+
// Note: values from below accessors are obsolete as soon as acquired, because of
104+
// concurrency.
105+
100106
std::size_t max_retries() const;
101-
std::size_t successful_transfers() const;
102-
std::size_t failed_transfers() const;
107+
MirrorStats capture_stats() const;
103108

104109
bool can_accept_more_connections() const;
105110
bool can_retry_with_fewer_connections() const;
@@ -119,8 +124,6 @@ namespace mamba::download
119124
MirrorID m_id;
120125
size_t m_max_retries;
121126

122-
static_assert(std::default_initializable<MirrorStats>);
123-
124127
util::synchronized_value<MirrorStats> m_stats;
125128
};
126129

libmamba/include/mamba/download/request.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,14 @@ namespace mamba::download
5757
std::optional<std::size_t> retry_wait_seconds = std::nullopt;
5858
std::optional<TransferData> transfer = std::nullopt;
5959
std::size_t attempt_number = std::size_t(1);
60+
bool is_stop = false;
6061
};
6162

63+
inline auto make_stop_error() -> Error
64+
{
65+
return { .message = "Stopped by user request", .is_stop = true };
66+
}
67+
6268
using Result = tl::expected<Success, Error>;
6369
using MultiResult = std::vector<Result>;
6470

@@ -86,6 +92,7 @@ namespace mamba::download
8692
// TODO: remove these functions when we plug a library with continuation
8793
using on_success_callback_t = std::function<expected_t<void>(const Success&)>;
8894
using on_failure_callback_t = std::function<void(const Error&)>;
95+
using on_stopped_callback_t = std::function<void()>;
8996

9097
std::string name;
9198
// If filename is not initialized, the data will be downloaded
@@ -100,6 +107,7 @@ namespace mamba::download
100107
std::optional<progress_callback_t> progress = std::nullopt;
101108
std::optional<on_success_callback_t> on_success = std::nullopt;
102109
std::optional<on_failure_callback_t> on_failure = std::nullopt;
110+
std::optional<on_stopped_callback_t> on_stopped = std::nullopt;
103111

104112
protected:
105113

libmamba/src/api/channel_loader.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ namespace mamba
240240
error_list.push_back(std::move(error));
241241
if (ec == mamba_error_code::user_interrupted)
242242
{
243-
return tl::unexpected(mamba_aggregated_error(std::move(error_list)));
243+
return tl::unexpected(mamba_aggregated_error(std::move(error_list), false));
244244
}
245245
}
246246

libmamba/src/api/install.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,13 +627,13 @@ namespace mamba
627627
auto maybe_load = load_channels(ctx, channel_context, db, package_caches);
628628
if (!maybe_load)
629629
{
630-
throw std::runtime_error(maybe_load.error().what());
630+
throw maybe_load.error();
631631
}
632632

633633
auto maybe_prefix_data = PrefixData::create(ctx.prefix_params.target_prefix, channel_context);
634634
if (!maybe_prefix_data)
635635
{
636-
throw std::runtime_error(maybe_prefix_data.error().what());
636+
throw maybe_prefix_data.error();
637637
}
638638
PrefixData& prefix_data = maybe_prefix_data.value();
639639

libmamba/src/core/error_handling.cpp

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#include <algorithm>
2+
13
#include "mamba/core/error_handling.hpp"
24
#include "mamba/core/logging.hpp"
35

@@ -55,36 +57,57 @@ namespace mamba
5557
return m_data;
5658
}
5759

58-
constexpr const char* mamba_aggregated_error::m_base_message; // = "Many errors occurred:\n";
60+
namespace
61+
{
62+
constexpr auto* message_aggregated_error_top = "Multiple errors occurred:\n";
63+
constexpr auto* message_aggregated_bug_report = "If you run into this error repeatedly, your package cache may be corrupted.\n"
64+
"Please try running `mamba clean -a` to remove this cache before retrying the operation.\n"
65+
"\n"
66+
"If you still are having issues, please report the error on `mamba-org/mamba`'s issue tracker:\n"
67+
"https://github.com/mamba-org/mamba/issues/new?assignees=&labels=&projects=&template=bug.yml";
68+
69+
}
5970

60-
mamba_aggregated_error::mamba_aggregated_error(error_list_t&& error_list)
61-
: base_type(mamba_aggregated_error::m_base_message, mamba_error_code::aggregated)
71+
mamba_aggregated_error::mamba_aggregated_error(error_list_t&& error_list, bool with_bug_report_info)
72+
: base_type(message_aggregated_error_top, mamba_error_code::aggregated)
6273
, m_error_list(std::move(error_list))
6374
, m_aggregated_message()
75+
, m_with_bug_report_message(with_bug_report_info)
6476
{
6577
}
6678

6779
const char* mamba_aggregated_error::what() const noexcept
6880
{
6981
if (m_aggregated_message.empty())
7082
{
71-
m_aggregated_message = m_base_message;
83+
// If we have only one error, don't say it's multiple errors.
84+
if (m_error_list.size() > 1)
85+
{
86+
m_aggregated_message = message_aggregated_error_top;
87+
}
7288

7389
for (const mamba_error& er : m_error_list)
7490
{
7591
m_aggregated_message += er.what();
7692
m_aggregated_message += "\n";
7793
}
7894

79-
m_aggregated_message += "If you run into this error repeatedly, your package cache may be corrupted.\n"
80-
"Please try running `mamba clean -a` to remove this cache before retrying the operation.\n"
81-
"\n"
82-
"If you still are having issues, please report the error on `mamba-org/mamba`'s issue tracker:\n"
83-
"https://github.com/mamba-org/mamba/issues/new?assignees=&labels=&projects=&template=bug.yml";
95+
if (m_with_bug_report_message)
96+
{
97+
m_aggregated_message += message_aggregated_bug_report;
98+
}
8499
}
85100
return m_aggregated_message.c_str();
86101
}
87102

103+
bool mamba_aggregated_error::has_only_error(const mamba_error_code code) const
104+
{
105+
return std::ranges::all_of(
106+
m_error_list,
107+
[&](const auto& err) { return err.error_code() == code; }
108+
);
109+
}
110+
88111
tl::unexpected<mamba_error> make_unexpected(const char* msg, mamba_error_code ec)
89112
{
90113
return tl::make_unexpected(mamba_error(msg, ec));

libmamba/src/core/logging.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ namespace mamba::logging
245245
details::message_logger_use_buffer = false;
246246
}
247247

248+
bool MessageLogger::is_buffer_enabled()
249+
{
250+
return details::message_logger_use_buffer;
251+
}
252+
248253
void MessageLogger::print_buffer(std::ostream& /*out*/)
249254
{
250255
details::MessageLoggerBuffer::buffer tmp;

libmamba/src/core/subdir_index.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,8 @@ namespace mamba
557557
// missing subdirs (e.g. local path as a `noarch` but no `linux-64`).
558558
for (auto& result : results)
559559
{
560-
if (!result.has_value())
560+
// Don't log if it's a user interruption.
561+
if (!result.has_value() and not result.error().is_stop)
561562
{
562563
LOG_WARNING << "Failed to load subdir: " << result.error().message;
563564
}

libmamba/src/core/transaction.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,16 +389,36 @@ namespace mamba
389389
return true;
390390
}
391391

392+
// before user confirmation
393+
if (is_sig_interrupted())
394+
{
395+
Console::stream() << "Interrupted by user - stopped before transaction.";
396+
return true;
397+
}
398+
392399
auto lf = LockFile(ctx.prefix_params.target_prefix / "conda-meta");
393400
clean_trash_files(ctx.prefix_params.target_prefix, false);
394401

402+
// after user confirmation or ctrl-c
403+
if (is_sig_interrupted())
404+
{
405+
Console::stream() << "Interrupted by user - stopped before transaction.";
406+
return true;
407+
}
408+
395409
Console::stream() << "\nTransaction starting";
396410
fetch_extract_packages(ctx, channel_context);
397411

412+
if (is_sig_interrupted())
413+
{
414+
Console::stream() << "Interrupted by user - transaction stopped.";
415+
return true;
416+
}
417+
398418
if (ctx.download_only)
399419
{
400420
Console::stream()
401-
<< "Download only - packages are downloaded and extracted. Skipping the linking phase.";
421+
<< "Download only - packages download and extraction is done. Skipping the linking phase.";
402422
return true;
403423
}
404424

0 commit comments

Comments
 (0)