Skip to content

Commit 5013a52

Browse files
authored
[Transform] Make transform _preview request cancellable (#91313) (#91388)
1 parent c8b4a1f commit 5013a52

File tree

10 files changed

+82
-13
lines changed

10 files changed

+82
-13
lines changed

docs/changelog/91313.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 91313
2+
summary: Make transform `_preview` request cancellable
3+
area: Transform
4+
type: bug
5+
issues:
6+
- 91286

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformAction.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import org.elasticsearch.common.io.stream.StreamOutput;
1818
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
1919
import org.elasticsearch.core.TimeValue;
20+
import org.elasticsearch.tasks.CancellableTask;
21+
import org.elasticsearch.tasks.Task;
22+
import org.elasticsearch.tasks.TaskId;
2023
import org.elasticsearch.xcontent.ConstructingObjectParser;
2124
import org.elasticsearch.xcontent.ParseField;
2225
import org.elasticsearch.xcontent.ToXContentObject;
@@ -37,6 +40,7 @@
3740
import java.util.Map;
3841
import java.util.Objects;
3942

43+
import static org.elasticsearch.core.Strings.format;
4044
import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg;
4145

4246
public class PreviewTransformAction extends ActionType<PreviewTransformAction.Response> {
@@ -135,6 +139,11 @@ public boolean equals(Object obj) {
135139
Request other = (Request) obj;
136140
return Objects.equals(config, other.config);
137141
}
142+
143+
@Override
144+
public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
145+
return new CancellableTask(id, type, action, format("preview_transform[%s]", config.getId()), parentTaskId, headers);
146+
}
138147
}
139148

140149
public static class Response extends ActionResponse implements ToXContentObject {

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/action/PreviewTransformActionRequestTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import org.elasticsearch.common.bytes.BytesArray;
1212
import org.elasticsearch.common.io.stream.Writeable;
1313
import org.elasticsearch.core.TimeValue;
14+
import org.elasticsearch.tasks.CancellableTask;
15+
import org.elasticsearch.tasks.Task;
16+
import org.elasticsearch.tasks.TaskId;
1417
import org.elasticsearch.xcontent.DeprecationHandler;
1518
import org.elasticsearch.xcontent.XContentParser;
1619
import org.elasticsearch.xcontent.json.JsonXContent;
@@ -22,9 +25,11 @@
2225
import org.elasticsearch.xpack.core.transform.transforms.pivot.PivotConfigTests;
2326

2427
import java.io.IOException;
28+
import java.util.Map;
2529

2630
import static org.elasticsearch.xpack.core.transform.transforms.SourceConfigTests.randomSourceConfig;
2731
import static org.hamcrest.Matchers.equalTo;
32+
import static org.hamcrest.Matchers.instanceOf;
2833
import static org.hamcrest.Matchers.is;
2934

3035
public class PreviewTransformActionRequestTests extends AbstractSerializingTransformTestCase<Request> {
@@ -132,4 +137,11 @@ private void testParsingOverwrites(
132137
assertThat(request.getConfig().getDestination().getPipeline(), is(equalTo(expectedDestPipeline)));
133138
}
134139
}
140+
141+
public void testCreateTask() {
142+
Request request = createTestInstance();
143+
Task task = request.createTask(123, "type", "action", TaskId.EMPTY_TASK_ID, Map.of());
144+
assertThat(task, is(instanceOf(CancellableTask.class)));
145+
assertThat(task.getDescription(), is(equalTo("preview_transform[transform-preview]")));
146+
}
135147
}

x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/transform/preview_transforms.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,26 @@ setup:
156156
- match: { generated_dest_index.mappings.properties.by-hour.type: "date" }
157157
- match: { generated_dest_index.mappings.properties.avg_response.type: "double" }
158158

