Merge branch 'dev' into feature/CTLE-434-oms-update-business-logic

# Conflicts:
#	qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
#	qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
This commit is contained in:
2023-06-02 09:02:05 -05:00
12 changed files with 1018 additions and 82 deletions

View File

@ -64,6 +64,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.api.exceptions.OAuthCredentialsException;
import com.kingsrook.qqq.backend.module.api.exceptions.OAuthExpiredTokenException;
import com.kingsrook.qqq.backend.module.api.exceptions.RateLimitException;
import com.kingsrook.qqq.backend.module.api.exceptions.RetryableServerErrorException;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.OutboundAPILog;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
@ -525,7 +526,7 @@ public class BaseAPIActionUtil
{
return;
}
else if(statusCode == HttpStatus.SC_BAD_GATEWAY)
else if(statusCode == HttpStatus.SC_BAD_GATEWAY || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT)
{
LOG.info("HTTP " + request.getMethod() + " failed", logPair("table", table.getName()), logPair("statusCode", statusCode), logPair("responseContent", StringUtils.safeTruncate(resultString, 1024, "...")));
didLog = true;
@ -890,8 +891,10 @@ public class BaseAPIActionUtil
*******************************************************************************/
public QHttpResponse makeRequest(QTableMetaData table, HttpRequestBase request) throws QException
{
int sleepMillis = getInitialRateLimitBackoffMillis();
int rateLimitSleepMillis = getInitialRateLimitBackoffMillis();
int serverErrorsSleepMillis = getInitialServerErrorBackoffMillis();
int rateLimitsCaught = 0;
int serverErrorsCaught = 0;
boolean caughtAnOAuthExpiredToken = false;
while(true)
@ -925,7 +928,11 @@ public class BaseAPIActionUtil
{
throw (new RateLimitException(qResponse.getContent()));
}
if(statusCode >= 400)
else if(shouldBeRetryableServerErrorException(qResponse))
{
throw (new RetryableServerErrorException(statusCode, qResponse.getContent()));
}
else if(statusCode >= 400)
{
handleResponseError(table, request, qResponse);
}
@ -962,9 +969,22 @@ public class BaseAPIActionUtil
throw (new QException(rle));
}
LOG.info("Caught RateLimitException", logPair("rateLimitsCaught", rateLimitsCaught), logPair("uri", request.getURI()), logPair("table", table.getName()), logPair("sleeping", sleepMillis));
SleepUtils.sleep(sleepMillis, TimeUnit.MILLISECONDS);
sleepMillis *= 2;
LOG.info("Caught RateLimitException", logPair("rateLimitsCaught", rateLimitsCaught), logPair("uri", request.getURI()), logPair("table", table.getName()), logPair("sleeping", rateLimitSleepMillis));
SleepUtils.sleep(rateLimitSleepMillis, TimeUnit.MILLISECONDS);
rateLimitSleepMillis *= 2;
}
catch(RetryableServerErrorException see)
{
serverErrorsCaught++;
if(serverErrorsCaught > getMaxAllowedServerErrors())
{
LOG.error("Giving up " + request.getMethod() + " to [" + table.getName() + "] after too many server-side errors (" + getMaxAllowedServerErrors() + ")");
throw (new QException(see));
}
LOG.info("Caught Server-side error during API request", logPair("serverErrorsCaught", serverErrorsCaught), logPair("uri", request.getURI()), logPair("code", see.getCode()), logPair("table", table.getName()), logPair("sleeping", serverErrorsSleepMillis));
SleepUtils.sleep(serverErrorsSleepMillis, TimeUnit.MILLISECONDS);
serverErrorsSleepMillis *= 2;
}
catch(QException qe)
{
@ -984,6 +1004,16 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected boolean shouldBeRetryableServerErrorException(QHttpResponse qResponse)
{
return (qResponse.getStatusCode() != null && qResponse.getStatusCode() >= 500);
}
/*******************************************************************************
** one-line method, factored out so mock/tests can override
*******************************************************************************/
@ -1153,6 +1183,16 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected int getInitialServerErrorBackoffMillis()
{
return (500);
}
/*******************************************************************************
**
*******************************************************************************/
@ -1163,6 +1203,16 @@ public class BaseAPIActionUtil
/*******************************************************************************
**
*******************************************************************************/
protected int getMaxAllowedServerErrors()
{
return (3);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,77 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.module.api.exceptions;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
**
*******************************************************************************/
public class RetryableServerErrorException extends QException
{
private Integer code;
/*******************************************************************************
**
*******************************************************************************/
public RetryableServerErrorException(Integer code, String message)
{
super(message);
this.code = code;
}
/*******************************************************************************
** Getter for code
*******************************************************************************/
public Integer getCode()
{
return (this.code);
}
/*******************************************************************************
** Setter for code
*******************************************************************************/
public void setCode(Integer code)
{
this.code = code;
}
/*******************************************************************************
** Fluent setter for code
*******************************************************************************/
public RetryableServerErrorException withCode(Integer code)
{
this.code = code;
return (this);
}
}

View File

@ -124,9 +124,16 @@ class BaseAPIActionUtilTest extends BaseTest
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
//////////////////////////
// set to retry 3 times //
//////////////////////////
for(int i = 0; i < 4; i++)
{
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
}
CountInput countInput = new CountInput();
countInput.setTableName(TestUtils.MOCK_TABLE_NAME);
@ -290,9 +297,16 @@ class BaseAPIActionUtilTest extends BaseTest
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
//////////////////////////
// set to retry 3 times //
//////////////////////////
for(int i = 0; i < 4; i++)
{
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
}
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.MOCK_TABLE_NAME);
@ -344,9 +358,16 @@ class BaseAPIActionUtilTest extends BaseTest
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
//////////////////////////
// set to retry 3 times //
//////////////////////////
for(int i = 0; i < 4; i++)
{
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
}
InsertInput insertInput = new InsertInput();
insertInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse")));
@ -411,9 +432,13 @@ class BaseAPIActionUtilTest extends BaseTest
// avoid the fully mocked makeRequest //
////////////////////////////////////////
mockApiUtilsHelper.setUseMock(false);
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
for(int i = 0; i < 4; i++)
{
mockApiUtilsHelper.enqueueMockResponse(new QHttpResponse().withStatusCode(500).withContent("""
{"error": "Server error"}
"""));
}
UpdateInput updateInput = new UpdateInput();
updateInput.setRecords(List.of(new QRecord().withValue("name", "Milhouse")));
@ -682,4 +707,4 @@ class BaseAPIActionUtilTest extends BaseTest
return (new GetAction().execute(getInput));
}
}
}