Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: honor timeout set for REST API calls #345

Merged
merged 2 commits into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions deploy/docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Image can be configured by setting environment variables.
| `ENCRYPTION_SALT` | Salt used for encrypting password | `lowcoder.org` |
| `CORS_ALLOWED_DOMAINS` | CORS allowed domains | `*` |
| `LOWCODER_MAX_REQUEST_SIZE` | Lowcoder max request size | `20m` |
| `LOWCODER_MAX_QUERY_TIMEOUT` | Lowcoder max query timeout (in seconds) | `120` |
| `LOWCODER_API_SERVICE_URL` | Lowcoder API service URL | `http://localhost:8080` |
| `LOWCODER_NODE_SERVICE_URL` | Lowcoder Node service (js executor) URL | `http://localhost:6060` |
| `DEFAULT_ORGS_PER_USER` | Default maximum organizations per user | `100` |
Expand Down Expand Up @@ -77,6 +78,8 @@ Image can be configured by setting environment variables.
| `DEFAULT_ORG_GROUP_COUNT` | Default maximum groups per organization | `100` |
| `DEFAULT_ORG_APP_COUNT` | Default maximum applications per organization | `1000` |
| `DEFAULT_DEVELOPER_COUNT` | Default maximum developers | `100` |
| `LOWCODER_MAX_QUERY_TIMEOUT` | Lowcoder max query timeout (in seconds) | `120` |
| `LOWCODER_MAX_REQUEST_SIZE` | Lowcoder max request size | `20m` |



Expand Down Expand Up @@ -122,6 +125,7 @@ Image can be configured by setting environment variables.
| --------------------------------| --------------------------------------------------------------------| ------------------------------------------------------- |
| `PUID` | ID of user running services. It will own all created logs and data. | `9001` |
| `PGID` | ID of group of the user running services. | `9001` |
| `LOWCODER_MAX_QUERY_TIMEOUT` | Lowcoder max query timeout (in seconds) | `120` |
| `LOWCODER_MAX_REQUEST_SIZE` | Lowcoder max request size | `20m` |
| `LOWCODER_API_SERVICE_URL` | Lowcoder API service URL | `http://localhost:8080` |
| `LOWCODER_NODE_SERVICE_URL` | Lowcoder Node service (js executor) URL | `http://localhost:6060` |
Expand Down
2 changes: 2 additions & 0 deletions deploy/docker/docker-compose-multi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ services:
MONGODB_URL: "mongodb://lowcoder:secret123@mongodb/lowcoder?authSource=admin"
REDIS_URL: "redis://redis:6379"
LOWCODER_NODE_SERVICE_URL: "http://lowcoder-node-service:6060"
LOWCODER_MAX_QUERY_TIMEOUT: 120
ENABLE_USER_SIGN_UP: "true"
ENCRYPTION_PASSWORD: "lowcoder.org"
ENCRYPTION_SALT: "lowcoder.org"
Expand Down Expand Up @@ -76,6 +77,7 @@ services:
PUID: "9001"
PGID: "9001"
LOWCODER_MAX_REQUEST_SIZE: 20m
LOWCODER_MAX_QUERY_TIMEOUT: 120
LOWCODER_API_SERVICE_URL: "http://lowcoder-api-service:8080"
LOWCODER_NODE_SERVICE_URL: "http://lowcoder-node-service:6060"
restart: unless-stopped
Expand Down
1 change: 1 addition & 0 deletions deploy/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ services:
LOWCODER_NODE_SERVICE_URL: "http://localhost:6060"
# frontend parameters
LOWCODER_MAX_REQUEST_SIZE: 20m
LOWCODER_MAX_QUERY_TIMEOUT: 120
volumes:
- ./lowcoder-stacks:/lowcoder-stacks
restart: unless-stopped
Expand Down
1 change: 1 addition & 0 deletions deploy/docker/frontend/01-update-nginx-conf.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ else
ln -s /etc/nginx/nginx-http.conf /etc/nginx/nginx.conf
fi;

