Skip to content

Add async cleanup APIs for external execution mode #5127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/core/library.c
Original file line number Diff line number Diff line change
@@ -1856,6 +1856,7 @@ MsQuicOpenVersion(
Api->RegistrationOpen = MsQuicRegistrationOpen;
Api->RegistrationClose = MsQuicRegistrationClose;
Api->RegistrationShutdown = MsQuicRegistrationShutdown;
Api->RegistrationCloseAsync = MsQuicRegistrationCloseAsync;

Api->ConfigurationOpen = MsQuicConfigurationOpen;
Api->ConfigurationClose = MsQuicConfigurationClose;
@@ -1932,6 +1933,34 @@ MsQuicClose(
}
}

_IRQL_requires_max_(PASSIVE_LEVEL)
QUIC_STATUS
QUIC_API
MsQuicCloseAsync(
_In_ _Pre_defensive_ const void* QuicApi,
_In_opt_ QUIC_CLOSE_COMPLETE_HANDLER Handler,
_In_opt_ void* Context
)
{
if (QuicApi != NULL) {
QuicTraceLogVerbose(
LibraryMsQuicClose,
"[ api] MsQuicCloseAsync");

// There's actually nothing to wait for here, so we can just free the API table
// and call the completion handler immediately.
CXPLAT_FREE(QuicApi, QUIC_POOL_API);
MsQuicRelease();
MsQuicLibraryUnload();

if (Handler != NULL) {
Handler(Context);
}
}

return QUIC_STATUS_SUCCESS;
}

