Skip to content
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

Prevent duplicate SharedShardContext.readerId #15520

Merged
merged 1 commit into from
Feb 13, 2024

Conversation

jeeminso
Copy link
Contributor

@jeeminso jeeminso commented Feb 6, 2024

Summary of the changes / Why this improves CrateDB

Fixes #15518.

As shown, SharedShardContext.readerId is duplicated causing the exception:

스크린샷, 2024-02-05 15-39-29

To my understanding, the only purpose of readerId is to be used as the keys for CollectTask.searchers. If so, we can replace it with shardId which is unique by design and prevents the race that caused the duplicate ids. See #15520 (comment) for details.

Checklist

  • Added an entry in the latest docs/appendices/release-notes/<x.y.0>.rst for user facing changes
  • Updated documentation & sql_features table for user facing changes
  • Touched code is covered by tests
  • CLA is signed
  • This does not contain breaking changes, or if it does:
    • It is released within a major release
    • It is recorded in the latest docs/appendices/release-notes/<x.y.0>.rst
    • It was marked as deprecated in an earlier release if possible
    • You've thought about the consequences and other components are adapted
      (E.g. AdminUI)

@jeeminso jeeminso changed the title Make SharedShardContext.readerId unique Prevent duplicate SharedShardContext.readerId Feb 6, 2024
@@ -102,9 +101,8 @@ public SharedShardContext getOrCreateContext(ShardId shardId) throws IndexNotFou
SharedShardContext sharedShardContext = allocatedShards.get(shardId);
if (sharedShardContext == null) {
IndexService indexService = indicesService.indexServiceSafe(shardId.getIndex());
sharedShardContext = new SharedShardContext(indexService, shardId, readerId, wrapSearcher);
sharedShardContext = new SharedShardContext(indexService, shardId, shardId.hashCode(), wrapSearcher);
Copy link
Contributor

Choose a reason for hiding this comment

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

If these are always tied to a shard, and there is only a single context per shard for a given job, then do we even need the separate reader ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you as you said it looks to me that we can eliminate reader ID completely. @mfussenegger could you clarify if this makes sense?

Copy link
Member

Choose a reason for hiding this comment

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

In the fetch case the readerId is used to encode into a FetchId which shard is used, so that we can lookup a document from the correct shard. See:

  • ReaderAllocations(TreeMap<String, Integer> bases,
    Map<String, Map<Integer, String>> shardNodes,
    Map<RelationName, Collection<String>> tableIndices) {
    this.bases = bases;
    this.tableIndices = tableIndices;
    this.indicesToIdents = new HashMap<>(tableIndices.values().size());
    for (Map.Entry<RelationName, Collection<String>> entry : tableIndices.entrySet()) {
    for (String index : entry.getValue()) {
    indicesToIdents.put(index, entry.getKey());
    }
    }
    for (Map.Entry<String, Integer> entry : bases.entrySet()) {
    readerIndices.put(entry.getValue(), entry.getKey());
    }
    for (Map.Entry<String, Map<Integer, String>> entry : shardNodes.entrySet()) {
    Integer base = bases.get(entry.getKey());
    if (base == null) {
    continue;
    }
    for (Map.Entry<Integer, String> nodeEntries : entry.getValue().entrySet()) {
    int readerId = base + nodeEntries.getKey();
    IntSet readerIds = nodeReaders.get(nodeEntries.getValue());
    if (readerIds == null) {
    readerIds = new IntHashSet();
    nodeReaders.put(nodeEntries.getValue(), readerIds);
    }
    readerIds.add(readerId);
    }
    }
    }
  • int readerId = base + shardId.id();
    SharedShardContext shardContext = shardContexts.get(readerId);
    if (shardContext == null) {
    try {
    shardContext = sharedShardContexts.createContext(shardId, readerId);

What'd be more interesting is how the increments can lead to duplicates. The whole code section is under the assumption that the preparation phase is run single threaded. See also #5248

If that assumption is no longer true - maybe due to #10373, or if the SharedShardContexts is accessed elsewhere, we probably need to change more than just the readerId.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the pointers, I think it is due to #10373 which changes ShardCollectSource.getIterator() to return future type.

CompletableFuture<BatchIterator<Row>> iterator = shardCollectorProvider
.awaitShardSearchActive()
.thenApply(batchIteratorFactory -> batchIteratorFactory.getIterator(
collectPhase,
requiresScroll,
collectTask
))

When more than one awaitShardSearchActive() completes, it would cause a race for a reader ID.

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 tried to sequentialize the calls to SharedShardContexts.getOrCreateContext() which is the only place that assigns reader ids to SharedShardContext, 3da1073.

Copy link
Contributor Author

@jeeminso jeeminso Feb 7, 2024

Choose a reason for hiding this comment

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

Hi @mfussenegger could you have a look if the fix is ok before I start adding tests? Now SharedShardContexts.getOrCreateContext() is only called from two places. Sorry my last minute fix causes test failures, I will look at this first.

Copy link
Member

Choose a reason for hiding this comment

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

I think I'd prefer adding synchronization to the methods again. But first we should ensure that we've identified the real problem. Did you do that?

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 think the real problem is #15520 (comment).

For example, If I attach a logger:

    public SharedShardContext getOrCreateContext(ShardId shardId) throws IndexNotFoundException {
        LOGGER.info("Begin getOrCreateContext : " + Thread.currentThread());
        SharedShardContext sharedShardContext = allocatedShards.get(shardId);
        if (sharedShardContext == null) {
            IndexService indexService = indicesService.indexServiceSafe(shardId.getIndex());
            sharedShardContext = new SharedShardContext(indexService, shardId, readerId, wrapSearcher);
            allocatedShards.put(shardId, sharedShardContext);
            readerId++;
        }
        LOGGER.info("End getOrCreateContext : " + Thread.currentThread());
        return sharedShardContext;
    }

I can observe two thread being interleaved:

[2024-02-07T09:20:43,629][INFO ][i.c.e.j.SharedShardContexts] [Grand Parpaillon] Begin getOrCreateContext : Thread[#356,cratedb[Grand Parpaillon][listener][T#3],5,main]
[2024-02-07T09:20:43,629][INFO ][i.c.e.j.SharedShardContexts] [Grand Parpaillon] Begin getOrCreateContext : Thread[#354,cratedb[Grand Parpaillon][listener][T#1],5,main]
[2024-02-07T09:20:43,629][INFO ][i.c.e.j.SharedShardContexts] [Grand Parpaillon] End getOrCreateContext : Thread[#356,cratedb[Grand Parpaillon][listener][T#3],5,main]
[2024-02-07T09:20:43,629][INFO ][i.c.e.j.SharedShardContexts] [Grand Parpaillon] End getOrCreateContext : Thread[#354,cratedb[Grand Parpaillon][listener][T#1],5,main]

right before the exception is thrown.

Copy link
Member

Choose a reason for hiding this comment

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

Can we then just revert parts of https://github.com/crate/crate/pull/5248/files and bring back the synchronization?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, reverted.

@jeeminso jeeminso force-pushed the jeeminso/shard-context-read-id branch 9 times, most recently from 5811a37 to 381b0ee Compare February 8, 2024 03:11
@jeeminso jeeminso added the v/5.6 label Feb 8, 2024
@jeeminso jeeminso marked this pull request as ready for review February 8, 2024 03:11
@jeeminso jeeminso force-pushed the jeeminso/shard-context-read-id branch from 381b0ee to 3928001 Compare February 8, 2024 14:09
@BaurzhanSakhariev
Copy link
Contributor

#11677 will be also addressed by this?

@jeeminso
Copy link
Contributor Author

jeeminso commented Feb 8, 2024

#11677 will be also addressed by this?

It is hard to say without being able to reproduce.

I just look into SharedShardContexts being shared between fetch and collect could cause duplicate reader Ids (within SharedShardContexts.allocatedShards) ultimately throwing the same exception. I think it is not possible since we setup tasks one at a time:

logger.trace("Starting task job={} phase={} name={}", jobId, phaseId, task.name());
CompletableFuture<Void> started = task.start();
if (started != null) {
return started.thenCompose(ignored -> start(taskIndex + 1));

Another possibility could be #15495 (comment) which I was looking into before Moll provided reproduction steps.

Comment on lines +108 to +107
synchronized (this) {
sharedShardContext = allocatedShards.get(shardId);
Copy link
Member

Choose a reason for hiding this comment

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

Other allocatedShards accesses within the file afaik also need to be synchronized to ensure this works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, It does look like createContext() needs to be synchronized but if there is a scenario where createContext() and getOrCreateContext() race for allocatedShards, shouldn't we fix it by not calling createContext()? Considering createContext() as a lightweight version of getOrCreateContext().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh I guess we need synchronization for allocatedShards.put() calls.

Comment on lines 72 to 74
- Fixed an issue that caused exceptions with messages like
'ShardCollectContext already added' in low heap situations causing multiple
shards to be idle and be active simultaneously.
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this has anything to do with low heap, as the heap has no influence on whether shards are idle and active.

And I also don't think the amount of shards going from idle to active has any impact on if the race condition happens.

Suggested change
- Fixed an issue that caused exceptions with messages like
'ShardCollectContext already added' in low heap situations causing multiple
shards to be idle and be active simultaneously.
- Fixed a race condition that could lead to ``ShardCollectContext already
added`` errors when making a query after a table had been idle without any
accesses for a while.

@@ -40,15 +40,18 @@

import com.carrotsearch.hppc.IntIndexedContainer;

import io.crate.common.annotations.VisibleForTesting;
import io.crate.metadata.IndexParts;

@NotThreadSafe
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@NotThreadSafe

by synchronizing part of `SharedShardContexts.getOrCreateContext()` that
increments `readerId` and populates `allocatedShards`
@jeeminso jeeminso force-pushed the jeeminso/shard-context-read-id branch from 46ebe4e to 636f55e Compare February 13, 2024 12:41
@jeeminso jeeminso added the ready-to-merge Let Mergify merge the PR once approved and checks pass label Feb 13, 2024
@mergify mergify bot merged commit bdd2c5e into master Feb 13, 2024
18 checks passed
@mergify mergify bot deleted the jeeminso/shard-context-read-id branch February 13, 2024 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready-to-merge Let Mergify merge the PR once approved and checks pass v/5.6
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error ShardCollectContext for {0,1,2} already added in low-memory situations
4 participants