@@ -45,34 +45,59 @@ abstract class AbstractTranslator : Translator, TranslatorConfigurable {
4545 companion object {
4646 protected val LOG = Logger .getInstance(AbstractTranslator ::class .java)
4747 private const val DEFAULT_CONTENT_TYPE = " application/x-www-form-urlencoded"
48- private const val DEFAULT_TIMEOUT_MS = 60 * 1000
48+ private const val DEFAULT_TIMEOUT_MS = 30 * 1000
49+ private const val MAX_RETRY_COUNT = 3
50+ private const val BASE_RETRY_DELAY_MS = 500L
4951 }
5052
5153 @Throws(TranslationException ::class )
5254 override fun doTranslate (fromLang : Lang , toLang : Lang , text : String ): String {
5355 checkSupportedLanguages(fromLang, toLang, text)
5456
5557 val requestUrl = getRequestUrl(fromLang, toLang, text)
56- val requestBuilder = createRequestBuilder(requestUrl)
57- configureRequestBuilder(requestBuilder)
58-
59- return try {
60- val payload = buildRequestPayload(fromLang, toLang, text)
61- requestBuilder.connect { request ->
62- payload.form?.let { request.write(it) }
63- payload.body?.let { body ->
64- if (payload.form != null && body.isNotEmpty()) {
65- request.write(" &" )
58+ var lastException: Exception ? = null
59+
60+ for (attempt in 0 .. MAX_RETRY_COUNT ) {
61+ val requestBuilder = createRequestBuilder(requestUrl)
62+ configureRequestBuilder(requestBuilder)
63+
64+ try {
65+ val payload = buildRequestPayload(fromLang, toLang, text)
66+ return requestBuilder.connect { request ->
67+ payload.form?.let { request.write(it) }
68+ payload.body?.let { body ->
69+ if (payload.form != null && body.isNotEmpty()) {
70+ request.write(" &" )
71+ }
72+ request.write(body)
6673 }
67- request.write(body)
74+ val resultText = request.readString()
75+ parsingResult(fromLang, toLang, text, resultText)
76+ }
77+ } catch (e: Exception ) {
78+ lastException = e
79+ val shouldRetry = e.isRecoverable() && attempt < MAX_RETRY_COUNT
80+ if (! shouldRetry) {
81+ LOG .error(" Translation request failed: ${e.message} " , e)
82+ throw TranslationException (fromLang, toLang, text, e)
83+ }
84+ LOG .warn(" Recoverable translation failure, retrying (attempt ${attempt + 1 } of ${MAX_RETRY_COUNT + 1 } ): ${e.message} " )
85+ val backoffDelayMs = BASE_RETRY_DELAY_MS * (1L shl attempt)
86+ try {
87+ Thread .sleep(backoffDelayMs)
88+ } catch (interruptedException: InterruptedException ) {
89+ Thread .currentThread().interrupt()
90+ throw TranslationException (fromLang, toLang, text, interruptedException)
6891 }
69- val resultText = request.readString()
70- parsingResult(fromLang, toLang, text, resultText)
7192 }
72- } catch (e: Exception ) {
73- LOG .warn(" Translation request failed: ${e.message} " , e)
74- throw TranslationException (fromLang, toLang, text, e)
7593 }
94+
95+ throw TranslationException (
96+ fromLang,
97+ toLang,
98+ text,
99+ lastException ? : IllegalStateException (" Translation failed without capturing an exception" )
100+ )
76101 }
77102
78103 protected open fun createRequestBuilder (requestUrl : String ): RequestBuilder {
@@ -143,5 +168,21 @@ abstract class AbstractTranslator : Translator, TranslatorConfigurable {
143168 return RequestPayload (requestParams, requestBody)
144169 }
145170}
146-
147171private data class RequestPayload (val form : String? , val body : String? )
172+
173+ private fun Throwable.isRecoverable (): Boolean {
174+ return generateSequence(this ) { it.cause }
175+ .any { cause ->
176+ when (cause) {
177+ is java.net.SocketTimeoutException ,
178+ is java.net.ConnectException ,
179+ is java.net.NoRouteToHostException ,
180+ is java.net.SocketException ,
181+ is java.util.concurrent.TimeoutException -> true
182+ else -> {
183+ val message = cause.message?.lowercase() ? : return @any false
184+ message.contains(" timeout" ) || message.contains(" connection refused" )
185+ }
186+ }
187+ }
188+ }
0 commit comments