[Enhancement] Implement ProfileActionV2 and QueryDetailActionV2 to obtain query information across all FEs (#61345)

Signed-off-by: zhaohehuhu <luoyedeyi@163.com>
This commit is contained in:
He Zhao 2025-09-16 13:47:50 +08:00 committed by GitHub
parent 4bb51cbd9e
commit a79d85173b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 991 additions and 1 deletions

View File

@ -132,6 +132,20 @@ public class BaseRequest {
return params.get(key);
}
public String getSingleParameter(String key, String defaultValue) {
String uri = request.uri();
if (decoder == null) {
decoder = new QueryStringDecoder(uri);
}
List<String> values = decoder.parameters().get(key);
if (values != null && !values.isEmpty()) {
return values.get(0);
}
return params.get(key) != null ? params.get(key) : defaultValue;
}
public String getContent() throws DdlException {
if (request instanceof FullHttpRequest) {
FullHttpRequest fullHttpRequest = (FullHttpRequest) request;

View File

@ -98,6 +98,8 @@ import com.starrocks.http.rest.TableRowCountAction;
import com.starrocks.http.rest.TableSchemaAction;
import com.starrocks.http.rest.TransactionLoadAction;
import com.starrocks.http.rest.TriggerAction;
import com.starrocks.http.rest.v2.ProfileActionV2;
import com.starrocks.http.rest.v2.QueryDetailActionV2;
import com.starrocks.http.rest.v2.TablePartitionAction;
import com.starrocks.metric.GaugeMetric;
import com.starrocks.metric.GaugeMetricImpl;
@ -223,8 +225,10 @@ public class HttpServer {
ColocateMetaService.UpdateGroupAction.registerAction(controller);
GlobalDictMetaService.ForbitTableAction.registerAction(controller);
ProfileAction.registerAction(controller);
ProfileActionV2.registerAction(controller);
QueryProgressAction.registerAction(controller);
QueryDetailAction.registerAction(controller);
QueryDetailActionV2.registerAction(controller);
ConnectionAction.registerAction(controller);
ShowDataAction.registerAction(controller);
QueryDumpAction.registerAction(controller);

View File

@ -0,0 +1,168 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.http;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import javax.net.ssl.SSLContext;
public class HttpUtils {
private static final Logger LOG = LoggerFactory.getLogger(HttpUtils.class);
private static class SingletonHolder {
private static final CloseableHttpClient INSTANCE = getHttpClient();
}
public static CloseableHttpClient getInstance() {
return SingletonHolder.INSTANCE;
}
private static final PoolingHttpClientConnectionManager CLIENT_CONNECTION_MANAGER;
private static CloseableHttpClient getHttpClient() {
Objects.requireNonNull(CLIENT_CONNECTION_MANAGER, "clientConnectionManager is not initialized");
RequestConfig requestConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES)
.setExpectContinueEnabled(Boolean.TRUE)
.setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.NTLM, AuthSchemes.DIGEST, AuthSchemes.SPNEGO))
.setProxyPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC, AuthSchemes.SPNEGO))
.setConnectTimeout(5000)
.setSocketTimeout(5000)
.setConnectionRequestTimeout(5000)
.setRedirectsEnabled(true)
.build();
return HttpClients.custom()
.setConnectionManager(CLIENT_CONNECTION_MANAGER)
.setDefaultRequestConfig(requestConfig)
.setRetryHandler(new DefaultHttpRequestRetryHandler(5, false))
.build();
}
static {
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = null;
try {
SSLContextBuilder builder = new SSLContextBuilder();
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLContext sslContext = builder.build();
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> socketFactoryRegistry =
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", socketFactory)
.build();
poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(50);
poolingHttpClientConnectionManager.setMaxTotal(100);
} catch (NoSuchAlgorithmException e) {
LOG.error("Got NoSuchAlgorithmException when SSLContext init", e);
} catch (KeyManagementException e) {
LOG.error("Got KeyManagementException when SSLContext init", e);
} catch (KeyStoreException e) {
LOG.error("Got KeyStoreException when SSLContext init", e);
}
CLIENT_CONNECTION_MANAGER = poolingHttpClientConnectionManager;
LOG.info(" initial http client successfully");
}
public static String get(String uri, Map<String, String> header) throws Exception {
HttpGet httpGet = new HttpGet(uri);
addHeaders(httpGet, header);
return executeRequest(uri, httpGet, null);
}
public static String post(String uri, AbstractHttpEntity entity, Map<String, String> header) throws Exception {
HttpPost httpPost = new HttpPost(uri);
httpPost.setEntity(entity);
addHeaders(httpPost, header);
return executeRequest(uri, httpPost, entity);
}
private static void addHeaders(HttpRequestBase request, Map<String, String> headers) {
if (headers != null && !headers.isEmpty()) {
headers.forEach(request::addHeader);
}
}
private static String executeRequest(String uri,
HttpUriRequest request,
AbstractHttpEntity entity) throws HttpRequestException {
CloseableHttpClient httpClient = getInstance();
if (Objects.isNull(httpClient)) {
LOG.error("HttpClient is null for uri: {}", uri);
throw new HttpRequestException("HttpClient is null for uri: " + uri);
}
try (CloseableHttpResponse response = httpClient.execute(request)) {
int code = response.getStatusLine().getStatusCode();
String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (code == HttpStatus.SC_OK) {
return result;
} else {
if (entity != null) {
String msg = String.format("Http request failed, url=%s, code=%d, body=%s, response=%s",
uri, code, entity, result);
LOG.error(msg);
throw new HttpRequestException(msg);
} else {
String msg = String.format("Http request failed, url=%s, code=%d, response=%s",
uri, code, result);
LOG.error(msg);
throw new HttpRequestException(msg);
}
}
} catch (IOException e) {
String msg = "Http request exception, uri=" + uri;
LOG.error(msg, e);
throw new HttpRequestException(msg, e);
}
}
}

