mirror of
https://github.com/heroiclabs/nakama-cpp.git
synced 2026-11-13 18:33:06 -10:00
* 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
318 lines
12 KiB
C++
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
|