Skip to content

Conversation

jjezra
Copy link
Contributor

@jjezra jjezra commented Aug 4, 2025

Each indexing session, for each index, will create a key-value heartbeat of the format:
[prefix, xid] -> [indexing-type, genesis time, heartbeat time]
Indexing session that are expected to be exclusive will throw an exception if another, active, session exists.

Motivation:
1. Represent the heartbeat in every index during multi target indexing (currently - only the master index has a sync lock)
2. Keep a heartbeat during mutual indexing, which can allow better automatic decision making
3. Decide about exclusiveness according to the indexing method (currently - user input)

With this change, the equivalent of a sync lock will be determined by the indexing type and cannot be set by the users. The index configuration function setUseSynchronizedSession will have no effect on the indexing process.

During graduate code upgrade on multiple servers, there may be a situation where one server is indexing with a synchronized session lock, while another server builds the same index with an exclusive heartbeat "lock". If that happens:
a) There will be no more than two concurrent active sessions
b) The indexing sessions will conflict each other until one of the indexers will give up. While this may be not optimal, the generated index will be valid.

Resolve #3529

@jjezra jjezra added the breaking change Changes that are not backwards compatible label Aug 4, 2025
@jjezra jjezra force-pushed the indexing_heartbeat branch from 89d0263 to f318f65 Compare August 7, 2025 15:55
@jjezra jjezra requested a review from ScottDugas August 8, 2025 17:18
@jjezra jjezra force-pushed the indexing_heartbeat branch from a680857 to 83830c6 Compare August 11, 2025 12:01
@jjezra jjezra marked this pull request as ready for review August 11, 2025 14:13
@jjezra jjezra marked this pull request as draft August 11, 2025 15:06
@jjezra jjezra marked this pull request as ready for review August 11, 2025 20:23
Copy link
Collaborator

@ScottDugas ScottDugas left a comment

Choose a reason for hiding this comment

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

I didn't get a chance to look at all of the code, we can discuss some of the things offline.

@@ -180,7 +178,6 @@ <M extends Message> void singleRebuild(
if (!safeBuild) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does safeBuild here mean in this context? Do we need to continue to prevent concurrency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The things that had left to non-safeBuild, as it seems, are clearing and marking the index as write-only and setting constrains that, IIUIC, should not be happening after the previous clearing.

After so many changes in the code, maybe it is time to re-design (and possibly simplify) the singleRebuild. Should that be a separate PR?

Copy link
Collaborator

Choose a reason for hiding this comment

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

changing and cleaning up safeBuild should not be part of this PR.

@@ -1237,7 +1267,7 @@ public static class Builder {
private DesiredAction ifReadable = DesiredAction.CONTINUE;
private boolean doAllowUniquePendingState = false;
private Set<TakeoverTypes> allowedTakeoverSet = null;
private long checkIndexingStampFrequency = 60_000;
private long checkIndexingStampFrequency = 10_000;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why check the type stamp more often?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To align the default stamp & heartbeat checking with the default lease time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't you want the lease time to be longer than the heartbeat?
If you only update the heartbeat every 10 seconds, and you consider it too-old after 10 seconds, it doesn't take much clock skew to have indexers stepping over each other.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had changed the default to check heartbeats and typestamp every 5 seconds. I would consider changing the default least time for longer than 10 seconds, but this should probably be done on a separate PR.

@jjezra jjezra force-pushed the indexing_heartbeat branch from 4cf86f4 to 97d02b6 Compare August 15, 2025 13:22
@jjezra jjezra requested a review from ScottDugas August 15, 2025 13:52
Copy link
Collaborator

@ScottDugas ScottDugas left a comment

Choose a reason for hiding this comment

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

There were also two comments left on my previous review, that I want to followup on. I resolved everything else in that review, or answered in a (I hope) definitive way, so it should be easy to go back through.

List<Index> indexes = new ArrayList<>();
indexes.add(new Index("indexA", field("num_value_2"), EmptyKeyExpression.EMPTY, IndexTypes.VALUE, IndexOptions.UNIQUE_OPTIONS));
indexes.add(new Index("indexB", field("num_value_3_indexed"), IndexTypes.VALUE));
FDBRecordStoreTestBase.RecordMetaDataHook hook = allIndexesHook(indexes);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's a matter of the store creation. When you create an FDBRecordStore it is passed in a SubspaceProvider, which could present something that is not a tuple. Looking briefly at the amount of testing infrastructure that you'd have to rework, I'm not sure how likely it is that something else wouldn't break.
I think it would be valuable to have, but I don't think it's worth the effort, and I think we might be better off focusing on changing FDBRecordStore to take a KeySpacePath only.
I don't think there's any way to do anything after that that is not a Tuple

startSemaphore.acquire();
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should always re-interrupt the current thread when catching InterruptedException

Copy link
Contributor Author

Choose a reason for hiding this comment

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

try (FDBRecordContext context = openContext()) {
final Map<UUID, IndexBuildProto.IndexBuildHeartbeat> heartbeats = IndexingHeartbeat.getIndexingHeartbeats(recordStore, indexes.get(0), 0).join();
heartbeatsQueries.add(heartbeats);
context.commit();
Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need to commit here, you're just doing a read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Getting warnings. I don't think that creating a context without commit passes a PRB now.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;

public class IndexingHeartbeat {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We discussed offline, and decided to take a balanced approach of changing the method to info, so that this is, on disk, future-proofed for being upgraded to be more generic if we want to use it elsewhere. There would still need to be some substantial changes to the code, but it would need to take a subspace instead of a store and an index.


// clear, partial
int countDeleted =
IndexingHeartbeat.clearIndexingHeartbeats(recordStore, index, 0, 7).join();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is clearing 7 entries, regardless of their last heartbeat, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Max age to delete is 0.

jjezra added 17 commits August 21, 2025 16:53
   Each indexing session, for each index, will create a key-value heartbeat of the format:
        [prefix, xid] -> [indexing-type, genesis time, heartbeat time]
   Indexing session that are expected to be exclusive will throw an exception if another, active, session exists.

   Motivation:

        1. Represent the heartbeat in every index during multi target indexing (currently - only the master index has a sync lock)
        2. Keep a heartbeat during mutual indexing, which can allow better automatic decision making
        3. Decide about exclusiveness according to the indexing method (currently - user input)

Resolve FoundationDB#3529
@jjezra jjezra force-pushed the indexing_heartbeat branch from 97d02b6 to 149b070 Compare August 22, 2025 20:24
@jjezra jjezra force-pushed the indexing_heartbeat branch from 86ad032 to 2a9f7e5 Compare August 25, 2025 15:35
@jjezra jjezra requested a review from ScottDugas August 25, 2025 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change Changes that are not backwards compatible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Online Indexer: replace the synchronized runner with a heartbeat
2 participants