View File

@ -37,9 +37,12 @@ package com.starrocks.http.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.starrocks.authorization.AccessDeniedException;
import com.starrocks.authorization.AuthorizationMgr;
import com.starrocks.catalog.UserIdentity;
import com.starrocks.common.Config;
import com.starrocks.common.DdlException;
import com.starrocks.common.ErrorCode;
import com.starrocks.common.Pair;
@ -50,13 +53,18 @@ import com.starrocks.http.BaseAction;
import com.starrocks.http.BaseRequest;
import com.starrocks.http.BaseResponse;
import com.starrocks.http.HttpConnectContext;
import com.starrocks.http.HttpUtils;
import com.starrocks.http.WebUtils;
import com.starrocks.qe.ConnectContext;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.system.Frontend;
import com.starrocks.thrift.TNetworkAddress;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.HttpHeaders;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -65,6 +73,7 @@ import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
public class RestBaseAction extends BaseAction {
@ -292,4 +301,88 @@ public class RestBaseAction extends BaseAction {
});
}
public static List<String> fetchResultFromOtherFrontendNodes(String queryPath,
String authorization,
HttpMethod method,
Boolean returnMultipleResults) {
List<Pair<String, Integer>> frontends = getOtherAliveFe();
if (frontends.isEmpty()) {
return List.of();
}
ImmutableMap<String, String> header = ImmutableMap.<String, String>builder()
.put(HttpHeaders.AUTHORIZATION, authorization).build();
List<String> result = Lists.newArrayList();
for (Pair<String, Integer> front : frontends) {
String url = String.format("http://%s:%d%s", front.first, front.second, queryPath);
try {
String data = null;
if (method == HttpMethod.GET) {
data = HttpUtils.get(url, header);
} else if (method == HttpMethod.POST) {
data = HttpUtils.post(url, null, header);
}
if (StringUtils.isNotBlank(data)) {
result.add(data);
}
if (!returnMultipleResults && !result.isEmpty()) {
return result;
}
} catch (Exception e) {
LOG.error("request url {} error", url, e);
}
}
return result;
}
public static List<Pair<String, Integer>> getAllAliveFe() {
if (GlobalStateMgr.getCurrentState() == null) {
return List.of();
} else {
return GlobalStateMgr.getCurrentState()
.getNodeMgr()
.getAllFrontends()
.stream()
.filter(Frontend::isAlive)
.map(fe -> new Pair<>(fe.getHost(), Config.http_port))
.collect(Collectors.toList());
}
}
public static Pair<String, Integer> getCurrentFe() {
if (GlobalStateMgr.getCurrentState() == null) {
return null;
} else {
return GlobalStateMgr.getCurrentState()
.getNodeMgr()
.getSelfNode();
}
}
public static List<Pair<String, Integer>> getOtherAliveFe() {
List<Pair<String, Integer>> allAliveFe = getAllAliveFe();
if (allAliveFe.isEmpty()) {
return List.of();
}
Pair<String, Integer> currentFe = getCurrentFe();
if (currentFe == null) {
return List.of();
}
String currentFeAddress = currentFe.first;
return allAliveFe.stream()
.filter(fe -> !fe.first.equals(currentFeAddress))
.collect(Collectors.toList());
}
protected void sendSuccessResponse(BaseResponse response, String content, BaseRequest request) {
response.getContent().append(content);
sendResult(request, response);
}
protected void sendErrorResponse(BaseResponse response, String message, HttpResponseStatus status, BaseRequest request) {
response.getContent().append(message);
sendResult(request, response, status);
}
}

