Skip to content

Commit b366fb9

Browse files
authored
Use setIfSeqNo(...) and setIfPrimaryTerm(...) for updating watch status if all nodes are at least on 6.7.0 (#40888)
Only use UpdateRequest#setIfSeqNo(...) and UpdateRequest#setIfPrimaryTerm(...) for updating watch status if all nodes are at least on 6.7.0. Otherwise fallback using UpdateRequest#version(...) Closes #40841
1 parent 70cca88 commit b366fb9

File tree

8 files changed

+154
-4
lines changed

8 files changed

+154
-4
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/Watch.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class Watch implements ToXContentObject {
4040

4141
private final long sourceSeqNo;
4242
private final long sourcePrimaryTerm;
43+
private transient long version;
4344

4445
public Watch(String id, Trigger trigger, ExecutableInput input, ExecutableCondition condition, @Nullable ExecutableTransform transform,
4546
@Nullable TimeValue throttlePeriod, List<ActionWrapper> actions, @Nullable Map<String, Object> metadata,
@@ -107,6 +108,14 @@ public long getSourcePrimaryTerm() {
107108
return sourcePrimaryTerm;
108109
}
109110

111+
public long version() {
112+
return version;
113+
}
114+
115+
public void version(long version) {
116+
this.version = version;
117+
}
118+
110119
/**
111120
* Sets the state of this watch to in/active
112121
*

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherIndexingListener.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public Engine.Index preIndex(ShardId shardId, Engine.Index operation) {
102102
try {
103103
Watch watch = parser.parseWithSecrets(operation.id(), true, operation.source(), now, XContentType.JSON,
104104
operation.getIfSeqNo(), operation.getIfPrimaryTerm());
105+
watch.version(operation.version());
105106
ShardAllocationConfiguration shardAllocationConfiguration = configuration.localShards.get(shardId);
106107
if (shardAllocationConfiguration == null) {
107108
logger.debug("no distributed watch execution info found for watch [{}] on shard [{}], got configuration for {}",

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/execution/ExecutionService.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.apache.logging.log4j.util.Supplier;
1111
import org.elasticsearch.ExceptionsHelper;
1212
import org.elasticsearch.ResourceNotFoundException;
13+
import org.elasticsearch.Version;
1314
import org.elasticsearch.action.ActionListener;
1415
import org.elasticsearch.action.bulk.BulkItemResponse;
1516
import org.elasticsearch.action.bulk.BulkResponse;
@@ -278,8 +279,10 @@ record = ctx.abortBeforeExecution(ExecutionState.NOT_EXECUTED_ALREADY_QUEUED, "W
278279
if (resp.isExists() == false) {
279280
throw new ResourceNotFoundException("watch [{}] does not exist", watchId);
280281
}
281-
return parser.parseWithSecrets(watchId, true, resp.getSourceAsBytesRef(), ctx.executionTime(), XContentType.JSON,
282-
resp.getSeqNo(), resp.getPrimaryTerm());
282+
Watch watch = parser.parseWithSecrets(watchId, true, resp.getSourceAsBytesRef(), ctx.executionTime(),
283+
XContentType.JSON, resp.getSeqNo(), resp.getPrimaryTerm());
284+
watch.version(resp.getVersion());
285+
return watch;
283286
});
284287
} catch (ResourceNotFoundException e) {
285288
String message = "unable to find watch for record [" + ctx.id() + "]";
@@ -350,8 +353,13 @@ public void updateWatchStatus(Watch watch) throws IOException {
350353

351354
UpdateRequest updateRequest = new UpdateRequest(Watch.INDEX, Watch.DOC_TYPE, watch.id());
352355
updateRequest.doc(source);
353-
updateRequest.setIfSeqNo(watch.getSourceSeqNo());
354-
updateRequest.setIfPrimaryTerm(watch.getSourcePrimaryTerm());
356+
boolean useSeqNoForCAS = clusterService.state().nodes().getMinNodeVersion().onOrAfter(Version.V_6_7_0);
357+
if (useSeqNoForCAS) {
358+
updateRequest.setIfSeqNo(watch.getSourceSeqNo());
359+
updateRequest.setIfPrimaryTerm(watch.getSourcePrimaryTerm());
360+
} else {
361+
updateRequest.version(watch.version());
362+
}
355363
try (ThreadContext.StoredContext ignore = stashWithOrigin(client.threadPool().getThreadContext(), WATCHER_ORIGIN)) {
356364
client.update(updateRequest).actionGet(indexDefaultTimeout);
357365
} catch (DocumentMissingException e) {

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/ack/TransportAckWatchAction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ protected void masterOperation(AckWatchRequest request, ClusterState state,
9393
DateTime now = new DateTime(clock.millis(), UTC);
9494
Watch watch = parser.parseWithSecrets(request.getWatchId(), true, getResponse.getSourceAsBytesRef(),
9595
now, XContentType.JSON, getResponse.getSeqNo(), getResponse.getPrimaryTerm());
96+
watch.version(getResponse.getVersion());
9697
watch.status().version(getResponse.getVersion());
9798
String[] actionIds = request.getActionIds();
9899
if (actionIds == null || actionIds.length == 0) {

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/activate/TransportActivateWatchAction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ protected void masterOperation(ActivateWatchRequest request, ClusterState state,
9595
if (getResponse.isExists()) {
9696
Watch watch = parser.parseWithSecrets(request.getWatchId(), true, getResponse.getSourceAsBytesRef(), now,
9797
XContentType.JSON, getResponse.getSeqNo(), getResponse.getPrimaryTerm());
98+
watch.version(getResponse.getVersion());
9899
watch.status().version(getResponse.getVersion());
99100
// if we are not yet running in distributed mode, only call triggerservice, if we are on the master node
100101
if (localExecute(request) == false && this.clusterService.state().nodes().isLocalNodeElectedMaster()) {

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/get/TransportGetWatchAction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ protected void masterOperation(GetWatchRequest request, ClusterState state,
7373
DateTime now = new DateTime(clock.millis(), UTC);
7474
Watch watch = parser.parseWithSecrets(request.getId(), true, getResponse.getSourceAsBytesRef(), now,
7575
XContentType.JSON, getResponse.getSeqNo(), getResponse.getPrimaryTerm());
76+
watch.version(getRequest.version());
7677
watch.toXContent(builder, WatcherParams.builder()
7778
.hideSecrets(true)
7879
.includeStatus(false)

x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
import org.elasticsearch.action.update.UpdateRequest;
1515
import org.elasticsearch.action.update.UpdateResponse;
1616
import org.elasticsearch.client.Client;
17+
import org.elasticsearch.cluster.ClusterName;
18+
import org.elasticsearch.cluster.ClusterState;
1719
import org.elasticsearch.cluster.node.DiscoveryNode;
20+
import org.elasticsearch.cluster.node.DiscoveryNodes;
1821
import org.elasticsearch.cluster.service.ClusterService;
1922
import org.elasticsearch.common.collect.Tuple;
2023
import org.elasticsearch.common.settings.Settings;
@@ -150,6 +153,9 @@ public void init() throws Exception {
150153
DiscoveryNode discoveryNode = new DiscoveryNode("node_1", ESTestCase.buildNewFakeTransportAddress(), Collections.emptyMap(),
151154
new HashSet<>(asList(DiscoveryNode.Role.values())), Version.CURRENT);
152155
ClusterService clusterService = mock(ClusterService.class);
156+
when(clusterService.state()).thenReturn(ClusterState.builder(new ClusterName("cluster"))
157+
.nodes(DiscoveryNodes.builder().add(discoveryNode).build())
158+
.build());
153159
when(clusterService.localNode()).thenReturn(discoveryNode);
154160

155161
executionService = new ExecutionService(Settings.EMPTY, historyStore, triggeredWatchStore, executor, clock, parser,
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.upgrades;
7+
8+
import org.elasticsearch.Version;
9+
import org.elasticsearch.client.Request;
10+
import org.elasticsearch.client.Response;
11+
import org.elasticsearch.cluster.routing.Murmur3HashFunction;
12+
import org.elasticsearch.common.Strings;
13+
import org.elasticsearch.common.UUIDs;
14+
import org.elasticsearch.common.settings.Settings;
15+
import org.elasticsearch.common.xcontent.XContentType;
16+
import org.elasticsearch.xpack.core.watcher.condition.AlwaysCondition;
17+
18+
import java.io.IOException;
19+
import java.util.Map;
20+
import java.util.concurrent.TimeUnit;
21+
22+
import static org.elasticsearch.xpack.watcher.actions.ActionBuilders.loggingAction;
23+
import static org.elasticsearch.xpack.watcher.client.WatchSourceBuilders.watchBuilder;
24+
import static org.elasticsearch.xpack.watcher.input.InputBuilders.simpleInput;
25+
import static org.elasticsearch.xpack.watcher.trigger.TriggerBuilders.schedule;
26+
import static org.elasticsearch.xpack.watcher.trigger.schedule.Schedules.interval;
27+
import static org.hamcrest.Matchers.equalTo;
28+
import static org.hamcrest.Matchers.greaterThan;
29+
30+
/**
31+
* This rolling upgrade node tests whether watcher is able to update the watch status after execution in a mixed cluster.
32+
*
33+
* Versions before 6.7.0 the watch status was using the version to do optimistic locking, after 6.7.0 sequence number and
34+
* primary term are used. The problem was that bwc logic was forgotten to be added, so in a mixed versions cluster, when
35+
* a watch is executed and its watch status is updated then an update request using sequence number / primary term as
36+
* way to do optimistic locking can be sent to nodes that don't support this.
37+
*
38+
* This test tries to simulate a situation where the bug manifests. This requires watches to be run by multiple nodes
39+
* holding a .watches index shard.
40+
*/
41+
public class WatcherUpgradeIT extends AbstractUpgradeTestCase {
42+
43+
public void testWatchesKeepRunning() throws Exception {
44+
if (UPGRADED_FROM_VERSION.before(Version.V_6_0_0)) {
45+
logger.info("Skipping test. Upgrading from before 6.0 makes this test too complicated.");
46+
return;
47+
}
48+
49+
final int numWatches = 16;
50+
51+
if (CLUSTER_TYPE.equals(ClusterType.OLD)) {
52+
final String watch = watchBuilder()
53+
.trigger(schedule(interval("5s")))
54+
.input(simpleInput())
55+
.condition(AlwaysCondition.INSTANCE)
56+
.addAction("_action1", loggingAction("{{ctx.watch_id}}"))
57+
.buildAsBytes(XContentType.JSON)
58+
.utf8ToString();
59+
60+
for (int i = 0; i < numWatches; i++) {
61+
// Using a random id helps to distribute the watches between watcher services on the different nodes with
62+
// a .watches index shard:
63+
String id = UUIDs.randomBase64UUID();
64+
logger.info("Adding watch [{}/{}]", id, Math.floorMod(Murmur3HashFunction.hash(id), 3));
65+
Request putWatchRequest = new Request("PUT", "/_xpack/watcher/watch/" + id);
66+
putWatchRequest.setJsonEntity(watch);
67+
assertOK(client().performRequest(putWatchRequest));
68+
69+
if (i == 0) {
70+
// Increasing the number of replicas to makes it more likely that an upgraded node sends an
71+
// update request (in order to update watch status) to a non upgraded node.
72+
Request updateSettingsRequest = new Request("PUT", "/.watches/_settings");
73+
updateSettingsRequest.setJsonEntity(Strings.toString(Settings.builder()
74+
.put("index.number_of_replicas", 2)
75+
.put("index.auto_expand_replicas", (String) null)
76+
.build()));
77+
assertOK(client().performRequest(updateSettingsRequest));
78+
ensureAllWatchesIndexShardsStarted();
79+
}
80+
}
81+
} else {
82+
ensureAllWatchesIndexShardsStarted();
83+
// Restarting watcher helps to ensure that after a node upgrade each node will be executing watches:
84+
// (and not that a non upgraded node is in charge of watches that an upgraded node should run)
85+
assertOK(client().performRequest(new Request("POST", "/_xpack/watcher/_stop")));
86+
assertOK(client().performRequest(new Request("POST", "/_xpack/watcher/_start")));
87+
88+
// Casually checking whether watches are executing:
89+
for (int i = 0; i < 10; i++) {
90+
int previous = getWatchHistoryEntriesCount();
91+
assertBusy(() -> {
92+
Integer totalHits = getWatchHistoryEntriesCount();
93+
assertThat(totalHits, greaterThan(previous));
94+
}, 30, TimeUnit.SECONDS);
95+
}
96+
}
97+
}
98+
99+
private int getWatchHistoryEntriesCount() throws IOException {
100+
Request refreshRequest = new Request("POST", "/.watcher-history-*/_refresh");
101+
assertOK(client().performRequest(refreshRequest));
102+
103+
Request searchRequest = new Request("GET", "/.watcher-history-*/_search");
104+
searchRequest.setJsonEntity("{\"query\": {\"match\": {\"state\": {\"query\": \"executed\"}}}}");
105+
106+
Response response = client().performRequest(searchRequest);
107+
assertEquals(200, response.getStatusLine().getStatusCode());
108+
Map<String, Object> responseBody = entityAsMap(response);
109+
return (Integer) ((Map<?, ?>) responseBody.get("hits")).get("total");
110+
}
111+
112+
private void ensureAllWatchesIndexShardsStarted() throws Exception {
113+
assertBusy(() -> {
114+
Request request = new Request("GET", "/_cluster/health/.watches");
115+
Response response = client().performRequest(request);
116+
assertEquals(200, response.getStatusLine().getStatusCode());
117+
Map<String, Object> responseBody = entityAsMap(response);
118+
int activeShards = (int) responseBody.get("active_shards");
119+
assertThat(activeShards, equalTo(3));
120+
}, 30, TimeUnit.SECONDS);
121+
}
122+
123+
}

0 commit comments

Comments
 (0)