fix(server): 翻译失败后允许下次候选请求重试 (#1021)
* fix(server): 翻译失败后允许下次候选请求重试 * fix(server): 翻译失败项按次数退避,未发出的批次尾部不记失败 完全去掉失败缓存后,凭据错误、限流和确实没有译文的词会在每次候选刷新时被整批重发, 计费提供方会重复扣费;批次开头持续超时的词还会每次都耗尽预算,让尾部永远发不出去。 - 失败项按词退避:15 秒起步逐次翻倍,封顶 8 分钟,成功即清除 - 牛翻与自定义接口在批次预算耗尽时截短结果数组,共享层据此跳过未发出的尾部 - 退避时长抽成 CloudTranslation::FailureRetryDelay 并补单测 --------- Co-authored-by: fanlusky <1730976608@qq.com>
This commit is contained in:
1 parent
0bad4c1aaa
commit
96b64d20bd
8 files changed
+62
-23
No files matched your search
@@ -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<std::string, std::string> g_cache;
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_negative;
|
||||
|
||||
// 提供方对超时、限流、凭据错误和「没有译文」都只回空串,分不清哪次值得马上重试,所以失败项
|
||||
// 按次数退避:网络恢复后很快能补上,持续失败的词也不会每次刷新候选都被重发、挤占批次预算。
|
||||
struct FailureBackoff
|
||||
{
|
||||
unsigned failures = 0;
|
||||
std::chrono::steady_clock::time_point retry_at;
|
||||
};
|
||||
std::unordered_map<std::string, FailureBackoff> 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<EnglishIme::TranslationQuery> &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))
|
||||
|
||||
@@ -136,7 +136,10 @@ std::vector<std::string> 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();
|
||||
|
||||
@@ -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<std::string> TextTranslateBatch(const Config &config, const std::vector<std::string> &texts,
|
||||
const std::string &source, const std::string &target);
|
||||
} // namespace CustomTranslation
|
||||
@@ -147,7 +147,10 @@ std::vector<std::string> 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.
|
||||
|
||||
@@ -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<std::string> TextTranslateBatch(const Config &config, const std::vector<std::string> &texts,
|
||||
const std::string &source, const std::string &target);
|
||||
} // namespace NiuTransTranslation
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "translation_gloss.h"
|
||||
|
||||
#include <utf8.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cctype>
|
||||
|
||||
@@ -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<std::chrono::seconds>(kMaxRetryDelay));
|
||||
}
|
||||
|
||||
size_t Utf8Length(const std::string &text)
|
||||
{
|
||||
if (text.empty() || !utf8::is_valid(text.begin(), text.end()))
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
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);
|
||||
|
||||
@@ -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("水杉"));
|
||||
|
||||
Reference in new issue
Block a user