View File

@ -0,0 +1,84 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.http.rest.v2;
import com.starrocks.common.util.ProfileManager;
import com.starrocks.http.ActionController;
import com.starrocks.http.BaseRequest;
import com.starrocks.http.BaseResponse;
import com.starrocks.http.IllegalArgException;
import com.starrocks.http.rest.RestBaseAction;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.util.List;
// This class is a RESTFUL interface to get query profile from all frontend nodes.
// Usage:
// wget http://fe_host:fe_http_port/api/v2/profile?query_id=123456&is_request_all_frontend=true;
public class ProfileActionV2 extends RestBaseAction {
private static final String QUERY_PLAN_URI = "/api/v2/profile?query_id=%s";
public ProfileActionV2(ActionController controller) {
super(controller);
}
public static void registerAction(ActionController controller) throws IllegalArgException {
controller.registerHandler(HttpMethod.GET, "/api/v2/profile", new ProfileActionV2(controller));
}
@Override
protected void executeWithoutPassword(BaseRequest request, BaseResponse response) {
String authorization = request.getAuthorizationHeader();
String queryId = request.getSingleParameter("query_id");
String isRequestAllStr = request.getSingleParameter("is_request_all_frontend", "false");
boolean isRequestAll = Boolean.parseBoolean(isRequestAllStr);
if (queryId == null || queryId.isEmpty()) {
sendErrorResponse(response,
"Invalid parameter: query_id",
HttpResponseStatus.BAD_REQUEST,
request);
return;
}
String queryProfileStr = ProfileManager.getInstance().getProfile(queryId);
if (queryProfileStr != null) {
sendSuccessResponse(response, queryProfileStr, request);
return;
}
if (isRequestAll) {
// If the query profile is not found in the local fe's ProfileManager,
// we will query other frontend nodes to get the query profile.
String queryPath = String.format(QUERY_PLAN_URI, queryId);
List<String> profileList = fetchResultFromOtherFrontendNodes(queryPath, authorization, HttpMethod.GET, false);
for (String profile : profileList) {
if (profile != null) {
sendSuccessResponse(response, profile, request);
return;
}
}
}
sendErrorResponse(response,
String.format("Query id %s not found.", queryId),
HttpResponseStatus.NOT_FOUND,
request);
}
}

View File

