Skip to content

Improve concurrency in TaskQueue - #17828

Merged
cryptoe merged 13 commits into
apache:masterfrom
kfaraz:remove_taskqueue_giantlock
May 19, 2025
Merged

cryptoe merged 13 commits into
apache:masterfrom
kfaraz:remove_taskqueue_giantlock

Conversation

@kfaraz

@kfaraz kfaraz commented Mar 23, 2025 •

Copy link
Copy Markdown
Contributor

Description

The giant lock in TaskQueue is acquired when performing any update or read operation, such as:

  • add a task
  • remove a task
  • read the list of tasks
  • sync from storage

On large clusters with several concurrent tasks, this can become a bottleneck.

This patch attempts to improve the concurrency in TaskQueue by using a concurrent hash map instead.

Changes

  • Use a ConcurrentHashMap to track active tasks. The giant lock was needed only to handle
    competing updates made to the same task ID. This can be handled by a ConcurrentHashMap too.
  • Perform any update on the entry for a task ID within ConcurrentHashMap.compute to ensure atomicity
  • Convert the giant lock from a ReentrantLock to a ReentrantReadWriteLock
  • Repurpose giant lock to ensure that the TaskQueue start and stop is mutually exclusive from any other
    normal operation on the TaskQueue
  • Keep a lastUpdatedTime in every TaskEntry to handle race conditions when syncing from metadata store

Handling race conditions

The only possible race conditions are with syncFromStorage(),
and are handled by maintaining a lastUpdatedTime for every task entry.

All updates to a task entry happen inside a ConcurrentHashMap.compute(taskId, entry -> update(entry))
and are thus thread-safe.

[A] Sync should not add a task to queue if it has just been removed

This is handled by ensuring that only the syncFromStorage() method can remove tasks from the queue.
A task that completed after the poll started would have a lastUpdatedTime after syncStartTime.
In this case, since an entry already exists in the queue, a new one will not be added.
The next invocation of syncFromStorage() will add the task, if necessary.

[B] Sync should not remove a task from queue if it has just been added

A task added after the poll started would have a lastUpdatedTime after syncStartTime.
In this case, the entry will not be removed.
The next invocation of syncFromStorage() will clean up the task, if necessary.

Pending

  • Unit tests for concurrency
  • Cluster testing

This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

@gianm gianm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, feel free to merge after considering the comment about the name of the giant lock.

* queue management is in progress. {@link #start} and {@link #stop} methods
* acquire a WRITE lock whereas all other operations acquire a READ lock.
*/
private final ReentrantReadWriteLock giant = new ReentrantReadWriteLock(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider renaming to startStopLock, since its main purpose now is to synchronize start and stop with each other and with the other operations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated. Thanks for the review.

public void stop()
{
giant.lock();
giant.writeLock().lock();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be good we we add tests triggering this behavior ie
one threads is hitting start/stop method while the other is trying to insert tasks to activeTasks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added TaskQueueConcurrencyTest.

final List<Task> tasks;
if (taskQueue.isPresent()) {
tasks = taskQueue.get().getActiveTasksForDatasource(dataSource);
return taskQueue.get().getActiveTasksForDatasource(dataSource);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can probably remove line 4296 and 4298 and fold them inside the else at 4301. The resulting method would be much leaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Simplified.

@maytasm

maytasm commented May 7, 2025

Copy link
Copy Markdown
Contributor

@kfaraz
Is there anything blocking for this PR to be merged? I saw @gianm approval on the PR and wondering what is the status on the PR? Thanks!

@kfaraz

kfaraz commented May 8, 2025

Copy link
Copy Markdown
Contributor Author

@maytasm , this PR needs some more tests. I will add them soon and get the PR merged.

@cryptoe
cryptoe merged commit e2fbb47 into apache:master May 19, 2025
@capistrant capistrant added this to the 34.0.0 milestone Jul 22, 2025
riovic918data added a commit to riovic918data/druid that referenced this pull request Jun 12, 2026
Description

The giant lock in TaskQueue is acquired when performing any update or read operation, such as:

    add a task
    remove a task
    read the list of tasks
    sync from storage

On large clusters with several concurrent tasks, this can become a bottleneck.

This patch attempts to improve the concurrency in TaskQueue by using a concurrent hash map instead.
Changes

    Use a ConcurrentHashMap to track active tasks. The giant lock was needed only to handle
    competing updates made to the same task ID. This can be handled by a ConcurrentHashMap too.
    Perform any update on the entry for a task ID within ConcurrentHashMap.compute to ensure atomicity
    Convert the giant lock from a ReentrantLock to a ReentrantReadWriteLock
    Repurpose giant lock to ensure that the TaskQueue start and stop is mutually exclusive from any other
    normal operation on the TaskQueue
    Keep a lastUpdatedTime in every TaskEntry to handle race conditions when syncing from metadata store

Handling race conditions

The only possible race conditions are with syncFromStorage(),
and are handled by maintaining a lastUpdatedTime for every task entry.

All updates to a task entry happen inside a ConcurrentHashMap.compute(taskId, entry -> update(entry))
and are thus thread-safe.
[A] Sync should not add a task to queue if it has just been removed

This is handled by ensuring that only the syncFromStorage() method can remove tasks from the queue.
A task that completed after the poll started would have a lastUpdatedTime after syncStartTime.
In this case, since an entry already exists in the queue, a new one will not be added.
The next invocation of syncFromStorage() will add the task, if necessary.
[B] Sync should not remove a task from queue if it has just been added

A task added after the poll started would have a lastUpdatedTime after syncStartTime.
In this case, the entry will not be removed.
The next invocation of syncFromStorage() will clean up the task, if necessary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants