Skip to content

Commit 476da8c

Browse files
authored
Script: Reindex & UpdateByQuery Metadata (#88665)
Adds metadata classes for Reindex and UpdateByQuery contexts. For Reindex metadata: * _index can't be null * _id, _routing and _version are writable and nullable * _now is read-only * op is read-write must be 'noop', 'index' or 'delete' Reindex metadata keeps the originx value for _index, _id, _routing and _version so that `Reindexer` can see if they've changed. If _version is null in the ctx map, or, equivalently, the augmentation `setVersionToInternal()` was called by the script, `Reindexer` sets document versioning to internal. If `_version` is `null` in the ctx map, `getVersion` returns `Long.MIN_VALUE`. For UpdateByQuery metadata: * _index, _id, _version, _routing are all read-only * _routing is also nullable * _now is read-only * op is read-write and one of 'index', 'noop', 'delete' Closes: #86472
1 parent 4ed027b commit 476da8c

File tree

26 files changed

+662
-282
lines changed

26 files changed

+662
-282
lines changed

docs/changelog/88665.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 88665
2+
summary: "Script: Reindex & `UpdateByQuery` Metadata"
3+
area: Infra/Scripting
4+
type: enhancement
5+
issues: []

modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,22 @@ class org.elasticsearch.painless.api.Json {
1818
String dump(def)
1919
String dump(def, boolean)
2020
}
21+
22+
class org.elasticsearch.script.Metadata {
23+
String getIndex()
24+
void setIndex(String)
25+
String getId()
26+
void setId(String)
27+
String getRouting()
28+
void setRouting(String)
29+
long getVersion()
30+
void setVersion(long)
31+
boolean org.elasticsearch.script.ReindexMetadata isVersionInternal()
32+
void org.elasticsearch.script.ReindexMetadata setVersionToInternal()
33+
String getOp()
34+
void setOp(String)
35+
}
36+
37+
class org.elasticsearch.script.ReindexScript {
38+
Metadata metadata()
39+
}

modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,16 @@ class org.elasticsearch.painless.api.Json {
1818
String dump(def)
1919
String dump(def, boolean)
2020
}
21+
22+
class org.elasticsearch.script.Metadata {
23+
String getIndex()
24+
String getId()
25+
String getRouting()
26+
long getVersion()
27+
String getOp()
28+
void setOp(String)
29+
}
30+
31+
class org.elasticsearch.script.UpdateByQueryScript {
32+
Metadata metadata()
33+
}

modules/reindex/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,12 @@ tasks.named("yamlRestTestV7CompatTransform").configure { task ->
178178

179179
task.addAllowedWarningRegex("\\[types removal\\].*")
180180
}
181+
182+
tasks.named("yamlRestTestV7CompatTest").configure {
183+
systemProperty 'tests.rest.blacklist', [
184+
'update_by_query/80_scripting/Can\'t change _id',
185+
'update_by_query/80_scripting/Set unsupported operation type',
186+
'update_by_query/80_scripting/Setting bogus context is an error',
187+
188+
].join(',')
189+
}

modules/reindex/src/main/java/org/elasticsearch/reindex/AbstractAsyncBulkByScrollAction.java

Lines changed: 29 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,15 @@
3030
import org.elasticsearch.core.Nullable;
3131
import org.elasticsearch.core.TimeValue;
3232
import org.elasticsearch.index.VersionType;
33-
import org.elasticsearch.index.mapper.IdFieldMapper;
34-
import org.elasticsearch.index.mapper.IndexFieldMapper;
35-
import org.elasticsearch.index.mapper.RoutingFieldMapper;
36-
import org.elasticsearch.index.mapper.SourceFieldMapper;
37-
import org.elasticsearch.index.mapper.VersionFieldMapper;
3833
import org.elasticsearch.index.reindex.AbstractBulkByScrollRequest;
3934
import org.elasticsearch.index.reindex.BulkByScrollResponse;
4035
import org.elasticsearch.index.reindex.BulkByScrollTask;
4136
import org.elasticsearch.index.reindex.ClientScrollableHitSource;
4237
import org.elasticsearch.index.reindex.ScrollableHitSource;
4338
import org.elasticsearch.index.reindex.ScrollableHitSource.SearchFailure;
4439
import org.elasticsearch.index.reindex.WorkerBulkByScrollTaskState;
40+
import org.elasticsearch.script.CtxMap;
41+
import org.elasticsearch.script.Metadata;
4542
import org.elasticsearch.script.Script;
4643
import org.elasticsearch.script.ScriptService;
4744
import org.elasticsearch.search.Scroll;
@@ -50,20 +47,18 @@
5047
import org.elasticsearch.threadpool.ThreadPool;
5148

5249
import java.util.ArrayList;
53-
import java.util.Arrays;
5450
import java.util.Collection;
5551
import java.util.Collections;
56-
import java.util.HashMap;
5752
import java.util.HashSet;
5853
import java.util.List;
59-
import java.util.Locale;
6054
import java.util.Map;
6155
import java.util.Objects;
6256
import java.util.Set;
6357
import java.util.concurrent.ConcurrentHashMap;
6458
import java.util.concurrent.atomic.AtomicInteger;
6559
import java.util.concurrent.atomic.AtomicLong;
6660
import java.util.function.BiFunction;
61+
import java.util.function.LongSupplier;
6762

6863
import static java.lang.Math.max;
6964
import static java.lang.Math.min;
@@ -819,147 +814,73 @@ public static RequestWrapper<DeleteRequest> wrap(DeleteRequest request) {
819814
/**
820815
* Apply a {@link Script} to a {@link RequestWrapper}
821816
*/
822-
public abstract static class ScriptApplier implements BiFunction<RequestWrapper<?>, ScrollableHitSource.Hit, RequestWrapper<?>> {
817+
public abstract static class ScriptApplier<T extends Metadata>
818+
implements
819+
BiFunction<RequestWrapper<?>, ScrollableHitSource.Hit, RequestWrapper<?>> {
820+
821+
// "index" is the default operation
822+
protected static final String INDEX = "index";
823823

824824
private final WorkerBulkByScrollTaskState taskWorker;
825825
protected final ScriptService scriptService;
826826
protected final Script script;
827827
protected final Map<String, Object> params;
828+
protected final LongSupplier nowInMillisSupplier;
828829

829830
public ScriptApplier(
830831
WorkerBulkByScrollTaskState taskWorker,
831832
ScriptService scriptService,
832833
Script script,
833-
Map<String, Object> params
834+
Map<String, Object> params,
835+
LongSupplier nowInMillisSupplier
834836
) {
835837
this.taskWorker = taskWorker;
836838
this.scriptService = scriptService;
837839
this.script = script;
838840
this.params = params;
841+
this.nowInMillisSupplier = nowInMillisSupplier;
839842
}
840843

841844
@Override
842-
@SuppressWarnings("unchecked")
843845
public RequestWrapper<?> apply(RequestWrapper<?> request, ScrollableHitSource.Hit doc) {
844846
if (script == null) {
845847
return request;
846848
}
847849

848-
Map<String, Object> context = new HashMap<>();
849-
context.put(IndexFieldMapper.NAME, doc.getIndex());
850-
context.put(IdFieldMapper.NAME, doc.getId());
851-
Long oldVersion = doc.getVersion();
852-
context.put(VersionFieldMapper.NAME, oldVersion);
853-
String oldRouting = doc.getRouting();
854-
context.put(RoutingFieldMapper.NAME, oldRouting);
855-
context.put(SourceFieldMapper.NAME, request.getSource());
856-
857-
OpType oldOpType = OpType.INDEX;
858-
context.put("op", oldOpType.toString());
850+
CtxMap<T> ctxMap = execute(doc, request.getSource());
859851

860-
execute(context);
852+
T metadata = ctxMap.getMetadata();
861853

862-
String newOp = (String) context.remove("op");
863-
if (newOp == null) {
864-
throw new IllegalArgumentException("Script cleared operation type");
865-
}
854+
request.setSource(ctxMap.getSource());
866855

867-
/*
868-
* It'd be lovely to only set the source if we know its been modified
869-
* but it isn't worth keeping two copies of it around just to check!
870-
*/
871-
request.setSource((Map<String, Object>) context.remove(SourceFieldMapper.NAME));
856+
updateRequest(request, metadata);
872857

873-
Object newValue = context.remove(IndexFieldMapper.NAME);
874-
if (false == doc.getIndex().equals(newValue)) {
875-
scriptChangedIndex(request, newValue);
876-
}
877-
newValue = context.remove(IdFieldMapper.NAME);
878-
if (false == doc.getId().equals(newValue)) {
879-
scriptChangedId(request, newValue);
880-
}
881-
newValue = context.remove(VersionFieldMapper.NAME);
882-
if (false == Objects.equals(oldVersion, newValue)) {
883-
scriptChangedVersion(request, newValue);
884-
}
885-
/*
886-
* Its important that routing comes after parent in case you want to
887-
* change them both.
888-
*/
889-
newValue = context.remove(RoutingFieldMapper.NAME);
890-
if (false == Objects.equals(oldRouting, newValue)) {
891-
scriptChangedRouting(request, newValue);
892-
}
858+
return requestFromOp(request, metadata.getOp());
859+
}
893860

894-
OpType newOpType = OpType.fromString(newOp);
895-
if (newOpType != oldOpType) {
896-
return scriptChangedOpType(request, oldOpType, newOpType);
897-
}
861+
protected abstract CtxMap<T> execute(ScrollableHitSource.Hit doc, Map<String, Object> source);
898862

899-
if (false == context.isEmpty()) {
900-
throw new IllegalArgumentException("Invalid fields added to context [" + String.join(",", context.keySet()) + ']');
901-
}
902-
return request;
903-
}
863+
protected abstract void updateRequest(RequestWrapper<?> request, T metadata);
904864

905-
protected RequestWrapper<?> scriptChangedOpType(RequestWrapper<?> request, OpType oldOpType, OpType newOpType) {
906-
switch (newOpType) {
907-
case NOOP -> {
865+
protected RequestWrapper<?> requestFromOp(RequestWrapper<?> request, String op) {
866+
switch (op) {
867+
case "noop" -> {
908868
taskWorker.countNoop();
909869
return null;
910870
}
911-
case DELETE -> {
871+
case "delete" -> {
912872
RequestWrapper<DeleteRequest> delete = wrap(new DeleteRequest(request.getIndex(), request.getId()));
913873
delete.setVersion(request.getVersion());
914874
delete.setVersionType(VersionType.INTERNAL);
915875
delete.setRouting(request.getRouting());
916876
return delete;
917877
}
918-
default -> throw new IllegalArgumentException(
919-
"Unsupported operation type change from [" + oldOpType + "] to [" + newOpType + "]"
920-
);
878+
case INDEX -> {
879+
return request;
880+
}
881+
default -> throw new IllegalArgumentException("Unsupported operation type change from [" + INDEX + "] to [" + op + "]");
921882
}
922883
}
923-
924-
protected abstract void scriptChangedIndex(RequestWrapper<?> request, Object to);
925-
926-
protected abstract void scriptChangedId(RequestWrapper<?> request, Object to);
927-
928-
protected abstract void scriptChangedVersion(RequestWrapper<?> request, Object to);
929-
930-
protected abstract void scriptChangedRouting(RequestWrapper<?> request, Object to);
931-
932-
protected abstract void execute(Map<String, Object> ctx);
933-
}
934-
935-
public enum OpType {
936-
937-
NOOP("noop"),
938-
INDEX("index"),
939-
DELETE("delete");
940-
941-
private final String id;
942-
943-
OpType(String id) {
944-
this.id = id;
945-
}
946-
947-
public static OpType fromString(String opType) {
948-
String lowerOpType = opType.toLowerCase(Locale.ROOT);
949-
return switch (lowerOpType) {
950-
case "noop" -> OpType.NOOP;
951-
case "index" -> OpType.INDEX;
952-
case "delete" -> OpType.DELETE;
953-
default -> throw new IllegalArgumentException(
954-
"Operation type [" + lowerOpType + "] not allowed, only " + Arrays.toString(values()) + " are allowed"
955-
);
956-
};
957-
}
958-
959-
@Override
960-
public String toString() {
961-
return id.toLowerCase(Locale.ROOT);
962-
}
963884
}
964885

965886
static class ScrollConsumableHitsResponse {

0 commit comments

Comments
 (0)