@ -0,0 +1,97 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.http.rest.v2;
import com.google.gson.Gson;
import com.starrocks.http.ActionController;
import com.starrocks.http.BaseRequest;
import com.starrocks.http.BaseResponse;
import com.starrocks.http.IllegalArgException;
import com.starrocks.http.rest.RestBaseAction;
import com.starrocks.qe.QueryDetail;
import com.starrocks.qe.QueryDetailQueue;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.util.Arrays;
import java.util.List;
// This class is a RESTFUL interface to get query profile from all frontend nodes.
// Usage:
// wget http://fe_host:fe_http_port/api/v2/query_detail?user=root&event_time=1753671088&is_request_all_frontend=true;
public class QueryDetailActionV2 extends RestBaseAction {
private static final String QUERY_PLAN_URI = "/api/v2/query_detail";
public QueryDetailActionV2(ActionController controller) {
super(controller);
}
public static void registerAction(ActionController controller) throws IllegalArgException {
controller.registerHandler(HttpMethod.GET, QUERY_PLAN_URI, new QueryDetailActionV2(controller));
}
@Override
public void executeWithoutPassword(BaseRequest request, BaseResponse response) {
String authorization = request.getAuthorizationHeader();
String eventTimeStr = request.getSingleParameter("event_time");
String user = request.getSingleParameter("user");
String isRequestAllStr = request.getSingleParameter("is_request_all_frontend", "false");
boolean isRequestAll = Boolean.parseBoolean(isRequestAllStr);
if (eventTimeStr == null || eventTimeStr.isEmpty()) {
sendErrorResponse(response,
"not valid parameter",
HttpResponseStatus.BAD_REQUEST,
request);
return;
}
long eventTime = Long.parseLong(eventTimeStr.trim());
List<QueryDetail> queryDetails;
if (user != null && !user.isEmpty()) {
// If user is specified, we will filter the query details by user.
queryDetails = QueryDetailQueue.getQueryDetailsAfterTime(eventTime, user);
} else {
// If user is not specified, we will get all query details after the specified event time
queryDetails = QueryDetailQueue.getQueryDetailsAfterTime(eventTime);
}
if (isRequestAll) {
// If the query profile is not found in the local fe's ProfileManager,
// we will query other frontend nodes to get the query profile.
String queryPath = QUERY_PLAN_URI + "?event_time=" + eventTimeStr;
if (user != null && !user.isEmpty()) {
queryPath += "&user=" + user;
}
List<String> dataList = fetchResultFromOtherFrontendNodes(queryPath, authorization, HttpMethod.GET, true);
if (dataList != null) {
for (String data : dataList) {
if (data != null) {
Gson gson = new Gson();
QueryDetail[] queryDetailArray = gson.fromJson(data, QueryDetail[].class);
queryDetails.addAll(Arrays.asList(queryDetailArray));
}
}
}
}
Gson gson = new Gson();
String jsonString = gson.toJson(queryDetails);
response.getContent().append(jsonString);
sendResult(request, response);
}
}

View File

@ -82,6 +82,16 @@ public class QueryDetailQueue {
return results;
}
public static List<QueryDetail> getQueryDetailsAfterTime(long eventTime, String user) {
List<QueryDetail> results = Lists.newArrayList();
for (QueryDetail queryDetail : TOTAL_QUERIES) {
if (queryDetail.getEventTime() > eventTime && queryDetail.getUser().equalsIgnoreCase(user)) {
results.add(queryDetail);
}
}
return results;
}
public static long getTotalQueriesCount() {
return TOTAL_QUERIES.size();
}

View File

