Skip to content

Commit 36b08c2

Browse files
Fix circular ref leak in postCoroTask and add CoroTask<T> tests
Fix a circular reference leak in CoroTaskRunner::expectEarlyExit(). When postCoroTask() fails to post, the coroutine frame holds a shared_ptr back to the CoroTaskRunner, creating an unreachable cycle. Breaking the cycle by destroying the task in expectEarlyExit() fixes the leak. Add 3 tests for CoroTask<T> (value-returning coroutines): - testValueReturn: inner coroutine returns int via co_return - testValueException: exception propagation from inner coroutine - testValueChaining: nested value-returning coroutine chain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 92289ac commit 36b08c2

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

include/xrpl/core/CoroTaskRunner.ipp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ JobQueue::CoroTaskRunner::expectEarlyExit()
133133
finished_ = true;
134134
#endif
135135
}
136+
// Destroy the coroutine frame to break a potential shared_ptr cycle.
137+
// The coroutine is at initial_suspend and never ran user code, so
138+
// destroying it is safe. Without this, the frame holds a shared_ptr
139+
// back to this CoroTaskRunner, creating an unreachable reference cycle.
140+
task_ = {};
136141
}
137142

138143
inline void

src/test/core/CoroTask_test.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,110 @@ class CoroTask_test : public beast::unit_test::suite
299299
BEAST_EXPECT(!runner->runnable());
300300
}
301301

302+
// Test: CoroTask<T> returns a value via co_return
303+
void
304+
testValueReturn()
305+
{
306+
using namespace std::chrono_literals;
307+
using namespace jtx;
308+
309+
testcase("value return");
310+
311+
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
312+
cfg->FORCE_MULTI_THREAD = true;
313+
return cfg;
314+
}));
315+
316+
gate g;
317+
int result = 0;
318+
auto runner = env.app().getJobQueue().postCoroTask(
319+
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
320+
auto inner = []() -> CoroTask<int> { co_return 42; };
321+
result = co_await inner();
322+
g.signal();
323+
co_return;
324+
});
325+
BEAST_EXPECT(runner);
326+
BEAST_EXPECT(g.wait_for(5s));
327+
runner->join();
328+
BEAST_EXPECT(result == 42);
329+
BEAST_EXPECT(!runner->runnable());
330+
}
331+
332+
// Test: CoroTask<T> propagates exceptions from inner coroutines
333+
void
334+
testValueException()
335+
{
336+
using namespace std::chrono_literals;
337+
using namespace jtx;
338+
339+
testcase("value exception");
340+
341+
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
342+
cfg->FORCE_MULTI_THREAD = true;
343+
return cfg;
344+
}));
345+
346+
gate g;
347+
bool caught = false;
348+
auto runner = env.app().getJobQueue().postCoroTask(
349+
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
350+
auto inner = []() -> CoroTask<int> {
351+
throw std::runtime_error("inner error");
352+
co_return 0;
353+
};
354+
try
355+
{
356+
co_await inner();
357+
}
358+
catch (std::runtime_error const& e)
359+
{
360+
caught = true;
361+
}
362+
g.signal();
363+
co_return;
364+
});
365+
BEAST_EXPECT(runner);
366+
BEAST_EXPECT(g.wait_for(5s));
367+
runner->join();
368+
BEAST_EXPECT(caught);
369+
BEAST_EXPECT(!runner->runnable());
370+
}
371+
372+
// Test: CoroTask<T> chaining — nested value-returning coroutines
373+
void
374+
testValueChaining()
375+
{
376+
using namespace std::chrono_literals;
377+
using namespace jtx;
378+
379+
testcase("value chaining");
380+
381+
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
382+
cfg->FORCE_MULTI_THREAD = true;
383+
return cfg;
384+
}));
385+
386+
gate g;
387+
int result = 0;
388+
auto runner = env.app().getJobQueue().postCoroTask(
389+
jtCLIENT, "CoroTaskTest", [&](auto) -> CoroTask<void> {
390+
auto add = [](int a, int b) -> CoroTask<int> { co_return a + b; };
391+
auto mul = [&](int a, int b) -> CoroTask<int> {
392+
int sum = co_await add(a, b);
393+
co_return sum * 2;
394+
};
395+
result = co_await mul(3, 4);
396+
g.signal();
397+
co_return;
398+
});
399+
BEAST_EXPECT(runner);
400+
BEAST_EXPECT(g.wait_for(5s));
401+
runner->join();
402+
BEAST_EXPECT(result == 14); // (3 + 4) * 2
403+
BEAST_EXPECT(!runner->runnable());
404+
}
405+
302406
// Test: postCoroTask returns nullptr when JobQueue is stopping
303407
void
304408
testShutdownRejection()
@@ -331,6 +435,9 @@ class CoroTask_test : public beast::unit_test::suite
331435
testThreadSpecificStorage();
332436
testExceptionPropagation();
333437
testMultipleYields();
438+
testValueReturn();
439+
testValueException();
440+
testValueChaining();
334441
testShutdownRejection();
335442
}
336443
};

0 commit comments

Comments
 (0)