diff --git a/server/src/cloud/cloud_translation.cpp b/server/src/cloud/cloud_translation.cpp index 808272c9..9aceec83 100644 --- a/server/src/cloud/cloud_translation.cpp +++ b/server/src/cloud/cloud_translation.cpp @@ -20,7 +20,6 @@ namespace { // Wait until the candidate window has been idle for 500ms before a TMT request. constexpr auto kIdleDelay = std::chrono::milliseconds(500); -constexpr auto kNegativeTtl = std::chrono::minutes(8); constexpr size_t kMaxSourceChars = 40; constexpr size_t kMaxCacheEntries = 4096; @@ -36,7 +35,15 @@ std::string g_db_path; CloudTranslation::ApplyCallback g_callback; std::unordered_map g_cache; -std::unordered_map g_negative; + +// 提供方对超时、限流、凭据错误和「没有译文」都只回空串,分不清哪次值得马上重试,所以失败项 +// 按次数退避:网络恢复后很快能补上,持续失败的词也不会每次刷新候选都被重发、挤占批次预算。 +struct FailureBackoff +{ + unsigned failures = 0; + std::chrono::steady_clock::time_point retry_at; +}; +std::unordered_map g_failures; std::string TargetLanguage(const TranslationConfig &config) { @@ -98,28 +105,22 @@ void RememberPositive(const std::string &identity, std::string gloss) if (g_cache.size() >= kMaxCacheEntries) g_cache.clear(); g_cache[identity] = std::move(gloss); - g_negative.erase(identity); + g_failures.erase(identity); } -bool IsNegative(const std::string &identity) +bool IsBackingOff(const std::string &identity) { - const auto found = g_negative.find(identity); - if (found == g_negative.end()) - return false; - if (found->second <= std::chrono::steady_clock::now()) - { - g_negative.erase(found); - return false; - } - return true; + const auto found = g_failures.find(identity); + return found != g_failures.end() && std::chrono::steady_clock::now() < found->second.retry_at; } -void RememberNegative(const std::string &identity) +void RememberFailure(const std::string &identity) { std::lock_guard lock(g_mutex); - if (g_negative.size() >= kMaxCacheEntries) - g_negative.clear(); - g_negative[identity] = std::chrono::steady_clock::now() + kNegativeTtl; + if (g_failures.size() >= kMaxCacheEntries && g_failures.find(identity) == g_failures.end()) + g_failures.clear(); + auto &entry = g_failures[identity]; + entry.retry_at = std::chrono::steady_clock::now() + CloudTranslation::FailureRetryDelay(++entry.failures); } TencentTmt::Credentials ResolveCredentials(const TencentTmtConfig &config) @@ -200,12 +201,14 @@ void ApplyTranslatedGroup(const std::vector &group { for (size_t i = 0; i < group.size(); ++i) { + // 结果数组比请求短,说明提供方的批次预算先用完了,后面的词根本没有发出,不算失败。 + if (i >= translated.size()) + break; const std::string identity = Identity(group[i], target_language, provider_scope); - const std::string raw = i < translated.size() ? translated[i] : std::string{}; - const std::string gloss = CloudTranslation::FormatGloss(raw); + const std::string gloss = CloudTranslation::FormatGloss(translated[i]); if (gloss.empty()) { - RememberNegative(identity); + RememberFailure(identity); continue; } RememberPositive(identity, gloss); @@ -301,7 +304,7 @@ void WorkerLoop() results.push_back({query.key, query.direction, cached->second}); continue; } - if (IsNegative(identity)) + if (IsBackingOff(identity)) continue; if (query.direction == EnglishIme::TranslationDirection::EnglishToChinese && CloudTranslation::IsCloudTranslatableEnglish(query.key)) diff --git a/server/src/cloud/custom_translation.cpp b/server/src/cloud/custom_translation.cpp index aa5bec22..8d4b5d73 100644 --- a/server/src/cloud/custom_translation.cpp +++ b/server/src/cloud/custom_translation.cpp @@ -136,7 +136,10 @@ std::vector TextTranslateBatch(const Config &config, const std::vec for (size_t i = 0; i < texts.size(); ++i) { if (std::chrono::steady_clock::now() - started >= kBatchBudget) + { + results.resize(i); break; + } const nlohmann::json body = { {"text", texts[i]}, {"source_lang", UppercaseLanguage(source)}, {"target_lang", UppercaseLanguage(target)}}; const std::string payload = body.dump(); diff --git a/server/src/cloud/custom_translation.h b/server/src/cloud/custom_translation.h index a679dc23..b7b2a660 100644 --- a/server/src/cloud/custom_translation.h +++ b/server/src/cloud/custom_translation.h @@ -15,7 +15,8 @@ bool IsSupportedEndpoint(const std::string &endpoint); std::string ParseTranslationResponse(const std::string &response); // Calls a DeepLX-compatible JSON endpoint once per source string. Empty -// entries mean that item failed. +// entries mean that item failed. When the batch time budget runs out the result +// is shorter than texts: the missing tail was never sent. std::vector TextTranslateBatch(const Config &config, const std::vector &texts, const std::string &source, const std::string &target); } // namespace CustomTranslation diff --git a/server/src/cloud/niutrans_translation.cpp b/server/src/cloud/niutrans_translation.cpp index 66190185..7b307c8b 100644 --- a/server/src/cloud/niutrans_translation.cpp +++ b/server/src/cloud/niutrans_translation.cpp @@ -147,7 +147,10 @@ std::vector TextTranslateBatch(const Config &config, const std::vec for (size_t i = 0; i < texts.size(); ++i) { if (std::chrono::steady_clock::now() - started >= kBatchBudget) + { + results.resize(i); break; + } const std::string timestamp = MillisecondTimestamp(); // authStr is computed over the raw (unencoded) parameter values; the POST // body sends the same values url-encoded. diff --git a/server/src/cloud/niutrans_translation.h b/server/src/cloud/niutrans_translation.h index 0fe2aaa2..744a05e1 100644 --- a/server/src/cloud/niutrans_translation.h +++ b/server/src/cloud/niutrans_translation.h @@ -24,7 +24,8 @@ std::string BuildAuthString(const Config &config, const std::string &from, const std::string ParseTranslationResponse(const std::string &response); // Calls the NiuTrans /v2/text/translate endpoint once per source string. Empty -// entries mean that item failed. +// entries mean that item failed. When the batch time budget runs out the result +// is shorter than texts: the missing tail was never sent. std::vector TextTranslateBatch(const Config &config, const std::vector &texts, const std::string &source, const std::string &target); } // namespace NiuTransTranslation diff --git a/server/src/cloud/translation_gloss.cpp b/server/src/cloud/translation_gloss.cpp index 3d85ce40..63887e83 100644 --- a/server/src/cloud/translation_gloss.cpp +++ b/server/src/cloud/translation_gloss.cpp @@ -1,6 +1,7 @@ #include "translation_gloss.h" #include +#include #include #include @@ -9,6 +10,8 @@ namespace CloudTranslation namespace { constexpr size_t kMaxPersistChars = 32; +constexpr auto kFirstRetryDelay = std::chrono::seconds(15); +constexpr auto kMaxRetryDelay = std::chrono::minutes(8); bool IsAsciiSpace(unsigned char ch) { @@ -67,6 +70,16 @@ bool IsEmojiOrPictograph(uint32_t cp) } } // namespace +std::chrono::seconds FailureRetryDelay(unsigned failures) +{ + if (failures == 0) + return std::chrono::seconds(0); + // 15s << 5 正好是 8 分钟,再往上移位只会被封顶,也避免大次数移位溢出。 + const unsigned shift = (std::min)(failures - 1, 5u); + return (std::min)(std::chrono::seconds(kFirstRetryDelay * (1u << shift)), + std::chrono::duration_cast(kMaxRetryDelay)); +} + size_t Utf8Length(const std::string &text) { if (text.empty() || !utf8::is_valid(text.begin(), text.end())) diff --git a/server/src/cloud/translation_gloss.h b/server/src/cloud/translation_gloss.h index 06cc5e82..22720d7a 100644 --- a/server/src/cloud/translation_gloss.h +++ b/server/src/cloud/translation_gloss.h @@ -1,9 +1,12 @@ #pragma once +#include #include namespace CloudTranslation { +// 同一词条连续失败 failures 次后,到下次允许重试前的等待:15 秒起步逐次翻倍,封顶 8 分钟。 +std::chrono::seconds FailureRetryDelay(unsigned failures); // Collapse whitespace for candidate-window gloss. Length is left to layout. std::string FormatGloss(const std::string &text); bool ShouldPersistGloss(const std::string &key, const std::string &formatted); diff --git a/server/tests/src/test_translation_gloss.cpp b/server/tests/src/test_translation_gloss.cpp index 17dd7a8e..e55c823b 100644 --- a/server/tests/src/test_translation_gloss.cpp +++ b/server/tests/src/test_translation_gloss.cpp @@ -26,6 +26,18 @@ TEST_CASE(CloudTranslationPersistPolicyRejectsUselessGloss) REQUIRE_EQ(CloudTranslation::TrimSecret(" AKIDabc \r\n"), std::string("AKIDabc")); } +TEST_CASE(CloudTranslationFailureRetryDelayDoublesUpToEightMinutes) +{ + using std::chrono::seconds; + REQUIRE(CloudTranslation::FailureRetryDelay(0) == seconds(0)); + REQUIRE(CloudTranslation::FailureRetryDelay(1) == seconds(15)); + REQUIRE(CloudTranslation::FailureRetryDelay(2) == seconds(30)); + REQUIRE(CloudTranslation::FailureRetryDelay(5) == seconds(240)); + REQUIRE(CloudTranslation::FailureRetryDelay(6) == seconds(480)); + REQUIRE(CloudTranslation::FailureRetryDelay(7) == seconds(480)); + REQUIRE(CloudTranslation::FailureRetryDelay(4000000000u) == seconds(480)); +} + TEST_CASE(CloudTranslationOnlySendsChineseWithoutEmoji) { REQUIRE(CloudTranslation::IsCloudTranslatableChinese("水杉"));