From f0da2b003f59cac5dd23326cb2589e16465dac07 Mon Sep 17 00:00:00 2001 From: vkamn Date: Thu, 23 Apr 2026 21:30:18 +0800 Subject: [PATCH] feat: add fallback proxy endpoint (#2518) --- .github/workflows/deploy.yml | 7 ++ .github/workflows/tag-deploy.yml | 1 + client/CMakeLists.txt | 1 + client/core/controllers/gatewayController.cpp | 79 +++++++++++-------- 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 99e24b3a4..669552a7d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,6 +17,7 @@ jobs: QIF_VERSION: 4.7 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} @@ -98,6 +99,7 @@ jobs: BUILD_ARCH: 64 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} @@ -204,6 +206,7 @@ jobs: CXX: c++ PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} @@ -318,6 +321,7 @@ jobs: PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} @@ -395,6 +399,7 @@ jobs: PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} @@ -477,6 +482,7 @@ jobs: PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} @@ -545,6 +551,7 @@ jobs: QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} diff --git a/.github/workflows/tag-deploy.yml b/.github/workflows/tag-deploy.yml index 31c334bfd..bed2862e1 100644 --- a/.github/workflows/tag-deploy.yml +++ b/.github/workflows/tag-deploy.yml @@ -17,6 +17,7 @@ jobs: QIF_VERSION: 4.5 PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} + FALLBACK_S3_ENDPOINT: ${{ secrets.FALLBACK_S3_ENDPOINT }} DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }} DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }} DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }} diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0f3ae7a0f..1a7e91433 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -25,6 +25,7 @@ add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}") add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}") add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}") +add_definitions(-DFALLBACK_S3_ENDPOINT="$ENV{FALLBACK_S3_ENDPOINT}") add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}") add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}") diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 30b4c572d..4631eac80 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -49,6 +49,8 @@ namespace constexpr int httpStatusCodeUnprocessableEntity = 422; constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?"); + + constexpr int proxyStorageRequestTimeoutMsecs = 3000; } GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, @@ -284,23 +286,30 @@ QFuture> GatewayController::postAsync(const QString auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); - QStringList baseUrls; + QStringList primaryBaseUrls; + QStringList fallbackBaseUrls; if (m_isDevEnvironment) { - baseUrls = QString(DEV_S3_ENDPOINT).split(", "); + primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); } else { - baseUrls = QString(PROD_S3_ENDPOINT).split(", "); + primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); + fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); } - QStringList proxyStorageUrls; - if (!serviceType.isEmpty()) { - for (const auto &baseUrl : baseUrls) { - QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); - proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) - + ".json"); + auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) { + if (!serviceType.isEmpty()) { + for (const auto &baseUrl : baseUrls) { + QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); + target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); + } } - } - for (const auto &baseUrl : baseUrls) - proxyStorageUrls.push_back(baseUrl + "endpoints.json"); + for (const auto &baseUrl : baseUrls) { + target.push_back(baseUrl + "endpoints.json"); + } + }; + + QStringList proxyStorageUrls; + appendStorageUrls(primaryBaseUrls, proxyStorageUrls); + appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) { getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) { @@ -327,40 +336,48 @@ QFuture> GatewayController::postAsync(const QString QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) { QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); + request.setTransferTimeout(proxyStorageRequestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; QList sslErrors; QNetworkReply *reply; - QStringList baseUrls; + QStringList primaryBaseUrls; + QStringList fallbackBaseUrls; if (m_isDevEnvironment) { - baseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); + primaryBaseUrls = QString(DEV_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); } else { - baseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); - } - - if (baseUrls.empty()) { - qDebug() << "empty storage endpoint list"; - return {}; + primaryBaseUrls = QString(PROD_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); + fallbackBaseUrls = QString(FALLBACK_S3_ENDPOINT).split(", ", Qt::SkipEmptyParts); } std::random_device randomDevice; std::mt19937 generator(randomDevice()); - std::shuffle(baseUrls.begin(), baseUrls.end(), generator); + std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator); + std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator); QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; - QStringList proxyStorageUrls; - if (!serviceType.isEmpty()) { - for (const auto &baseUrl : baseUrls) { - QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); - proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); + auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) { + if (!serviceType.isEmpty()) { + for (const auto &baseUrl : baseUrls) { + QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); + target.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); + } } - } - for (const auto &baseUrl : baseUrls) { - proxyStorageUrls.push_back(baseUrl + "endpoints.json"); + for (const auto &baseUrl : baseUrls) { + target.push_back(baseUrl + "endpoints.json"); + } + }; + + QStringList proxyStorageUrls; + appendStorageUrls(primaryBaseUrls, proxyStorageUrls); + appendStorageUrls(fallbackBaseUrls, proxyStorageUrls); + + if (proxyStorageUrls.empty()) { + qDebug() << "empty storage endpoint list"; + return {}; } for (const auto &proxyStorageUrl : proxyStorageUrls) { @@ -562,7 +579,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co } QNetworkRequest request; - request.setTransferTimeout(m_requestTimeoutMsecs); + request.setTransferTimeout(proxyStorageRequestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);