159+
---
160+
"Test preview transform with timeout":
161+
- do:
162+
transform.preview_transform:
163+
timeout: "10s"
164+
body: >
165+
{
166+
"source": { "index": "airline-data" },
167+
"pivot": {
168+
"group_by": {
169+
"airline": {"terms": {"field": "airline"}},
170+
"by-hour": {"date_histogram": {"fixed_interval": "1h", "field": "time"}}},
171+
"aggs": {
172+
"avg_response": {"avg": {"field": "responsetime"}},
173+
"time.max": {"max": {"field": "time"}},
174+
"time.min": {"min": {"field": "time"}}
175+
}
176+
}
177+
}
178+
159179
---
160180
"Test preview transform with disabled mapping deduction":
161181
- do:

x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportPreviewTransformAction.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.action.support.ActionFilters;
1616
import org.elasticsearch.action.support.HandledTransportAction;
1717
import org.elasticsearch.client.internal.Client;
18+
import org.elasticsearch.client.internal.ParentTaskAssigningClient;
1819
import org.elasticsearch.cluster.ClusterState;
1920
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
2021
import org.elasticsearch.cluster.node.DiscoveryNode;
@@ -25,10 +26,12 @@
2526
import org.elasticsearch.common.settings.Settings;
2627
import org.elasticsearch.common.xcontent.XContentHelper;
2728
import org.elasticsearch.common.xcontent.support.XContentMapValues;
29+
import org.elasticsearch.core.TimeValue;
2830
import org.elasticsearch.ingest.IngestService;
2931
import org.elasticsearch.license.License;
3032
import org.elasticsearch.license.RemoteClusterLicenseChecker;
3133
import org.elasticsearch.tasks.Task;
34+
import org.elasticsearch.tasks.TaskId;
3235
import org.elasticsearch.threadpool.ThreadPool;
3336
import org.elasticsearch.transport.TransportService;
3437
import org.elasticsearch.xcontent.ToXContent;
@@ -112,6 +115,7 @@ public TransportPreviewTransformAction(
112115

113116
@Override
114117
protected void doExecute(Task task, Request request, ActionListener<Response> listener) {
118+
TaskId parentTaskId = new TaskId(clusterService.localNode().getId(), task.getId());
115119
final ClusterState clusterState = clusterService.state();
116120
TransformNodes.throwIfNoTransformNodes(clusterState);
117121

@@ -137,6 +141,8 @@ protected void doExecute(Task task, Request request, ActionListener<Response> li
137141
validateConfigResponse -> useSecondaryAuthIfAvailable(
138142
securityContext,
139143
() -> getPreview(
144+
parentTaskId,
145+
request.timeout(),
140146
config.getId(), // note: @link{PreviewTransformAction} sets an id, so this is never null
141147
function,
142148
config.getSource(),
@@ -175,7 +181,7 @@ protected void doExecute(Task task, Request request, ActionListener<Response> li
175181
securityContext,
176182
indexNameExpressionResolver,
177183
clusterState,
178-
client,
184+
new ParentTaskAssigningClient(client, parentTaskId),
179185
config,
180186
// We don't want to check privileges for a dummy (placeholder) index and the placeholder is inserted as config.dest.index
181187
// early in the REST action so the only possibility we have here is string comparison.
@@ -189,6 +195,8 @@ protected void doExecute(Task task, Request request, ActionListener<Response> li
189195

190196
@SuppressWarnings("unchecked")
191197
private void getPreview(
198+
TaskId parentTaskId,
199+
TimeValue timeout,
192200
String transformId,
193201
Function function,
194202
SourceConfig source,
@@ -197,6 +205,8 @@ private void getPreview(
197205
SyncConfig syncConfig,
198206
ActionListener<Response> listener
199207
) {
208+
Client parentTaskAssigningClient = new ParentTaskAssigningClient(client, parentTaskId);
209+
200210
final SetOnce<Map<String, String>> mappings = new SetOnce<>();
201211

202212
ActionListener<SimulatePipelineResponse> pipelineResponseActionListener = ActionListener.wrap(simulatePipelineResponse -> {
@@ -256,15 +266,16 @@ private void getPreview(
256266
builder.endObject();
257267
var pipelineRequest = new SimulatePipelineRequest(BytesReference.bytes(builder), XContentType.JSON);
258268
pipelineRequest.setId(pipeline);
259-
client.execute(SimulatePipelineAction.INSTANCE, pipelineRequest, pipelineResponseActionListener);
269+
parentTaskAssigningClient.execute(SimulatePipelineAction.INSTANCE, pipelineRequest, pipelineResponseActionListener);
260270
}
261271
}
262272
}, listener::onFailure);
263273

264274
ActionListener<Map<String, String>> deduceMappingsListener = ActionListener.wrap(deducedMappings -> {
265275
mappings.set(deducedMappings);
266276
function.preview(
267-
client,
277+
parentTaskAssigningClient,
278+
timeout,
268279
ClientHelper.getPersistableSafeSecurityHeaders(threadPool.getThreadContext(), clusterService.state()),
269280
source,
270281
deducedMappings,
@@ -273,6 +284,6 @@ private void getPreview(
273284
);
274285
}, listener::onFailure);
275286

276-
function.deduceMappings(client, source, deduceMappingsListener);
287+
function.deduceMappings(parentTaskAssigningClient, source, deduceMappingsListener);
277288
}
278289
}

x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportValidateTransformAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ protected void doExecute(Task task, Request request, ActionListener<Response> li
136136
if (request.isDeferValidation()) {
137137
validateQueryListener.onResponse(true);
138138
} else {
139-
function.validateQuery(client, config.getSource(), validateQueryListener);
139+
function.validateQuery(client, config.getSource(), request.timeout(), validateQueryListener);
140140
}
141141
}, listener::onFailure);
142142

x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/rest/action/RestPreviewTransformAction.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
import org.apache.lucene.util.SetOnce;
1111
import org.elasticsearch.action.ActionListener;
1212
import org.elasticsearch.action.support.master.AcknowledgedRequest;
13+
import org.elasticsearch.client.internal.Client;
1314
import org.elasticsearch.client.internal.node.NodeClient;
1415
import org.elasticsearch.common.Strings;
1516
import org.elasticsearch.core.TimeValue;
1617
import org.elasticsearch.rest.BaseRestHandler;
1718
import org.elasticsearch.rest.RestRequest;
19+
import org.elasticsearch.rest.action.RestCancellableNodeClient;
1820
import org.elasticsearch.rest.action.RestToXContentListener;
1921
import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
2022
import org.elasticsearch.xpack.core.transform.TransformField;
@@ -47,7 +49,7 @@ public String getName() {
4749
}
4850

4951
@Override
50-
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
52+
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient nodeClient) throws IOException {
5153
String transformId = restRequest.param(TransformField.ID.getPreferredName());
5254

5355
if (Strings.isNullOrEmpty(transformId) && restRequest.hasContentOrSourceParam() == false) {
@@ -72,6 +74,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient
7274
previewRequestHolder.set(PreviewTransformAction.Request.fromXContent(restRequest.contentOrSourceParamParser(), timeout));
7375
}
7476

77+
Client client = new RestCancellableNodeClient(nodeClient, restRequest.getHttpChannel());
7578
return channel -> {
7679
RestToXContentListener<PreviewTransformAction.Response> listener = new RestToXContentListener<>(channel);
7780

x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/Function.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.elasticsearch.action.index.IndexRequest;
1212
import org.elasticsearch.action.search.SearchResponse;
1313
import org.elasticsearch.client.internal.Client;
14+
import org.elasticsearch.core.Nullable;
15+
import org.elasticsearch.core.TimeValue;
1416
import org.elasticsearch.core.Tuple;
1517
import org.elasticsearch.index.query.QueryBuilder;
1618
import org.elasticsearch.search.builder.SearchSourceBuilder;
@@ -124,6 +126,7 @@ interface ChangeCollector {
124126
* Create a preview of the function.
125127
*
126128
* @param client a client instance for querying
129+
* @param timeout search query timeout
127130
* @param headers headers to be used to query only for what the caller is allowed to
128131
* @param sourceConfig the source configuration
129132
* @param fieldTypeMap mapping of field types
@@ -132,6 +135,7 @@ interface ChangeCollector {
132135
*/
133136
void preview(
134137
Client client,
138+
@Nullable TimeValue timeout,
135139
Map<String, String> headers,
136140
SourceConfig sourceConfig,
137141
Map<String, String> fieldTypeMap,
@@ -175,9 +179,10 @@ void preview(
175179
*
176180
* @param client a client instance for querying the source
177181
* @param sourceConfig the source configuration
182+
* @param timeout search query timeout
178183
* @param listener the result listener
179184
*/
180-
void validateQuery(Client client, SourceConfig sourceConfig, ActionListener<Boolean> listener);
185+
void validateQuery(Client client, SourceConfig sourceConfig, @Nullable TimeValue timeout, ActionListener<Boolean> listener);
181186

182187
/**
183188
* Create a change collector instance and return it

x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/common/AbstractCompositeAggFunction.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.action.support.IndicesOptions;
1818
import org.elasticsearch.client.internal.Client;
1919
import org.elasticsearch.common.ValidationException;
20+
import org.elasticsearch.core.TimeValue;
2021
import org.elasticsearch.core.Tuple;
2122
import org.elasticsearch.rest.RestStatus;
2223
import org.elasticsearch.search.aggregations.Aggregations;
@@ -63,6 +64,7 @@ public SearchSourceBuilder buildSearchQuery(SearchSourceBuilder builder, Map<Str
6364
@Override
6465
public void preview(
6566
Client client,
67+
TimeValue timeout,
6668
Map<String, String> headers,
6769
SourceConfig sourceConfig,
6870
Map<String, String> fieldTypeMap,
@@ -75,7 +77,7 @@ public void preview(
7577
ClientHelper.TRANSFORM_ORIGIN,
7678
client,
7779
SearchAction.INSTANCE,
78-
buildSearchRequest(sourceConfig, null, numberOfBuckets),
80+
buildSearchRequest(sourceConfig, timeout, numberOfBuckets),
7981
ActionListener.wrap(r -> {
8082
try {
8183
final Aggregations aggregations = r.getAggregations();
@@ -102,8 +104,8 @@ public void preview(
102104
}
103105

104106
@Override
105-
public void validateQuery(Client client, SourceConfig sourceConfig, ActionListener<Boolean> listener) {
106-
SearchRequest searchRequest = buildSearchRequest(sourceConfig, null, TEST_QUERY_PAGE_SIZE);
107+
public void validateQuery(Client client, SourceConfig sourceConfig, TimeValue timeout, ActionListener<Boolean> listener) {
108+
SearchRequest searchRequest = buildSearchRequest(sourceConfig, timeout, TEST_QUERY_PAGE_SIZE);
107109
client.execute(SearchAction.INSTANCE, searchRequest, ActionListener.wrap(response -> {
108110
if (response == null) {
109111
listener.onFailure(new ValidationException().addValidationError("Unexpected null response from test query"));
@@ -173,9 +175,10 @@ protected abstract Stream<Map<String, Object>> extractResults(
173175
TransformProgress progress
174176
);
175177

176-
private SearchRequest buildSearchRequest(SourceConfig sourceConfig, Map<String, Object> position, int pageSize) {
178+
private SearchRequest buildSearchRequest(SourceConfig sourceConfig, TimeValue timeout, int pageSize) {
177179
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder().query(sourceConfig.getQueryConfig().getQuery())
178-
.runtimeMappings(sourceConfig.getRuntimeMappings());
180+
.runtimeMappings(sourceConfig.getRuntimeMappings())
181+
.timeout(timeout);
179182
buildSearchQuery(sourceBuilder, null, pageSize);
180183
return new SearchRequest(sourceConfig.getIndex()).source(sourceBuilder).indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN);
181184
}

x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/PivotTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ private static void assertInvalidTransform(Client client, SourceConfig source, F
447447
private static void validate(Client client, SourceConfig source, Function pivot, boolean expectValid) throws Exception {
448448
CountDownLatch latch = new CountDownLatch(1);
449449
final AtomicReference<Exception> exceptionHolder = new AtomicReference<>();
450-
pivot.validateQuery(client, source, ActionListener.wrap(validity -> {
450+
pivot.validateQuery(client, source, null, ActionListener.wrap(validity -> {
451451
assertEquals(expectValid, validity);
452452
latch.countDown();
453453
}, e -> {

0 commit comments

Comments
 (0)