-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract batch executor out of cluster service (#24102)
Refactoring that extracts the task batching functionality from ClusterService and makes it a reusable component that can be tested in isolation.
- Loading branch information
Showing
9 changed files
with
1,050 additions
and
604 deletions.
There are no files selected for viewing
237 changes: 66 additions & 171 deletions
237
core/src/main/java/org/elasticsearch/cluster/service/ClusterService.java
Large diffs are not rendered by default.
Oops, something went wrong.
44 changes: 44 additions & 0 deletions
44
core/src/main/java/org/elasticsearch/cluster/service/SourcePrioritizedRunnable.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Licensed to Elasticsearch under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.elasticsearch.cluster.service; | ||
|
||
import org.elasticsearch.common.Priority; | ||
import org.elasticsearch.common.util.concurrent.PrioritizedRunnable; | ||
|
||
/** | ||
* PrioritizedRunnable that also has a source string | ||
*/ | ||
public abstract class SourcePrioritizedRunnable extends PrioritizedRunnable { | ||
protected final String source; | ||
|
||
public SourcePrioritizedRunnable(Priority priority, String source) { | ||
super(priority); | ||
this.source = source; | ||
} | ||
|
||
public String source() { | ||
return source; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "[" + source + "]"; | ||
} | ||
} |
207 changes: 207 additions & 0 deletions
207
core/src/main/java/org/elasticsearch/cluster/service/TaskBatcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
/* | ||
* Licensed to Elasticsearch under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
package org.elasticsearch.cluster.service; | ||
|
||
import org.apache.logging.log4j.Logger; | ||
import org.elasticsearch.common.Nullable; | ||
import org.elasticsearch.common.Priority; | ||
import org.elasticsearch.common.unit.TimeValue; | ||
import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; | ||
import org.elasticsearch.common.util.concurrent.PrioritizedEsThreadPoolExecutor; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.IdentityHashMap; | ||
import java.util.LinkedHashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* Batching support for {@link PrioritizedEsThreadPoolExecutor} | ||
* Tasks that share the same batching key are batched (see {@link BatchedTask#batchingKey}) | ||
*/ | ||
public abstract class TaskBatcher { | ||
|
||
private final Logger logger; | ||
private final PrioritizedEsThreadPoolExecutor threadExecutor; | ||
// package visible for tests | ||
final Map<Object, LinkedHashSet<BatchedTask>> tasksPerBatchingKey = new HashMap<>(); | ||
|
||
public TaskBatcher(Logger logger, PrioritizedEsThreadPoolExecutor threadExecutor) { | ||
this.logger = logger; | ||
this.threadExecutor = threadExecutor; | ||
} | ||
|
||
public void submitTasks(List<? extends BatchedTask> tasks, @Nullable TimeValue timeout) throws EsRejectedExecutionException { | ||
if (tasks.isEmpty()) { | ||
return; | ||
} | ||
final BatchedTask firstTask = tasks.get(0); | ||
assert tasks.stream().allMatch(t -> t.batchingKey == firstTask.batchingKey) : | ||
"tasks submitted in a batch should share the same batching key: " + tasks; | ||
// convert to an identity map to check for dups based on task identity | ||
final Map<Object, BatchedTask> tasksIdentity = tasks.stream().collect(Collectors.toMap( | ||
BatchedTask::getTask, | ||
Function.identity(), | ||
(a, b) -> { throw new IllegalStateException("cannot add duplicate task: " + a); }, | ||
IdentityHashMap::new)); | ||
|
||
synchronized (tasksPerBatchingKey) { | ||
LinkedHashSet<BatchedTask> existingTasks = tasksPerBatchingKey.computeIfAbsent(firstTask.batchingKey, | ||
k -> new LinkedHashSet<>(tasks.size())); | ||
for (BatchedTask existing : existingTasks) { | ||
// check that there won't be two tasks with the same identity for the same batching key | ||
BatchedTask duplicateTask = tasksIdentity.get(existing.getTask()); | ||
if (duplicateTask != null) { | ||
throw new IllegalStateException("task [" + duplicateTask.describeTasks( | ||
Collections.singletonList(existing)) + "] with source [" + duplicateTask.source + "] is already queued"); | ||
} | ||
} | ||
existingTasks.addAll(tasks); | ||
} | ||
|
||
if (timeout != null) { | ||
threadExecutor.execute(firstTask, timeout, () -> onTimeoutInternal(tasks, timeout)); | ||
} else { | ||
threadExecutor.execute(firstTask); | ||
} | ||
} | ||
|
||
private void onTimeoutInternal(List<? extends BatchedTask> tasks, TimeValue timeout) { | ||
final ArrayList<BatchedTask> toRemove = new ArrayList<>(); | ||
for (BatchedTask task : tasks) { | ||
if (task.processed.getAndSet(true) == false) { | ||
logger.debug("task [{}] timed out after [{}]", task.source, timeout); | ||
toRemove.add(task); | ||
} | ||
} | ||
if (toRemove.isEmpty() == false) { | ||
BatchedTask firstTask = toRemove.get(0); | ||
Object batchingKey = firstTask.batchingKey; | ||
assert tasks.stream().allMatch(t -> t.batchingKey == batchingKey) : | ||
"tasks submitted in a batch should share the same batching key: " + tasks; | ||
synchronized (tasksPerBatchingKey) { | ||
LinkedHashSet<BatchedTask> existingTasks = tasksPerBatchingKey.get(batchingKey); | ||
if (existingTasks != null) { | ||
existingTasks.removeAll(toRemove); | ||
if (existingTasks.isEmpty()) { | ||
tasksPerBatchingKey.remove(batchingKey); | ||
} | ||
} | ||
} | ||
onTimeout(toRemove, timeout); | ||
} | ||
} | ||
|
||
/** | ||
* Action to be implemented by the specific batching implementation. | ||
* All tasks have the same batching key. | ||
*/ | ||
protected abstract void onTimeout(List<? extends BatchedTask> tasks, TimeValue timeout); | ||
|
||
void runIfNotProcessed(BatchedTask updateTask) { | ||
// if this task is already processed, it shouldn't execute other tasks with same batching key that arrived later, | ||
// to give other tasks with different batching key a chance to execute. | ||
if (updateTask.processed.get() == false) { | ||
final List<BatchedTask> toExecute = new ArrayList<>(); | ||
final Map<String, List<BatchedTask>> processTasksBySource = new HashMap<>(); | ||
synchronized (tasksPerBatchingKey) { | ||
LinkedHashSet<BatchedTask> pending = tasksPerBatchingKey.remove(updateTask.batchingKey); | ||
if (pending != null) { | ||
for (BatchedTask task : pending) { | ||
if (task.processed.getAndSet(true) == false) { | ||
logger.trace("will process {}", task); | ||
toExecute.add(task); | ||
processTasksBySource.computeIfAbsent(task.source, s -> new ArrayList<>()).add(task); | ||
} else { | ||
logger.trace("skipping {}, already processed", task); | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (toExecute.isEmpty() == false) { | ||
final String tasksSummary = processTasksBySource.entrySet().stream().map(entry -> { | ||
String tasks = updateTask.describeTasks(entry.getValue()); | ||
return tasks.isEmpty() ? entry.getKey() : entry.getKey() + "[" + tasks + "]"; | ||
}).reduce((s1, s2) -> s1 + ", " + s2).orElse(""); | ||
|
||
run(updateTask.batchingKey, toExecute, tasksSummary); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Action to be implemented by the specific batching implementation | ||
* All tasks have the given batching key. | ||
*/ | ||
protected abstract void run(Object batchingKey, List<? extends BatchedTask> tasks, String tasksSummary); | ||
|
||
/** | ||
* Represents a runnable task that supports batching. | ||
* Implementors of TaskBatcher can subclass this to add a payload to the task. | ||
*/ | ||
protected abstract class BatchedTask extends SourcePrioritizedRunnable { | ||
/** | ||
* whether the task has been processed already | ||
*/ | ||
protected final AtomicBoolean processed = new AtomicBoolean(); | ||
|
||
/** | ||
* the object that is used as batching key | ||
*/ | ||
protected final Object batchingKey; | ||
/** | ||
* the task object that is wrapped | ||
*/ | ||
protected final Object task; | ||
|
||
protected BatchedTask(Priority priority, String source, Object batchingKey, Object task) { | ||
super(priority, source); | ||
this.batchingKey = batchingKey; | ||
this.task = task; | ||
} | ||
|
||
@Override | ||
public void run() { | ||
runIfNotProcessed(this); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
String taskDescription = describeTasks(Collections.singletonList(this)); | ||
if (taskDescription.isEmpty()) { | ||
return "[" + source + "]"; | ||
} else { | ||
return "[" + source + "[" + taskDescription + "]]"; | ||
} | ||
} | ||
|
||
public abstract String describeTasks(List<? extends BatchedTask> tasks); | ||
|
||
public Object getTask() { | ||
return task; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.