@ -0,0 +1,119 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.http;
import mockit.Mock;
import mockit.MockUp;
import org.apache.http.HttpHeaders;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class HttpUtilsTest extends StarRocksHttpTestCase {
private static final String QUERY_PLAN_URI = "/system";
@Test
public void testGetHttpClient() {
CloseableHttpClient httpClient1 = HttpUtils.getInstance();
CloseableHttpClient httpClient2 = HttpUtils.getInstance();
Assertions.assertEquals(httpClient1, httpClient2);
}
@Test
public void testHttpGet() throws Exception {
Map<String, String> header = Map.of(HttpHeaders.AUTHORIZATION, rootAuth);
String url = "http://localhost:" + HTTP_PORT + QUERY_PLAN_URI + "?path=/backends";
String result = HttpUtils.get(url, header);
Assertions.assertNotNull(result);
}
@Test
public void testHttpPostFails() {
Map<String, String> header = Map.of(HttpHeaders.AUTHORIZATION, rootAuth);
String url = URI + "/_query_plan";
StringEntity entity = new StringEntity(
"{ \"sql\" : \" select k1 as alias_1,k2 from " + DB_NAME + "." + TABLE_NAME + " \" }",
StandardCharsets.UTF_8);
Assertions.assertThrows(HttpRequestException.class, () -> {
HttpUtils.post(url, entity, header);
});
}
@Test
public void testHttpPostFailsWithoutEntity() {
Map<String, String> header = Map.of(HttpHeaders.AUTHORIZATION, rootAuth);
String url = URI + "/_query_plan";
StringEntity entity = null;
Assertions.assertThrows(HttpRequestException.class, () -> {
HttpUtils.post(url, entity, header);
});
}
@Test
public void testHttpPostFailsWithIOException() {
new MockUp<CloseableHttpClient>() {
@Mock
public CloseableHttpResponse execute(
final HttpUriRequest request) throws IOException, ClientProtocolException {
throw new IOException("http execute failed");
}
};
Map<String, String> header = Map.of(HttpHeaders.AUTHORIZATION, rootAuth);
String url = URI + "/_query_plan";
StringEntity entity = null;
Assertions.assertThrows(HttpRequestException.class, () -> {
HttpUtils.post(url, entity, header);
});
}
@Test
public void testHttpPostFailsWhenHttpClientUnavailable() {
new MockUp<HttpUtils>() {
@Mock
public static CloseableHttpClient getInstance() {
return null;
}
};
Map<String, String> header = Map.of(HttpHeaders.AUTHORIZATION, rootAuth);
String url = URI + "/_query_plan";
StringEntity entity = new StringEntity(
"{ \"sql\" : \" select k1 as alias_1,k2 from " + DB_NAME + "." + TABLE_NAME + " \" }",
StandardCharsets.UTF_8);
Assertions.assertThrows(HttpRequestException.class, () -> {
HttpUtils.post(url, entity, header);
});
}
@Test
public void testGetInstance() {
CloseableHttpClient client1 = HttpUtils.getInstance();
CloseableHttpClient client2 = HttpUtils.getInstance();
Assertions.assertSame(client1, client2);
}
}

View File

@ -14,7 +14,13 @@
package com.starrocks.http;
import com.google.common.collect.Lists;
import com.starrocks.common.Config;
import com.starrocks.common.Pair;
import com.starrocks.ha.FrontendNodeType;
import com.starrocks.http.rest.RestBaseAction;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.system.Frontend;
import com.starrocks.thrift.TNetworkAddress;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
@ -23,10 +29,15 @@ import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import mockit.Expectations;
import mockit.Mock;
import mockit.MockUp;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.util.List;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@ -85,4 +96,81 @@ public class RestBaseActionTest {
restBaseAction.redirectTo(mockRequest, mockResponse, mockAddr);
verify(mockResponse).updateHeader(HttpHeaderNames.LOCATION.toString(), asciiUri);
}
@Test
public void testGetOtherAliveFronts() {
new MockUp<RestBaseAction>() {
@Mock
public static List<Pair<String, Integer>> getAllAliveFe() {
return List.of(
new Pair<>("fe1", 8030),
new Pair<>("fe2", 8031)
);
}
@Mock
public static Pair<String, Integer> getCurrentFe() {
return new Pair<>("fe1", 8030);
}
};
List<Pair<String, Integer>> fronts = restBaseAction.getOtherAliveFe();
Assertions.assertEquals(fronts.size(), 1);
}
@Test
public void verifyGetEmptyOtherAliveFronts() {
new Expectations(GlobalStateMgr.getCurrentState().getNodeMgr()) {
{
GlobalStateMgr.getCurrentState();
minTimes = 1;
result = null;
}
};
List<Pair<String, Integer>> fronts = restBaseAction.getOtherAliveFe();
Assertions.assertEquals(fronts.size(), 0);
}
@Test
public void verifyGetNullCurrentFe() {
new Expectations(GlobalStateMgr.getCurrentState()) {
{
GlobalStateMgr.getCurrentState();
minTimes = 1;
result = null;
}
};
Pair<String, Integer> currentFe = restBaseAction.getCurrentFe();
Assertions.assertNull(currentFe);
}
@Test
public void testGetAllAliveFe() {
Frontend frontend = new Frontend(0, FrontendNodeType.LEADER, "", "localhost", 0);
frontend.setAlive(true);
new Expectations(GlobalStateMgr.getCurrentState().getNodeMgr()) {
{
GlobalStateMgr.getCurrentState().getNodeMgr().getAllFrontends();
minTimes = 1;
result = Lists.newArrayList(frontend);
}
};
List<Pair<String, Integer>> result = restBaseAction.getAllAliveFe();
Assertions.assertEquals(1, result.size());
Assertions.assertEquals(frontend.getHost(), result.get(0).first);
Assertions.assertEquals(Config.http_port, result.get(0).second);
}
}

