Skip to content

Commit 050e470

Browse files
authored
Merge pull request #340 from lingarr-translate/add_retry_mechanism
added retry mechanism to libretranslate and public translate services
2 parents 7075d69 + baae0b6 commit 050e470

File tree

11 files changed

+194
-92
lines changed

11 files changed

+194
-92
lines changed

Lingarr.Client/src/components/common/TranslationAction.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@click="router.push({ name: 'translation-detail', params: { id: item.id } })">
66
<EyeOnIcon class="h-5 w-5" />
77
</button>
8-
<div class="flex w-[3rem] items-center gap-2">
8+
<div class="flex w-12 items-center gap-2">
99
<LoaderCircleIcon v-if="loading" class="h-5 w-5 animate-spin" />
1010
<button
1111
v-else-if="inProgress"
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
<template>
22
<span :title="translationStatus.toString()">
3-
{{ translationStatus }}
3+
{{ displayStatus }}
44
</span>
55
</template>
66

77
<script setup lang="ts">
8-
import { TranslationStatus } from '@/ts'
8+
import { computed } from 'vue'
9+
import { TranslationStatus, TRANSLATION_STATUS } from '@/ts'
910
10-
defineProps<{
11+
const props = defineProps<{
1112
translationStatus: TranslationStatus
1213
}>()
14+
15+
const displayStatus = computed(() => {
16+
switch (props.translationStatus) {
17+
case TRANSLATION_STATUS.INPROGRESS:
18+
return 'In Progress'
19+
default:
20+
return props.translationStatus
21+
}
22+
})
1323
</script>

Lingarr.Client/src/components/features/settings/IntegrationSettings.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const saveNotification = ref<InstanceType<typeof SaveNotification> | null>(null)
6565
const settingsStore = useSettingStore()
6666
6767
const radarrApiKey = computed({
68-
get: (): string => settingsStore.getEncryptedSetting(ENCRYPTED_SETTINGS.RADARR_API_KEY) as string,
68+
get: (): string | null => settingsStore.getEncryptedSetting(ENCRYPTED_SETTINGS.RADARR_API_KEY) ?? null,
6969
set: (newValue: string): void => {
7070
settingsStore.updateEncryptedSetting(ENCRYPTED_SETTINGS.RADARR_API_KEY, newValue, isValid.radarrApiKey)
7171
if (isValid.radarrApiKey) {
@@ -74,7 +74,7 @@ const radarrApiKey = computed({
7474
}
7575
})
7676
const sonarrApiKey = computed({
77-
get: (): string => settingsStore.getEncryptedSetting(ENCRYPTED_SETTINGS.SONARR_API_KEY) as string,
77+
get: (): string | null => settingsStore.getEncryptedSetting(ENCRYPTED_SETTINGS.SONARR_API_KEY) ?? null,
7878
set: (newValue: string): void => {
7979
settingsStore.updateEncryptedSetting(ENCRYPTED_SETTINGS.SONARR_API_KEY, newValue, isValid.sonarrApiKey)
8080
if (isValid.sonarrApiKey) {
@@ -83,7 +83,7 @@ const sonarrApiKey = computed({
8383
}
8484
})
8585
const radarrUrl = computed({
86-
get: (): string => settingsStore.getSetting(SETTINGS.RADARR_URL) as string,
86+
get: (): string | null => (settingsStore.getSetting(SETTINGS.RADARR_URL) as string) ?? null,
8787
set: (newValue: string): void => {
8888
settingsStore.updateSetting(SETTINGS.RADARR_URL, newValue, isValid.radarrUrl)
8989
if (isValid.radarrUrl) {
@@ -92,7 +92,7 @@ const radarrUrl = computed({
9292
}
9393
})
9494
const sonarrUrl = computed({
95-
get: (): string => settingsStore.getSetting(SETTINGS.SONARR_URL) as string,
95+
get: (): string | null => (settingsStore.getSetting(SETTINGS.SONARR_URL) as string) ?? null,
9696
set: (newValue: string): void => {
9797
settingsStore.updateSetting(SETTINGS.SONARR_URL, newValue, isValid.sonarrUrl)
9898
if (isValid.sonarrUrl) {

Lingarr.Client/src/components/features/settings/TranslationSettings.vue

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
<template>
22
<CardComponent title="Translation Request">
33
<template #description>
4-
Modify translation request settings by changing batch size or retry options.
4+
Modify translation request settings by changing retry options or batch size if available in the service.
55
</template>
66
<template #content>
77
<SaveNotification ref="saveNotification" />
88

9-
<div class="flex flex-col space-x-2">
10-
<span class="font-semibold">Use batch translation</span>
11-
Process multiple subtitle lines together in batches to improve translation
12-
efficiency and context awareness. Note that single-line translations with context
13-
are still more reliable and of higher quality.
14-
</div>
15-
<ToggleButton v-model="useBatchTranslation">
16-
<span class="text-sm font-medium text-primary-content">
17-
{{ useBatchTranslation == 'true' ? 'Enabled' : 'Disabled' }}
18-
</span>
19-
</ToggleButton>
9+
<template
10+
v-if="
11+
[
12+
SERVICE_TYPE.ANTHROPIC,
13+
SERVICE_TYPE.DEEPSEEK,
14+
SERVICE_TYPE.GEMINI,
15+
SERVICE_TYPE.LOCALAI,
16+
SERVICE_TYPE.OPENAI
17+
].includes(
18+
serviceType as 'openai' | 'anthropic' | 'localai' | 'gemini' | 'deepseek'
19+
)
20+
">
21+
<div class="flex flex-col space-x-2">
22+
<span class="font-semibold">Use batch translation</span>
23+
Process multiple subtitle lines together in batches to improve translation
24+
efficiency and context awareness. Note that single-line translations with context
25+
are still more reliable and of higher quality.
26+
</div>
27+
<ToggleButton v-model="useBatchTranslation">
28+
<span class="text-sm font-medium text-primary-content">
29+
{{ useBatchTranslation == 'true' ? 'Enabled' : 'Disabled' }}
30+
</span>
31+
</ToggleButton>
32+
</template>
2033
<InputComponent
2134
v-if="useBatchTranslation == 'true'"
2235
v-model="maxBatchSize"
@@ -56,7 +69,7 @@
5669
<script setup lang="ts">
5770
import { computed, ref, reactive } from 'vue'
5871
import { useSettingStore } from '@/store/setting'
59-
import { INPUT_VALIDATION_TYPE, SETTINGS } from '@/ts'
72+
import { INPUT_VALIDATION_TYPE, SERVICE_TYPE, SETTINGS } from '@/ts'
6073
import CardComponent from '@/components/common/CardComponent.vue'
6174
import SaveNotification from '@/components/common/SaveNotification.vue'
6275
import InputComponent from '@/components/common/InputComponent.vue'
@@ -70,6 +83,7 @@ const isValid = reactive({
7083
retryDelay: true,
7184
retryDelayMultiplier: true
7285
})
86+
const serviceType = computed(() => settingsStore.getSetting(SETTINGS.SERVICE_TYPE))
7387
7488
const useBatchTranslation = computed({
7589
get: (): string => settingsStore.getSetting(SETTINGS.USE_BATCH_TRANSLATION) as string,

Lingarr.Client/src/components/features/settings/WebhookInstructions.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
and use this URL:
1313
</span>
1414
<span class="font-semibold">Radarr</span>
15-
<code class="bg-accent/20 mt-1 block rounded p-2 text-sm">
15+
<code class="bg-accent/20 mt-1 block rounded p-2 text-sm overflow-x-auto">
1616
{{ webhookUrl }}/api/webhook/radarr
1717
</code>
1818
<span class="font-semibold">Sonarr</span>
19-
<code class="bg-accent/20 mt-1 block rounded p-2 text-sm">
19+
<code class="bg-accent/20 mt-1 block rounded p-2 text-sm overflow-x-auto">
2020
{{ webhookUrl }}/api/webhook/sonarr
2121
</code>
2222
</div>

Lingarr.Client/src/pages/TranslationDetailPage.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ const latestPosition = ref<number>(0)
163163
const reversedLines = computed(() => (detail.value ? [...detail.value.lines].reverse() : []))
164164
165165
const showProgress = computed(() => {
166-
if (!detail.value) return false
166+
if (!detail.value) {
167+
return false
168+
}
167169
const status = detail.value.status
168170
return (
169171
(status === TRANSLATION_STATUS.INPROGRESS && progress.value > 0) ||

Lingarr.Client/src/pages/settings/ServicesPage.vue

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,11 @@
22
<div
33
class="grid grid-flow-row auto-rows-max grid-cols-1 gap-4 p-4 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3">
44
<ServicesSettings />
5-
<TranslationSettings
6-
v-if="
7-
[
8-
SERVICE_TYPE.ANTHROPIC,
9-
SERVICE_TYPE.DEEPSEEK,
10-
SERVICE_TYPE.GEMINI,
11-
SERVICE_TYPE.LOCALAI,
12-
SERVICE_TYPE.OPENAI
13-
].includes(
14-
serviceType as 'openai' | 'anthropic' | 'localai' | 'gemini' | 'deepseek'
15-
)
16-
" />
5+
<TranslationSettings />
176
</div>
187
</template>
198

209
<script setup lang="ts">
21-
import { computed } from 'vue'
22-
import { SETTINGS, SERVICE_TYPE } from '@/ts'
23-
import { useSettingStore } from '@/store/setting'
2410
import ServicesSettings from '@/components/features/settings/ServicesSettings.vue'
2511
import TranslationSettings from '@/components/features/settings/TranslationSettings.vue'
26-
27-
const settingsStore = useSettingStore()
28-
const serviceType = computed(() => settingsStore.getSetting(SETTINGS.SERVICE_TYPE))
2912
</script>

Lingarr.Server/Services/SettingService.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,20 @@ public async Task<Dictionary<string, string>> GetSettings(IEnumerable<string> ke
8989
}
9090
}
9191

92-
if (keysToFetch.Any())
92+
if (!keysToFetch.Any())
9393
{
94-
var dbSettings = await _dbContext.Settings
95-
.Where(s => keysToFetch.Contains(s.Key))
96-
.ToListAsync();
94+
return result;
95+
}
96+
var dbSettings = await _dbContext.Settings
97+
.Where(s => keysToFetch.Contains(s.Key))
98+
.ToListAsync();
9799

98-
foreach (var setting in dbSettings)
99-
{
100-
result[setting.Key] = setting.Value;
101-
_cache.Set(setting.Key, setting.Value, _cacheOptions);
102-
}
100+
foreach (var setting in dbSettings)
101+
{
102+
result[setting.Key] = setting.Value;
103+
_cache.Set(setting.Key, setting.Value, _cacheOptions);
103104
}
104-
105+
105106
return result;
106107
}
107108

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
using System.Net;
1+
using System.Net;
22
using GTranslate.Translators;
3+
using Lingarr.Core.Configuration;
34
using Lingarr.Server.Exceptions;
45
using Lingarr.Server.Interfaces.Services;
56
using Lingarr.Server.Services.Translation.Base;
@@ -9,6 +10,13 @@ namespace Lingarr.Server.Services.Translation;
910
public class GTranslatorService<T> : BaseLanguageService where T : ITranslator
1011
{
1112
private readonly T _translator;
13+
private bool _initialized;
14+
private readonly SemaphoreSlim _initLock = new(1, 1);
15+
16+
// retry settings
17+
private int _maxRetries;
18+
private TimeSpan _retryDelay;
19+
private int _retryDelayMultiplier;
1220

1321
/// <inheritdoc />
1422
public override string? ModelName => null;
@@ -22,49 +30,85 @@ public GTranslatorService(
2230
{
2331
_translator = translator;
2432
}
25-
33+
34+
private async Task InitializeAsync()
35+
{
36+
if (_initialized) return;
37+
38+
try
39+
{
40+
await _initLock.WaitAsync();
41+
if (_initialized) return;
42+
43+
var settings = await _settings.GetSettings([
44+
SettingKeys.Translation.MaxRetries,
45+
SettingKeys.Translation.RetryDelay,
46+
SettingKeys.Translation.RetryDelayMultiplier
47+
]);
48+
49+
_maxRetries = int.TryParse(settings[SettingKeys.Translation.MaxRetries], out var maxRetries)
50+
? maxRetries
51+
: 3;
52+
53+
var retryDelaySeconds = int.TryParse(settings[SettingKeys.Translation.RetryDelay], out var delaySeconds)
54+
? delaySeconds
55+
: 2;
56+
_retryDelay = TimeSpan.FromSeconds(retryDelaySeconds);
57+
58+
_retryDelayMultiplier = int.TryParse(settings[SettingKeys.Translation.RetryDelayMultiplier], out var multiplier)
59+
? multiplier
60+
: 2;
61+
62+
_initialized = true;
63+
}
64+
finally
65+
{
66+
_initLock.Release();
67+
}
68+
}
69+
2670
/// <inheritdoc />
2771
public override async Task<string> TranslateAsync(
28-
string text,
29-
string sourceLanguage,
72+
string text,
73+
string sourceLanguage,
3074
string targetLanguage,
31-
List<string>? contextLinesBefore,
32-
List<string>? contextLinesAfter,
75+
List<string>? contextLinesBefore,
76+
List<string>? contextLinesAfter,
3377
CancellationToken cancellationToken)
3478
{
79+
await InitializeAsync();
80+
3581
using var retry = new CancellationTokenSource();
3682
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, retry.Token);
37-
38-
const int maxRetries = 5;
39-
var delay = TimeSpan.FromSeconds(1);
40-
var maxDelay = TimeSpan.FromSeconds(32);
41-
for (var attempt = 1; attempt <= maxRetries; attempt++)
83+
84+
var delay = _retryDelay;
85+
for (var attempt = 1; attempt <= _maxRetries + 1; attempt++)
4286
{
4387
try
4488
{
4589
var result = await _translator.TranslateAsync(
46-
text,
47-
targetLanguage,
48-
sourceLanguage)
90+
text,
91+
targetLanguage,
92+
sourceLanguage)
4993
.WaitAsync(linked.Token)
5094
.ConfigureAwait(false);
51-
95+
5296
return result.Translation;
5397
}
5498
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.ServiceUnavailable)
5599
{
56-
if (attempt == maxRetries)
100+
if (attempt > _maxRetries)
57101
{
58102
_logger.LogError(ex, "Max retries exhausted ({StatusCode}) for text: {Text}", ex.StatusCode, text);
59103
throw new TranslationException($"Retry limit reached after {ex.StatusCode}.", ex);
60104
}
61105

62106
_logger.LogWarning(
63107
"{ServiceName} received {StatusCode}. Retrying in {Delay}... (Attempt {Attempt}/{MaxRetries})",
64-
"GTranslator", ex.StatusCode, delay, attempt, maxRetries);
108+
"GTranslator", ex.StatusCode, delay, attempt, _maxRetries);
65109

66110
await Task.Delay(delay, linked.Token).ConfigureAwait(false);
67-
delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, maxDelay.Ticks));
111+
delay = TimeSpan.FromTicks(delay.Ticks * _retryDelayMultiplier);
68112
}
69113
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
70114
{
@@ -79,4 +123,4 @@ public override async Task<string> TranslateAsync(
79123

80124
throw new TranslationException("Translation failed after maximum retry attempts.");
81125
}
82-
}
126+
}

0 commit comments

Comments
 (0)