Skip to content
Closed
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4d3550c
Avoid loading shard metadata while closing
DaveCTurner Mar 19, 2018
fb684fb
Merge branch 'master' into 2018-03-19-load-latest-shard-state-under-lock
DaveCTurner Mar 27, 2018
487e785
Use IndexShard's mutex to protect the call to loadLatestState
DaveCTurner Mar 27, 2018
fb5f8c9
Add test
DaveCTurner Apr 4, 2018
f8a4c0d
Merge
DaveCTurner Apr 4, 2018
358a94c
Merge branch '2018-03-19-load-latest-shard-state-under-lock-TEST-ONLY…
DaveCTurner Apr 4, 2018
fed85bf
Much simpler test
DaveCTurner Apr 5, 2018
0ce8021
Tidy imports
DaveCTurner Apr 5, 2018
b654d9d
Try repeats
DaveCTurner Apr 5, 2018
e9ef547
Try without a loop
DaveCTurner Apr 5, 2018
7641ac2
Multiple threads, one request each, synchronised starts
DaveCTurner Apr 5, 2018
62ae05c
Try doing listing and deletions all in the background, with multiple …
DaveCTurner Apr 5, 2018
ed37174
Also don't care if the index is not found
DaveCTurner Apr 5, 2018
be76bb0
Add comments
DaveCTurner Apr 5, 2018
d534ffd
Reinstate logging
DaveCTurner Apr 5, 2018
f89d9b1
Add note about logging making failures more common
DaveCTurner Apr 5, 2018
e9e8ad5
Merge branch '2018-03-19-load-latest-shard-state-under-lock-TEST-ONLY…
DaveCTurner Apr 5, 2018
be42cca
Merge branch 'master' into 2018-03-19-load-latest-shard-state-under-lock
DaveCTurner Apr 5, 2018
819d274
NOCOMMIT reinstate logging for repro test
DaveCTurner Apr 5, 2018
fd9dc31
Revert "NOCOMMIT reinstate logging for repro test"
DaveCTurner Apr 5, 2018
032be06
Merge branch 'master' into 2018-03-19-load-latest-shard-state-under-lock
DaveCTurner May 18, 2018
3eff6c9
Can construct ShardStateMetaData from an IndexShard directly, no need…
DaveCTurner May 18, 2018
7f835cc
Inline method and avoid calling indicesService.getShardOrNull(shardId…
DaveCTurner May 18, 2018
48f6d46
Assert that directory is really deleted
DaveCTurner May 18, 2018
7e58bc6
debug -> trace
DaveCTurner May 24, 2018
91c101a
Merge branch 'master' into 2018-03-19-load-latest-shard-state-under-lock
DaveCTurner May 24, 2018
903ef15
Merge branch 'master' into 2018-03-19-load-latest-shard-state-under-lock
DaveCTurner May 30, 2018
1d4e044
No need for mutex, just read shardRouting once
DaveCTurner May 30, 2018
61b4e4e
Only obtain lock once in TransportNodesListShardStoreMetaData
DaveCTurner May 30, 2018
8f1a5e2
Only obtain lock once in TransportNodesListGatewayStartedShards
DaveCTurner May 30, 2018
ac8902b
Merge branch 'master' into 2018-03-19-load-latest-shard-state-under-lock
DaveCTurner May 31, 2018
78c0526
Only get lock once in TransportNodesListGatewayStartedShards, and ret…
DaveCTurner May 31, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.NodeEnvironment;
import org.elasticsearch.env.ShardLock;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.index.shard.ShardPath;
import org.elasticsearch.index.shard.ShardStateMetaData;
Expand All @@ -52,6 +54,7 @@

import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
* This transport action is used to fetch the shard version from each node during primary allocation in {@link GatewayAllocator}.
Expand Down Expand Up @@ -116,55 +119,71 @@ protected NodeGatewayStartedShards nodeOperation(NodeRequest request) {
try {
final ShardId shardId = request.getShardId();
logger.trace("{} loading local shard state info", shardId);
ShardStateMetaData shardStateMetaData = ShardStateMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY,
nodeEnv.availableShardPaths(request.shardId));
if (shardStateMetaData != null) {
IndexMetaData metaData = clusterService.state().metaData().index(shardId.getIndex());
if (metaData == null) {
// we may send this requests while processing the cluster state that recovered the index
// sometimes the request comes in before the local node processed that cluster state
// in such cases we can load it from disk
metaData = IndexMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY,
nodeEnv.indexPaths(shardId.getIndex()));
}
if (metaData == null) {
ElasticsearchException e = new ElasticsearchException("failed to find local IndexMetaData");
e.setShard(request.shardId);
throw e;
}

if (indicesService.getShardOrNull(shardId) == null) {
// we don't have an open shard on the store, validate the files on disk are openable
ShardPath shardPath = null;
try {
IndexSettings indexSettings = new IndexSettings(metaData, settings);
shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings);
if (shardPath == null) {
throw new IllegalStateException(shardId + " no shard path found");
}
Store.tryOpenIndex(shardPath.resolveIndex(), shardId, nodeEnv::shardLock, logger);
} catch (Exception exception) {
final ShardPath finalShardPath = shardPath;
logger.trace(() -> new ParameterizedMessage(
"{} can't open index for shard [{}] in path [{}]",
shardId,
shardStateMetaData,
(finalShardPath != null) ? finalShardPath.resolveIndex() : ""),
exception);
String allocationId = shardStateMetaData.allocationId != null ?
shardStateMetaData.allocationId.getId() : null;
return new NodeGatewayStartedShards(clusterService.localNode(), allocationId, shardStateMetaData.primary,
exception);
}
}
final IndexShard indexShard = indicesService.getShardOrNull(shardId);
if (indexShard != null) {
final ShardStateMetaData shardStateMetaData = indexShard.getShardStateMetaData();
final String allocationId = shardStateMetaData.allocationId != null ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allocationIds have been around since I don't know how long. When can this be null?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its declaration says this:

@Nullable
public final AllocationId allocationId; // can be null if we read from legacy format (see fromXContent and MultiDataPathUpgrader)

There are lots of other null checks too. Maybe worth addressing separately?

shardStateMetaData.allocationId.getId() : null;
logger.trace("{} shard state info found: [{}]", shardId, shardStateMetaData);
return new NodeGatewayStartedShards(clusterService.localNode(), allocationId, shardStateMetaData.primary);
}

final ShardStateMetaData shardStateMetaData;
try (ShardLock ignored = nodeEnv.shardLock(shardId, TimeUnit.SECONDS.toMillis(5))) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I just spotted this - there are still two calls to nodeEnv.shardLock here. TBH I don't know what we should be doing on failure of this one.

shardStateMetaData = ShardStateMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY,
nodeEnv.availableShardPaths(shardId));
}