View File

@ -545,7 +545,8 @@ public abstract class StarRocksHttpTestCase {
}
};
Frontend frontend = new Frontend(0, FrontendNodeType.LEADER, "", "", 0);
Frontend frontend = new Frontend(0, FrontendNodeType.LEADER, "", "localhost", 0);
frontend.setAlive(true);
new Expectations(nodeMgr) {
{
nodeMgr.getClusterInfo();

View File

@ -0,0 +1,167 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.http.rest.v2;
import com.starrocks.common.Pair;
import com.starrocks.common.util.ProfileManager;
import com.starrocks.ha.FrontendNodeType;
import com.starrocks.http.StarRocksHttpTestCase;
import com.starrocks.http.rest.RestBaseAction;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.system.Frontend;
import mockit.Expectations;
import mockit.Mock;
import mockit.MockUp;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ProfileActionV2Test extends StarRocksHttpTestCase {
private static final String QUERY_PLAN_URI = "/api/v2/profile";
@Test
public void testQueryProfile() throws IOException {
Request request = new Request.Builder()
.get()
.addHeader("Authorization", rootAuth)
.url("http://localhost:" + HTTP_PORT + QUERY_PLAN_URI + "?query_id=eaff21d2-3734-11ee-909f-8e20563011de")
.build();
Response response = networkClient.newCall(request).execute();
String respStr = response.body().string();
Assertions.assertTrue(respStr.contains("Query id eaff21d2-3734-11ee-909f-8e20563011de not found."));
}
@Test
public void testQueryProfileFromLeaderFront() throws Exception {
new MockUp<ProfileManager>() {
@Mock
public String getProfile(String queryId) {
if (queryId.equalsIgnoreCase("eaff21d2-3734-11ee-909f-8e20563011de")) {
String queryProfileStr = "Query:\n" +
" Summary:\n" +
" - Query ID: eaff21d2-3734-11ee-909f-8e20563011de\n" +
" - Start Time: 2023-08-10 12:18:11\n" +
" - End Time: 2023-08-10 12:18:11\n" +
" - Total: 150ms\n" +
" - Query Type: Query\n" +
" - Query State: Finished\n" +
" - StarRocks Version: bugfix2-0da335ff34\n" +
" - User: root\n" +
" - Test: a<b<c\n" +
" - Default Db: ssb\n" +
" - Sql Statement: select count(s_suppkey), count(s_name), count(s_address), count(s_city), " +
"count(s_nation), count(s_region), count(s_phone), count(lo_revenue), count(lo_shipmode), " +
"count(lo_quantity), count(lo_partkey), count(lo_discount) from lineorder join supplier on " +
"lo_suppkey=s_suppkey and lo_partkey<s_suppkey and lo_quantity>100\n" +
" - Variables: parallel_fragment_exec_instance_num=1,max_parallel_scan_instance_num=-1," +
"pipeline_dop=0,enable_adaptive_sink_dop=true,enable_runtime_adaptive_dop=false," +
"runtime_profile_report_interval=10\n" +
" - Collect Profile Time: 41ms";
return queryProfileStr;
}
return null;
}
};
Request request = new Request.Builder()
.get()
.addHeader("Authorization", rootAuth)
.url("http://localhost:" + HTTP_PORT + QUERY_PLAN_URI + "?query_id=eaff21d2-3734-11ee-909f-8e20563011de"
+ "&is_request_all_frontend=true")
.build();
Response response = networkClient.newCall(request).execute();
String respStr = response.body().string();
Assertions.assertTrue(respStr.contains("Query ID: eaff21d2-3734-11ee-909f-8e20563011de"));
}
@Test
public void testQueryProfileFromFronts() throws Exception {
Frontend frontend = new Frontend(0, FrontendNodeType.LEADER, "", "localhost", 0);
new Expectations(GlobalStateMgr.getCurrentState().getNodeMgr()) {
{
GlobalStateMgr.getCurrentState().getNodeMgr().getSelfNode();
minTimes = 1;
result = new Pair<>(frontend.getHost(), HTTP_PORT);
}
};
new MockUp<RestBaseAction>() {
@Mock
public static List<Pair<String, Integer>> getOtherAliveFe() {
Pair<String, Integer> frontNode = GlobalStateMgr.getCurrentState()
.getNodeMgr()
.getSelfNode();
List<Pair<String, Integer>> frontNodes = new ArrayList<>();
frontNodes.add(frontNode);
return frontNodes;
}
};
new MockUp<ProfileManager>() {
int callCount = 0;
@Mock
public String getProfile(String queryId) {
if (callCount <= 0) {
callCount++;
// Simulate that the profile is not found in the local ProfileManager
return null;
}
String queryProfileStr = "Query:\n" +
" Summary:\n" +
" - Query ID: eaff21d2-3734-11ee-909f-8e20563011de\n" +
" - Start Time: 2023-08-10 12:18:11\n" +
" - End Time: 2023-08-10 12:18:11\n" +
" - Total: 150ms\n" +
" - Query Type: Query\n" +
" - Query State: Finished\n" +
" - StarRocks Version: bugfix2-0da335ff34\n" +
" - User: root\n" +
" - Test: a<b<c\n" +
" - Default Db: ssb\n" +
" - Sql Statement: select count(s_suppkey), count(s_name), count(s_address), count(s_city), " +
"count(s_nation), count(s_region), count(s_phone), count(lo_revenue), count(lo_shipmode), " +
"count(lo_quantity), count(lo_partkey), count(lo_discount) from lineorder join supplier on " +
"lo_suppkey=s_suppkey and lo_partkey<s_suppkey and lo_quantity>100\n" +
" - Variables: parallel_fragment_exec_instance_num=1,max_parallel_scan_instance_num=-1," +
"pipeline_dop=0,enable_adaptive_sink_dop=true,enable_runtime_adaptive_dop=false," +
"runtime_profile_report_interval=10\n" +
" - Collect Profile Time: 41ms";
return queryProfileStr;
}
};
Request request = new Request.Builder()
.get()
.addHeader("Authorization", rootAuth)
.url("http://localhost:" + HTTP_PORT + QUERY_PLAN_URI + "?query_id=eaff21d2-3734-11ee-909f-8e20563011de"
+ "&is_request_all_frontend=true")
.build();
Response response = networkClient.newCall(request).execute();
String respStr = response.body().string();
Assertions.assertTrue(respStr.contains("Query ID: eaff21d2-3734-11ee-909f-8e20563011de"));
}
}

View File

@ -0,0 +1,145 @@
// Copyright 2021-present StarRocks, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.starrocks.http.rest.v2;
import com.google.gson.Gson;
import com.starrocks.common.Pair;
import com.starrocks.ha.FrontendNodeType;
import com.starrocks.http.StarRocksHttpTestCase;
import com.starrocks.http.rest.RestBaseAction;
import com.starrocks.qe.QueryDetail;
import com.starrocks.qe.QueryDetailQueue;
import com.starrocks.server.GlobalStateMgr;
import com.starrocks.system.Frontend;
import mockit.Expectations;
import mockit.Mock;
import mockit.MockUp;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class QueryDetailV2Test extends StarRocksHttpTestCase {
private static final String QUERY_PLAN_URI = "/api/v2/query_detail";
@Test
public void testQueryDetail() throws IOException {
Request request = new Request.Builder()
.get()
.addHeader("Authorization", rootAuth)
.url("http://localhost:" + HTTP_PORT + QUERY_PLAN_URI + "?event_time=1753671088")
.build();
Response response = networkClient.newCall(request).execute();
String respStr = response.body().string();
Assertions.assertNotNull(respStr);
}
@Test
public void testQueryDetailFromLeaderFront() throws IOException {
QueryDetail startQueryDetail = new QueryDetail("219a2d5443c542d4-8fc938db37c892e3", false, 1, "127.0.0.1",
System.currentTimeMillis(), -1, -1, QueryDetail.QueryMemState.RUNNING,
"testDb", "select * from table1 limit 1",
"root", "", "default_catalog");
startQueryDetail.setScanRows(100);
startQueryDetail.setScanBytes(10001);
startQueryDetail.setReturnRows(1);
startQueryDetail.setCpuCostNs(1002);
startQueryDetail.setMemCostBytes(100003);
QueryDetailQueue.addQueryDetail(startQueryDetail);
long eventTime = startQueryDetail.getEventTime() - 1;
Request request = new Request.Builder()
.get()
.addHeader("Authorization", rootAuth)
.url("http://localhost:" + HTTP_PORT + QUERY_PLAN_URI + "?event_time=" + eventTime)
.build();
Response response = networkClient.newCall(request).execute();
String respStr = response.body().string();
Assertions.assertTrue(respStr.contains("219a2d5443c542d4-8fc938db37c892e3"));
}
@Test
public void testQueryDetailFromFronts() throws IOException {
Frontend frontend = new Frontend(0, FrontendNodeType.LEADER, "", "localhost", 0);
new Expectations(GlobalStateMgr.getCurrentState().getNodeMgr()) {
{
GlobalStateMgr.getCurrentState().getNodeMgr().getSelfNode();
minTimes = 1;
result = new Pair<>(frontend.getHost(), HTTP_PORT);
}
};
QueryDetail startQueryDetail = new QueryDetail("219a2d5443c542d4-8fc938db37c892e3", false, 1, "127.0.0.1",
System.currentTimeMillis(), -1, -1, QueryDetail.QueryMemState.RUNNING,
"testDb", "select * from table1 limit 1",
"root", "", "default_catalog");
startQueryDetail.setScanRows(100);
startQueryDetail.setScanBytes(10001);
startQueryDetail.setReturnRows(1);
startQueryDetail.setCpuCostNs(1002);
startQueryDetail.setMemCostBytes(100003);
QueryDetailQueue.addQueryDetail(startQueryDetail);
new MockUp<RestBaseAction>() {
@Mock
public static List<Pair<String, Integer>> getOtherAliveFe() {
QueryDetail startQueryDetail = new QueryDetail("219a2d5443c542d4-8fc938db37c892e5", false, 1, "127.0.0.1",
System.currentTimeMillis(), -1, -1, QueryDetail.QueryMemState.RUNNING,
"testDb", "select * from table2 limit 1",
"root", "", "default_catalog");
startQueryDetail.setScanRows(150);
startQueryDetail.setScanBytes(10005);
startQueryDetail.setReturnRows(1);
startQueryDetail.setCpuCostNs(1005);
startQueryDetail.setMemCostBytes(100005);
QueryDetailQueue.addQueryDetail(startQueryDetail);
Pair<String, Integer> frontNode = GlobalStateMgr.getCurrentState()
.getNodeMgr()
.getSelfNode();
List<Pair<String, Integer>> frontNodes = new ArrayList<>();
frontNodes.add(frontNode);
return frontNodes;
}
};
long eventTime = startQueryDetail.getEventTime() - 1;
Request request = new Request.Builder()
.get()
.addHeader("Authorization", rootAuth)
.url("http://localhost:" + HTTP_PORT + QUERY_PLAN_URI +
"?event_time=" + eventTime +
"&user=root" +
"&is_request_all_frontend=true")
.build();
Response response = networkClient.newCall(request).execute();
String respStr = Objects.requireNonNull(response.body()).string();
Gson gson = new Gson();
QueryDetail[] details = gson.fromJson(respStr, QueryDetail[].class);
Assertions.assertEquals(3, details.length);
}
}