-
Notifications
You must be signed in to change notification settings - Fork 25.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Timing out stale remote master history #86936
Changes from 7 commits
7c758c4
649f5fa
caf530a
995c4b0
a103db2
3cd4b3d
8327203
5107b18
e6fa99d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -16,6 +16,7 @@ | |||||
import org.elasticsearch.action.admin.cluster.coordination.MasterHistoryAction; | ||||||
import org.elasticsearch.cluster.node.DiscoveryNode; | ||||||
import org.elasticsearch.cluster.service.ClusterService; | ||||||
import org.elasticsearch.common.settings.Setting; | ||||||
import org.elasticsearch.core.Nullable; | ||||||
import org.elasticsearch.core.TimeValue; | ||||||
import org.elasticsearch.threadpool.ThreadPool; | ||||||
|
@@ -25,6 +26,8 @@ | |||||
import org.elasticsearch.transport.TransportService; | ||||||
|
||||||
import java.util.List; | ||||||
import java.util.concurrent.TimeUnit; | ||||||
import java.util.function.LongSupplier; | ||||||
|
||||||
/** | ||||||
* This service provides access to this node's view of the master history, as well as access to other nodes' view of master stability. | ||||||
|
@@ -33,17 +36,34 @@ public class MasterHistoryService { | |||||
private final TransportService transportService; | ||||||
private final MasterHistory localMasterHistory; | ||||||
private final ClusterService clusterService; | ||||||
private final LongSupplier currentTimeMillisSupplier; | ||||||
private final TimeValue acceptableRemoteHistoryAge; | ||||||
/* | ||||||
* This is a view of the master history one a remote node, or the exception that fetching it resulted in. This is populated | ||||||
* asynchronously. | ||||||
* asynchronously. It is non-private for testing. Note that this field is not nulled out after its time to live expires. That check | ||||||
* is only done in getRemoteMasterHistory(). All non-testing access to this field needs to go through getRemoteMasterHistory(). | ||||||
*/ | ||||||
volatile RemoteHistoryOrException remoteHistoryOrException = new RemoteHistoryOrException(null, null); // non-private for testing | ||||||
volatile RemoteHistoryOrException remoteHistoryOrException = new RemoteHistoryOrException(null, null, Long.MIN_VALUE); | ||||||
private static final Logger logger = LogManager.getLogger(MasterHistoryService.class); | ||||||
|
||||||
private static final TimeValue DEFAULT_REMOTE_HISTORY_TIME_TO_LIVE = new TimeValue(5, TimeUnit.MINUTES); | ||||||
|
||||||
/** | ||||||
* This is the amount of time that can pass after a RemoteHistoryOrException is returned from the remote master until it is | ||||||
* considered stale and not usable. | ||||||
*/ | ||||||
public static final Setting<TimeValue> REMOTE_HISTORY_TIME_TO_LIVE_SETTING = Setting.positiveTimeSetting( | ||||||
"master_history.remote_history_time_to_live", | ||||||
DEFAULT_REMOTE_HISTORY_TIME_TO_LIVE, | ||||||
Setting.Property.NodeScope | ||||||
); | ||||||
|
||||||
public MasterHistoryService(TransportService transportService, ThreadPool threadPool, ClusterService clusterService) { | ||||||
this.transportService = transportService; | ||||||
this.localMasterHistory = new MasterHistory(threadPool, clusterService); | ||||||
this.clusterService = clusterService; | ||||||
this.currentTimeMillisSupplier = threadPool::relativeTimeInMillis; | ||||||
this.acceptableRemoteHistoryAge = REMOTE_HISTORY_TIME_TO_LIVE_SETTING.get(clusterService.getSettings()); | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -60,14 +80,23 @@ public MasterHistory getLocalMasterHistory() { | |||||
* updated even if the ClusterState is updated on this node or the remote node. The history is retrieved asynchronously, and only if | ||||||
* requestRemoteMasterHistory has been called for this node. If anything has gone wrong fetching it, the exception returned by the | ||||||
* remote machine will be thrown here. If the remote history has not been fetched or if something went wrong and there was no exception, | ||||||
* the returned value will be null. | ||||||
* the returned value will be null. If the remote history is old enough to be considered stale (that is, older than | ||||||
* MAX_USABLE_REMOTE_HISTORY_AGE_SETTING), then the returned value will be null. | ||||||
* @return The MasterHistory from a remote node's point of view. This MasterHistory object will not be updated with future changes | ||||||
* @throws Exception the exception (if any) returned by the remote machine when fetching the history | ||||||
*/ | ||||||
@Nullable | ||||||
public List<DiscoveryNode> getRemoteMasterHistory() throws Exception { | ||||||
// Grabbing a reference to the object in case it is replaced in another thread during this method: | ||||||
RemoteHistoryOrException remoteHistoryOrExceptionCopy = remoteHistoryOrException; | ||||||
/* | ||||||
* If the remote history we have is too old, we just return null with the assumption that it is stale and the new one has not | ||||||
* come in yet. | ||||||
*/ | ||||||
long acceptableRemoteHistoryTime = currentTimeMillisSupplier.getAsLong() - acceptableRemoteHistoryAge.getMillis(); | ||||||
if (remoteHistoryOrExceptionCopy.creationTimestamp < acceptableRemoteHistoryTime) { | ||||||
return null; | ||||||
} | ||||||
if (remoteHistoryOrExceptionCopy.exception != null) { | ||||||
throw remoteHistoryOrExceptionCopy.exception; | ||||||
} | ||||||
|
@@ -108,13 +137,16 @@ public void onResponse(Transport.Connection connection) { | |||||
public void onResponse(MasterHistoryAction.Response response) { | ||||||
long endTime = System.nanoTime(); | ||||||
logger.trace("Received history from {} in {}", node, TimeValue.timeValueNanos(endTime - startTime)); | ||||||
remoteHistoryOrException = new RemoteHistoryOrException(response.getMasterHistory()); | ||||||
remoteHistoryOrException = new RemoteHistoryOrException( | ||||||
response.getMasterHistory(), | ||||||
currentTimeMillisSupplier.getAsLong() | ||||||
); | ||||||
} | ||||||
|
||||||
@Override | ||||||
public void onFailure(Exception e) { | ||||||
logger.warn("Exception in master history request to master node", e); | ||||||
remoteHistoryOrException = new RemoteHistoryOrException(e); | ||||||
remoteHistoryOrException = new RemoteHistoryOrException(e, currentTimeMillisSupplier.getAsLong()); | ||||||
} | ||||||
}, connection::close), MasterHistoryAction.Response::new) | ||||||
); | ||||||
|
@@ -132,26 +164,27 @@ public void onFailure(Exception e) { | |||||
@Override | ||||||
public void onFailure(Exception e) { | ||||||
logger.warn("Exception connecting to master node", e); | ||||||
remoteHistoryOrException = new RemoteHistoryOrException(e); | ||||||
remoteHistoryOrException = new RemoteHistoryOrException(e, currentTimeMillisSupplier.getAsLong()); | ||||||
} | ||||||
} | ||||||
); | ||||||
} | ||||||
|
||||||
record RemoteHistoryOrException(List<DiscoveryNode> remoteHistory, Exception exception) { // non-private for testing | ||||||
// non-private for testing | ||||||
record RemoteHistoryOrException(List<DiscoveryNode> remoteHistory, Exception exception, long creationTimestamp) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
public RemoteHistoryOrException { | ||||||
if (remoteHistory != null && exception != null) { | ||||||
throw new IllegalArgumentException("Remote history and exception cannot both be non-null"); | ||||||
} | ||||||
} | ||||||
|
||||||
RemoteHistoryOrException(List<DiscoveryNode> remoteHistory) { | ||||||
this(remoteHistory, null); | ||||||
RemoteHistoryOrException(List<DiscoveryNode> remoteHistory, long creationTimestamp) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we rename the creationTimestamp to include the unit everywhere? ( https://github.com/elastic/elasticsearch/pull/86936/files#r880273628 ) |
||||||
this(remoteHistory, null, creationTimestamp); | ||||||
} | ||||||
|
||||||
RemoteHistoryOrException(Exception exception) { | ||||||
this(null, exception); | ||||||
RemoteHistoryOrException(Exception exception, long creationTimestamp) { | ||||||
this(null, exception, creationTimestamp); | ||||||
} | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we want to also at some point null-ify
remoteHistoryOrException
? (we'd likely need an AtomicReference or such to do a compare and swap)What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thinking was that there was no harm in holding onto a single one, and it avoids having to synchronize access to it. Synchronizing it would not be super expensive (we'd need to read it, do the time calculation, and set it to null in an atomic action), but it seemed like an unnecessary complication. Is it the memory that you're concerned about? We cap these things to have at most 50 entries, so I'd expect at most a few KB here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was mostly worried about the
remoteHistoryOrException
leaking outside the service by other means thangetRemoteMasterHistory
or the service evolving and using theremoteHistoryOrException
in a private way (assuming it'snull
when it's stale, which would not be a wild assumption given what the getter returns) - @DaveCTurner would you have a strong opinion here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That second concern would maybe mean having a thread to time this thing out. That seems like something we ought to deal with if we really need it in the future, but seems overly complex for the current situation doesn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am inclined to leave it in place too. You never know, it might even end up being useful in a heap dump. Maybe add a comment to the field indicating that it might be stale.
For future reference it wouldn't be a big deal to clear it after the timeout either:
(also there's no need for a
ThreadPool
constructor argument if you have aTransportService
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
++ let's leave it as is then