if (shardStateMetaData == null) {
logger.trace("{} no local shard info found", shardId);
return new NodeGatewayStartedShards(clusterService.localNode(), null, false);
}

logger.debug("{} shard state info found: [{}]", shardId, shardStateMetaData);
IndexMetaData metaData = clusterService.state().metaData().index(shardId.getIndex());
if (metaData == null) {
// we may send this requests while processing the cluster state that recovered the index
// sometimes the request comes in before the local node processed that cluster state
// in such cases we can load it from disk
metaData = IndexMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY,
nodeEnv.indexPaths(shardId.getIndex()));
}
if (metaData == null) {
ElasticsearchException e = new ElasticsearchException("failed to find local IndexMetaData");
e.setShard(request.shardId);
throw e;
}

// we don't have an open shard on the store, validate the files on disk are openable
ShardPath shardPath = null;
try {
IndexSettings indexSettings = new IndexSettings(metaData, settings);
try (ShardLock ignored = nodeEnv.shardLock(shardId, TimeUnit.SECONDS.toMillis(5))) {
shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings);
}
if (shardPath == null) {
throw new IllegalStateException(shardId + " no shard path found");
}
Store.tryOpenIndex(shardPath.resolveIndex(), shardId, nodeEnv::shardLock, logger);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of acquiring the shard lock for a second time, I would prefer if we would do it once, and move this call under that lock and just rename tryOpenIndex to tryOpenIndexUnderLock, removing the locking mechanism from it.

