mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-04-25 07:37:19 -04:00
[Transform] Add from
parameter to Transform Start API (#91116)
This commit is contained in:
parent
0a899c6e7c
commit
40d32205db
25 changed files with 348 additions and 32 deletions
6
docs/changelog/91116.yaml
Normal file
6
docs/changelog/91116.yaml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pr: 91116
|
||||||
|
summary: Add from parameter to Transform Start API
|
||||||
|
area: Transform
|
||||||
|
type: enhancement
|
||||||
|
issues:
|
||||||
|
- 88646
|
|
@ -61,6 +61,11 @@ Identifier for the {transform}.
|
||||||
[[start-transform-query-parms]]
|
[[start-transform-query-parms]]
|
||||||
== {api-query-parms-title}
|
== {api-query-parms-title}
|
||||||
|
|
||||||
|
`from`::
|
||||||
|
(Optional, string) Restricts the set of transformed entities to those changed
|
||||||
|
after this time. Relative times like now-30d are supported.
|
||||||
|
Only applicable for continuous transforms.
|
||||||
|
|
||||||
`timeout`::
|
`timeout`::
|
||||||
(Optional, time)
|
(Optional, time)
|
||||||
Period to wait for a response. If no response is received before the timeout
|
Period to wait for a response. If no response is received before the timeout
|
||||||
|
|
|
@ -26,6 +26,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params":{
|
"params":{
|
||||||
|
"from":{
|
||||||
|
"type":"string",
|
||||||
|
"required":false,
|
||||||
|
"description":"Restricts the set of transformed entities to those changed after this time"
|
||||||
|
},
|
||||||
"timeout":{
|
"timeout":{
|
||||||
"type":"time",
|
"type":"time",
|
||||||
"required":false,
|
"required":false,
|
||||||
|
|
|
@ -22,6 +22,7 @@ public final class TransformField {
|
||||||
public static final ParseField COUNT = new ParseField("count");
|
public static final ParseField COUNT = new ParseField("count");
|
||||||
public static final ParseField GROUP_BY = new ParseField("group_by");
|
public static final ParseField GROUP_BY = new ParseField("group_by");
|
||||||
public static final ParseField TIMEOUT = new ParseField("timeout");
|
public static final ParseField TIMEOUT = new ParseField("timeout");
|
||||||
|
public static final ParseField FROM = new ParseField("from");
|
||||||
public static final ParseField WAIT_FOR_COMPLETION = new ParseField("wait_for_completion");
|
public static final ParseField WAIT_FOR_COMPLETION = new ParseField("wait_for_completion");
|
||||||
public static final ParseField WAIT_FOR_CHECKPOINT = new ParseField("wait_for_checkpoint");
|
public static final ParseField WAIT_FOR_CHECKPOINT = new ParseField("wait_for_checkpoint");
|
||||||
public static final ParseField STATS_FIELD = new ParseField("stats");
|
public static final ParseField STATS_FIELD = new ParseField("stats");
|
||||||
|
|
|
@ -75,6 +75,8 @@ public class TransformMessages {
|
||||||
|
|
||||||
public static final String FAILED_TO_PARSE_TRANSFORM_CHECKPOINTS = "Failed to parse transform checkpoints for [{0}]";
|
public static final String FAILED_TO_PARSE_TRANSFORM_CHECKPOINTS = "Failed to parse transform checkpoints for [{0}]";
|
||||||
|
|
||||||
|
public static final String FAILED_TO_PARSE_DATE = "Failed to parse date for [{0}]";
|
||||||
|
|
||||||
public static final String ID_TOO_LONG = "The id cannot contain more than {0} characters.";
|
public static final String ID_TOO_LONG = "The id cannot contain more than {0} characters.";
|
||||||
public static final String INVALID_ID = "Invalid {0}; ''{1}'' can contain lowercase alphanumeric (a-z and 0-9), hyphens or "
|
public static final String INVALID_ID = "Invalid {0}; ''{1}'' can contain lowercase alphanumeric (a-z and 0-9), hyphens or "
|
||||||
+ "underscores; must start and end with alphanumeric";
|
+ "underscores; must start and end with alphanumeric";
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package org.elasticsearch.xpack.core.transform.action;
|
package org.elasticsearch.xpack.core.transform.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.Version;
|
||||||
import org.elasticsearch.action.ActionRequestValidationException;
|
import org.elasticsearch.action.ActionRequestValidationException;
|
||||||
import org.elasticsearch.action.ActionType;
|
import org.elasticsearch.action.ActionType;
|
||||||
import org.elasticsearch.action.support.master.AcknowledgedRequest;
|
import org.elasticsearch.action.support.master.AcknowledgedRequest;
|
||||||
|
@ -20,6 +21,7 @@ import org.elasticsearch.xpack.core.transform.TransformField;
|
||||||
import org.elasticsearch.xpack.core.transform.utils.ExceptionsHelper;
|
import org.elasticsearch.xpack.core.transform.utils.ExceptionsHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -35,25 +37,39 @@ public class StartTransformAction extends ActionType<StartTransformAction.Respon
|
||||||
public static class Request extends AcknowledgedRequest<Request> {
|
public static class Request extends AcknowledgedRequest<Request> {
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
|
private final Instant from;
|
||||||
|
|
||||||
public Request(String id, TimeValue timeout) {
|
public Request(String id, Instant from, TimeValue timeout) {
|
||||||
super(timeout);
|
super(timeout);
|
||||||
this.id = ExceptionsHelper.requireNonNull(id, TransformField.ID.getPreferredName());
|
this.id = ExceptionsHelper.requireNonNull(id, TransformField.ID.getPreferredName());
|
||||||
|
this.from = from;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Request(StreamInput in) throws IOException {
|
public Request(StreamInput in) throws IOException {
|
||||||
super(in);
|
super(in);
|
||||||
id = in.readString();
|
id = in.readString();
|
||||||
|
if (in.getVersion().onOrAfter(Version.V_8_7_0)) {
|
||||||
|
from = in.readOptionalInstant();
|
||||||
|
} else {
|
||||||
|
from = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Instant from() {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(StreamOutput out) throws IOException {
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
super.writeTo(out);
|
super.writeTo(out);
|
||||||
out.writeString(id);
|
out.writeString(id);
|
||||||
|
if (out.getVersion().onOrAfter(Version.V_8_7_0)) {
|
||||||
|
out.writeOptionalInstant(from);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,7 +80,7 @@ public class StartTransformAction extends ActionType<StartTransformAction.Respon
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
// the base class does not implement hashCode, therefore we need to hash timeout ourselves
|
// the base class does not implement hashCode, therefore we need to hash timeout ourselves
|
||||||
return Objects.hash(timeout(), id);
|
return Objects.hash(timeout(), id, from);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -77,7 +93,7 @@ public class StartTransformAction extends ActionType<StartTransformAction.Respon
|
||||||
}
|
}
|
||||||
Request other = (Request) obj;
|
Request other = (Request) obj;
|
||||||
// the base class does not implement equals, therefore we need to check timeout ourselves
|
// the base class does not implement equals, therefore we need to check timeout ourselves
|
||||||
return Objects.equals(id, other.id) && timeout().equals(other.timeout());
|
return Objects.equals(id, other.id) && Objects.equals(from, other.from) && timeout().equals(other.timeout());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,12 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstr
|
||||||
*/
|
*/
|
||||||
public class TransformCheckpoint implements Writeable, ToXContentObject {
|
public class TransformCheckpoint implements Writeable, ToXContentObject {
|
||||||
|
|
||||||
public static TransformCheckpoint EMPTY = new TransformCheckpoint("empty", 0L, -1L, Collections.emptyMap(), 0L);
|
public static String EMPTY_NAME = "_empty";
|
||||||
|
public static TransformCheckpoint EMPTY = createEmpty(0);
|
||||||
|
|
||||||
|
public static TransformCheckpoint createEmpty(long timestampMillis) {
|
||||||
|
return new TransformCheckpoint(EMPTY_NAME, timestampMillis, -1L, Collections.emptyMap(), timestampMillis);
|
||||||
|
}
|
||||||
|
|
||||||
// the own checkpoint
|
// the own checkpoint
|
||||||
public static final ParseField CHECKPOINT = new ParseField("checkpoint");
|
public static final ParseField CHECKPOINT = new ParseField("checkpoint");
|
||||||
|
@ -128,7 +133,7 @@ public class TransformCheckpoint implements Writeable, ToXContentObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return this.equals(EMPTY);
|
return EMPTY_NAME.equals(transformId) && checkpoint == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,44 +20,54 @@ import org.elasticsearch.xcontent.XContentParser;
|
||||||
import org.elasticsearch.xpack.core.transform.TransformField;
|
import org.elasticsearch.xpack.core.transform.TransformField;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>, PersistentTaskParams {
|
public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>, PersistentTaskParams {
|
||||||
|
|
||||||
public static final String NAME = TransformField.TASK_NAME;
|
public static final String NAME = TransformField.TASK_NAME;
|
||||||
|
public static final ParseField FROM = TransformField.FROM;
|
||||||
public static final ParseField FREQUENCY = TransformField.FREQUENCY;
|
public static final ParseField FREQUENCY = TransformField.FREQUENCY;
|
||||||
public static final ParseField REQUIRES_REMOTE = new ParseField("requires_remote");
|
public static final ParseField REQUIRES_REMOTE = new ParseField("requires_remote");
|
||||||
|
|
||||||
private final String transformId;
|
private final String transformId;
|
||||||
private final Version version;
|
private final Version version;
|
||||||
|
private final Instant from;
|
||||||
private final TimeValue frequency;
|
private final TimeValue frequency;
|
||||||
private final Boolean requiresRemote;
|
private final Boolean requiresRemote;
|
||||||
|
|
||||||
public static final ConstructingObjectParser<TransformTaskParams, Void> PARSER = new ConstructingObjectParser<>(
|
public static final ConstructingObjectParser<TransformTaskParams, Void> PARSER = new ConstructingObjectParser<>(
|
||||||
NAME,
|
NAME,
|
||||||
true,
|
true,
|
||||||
a -> new TransformTaskParams((String) a[0], (String) a[1], (String) a[2], (Boolean) a[3])
|
a -> new TransformTaskParams((String) a[0], (String) a[1], (Long) a[2], (String) a[3], (Boolean) a[4])
|
||||||
);
|
);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
PARSER.declareString(ConstructingObjectParser.constructorArg(), TransformField.ID);
|
PARSER.declareString(ConstructingObjectParser.constructorArg(), TransformField.ID);
|
||||||
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), TransformField.VERSION);
|
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), TransformField.VERSION);
|
||||||
|
PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), FROM);
|
||||||
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), FREQUENCY);
|
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), FREQUENCY);
|
||||||
PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), REQUIRES_REMOTE);
|
PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), REQUIRES_REMOTE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TransformTaskParams(String transformId, String version, String frequency, Boolean remote) {
|
private TransformTaskParams(String transformId, String version, Long from, String frequency, Boolean remote) {
|
||||||
this(
|
this(
|
||||||
transformId,
|
transformId,
|
||||||
version == null ? null : Version.fromString(version),
|
version == null ? null : Version.fromString(version),
|
||||||
|
from == null ? null : Instant.ofEpochMilli(from),
|
||||||
frequency == null ? null : TimeValue.parseTimeValue(frequency, FREQUENCY.getPreferredName()),
|
frequency == null ? null : TimeValue.parseTimeValue(frequency, FREQUENCY.getPreferredName()),
|
||||||
remote == null ? false : remote.booleanValue()
|
remote == null ? false : remote.booleanValue()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TransformTaskParams(String transformId, Version version, TimeValue frequency, boolean remote) {
|
public TransformTaskParams(String transformId, Version version, TimeValue frequency, boolean remote) {
|
||||||
|
this(transformId, version, null, frequency, remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformTaskParams(String transformId, Version version, Instant from, TimeValue frequency, boolean remote) {
|
||||||
this.transformId = transformId;
|
this.transformId = transformId;
|
||||||
this.version = version == null ? Version.V_7_2_0 : version;
|
this.version = version == null ? Version.V_7_2_0 : version;
|
||||||
|
this.from = from;
|
||||||
this.frequency = frequency;
|
this.frequency = frequency;
|
||||||
this.requiresRemote = remote;
|
this.requiresRemote = remote;
|
||||||
}
|
}
|
||||||
|
@ -65,6 +75,11 @@ public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>,
|
||||||
public TransformTaskParams(StreamInput in) throws IOException {
|
public TransformTaskParams(StreamInput in) throws IOException {
|
||||||
this.transformId = in.readString();
|
this.transformId = in.readString();
|
||||||
this.version = Version.readVersion(in);
|
this.version = Version.readVersion(in);
|
||||||
|
if (in.getVersion().onOrAfter(Version.V_8_7_0)) {
|
||||||
|
this.from = in.readOptionalInstant();
|
||||||
|
} else {
|
||||||
|
this.from = null;
|
||||||
|
}
|
||||||
this.frequency = in.readOptionalTimeValue();
|
this.frequency = in.readOptionalTimeValue();
|
||||||
this.requiresRemote = in.readBoolean();
|
this.requiresRemote = in.readBoolean();
|
||||||
}
|
}
|
||||||
|
@ -83,6 +98,9 @@ public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>,
|
||||||
public void writeTo(StreamOutput out) throws IOException {
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
out.writeString(transformId);
|
out.writeString(transformId);
|
||||||
Version.writeVersion(version, out);
|
Version.writeVersion(version, out);
|
||||||
|
if (out.getVersion().onOrAfter(Version.V_8_7_0)) {
|
||||||
|
out.writeOptionalInstant(from);
|
||||||
|
}
|
||||||
out.writeOptionalTimeValue(frequency);
|
out.writeOptionalTimeValue(frequency);
|
||||||
out.writeBoolean(requiresRemote);
|
out.writeBoolean(requiresRemote);
|
||||||
}
|
}
|
||||||
|
@ -92,6 +110,9 @@ public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>,
|
||||||
builder.startObject();
|
builder.startObject();
|
||||||
builder.field(TransformField.ID.getPreferredName(), transformId);
|
builder.field(TransformField.ID.getPreferredName(), transformId);
|
||||||
builder.field(TransformField.VERSION.getPreferredName(), version);
|
builder.field(TransformField.VERSION.getPreferredName(), version);
|
||||||
|
if (from != null) {
|
||||||
|
builder.field(FROM.getPreferredName(), from.toEpochMilli());
|
||||||
|
}
|
||||||
if (frequency != null) {
|
if (frequency != null) {
|
||||||
builder.field(FREQUENCY.getPreferredName(), frequency.getStringRep());
|
builder.field(FREQUENCY.getPreferredName(), frequency.getStringRep());
|
||||||
}
|
}
|
||||||
|
@ -108,6 +129,10 @@ public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>,
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Instant from() {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
public TimeValue getFrequency() {
|
public TimeValue getFrequency() {
|
||||||
return frequency;
|
return frequency;
|
||||||
}
|
}
|
||||||
|
@ -134,12 +159,13 @@ public class TransformTaskParams implements SimpleDiffable<TransformTaskParams>,
|
||||||
|
|
||||||
return Objects.equals(this.transformId, that.transformId)
|
return Objects.equals(this.transformId, that.transformId)
|
||||||
&& Objects.equals(this.version, that.version)
|
&& Objects.equals(this.version, that.version)
|
||||||
|
&& Objects.equals(this.from, that.from)
|
||||||
&& Objects.equals(this.frequency, that.frequency)
|
&& Objects.equals(this.frequency, that.frequency)
|
||||||
&& this.requiresRemote == that.requiresRemote;
|
&& this.requiresRemote == that.requiresRemote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(transformId, version, frequency, requiresRemote);
|
return Objects.hash(transformId, version, from, frequency, requiresRemote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,17 @@ import org.elasticsearch.test.AbstractWireSerializingTestCase;
|
||||||
import org.elasticsearch.xpack.core.transform.action.StartTransformAction.Request;
|
import org.elasticsearch.xpack.core.transform.action.StartTransformAction.Request;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
public class StartTransformActionRequestTests extends AbstractWireSerializingTestCase<Request> {
|
public class StartTransformActionRequestTests extends AbstractWireSerializingTestCase<Request> {
|
||||||
@Override
|
@Override
|
||||||
protected Request createTestInstance() {
|
protected Request createTestInstance() {
|
||||||
return new Request(randomAlphaOfLengthBetween(1, 20), TimeValue.parseTimeValue(randomTimeValue(), "timeout"));
|
return new Request(
|
||||||
|
randomAlphaOfLengthBetween(1, 20),
|
||||||
|
randomBoolean() ? Instant.ofEpochMilli(randomNonNegativeLong()) : null,
|
||||||
|
TimeValue.parseTimeValue(randomTimeValue(), "timeout")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -28,14 +34,16 @@ public class StartTransformActionRequestTests extends AbstractWireSerializingTes
|
||||||
@Override
|
@Override
|
||||||
protected Request mutateInstance(Request instance) throws IOException {
|
protected Request mutateInstance(Request instance) throws IOException {
|
||||||
String id = instance.getId();
|
String id = instance.getId();
|
||||||
|
Instant from = instance.from();
|
||||||
TimeValue timeout = instance.timeout();
|
TimeValue timeout = instance.timeout();
|
||||||
|
|
||||||
switch (between(0, 1)) {
|
switch (between(0, 2)) {
|
||||||
case 0 -> id += randomAlphaOfLengthBetween(1, 5);
|
case 0 -> id += randomAlphaOfLengthBetween(1, 5);
|
||||||
case 1 -> timeout = new TimeValue(timeout.duration() + randomLongBetween(1, 5), timeout.timeUnit());
|
case 1 -> from = from != null ? from.plus(Duration.ofDays(1)) : Instant.ofEpochMilli(randomNonNegativeLong());
|
||||||
|
case 2 -> timeout = new TimeValue(timeout.duration() + randomLongBetween(1, 5), timeout.timeUnit());
|
||||||
default -> throw new AssertionError("Illegal randomization branch");
|
default -> throw new AssertionError("Illegal randomization branch");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Request(id, timeout);
|
return new Request(id, from, timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,18 @@ public class TransformCheckpointTests extends AbstractSerializingTransformTestCa
|
||||||
|
|
||||||
public void testEmpty() {
|
public void testEmpty() {
|
||||||
assertTrue(TransformCheckpoint.EMPTY.isEmpty());
|
assertTrue(TransformCheckpoint.EMPTY.isEmpty());
|
||||||
|
assertTrue(new TransformCheckpoint("_empty", 123L, -1, Collections.emptyMap(), 456L).isEmpty());
|
||||||
assertFalse(new TransformCheckpoint("some_id", 0L, -1, Collections.emptyMap(), 0L).isEmpty());
|
assertFalse(new TransformCheckpoint("some_id", 0L, -1, Collections.emptyMap(), 0L).isEmpty());
|
||||||
|
assertFalse(new TransformCheckpoint("some_id", 0L, 0, Collections.emptyMap(), 0L).isEmpty());
|
||||||
|
assertFalse(new TransformCheckpoint("some_id", 0L, 1, Collections.emptyMap(), 0L).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTransient() {
|
||||||
|
assertTrue(TransformCheckpoint.EMPTY.isTransient());
|
||||||
|
assertTrue(new TransformCheckpoint("_empty", 123L, -1, Collections.emptyMap(), 456L).isTransient());
|
||||||
|
assertTrue(new TransformCheckpoint("some_id", 0L, -1, Collections.emptyMap(), 0L).isTransient());
|
||||||
|
assertFalse(new TransformCheckpoint("some_id", 0L, 0, Collections.emptyMap(), 0L).isTransient());
|
||||||
|
assertFalse(new TransformCheckpoint("some_id", 0L, 1, Collections.emptyMap(), 0L).isTransient());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testGetBehind() {
|
public void testGetBehind() {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.elasticsearch.xcontent.XContentParser;
|
||||||
import org.elasticsearch.xpack.core.transform.AbstractSerializingTransformTestCase;
|
import org.elasticsearch.xpack.core.transform.AbstractSerializingTransformTestCase;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
public class TransformTaskParamsTests extends AbstractSerializingTransformTestCase<TransformTaskParams> {
|
public class TransformTaskParamsTests extends AbstractSerializingTransformTestCase<TransformTaskParams> {
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ public class TransformTaskParamsTests extends AbstractSerializingTransformTestCa
|
||||||
return new TransformTaskParams(
|
return new TransformTaskParams(
|
||||||
randomAlphaOfLengthBetween(1, 10),
|
randomAlphaOfLengthBetween(1, 10),
|
||||||
randomBoolean() ? VersionUtils.randomVersion(random()) : null,
|
randomBoolean() ? VersionUtils.randomVersion(random()) : null,
|
||||||
|
randomBoolean() ? Instant.ofEpochMilli(randomLongBetween(0, 1_000_000_000_000L)) : null,
|
||||||
randomBoolean() ? TimeValue.timeValueSeconds(randomLongBetween(1, 24 * 60 * 60)) : null,
|
randomBoolean() ? TimeValue.timeValueSeconds(randomLongBetween(1, 24 * 60 * 60)) : null,
|
||||||
randomBoolean()
|
randomBoolean()
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.elasticsearch.xcontent.XContentParser;
|
||||||
import org.elasticsearch.xpack.core.transform.AbstractSerializingTransformTestCase;
|
import org.elasticsearch.xpack.core.transform.AbstractSerializingTransformTestCase;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
public class TransformTests extends AbstractSerializingTransformTestCase<TransformTaskParams> {
|
public class TransformTests extends AbstractSerializingTransformTestCase<TransformTaskParams> {
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ public class TransformTests extends AbstractSerializingTransformTestCase<Transfo
|
||||||
return new TransformTaskParams(
|
return new TransformTaskParams(
|
||||||
randomAlphaOfLength(10),
|
randomAlphaOfLength(10),
|
||||||
randomBoolean() ? null : Version.CURRENT,
|
randomBoolean() ? null : Version.CURRENT,
|
||||||
|
randomBoolean() ? Instant.ofEpochMilli(randomLongBetween(0, 1_000_000_000_000L)) : null,
|
||||||
randomBoolean() ? null : TimeValue.timeValueMillis(randomIntBetween(1_000, 3_600_000)),
|
randomBoolean() ? null : TimeValue.timeValueMillis(randomIntBetween(1_000, 3_600_000)),
|
||||||
randomBoolean()
|
randomBoolean()
|
||||||
);
|
);
|
||||||
|
|
|
@ -256,6 +256,31 @@ teardown:
|
||||||
transform_id: "airline-transform-start-stop-continuous"
|
transform_id: "airline-transform-start-stop-continuous"
|
||||||
wait_for_completion: true
|
wait_for_completion: true
|
||||||
- match: { acknowledged: true }
|
- match: { acknowledged: true }
|
||||||
|
|
||||||
|
---
|
||||||
|
"Test start transform with empty value of from parameter":
|
||||||
|
- do:
|
||||||
|
catch: /Failed to parse date for \[from\]/
|
||||||
|
transform.start_transform:
|
||||||
|
from: ""
|
||||||
|
transform_id: "airline-transform-start-stop-continuous"
|
||||||
|
|
||||||
|
---
|
||||||
|
"Test start transform with invalid value of from parameter":
|
||||||
|
- do:
|
||||||
|
catch: /Failed to parse date for \[from\]/
|
||||||
|
transform.start_transform:
|
||||||
|
from: "not-a-valid-timestamp"
|
||||||
|
transform_id: "airline-transform-start-stop-continuous"
|
||||||
|
|
||||||
|
---
|
||||||
|
"Test start batch transform with from parameter":
|
||||||
|
- do:
|
||||||
|
catch: /\[from\] parameter is currently not supported for batch \(non-continuous\) transforms/
|
||||||
|
transform.start_transform:
|
||||||
|
from: "2023-01-11T12:00:00"
|
||||||
|
transform_id: "airline-transform-start-stop"
|
||||||
|
|
||||||
---
|
---
|
||||||
"Test stop missing transform":
|
"Test stop missing transform":
|
||||||
- do:
|
- do:
|
||||||
|
|
|
@ -499,6 +499,82 @@ public class TransformPivotRestIT extends TransformRestTestCase {
|
||||||
assertOnePivotValue(transformIndex + "/_search?q=reviewer:user_42", 2.0);
|
assertOnePivotValue(transformIndex + "/_search?q=reviewer:user_42", 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testContinuousPivotFrom() throws Exception {
|
||||||
|
String indexName = "continuous_reviews_from";
|
||||||
|
createReviewsIndex(indexName);
|
||||||
|
String transformId = "continuous_pivot_from";
|
||||||
|
String transformIndex = "pivot_reviews_continuous_from";
|
||||||
|
setupDataAccessRole(DATA_ACCESS_ROLE, indexName, transformIndex);
|
||||||
|
final Request createTransformRequest = createRequestWithAuth(
|
||||||
|
"PUT",
|
||||||
|
getTransformEndpoint() + transformId,
|
||||||
|
BASIC_AUTH_VALUE_TRANSFORM_ADMIN_WITH_SOME_DATA_ACCESS
|
||||||
|
);
|
||||||
|
String config = Strings.format("""
|
||||||
|
{
|
||||||
|
"source": {
|
||||||
|
"index": "%s"
|
||||||
|
},
|
||||||
|
"dest": {
|
||||||
|
"index": "%s"
|
||||||
|
},
|
||||||
|
"frequency": "1s",
|
||||||
|
"sync": {
|
||||||
|
"time": {
|
||||||
|
"field": "timestamp",
|
||||||
|
"delay": "1s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pivot": {
|
||||||
|
"group_by": {
|
||||||
|
"reviewer": {
|
||||||
|
"terms": {
|
||||||
|
"field": "user_id",
|
||||||
|
"missing_bucket": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aggregations": {
|
||||||
|
"avg_rating": {
|
||||||
|
"avg": {
|
||||||
|
"field": "stars"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}""", indexName, transformIndex);
|
||||||
|
createTransformRequest.setJsonEntity(config);
|
||||||
|
Map<String, Object> createTransformResponse = entityAsMap(client().performRequest(createTransformRequest));
|
||||||
|
assertThat(createTransformResponse.get("acknowledged"), equalTo(Boolean.TRUE));
|
||||||
|
|
||||||
|
final StringBuilder bulk = new StringBuilder();
|
||||||
|
bulk.append(Strings.format("""
|
||||||
|
{"index":{"_index":"%s"}}
|
||||||
|
{"user_id":"user_%s","business_id":"business_%s","stars":%s,"location":"%s","timestamp":%s}
|
||||||
|
""", indexName, 666, 777, 7, 888, "\"2017-01-20\""));
|
||||||
|
bulk.append("\r\n");
|
||||||
|
|
||||||
|
final Request bulkRequest = new Request("POST", "/_bulk");
|
||||||
|
bulkRequest.addParameter("refresh", "true");
|
||||||
|
bulkRequest.setJsonEntity(bulk.toString());
|
||||||
|
Map<String, Object> bulkResponse = entityAsMap(client().performRequest(bulkRequest));
|
||||||
|
assertThat(bulkResponse.get("errors"), equalTo(Boolean.FALSE));
|
||||||
|
|
||||||
|
startAndWaitForContinuousTransform(transformId, transformIndex, null, "2017-01-23", 1L);
|
||||||
|
assertTrue(indexExists(transformIndex));
|
||||||
|
|
||||||
|
assertEquals(27, XContentMapValues.extractValue("_all.total.docs.count", getAsMap(transformIndex + "/_stats")));
|
||||||
|
|
||||||
|
// get and check some users
|
||||||
|
assertOnePivotValue(transformIndex + "/_search?q=reviewer:user_0", 3.776978417);
|
||||||
|
assertOnePivotValue(transformIndex + "/_search?q=reviewer:user_11", 3.846153846);
|
||||||
|
assertOnePivotValue(transformIndex + "/_search?q=reviewer:user_20", 3.769230769);
|
||||||
|
assertOnePivotValue(transformIndex + "/_search?q=reviewer:user_26", 3.918918918);
|
||||||
|
|
||||||
|
stopTransform(transformId, false);
|
||||||
|
deleteIndex(indexName);
|
||||||
|
}
|
||||||
|
|
||||||
public void testHistogramPivot() throws Exception {
|
public void testHistogramPivot() throws Exception {
|
||||||
String transformId = "simple_histogram_pivot";
|
String transformId = "simple_histogram_pivot";
|
||||||
String transformIndex = "pivot_reviews_via_histogram";
|
String transformIndex = "pivot_reviews_via_histogram";
|
||||||
|
|
|
@ -400,15 +400,10 @@ public abstract class TransformRestTestCase extends ESRestTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startTransform(String transformId) throws IOException {
|
protected void startTransform(String transformId) throws IOException {
|
||||||
startTransform(transformId, null);
|
startTransform(transformId, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startTransform(String transformId, String authHeader, String... warnings) throws IOException {
|
protected void startTransform(String transformId, String authHeader, String secondaryAuthHeader, String from, String... warnings)
|
||||||
// start the transform
|
|
||||||
startTransform(transformId, authHeader, null, warnings);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void startTransform(String transformId, String authHeader, String secondaryAuthHeader, String... warnings)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
// start the transform
|
// start the transform
|
||||||
final Request startTransformRequest = createRequestWithSecondaryAuth(
|
final Request startTransformRequest = createRequestWithSecondaryAuth(
|
||||||
|
@ -417,6 +412,9 @@ public abstract class TransformRestTestCase extends ESRestTestCase {
|
||||||
authHeader,
|
authHeader,
|
||||||
secondaryAuthHeader
|
secondaryAuthHeader
|
||||||
);
|
);
|
||||||
|
if (from != null) {
|
||||||
|
startTransformRequest.addParameter(TransformField.FROM.getPreferredName(), from);
|
||||||
|
}
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
startTransformRequest.setOptions(expectWarnings(warnings));
|
startTransformRequest.setOptions(expectWarnings(warnings));
|
||||||
}
|
}
|
||||||
|
@ -462,7 +460,7 @@ public abstract class TransformRestTestCase extends ESRestTestCase {
|
||||||
String... warnings
|
String... warnings
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
// start the transform
|
// start the transform
|
||||||
startTransform(transformId, authHeader, secondaryAuthHeader, warnings);
|
startTransform(transformId, authHeader, secondaryAuthHeader, null, warnings);
|
||||||
assertTrue(indexExists(transformIndex));
|
assertTrue(indexExists(transformIndex));
|
||||||
// wait until the transform has been created and all data is available
|
// wait until the transform has been created and all data is available
|
||||||
waitForTransformCheckpoint(transformId);
|
waitForTransformCheckpoint(transformId);
|
||||||
|
@ -472,13 +470,18 @@ public abstract class TransformRestTestCase extends ESRestTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startAndWaitForContinuousTransform(String transformId, String transformIndex, String authHeader) throws Exception {
|
protected void startAndWaitForContinuousTransform(String transformId, String transformIndex, String authHeader) throws Exception {
|
||||||
startAndWaitForContinuousTransform(transformId, transformIndex, authHeader, 1L);
|
startAndWaitForContinuousTransform(transformId, transformIndex, authHeader, null, 1L);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startAndWaitForContinuousTransform(String transformId, String transformIndex, String authHeader, long checkpoint)
|
protected void startAndWaitForContinuousTransform(
|
||||||
throws Exception {
|
String transformId,
|
||||||
|
String transformIndex,
|
||||||
|
String authHeader,
|
||||||
|
String from,
|
||||||
|
long checkpoint
|
||||||
|
) throws Exception {
|
||||||
// start the transform
|
// start the transform
|
||||||
startTransform(transformId, authHeader, new String[0]);
|
startTransform(transformId, authHeader, null, from, new String[0]);
|
||||||
assertTrue(indexExists(transformIndex));
|
assertTrue(indexExists(transformIndex));
|
||||||
// wait until the transform has been created and all data is available
|
// wait until the transform has been created and all data is available
|
||||||
waitForTransformCheckpoint(transformId, checkpoint);
|
waitForTransformCheckpoint(transformId, checkpoint);
|
||||||
|
|
|
@ -123,6 +123,7 @@ public class TransformOldTransformsIT extends TransformSingleNodeTestCase {
|
||||||
|
|
||||||
StartTransformAction.Request startTransformRequest = new StartTransformAction.Request(
|
StartTransformAction.Request startTransformRequest = new StartTransformAction.Request(
|
||||||
transformId,
|
transformId,
|
||||||
|
null,
|
||||||
AcknowledgedRequest.DEFAULT_ACK_TIMEOUT
|
AcknowledgedRequest.DEFAULT_ACK_TIMEOUT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.elasticsearch.ElasticsearchStatusException;
|
import org.elasticsearch.ElasticsearchStatusException;
|
||||||
import org.elasticsearch.action.ActionListener;
|
import org.elasticsearch.action.ActionListener;
|
||||||
|
import org.elasticsearch.action.ActionRequestValidationException;
|
||||||
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
|
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
|
||||||
import org.elasticsearch.action.support.ActionFilters;
|
import org.elasticsearch.action.support.ActionFilters;
|
||||||
import org.elasticsearch.action.support.IndicesOptions;
|
import org.elasticsearch.action.support.IndicesOptions;
|
||||||
|
@ -21,7 +22,6 @@ import org.elasticsearch.cluster.block.ClusterBlockException;
|
||||||
import org.elasticsearch.cluster.block.ClusterBlockLevel;
|
import org.elasticsearch.cluster.block.ClusterBlockLevel;
|
||||||
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
|
||||||
import org.elasticsearch.cluster.service.ClusterService;
|
import org.elasticsearch.cluster.service.ClusterService;
|
||||||
import org.elasticsearch.common.ValidationException;
|
|
||||||
import org.elasticsearch.common.inject.Inject;
|
import org.elasticsearch.common.inject.Inject;
|
||||||
import org.elasticsearch.core.TimeValue;
|
import org.elasticsearch.core.TimeValue;
|
||||||
import org.elasticsearch.persistent.PersistentTasksCustomMetadata;
|
import org.elasticsearch.persistent.PersistentTasksCustomMetadata;
|
||||||
|
@ -52,6 +52,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import static org.elasticsearch.action.ValidateActions.addValidationError;
|
||||||
import static org.elasticsearch.core.Strings.format;
|
import static org.elasticsearch.core.Strings.format;
|
||||||
import static org.elasticsearch.xpack.core.transform.TransformMessages.CANNOT_START_FAILED_TRANSFORM;
|
import static org.elasticsearch.xpack.core.transform.TransformMessages.CANNOT_START_FAILED_TRANSFORM;
|
||||||
|
|
||||||
|
@ -203,7 +204,13 @@ public class TransportStartTransformAction extends TransportMasterNodeAction<Sta
|
||||||
|
|
||||||
// <2> run transform validations
|
// <2> run transform validations
|
||||||
ActionListener<TransformConfig> getTransformListener = ActionListener.wrap(config -> {
|
ActionListener<TransformConfig> getTransformListener = ActionListener.wrap(config -> {
|
||||||
ValidationException validationException = config.validate(null);
|
ActionRequestValidationException validationException = config.validate(null);
|
||||||
|
if (request.from() != null && config.getSyncConfig() == null) {
|
||||||
|
validationException = addValidationError(
|
||||||
|
"[from] parameter is currently not supported for batch (non-continuous) transforms",
|
||||||
|
validationException
|
||||||
|
);
|
||||||
|
}
|
||||||
if (validationException != null) {
|
if (validationException != null) {
|
||||||
listener.onFailure(
|
listener.onFailure(
|
||||||
new ElasticsearchStatusException(
|
new ElasticsearchStatusException(
|
||||||
|
@ -221,6 +228,7 @@ public class TransportStartTransformAction extends TransportMasterNodeAction<Sta
|
||||||
new TransformTaskParams(
|
new TransformTaskParams(
|
||||||
config.getId(),
|
config.getId(),
|
||||||
config.getVersion(),
|
config.getVersion(),
|
||||||
|
request.from(),
|
||||||
config.getFrequency(),
|
config.getFrequency(),
|
||||||
config.getSource().requiresRemoteCluster()
|
config.getSource().requiresRemoteCluster()
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,16 +7,23 @@
|
||||||
|
|
||||||
package org.elasticsearch.xpack.transform.rest.action;
|
package org.elasticsearch.xpack.transform.rest.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
import org.elasticsearch.action.support.master.AcknowledgedRequest;
|
import org.elasticsearch.action.support.master.AcknowledgedRequest;
|
||||||
import org.elasticsearch.client.internal.node.NodeClient;
|
import org.elasticsearch.client.internal.node.NodeClient;
|
||||||
|
import org.elasticsearch.common.time.DateMathParser;
|
||||||
import org.elasticsearch.core.TimeValue;
|
import org.elasticsearch.core.TimeValue;
|
||||||
|
import org.elasticsearch.index.mapper.DateFieldMapper;
|
||||||
import org.elasticsearch.rest.BaseRestHandler;
|
import org.elasticsearch.rest.BaseRestHandler;
|
||||||
import org.elasticsearch.rest.RestRequest;
|
import org.elasticsearch.rest.RestRequest;
|
||||||
import org.elasticsearch.rest.action.RestToXContentListener;
|
import org.elasticsearch.rest.action.RestToXContentListener;
|
||||||
|
import org.elasticsearch.xcontent.ParseField;
|
||||||
import org.elasticsearch.xpack.core.transform.TransformField;
|
import org.elasticsearch.xpack.core.transform.TransformField;
|
||||||
|
import org.elasticsearch.xpack.core.transform.TransformMessages;
|
||||||
import org.elasticsearch.xpack.core.transform.action.StartTransformAction;
|
import org.elasticsearch.xpack.core.transform.action.StartTransformAction;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.LongSupplier;
|
||||||
|
|
||||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||||
|
|
||||||
|
@ -30,12 +37,24 @@ public class RestStartTransformAction extends BaseRestHandler {
|
||||||
@Override
|
@Override
|
||||||
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
|
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
|
||||||
String id = restRequest.param(TransformField.ID.getPreferredName());
|
String id = restRequest.param(TransformField.ID.getPreferredName());
|
||||||
|
String fromAsString = restRequest.param(TransformField.FROM.getPreferredName());
|
||||||
|
Instant from = fromAsString != null ? parseDateOrThrow(fromAsString, TransformField.FROM, System::currentTimeMillis) : null;
|
||||||
TimeValue timeout = restRequest.paramAsTime(TransformField.TIMEOUT.getPreferredName(), AcknowledgedRequest.DEFAULT_ACK_TIMEOUT);
|
TimeValue timeout = restRequest.paramAsTime(TransformField.TIMEOUT.getPreferredName(), AcknowledgedRequest.DEFAULT_ACK_TIMEOUT);
|
||||||
|
|
||||||
StartTransformAction.Request request = new StartTransformAction.Request(id, timeout);
|
StartTransformAction.Request request = new StartTransformAction.Request(id, from, timeout);
|
||||||
return channel -> client.execute(StartTransformAction.INSTANCE, request, new RestToXContentListener<>(channel));
|
return channel -> client.execute(StartTransformAction.INSTANCE, request, new RestToXContentListener<>(channel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Instant parseDateOrThrow(String date, ParseField paramName, LongSupplier now) {
|
||||||
|
DateMathParser dateMathParser = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser();
|
||||||
|
try {
|
||||||
|
return dateMathParser.parse(date, now);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = TransformMessages.getMessage(TransformMessages.FAILED_TO_PARSE_DATE, paramName.getPreferredName(), date);
|
||||||
|
throw new ElasticsearchParseException(msg, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "transform_start_transform_action";
|
return "transform_start_transform_action";
|
||||||
|
|
|
@ -52,7 +52,10 @@ class ClientTransformIndexerBuilder {
|
||||||
initialStats,
|
initialStats,
|
||||||
transformConfig,
|
transformConfig,
|
||||||
progress,
|
progress,
|
||||||
TransformCheckpoint.isNullOrEmpty(lastCheckpoint) ? TransformCheckpoint.EMPTY : lastCheckpoint,
|
// If there already exists at least one checkpoint, the "from" setting is effectively ignored.
|
||||||
|
TransformCheckpoint.isNullOrEmpty(lastCheckpoint)
|
||||||
|
? (context.from() != null ? TransformCheckpoint.createEmpty(context.from().toEpochMilli()) : TransformCheckpoint.EMPTY)
|
||||||
|
: lastCheckpoint,
|
||||||
TransformCheckpoint.isNullOrEmpty(nextCheckpoint) ? TransformCheckpoint.EMPTY : nextCheckpoint,
|
TransformCheckpoint.isNullOrEmpty(nextCheckpoint) ? TransformCheckpoint.EMPTY : nextCheckpoint,
|
||||||
seqNoPrimaryTermAndIndex,
|
seqNoPrimaryTermAndIndex,
|
||||||
context,
|
context,
|
||||||
|
|
|
@ -47,10 +47,17 @@ public class TransformContext {
|
||||||
// Note: Each indexer run creates a new future checkpoint which becomes the current checkpoint only after the indexer run finished
|
// Note: Each indexer run creates a new future checkpoint which becomes the current checkpoint only after the indexer run finished
|
||||||
private final AtomicLong currentCheckpoint;
|
private final AtomicLong currentCheckpoint;
|
||||||
|
|
||||||
|
private final Instant from;
|
||||||
|
|
||||||
public TransformContext(TransformTaskState taskState, String stateReason, long currentCheckpoint, Listener taskListener) {
|
public TransformContext(TransformTaskState taskState, String stateReason, long currentCheckpoint, Listener taskListener) {
|
||||||
|
this(taskState, stateReason, currentCheckpoint, null, taskListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformContext(TransformTaskState taskState, String stateReason, long currentCheckpoint, Instant from, Listener taskListener) {
|
||||||
this.taskState = new AtomicReference<>(taskState);
|
this.taskState = new AtomicReference<>(taskState);
|
||||||
this.stateReason = new AtomicReference<>(stateReason);
|
this.stateReason = new AtomicReference<>(stateReason);
|
||||||
this.currentCheckpoint = new AtomicLong(currentCheckpoint);
|
this.currentCheckpoint = new AtomicLong(currentCheckpoint);
|
||||||
|
this.from = from;
|
||||||
this.taskListener = taskListener;
|
this.taskListener = taskListener;
|
||||||
this.failureCount = new AtomicInteger(0);
|
this.failureCount = new AtomicInteger(0);
|
||||||
}
|
}
|
||||||
|
@ -99,6 +106,10 @@ public class TransformContext {
|
||||||
return currentCheckpoint.get();
|
return currentCheckpoint.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Instant from() {
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
long incrementAndGetCheckpoint() {
|
long incrementAndGetCheckpoint() {
|
||||||
return currentCheckpoint.incrementAndGet();
|
return currentCheckpoint.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1097,7 +1097,7 @@ public abstract class TransformIndexer extends AsyncTwoPhaseIndexer<TransformInd
|
||||||
.filter(config.getSyncConfig().getRangeQuery(nextCheckpoint));
|
.filter(config.getSyncConfig().getRangeQuery(nextCheckpoint));
|
||||||
|
|
||||||
// Only apply extra filter if it is the subsequent run of the continuous transform
|
// Only apply extra filter if it is the subsequent run of the continuous transform
|
||||||
if (nextCheckpoint.getCheckpoint() > 1 && changeCollector != null) {
|
if (changeCollector != null) {
|
||||||
QueryBuilder filter = changeCollector.buildFilterQuery(lastCheckpoint, nextCheckpoint);
|
QueryBuilder filter = changeCollector.buildFilterQuery(lastCheckpoint, nextCheckpoint);
|
||||||
if (filter != null) {
|
if (filter != null) {
|
||||||
filteredQuery.filter(filter);
|
filteredQuery.filter(filter);
|
||||||
|
@ -1154,6 +1154,10 @@ public abstract class TransformIndexer extends AsyncTwoPhaseIndexer<TransformInd
|
||||||
}
|
}
|
||||||
|
|
||||||
private RunState determineRunStateAtStart() {
|
private RunState determineRunStateAtStart() {
|
||||||
|
if (context.from() != null) {
|
||||||
|
return RunState.IDENTIFY_CHANGES;
|
||||||
|
}
|
||||||
|
|
||||||
// either 1st run or not a continuous transform
|
// either 1st run or not a continuous transform
|
||||||
if (nextCheckpoint.getCheckpoint() == 1 || isContinuous() == false) {
|
if (nextCheckpoint.getCheckpoint() == 1 || isContinuous() == false) {
|
||||||
return RunState.APPLY_RESULTS;
|
return RunState.APPLY_RESULTS;
|
||||||
|
|
|
@ -114,7 +114,7 @@ public class TransformTask extends AllocatedPersistentTask implements TransformS
|
||||||
this.initialIndexerState = initialState;
|
this.initialIndexerState = initialState;
|
||||||
this.initialPosition = initialPosition;
|
this.initialPosition = initialPosition;
|
||||||
|
|
||||||
this.context = new TransformContext(initialTaskState, initialReason, initialCheckpoint, this);
|
this.context = new TransformContext(initialTaskState, initialReason, initialCheckpoint, transform.from(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParentTaskAssigningClient getParentTaskClient() {
|
public ParentTaskAssigningClient getParentTaskClient() {
|
||||||
|
|
|
@ -384,6 +384,10 @@ public class CompositeBucketsChangeCollector implements ChangeCollector {
|
||||||
if (missingBucket) {
|
if (missingBucket) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// filterByChanges has been called before collectChangesFromAggregations
|
||||||
|
if (lowerBound == 0 && upperBound == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return new RangeQueryBuilder(sourceFieldName).gte(lowerBound).lte(upperBound).format("epoch_millis");
|
return new RangeQueryBuilder(sourceFieldName).gte(lowerBound).lte(upperBound).format("epoch_millis");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,11 +484,15 @@ public class CompositeBucketsChangeCollector implements ChangeCollector {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public QueryBuilder filterByChanges(long lastCheckpointTimestamp, long nextcheckpointTimestamp) {
|
public QueryBuilder filterByChanges(long lastCheckpointTimestamp, long nextcheckpointTimestamp) {
|
||||||
|
|
||||||
if (missingBucket) {
|
if (missingBucket) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// filterByChanges has been called before collectChangesFromAggregations
|
||||||
// (upperBound - lowerBound) >= interval, so never 0
|
if (lowerBound == 0 && upperBound == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// (upperBound - lowerBound) >= interval, so never 0.
|
||||||
if ((maxUpperBound - minLowerBound) / (upperBound - lowerBound) < MIN_CUT_OFF) {
|
if ((maxUpperBound - minLowerBound) / (upperBound - lowerBound) < MIN_CUT_OFF) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.xpack.transform.rest.action;
|
||||||
|
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.client.internal.node.NodeClient;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.elasticsearch.test.rest.FakeRestRequest;
|
||||||
|
import org.elasticsearch.xcontent.NamedXContentRegistry;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class RestStartTransformActionTests extends ESTestCase {
|
||||||
|
|
||||||
|
private static final String ID = "id";
|
||||||
|
private static final String FROM = "from";
|
||||||
|
|
||||||
|
public void testFromValid() throws Exception {
|
||||||
|
testFromValid(null);
|
||||||
|
testFromValid("12345678");
|
||||||
|
testFromValid("2022-10-25");
|
||||||
|
testFromValid("now-1d");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testFromValid(String from) {
|
||||||
|
RestStartTransformAction handler = new RestStartTransformAction();
|
||||||
|
FakeRestRequest.Builder requestBuilder = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY);
|
||||||
|
if (from == null) {
|
||||||
|
requestBuilder.withParams(Map.of(ID, "my-id"));
|
||||||
|
} else {
|
||||||
|
requestBuilder.withParams(Map.of(ID, "my-id", FROM, from));
|
||||||
|
}
|
||||||
|
FakeRestRequest request = requestBuilder.build();
|
||||||
|
handler.prepareRequest(request, mock(NodeClient.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFromInvalid() {
|
||||||
|
testFromInvalid("");
|
||||||
|
testFromInvalid("not-a-valid-timestamp");
|
||||||
|
testFromInvalid("2023-17-42");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testFromInvalid(String from) {
|
||||||
|
final RestStartTransformAction handler = new RestStartTransformAction();
|
||||||
|
final FakeRestRequest request = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withParams(Map.of(ID, "my-id", FROM, from))
|
||||||
|
.build();
|
||||||
|
ElasticsearchParseException e = expectThrows(
|
||||||
|
ElasticsearchParseException.class,
|
||||||
|
() -> handler.prepareRequest(request, mock(NodeClient.class))
|
||||||
|
);
|
||||||
|
assertThat(e.getMessage(), equalTo("Failed to parse date for [from]"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,8 @@ import org.elasticsearch.xpack.core.transform.transforms.TransformTaskState;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
@ -88,4 +90,10 @@ public class TransformContextTests extends ESTestCase {
|
||||||
|
|
||||||
verify(listener).failureCountChanged();
|
verify(listener).failureCountChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testFrom() {
|
||||||
|
Instant from = Instant.ofEpochMilli(randomLongBetween(0, 1_000_000_000_000L));
|
||||||
|
TransformContext context = new TransformContext(TransformTaskState.STARTED, null, 0, from, listener);
|
||||||
|
assertThat(context.from(), is(equalTo(from)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue