Files
Andrés Felipe Méndez 4eb339156b fix crash add test, add worker thread (#192)
* fix crash add test, add worker thread

* Improve HTTP/WS cleanup and test timing

Resolves cleanup/race issues and speeds up integration tests:

- impl/httpCurl/NHttpClientLibCurl.cpp: Remove curl handle earlier and deduplicate calls to curl_multi_remove_handle(), logging errors immediately if removal fails to avoid double-remove races.
- impl/wsWslay/NWebsocketWslay.cpp: Avoid heap allocation for received message by capturing a moved stack NBytes in the callback; ensure destructor always stops IO, joins the thread and cleans up the connection to prevent lingering IO.
- integrationtests/src/NTest.cpp: Run deleteAccountAsync calls concurrently and pump only the HTTP client while waiting to avoid RT client races; consume results best-effort. Also reduce tick/check timeouts from 50ms to 5ms to speed test loops.

* pr comments

* Add exception destructors and Android test updates

Provide out-of-line destructor definitions for NException and NRtException and declare their destructors in headers to satisfy the ABI and avoid inline dtor issues.

Revamp Android integration test setup: add gradle.properties and .gitignore entries, switch build.gradle to use prebuilt native libraries staged under jniLibs (avoid Gradle/CMake integration conflicts), add usesCleartextTraffic to AndroidManifest, and update integration main to run tests with client/rt client factories and server params. The deploy.sh script was rewritten to support building native libs via CMake presets, staging .so and libc++_shared, assembling the APK, installing to a device/emulator, setting up adb reverse, streaming logcat and parsing test results. Also increase tournament test durations to 60s to be more tolerant of slow environments (e.g. emulators).

* Refactor test cleanup, logging and Taskfile

Multiple changes to improve test reliability and logging behavior:

- integrationtests/src/NTest.cpp: Use a detached cleanup thread to pump the HTTP client with a 10s timeout, wait/join or detach based on completion, and avoid hangs when tick() blocks. Added <future> and best-effort future get handling; emit warning if cleanup times out.
- impl/httpLibHttpClient/NHttpClientLibHC.cpp and impl/wsLibHttpClient/NWebsocketLibHC.cpp: Map NLogLevel::Debug to HCTraceLevel::Warning (was Verbose) to adjust trace level mapping.
- Taskfile.yml: Only apply Xcode code-signing overrides on macOS, add docker compose --wait to service startup in multiple tasks, make the test task start the server automatically and copy built .dlls into the integrationtests folder (best-effort) so tests can find native binaries.
- interface headers: add <stdexcept> includes in NException.h and realtime/rtdata/NRtException.h to ensure exception types are available.

These changes aim to prevent test hangs during shutdown, ensure dependent services are ready before tests run, and fix missing includes and logging-level mapping.

* Add cancellation and retry integration tests

Introduce integrationtests/src/test_cancellation.cpp with a suite of tests covering request cancellation, disconnect recovery, and retry/idempotency scenarios (cancel_noPendingRequests, cancel_inflightRequests, cancel_recoveryAfterDisconnect, retry_afterConnectionError, retry_afterUnauthenticated, retry_afterSessionExpiry, retry_storageIdempotent, retry_authAndGetAccount). Also update integrationtests/src/NTestLib.cpp to declare and run test_cancellation so the new tests are included in the test run. These tests improve coverage around client disconnect behavior, CancelledByUser handling, and recovery/retry patterns.

* Add configure status; recreate XTaskQueue

NHttpClientLibHC.cpp: ensure m_queue_terminated is reset before terminating, then recreate the XTaskQueue after XTaskQueueTerminate so the client can be reused after cancellation; log an error if queue creation fails.

* Make Android deploy script cross-platform

Taskfile: loosened tests-android invocation (DEVICE is optional, ABI default kept) and delegate build/deploy to integrationtests/android/deploy.sh with an --abi flag.

deploy.sh: added OS detection and portability fixes (Windows/macOS/Linux), auto-detect ANDROID_HOME (including LOCALAPPDATA on Windows) and normalize backslashes, prefer adb from PATH with fallback to ANDROID_HOME, auto-detect/suggest JDK 17 and set JAVA_HOME if found, and normalize ANDROID_NDK_HOME. Build now runs from repo root (pushd/popd) and uses HOST_OS for NDK STL path. All adb calls now go through a resolved ADB variable. These changes improve robustness and cross-platform support for Android integration tests.

* linux build

* inline deploy.sh

* rename

* run android tests windows

* dis own proces on powershell

* ensure exported interfaces have out-of-line key functions avoiding linker RTTI issues

* revert to private wslay

* change to 10 ms
2026-03-05 13:51:41 -05:00

318 lines
12 KiB
C++

/*
* Copyright 2022 The Nakama Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "NHttpClientLibHC.h"
#include <httpClient/httpClient.h>
#include <nakama-cpp/log/NLogger.h>
namespace Nakama {
HC_DEFINE_TRACE_AREA(httpTransportLibHC, HCTraceLevel::Error);
// Converts our loglevel to HCTracelevel
static HCTraceLevel HCLevelForNLevel(NLogLevel lvl) {
switch (lvl) {
case NLogLevel::Debug:
return HCTraceLevel::Warning;
case NLogLevel::Info:
return HCTraceLevel::Information;
case NLogLevel::Warn:
return HCTraceLevel::Warning;
case NLogLevel::Error:
return HCTraceLevel::Error;
case NLogLevel::Fatal:
return HCTraceLevel::Error;
}
return HCTraceLevel::Off;
}
static void configureNLogger() {
static bool configured = false;
auto l = NLogger::getSink();
if (!l) {
return;
}
if (configured)
return;
configured = true;
HCSettingsSetTraceLevel(HCLevelForNLevel(l->getLevel()));
HCTraceSetClientCallback(
[](const char* areaName, HCTraceLevel level, uint64_t /*threadId*/, uint64_t /*timestamp*/, const char* message) {
switch (level) {
case HCTraceLevel::Verbose:
return NLogger::Debug(message, areaName);
case HCTraceLevel::Information:
return NLogger::Info(message, areaName);
case HCTraceLevel::Important:
case HCTraceLevel::Warning:
return NLogger::Warn(message, areaName);
case HCTraceLevel::Error:
return NLogger::Error(message, areaName);
case HCTraceLevel::Off:
return;
}
});
}
NHttpClientLibHC::NHttpClientLibHC(const NPlatformParameters& platformParams)
: m_queue(nullptr, &XTaskQueueCloseHandle), m_baseUri(), m_queue_terminated(false) {
configureNLogger();
#ifdef __ANDROID__
#error libhttpclient is not currently supported on Android in libnakama!
#else
(void)platformParams;
HCInitArgs* initArgsParam = nullptr;
#endif
HRESULT hr = HCInitialize(initArgsParam);
if (FAILED(hr)) {
HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "HCInitialize failed");
}
XTaskQueueHandle q;
XTaskQueueCreate(
XTaskQueueDispatchMode::ThreadPool,
XTaskQueueDispatchMode::Manual, // callbacks from ticks
&q);
m_queue.reset(q);
}
NHttpClientLibHC::~NHttpClientLibHC() noexcept { HCCleanup(); }
void NHttpClientLibHC::setBaseUri(const std::string& uri) { m_baseUri = uri; }
void NHttpClientLibHC::setTimeout(std::chrono::milliseconds timeout) {
m_timeout = timeout;
if (m_timeout >= std::chrono::milliseconds(0)) {
HRESULT hr =
HCHttpCallRequestSetTimeout(nullptr, std::chrono::duration_cast<std::chrono::seconds>(m_timeout).count());
if (FAILED(hr)) {
HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "HCHttpCallRequestSetTimeout default failed");
}
}
}
#define CHECK_CB(exp, msg, cb) \
{ \
HRESULT hr = exp; \
if (FAILED(hr)) { \
HC_TRACE_ERROR_HR(httpTransportLibHC, hr, msg); \
submit_cb(cb, InternalStatusCodes::INTERNAL_TRANSPORT_ERROR, "", msg); \
return; \
} \
}
#define CHECK_AND_LOG(hr, msg) \
{ \
if (FAILED(hr)) { \
HC_TRACE_ERROR_HR(httpTransportLibHC, hr, msg); \
return hr; \
} \
}
using call_ptr = std::unique_ptr<HC_CALL, decltype(&HCHttpCallCloseHandle)>;
HRESULT NHttpClientLibHC::prep_hc_call(const NHttpRequest& req, call_ptr& call) {
HCCallHandle pcall = nullptr;
CHECK_AND_LOG(HCHttpCallCreate(&pcall), "HCHttpCallCreate failed");
call.reset(pcall);
const char* callMethod = nullptr;
switch (req.method) {
case NHttpReqMethod::POST:
callMethod = "POST";
break;
case NHttpReqMethod::GET:
callMethod = "GET";
break;
case NHttpReqMethod::PUT:
callMethod = "PUT";
break;
case NHttpReqMethod::DEL:
callMethod = "DELETE";
break;
}
std::string uri = m_baseUri + req.path + (req.queryArgs.empty() ? "" : "?");
for (auto p : req.queryArgs) {
uri += p.first + "=" + p.second + "&";
};
CHECK_AND_LOG(HCHttpCallRequestSetUrl(pcall, callMethod, uri.c_str()), "HCHttpCallRequestSetUrl failed");
if (req.body.size()) {
CHECK_AND_LOG(
HCHttpCallRequestSetRequestBodyBytes(
pcall, reinterpret_cast<const uint8_t*>(req.body.data()), uint32_t(req.body.size())),
"HCHttpCallRequestSetRequestBodyBytes");
}
for (auto p : req.headers) {
HCHttpCallRequestSetHeader(pcall, p.first.c_str(), p.second.c_str(), true);
}
CHECK_AND_LOG(HCHttpCallRequestSetRetryAllowed(pcall, false), "HCHttpCallRequestSetRetryAllowed failed");
return S_OK;
}
using ctx_t = std::tuple<call_ptr, NHttpResponseCallback, std::string>;
static HRESULT setup_hc_response_to_string(ctx_t& ctx) {
HC_CALL* call = std::get<0>(ctx).get();
HCHttpCallResponseBodyWriteFunction write_func = [](HCCallHandle /*call*/, const uint8_t* source,
size_t bytesAvailable, void* context) {
ctx_t* ctx = static_cast<ctx_t*>(context);
std::get<2>(*ctx).append(reinterpret_cast<const char*>(source), bytesAvailable);
return S_OK;
};
return HCHttpCallResponseSetResponseBodyWriteFunction(call, write_func, static_cast<void*>(&ctx));
}
// This call back is called libHC when request is completed or failed
// Prior to performing request we stashed ctx_t (HC_CALL, SDK callbacks, body)
// in the asyncBlock context. Here we extract relevant information
// to call SDK callback. Once this function is done final cleanup is
// done via unique_ptr<HC_CALL> destructor implicitly.
static void __stdcall hc_req_completed_cb(XAsyncBlock* ab) noexcept {
const std::unique_ptr<XAsyncBlock> asyncBlock(ab);
const std::unique_ptr<ctx_t> ctx(static_cast<ctx_t*>(asyncBlock->context));
HC_CALL* call = std::get<0>(*ctx).get();
auto& callback = std::get<1>(*ctx);
{
NHttpResponse resp{InternalStatusCodes::CANCELLED_BY_USER, "", ""};
HRESULT hr = XAsyncGetStatus(asyncBlock.get(), false);
if (E_ABORT == hr) {
HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "Aborted XAsyncGetStatus");
resp.statusCode = InternalStatusCodes::CANCELLED_BY_USER;
callback(std::make_shared<NHttpResponse>(resp));
}
// else if (FAILED(hr)) {
// HC_TRACE_ERROR_HR(httpTransportLibHC, hr, "XAsyncGetStatus");
// resp.statusCode =
// InternalStatusCodes::INTERNAL_TRANSPORT_ERROR;
// callback(std::make_shared<NHttpResponse>(resp));
// }
}
HRESULT errCode = 0;
uint32_t platErrCode = 0;
uint32_t hc_statusCode = 0;
int statusCode = 0;
std::string responseString;
std::string errMessage;
HCHttpCallResponseGetNetworkErrorCode(call, &errCode, &platErrCode);
HCHttpCallResponseGetStatusCode(call, &hc_statusCode);
statusCode = hc_statusCode;
if (FAILED(errCode)) {
const char* errMsg;
HCHttpCallResponseGetPlatformNetworkErrorMessage(call, &errMsg);
errMessage = errMsg;
statusCode = InternalStatusCodes::CONNECTION_ERROR; // FIXME: need a way to indicate
// request timeout
}
callback(
std::make_shared<NHttpResponse>(NHttpResponse{statusCode, std::move(std::get<2>(*ctx)), std::move(errMessage)}));
};
void NHttpClientLibHC::request(const NHttpRequest& req, const NHttpResponseCallback& callback) {
if (m_baseUri.empty()) {
submit_cb(
callback, InternalStatusCodes::NOT_INITIALIZED_ERROR, "", "[NHttpClientLibHC::request] base uri is not set");
return;
}
call_ptr call(nullptr, &HCHttpCallCloseHandle);
CHECK_CB(prep_hc_call(req, call), "Error preparing HC_CALL", callback);
XAsyncBlock* asyncBlock = new XAsyncBlock{};
ctx_t* ctx = new ctx_t(std::move(call), callback, std::string());
asyncBlock->context = ctx;
asyncBlock->queue = m_queue.get(); // unique_ptr m_queue will outlive asyncBlock, so thats fine
if (callback) {
CHECK_CB(setup_hc_response_to_string(*ctx), "HCHttpCallResponseSetResponseBodyWriteFunction failed", callback);
asyncBlock->callback = &hc_req_completed_cb;
};
if (m_timeout >= std::chrono::seconds(0)) {
CHECK_CB(
HCHttpCallRequestSetTimeout(
std::get<0>(*ctx).get(), std::chrono::duration_cast<std::chrono::seconds>(m_timeout).count()),
"HCHttpCallPerformAsync failed", callback);
}
CHECK_CB(HCHttpCallPerformAsync(std::get<0>(*ctx).get(), asyncBlock), "HCHttpCallPerformAsync failed", callback);
}
void NHttpClientLibHC::tick() {
while (XTaskQueueDispatch(m_queue.get(), XTaskQueuePort::Completion, 0)) {
};
}
void NHttpClientLibHC::submit_cb(
const NHttpResponseCallback& cb,
int statusCode,
std::string body,
std::string err) noexcept {
if (cb) {
using xtask_cb_ctx_t = std::tuple<NHttpResponseCallback, NHttpResponse>;
xtask_cb_ctx_t* ctx = new auto(std::make_tuple(cb, NHttpResponse{statusCode, body, err}));
XTaskQueueSubmitCallback(m_queue.get(), XTaskQueuePort::Completion, ctx, [](void* ctx, bool /*cancelled*/) {
// We deliberately ignore _cancelled and deliver callback as it was
// requested
std::unique_ptr<xtask_cb_ctx_t> context(static_cast<xtask_cb_ctx_t*>(ctx));
auto& cb = std::get<0>(*context);
auto& response = std::get<1>(*context);
cb(std::make_shared<NHttpResponse>(std::move(response)));
});
}
}
void NHttpClientLibHC::cancelAllRequests() {
// We can't use `wait` because if we are called from the same thread tick is
// being called, then we deadlock should there be any outstanding requests. So
// instead we use termination callback which will be a last callback on the
// Completion port
m_queue_terminated.store(false, std::memory_order_relaxed);
XTaskQueueTerminate(m_queue.get(), false, &m_queue_terminated, [](void* ctx) {
static_cast<std::atomic<bool>*>(ctx)->store(true, std::memory_order_relaxed);
});
// Pump completion queue until our callback is executed. tick() can still be
// called in parallel thread, then it will be set from there, but we do our
// pumping here in case tick is not called anymore. That is also why we have
// atomic bool, not just bool.
while (!m_queue_terminated.load(std::memory_order_relaxed)) {
XTaskQueueDispatch(m_queue.get(), XTaskQueuePort::Completion, 50);
}
// XTaskQueueTerminate permanently kills the queue. Create a fresh one
// so the client can be reused after cancellation.
m_queue.reset(nullptr);
XTaskQueueHandle q = nullptr;
HRESULT hr = XTaskQueueCreate(XTaskQueueDispatchMode::ThreadPool, XTaskQueueDispatchMode::Manual, &q);
if (FAILED(hr)) {
HC_TRACE_ERROR(httpTransportLibHC, "Failed to re-create XTaskQueue after cancel: hr=0x%08x", hr);
} else {
m_queue.reset(q);
}
}
} // namespace Nakama