Same thing for TransportNodesListShardStoreMetaData. You can then also remove the ShardLocker interface, which irked me for a while.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I pushed 61b4e4e and 8f1a5e2. Could you take another look, @ywelsch?

} catch (Exception exception) {
final ShardPath finalShardPath = shardPath;
logger.trace(() -> new ParameterizedMessage(
"{} can't open index for shard [{}] in path [{}]",
shardId,
shardStateMetaData,
(finalShardPath != null) ? finalShardPath.resolveIndex() : ""),
exception);
String allocationId = shardStateMetaData.allocationId != null ?
shardStateMetaData.allocationId.getId() : null;
return new NodeGatewayStartedShards(clusterService.localNode(), allocationId, shardStateMetaData.primary);
return new NodeGatewayStartedShards(clusterService.localNode(), allocationId, shardStateMetaData.primary,
exception);
}
logger.trace("{} no local shard info found", shardId);
return new NodeGatewayStartedShards(clusterService.localNode(), null, false);

logger.debug("{} shard state info found: [{}]", shardId, shardStateMetaData);
String allocationId = shardStateMetaData.allocationId != null ?
shardStateMetaData.allocationId.getId() : null;
return new NodeGatewayStartedShards(clusterService.localNode(), allocationId, shardStateMetaData.primary);

} catch (Exception e) {
throw new ElasticsearchException("failed to load started shards", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,12 @@ public void startRecovery(RecoveryState recoveryState, PeerRecoveryTargetService
}
}

public ShardStateMetaData getShardStateMetaData() {
synchronized (mutex) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can avoid the mutex here. just do a one-time volatile read of shardrouting (which is an immutable object). indexSettings.getUUID() are a final object and the uuid is immutable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I pushed 1d4e044

return new ShardStateMetaData(shardRouting.primary(), indexSettings.getUUID(), shardRouting.allocationId());
}
}