sed -i "s@__LOWCODER_MAX_QUERY_TIMEOUT__@${LOWCODER_MAX_QUERY_TIMEOUT:=120}@" /etc/nginx/nginx.conf
sed -i "s@__LOWCODER_MAX_REQUEST_SIZE__@${LOWCODER_MAX_REQUEST_SIZE:=20m}@" /etc/nginx/nginx.conf
sed -i "s@__LOWCODER_API_SERVICE_URL__@${LOWCODER_API_SERVICE_URL:=http://localhost:8080}@" /etc/nginx/nginx.conf
sed -i "s@__LOWCODER_NODE_SERVICE_URL__@${LOWCODER_NODE_SERVICE_URL:=http://localhost:6060}@" /etc/nginx/nginx.conf
Expand Down
3 changes: 3 additions & 0 deletions deploy/docker/frontend/nginx-http.conf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ http {
listen 3000 default_server;
root /lowcoder/client;

proxy_connect_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
proxy_send_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
proxy_read_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;

location / {
try_files $uri /index.html;
Expand Down
4 changes: 4 additions & 0 deletions deploy/docker/frontend/nginx-https.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ http {
include /etc/nginx/ssl-certificate.conf;
include /etc/nginx/ssl-params.conf;

proxy_connect_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
proxy_send_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;
proxy_read_timeout __LOWCODER_MAX_QUERY_TIMEOUT__;

location / {
try_files $uri /index.html;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.lowcoder.domain.plugin.client.DatasourcePluginClient;
import org.lowcoder.domain.plugin.service.DatasourceMetaInfoService;
import org.lowcoder.domain.query.util.QueryTimeoutUtils;
import org.lowcoder.sdk.config.CommonConfig;
import org.lowcoder.sdk.exception.BizException;
import org.lowcoder.sdk.exception.PluginException;
import org.lowcoder.sdk.models.QueryExecutionResult;
Expand All @@ -40,10 +41,14 @@ public class QueryExecutionService {
@Autowired
private DatasourcePluginClient datasourcePluginClient;

@Autowired
private CommonConfig common;

public Mono<QueryExecutionResult> executeQuery(Datasource datasource, Map<String, Object> queryConfig, Map<String, Object> requestParams,
String timeoutStr, QueryVisitorContext queryVisitorContext) {

int timeoutMs = QueryTimeoutUtils.parseQueryTimeoutMs(timeoutStr, requestParams);
int timeoutMs = QueryTimeoutUtils.parseQueryTimeoutMs(timeoutStr, requestParams, common.getMaxQueryTimeout());
queryConfig.putIfAbsent("timeoutMs", timeoutMs);

return Mono.defer(() -> {
if (datasourceMetaInfoService.isJsDatasourcePlugin(datasource.getType())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
public final class QueryTimeoutUtils {

private static final int DEFAULT_QUERY_TIMEOUT_MILLIS = 10000;
private static final int MAX_QUERY_TIMEOUT_SECONDS = 120;

public static int parseQueryTimeoutMs(String timeoutStr, Map<String, Object> paramMap) {
return parseQueryTimeoutMs(renderMustacheString(timeoutStr, paramMap));
public static int parseQueryTimeoutMs(String timeoutStr, Map<String, Object> paramMap, int maxQueryTimeout) {
return parseQueryTimeoutMs(renderMustacheString(timeoutStr, paramMap), maxQueryTimeout);
}

@VisibleForTesting
public static int parseQueryTimeoutMs(String timeoutStr) {
public static int parseQueryTimeoutMs(String timeoutStr, int maxQueryTimeout) {
if (StringUtils.isBlank(timeoutStr)) {
return DEFAULT_QUERY_TIMEOUT_MILLIS;
}
Expand All @@ -44,10 +43,10 @@ public static int parseQueryTimeoutMs(String timeoutStr) {
if (value < 0) {
throw new PluginException(QUERY_ARGUMENT_ERROR, "INVALID_TIMEOUT_SETTING", timeoutStr);
}

int millis = convertToMs(value, unit);
if (millis > Duration.ofSeconds(MAX_QUERY_TIMEOUT_SECONDS).toMillis()) {
throw new PluginException(EXCEED_MAX_QUERY_TIMEOUT, "EXCEED_MAX_QUERY_TIMEOUT", MAX_QUERY_TIMEOUT_SECONDS);
if (millis > Duration.ofSeconds(maxQueryTimeout).toMillis()) {
throw new PluginException(EXCEED_MAX_QUERY_TIMEOUT, "EXCEED_MAX_QUERY_TIMEOUT", maxQueryTimeout);
}

return millis;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,48 @@

package org.lowcoder.plugin.restapi;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;
import lombok.Builder;
import lombok.Getter;
import static com.google.common.base.MoreObjects.firstNonNull;
import static org.apache.commons.collections4.MapUtils.emptyIfNull;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.lowcoder.plugin.restapi.RestApiError.REST_API_EXECUTION_ERROR;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isBinary;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isJson;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isJsonContentType;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isPicture;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.isValidContentType;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.parseContentType;
import static org.lowcoder.sdk.exception.PluginCommonError.JSON_PARSE_ERROR;
import static org.lowcoder.sdk.exception.PluginCommonError.QUERY_ARGUMENT_ERROR;
import static org.lowcoder.sdk.exception.PluginCommonError.QUERY_EXECUTION_ERROR;
import static org.lowcoder.sdk.plugin.restapi.DataUtils.convertToMultiformFileValue;
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.DIGEST_AUTH;
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.OAUTH2_INHERIT_FROM_LOGIN;
import static org.lowcoder.sdk.util.ExceptionUtils.propagateError;
import static org.lowcoder.sdk.util.JsonUtils.readTree;
import static org.lowcoder.sdk.util.JsonUtils.toJsonThrows;
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheJson;
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheString;
import static org.lowcoder.sdk.util.StreamUtils.collectList;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -51,41 +87,29 @@
import org.lowcoder.sdk.query.QueryVisitorContext;
import org.lowcoder.sdk.webclient.WebClientBuildHelper;
import org.pf4j.Extension;
import org.springframework.http.*;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;

import static com.google.common.base.MoreObjects.firstNonNull;
import static org.apache.commons.collections4.MapUtils.emptyIfNull;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.lowcoder.plugin.restapi.RestApiError.REST_API_EXECUTION_ERROR;
import static org.lowcoder.plugin.restapi.helpers.ContentTypeHelper.*;
import static org.lowcoder.sdk.exception.PluginCommonError.*;
import static org.lowcoder.sdk.plugin.restapi.DataUtils.convertToMultiformFileValue;
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.DIGEST_AUTH;
import static org.lowcoder.sdk.plugin.restapi.auth.RestApiAuthType.OAUTH2_INHERIT_FROM_LOGIN;
import static org.lowcoder.sdk.util.ExceptionUtils.propagateError;
import static org.lowcoder.sdk.util.JsonUtils.readTree;
import static org.lowcoder.sdk.util.JsonUtils.toJsonThrows;
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheJson;
import static org.lowcoder.sdk.util.MustacheHelper.renderMustacheString;
import static org.lowcoder.sdk.util.StreamUtils.collectList;
import lombok.Builder;
import lombok.Getter;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

@Extension
public class RestApiExecutor implements QueryExecutor<RestApiDatasourceConfig, Object, RestApiQueryExecutionContext> {
Expand Down Expand Up @@ -176,6 +200,7 @@ public RestApiQueryExecutionContext buildQueryExecutionContext(RestApiDatasource
.authConfig(datasourceConfig.getAuthConfig())
.sslConfig(datasourceConfig.getSslConfig())
.authTokenMono(queryVisitorContext.getAuthTokenMono())
.timeoutMs(queryConfig.getTimeoutMs())
.build();
}

Expand Down Expand Up @@ -235,9 +260,13 @@ public Mono<QueryExecutionResult> executeQuery(Object webClientFilter, RestApiQu
webClientBuilder.filter(new BufferingFilter());
}

HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofMillis(context.getTimeoutMs()));

webClientBuilder.defaultCookies(injectCookies(context));
WebClient client = webClientBuilder
.exchangeStrategies(exchangeStrategies)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

BodyInserter<?, ? super ClientHttpRequest> bodyInserter = buildBodyInserter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@ public class RestApiQueryConfig {
private final List<Property> params;
private final List<Property> headers;
private final List<Property> bodyFormData;
private final long timeoutMs;

@JsonCreator
private RestApiQueryConfig(HttpMethod httpMethod, boolean disableEncodingParams, String body, String path,
List<Property> params, List<Property> headers, List<Property> bodyFormData) {
List<Property> params, List<Property> headers, List<Property> bodyFormData, long timeoutMs) {
this.httpMethod = httpMethod;
this.disableEncodingParams = disableEncodingParams;
this.body = body;
this.path = path;
this.params = params;
this.headers = headers;
this.bodyFormData = bodyFormData;
this.timeoutMs = timeoutMs;
}

public static RestApiQueryConfig from(Map<String, Object> queryConfigs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class RestApiQueryExecutionContext extends QueryExecutionContext {
@Getter
private Mono<List<Property>> authTokenMono;
private SslConfig sslConfig;
private long timeoutMs;

public URI getUri() {
return uri;
Expand Down Expand Up @@ -96,4 +97,8 @@ public AuthConfig getAuthConfig() {
public SslConfig getSslConfig() {
return sslConfig;
}

public long getTimeoutMs() {
return timeoutMs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class CommonConfig {
private String version;
private boolean blockHoundEnable;
private String cookieName;
private int maxQueryTimeout = 300;
private String maxUploadSize = "20MB";
private String maxQueryRequestSize = "20MB";
private String maxQueryResponseSize = "20MB";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ common:
max-query-request-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
max-query-response-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
max-upload-size: ${LOWCODER_MAX_REQUEST_SIZE:20m}
max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120}

material:
mongodb-grid-fs:
Expand Down
Loading