Skip to content

Commit b40f087

Browse files
EdenKikChaho12
authored andcommitted
Modify external routing to configurably pass errors to client
1 parent f38c17c commit b40f087

File tree

9 files changed

+197
-48
lines changed

9 files changed

+197
-48
lines changed

docs/routing-rules.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
Trino Gateway includes a routing rules engine.
44

55
By default, Trino Gateway reads the `X-Trino-Routing-Group` request header to
6-
route requests. If this header is not specified, requests are sent to the
7-
default routing group called `adhoc`.
6+
route requests. Use the `defaultRoutingGroup` setting to specify a fallback group,
7+
defaults to the `adhoc` group.
8+
9+
10+
```yaml
11+
routing:
12+
defaultRoutingGroup: "test-group"
13+
```
814
915
The routing rules engine feature enables you to either write custom logic to
1016
route requests based on the request info such as any of the [request
@@ -40,13 +46,16 @@ routingRules:
4046
excludeHeaders:
4147
- 'Authorization'
4248
- 'Accept-Encoding'
49+
propagateErrors: false
4350
```
4451

4552
* Redirect URLs are not supported.
4653
* Optionally, add headers to the `excludeHeaders` list to exclude requests with
4754
corresponding header values from being sent in the POST request.
4855
* Check headers to exclude when making API requests, specifics depend on the
4956
network configuration.
57+
* Set `propagateErrors` to `true` to forward routing service errors to
58+
clients if present in the response.
5059

5160
If there is error parsing the routing rules configuration file, an error is
5261
logged, and requests are routed using the routing group header
@@ -93,7 +102,7 @@ return a result with the following criteria:
93102
* Response status code of OK (200)
94103
* Message in JSON format
95104
* Only one group can be returned
96-
* If errors is not null, then query would route to default routing group adhoc
105+
* If `errors` is not null, the query is routed to the configured default group
97106

98107
#### Request headers modification
99108

@@ -151,7 +160,7 @@ In addition to the default objects, rules may optionally utilize
151160
, which provide information about the user and query respectively.
152161
You must include an action of the form `result.put(\"routingGroup\", \"foo\")`
153162
to trigger routing of a request that satisfies the condition to the specific
154-
routing group. Without this action, the default adhoc group is used and the
163+
routing group. Without this action, the configured default group is used and the
155164
whole routing rule is redundant.
156165

157166
The condition and actions are written in [MVEL](http://mvel.documentnode.com/),
@@ -184,8 +193,7 @@ You can use the `contains` operator
184193
condition: 'request.getHeader("X-Trino-Client-Tags") contains "label=foo"'
185194
```
186195

187-
If no rules match, then the request is routed to the default `adhoc` routing
188-
group.
196+
If no rules match, then the request is routed to the configured default routing group.
189197

190198
### TrinoStatus
191199

gateway-ha/src/main/java/io/trino/gateway/ha/config/RulesExternalConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class RulesExternalConfiguration
1919
{
2020
private String urlPath;
2121
private List<String> excludeHeaders;
22+
private boolean propagateErrors;
2223

2324
public String getUrlPath()
2425
{
@@ -39,4 +40,14 @@ public void setExcludeHeaders(List<String> excludeHeaders)
3940
{
4041
this.excludeHeaders = excludeHeaders;
4142
}
43+
44+
public boolean isPropagateErrors()
45+
{
46+
return this.propagateErrors;
47+
}
48+
49+
public void setPropagateErrors(boolean propagateErrors)
50+
{
51+
this.propagateErrors = propagateErrors;
52+
}
4253
}

gateway-ha/src/main/java/io/trino/gateway/ha/handler/RoutingTargetHandler.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class RoutingTargetHandler
5454
private static final Logger log = Logger.get(RoutingTargetHandler.class);
5555
private final RoutingManager routingManager;
5656
private final RoutingGroupSelector routingGroupSelector;
57+
private final String defaultRoutingGroup;
5758
private final List<String> statementPaths;
5859
private final List<Pattern> extraWhitelistPaths;
5960
private final boolean requestAnalyserClientsUseV2Format;
@@ -68,6 +69,7 @@ public RoutingTargetHandler(
6869
{
6970
this.routingManager = requireNonNull(routingManager);
7071
this.routingGroupSelector = requireNonNull(routingGroupSelector);
72+
this.defaultRoutingGroup = haGatewayConfiguration.getRouting().getDefaultRoutingGroup();
7173
statementPaths = requireNonNull(haGatewayConfiguration.getStatementPaths());
7274
extraWhitelistPaths = requireNonNull(haGatewayConfiguration.getExtraWhitelistPaths()).stream().map(Pattern::compile).collect(toImmutableList());
7375
requestAnalyserClientsUseV2Format = haGatewayConfiguration.getRequestAnalyzerConfig().isClientsUseV2Format();
@@ -79,15 +81,17 @@ public RoutingTargetResponse resolveRouting(HttpServletRequest request)
7981
{
8082
Optional<String> queryId = extractQueryIdIfPresent(request, statementPaths, requestAnalyserClientsUseV2Format, requestAnalyserMaxBodySize);
8183
Optional<String> previousCluster = getPreviousCluster(queryId, request);
84+
8285
RoutingTargetResponse routingTargetResponse = previousCluster.map(cluster -> {
8386
String routingGroup = queryId.map(routingManager::findRoutingGroupForQueryId)
84-
.orElse("adhoc");
87+
.orElse(defaultRoutingGroup);
8588
String externalUrl = queryId.map(routingManager::findExternalUrlForQueryId)
8689
.orElse(cluster);
8790
return new RoutingTargetResponse(
8891
new RoutingDestination(routingGroup, cluster, buildUriWithNewCluster(cluster, request), externalUrl),
8992
request);
9093
}).orElse(getRoutingTargetResponse(request));
94+
9195
logRewrite(routingTargetResponse.routingDestination().clusterHost(), request);
9296
return routingTargetResponse;
9397
}
@@ -96,10 +100,11 @@ private RoutingTargetResponse getRoutingTargetResponse(HttpServletRequest reques
96100
{
97101
RoutingSelectorResponse routingDestination = routingGroupSelector.findRoutingDestination(request);
98102
String user = request.getHeader(USER_HEADER);
99-
// This falls back on adhoc routing group if there is no cluster found (or value is empty) for the routing group.
100-
String routingGroup = (routingDestination.routingGroup() != null && !routingDestination.routingGroup().isEmpty())
103+
104+
// This falls back on default routing group backend if there is no cluster found for the routing group.
105+
String routingGroup = !isNullOrEmpty(routingDestination.routingGroup())
101106
? routingDestination.routingGroup()
102-
: "adhoc";
107+
: defaultRoutingGroup;
103108
ProxyBackendConfiguration backendConfiguration = routingManager.provideBackendConfiguration(routingGroup, user);
104109
String clusterHost = backendConfiguration.getProxyTo();
105110
String externalUrl = backendConfiguration.getExternalUrl();

gateway-ha/src/main/java/io/trino/gateway/ha/router/ExternalRoutingGroupSelector.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import io.trino.gateway.ha.router.schema.RoutingGroupExternalBody;
3030
import io.trino.gateway.ha.router.schema.RoutingSelectorResponse;
3131
import jakarta.servlet.http.HttpServletRequest;
32+
import jakarta.ws.rs.WebApplicationException;
33+
import jakarta.ws.rs.core.Response;
3234

3335
import java.net.URI;
3436
import java.net.URISyntaxException;
@@ -37,6 +39,7 @@
3739
import java.util.Optional;
3840
import java.util.Set;
3941

42+
import static com.google.common.base.Throwables.throwIfInstanceOf;
4043
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
4144
import static com.google.common.net.MediaType.JSON_UTF_8;
4245
import static io.airlift.http.client.JsonBodyGenerator.jsonBodyGenerator;
@@ -52,6 +55,7 @@ public class ExternalRoutingGroupSelector
5255
private static final Logger log = Logger.get(ExternalRoutingGroupSelector.class);
5356
private final Set<String> excludeHeaders;
5457
private final URI uri;
58+
private final boolean propagateErrors;
5559
private final HttpClient httpClient;
5660
private final RequestAnalyzerConfig requestAnalyzerConfig;
5761
private final TrinoRequestUser.TrinoRequestUserProvider trinoRequestUserProvider;
@@ -67,6 +71,7 @@ public class ExternalRoutingGroupSelector
6771
.add("Content-Length")
6872
.addAll(rulesExternalConfiguration.getExcludeHeaders())
6973
.build();
74+
propagateErrors = rulesExternalConfiguration.isPropagateErrors();
7075

7176
this.requestAnalyzerConfig = requestAnalyzerConfig;
7277
trinoRequestUserProvider = new TrinoRequestUser.TrinoRequestUserProvider(requestAnalyzerConfig);
@@ -101,7 +106,13 @@ public RoutingSelectorResponse findRoutingDestination(HttpServletRequest servlet
101106
throw new RuntimeException("Unexpected response: null");
102107
}
103108
else if (response.errors() != null && !response.errors().isEmpty()) {
104-
throw new RuntimeException("Response with error: " + String.join(", ", response.errors()));
109+
if (propagateErrors) {
110+
log.warn("Query validation failed with errors: %s", String.join(", ", response.errors()));
111+
throw new WebApplicationException(
112+
Response.status(Response.Status.BAD_REQUEST)
113+
.entity(response.errors())
114+
.build());
115+
}
105116
}
106117

107118
// Filter out excluded headers and null values
@@ -120,6 +131,7 @@ else if (response.errors() != null && !response.errors().isEmpty()) {
120131
return new RoutingSelectorResponse(response.routingGroup(), filteredHeaders);
121132
}
122133
catch (Exception e) {
134+
throwIfInstanceOf(e, WebApplicationException.class);
123135
log.error(e, "Error occurred while retrieving routing group "
124136
+ "from external routing rules processing at " + uri);
125137
}

gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingManager.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void setRoutingGroupForQueryId(String queryId, String routingGroup)
125125
/**
126126
* Performs routing to a default backend.
127127
*/
128-
private ProxyBackendConfiguration provideDefaultCluster()
128+
private ProxyBackendConfiguration provideDefaultBackendConfiguration()
129129
{
130130
List<ProxyBackendConfiguration> backends = gatewayBackendManager.getActiveDefaultBackends();
131131
backends.removeIf(backend -> isBackendNotHealthy(backend.getName()));
@@ -136,6 +136,11 @@ private ProxyBackendConfiguration provideDefaultCluster()
136136
return backends.get(backendId);
137137
}
138138

139+
public String provideDefaultCluster(String user)
140+
{
141+
return provideDefaultBackendConfiguration().getProxyTo();
142+
}
143+
139144
/**
140145
* Performs routing to a given cluster group. This falls back to a default backend, if no scheduled
141146
* backend is found.
@@ -146,7 +151,7 @@ public ProxyBackendConfiguration provideBackendConfiguration(String routingGroup
146151
gatewayBackendManager.getActiveBackends(routingGroup);
147152
backends.removeIf(backend -> isBackendNotHealthy(backend.getName()));
148153
if (backends.isEmpty()) {
149-
return provideDefaultCluster();
154+
return provideDefaultBackendConfiguration();
150155
}
151156
int backendId = Math.abs(RANDOM.nextInt()) % backends.size();
152157
return backends.get(backendId);

gateway-ha/src/test/java/io/trino/gateway/ha/handler/TestRoutingTargetHandler.java

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,21 @@
2727
import io.trino.gateway.ha.router.schema.ExternalRouterResponse;
2828
import jakarta.servlet.http.HttpServletRequest;
2929
import jakarta.ws.rs.HttpMethod;
30+
import jakarta.ws.rs.WebApplicationException;
3031
import org.junit.jupiter.api.BeforeAll;
3132
import org.junit.jupiter.api.Test;
3233
import org.junit.jupiter.api.TestInstance;
3334
import org.junit.jupiter.api.extension.ExtendWith;
3435
import org.mockito.Mockito;
3536
import org.mockito.junit.jupiter.MockitoExtension;
3637

37-
import java.util.Arrays;
3838
import java.util.Collections;
3939
import java.util.List;
4040
import java.util.Map;
4141

4242
import static io.trino.gateway.ha.handler.HttpUtils.USER_HEADER;
4343
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4445
import static org.mockito.ArgumentMatchers.any;
4546
import static org.mockito.Mockito.when;
4647

@@ -60,6 +61,8 @@ static HaGatewayConfiguration provideGatewayConfiguration()
6061
HaGatewayConfiguration config = new HaGatewayConfiguration();
6162
config.setRequestAnalyzerConfig(new RequestAnalyzerConfig());
6263

64+
config.getRouting().setDefaultRoutingGroup("default-group");
65+
6366
// Configure excluded headers
6467
RulesExternalConfiguration rulesExternalConfig = new RulesExternalConfiguration();
6568
rulesExternalConfig.setExcludeHeaders(List.of("Authorization", "Cookie"));
@@ -109,7 +112,9 @@ void setUp()
109112
request = prepareMockRequest();
110113

111114
// Initialize the handler with the configuration
112-
handler = new RoutingTargetHandler(routingManager, RoutingGroupSelector.byRoutingExternal(httpClient, config.getRoutingRules().getRulesExternalConfiguration(), config.getRequestAnalyzerConfig()), config);
115+
handler = new RoutingTargetHandler(
116+
routingManager,
117+
RoutingGroupSelector.byRoutingExternal(httpClient, config.getRoutingRules().getRulesExternalConfiguration(), config.getRequestAnalyzerConfig()), config);
113118
}
114119

115120
@Test
@@ -220,29 +225,86 @@ void testEmptyRoutingGroup()
220225
RoutingTargetResponse response = handler.resolveRouting(request);
221226

222227
// Verify that when no routing group header is set, we default to "adhoc"
223-
assertThat(response.routingDestination().routingGroup()).isEqualTo("adhoc");
228+
assertThat(response.routingDestination().routingGroup()).isEqualTo("default-group");
224229
assertThat(response.modifiedRequest().getHeader("X-Empty-Group-Header"))
225230
.isEqualTo("should-be-set");
226231
}
227232

228233
@Test
229-
void testErrorsGiven()
230-
throws Exception
234+
void testResponsePropertiesNull()
235+
{
236+
ExternalRouterResponse mockResponse = new ExternalRouterResponse(null, null, ImmutableMap.of());
237+
when(httpClient.execute(any(), any())).thenReturn(mockResponse);
238+
239+
RoutingTargetResponse result = handler.resolveRouting(request);
240+
241+
assertThat(result.routingDestination().routingGroup()).isEqualTo("default-group");
242+
}
243+
244+
@Test
245+
void testResponseGroupSetResponseErrorsNull()
231246
{
232-
// Setup routing group selector response with errors
233-
Map<String, String> modifiedHeaders = ImmutableMap.of(
234-
"X-New-Header", "new-value");
235-
List<String> someErrors = Arrays.asList("ErrorA", "ErrorB");
236247
ExternalRouterResponse mockResponse = new ExternalRouterResponse(
237-
"test-group", // This value should be ignored due to errors
238-
someErrors,
239-
modifiedHeaders);
248+
"test-group", null, ImmutableMap.of());
240249
when(httpClient.execute(any(), any())).thenReturn(mockResponse);
241250

242-
// Execute
243-
RoutingTargetResponse response = handler.resolveRouting(request);
251+
RoutingTargetResponse result = handler.resolveRouting(request);
252+
253+
assertThat(result.routingDestination().routingGroup()).isEqualTo("test-group");
254+
}
255+
256+
@Test
257+
void testPropagateErrorsFalseResponseGroupNullResponseErrorsSet()
258+
{
259+
ExternalRouterResponse mockResponse = new ExternalRouterResponse(null, List.of("some-error"), ImmutableMap.of());
260+
when(httpClient.execute(any(), any())).thenReturn(mockResponse);
261+
262+
RoutingTargetResponse result = handler.resolveRouting(request);
263+
264+
assertThat(result.routingDestination().routingGroup()).isEqualTo("default-group");
265+
}
266+
267+
@Test
268+
void testPropagateErrorsFalseResponseGroupAndErrorsSet()
269+
{
270+
ExternalRouterResponse mockResponse = new ExternalRouterResponse("test-group", List.of("some-error"), ImmutableMap.of());
271+
when(httpClient.execute(any(), any())).thenReturn(mockResponse);
272+
273+
RoutingTargetResponse result = handler.resolveRouting(request);
274+
275+
assertThat(result.routingDestination().routingGroup()).isEqualTo("test-group");
276+
}
277+
278+
@Test
279+
void testPropagateErrorsTrueResponseGroupNullResponseErrorsSet()
280+
{
281+
RoutingTargetHandler handler = createHandlerWithPropagateErrorsTrue();
244282

245-
// Verify that when errors are present, we default to "adhoc" group
246-
assertThat(response.routingDestination().routingGroup()).isEqualTo("adhoc");
283+
config.getRoutingRules().getRulesExternalConfiguration().setPropagateErrors(true);
284+
ExternalRouterResponse mockResponse = new ExternalRouterResponse(null, List.of("some-error"), ImmutableMap.of());
285+
when(httpClient.execute(any(), any())).thenReturn(mockResponse);
286+
287+
assertThatThrownBy(() -> handler.resolveRouting(request))
288+
.isInstanceOf(WebApplicationException.class);
289+
}
290+
291+
@Test
292+
void testPropagateErrorsTrueResponseGroupAndErrorsSet()
293+
{
294+
RoutingTargetHandler handler = createHandlerWithPropagateErrorsTrue();
295+
296+
ExternalRouterResponse response = new ExternalRouterResponse("test-group", List.of("some-error"), ImmutableMap.of());
297+
when(httpClient.execute(any(), any())).thenReturn(response);
298+
299+
assertThatThrownBy(() -> handler.resolveRouting(request))
300+
.isInstanceOf(WebApplicationException.class);
301+
}
302+
303+
private RoutingTargetHandler createHandlerWithPropagateErrorsTrue()
304+
{
305+
config.getRoutingRules().getRulesExternalConfiguration().setPropagateErrors(true);
306+
return new RoutingTargetHandler(
307+
routingManager,
308+
RoutingGroupSelector.byRoutingExternal(httpClient, config.getRoutingRules().getRulesExternalConfiguration(), config.getRequestAnalyzerConfig()), config);
247309
}
248310
}

0 commit comments

Comments
 (0)