/**
* Returns whether the shard is in primary mode, i.e., in charge of replicating changes (see {@link ReplicationTracker}).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.NodeEnvironment;
import org.elasticsearch.env.ShardLock;
import org.elasticsearch.gateway.AsyncShardFetch;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.IndexSettings;
Expand Down Expand Up @@ -139,7 +140,10 @@ private StoreFilesMetaData listStoreMetaData(ShardId shardId) throws IOException
return new StoreFilesMetaData(shardId, Store.MetadataSnapshot.EMPTY);
}
final IndexSettings indexSettings = indexService != null ? indexService.getIndexSettings() : new IndexSettings(metaData, settings);
final ShardPath shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings);
final ShardPath shardPath;
try (ShardLock ignored = nodeEnv.shardLock(shardId, TimeUnit.SECONDS.toMillis(5))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you double check what the effect is of failing to get the lock?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could potentially mean infinite shard fetching / reroute retry loop if the shard lock is unavailable for an extended time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at how we could be in a situation in which the shard lock is unavailable for a long time. This'd be the case if the shard was open, but that means there's an IndexShard so we don't get here. More precisely, there are some circumstances in which we could get here and then fail to get the shard lock because the shard is now open, but retrying is the thing to do here.

All the other usages of the shard lock seem short-lived. They protect some IO (e.g. deleting the shards, etc) so may take some time, but not infinitely long.

Also, we obtain the same shard lock a few lines down, in Store.readMetadataSnapshot, unless ShardPath.loadShardPath returns null.

Could you clarify, @ywelsch?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In TransportNodesListGatewayStartedShards and in Store.readMetadataSnapshot, which we call below, we catch the ShardLockObtainFailedException and treat it either as an empty store (in case of TransportNodesListShardStoreMetaData) or as an ok target for primary allocation (see TransportNodesListGatewayStartedShards and PrimaryShardAllocator.buildNodeShardsResult), but we've made sure not to end up in a situation where the master goes into a potentially long retry loop (which causes a reroute storm on the master). I don't want to open this box of Pandora here, so my suggestion is to add

} catch (ShardLockObtainFailedException ex) {
    logger.info(() -> new ParameterizedMessage("{}: failed to obtain shard lock", shardId), ex);
    return new StoreFilesMetaData(shardId, Store.MetadataSnapshot.EMPTY);
}

here so as not to mess with existing behavior.

shardPath = ShardPath.loadShardPath(logger, nodeEnv, shardId, indexSettings);
}
if (shardPath == null) {
return new StoreFilesMetaData(shardId, Store.MetadataSnapshot.EMPTY);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.NodeEnvironment;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.query.QueryBuilders;
Expand All @@ -46,8 +47,10 @@
import org.elasticsearch.test.ESIntegTestCase.Scope;
import org.elasticsearch.test.InternalTestCluster;
import org.elasticsearch.test.InternalTestCluster.RestartCallback;
import org.elasticsearch.test.junit.annotations.TestLogging;
import org.elasticsearch.test.store.MockFSIndexStore;

import java.io.File;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -58,6 +61,8 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.stream.IntStream;

import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
Expand Down Expand Up @@ -567,4 +572,70 @@ public Settings onNodeStopped(String nodeName) throws Exception {
// start another node so cluster consistency checks won't time out due to the lack of state
internalCluster().startNode();
}

public void testLoadLatestStateWhileClosingShardDoesNotResurrectMetadataDirectory() throws Exception {

// This test pertains to a race condition in which deleting a shard concurrently with a TransportNodesListGatewayStartedShards
// request could resurrect the shard's metadata folder after it was deleted. Here we try and recreate the race, but it is quite
// delicate so this test does not always fail. Experimentation showed that setting the thread counts as below would yield a failure
// after a reasonable number of iterations: running repeatedly with -Dtests.iters=1000 saw 6 failures out of 10 runs.
//
// NB this experiment was run with
// @TestLogging("org.elasticsearch.env.NodeEnvironment:TRACE,org.elasticsearch.gateway.MetaDataStateFormat:TRACE," +
// "org.elasticsearch.gateway.TransportNodesListGatewayStartedShards:TRACE,org.elasticsearch.index.shard.IndexShard:TRACE")
// but with less verbose logging the failures seem rarer.

final String nodeName = internalCluster().startNode();
DiscoveryNode node = internalCluster().getInstance(ClusterService.class, nodeName).localNode();

assertAcked(prepareCreate("test").setSettings(Settings.builder()
.put(SETTING_NUMBER_OF_SHARDS, 1).put(SETTING_NUMBER_OF_REPLICAS, 0)));
final ShardId shardId = new ShardId(resolveIndex("test"), 0);

final int listingThreadCount = 2;
final int deletingThreadCount = 2;

final CountDownLatch countDownLatch = new CountDownLatch(listingThreadCount + deletingThreadCount);

Thread threads[] = new Thread[listingThreadCount + deletingThreadCount];
for (int threadIndex = 0; threadIndex < listingThreadCount + deletingThreadCount; threadIndex++) {
final boolean isListingThread = threadIndex < listingThreadCount;
threads[threadIndex] = new Thread(() -> {
try {
countDownLatch.countDown();
countDownLatch.await();

if (isListingThread) {
internalCluster().getInstance(TransportNodesListGatewayStartedShards.class)
.execute(new TransportNodesListGatewayStartedShards.Request(shardId, new DiscoveryNode[]{node}))
.get();
} else {
assertAcked(client().admin().indices().prepareDelete("test"));
}
} catch (InterruptedException | ExecutionException | IndexNotFoundException ignored) {
// don't care if this fails
}
}, (isListingThread ? "Listing" : "Deleting") + "[" + threadIndex + "]");
}

NodeEnvironment nodeEnvironment = internalCluster().getInstance(NodeEnvironment.class, nodeName);

boolean directoryExists = false;
for (Path path : nodeEnvironment.availableShardPaths(shardId)) {
directoryExists = directoryExists || Files.exists(path);
}
assertTrue(directoryExists);

for (final Thread thread : threads) {
thread.start();
}

for (final Thread thread : threads) {
thread.join();
}

for (Path path : nodeEnvironment.availableShardPaths(shardId)) {
assertFalse(path + " should not exist", Files.exists(path));
}
}
}