diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java new file mode 100644 index 0000000000000..ff11c3a87a183 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java @@ -0,0 +1,398 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.searchablesnapshots; + +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.repositories.IndexId; +import org.elasticsearch.snapshots.SnapshotId; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toList; + +public class SearchableSnapshotShardStats implements Writeable, ToXContentObject { + + private final List inputStats; + private final ShardRouting shardRouting; + private final SnapshotId snapshotId; + private final IndexId indexId; + + public SearchableSnapshotShardStats(ShardRouting shardRouting, SnapshotId snapshotId, IndexId indexId, + List stats) { + this.shardRouting = Objects.requireNonNull(shardRouting); + this.snapshotId = Objects.requireNonNull(snapshotId); + this.indexId = Objects.requireNonNull(indexId); + this.inputStats = unmodifiableList(Objects.requireNonNull(stats)); + } + + public SearchableSnapshotShardStats(StreamInput in) throws IOException { + this.shardRouting = new ShardRouting(in); + this.snapshotId = new SnapshotId(in); + this.indexId = new IndexId(in); + this.inputStats = in.readList(CacheIndexInputStats::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + shardRouting.writeTo(out); + snapshotId.writeTo(out); + indexId.writeTo(out); + out.writeList(inputStats); + } + + public ShardRouting getShardRouting() { + return shardRouting; + } + + public SnapshotId getSnapshotId() { + return snapshotId; + } + + public IndexId getIndexId() { + return indexId; + } + + public List getStats() { + return inputStats; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.field("snapshot_uuid", getSnapshotId().getUUID()); + builder.field("index_uuid", getIndexId().getId()); + builder.startObject("shard"); + { + builder.field("state", shardRouting.state()); + builder.field("primary", shardRouting.primary()); + builder.field("node", shardRouting.currentNodeId()); + if (shardRouting.relocatingNodeId() != null) { + builder.field("relocating_node", shardRouting.relocatingNodeId()); + } + } + builder.endObject(); + builder.startArray("files"); + { + List stats = inputStats.stream() + .sorted(Comparator.comparing(CacheIndexInputStats::getFileName)).collect(toList()); + for (CacheIndexInputStats stat : stats) { + stat.toXContent(builder, params); + } + } + builder.endArray(); + } + return builder.endObject(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + SearchableSnapshotShardStats that = (SearchableSnapshotShardStats) other; + return Objects.equals(shardRouting, that.shardRouting) + && Objects.equals(snapshotId, that.snapshotId) + && Objects.equals(indexId, that.indexId) + && Objects.equals(inputStats, that.inputStats); + } + + @Override + public int hashCode() { + return Objects.hash(shardRouting, snapshotId, indexId, inputStats); + } + + + public static class CacheIndexInputStats implements Writeable, ToXContentObject { + + private final String fileName; + private final long fileLength; + + private final long openCount; + private final long innerCount; + private final long closeCount; + + private final Counter forwardSmallSeeks; + private final Counter backwardSmallSeeks; + private final Counter forwardLargeSeeks; + private final Counter backwardLargeSeeks; + private final Counter contiguousReads; + private final Counter nonContiguousReads; + private final Counter cachedBytesRead; + private final Counter cachedBytesWritten; + private final Counter directBytesRead; + + public CacheIndexInputStats(String fileName, long fileLength, long openCount, long innerCount, long closeCount, + Counter forwardSmallSeeks, Counter backwardSmallSeeks, + Counter forwardLargeSeeks, Counter backwardLargeSeeks, + Counter contiguousReads, Counter nonContiguousReads, + Counter cachedBytesRead, Counter cachedBytesWritten, + Counter directBytesRead) { + this.fileName = fileName; + this.fileLength = fileLength; + this.openCount = openCount; + this.innerCount = innerCount; + this.closeCount = closeCount; + this.forwardSmallSeeks = forwardSmallSeeks; + this.backwardSmallSeeks = backwardSmallSeeks; + this.forwardLargeSeeks = forwardLargeSeeks; + this.backwardLargeSeeks = backwardLargeSeeks; + this.contiguousReads = contiguousReads; + this.nonContiguousReads = nonContiguousReads; + this.cachedBytesRead = cachedBytesRead; + this.cachedBytesWritten = cachedBytesWritten; + this.directBytesRead = directBytesRead; + } + + CacheIndexInputStats(final StreamInput in) throws IOException { + this.fileName = in.readString(); + this.fileLength = in.readVLong(); + this.openCount = in.readVLong(); + this.innerCount = in.readVLong(); + this.closeCount = in.readVLong(); + this.forwardSmallSeeks = new Counter(in); + this.backwardSmallSeeks = new Counter(in); + this.forwardLargeSeeks = new Counter(in); + this.backwardLargeSeeks = new Counter(in); + this.contiguousReads = new Counter(in); + this.nonContiguousReads = new Counter(in); + this.cachedBytesRead = new Counter(in); + this.cachedBytesWritten = new Counter(in); + this.directBytesRead = new Counter(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(fileName); + out.writeVLong(fileLength); + out.writeVLong(openCount); + out.writeVLong(innerCount); + out.writeVLong(closeCount); + + forwardSmallSeeks.writeTo(out); + backwardSmallSeeks.writeTo(out); + forwardLargeSeeks.writeTo(out); + backwardLargeSeeks.writeTo(out); + contiguousReads.writeTo(out); + nonContiguousReads.writeTo(out); + cachedBytesRead.writeTo(out); + cachedBytesWritten.writeTo(out); + directBytesRead.writeTo(out); + } + + public String getFileName() { + return fileName; + } + + public long getFileLength() { + return fileLength; + } + + public long getOpenCount() { + return openCount; + } + + public long getInnerCount() { + return innerCount; + } + + public long getCloseCount() { + return closeCount; + } + + public Counter getForwardSmallSeeks() { + return forwardSmallSeeks; + } + + public Counter getBackwardSmallSeeks() { + return backwardSmallSeeks; + } + + public Counter getForwardLargeSeeks() { + return forwardLargeSeeks; + } + + public Counter getBackwardLargeSeeks() { + return backwardLargeSeeks; + } + + public Counter getContiguousReads() { + return contiguousReads; + } + + public Counter getNonContiguousReads() { + return nonContiguousReads; + } + + public Counter getCachedBytesRead() { + return cachedBytesRead; + } + + public Counter getCachedBytesWritten() { + return cachedBytesWritten; + } + + public Counter getDirectBytesRead() { + return directBytesRead; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.field("name", getFileName()); + builder.field("length", getFileLength()); + builder.field("open_count", getOpenCount()); + builder.field("inner_count", getInnerCount()); + builder.field("close_count", getCloseCount()); + builder.field("contiguous_bytes_read", getContiguousReads()); + builder.field("non_contiguous_bytes_read", getNonContiguousReads()); + builder.field("cached_bytes_read", getCachedBytesRead()); + builder.field("cached_bytes_written", getCachedBytesWritten()); + builder.field("direct_bytes_read", getDirectBytesRead()); + { + builder.startObject("forward_seeks"); + builder.field("small", getForwardSmallSeeks()); + builder.field("large", getForwardLargeSeeks()); + builder.endObject(); + } + { + builder.startObject("backward_seeks"); + builder.field("small", getBackwardSmallSeeks()); + builder.field("large", getBackwardLargeSeeks()); + builder.endObject(); + } + } + return builder.endObject(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + CacheIndexInputStats stats = (CacheIndexInputStats) other; + return fileLength == stats.fileLength + && openCount == stats.openCount + && innerCount == stats.innerCount + && closeCount == stats.closeCount + && Objects.equals(fileName, stats.fileName) + && Objects.equals(forwardSmallSeeks, stats.forwardSmallSeeks) + && Objects.equals(backwardSmallSeeks, stats.backwardSmallSeeks) + && Objects.equals(forwardLargeSeeks, stats.forwardLargeSeeks) + && Objects.equals(backwardLargeSeeks, stats.backwardLargeSeeks) + && Objects.equals(contiguousReads, stats.contiguousReads) + && Objects.equals(nonContiguousReads, stats.nonContiguousReads) + && Objects.equals(cachedBytesRead, stats.cachedBytesRead) + && Objects.equals(cachedBytesWritten, stats.cachedBytesWritten) + && Objects.equals(directBytesRead, stats.directBytesRead); + } + + @Override + public int hashCode() { + return Objects.hash(fileName, fileLength, openCount, innerCount, closeCount, + forwardSmallSeeks, backwardSmallSeeks, + forwardLargeSeeks, backwardLargeSeeks, + contiguousReads, nonContiguousReads, + cachedBytesRead, cachedBytesWritten, + directBytesRead); + } + } + + public static class Counter implements Writeable, ToXContentObject { + + private final long count; + private final long total; + private final long min; + private final long max; + + public Counter(final long count, final long total, final long min, final long max) { + this.count = count; + this.total = total; + this.min = min; + this.max = max; + } + + Counter(final StreamInput in) throws IOException { + this.count = in.readZLong(); + this.total = in.readZLong(); + this.min = in.readZLong(); + this.max = in.readZLong(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeZLong(count); + out.writeZLong(total); + out.writeZLong(min); + out.writeZLong(max); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.field("count", count); + builder.field("sum", total); + builder.field("min", min); + builder.field("max", max); + } + builder.endObject(); + return builder; + } + + public long getCount() { + return count; + } + + public long getTotal() { + return total; + } + + public long getMin() { + return min; + } + + public long getMax() { + return max; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Counter that = (Counter) other; + return count == that.count + && total == that.total + && min == that.min + && max == that.max; + } + + @Override + public int hashCode() { + return Objects.hash(count, total, min, max); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java new file mode 100644 index 0000000000000..67dc8876c6fee --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.searchablesnapshots; + +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardRoutingState; +import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.repositories.IndexId; +import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.CacheIndexInputStats; +import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.Counter; + +import java.util.ArrayList; +import java.util.List; + +public class SearchableSnapshotShardStatsTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return SearchableSnapshotShardStats::new; + } + + @Override + protected SearchableSnapshotShardStats createTestInstance() { + SnapshotId snapshotId = new SnapshotId(randomAlphaOfLength(5), randomAlphaOfLength(5)); + IndexId indexId = new IndexId(randomAlphaOfLength(5), randomAlphaOfLength(5)); + ShardRouting shardRouting = TestShardRouting.newShardRouting(randomAlphaOfLength(5), randomInt(10), randomAlphaOfLength(5), + randomBoolean(), ShardRoutingState.STARTED); + + final List inputStats = new ArrayList<>(); + for (int j = 0; j < randomInt(20); j++) { + inputStats.add(randomCacheIndexInputStats()); + } + return new SearchableSnapshotShardStats(shardRouting, snapshotId, indexId, inputStats); + } + + private CacheIndexInputStats randomCacheIndexInputStats() { + return new CacheIndexInputStats(randomAlphaOfLength(10), randomNonNegativeLong(), + randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong(), + randomCounter(), randomCounter(), + randomCounter(), randomCounter(), + randomCounter(), randomCounter(), + randomCounter(), randomCounter(), + randomCounter()); + } + + private Counter randomCounter() { + return new Counter(randomLong(), randomLong(), randomLong(), randomLong()); + } +} diff --git a/x-pack/plugin/searchable-snapshots/build.gradle b/x-pack/plugin/searchable-snapshots/build.gradle index dd928bfbcc41c..dcb22ec38a51f 100644 --- a/x-pack/plugin/searchable-snapshots/build.gradle +++ b/x-pack/plugin/searchable-snapshots/build.gradle @@ -18,3 +18,12 @@ dependencies { // installing them as individual plugins for integ tests doesn't make sense, // so we disable integ tests integTest.enabled = false + +// add all sub-projects of the qa sub-project +gradle.projectsEvaluated { + project.subprojects + .find { it.path == project.path + ":qa" } + .subprojects + .findAll { it.path.startsWith(project.path + ":qa") } + .each { check.dependsOn it.check } +} diff --git a/x-pack/plugin/searchable-snapshots/qa/build.gradle b/x-pack/plugin/searchable-snapshots/qa/build.gradle new file mode 100644 index 0000000000000..b2e39e506a5b0 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/qa/build.gradle @@ -0,0 +1,17 @@ +import org.elasticsearch.gradle.test.RestIntegTestTask + +apply plugin: 'elasticsearch.build' +test.enabled = false + +dependencies { + compile project(':test:framework') +} + +subprojects { + project.tasks.withType(RestIntegTestTask) { + final File xPackResources = new File(xpackProject('plugin:searchable-snapshots').projectDir, 'src/test/resources') + project.copyRestSpec.from(xPackResources) { + include 'rest-api-spec/api/**' + } + } +} diff --git a/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle b/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle new file mode 100644 index 0000000000000..5a14efb38f466 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/qa/rest/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'elasticsearch.testclusters' +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-test' + +testClusters.integTest { + testDistribution = 'DEFAULT' + setting 'xpack.license.self_generated.type', 'basic' +} diff --git a/x-pack/plugin/searchable-snapshots/qa/rest/src/test/java/org/elasticsearch/xpack/searchablesnapshots/rest/SearchableSnapshotsRestIT.java b/x-pack/plugin/searchable-snapshots/qa/rest/src/test/java/org/elasticsearch/xpack/searchablesnapshots/rest/SearchableSnapshotsRestIT.java new file mode 100644 index 0000000000000..025f594ef669a --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/qa/rest/src/test/java/org/elasticsearch/xpack/searchablesnapshots/rest/SearchableSnapshotsRestIT.java @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.rest; + +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; + +public class SearchableSnapshotsRestIT extends ESClientYamlSuiteTestCase { + + public SearchableSnapshotsRestIT(final ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return ESClientYamlSuiteTestCase.createParameters(); + } +} diff --git a/x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/stats.yml b/x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/stats.yml new file mode 100644 index 0000000000000..24e6eab79c6b8 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/stats.yml @@ -0,0 +1,192 @@ +--- +setup: + + - do: + indices.create: + index: docs + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + + - do: + bulk: + body: + - index: + _index: docs + _id: 1 + - field: foo + - index: + _index: docs + _id: 2 + - field: bar + - index: + _index: docs + _id: 3 + - field: baz + + - do: + snapshot.create_repository: + repository: repository-fs + body: + type: fs + settings: + location: "repository-fs" + + - do: + snapshot.create: + repository: repository-fs + snapshot: snapshot + wait_for_completion: true + + - do: + indices.delete: + index: docs +--- +teardown: + + - do: + snapshot.delete: + repository: repository-fs + snapshot: snapshot + ignore: 404 + + - do: + snapshot.delete_repository: + repository: repository-fs + +--- +"Tests searchable snapshots stats": + - skip: + version: " - 7.99.99" + reason: searchable snapshots introduced in 8.0 + + - do: + catch: missing + searchable_snapshots.stats: {} + + - match: { error.root_cause.0.type: "resource_not_found_exception" } + - match: { error.root_cause.0.reason: "No searchable snapshots indices found" } + + - do: + catch: missing + searchable_snapshots.stats: + index: _all + + - match: { error.root_cause.0.type: "resource_not_found_exception" } + - match: { error.root_cause.0.reason: "No searchable snapshots indices found" } + + - do: + catch: missing + searchable_snapshots.stats: + index: "unknown" + + - do: + indices.create: + index: non_searchable_snapshot_index + + - do: + catch: missing + searchable_snapshots.stats: + index: non_* + + - match: { error.root_cause.0.type: "resource_not_found_exception" } + - match: { error.root_cause.0.reason: "No searchable snapshots indices found" } + + - do: + snapshot.create_repository: + repository: repository-searchable-snapshots + body: + type: searchable + settings: + delegate_type: fs + location: "repository-fs" + + - match: { acknowledged: true } + + - do: + snapshot.restore: + repository: repository-searchable-snapshots + snapshot: snapshot + wait_for_completion: true + + - match: { snapshot.snapshot: snapshot } + - match: { snapshot.shards.failed: 0 } + - match: { snapshot.shards.successful: 1 } + + - do: + search: + rest_total_hits_as_int: true + index: docs + body: + query: + match_all: {} + + - match: { hits.total: 3 } + + - do: + nodes.info: {} + - set: + nodes._arbitrary_key_: node_id + + - do: + searchable_snapshots.stats: + index: "d*" + + - length: { indices: 1 } + - length: { indices.docs.shards: 1 } + - length: { indices.docs.shards.0: 1 } + - is_true: indices.docs.shards.0.0.snapshot_uuid + - is_true: indices.docs.shards.0.0.index_uuid + - match: { indices.docs.shards.0.0.shard.state: STARTED } + - match: { indices.docs.shards.0.0.shard.primary: true } + - match: { indices.docs.shards.0.0.shard.node: $node_id } + + - is_true: indices.docs.shards.0.0.files.0.name + - gt: { indices.docs.shards.0.0.files.0.length: 0 } + - gt: { indices.docs.shards.0.0.files.0.open_count: 0 } + - gte: { indices.docs.shards.0.0.files.0.inner_count: 0 } + - gt: { indices.docs.shards.0.0.files.0.close_count: 0 } + + - gte: { indices.docs.shards.0.0.files.0.contiguous_bytes_read.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.contiguous_bytes_read.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.contiguous_bytes_read.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.contiguous_bytes_read.max: 0 } + + - gte: { indices.docs.shards.0.0.files.0.non_contiguous_bytes_read.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.non_contiguous_bytes_read.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.non_contiguous_bytes_read.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.non_contiguous_bytes_read.max: 0 } + + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_read.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_read.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_read.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_read.max: 0 } + + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_written.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_written.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_written.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.cached_bytes_written.max: 0 } + + - gte: { indices.docs.shards.0.0.files.0.direct_bytes_read.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.direct_bytes_read.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.direct_bytes_read.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.direct_bytes_read.max: 0 } + + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.small.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.small.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.small.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.small.max: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.large.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.large.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.large.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.forward_seeks.large.max: 0 } + + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.small.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.small.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.small.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.small.max: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.large.count: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.large.sum: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.large.min: 0 } + - gte: { indices.docs.shards.0.0.files.0.backward_seeks.large.max: 0 } diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/InMemoryNoOpCommitDirectory.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/InMemoryNoOpCommitDirectory.java index bab3b0e53f270..802c2a471dc7f 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/InMemoryNoOpCommitDirectory.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/InMemoryNoOpCommitDirectory.java @@ -25,6 +25,7 @@ * of the same, so that we can start a shard on a completely readonly data set. */ public class InMemoryNoOpCommitDirectory extends FilterDirectory { + private final Directory realDirectory; InMemoryNoOpCommitDirectory(Directory realDirectory) { @@ -32,6 +33,10 @@ public class InMemoryNoOpCommitDirectory extends FilterDirectory { this.realDirectory = realDirectory; } + public Directory getRealDirectory() { + return realDirectory; + } + @Override public String[] listAll() throws IOException { final String[] ephemeralFiles = in.listAll(); diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java index 61e277bb8ae4d..7374a36fd722d 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java @@ -6,11 +6,18 @@ package org.elasticsearch.xpack.searchablesnapshots; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; @@ -18,6 +25,7 @@ import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.ReadOnlyEngine; import org.elasticsearch.index.translog.TranslogStats; +import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.EnginePlugin; import org.elasticsearch.plugins.IndexStorePlugin; import org.elasticsearch.plugins.Plugin; @@ -25,10 +33,15 @@ import org.elasticsearch.repositories.RepositoriesModule; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; import org.elasticsearch.script.ScriptService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.searchablesnapshots.action.SearchableSnapshotsStatsAction; +import org.elasticsearch.xpack.searchablesnapshots.action.TransportSearchableSnapshotsStatsAction; import org.elasticsearch.xpack.searchablesnapshots.cache.CacheService; +import org.elasticsearch.xpack.searchablesnapshots.rest.RestSearchableSnapshotsStatsAction; import java.util.Collection; import java.util.Collections; @@ -36,13 +49,14 @@ import java.util.Map; import java.util.Optional; import java.util.function.Function; +import java.util.function.Supplier; import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING; /** * Plugin for Searchable Snapshots feature */ -public class SearchableSnapshots extends Plugin implements IndexStorePlugin, RepositoryPlugin, EnginePlugin { +public class SearchableSnapshots extends Plugin implements IndexStorePlugin, RepositoryPlugin, EnginePlugin, ActionPlugin { private final SetOnce repositoriesService; private final SetOnce cacheService; @@ -107,5 +121,17 @@ public Map getRepositories(Environment env, NamedXCo ClusterService clusterService) { return Collections.singletonMap(SearchableSnapshotRepository.TYPE, SearchableSnapshotRepository.getRepositoryFactory()); } + + @Override + public List> getActions() { + return List.of(new ActionHandler<>(SearchableSnapshotsStatsAction.INSTANCE, TransportSearchableSnapshotsStatsAction.class)); + } + + public List getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster) { + return List.of(new RestSearchableSnapshotsStatsAction(restController)); + } } diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsAction.java new file mode 100644 index 0000000000000..f59e001cea23b --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsAction.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.ActionType; + +public class SearchableSnapshotsStatsAction extends ActionType { + + public static final SearchableSnapshotsStatsAction INSTANCE = new SearchableSnapshotsStatsAction(); + static final String NAME = "cluster:monitor/xpack/searchable_snapshots/stats"; + + private SearchableSnapshotsStatsAction() { + super(NAME, SearchableSnapshotsStatsResponse::new); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsRequest.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsRequest.java new file mode 100644 index 0000000000000..3659c9641d040 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsRequest.java @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.broadcast.BroadcastRequest; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + +public class SearchableSnapshotsStatsRequest extends BroadcastRequest { + + SearchableSnapshotsStatsRequest(StreamInput in) throws IOException { + super(in); + } + + public SearchableSnapshotsStatsRequest(String... indices) { + super(indices); + } + + public SearchableSnapshotsStatsRequest(String[] indices, IndicesOptions indicesOptions) { + super(indices, indicesOptions); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java new file mode 100644 index 0000000000000..a39b18f51a136 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.support.DefaultShardOperationFailedException; +import org.elasticsearch.action.support.broadcast.BroadcastResponse; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.Index; +import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +public class SearchableSnapshotsStatsResponse extends BroadcastResponse { + + private List stats; + + SearchableSnapshotsStatsResponse(StreamInput in) throws IOException { + super(in); + this.stats = in.readList(SearchableSnapshotShardStats::new); + } + + SearchableSnapshotsStatsResponse(List stats, int totalShards, int successfulShards, int failedShards, + List shardFailures) { + super(totalShards, successfulShards, failedShards, shardFailures); + this.stats = Objects.requireNonNull(stats); + } + + public List getStats() { + return stats; + } + + @Override + protected void addCustomXContentFields(XContentBuilder builder, Params params) throws IOException { + final List indices = getStats().stream() + .filter(stats -> stats.getStats().isEmpty() == false) + .map(SearchableSnapshotShardStats::getShardRouting) + .map(ShardRouting::index) + .sorted(Comparator.comparing(Index::getName)) + .collect(toList()); + + builder.startObject("indices"); + for (Index index : indices) { + builder.startObject(index.getName()); + { + builder.startObject("shards"); + { + List listOfStats = getStats().stream() + .filter(dirStats -> dirStats.getShardRouting().index().equals(index)) + .sorted(Comparator.comparingInt(dir -> dir.getShardRouting().getId())) + .collect(Collectors.toList()); + + int minShard = listOfStats.stream().map(stat -> stat.getShardRouting().getId()).min(Integer::compareTo).orElse(0); + int maxShard = listOfStats.stream().map(stat -> stat.getShardRouting().getId()).max(Integer::compareTo).orElse(0); + + for (int i = minShard; i <= maxShard; i++) { + builder.startArray(Integer.toString(i)); + for (SearchableSnapshotShardStats stat : listOfStats) { + if (stat.getShardRouting().getId() == i) { + stat.toXContent(builder, params); + } + } + builder.endArray(); + } + } + builder.endObject(); + } + builder.endObject(); + } + builder.endObject(); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java new file mode 100644 index 0000000000000..8985c0e1c1fac --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FilterDirectory; +import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.DefaultShardOperationFailedException; +import org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardsIterator; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.indices.IndicesService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats; +import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.CacheIndexInputStats; +import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.Counter; +import org.elasticsearch.xpack.searchablesnapshots.InMemoryNoOpCommitDirectory; +import org.elasticsearch.xpack.searchablesnapshots.cache.CacheDirectory; +import org.elasticsearch.xpack.searchablesnapshots.cache.IndexInputStats; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.elasticsearch.index.IndexModule.INDEX_STORE_TYPE_SETTING; +import static org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshotRepository.SNAPSHOT_DIRECTORY_FACTORY_KEY; + +public class TransportSearchableSnapshotsStatsAction extends TransportBroadcastByNodeAction { + private final IndicesService indicesService; + + @Inject + public TransportSearchableSnapshotsStatsAction(ClusterService clusterService, TransportService transportService, + IndicesService indicesService, ActionFilters actionFilters, + IndexNameExpressionResolver indexNameExpressionResolver) { + super(SearchableSnapshotsStatsAction.NAME, clusterService, transportService, actionFilters, indexNameExpressionResolver, + SearchableSnapshotsStatsRequest::new, ThreadPool.Names.MANAGEMENT); + this.indicesService = indicesService; + } + + @Override + protected ClusterBlockException checkGlobalBlock(ClusterState state, SearchableSnapshotsStatsRequest request) { + return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ); + } + + @Override + protected ClusterBlockException checkRequestBlock(ClusterState state, SearchableSnapshotsStatsRequest request, String[] indices) { + return state.blocks().indicesBlockedException(ClusterBlockLevel.METADATA_READ, indices); + } + + @Override + protected SearchableSnapshotShardStats readShardResult(StreamInput in) throws IOException { + return new SearchableSnapshotShardStats(in); + } + + @Override + protected SearchableSnapshotsStatsResponse newResponse(SearchableSnapshotsStatsRequest request, + int totalShards, int successfulShards, int failedShards, + List shardsStats, + List shardFailures, + ClusterState clusterState) { + return new SearchableSnapshotsStatsResponse(shardsStats, totalShards, successfulShards, failedShards, shardFailures); + } + + @Override + protected SearchableSnapshotsStatsRequest readRequestFrom(StreamInput in) throws IOException { + return new SearchableSnapshotsStatsRequest(in); + } + + @Override + protected ShardsIterator shards(ClusterState state, SearchableSnapshotsStatsRequest request, String[] concreteIndices) { + final List searchableSnapshotIndices = new ArrayList<>(); + for (String concreteIndex : concreteIndices) { + IndexMetaData indexMetaData = state.metaData().index(concreteIndex); + if (indexMetaData != null) { + Settings indexSettings = indexMetaData.getSettings(); + if (INDEX_STORE_TYPE_SETTING.get(indexSettings).equals(SNAPSHOT_DIRECTORY_FACTORY_KEY)) { + searchableSnapshotIndices.add(concreteIndex); + } + } + } + if (searchableSnapshotIndices.isEmpty()) { + throw new ResourceNotFoundException("No searchable snapshots indices found"); + } + return state.routingTable().allShards(searchableSnapshotIndices.toArray(new String[0])); + } + + @Override + protected SearchableSnapshotShardStats shardOperation(SearchableSnapshotsStatsRequest request, ShardRouting shardRouting) { + final IndexShard indexShard = indicesService.indexServiceSafe(shardRouting.index()).getShard(shardRouting.id()); + final CacheDirectory cacheDirectory = unwrap(indexShard.store().directory()); + assert cacheDirectory != null; + assert cacheDirectory.getShardId().equals(shardRouting.shardId()); + + return new SearchableSnapshotShardStats(shardRouting, cacheDirectory.getSnapshotId(), cacheDirectory.getIndexId(), + cacheDirectory.getStats().entrySet().stream() + .map(entry -> toCacheIndexInputStats(entry.getKey(), entry.getValue())) + .collect(Collectors.toList())); + } + + private static CacheIndexInputStats toCacheIndexInputStats(final String fileName, final IndexInputStats inputStats) { + return new CacheIndexInputStats(fileName, inputStats.getFileLength(), + inputStats.getOpened().sum(), inputStats.getInnerOpened().sum(), inputStats.getClosed().sum(), + toCounter(inputStats.getForwardSmallSeeks()), toCounter(inputStats.getBackwardSmallSeeks()), + toCounter(inputStats.getForwardLargeSeeks()), toCounter(inputStats.getBackwardLargeSeeks()), + toCounter(inputStats.getContiguousReads()), toCounter(inputStats.getNonContiguousReads()), + toCounter(inputStats.getCachedBytesRead()), toCounter(inputStats.getCachedBytesWritten()), + toCounter(inputStats.getDirectBytesRead())); + } + + private static Counter toCounter(final IndexInputStats.Counter counter) { + return new Counter(counter.count(), counter.total(), counter.min(), counter.max()); + } + + @Nullable + private static CacheDirectory unwrap(Directory dir) { + while (dir != null) { + if (dir instanceof CacheDirectory) { + return (CacheDirectory) dir; + } else if (dir instanceof InMemoryNoOpCommitDirectory) { + dir = ((InMemoryNoOpCommitDirectory) dir).getRealDirectory(); + } else if (dir instanceof FilterDirectory) { + dir = ((FilterDirectory) dir).getDelegate(); + } else { + dir = null; + } + } + return null; + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/CacheDirectory.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/CacheDirectory.java index 2246244000d7f..8cfe97d85b2e0 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/CacheDirectory.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/CacheDirectory.java @@ -29,6 +29,7 @@ import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -64,6 +65,22 @@ private CacheKey createCacheKey(String fileName) { return new CacheKey(snapshotId, indexId, shardId, fileName); } + public SnapshotId getSnapshotId() { + return snapshotId; + } + + public IndexId getIndexId() { + return indexId; + } + + public ShardId getShardId() { + return shardId; + } + + public Map getStats() { + return Collections.unmodifiableMap(stats); + } + // pkg private for tests @Nullable IndexInputStats getStats(String name) { return stats.get(name); diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStats.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStats.java index f4a5d1d87cc89..9b662a95a40f5 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStats.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStats.java @@ -23,6 +23,7 @@ public class IndexInputStats { static final ByteSizeValue SEEKING_THRESHOLD = new ByteSizeValue(8, ByteSizeUnit.MB); private final long fileLength; + private final long seekingThreshold; private final LongAdder opened = new LongAdder(); private final LongAdder inner = new LongAdder(); @@ -43,7 +44,13 @@ public class IndexInputStats { private final Counter cachedBytesWritten = new Counter(); public IndexInputStats(long fileLength) { + this(fileLength, SEEKING_THRESHOLD.getBytes()); + } + + // pkg-private for testing + IndexInputStats(long fileLength, long seekingThreshold) { this.fileLength = fileLength; + this.seekingThreshold = seekingThreshold; } public void incrementOpenCount() { @@ -96,64 +103,64 @@ public void incrementSeeks(long currentPosition, long newPosition) { } } - long getFileLength() { + public long getFileLength() { return fileLength; } - LongAdder getOpened() { + public LongAdder getOpened() { return opened; } - LongAdder getInnerOpened() { + public LongAdder getInnerOpened() { return inner; } - LongAdder getClosed() { + public LongAdder getClosed() { return closed; } - Counter getForwardSmallSeeks() { + public Counter getForwardSmallSeeks() { return forwardSmallSeeks; } - Counter getBackwardSmallSeeks() { + public Counter getBackwardSmallSeeks() { return backwardSmallSeeks; } - Counter getForwardLargeSeeks() { + public Counter getForwardLargeSeeks() { return forwardLargeSeeks; } - Counter getBackwardLargeSeeks() { + public Counter getBackwardLargeSeeks() { return backwardLargeSeeks; } - Counter getContiguousReads() { + public Counter getContiguousReads() { return contiguousReads; } - Counter getNonContiguousReads() { + public Counter getNonContiguousReads() { return nonContiguousReads; } - Counter getDirectBytesRead() { + public Counter getDirectBytesRead() { return directBytesRead; } - Counter getCachedBytesRead() { + public Counter getCachedBytesRead() { return cachedBytesRead; } - Counter getCachedBytesWritten() { + public Counter getCachedBytesWritten() { return cachedBytesWritten; } @SuppressForbidden(reason = "Handles Long.MIN_VALUE before using Math.abs()") - boolean isLargeSeek(long delta) { - return delta != Long.MIN_VALUE && Math.abs(delta) > SEEKING_THRESHOLD.getBytes(); + private boolean isLargeSeek(long delta) { + return delta != Long.MIN_VALUE && Math.abs(delta) > seekingThreshold; } - static class Counter { + public static class Counter { private final LongAdder count = new LongAdder(); private final LongAdder total = new LongAdder(); @@ -167,15 +174,15 @@ void add(final long value) { max.updateAndGet(prev -> Math.max(prev, value)); } - long count() { + public long count() { return count.sum(); } - long total() { + public long total() { return total.sum(); } - long min() { + public long min() { final long value = min.get(); if (value == Long.MAX_VALUE) { return 0L; @@ -183,7 +190,7 @@ long min() { return value; } - long max() { + public long max() { final long value = max.get(); if (value == Long.MIN_VALUE) { return 0L; diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java new file mode 100644 index 0000000000000..de2052c32781a --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.searchablesnapshots.rest; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.searchablesnapshots.action.SearchableSnapshotsStatsAction; +import org.elasticsearch.xpack.searchablesnapshots.action.SearchableSnapshotsStatsRequest; + +public class RestSearchableSnapshotsStatsAction extends BaseRestHandler { + + public RestSearchableSnapshotsStatsAction(final RestController controller) { + controller.registerHandler(RestRequest.Method.GET, "/_searchable_snapshots/stats", this); + controller.registerHandler(RestRequest.Method.GET, "/{index}/_searchable_snapshots/stats", this); + } + + @Override + public String getName() { + return "searchable_snapshots_stats_action"; + } + + @Override + public RestChannelConsumer prepareRequest(final RestRequest restRequest, final NodeClient client) { + String[] indices = Strings.splitStringByCommaToArray(restRequest.param("index")); + return channel -> client.execute(SearchableSnapshotsStatsAction.INSTANCE, + new SearchableSnapshotsStatsRequest(indices), new RestToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStatsTests.java b/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStatsTests.java index 040c2808af3ba..ceb86c04d7813 100644 --- a/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStatsTests.java +++ b/x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/cache/IndexInputStatsTests.java @@ -44,7 +44,8 @@ public void testReads() { public void testSeeks() { final long fileLength = randomLongBetween(1L, 1_000L); - final IndexInputStats inputStats = new IndexInputStats(fileLength); + final long seekingThreshold = randomBoolean() ? randomLongBetween(1L, fileLength) : SEEKING_THRESHOLD.getBytes(); + final IndexInputStats inputStats = new IndexInputStats(fileLength, seekingThreshold); assertCounter(inputStats.getForwardSmallSeeks(), 0L, 0L, 0L, 0L); assertCounter(inputStats.getForwardLargeSeeks(), 0L, 0L, 0L, 0L); @@ -63,10 +64,10 @@ public void testSeeks() { final long delta = seekToPosition - currentPosition; if (delta > 0) { - IndexInputStats.Counter forwardCounter = (delta <= SEEKING_THRESHOLD.getBytes()) ? fwSmallSeeks : fwLargeSeeks; + IndexInputStats.Counter forwardCounter = (delta <= seekingThreshold) ? fwSmallSeeks : fwLargeSeeks; forwardCounter.add(delta); } else if (delta < 0) { - IndexInputStats.Counter backwardCounter = (delta >= -1 * SEEKING_THRESHOLD.getBytes()) ? bwSmallSeeks : bwLargeSeeks; + IndexInputStats.Counter backwardCounter = (delta >= -1 * seekingThreshold) ? bwSmallSeeks : bwLargeSeeks; backwardCounter.add(delta); } } diff --git a/x-pack/plugin/searchable-snapshots/src/test/resources/rest-api-spec/api/searchable_snapshots.stats.json b/x-pack/plugin/searchable-snapshots/src/test/resources/rest-api-spec/api/searchable_snapshots.stats.json new file mode 100644 index 0000000000000..604e30a7cac5e --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/test/resources/rest-api-spec/api/searchable_snapshots.stats.json @@ -0,0 +1,30 @@ +{ + "searchable_snapshots.stats": { + "documentation": { + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/current/searchable-snapshots-get-stats.html //NORELEASE This API should be documented. We expect this API to be stable at the time it is merged in master, but in case it is not its stability should be documented appropriately." + }, + "stability": "experimental", + "url": { + "paths": [ + { + "path": "/_searchable_snapshots/stats", + "methods": [ + "GET" + ] + }, + { + "path": "/{index}/_searchable_snapshots/stats", + "methods": [ + "GET" + ], + "parts": { + "index": { + "type": "list", + "description": "A comma-separated list of index names" + } + } + } + ] + } + } +}