Skip to content

CASSANDRA-20476 Cluster is unable to recover after shutdown if IPs change - #4979

Open
beobal wants to merge 28 commits into
apache:cassandra-6.0from
beobal:samt/CASSANDRA-20476
Open

CASSANDRA-20476 Cluster is unable to recover after shutdown if IPs change#4979
beobal wants to merge 28 commits into
apache:cassandra-6.0from
beobal:samt/CASSANDRA-20476

Conversation

@beobal

@beobal beobal commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Thanks for sending a pull request! Here are some tips if you're new here:

  • Ensure you have added or run the appropriate tests for your PR.
  • Be sure to keep the PR description updated to reflect all changes.
  • Write your PR title to summarize what this PR proposes.
  • If possible, provide a concise example to reproduce the issue for a faster review.
  • Read our contributor guidelines
  • If you're making a documentation change, see our guide to documentation contribution

Commit messages should follow the following format:

<One sentence description, usually Jira title or CHANGES.txt summary>

<Optional lengthier description (context on patch)>

patch by <Authors>; reviewed by <Reviewers> for CASSANDRA-#####

Co-authored-by: Name1 <email1>
Co-authored-by: Name2 <email2>

The Cassandra Jira

beobal and others added 27 commits July 29, 2026 14:58
If two nodes restart with new broadcast addresses concurrently
and one is a CMS member, the non-member may send a TCM_COMMIT_REQ
containing its STARTUP transform to the CMS member's old address.
If this is no longer reachable, the sender should time out quickly
so it can resend to another CMS member, or try to discover the new
CMS address(es).
public CandidateIterator(Collection<InetAddressAndPort> initialContacts, boolean checkLive)
{
this.candidates = new ConcurrentLinkedDeque<>(initialContacts);
this.elements = new HashSet<>(initialContacts);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this needs to be a concurrent set

// recalculate CMS placement using endpoint mappings from lookup
cmsDataPlacement = calculateCMSPlacement(placements, cmsMembership, proposedLookup);

// We shouldn't need to null out the other lazily initialized CMS fields when we refresh the CMSLookup as the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we assert that they are null?

logger.debug("Received {} responses. {} required.", responses.size(), requiredResponses);
if (responses.size() >= requiredResponses)
{
isDone = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

markDone() instead to get the logging?

this(ClusterMetadata.current().metadataIdentifier, MessagingService::instance);
}

public SurveyRequestHandler(int metadataId, Supplier<MessageDelivery> messaging)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit, @ VisibleForTesting

// During first initialization, placements are hardcoded to the local address after the PRE_INITIALIZE_CMS is
// committed so that the subsequent INITIALIZE_CMS can be.
//
// * Some changes to the addresses of the CMS members have been changed and whilst this is known (i.e. has been

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit, Some addresses of the CMS members have been changed...

@Override
public InetAddressAndPort endpoint(NodeId id)
{
if (overrides.containsKey(id))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit, avoid looking up the key twice:

                Pair<InetAddressAndPort, InetAddressAndPort> override = overrides.get(id);
                if (override != null)
                    return override.right;
                return lookup.endpoint(id);
...


Result.Success success = result.success();
Directory directory = directorySupplier.get();
EndpointLookup lookup = lookupSupplier.get();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is there a possibility that we get "mismatching" directory/lookup here since we call suppliers twice? Maybe the supplier should be something like Supplier<Pair<Directory, EndpointLookup>> to make sure it's backed by the same ClusterMetadata?

{
logger.info("Waiting for pending CMS address changes to complete {}", replayed.cmsLookup);
TimeUnit.MILLISECONDS.sleep(1000);
replayed = ClusterMetadata.current();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a chance we miss a Startup and get stuck forever here? Should we do an explicit log fetch periodically? Maybe not every second, but once a minute or so, just to make sure?

}

logger.info("Proposed endpoint overrides: {}", nextOverrides);
State state = nextOverrides.isEmpty() ? State.RETIRED : State.ACTIVE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could/should we retire the lookup once we have a quorum committed? This would allow us to recover if one of the CMS nodes is gone forever

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants