Skip to content

Commit d213efd

Browse files
authored
Add a new index setting to skip recovery source when synthetic source is enabled (#114618)
This change adds a new undocumented index settings that allows to use synthetic source for recovery and CCR without storing a recovery source.
1 parent 16c4e14 commit d213efd

File tree

46 files changed

+2032
-897
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2032
-897
lines changed

docs/changelog/114618.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 114618
2+
summary: Add a new index setting to skip recovery source when synthetic source is enabled
3+
area: Logs
4+
type: enhancement
5+
issues: []

server/src/internalClusterTest/java/org/elasticsearch/action/admin/indices/create/CloneIndexIT.java

+47
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.common.ValidationException;
1616
import org.elasticsearch.common.settings.Settings;
1717
import org.elasticsearch.index.IndexVersion;
18+
import org.elasticsearch.index.IndexVersions;
1819
import org.elasticsearch.index.query.TermsQueryBuilder;
1920
import org.elasticsearch.index.seqno.SeqNoStats;
2021
import org.elasticsearch.test.ESIntegTestCase;
@@ -26,6 +27,7 @@
2627
import static org.elasticsearch.action.admin.indices.create.ShrinkIndexIT.assertNoResizeSourceIndexSettings;
2728
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
2829
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
30+
import static org.hamcrest.Matchers.anyOf;
2931
import static org.hamcrest.Matchers.containsString;
3032
import static org.hamcrest.Matchers.equalTo;
3133

@@ -143,6 +145,51 @@ public void testResizeChangeSyntheticSource() {
143145
assertThat(error.getMessage(), containsString("can't change setting [index.mapping.source.mode] during resize"));
144146
}
145147

148+
public void testResizeChangeRecoveryUseSyntheticSource() {
149+
prepareCreate("source").setSettings(
150+
indexSettings(between(1, 5), 0).put("index.mode", "logsdb")
151+
.put(
152+
"index.version.created",
153+
IndexVersionUtils.randomVersionBetween(
154+
random(),
155+
IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY,
156+
IndexVersion.current()
157+
)
158+
)
159+
).setMapping("@timestamp", "type=date", "host.name", "type=keyword").get();
160+
updateIndexSettings(Settings.builder().put("index.blocks.write", true), "source");
161+
IllegalArgumentException error = expectThrows(IllegalArgumentException.class, () -> {
162+
indicesAdmin().prepareResizeIndex("source", "target")
163+
.setResizeType(ResizeType.CLONE)
164+
.setSettings(
165+
Settings.builder()
166+
.put(
167+
"index.version.created",
168+
IndexVersionUtils.randomVersionBetween(
169+
random(),
170+
IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY,
171+
IndexVersion.current()
172+
)
173+
)
174+
.put("index.recovery.use_synthetic_source", true)
175+
.put("index.mode", "logsdb")
176+
.putNull("index.blocks.write")
177+
.build()
178+
)
179+
.get();
180+
});
181+
// The index.recovery.use_synthetic_source setting requires either index.mode or index.mapping.source.mode
182+
// to be present in the settings. Since these are all unmodifiable settings with a non-deterministic evaluation
183+
// order, any of them may trigger a failure first.
184+
assertThat(
185+
error.getMessage(),
186+
anyOf(
187+
containsString("can't change setting [index.mode] during resize"),
188+
containsString("can't change setting [index.recovery.use_synthetic_source] during resize")
189+
)
190+
);
191+
}
192+
146193
public void testResizeChangeIndexSorts() {
147194
prepareCreate("source").setSettings(indexSettings(between(1, 5), 0))
148195
.setMapping("@timestamp", "type=date", "host.name", "type=keyword")

server/src/internalClusterTest/java/org/elasticsearch/index/shard/IndexShardIT.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,15 @@ public void testShardChangesWithDefaultDocType() throws Exception {
715715
}
716716
IndexShard shard = indexService.getShard(0);
717717
try (
718-
Translog.Snapshot luceneSnapshot = shard.newChangesSnapshot("test", 0, numOps - 1, true, randomBoolean(), randomBoolean());
718+
Translog.Snapshot luceneSnapshot = shard.newChangesSnapshot(
719+
"test",
720+
0,
721+
numOps - 1,
722+
true,
723+
randomBoolean(),
724+
randomBoolean(),
725+
randomLongBetween(1, ByteSizeValue.ofMb(32).getBytes())
726+
);
719727
Translog.Snapshot translogSnapshot = getTranslog(shard).newSnapshot()
720728
) {
721729
List<Translog.Operation> opsFromLucene = TestTranslog.drainSnapshot(luceneSnapshot, true);

server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@
156156
import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED;
157157
import static org.elasticsearch.indices.IndexingMemoryController.SHARD_INACTIVE_TIME_SETTING;
158158
import static org.elasticsearch.node.NodeRoleSettings.NODE_ROLES_SETTING;
159-
import static org.elasticsearch.node.RecoverySettingsChunkSizePlugin.CHUNK_SIZE_SETTING;
160159
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
161160
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
162161
import static org.hamcrest.Matchers.empty;
@@ -257,7 +256,7 @@ private void assertOnGoingRecoveryState(
257256
public Settings.Builder createRecoverySettingsChunkPerSecond(long chunkSizeBytes) {
258257
return Settings.builder()
259258
// Set the chunk size in bytes
260-
.put(CHUNK_SIZE_SETTING.getKey(), new ByteSizeValue(chunkSizeBytes, ByteSizeUnit.BYTES))
259+
.put(RecoverySettings.INDICES_RECOVERY_CHUNK_SIZE.getKey(), new ByteSizeValue(chunkSizeBytes, ByteSizeUnit.BYTES))
261260
// Set one chunk of bytes per second.
262261
.put(RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING.getKey(), chunkSizeBytes, ByteSizeUnit.BYTES);
263262
}
@@ -280,7 +279,7 @@ private void unthrottleRecovery() {
280279
Settings.builder()
281280
// 200mb is an arbitrary number intended to be large enough to avoid more throttling.
282281
.put(RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING.getKey(), "200mb")
283-
.put(CHUNK_SIZE_SETTING.getKey(), RecoverySettings.DEFAULT_CHUNK_SIZE)
282+
.put(RecoverySettings.INDICES_RECOVERY_CHUNK_SIZE.getKey(), RecoverySettings.DEFAULT_CHUNK_SIZE)
284283
);
285284
}
286285

server/src/internalClusterTest/java/org/elasticsearch/recovery/TruncatedRecoveryIT.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.elasticsearch.indices.recovery.PeerRecoveryTargetService;
2525
import org.elasticsearch.indices.recovery.RecoveryFileChunkRequest;
2626
import org.elasticsearch.indices.recovery.RecoveryFilesInfoRequest;
27-
import org.elasticsearch.node.RecoverySettingsChunkSizePlugin;
27+
import org.elasticsearch.indices.recovery.RecoverySettings;
2828
import org.elasticsearch.plugins.Plugin;
2929
import org.elasticsearch.test.ESIntegTestCase;
3030
import org.elasticsearch.test.transport.MockTransportService;
@@ -41,7 +41,6 @@
4141
import java.util.concurrent.atomic.AtomicBoolean;
4242
import java.util.function.Function;
4343

44-
import static org.elasticsearch.node.RecoverySettingsChunkSizePlugin.CHUNK_SIZE_SETTING;
4544
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
4645
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
4746
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
@@ -52,7 +51,7 @@ public class TruncatedRecoveryIT extends ESIntegTestCase {
5251

5352
@Override
5453
protected Collection<Class<? extends Plugin>> nodePlugins() {
55-
return Arrays.asList(MockTransportService.TestPlugin.class, RecoverySettingsChunkSizePlugin.class);
54+
return Arrays.asList(MockTransportService.TestPlugin.class);
5655
}
5756

5857
/**
@@ -63,7 +62,11 @@ protected Collection<Class<? extends Plugin>> nodePlugins() {
6362
*/
6463
public void testCancelRecoveryAndResume() throws Exception {
6564
updateClusterSettings(
66-
Settings.builder().put(CHUNK_SIZE_SETTING.getKey(), new ByteSizeValue(randomIntBetween(50, 300), ByteSizeUnit.BYTES))
65+
Settings.builder()
66+
.put(
67+
RecoverySettings.INDICES_RECOVERY_CHUNK_SIZE.getKey(),
68+
new ByteSizeValue(randomIntBetween(50, 300), ByteSizeUnit.BYTES)
69+
)
6770
);
6871

6972
NodesStatsResponse nodeStats = clusterAdmin().prepareNodesStats().get();

server/src/internalClusterTest/java/org/elasticsearch/snapshots/RestoreSnapshotIT.java

+18
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,24 @@ public void testRestoreChangeSyntheticSource() {
809809
assertThat(error.getMessage(), containsString("cannot modify setting [index.mapping.source.mode] on restore"));
810810
}
811811

812+
public void testRestoreChangeRecoveryUseSyntheticSource() {
813+
Client client = client();
814+
createRepository("test-repo", "fs");
815+
String indexName = "test-idx";
816+
assertAcked(client.admin().indices().prepareCreate(indexName).setSettings(Settings.builder().put(indexSettings())));
817+
createSnapshot("test-repo", "test-snap", Collections.singletonList(indexName));
818+
cluster().wipeIndices(indexName);
819+
var error = expectThrows(SnapshotRestoreException.class, () -> {
820+
client.admin()
821+
.cluster()
822+
.prepareRestoreSnapshot(TEST_REQUEST_TIMEOUT, "test-repo", "test-snap")
823+
.setIndexSettings(Settings.builder().put("index.recovery.use_synthetic_source", true))
824+
.setWaitForCompletion(true)
825+
.get();
826+
});
827+
assertThat(error.getMessage(), containsString("cannot modify setting [index.recovery.use_synthetic_source] on restore"));
828+
}
829+
812830
public void testRestoreChangeIndexSorts() {
813831
Client client = client();
814832
createRepository("test-repo", "fs");

server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java

+1
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,7 @@ static void validateCloneIndex(
15911591
private static final Set<String> UNMODIFIABLE_SETTINGS_DURING_RESIZE = Set.of(
15921592
IndexSettings.MODE.getKey(),
15931593
SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(),
1594+
IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey(),
15941595
IndexSortConfig.INDEX_SORT_FIELD_SETTING.getKey(),
15951596
IndexSortConfig.INDEX_SORT_ORDER_SETTING.getKey(),
15961597
IndexSortConfig.INDEX_SORT_MODE_SETTING.getKey(),

server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java

+1
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ public void apply(Settings value, Settings current, Settings previous) {
257257
RecoverySettings.INDICES_RECOVERY_USE_SNAPSHOTS_SETTING,
258258
RecoverySettings.INDICES_RECOVERY_MAX_CONCURRENT_SNAPSHOT_FILE_DOWNLOADS,
259259
RecoverySettings.INDICES_RECOVERY_MAX_CONCURRENT_SNAPSHOT_FILE_DOWNLOADS_PER_NODE,
260+
RecoverySettings.INDICES_RECOVERY_CHUNK_SIZE,
260261
RecoverySettings.NODE_BANDWIDTH_RECOVERY_FACTOR_READ_SETTING,
261262
RecoverySettings.NODE_BANDWIDTH_RECOVERY_FACTOR_WRITE_SETTING,
262263
RecoverySettings.NODE_BANDWIDTH_RECOVERY_OPERATOR_FACTOR_SETTING,

server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java

+1
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ public final class IndexScopedSettings extends AbstractScopedSettings {
188188
IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_WRITE_SETTING,
189189
IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_READ_SETTING,
190190
SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING,
191+
IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING,
191192

192193
// validate that built-in similarities don't get redefined
193194
Setting.groupSetting("index.similarity.", (s) -> {

server/src/main/java/org/elasticsearch/index/IndexSettings.java

+68-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.Collections;
3939
import java.util.Iterator;
4040
import java.util.List;
41+
import java.util.Locale;
4142
import java.util.Map;
4243
import java.util.concurrent.TimeUnit;
4344
import java.util.function.Consumer;
@@ -51,6 +52,7 @@
5152
import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING;
5253
import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_NESTED_FIELDS_LIMIT_SETTING;
5354
import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING;
55+
import static org.elasticsearch.index.mapper.SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING;
5456

5557
/**
5658
* This class encapsulates all index level settings and handles settings updates.
@@ -653,6 +655,62 @@ public Iterator<Setting<?>> settings() {
653655
Property.Final
654656
);
655657

658+
public static final Setting<Boolean> RECOVERY_USE_SYNTHETIC_SOURCE_SETTING = Setting.boolSetting(
659+
"index.recovery.use_synthetic_source",
660+
false,
661+
new Setting.Validator<>() {
662+
@Override
663+
public void validate(Boolean value) {}
664+
665+
@Override
666+
public void validate(Boolean enabled, Map<Setting<?>, Object> settings) {
667+
if (enabled == false) {
668+
return;
669+
}
670+
671+
// Verify if synthetic source is enabled on the index; fail if it is not
672+
var indexMode = (IndexMode) settings.get(MODE);
673+
if (indexMode.defaultSourceMode() != SourceFieldMapper.Mode.SYNTHETIC) {
674+
var sourceMode = (SourceFieldMapper.Mode) settings.get(INDEX_MAPPER_SOURCE_MODE_SETTING);
675+
if (sourceMode != SourceFieldMapper.Mode.SYNTHETIC) {
676+
throw new IllegalArgumentException(
677+
String.format(
678+
Locale.ROOT,
679+
"The setting [%s] is only permitted when [%s] is set to [%s]. Current mode: [%s].",
680+
RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey(),
681+
INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(),
682+
SourceFieldMapper.Mode.SYNTHETIC.name(),
683+
sourceMode.name()
684+
)
685+
);
686+
}
687+
}
688+
689+
// Verify that all nodes can handle this setting
690+
var version = (IndexVersion) settings.get(SETTING_INDEX_VERSION_CREATED);
691+
if (version.before(IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY)) {
692+
throw new IllegalArgumentException(
693+
String.format(
694+
Locale.ROOT,
695+
"The setting [%s] is unavailable on this cluster because some nodes are running older "
696+
+ "versions that do not support it. Please upgrade all nodes to the latest version "
697+
+ "and try again.",
698+
RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey()
699+
)
700+
);
701+
}
702+
}
703+
704+
@Override
705+
public Iterator<Setting<?>> settings() {
706+
List<Setting<?>> res = List.of(INDEX_MAPPER_SOURCE_MODE_SETTING, SETTING_INDEX_VERSION_CREATED, MODE);
707+
return res.iterator();
708+
}
709+
},
710+
Property.IndexScope,
711+
Property.Final
712+
);
713+
656714
/**
657715
* Returns <code>true</code> if TSDB encoding is enabled. The default is <code>true</code>
658716
*/
@@ -824,6 +882,7 @@ private void setRetentionLeaseMillis(final TimeValue retentionLease) {
824882
private volatile boolean skipIgnoredSourceRead;
825883
private final SourceFieldMapper.Mode indexMappingSourceMode;
826884
private final boolean recoverySourceEnabled;
885+
private final boolean recoverySourceSyntheticEnabled;
827886

828887
/**
829888
* The maximum number of refresh listeners allows on this shard.
@@ -984,8 +1043,9 @@ public IndexSettings(final IndexMetadata indexMetadata, final Settings nodeSetti
9841043
es87TSDBCodecEnabled = scopedSettings.get(TIME_SERIES_ES87TSDB_CODEC_ENABLED_SETTING);
9851044
skipIgnoredSourceWrite = scopedSettings.get(IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_WRITE_SETTING);
9861045
skipIgnoredSourceRead = scopedSettings.get(IgnoredSourceFieldMapper.SKIP_IGNORED_SOURCE_READ_SETTING);
987-
indexMappingSourceMode = scopedSettings.get(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING);
1046+
indexMappingSourceMode = scopedSettings.get(INDEX_MAPPER_SOURCE_MODE_SETTING);
9881047
recoverySourceEnabled = RecoverySettings.INDICES_RECOVERY_SOURCE_ENABLED_SETTING.get(nodeSettings);
1048+
recoverySourceSyntheticEnabled = scopedSettings.get(RECOVERY_USE_SYNTHETIC_SOURCE_SETTING);
9891049

9901050
scopedSettings.addSettingsUpdateConsumer(
9911051
MergePolicyConfig.INDEX_COMPOUND_FORMAT_SETTING,
@@ -1677,6 +1737,13 @@ public boolean isRecoverySourceEnabled() {
16771737
return recoverySourceEnabled;
16781738
}
16791739

1740+
/**
1741+
* @return Whether recovery source should always be bypassed in favor of using synthetic source.
1742+
*/
1743+
public boolean isRecoverySourceSyntheticEnabled() {
1744+
return recoverySourceSyntheticEnabled;
1745+
}
1746+
16801747
/**
16811748
* The bounds for {@code @timestamp} on this index or
16821749
* {@code null} if there are no bounds.

server/src/main/java/org/elasticsearch/index/IndexVersions.java

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ private static Version parseUnchecked(String version) {
136136
public static final IndexVersion LOGSDB_DEFAULT_IGNORE_DYNAMIC_BEYOND_LIMIT = def(9_001_00_0, Version.LUCENE_10_0_0);
137137
public static final IndexVersion TIME_BASED_K_ORDERED_DOC_ID = def(9_002_00_0, Version.LUCENE_10_0_0);
138138
public static final IndexVersion DEPRECATE_SOURCE_MODE_MAPPER = def(9_003_00_0, Version.LUCENE_10_0_0);
139+
public static final IndexVersion USE_SYNTHETIC_SOURCE_FOR_RECOVERY = def(9_004_00_0, Version.LUCENE_10_0_0);
139140
/*
140141
* STOP! READ THIS FIRST! No, really,
141142
* ____ _____ ___ ____ _ ____ _____ _ ____ _____ _ _ ___ ____ _____ ___ ____ ____ _____ _

server/src/main/java/org/elasticsearch/index/engine/CombinedDocValues.java

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ final class CombinedDocValues {
2424
private final NumericDocValues primaryTermDV;
2525
private final NumericDocValues tombstoneDV;
2626
private final NumericDocValues recoverySource;
27+
private final NumericDocValues recoverySourceSize;
2728

2829
CombinedDocValues(LeafReader leafReader) throws IOException {
2930
this.versionDV = Objects.requireNonNull(leafReader.getNumericDocValues(VersionFieldMapper.NAME), "VersionDV is missing");
@@ -34,6 +35,7 @@ final class CombinedDocValues {
3435
);
3536
this.tombstoneDV = leafReader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME);
3637
this.recoverySource = leafReader.getNumericDocValues(SourceFieldMapper.RECOVERY_SOURCE_NAME);
38+
this.recoverySourceSize = leafReader.getNumericDocValues(SourceFieldMapper.RECOVERY_SOURCE_SIZE_NAME);
3739
}
3840

3941
long docVersion(int segmentDocId) throws IOException {
@@ -79,4 +81,12 @@ boolean hasRecoverySource(int segmentDocId) throws IOException {
7981
assert recoverySource.docID() < segmentDocId;
8082
return recoverySource.advanceExact(segmentDocId);
8183
}
84+
85+
long recoverySourceSize(int segmentDocId) throws IOException {
86+
if (recoverySourceSize == null) {
87+
return -1;
88+
}
89+
assert recoverySourceSize.docID() < segmentDocId;
90+
return recoverySourceSize.advanceExact(segmentDocId) ? recoverySourceSize.longValue() : -1;
91+
}
8292
}

0 commit comments

Comments
 (0)