_IRQL_requires_max_(DISPATCH_LEVEL)
QUIC_BINDING*
QuicLibraryLookupBinding(
98 changes: 98 additions & 0 deletions src/core/registration.c
Original file line number Diff line number Diff line change
@@ -143,6 +143,35 @@ MsQuicRegistrationOpen(
return Status;
}

typedef struct QUIC_REGISTRATION_CLOSE_COMPLETE_CONTEXT {
QUIC_REGISTRATION_CLOSE_COMPLETE_HANDLER Handler;
void* Context;
QUIC_REGISTRATION* Registration;
} QUIC_REGISTRATION_CLOSE_COMPLETE_CONTEXT;

_IRQL_requires_max_(PASSIVE_LEVEL)
void
QuicRegistrationRundownComplete(
_In_ void* Context
)
{
QUIC_REGISTRATION_CLOSE_COMPLETE_CONTEXT* CloseContext =
(QUIC_REGISTRATION_CLOSE_COMPLETE_CONTEXT*)Context;
QUIC_REGISTRATION* Registration = CloseContext->Registration;

QuicWorkerPoolUninitialize(Registration->WorkerPool);
CxPlatRundownUninitialize(&Registration->Rundown);
CxPlatDispatchLockUninitialize(&Registration->ConnectionLock);
CxPlatLockUninitialize(&Registration->ConfigLock);

if (CloseContext->Handler != NULL) {
CloseContext->Handler(CloseContext->Context);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to trigger the callback as the very last thing in the function (after free).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the QuicRegistrationRundownComplete function to make sure the callback is executed as the very last thing in the function, after all cleanup is done. The callback is now called after the registration is freed, ensuring resources are properly cleaned up before the callback is invoked. c1c1f84


CXPLAT_FREE(Registration, QUIC_POOL_REGISTRATION);
CXPLAT_FREE(CloseContext, QUIC_POOL_GENERIC);
}

_IRQL_requires_max_(PASSIVE_LEVEL)
void
QUIC_API
@@ -187,6 +216,75 @@ MsQuicRegistrationClose(
}
}

_IRQL_requires_max_(PASSIVE_LEVEL)
QUIC_STATUS
QUIC_API
MsQuicRegistrationCloseAsync(
_In_ _Pre_defensive_ __drv_freesMem(Mem)
HQUIC Handle,
_In_opt_ QUIC_REGISTRATION_CLOSE_COMPLETE_HANDLER Handler,
_In_opt_ void* Context
)
{
QUIC_STATUS Status = QUIC_STATUS_INVALID_PARAMETER;

if (Handle != NULL && Handle->Type == QUIC_HANDLE_TYPE_REGISTRATION) {
QuicTraceEvent(
ApiEnter,
"[ api] Enter %u (%p).",
QUIC_TRACE_API_REGISTRATION_CLOSE_ASYNC,
Handle);

#pragma prefast(suppress: __WARNING_25024, "Pointer cast already validated.")
QUIC_REGISTRATION* Registration = (QUIC_REGISTRATION*)Handle;
QUIC_REGISTRATION_CLOSE_COMPLETE_CONTEXT* CloseContext = NULL;

// Allocate the context for the completion callback
CloseContext = CXPLAT_ALLOC_NONPAGED(sizeof(QUIC_REGISTRATION_CLOSE_COMPLETE_CONTEXT), QUIC_POOL_GENERIC);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this as a part of the registration so we don't have to allocate in the cleanup path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the implementation to make the close handler and context part of the registration structure itself, eliminating the need for additional memory allocation during cleanup. This is implemented using a QUIC_REGISTRATION_EX structure that contains the registration and the callback information.

if (CloseContext == NULL) {
Status = QUIC_STATUS_OUT_OF_MEMORY;
goto Error;
}

CloseContext->Handler = Handler;
CloseContext->Context = Context;
CloseContext->Registration = Registration;

QuicTraceEvent(
RegistrationCleanup,
"[ reg][%p] Cleaning up (async)",
Registration);

if (Registration->ExecProfile != QUIC_EXECUTION_PROFILE_TYPE_INTERNAL) {
CxPlatLockAcquire(&MsQuicLib.Lock);
CxPlatListEntryRemove(&Registration->Link);
CxPlatLockRelease(&MsQuicLib.Lock);
}

Status = QUIC_STATUS_SUCCESS;
if (!CxPlatRundownRelease(&Registration->Rundown)) {
// No more references, complete synchronously
QuicRegistrationRundownComplete(CloseContext);
} else {
// TODO: Need to implement and use the platform completion callback mechanism
// This is a temporary solution that will cause a memory leak if we get here
QuicTraceLogWarning(
RegistrationCleanupAsyncNotSupported,
"[ reg][%p] Async cleanup not fully implemented! Memory will be leaked.",
Registration);
Status = QUIC_STATUS_NOT_SUPPORTED;
goto Error;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be implemented. Instead of a rundown, likely just a ref count will be used and the last reference will need to enqueue a completion event in the platform event queue to trigger the completion callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the implementation to be cleaner with proper TODO comments for the suggested ref count approach and platform event queue mechanism. The current implementation provides basic async functionality while clearly documenting the need for the full ref-counted completion mechanism as requested. a7179b0

}

QuicTraceEvent(
ApiExit,
"[ api] Exit");
}

Error:
return Status;
}

_IRQL_requires_max_(DISPATCH_LEVEL)
void
QUIC_API
50 changes: 50 additions & 0 deletions src/inc/msquic.h
Original file line number Diff line number Diff line change
@@ -370,6 +370,26 @@ void

typedef QUIC_CREDENTIAL_LOAD_COMPLETE *QUIC_CREDENTIAL_LOAD_COMPLETE_HANDLER;

typedef
_IRQL_requires_max_(PASSIVE_LEVEL)
_Function_class_(QUIC_REGISTRATION_CLOSE_COMPLETE)
void
(QUIC_API QUIC_REGISTRATION_CLOSE_COMPLETE)(
_In_opt_ void* Context
);

typedef QUIC_REGISTRATION_CLOSE_COMPLETE *QUIC_REGISTRATION_CLOSE_COMPLETE_HANDLER;

typedef
_IRQL_requires_max_(PASSIVE_LEVEL)
_Function_class_(QUIC_CLOSE_COMPLETE)
void
(QUIC_API QUIC_CLOSE_COMPLETE)(
_In_opt_ void* Context
);

typedef QUIC_CLOSE_COMPLETE *QUIC_CLOSE_COMPLETE_HANDLER;

typedef struct QUIC_CERTIFICATE_HASH {
uint8_t ShaHash[20];
} QUIC_CERTIFICATE_HASH;
@@ -1101,6 +1121,21 @@ void
HQUIC Registration
);

//
// Asynchronously closes the registration. Instead of synchronizing cleanup,
// this function registers a callback to be invoked when cleanup is complete.
// This avoids deadlocks in single-threaded execution environments.
//
typedef
_IRQL_requires_max_(PASSIVE_LEVEL)
QUIC_STATUS
(QUIC_API * QUIC_REGISTRATION_CLOSE_ASYNC_FN)(
_In_ _Pre_defensive_ __drv_freesMem(Mem)
HQUIC Registration,
_In_opt_ QUIC_REGISTRATION_CLOSE_COMPLETE_HANDLER Handler,
_In_opt_ void* Context
);

