@@ -109,18 +109,21 @@ TEST(SubscriptionLeaseManagementTest, NormalLifecycle) {
109109 // will verify that only the remaining messages have their lease extended.
110110 uut->AckMessage (" ack-0-1" );
111111 fake_cq->SimulateCompletion (true );
112+ // RunAsync, drain the deferred OnRefreshTimer
113+ fake_cq->SimulateCompletion (true );
112114 ASSERT_EQ (1U , fake_cq->size ());
113115
114116 // Ack one more message and trigger the new timer.
115117 uut->NackMessage (" ack-0-2" );
116118 fake_cq->SimulateCompletion (true );
119+ // RunAsync, drain the deferred OnRefreshTimer
120+ fake_cq->SimulateCompletion (true );
117121 ASSERT_EQ (1U , fake_cq->size ());
118122
119123 shutdown_manager->MarkAsShutdown (__func__, Status{});
120124 uut->Shutdown ();
121125
122- fake_cq->SimulateCompletion (false );
123- ASSERT_EQ (0U , fake_cq->size ());
126+ while (!fake_cq->empty ()) fake_cq->SimulateCompletion (true );
124127 EXPECT_THAT (done.get (), IsOk ());
125128}
126129
@@ -154,8 +157,7 @@ TEST(SubscriptionLeaseManagementTest, ShutdownOnError) {
154157 Status (StatusCode::kPermissionDenied , " uh-oh" ))});
155158 ASSERT_EQ (1U , fake_cq->size ());
156159
157- fake_cq->SimulateCompletion (false );
158- ASSERT_EQ (0U , fake_cq->size ());
160+ while (!fake_cq->empty ()) fake_cq->SimulateCompletion (true );
159161 EXPECT_THAT (done.get (), StatusIs (StatusCode::kPermissionDenied ));
160162}
161163
@@ -219,12 +221,13 @@ TEST(SubscriptionLeaseManagementTest, UsesDeadlineExtension) {
219221
220222 // Ignore message and then fire the timer. This will extend the deadline.
221223 fake_cq->SimulateCompletion (true );
224+ // RunAsync, drain the deferred OnRefreshTimer
225+ fake_cq->SimulateCompletion (true );
222226 ASSERT_EQ (1U , fake_cq->size ());
223227
224228 shutdown_manager->MarkAsShutdown (__func__, Status{});
225229 uut->Shutdown ();
226- fake_cq->SimulateCompletion (false );
227- ASSERT_EQ (0U , fake_cq->size ());
230+ while (!fake_cq->empty ()) fake_cq->SimulateCompletion (true );
228231 EXPECT_THAT (done.get (), IsOk ());
229232}
230233
@@ -270,13 +273,78 @@ TEST(SubscriptionLeaseManagementTest, ExpiredMessage) {
270273 // will verify that only the remaining messages have their lease extended.
271274 uut->AckMessage (" ack-0-1" );
272275 fake_cq->SimulateCompletion (true );
276+ // RunAsync, drain the deferred OnRefreshTimer
277+ fake_cq->SimulateCompletion (true );
273278 ASSERT_EQ (1U , fake_cq->size ());
274279
275280 shutdown_manager->MarkAsShutdown (__func__, Status{});
276281 uut->Shutdown ();
282+ while (!fake_cq->empty ()) fake_cq->SimulateCompletion (true );
283+ EXPECT_THAT (done.get (), IsOk ());
284+ }
285+
286+ // / @test Regression test for the self-deadlock in StartRefreshTimer: the lease
287+ // / refresh-timer continuation must be dispatched via `RunAsync` rather than run
288+ // / inline in the timer callback. Running it inline re-entered the already-held
289+ // / `mu_` and self-deadlocked the subscription. Here we verify that firing the
290+ // / timer only *schedules* the refresh (a RunAsync task); the lease extension
291+ // / runs on a subsequent completion, never synchronously in the timer callback.
292+ TEST (SubscriptionLeaseManagementTest,
293+ RefreshTimerContinuationDispatchedViaRunAsync) {
294+ auto mock = std::make_shared<pubsub_testing::MockSubscriptionBatchSource>();
295+ std::shared_ptr<BatchCallback> batch_callback;
296+ EXPECT_CALL (*mock, Start).WillOnce ([&](std::shared_ptr<BatchCallback> cb) {
297+ batch_callback = std::move (cb);
298+ });
277299
278- fake_cq->SimulateCompletion (false );
279- ASSERT_EQ (0U , fake_cq->size ());
300+ auto mock_batch_callback =
301+ std::make_shared<pubsub_testing::MockBatchCallback>();
302+ EXPECT_CALL (*mock_batch_callback, callback).Times (1 );
303+
304+ int extend_calls = 0 ;
305+ EXPECT_CALL (*mock, ExtendLeases)
306+ .WillRepeatedly (
307+ [&](std::vector<std::string> const &, std::chrono::seconds) {
308+ ++extend_calls;
309+ return make_ready_future (Status{});
310+ });
311+ EXPECT_CALL (*mock, BulkNack)
312+ .WillRepeatedly ([](std::vector<std::string> const &) {
313+ return make_ready_future (Status{});
314+ });
315+ EXPECT_CALL (*mock, Shutdown).Times (1 );
316+
317+ auto fake_cq = std::make_shared<FakeCompletionQueueImpl>();
318+ CompletionQueue cq (fake_cq);
319+ auto shutdown_manager = std::make_shared<SessionShutdownManager>();
320+ auto uut = SubscriptionLeaseManagement::Create (cq, shutdown_manager, mock,
321+ std::chrono::seconds (345 ),
322+ std::chrono::seconds (600 ));
323+
324+ auto done = shutdown_manager->Start ({});
325+ uut->Start (mock_batch_callback);
326+ batch_callback->callback (
327+ BatchCallback::StreamingPullResponse{GenerateMessages (" 0-" , 1 )});
328+ ASSERT_EQ (1U , fake_cq->size ()); // the refresh timer is pending
329+
330+ // Fire the timer. With the fix, the timer callback only *schedules*
331+ // OnRefreshTimer via RunAsync; it must NOT extend leases inline (doing so
332+ // re-enters the held mutex and deadlocks).
333+ fake_cq->SimulateCompletion (true );
334+ EXPECT_EQ (0 , extend_calls)
335+ << " lease refresh ran inline in the timer callback (deadlock path)" ;
336+ EXPECT_EQ (1U , fake_cq->size ()); // a RunAsync task is now pending
337+
338+ // Draining the RunAsync task runs OnRefreshTimer -> ExtendLeases.
339+ fake_cq->SimulateCompletion (true );
340+ EXPECT_EQ (1 , extend_calls);
341+
342+ // The fix defers OnRefreshTimer to RunAsync, which the fake CQ only executes
343+ // on SimulateCompletion(true) (false drops the task). Drain with true so the
344+ // deferred refresh operation finishes and the session shutdown completes.
345+ shutdown_manager->MarkAsShutdown (__func__, Status{});
346+ uut->Shutdown ();
347+ while (!fake_cq->empty ()) fake_cq->SimulateCompletion (true );
280348 EXPECT_THAT (done.get (), IsOk ());
281349}
282350
0 commit comments