Skip to content

Commit bbcc4f5

Browse files
committed
cluster_link: add stop_completes_with_task_wedged_in_source_cluster
Demonstrates deadlocks present in `link::stop()`. A wedged `link` waiting for an inflight schema registry request to finish which calls `stop()` is currently only awakened by the `cluster`'s `abort_source`, which in turn is only aborted when `cluster::stop()` is called, but only _after_ the `link` manages to close its gate. In summary, the link's gate waits on the hung request, the request waits on the abort source, and the abort source waits on the gate being closed, leading to a deadlock.
1 parent 6e297d2 commit bbcc4f5

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

src/v/cluster_link/tests/link_test.cc

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,4 +494,105 @@ TEST_F_CORO(evil_link_test, test_evil_link_start_stop) {
494494
<< "Link should be removed after reconciler loop";
495495
}
496496

497+
namespace {
498+
499+
/// A task that refreshes metadata from the source cluster, mirroring the
500+
/// first step of source_topic_syncer::run_impl. request_metadata_update()
501+
/// takes no abort source: its waits (metadata update lock, seed reconnect
502+
/// backoff) answer only to the kafka::client::cluster's internal abort
503+
/// source.
504+
class metadata_refresh_task : public task {
505+
public:
506+
static constexpr auto task_name = "metadata_refresh_task";
507+
explicit metadata_refresh_task(link* link)
508+
: task(link, 100ms, task_name) {}
509+
510+
bool should_start_impl(ss::shard_id, ::model::node_id) const override {
511+
return true;
512+
}
513+
bool should_stop_impl(ss::shard_id, ::model::node_id) const override {
514+
return false;
515+
}
516+
void update_config(const model::metadata&) override {}
517+
model::enabled_t is_enabled() const final { return model::enabled_t::yes; }
518+
519+
ss::future<state_transition> run_impl(ss::abort_source&) override {
520+
try {
521+
co_await get_link()
522+
->get_cluster_connection()
523+
.request_metadata_update();
524+
} catch (const std::exception& e) {
525+
co_return state_transition{
526+
.desired_state = model::task_state::link_unavailable,
527+
.reason = ssx::sformat(
528+
"Failed to update metadata: {}", e.what())};
529+
}
530+
co_return state_transition{
531+
.desired_state = model::task_state::active, .reason = "ok"};
532+
}
533+
};
534+
535+
class metadata_refresh_task_factory : public task_factory {
536+
public:
537+
std::string_view created_task_name() const noexcept override {
538+
return metadata_refresh_task::task_name;
539+
}
540+
std::unique_ptr<task> create_task(link* link) override {
541+
return std::make_unique<metadata_refresh_task>(link);
542+
}
543+
};
544+
545+
} // namespace
546+
547+
// Regression test for deadlock between `link::stop()` and `cluster::stop()`.
548+
TEST_F_CORO(
549+
link_test_manager_started,
550+
stop_completes_with_task_wedged_in_source_cluster) {
551+
auto link_uuid = model::uuid_t(::uuid_t::create());
552+
auto md = ss::make_lw_shared<const model::metadata>(model::metadata{
553+
.name = model::name_t("wedged_link"),
554+
.uuid = link_uuid,
555+
.connection = model::connection_config{}});
556+
557+
// A source cluster connection with no seed brokers: start() succeeds
558+
// vacuously (nothing to connect to).
559+
auto wedged_link = std::make_unique<link>(
560+
::model::node_id(0),
561+
model::id_t(1),
562+
_manager.get(),
563+
1s,
564+
md,
565+
std::make_unique<kafka::client::cluster>(
566+
kafka::client::connection_configuration{
567+
.client_id = "wedge-test",
568+
}),
569+
std::make_unique<default_config_provider>(),
570+
std::make_unique<data_src_factory>(),
571+
std::make_unique<data_sink_factory>());
572+
co_await wedged_link->start();
573+
574+
// Now point it at a seed broker that refuses connections, with a large
575+
// connection_timeout so the reconnect loop parks in long backoff
576+
// sleeps — the production wedge for a link whose source cluster is
577+
// unreachable.
578+
wedged_link->get_cluster_connection().update_configuration(
579+
kafka::client::connection_configuration{
580+
.initial_brokers = {net::unresolved_address{"127.0.0.1", 1}},
581+
.client_id = "wedge-test",
582+
.connection_timeout = 10min,
583+
});
584+
585+
metadata_refresh_task_factory tf;
586+
auto res = co_await wedged_link->register_task(&tf);
587+
ASSERT_TRUE_CORO(res.has_value());
588+
589+
// Let the task run into the wedge: connect to the dead seed, fail,
590+
// and park in the reconnect backoff.
591+
co_await ss::sleep(1s);
592+
593+
// Stopping the link must complete even with the task fiber wedged in
594+
// the source cluster's reconnect backoff.
595+
co_await wedged_link->stop();
596+
}
597+
497598
} // namespace cluster_link::tests

0 commit comments

Comments
 (0)