### Description

`SearchResponse.Clusters` currently uses a `HashMap<String, AtomicReference>` for storing Cluster objects. The AtomicReference CAS swaps are done in a do/while loop in most cases (e.g., in `CCSSingleCoordinatorSearchProgressListener`).

This can be replaced with a `ConcurrentHashMap` in conjunction with either `compute()` method or the `replace(key, assumed_previous_value, value)` method for atomically swapping in a new Cluster object with a CAS operation, assuring that the updates don't lose data from competing threads doing separate Cluster object updates.

See: https://github.com/elastic/elasticsearch/pull/98457/files#r1310467586 
server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java
try (XContentParser parser = createParser(xcontentType.xContent(), mutated)) {
            SearchResponse parsed = SearchResponse.fromXContent(parser);
            assertToXContentEquivalent(originalBytes, XContentHelper.toXContent(parsed, xcontentType, params, humanReadable), xcontentType);
            assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken());
            assertNull(parser.nextToken());
        }
    }
    /**
     * The "_shard/total/failures" section makes if impossible to directly compare xContent, because
     * the failures in the parsed SearchResponse are wrapped in an extra ElasticSearchException on the client side.
     * Because of this, in this special test case we compare the "top level" fields for equality
     * and the subsections xContent equivalence independently
     */
    public void testFromXContentWithFailures() throws IOException {
        int numFailures = randomIntBetween(1, 5);
        ShardSearchFailure[] failures = new ShardSearchFailure[numFailures];
        for (int i = 0; i < failures.length; i++) {
            failures[i] = ShardSearchFailureTests.createTestItem(IndexMetadata.INDEX_UUID_NA_VALUE);
        }
        SearchResponse response = createTestItem(failures);
        XContentType xcontentType = randomFrom(XContentType.values());
        final ToXContent.Params params = new ToXContent.MapParams(singletonMap(RestSearchAction.TYPED_KEYS_PARAM, "true"));
        BytesReference originalBytes = toShuffledXContent(
            ChunkedToXContent.wrapAsToXContent(response),
            xcontentType,
            params,
            randomBoolean()
        );
        try (XContentParser parser = createParser(xcontentType.xContent(), originalBytes)) {
            SearchResponse parsed = SearchResponse.fromXContent(parser);