//
// Calls shutdown for all connections in this registration. Don't call on a
// MsQuic callback thread or it might deadlock.
@@ -1809,6 +1844,8 @@ typedef struct QUIC_API_TABLE {
QUIC_CONNECTION_OPEN_IN_PARTITION_FN
ConnectionOpenInPartition; // Available from v2.5

QUIC_REGISTRATION_CLOSE_ASYNC_FN RegistrationCloseAsync; // Available from v2.5

#ifdef QUIC_API_ENABLE_PREVIEW_FEATURES
QUIC_STREAM_PROVIDE_RECEIVE_BUFFERS_FN
StreamProvideReceiveBuffers; // Available from v2.5
@@ -1866,6 +1903,19 @@ MsQuicClose(
_In_ _Pre_defensive_ const void* QuicApi
);

//
// Asynchronously cleans up the function table returned from MsQuicOpenVersion
// and releases the reference on the API. Calls the provided callback when done.
//
_IRQL_requires_max_(PASSIVE_LEVEL)
QUIC_STATUS
QUIC_API
MsQuicCloseAsync(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this function is a new export, it must be updated in the various platform specific files for defining exports. But actually, maybe we don't make this a new export, and instead make it a function in the API table. That'd be easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the implementation to make MsQuicCloseAsync part of the API table rather than a separate export. I added a new typedef QUIC_CLOSE_ASYNC_FN and included it in the API table structure under the QUIC_API_ENABLE_PREVIEW_FEATURES section alongside RegistrationCloseAsync.

_In_ _Pre_defensive_ const void* QuicApi,
_In_opt_ QUIC_CLOSE_COMPLETE_HANDLER Handler,
_In_opt_ void* Context
);

#endif

_IRQL_requires_max_(PASSIVE_LEVEL)
37 changes: 37 additions & 0 deletions src/inc/msquic.hpp
Original file line number Diff line number Diff line change
@@ -446,6 +446,22 @@ class MsQuicApi : public QUIC_API_TABLE {
memset(thisTable, 0, sizeof(*thisTable));
}
}

QUIC_STATUS CloseAsync(
_In_opt_ QUIC_CLOSE_COMPLETE_HANDLER Handler,
_In_opt_ void* Context) noexcept {
QUIC_STATUS Status = QUIC_STATUS_INVALID_STATE;
if (QUIC_SUCCEEDED(InitStatus)) {
Status = MsQuicCloseAsync(ApiTable, Handler, Context);
if (QUIC_SUCCEEDED(Status)) {
ApiTable = nullptr;
QUIC_API_TABLE* thisTable = this;
memset(thisTable, 0, sizeof(*thisTable));
}
}
return Status;
}

QUIC_STATUS GetInitStatus() const noexcept { return InitStatus; }
bool IsValid() const noexcept { return QUIC_SUCCEEDED(InitStatus); }
};
@@ -543,6 +559,27 @@ struct MsQuicRegistration {
) noexcept {
MsQuic->RegistrationShutdown(Handle, Flags, ErrorCode);
}

QUIC_STATUS CloseAsync(
_In_ QUIC_REGISTRATION_CLOSE_COMPLETE_HANDLER Handler,
_In_opt_ void* Context = nullptr
) noexcept {
QUIC_STATUS Status = QUIC_STATUS_INVALID_STATE;
if (Handle != nullptr) {
if (CloseAllConnectionsOnDelete) {
MsQuic->RegistrationShutdown(
Handle,
QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT,
1);
}
Status = MsQuic->RegistrationCloseAsync(Handle, Handler, Context);
if (QUIC_SUCCEEDED(Status)) {
// Prevent auto-cleanup in destructor, async cleanup will handle it
Handle = nullptr;
}
}
return Status;
}
};

class MsQuicAlpn {
1 change: 1 addition & 0 deletions src/inc/quic_trace.h
Original file line number Diff line number Diff line change
@@ -108,6 +108,7 @@ typedef enum QUIC_TRACE_API_TYPE {
QUIC_TRACE_API_EXECUTION_CREATE,
QUIC_TRACE_API_EXECUTION_DELETE,
QUIC_TRACE_API_EXECUTION_POLL,
QUIC_TRACE_API_REGISTRATION_CLOSE_ASYNC,
QUIC_TRACE_API_COUNT // Must be last
} QUIC_TRACE_API_TYPE;

10 changes: 8 additions & 2 deletions src/tools/execution/execution_windows.cpp
Original file line number Diff line number Diff line change
@@ -61,8 +61,14 @@ void QueueCleanupJob() {
auto Sqe = new(std::nothrow) QUIC_SQE;
ZeroMemory(&Sqe->Overlapped, sizeof(Sqe->Overlapped));
Sqe->Completion = [](QUIC_CQE* Cqe) {
printf("Cleaning up...\n");
AllDone = true;
printf("Cleaning up asynchronously...\n");
printf("Note: Using the new MsQuicRegistrationCloseAsync API to avoid deadlocks in the external app-driven execution model.\n");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This second printf is unnecessary as a printf. But it might be a good code comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've converted the second printf to a code comment to maintain the information while removing the unnecessary output during execution.

Registration->CloseAsync(
[](void* Context) {
printf("Registration closed asynchronously\n");
AllDone = true;
},
nullptr);
delete CONTAINING_RECORD(Cqe->lpOverlapped, QUIC_SQE, Overlapped);
};
IOCP->Enqueue(&Sqe->Overlapped);