Skip to content

Commit 1cd9640

Browse files
committed
Add stale delayed callback repro
1 parent d2504c0 commit 1cd9640

4 files changed

Lines changed: 103 additions & 0 deletions

File tree

Source/Task/TaskQueue.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2363,4 +2363,20 @@ STDAPI XTaskQueueSetTestHooks(
23632363
aq->SetTestHooks(hooks);
23642364
return S_OK;
23652365
}
2366+
2367+
STDAPI XTaskQueueSubmitPendingCallbackForTests(
2368+
_In_ XTaskQueueHandle queue,
2369+
_In_ XTaskQueuePort port
2370+
) noexcept
2371+
{
2372+
referenced_ptr<ITaskQueue> aq(GetQueue(queue));
2373+
RETURN_HR_IF(E_GAMERUNTIME_INVALID_HANDLE, aq == nullptr);
2374+
2375+
referenced_ptr<ITaskQueuePortContext> portContext;
2376+
RETURN_IF_FAILED(aq->GetPortContext(port, portContext.address_of()));
2377+
2378+
auto* portImpl = static_cast<TaskQueuePortImpl*>(portContext->GetPort());
2379+
portImpl->SubmitPendingCallbackForTests();
2380+
return S_OK;
2381+
}
23662382
#endif

Source/Task/TaskQueueImpl.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ class TaskQueuePortImpl: public Api<ApiId::TaskQueuePort, ITaskQueuePort>
215215
void __stdcall SuspendPort();
216216
void __stdcall ResumePort();
217217

218+
#ifdef HC_UNITTEST_API
219+
void __stdcall SubmitPendingCallbackForTests()
220+
{
221+
SubmitPendingCallback();
222+
}
223+
#endif
224+
218225
private:
219226

220227
struct WaitRegistration;

Source/Task/XTaskQueuePriv.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ STDAPI XTaskQueueSetTestHooks(
7474
_In_ XTaskQueueHandle queue,
7575
_In_ XTaskQueueTestHooks* hooks
7676
) noexcept;
77+
78+
/// <summary>
79+
/// Directly invokes the delayed-callback notification path for unit tests.
80+
/// This is used to model stale threadpool timer callbacks that were already
81+
/// queued before the timer was retargeted.
82+
/// </summary>
83+
STDAPI XTaskQueueSubmitPendingCallbackForTests(
84+
_In_ XTaskQueueHandle queue,
85+
_In_ XTaskQueuePort port
86+
) noexcept;
7787
#endif
7888

7989
//----------------------------------------------------------------//

Tests/UnitTests/Tests/TaskQueueTests.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,4 +2191,74 @@ DEFINE_TEST_CLASS(TaskQueueTests)
21912191
{
21922192
}
21932193
}
2194+
2195+
DEFINE_TEST_CASE(VerifyStaleDelayedCallbackDoesNotEarlyPromoteNextPendingEntry)
2196+
{
2197+
struct CallbackState
2198+
{
2199+
std::atomic<bool> invoked{ false };
2200+
std::atomic<bool> canceled{ false };
2201+
std::atomic<uint64_t> firedAtTicks{ 0 };
2202+
2203+
static void CALLBACK Invoke(void* ctx, bool canceled)
2204+
{
2205+
auto* self = static_cast<CallbackState*>(ctx);
2206+
self->canceled.store(canceled, std::memory_order_release);
2207+
self->firedAtTicks.store(GetTickCount64(), std::memory_order_release);
2208+
self->invoked.store(true, std::memory_order_release);
2209+
}
2210+
};
2211+
2212+
AutoQueueHandle queue;
2213+
VERIFY_SUCCEEDED(XTaskQueueCreate(
2214+
XTaskQueueDispatchMode::Manual,
2215+
XTaskQueueDispatchMode::Manual,
2216+
&queue));
2217+
2218+
CallbackState firstState;
2219+
CallbackState secondState;
2220+
2221+
constexpr uint32_t firstDelayMs = 10;
2222+
constexpr uint32_t secondDelayMs = 1000;
2223+
const uint64_t submittedAt = GetTickCount64();
2224+
2225+
VERIFY_SUCCEEDED(XTaskQueueSubmitDelayedCallback(
2226+
queue,
2227+
XTaskQueuePort::Work,
2228+
firstDelayMs,
2229+
&firstState,
2230+
&CallbackState::Invoke));
2231+
2232+
VERIFY_SUCCEEDED(XTaskQueueSubmitDelayedCallback(
2233+
queue,
2234+
XTaskQueuePort::Work,
2235+
secondDelayMs,
2236+
&secondState,
2237+
&CallbackState::Invoke));
2238+
2239+
// Dispatch the first callback. The timer callback that promoted it has
2240+
// already re-armed the shared delayed-callback timer for secondState.
2241+
VERIFY_IS_TRUE(XTaskQueueDispatch(queue, XTaskQueuePort::Work, 5000));
2242+
2243+
VERIFY_IS_TRUE(firstState.invoked.load(std::memory_order_acquire));
2244+
VERIFY_IS_FALSE(firstState.canceled.load(std::memory_order_acquire));
2245+
VERIFY_IS_FALSE(secondState.invoked.load(std::memory_order_acquire));
2246+
VERIFY_IS_LESS_THAN(GetTickCount64() - submittedAt, static_cast<uint64_t>(500));
2247+
2248+
// Simulate a stale delayed-callback notification that was already
2249+
// queued before the timer was re-armed for secondState. This must not
2250+
// promote the later pending entry before its own deadline.
2251+
VERIFY_SUCCEEDED(XTaskQueueSubmitPendingCallbackForTests(queue, XTaskQueuePort::Work));
2252+
2253+
VERIFY_IS_FALSE(XTaskQueueDispatch(queue, XTaskQueuePort::Work, 0));
2254+
VERIFY_IS_FALSE(XTaskQueueDispatch(queue, XTaskQueuePort::Work, 200));
2255+
VERIFY_IS_FALSE(secondState.invoked.load(std::memory_order_acquire));
2256+
2257+
VERIFY_IS_TRUE(XTaskQueueDispatch(queue, XTaskQueuePort::Work, 2000));
2258+
VERIFY_IS_TRUE(secondState.invoked.load(std::memory_order_acquire));
2259+
VERIFY_IS_FALSE(secondState.canceled.load(std::memory_order_acquire));
2260+
VERIFY_IS_GREATER_THAN_OR_EQUAL(
2261+
secondState.firedAtTicks.load(std::memory_order_acquire) - submittedAt,
2262+
static_cast<uint64_t>(600));
2263+
}
21942264
};

0 commit comments

Comments
 (0)