Repository: apache/druid
Pull request #16889: Global limit for MSQ controller tasks implemented
Author: login: nozjkoitop
Labels: name: Area - Documentation
name: Area - Ingestion
Created: 2024-08-13T07:34:51Z | Merged: 2025-03-06T15:57:35Z

# Improvements

Implemented a way to limit on the number of query controller tasks running at the same time. This limit specifies what percentage or amount of task slots can be allocated to query controllers. If the limit is reached, the tasks would wait for resources instead of potentially blocking the execution of other tasks (and failing after a timeout).

### Rationale

 There is no mechanism in Druid to prevent the cluster from being overloaded with controller tasks. Currently, it could cause a significant slowdown in processing and may lead to temporary deadlock situations.

### Introduced new configuration options

* `druid.indexer.queue.controllerTaskSlotRatio` - optional value which defines the proportion of available task slots that can be allocated to msq controller tasks. This is a floating-point value between 0 and 1. Defaults to null.
* `druid.indexer.queue.maxControllerTaskSlots` - optional value which specifies the maximum number of task slots that can be allocated to controller tasks. This is an integer value that provides a hard limit on the number of task slots available for msq controller tasks. Defaults to null.

This PR has:

- [x] been self-reviewed.
- [x] added documentation for new or modified features or behaviors.
- [x] a release note entry in the PR description.
- [x] 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](https://github.com/apache/druid/blob/master/dev/license.md)
- [ ] added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
- [x] added unit tests or modified existing tests to cover new code paths, ensuring the threshold for [code coverage](https://github.com/apache/druid/blob/master/dev/code-review/code-coverage.md) is met.
- [ ] added integration tests.
- [x] been tested in a test Druid cluster.



Diff:
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index cd1b8dfc1f9e..790bcb1278bc 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -1277,6 +1277,7 @@ This evenly distributes work across your Middle Managers.
 |--------|-----------|-------|
 |`type`|`equalDistribution`|required; must be `equalDistribution`|
 |`affinityConfig`|[`AffinityConfig`](#affinityconfig) object|null (no affinity)|
+|`taskLimits`|[`TaskLimits`](#tasklimits) object|null (no limits)|
 
 ###### `equalDistributionWithCategorySpec`
 
@@ -1288,6 +1289,7 @@ This strategy doesn't work with `AutoScaler` since the behavior is undefined.
 |--------|-----------|-------|
 |`type`|`equalDistributionWithCategorySpec`|required; must be `equalDistributionWithCategorySpec`|
 |`workerCategorySpec`|[`WorkerCategorySpec`](#workercategoryspec) object|null (no worker category spec)|
+|`taskLimits`|[`TaskLimits`](#tasklimits) object|null (no limits)|
 
 The following example shows tasks of type `index_kafka` that default to running on Middle Managers of category `c1`, except for tasks that write to datasource `ds1`, which run on Middle Managers of category `c2`.
 
@@ -1323,6 +1325,7 @@ Middle Managers up to capacity simultaneously, rather than a single Middle Manag
 |--------|-----------|-------|
 |`type`| `fillCapacity`|required; must be `fillCapacity`|
 |`affinityConfig`| [`AffinityConfig`](#affinityconfig) object |null (no affinity)|
+|`taskLimits`|[`TaskLimits`](#tasklimits) object|null (no limits)|
 
 ###### `fillCapacityWithCategorySpec`
 
@@ -1334,6 +1337,7 @@ This strategy doesn't work with `AutoScaler` since the behavior is undefined.
 |--------|-----------|-------|
 |`type`|`fillCapacityWithCategorySpec`.|required; must be `fillCapacityWithCategorySpec`|
 |`workerCategorySpec`|[`WorkerCategorySpec`](#workercategoryspec) object|null (no worker category spec)|
+|`taskLimits`|[`TaskLimits`](#tasklimits) object|null (no limits)|
 
 <a name="javascript-worker-select-strategy"></a>
 
@@ -1383,6 +1387,16 @@ field. If not provided, the default is to not use it at all.
 |`categoryMap`|A JSON map object mapping a task type String name to a [CategoryConfig](#categoryconfig) object, by which you can specify category config for different task type.|`{}`|
 |`strong`|With weak workerCategorySpec (the default), tasks for a dataSource may be assigned to other Middle Managers if the Middle Managers specified in `categoryMap` are not able to run all pending tasks in the queue for that dataSource. With strong workerCategorySpec, tasks for a dataSource will only ever be assigned to their specified Middle Managers, and will wait in the pending queue if necessary.|false|
 
+###### `taskLimits`
+
+The `taskLimits` field can be used with the `equalDistribution`, `fillCapacity`, `equalDistributionWithCategorySpec` and `fillCapacityWithCategorySpec` strategies.
+If you don't provide it, it will default to not being used.
+
+|Property|Description|Default|
+|--------|-----------|-------|
+|`maxSlotCountByType`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots in total. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `maxSlotCountByType = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|`{}`|
+|`maxSlotRatioByType`|A map where each key is a task type (`String`), and the corresponding value is a `Double` which should be in the range [0, 1], representing the ratio of task slots that tasks of this type can occupy. This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `maxSlotRatioByType = {"index_parallel": 0.5, "query_controller": 0.25}`. In this example, parallel indexing tasks can occupy up to 50% of the total task slots, and query controllers can occupy up to 25% of the total task slots.|`{}`|
+
 ###### CategoryConfig
 
 |Property|Description|Default|
diff --git a/extensions-core/ec2-extensions/src/test/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfigTest.java b/extensions-core/ec2-extensions/src/test/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfigTest.java
index a47a79fa9bc1..f777bad42e83 100644
--- a/extensions-core/ec2-extensions/src/test/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfigTest.java
+++ b/extensions-core/ec2-extensions/src/test/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfigTest.java
@@ -45,7 +45,8 @@ public void testSerde() throws Exception
             new AffinityConfig(
                 ImmutableMap.of("foo", ImmutableSet.of("localhost")),
                 false
-            )
+            ),
+            null
         ),
         new EC2AutoScaler(
             7,
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
index b1eafe0dc59d..d0a938e7022a 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
@@ -23,6 +23,7 @@
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.ImmutableSet;
+import org.apache.druid.common.config.Configs;
 import org.apache.druid.guice.annotations.PublicApi;
 import org.apache.druid.indexing.common.task.Task;
 import org.apache.druid.indexing.common.task.TaskResource;
@@ -33,6 +34,8 @@
 
 import javax.annotation.Nullable;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -45,6 +48,7 @@ public class ImmutableWorkerInfo
   private final Worker worker;
   private final int currCapacityUsed;
   private final int currParallelIndexCapacityUsed;
+  private final Map<String, Integer> currCapacityUsedByTaskType;
   private final ImmutableSet<String> availabilityGroups;
   private final ImmutableSet<String> runningTasks;
   private final DateTime lastCompletedTaskTime;
@@ -57,6 +61,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currCapacityUsedByTaskType") Map<String, Integer> currCapacityUsedByTaskType,
       @JsonProperty("availabilityGroups") Set<String> availabilityGroups,
       @JsonProperty("runningTasks") Collection<String> runningTasks,
       @JsonProperty("lastCompletedTaskTime") DateTime lastCompletedTaskTime,
@@ -66,6 +71,7 @@ public ImmutableWorkerInfo(
     this.worker = worker;
     this.currCapacityUsed = currCapacityUsed;
     this.currParallelIndexCapacityUsed = currParallelIndexCapacityUsed;
+    this.currCapacityUsedByTaskType = Configs.valueOrDefault(currCapacityUsedByTaskType, Collections.emptyMap());
     this.availabilityGroups = ImmutableSet.copyOf(availabilityGroups);
     this.runningTasks = ImmutableSet.copyOf(runningTasks);
     this.lastCompletedTaskTime = lastCompletedTaskTime;
@@ -76,12 +82,13 @@ public ImmutableWorkerInfo(
       Worker worker,
       int currCapacityUsed,
       int currParallelIndexCapacityUsed,
+      Map<String, Integer> currCapacityUsedByTaskType,
       Set<String> availabilityGroups,
       Collection<String> runningTasks,
       DateTime lastCompletedTaskTime
   )
   {
-    this(worker, currCapacityUsed, currParallelIndexCapacityUsed, availabilityGroups,
+    this(worker, currCapacityUsed, currParallelIndexCapacityUsed, currCapacityUsedByTaskType, availabilityGroups,
          runningTasks, lastCompletedTaskTime, null
     );
   }
@@ -94,7 +101,7 @@ public ImmutableWorkerInfo(
       DateTime lastCompletedTaskTime
   )
   {
-    this(worker, currCapacityUsed, 0, availabilityGroups, runningTasks, lastCompletedTaskTime, null);
+    this(worker, currCapacityUsed, 0, Collections.emptyMap(), availabilityGroups, runningTasks, lastCompletedTaskTime, null);
   }
 
   /**
@@ -111,6 +118,8 @@ public static ImmutableWorkerInfo fromWorkerAnnouncements(
     int currParallelIndexCapacityUsed = 0;
     ImmutableSet.Builder<String> taskIds = ImmutableSet.builder();
     ImmutableSet.Builder<String> availabilityGroups = ImmutableSet.builder();
+    Map<String, Integer> currCapacityUsedByTaskType = new HashMap<>();
+
 
     for (final Map.Entry<String, TaskAnnouncement> entry : announcements.entrySet()) {
       final TaskAnnouncement announcement = entry.getValue();
@@ -126,6 +135,8 @@ public static ImmutableWorkerInfo fromWorkerAnnouncements(
           currParallelIndexCapacityUsed += requiredCapacity;
         }
 
+        currCapacityUsedByTaskType.merge(announcement.getTaskType(), 1, Integer::sum);
+
         taskIds.add(taskId);
         availabilityGroups.add(taskResource.getAvailabilityGroup());
       }
@@ -135,6 +146,7 @@ public static ImmutableWorkerInfo fromWorkerAnnouncements(
         worker,
         currCapacityUsed,
         currParallelIndexCapacityUsed,
+        currCapacityUsedByTaskType,
         availabilityGroups.build(),
         taskIds.build(),
         lastCompletedTaskTime,
@@ -160,6 +172,13 @@ public int getCurrParallelIndexCapacityUsed()
     return currParallelIndexCapacityUsed;
   }
 
+  @JsonProperty("currCapacityUsedByTaskType")
+  @JsonInclude(JsonInclude.Include.NON_EMPTY)
+  public Map<String, Integer> getCurrCapacityUsedByTaskType()
+  {
+    return currCapacityUsedByTaskType;
+  }
+
   @JsonProperty("availabilityGroups")
   public Set<String> getAvailabilityGroups()
   {
@@ -243,6 +262,9 @@ public boolean equals(Object o)
     if (currParallelIndexCapacityUsed != that.currParallelIndexCapacityUsed) {
       return false;
     }
+    if (!currCapacityUsedByTaskType.equals(that.currCapacityUsedByTaskType)) {
+      return false;
+    }
     if (!worker.equals(that.worker)) {
       return false;
     }
@@ -266,6 +288,7 @@ public int hashCode()
     int result = worker.hashCode();
     result = 31 * result + currCapacityUsed;
     result = 31 * result + currParallelIndexCapacityUsed;
+    result = 31 * result + currCapacityUsedByTaskType.hashCode();
     result = 31 * result + availabilityGroups.hashCode();
     result = 31 * result + runningTasks.hashCode();
     result = 31 * result + lastCompletedTaskTime.hashCode();
@@ -280,6 +303,7 @@ public String toString()
            "worker=" + worker +
            ", currCapacityUsed=" + currCapacityUsed +
            ", currParallelIndexCapacityUsed=" + currParallelIndexCapacityUsed +
+           ", currCapacityUsedByTaskType=" + currCapacityUsedByTaskType +
            ", availabilityGroups=" + availabilityGroups +
            ", runningTasks=" + runningTasks +
            ", lastCompletedTaskTime=" + lastCompletedTaskTime +
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedWorkerProvisioningStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedWorkerProvisioningStrategy.java
index 0d715b53549f..101226788936 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedWorkerProvisioningStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedWorkerProvisioningStrategy.java
@@ -482,10 +482,16 @@ private static ImmutableWorkerInfo workerWithTask(ImmutableWorkerInfo immutableW
     int parallelIndexTaskCapacity = task.getType().equals(ParallelIndexSupervisorTask.TYPE)
                                     ? task.getTaskResource().getRequiredCapacity()
                                     : 0;
+    int taskCapacity = task.getTaskResource().getRequiredCapacity();
+
+    final Map<String, Integer> typeSpecificCapacity = new HashMap<>(immutableWorker.getCurrCapacityUsedByTaskType());
+    typeSpecificCapacity.merge(task.getType(), taskCapacity, Integer::sum);
+
     return new ImmutableWorkerInfo(
         immutableWorker.getWorker(),
         immutableWorker.getCurrCapacityUsed() + 1,
         immutableWorker.getCurrParallelIndexCapacityUsed() + parallelIndexTaskCapacity,
+        typeSpecificCapacity,
         Sets.union(
             immutableWorker.getAvailabilityGroups(),
             Sets.newHashSet(
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategy.java
index 6d28ae78fd04..e60aeae018a7 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategy.java
@@ -29,9 +29,10 @@ public class EqualDistributionWithAffinityWorkerSelectStrategy extends EqualDist
 {
   @JsonCreator
   public EqualDistributionWithAffinityWorkerSelectStrategy(
-      @JsonProperty("affinityConfig") AffinityConfig affinityConfig
+      @JsonProperty("affinityConfig") AffinityConfig affinityConfig,
+      @JsonProperty("taskLimits") TaskLimits taskLimits
   )
   {
-    super(affinityConfig);
+    super(affinityConfig, taskLimits);
   }
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategy.java
index ec65693ac3dd..b3715aec2859 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategy.java
@@ -22,6 +22,7 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.ImmutableMap;
+import org.apache.druid.common.config.Configs;
 import org.apache.druid.indexing.common.task.Task;
 import org.apache.druid.indexing.overlord.ImmutableWorkerInfo;
 import org.apache.druid.indexing.overlord.config.WorkerTaskRunnerConfig;
@@ -32,13 +33,16 @@
 public class EqualDistributionWithCategorySpecWorkerSelectStrategy implements WorkerSelectStrategy
 {
   private final WorkerCategorySpec workerCategorySpec;
+  private final TaskLimits taskLimits;
 
   @JsonCreator
   public EqualDistributionWithCategorySpecWorkerSelectStrategy(
-      @JsonProperty("workerCategorySpec") WorkerCategorySpec workerCategorySpec
+      @JsonProperty("workerCategorySpec") WorkerCategorySpec workerCategorySpec,
+      @JsonProperty("taskLimits") @Nullable TaskLimits taskLimits
   )
   {
     this.workerCategorySpec = workerCategorySpec;
+    this.taskLimits = Configs.valueOrDefault(taskLimits, TaskLimits.EMPTY);
   }
 
   @JsonProperty
@@ -47,6 +51,12 @@ public WorkerCategorySpec getWorkerCategorySpec()
     return workerCategorySpec;
   }
 
+  @JsonProperty
+  public TaskLimits getTaskLimits()
+  {
+    return taskLimits;
+  }
+
   @Nullable
   @Override
   public ImmutableWorkerInfo findWorkerForTask(
@@ -60,7 +70,8 @@ public ImmutableWorkerInfo findWorkerForTask(
         zkWorkers,
         config,
         workerCategorySpec,
-        EqualDistributionWorkerSelectStrategy::selectFromEligibleWorkers
+        EqualDistributionWorkerSelectStrategy::selectFromEligibleWorkers,
+        taskLimits
     );
   }
 
@@ -74,13 +85,14 @@ public boolean equals(final Object o)
       return false;
     }
     final EqualDistributionWithCategorySpecWorkerSelectStrategy that = (EqualDistributionWithCategorySpecWorkerSelectStrategy) o;
-    return Objects.equals(workerCategorySpec, that.workerCategorySpec);
+    return Objects.equals(workerCategorySpec, that.workerCategorySpec)
+           && Objects.equals(taskLimits, that.taskLimits);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(workerCategorySpec);
+    return Objects.hash(workerCategorySpec, taskLimits);
   }
 
   @Override
@@ -88,6 +100,7 @@ public String toString()
   {
     return "EqualDistributionWithCategorySpecWorkerSelectStrategy{" +
            "workerCategorySpec=" + workerCategorySpec +
+           ", taskLimits=" + taskLimits +
            '}';
   }
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategy.java
index afb020e9f7ab..f3775d91e52c 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategy.java
@@ -22,10 +22,12 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.ImmutableMap;
+import org.apache.druid.common.config.Configs;
 import org.apache.druid.indexing.common.task.Task;
 import org.apache.druid.indexing.overlord.ImmutableWorkerInfo;
 import org.apache.druid.indexing.overlord.config.WorkerTaskRunnerConfig;
 
+import javax.annotation.Nullable;
 import java.util.Comparator;
 import java.util.Map;
 import java.util.Objects;
@@ -35,13 +37,16 @@
 public class EqualDistributionWorkerSelectStrategy implements WorkerSelectStrategy
 {
   private final AffinityConfig affinityConfig;
+  private final TaskLimits taskLimits;
 
   @JsonCreator
   public EqualDistributionWorkerSelectStrategy(
-      @JsonProperty("affinityConfig") AffinityConfig affinityConfig
+      @JsonProperty("affinityConfig") AffinityConfig affinityConfig,
+      @JsonProperty("taskLimits") @Nullable TaskLimits taskLimits
   )
   {
     this.affinityConfig = affinityConfig;
+    this.taskLimits = Configs.valueOrDefault(taskLimits, TaskLimits.EMPTY);
   }
 
   @JsonProperty
@@ -50,6 +55,12 @@ public AffinityConfig getAffinityConfig()
     return affinityConfig;
   }
 
+  @JsonProperty
+  public TaskLimits getTaskLimits()
+  {
+    return taskLimits;
+  }
+
   @Override
   public ImmutableWorkerInfo findWorkerForTask(
       final WorkerTaskRunnerConfig config,
@@ -62,7 +73,8 @@ public ImmutableWorkerInfo findWorkerForTask(
         zkWorkers,
         config,
         affinityConfig,
-        EqualDistributionWorkerSelectStrategy::selectFromEligibleWorkers
+        EqualDistributionWorkerSelectStrategy::selectFromEligibleWorkers,
+        taskLimits
     );
   }
 
@@ -83,13 +95,14 @@ public boolean equals(final Object o)
       return false;
     }
     final EqualDistributionWorkerSelectStrategy that = (EqualDistributionWorkerSelectStrategy) o;
-    return Objects.equals(affinityConfig, that.affinityConfig);
+    return Objects.equals(affinityConfig, that.affinityConfig)
+           && Objects.equals(taskLimits, that.taskLimits);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(affinityConfig);
+    return Objects.hash(affinityConfig, taskLimits);
   }
 
   @Override
@@ -97,6 +110,7 @@ public String toString()
   {
     return "EqualDistributionWorkerSelectStrategy{" +
            "affinityConfig=" + affinityConfig +
+           ", taskLimits=" + taskLimits +
            '}';
   }
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategy.java
index 46c303f90481..efa24b75b5f2 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategy.java
@@ -29,9 +29,10 @@ public class FillCapacityWithAffinityWorkerSelectStrategy extends FillCapacityWo
 {
   @JsonCreator
   public FillCapacityWithAffinityWorkerSelectStrategy(
-      @JsonProperty("affinityConfig") AffinityConfig affinityConfig
+      @JsonProperty("affinityConfig") AffinityConfig affinityConfig,
+      @JsonProperty("taskLimits") TaskLimits taskLimits
   )
   {
-    super(affinityConfig);
+    super(affinityConfig, taskLimits);
   }
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategy.java
index 3dcdfe9a5401..d50b19824cf3 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategy.java
@@ -22,6 +22,7 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.ImmutableMap;
+import org.apache.druid.common.config.Configs;
 import org.apache.druid.indexing.common.task.Task;
 import org.apache.druid.indexing.overlord.ImmutableWorkerInfo;
 import org.apache.druid.indexing.overlord.config.WorkerTaskRunnerConfig;
@@ -32,13 +33,17 @@
 public class FillCapacityWithCategorySpecWorkerSelectStrategy implements WorkerSelectStrategy
 {
   private final WorkerCategorySpec workerCategorySpec;
+  private final TaskLimits taskLimits;
+
 
   @JsonCreator
   public FillCapacityWithCategorySpecWorkerSelectStrategy(
-      @JsonProperty("workerCategorySpec") WorkerCategorySpec workerCategorySpec
+      @JsonProperty("workerCategorySpec") WorkerCategorySpec workerCategorySpec,
+      @JsonProperty("taskLimits") @Nullable TaskLimits taskLimits
   )
   {
     this.workerCategorySpec = workerCategorySpec;
+    this.taskLimits = Configs.valueOrDefault(taskLimits, TaskLimits.EMPTY);
   }
 
   @JsonProperty
@@ -47,6 +52,12 @@ public WorkerCategorySpec getWorkerCategorySpec()
     return workerCategorySpec;
   }
 
+  @JsonProperty
+  public TaskLimits getTaskLimits()
+  {
+    return taskLimits;
+  }
+
   @Nullable
   @Override
   public ImmutableWorkerInfo findWorkerForTask(
@@ -60,7 +71,8 @@ public ImmutableWorkerInfo findWorkerForTask(
         zkWorkers,
         config,
         workerCategorySpec,
-        FillCapacityWorkerSelectStrategy::selectFromEligibleWorkers
+        FillCapacityWorkerSelectStrategy::selectFromEligibleWorkers,
+        taskLimits
     );
   }
 
@@ -74,13 +86,14 @@ public boolean equals(final Object o)
       return false;
     }
     final FillCapacityWithCategorySpecWorkerSelectStrategy that = (FillCapacityWithCategorySpecWorkerSelectStrategy) o;
-    return Objects.equals(workerCategorySpec, that.workerCategorySpec);
+    return Objects.equals(workerCategorySpec, that.workerCategorySpec)
+           && Objects.equals(taskLimits, that.taskLimits);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(workerCategorySpec);
+    return Objects.hash(workerCategorySpec, taskLimits);
   }
 
   @Override
@@ -88,6 +101,7 @@ public String toString()
   {
     return "FillCapacityWithCategorySpecWorkerSelectStrategy{" +
            "workerCategorySpec=" + workerCategorySpec +
+           ", taskLimits=" + taskLimits +
            '}';
   }
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWorkerSelectStrategy.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWorkerSelectStrategy.java
index 47ac65b8271d..bd8b53bacecd 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWorkerSelectStrategy.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWorkerSelectStrategy.java
@@ -22,10 +22,12 @@
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.collect.ImmutableMap;
+import org.apache.druid.common.config.Configs;
 import org.apache.druid.indexing.common.task.Task;
 import org.apache.druid.indexing.overlord.ImmutableWorkerInfo;
 import org.apache.druid.indexing.overlord.config.WorkerTaskRunnerConfig;
 
+import javax.annotation.Nullable;
 import java.util.Comparator;
 import java.util.Map;
 import java.util.Objects;
@@ -33,13 +35,16 @@
 public class FillCapacityWorkerSelectStrategy implements WorkerSelectStrategy
 {
   private final AffinityConfig affinityConfig;
+  private final TaskLimits taskLimits;
 
   @JsonCreator
   public FillCapacityWorkerSelectStrategy(
-      @JsonProperty("affinityConfig") AffinityConfig affinityConfig
+      @JsonProperty("affinityConfig") AffinityConfig affinityConfig,
+      @JsonProperty("taskLimits") @Nullable TaskLimits taskLimits
   )
   {
     this.affinityConfig = affinityConfig;
+    this.taskLimits = Configs.valueOrDefault(taskLimits, TaskLimits.EMPTY);
   }
 
   @JsonProperty
@@ -48,6 +53,12 @@ public AffinityConfig getAffinityConfig()
     return affinityConfig;
   }
 
+  @JsonProperty
+  public TaskLimits getTaskLimits()
+  {
+    return taskLimits;
+  }
+
   @Override
   public ImmutableWorkerInfo findWorkerForTask(
       final WorkerTaskRunnerConfig config,
@@ -60,7 +71,8 @@ public ImmutableWorkerInfo findWorkerForTask(
         zkWorkers,
         config,
         affinityConfig,
-        FillCapacityWorkerSelectStrategy::selectFromEligibleWorkers
+        FillCapacityWorkerSelectStrategy::selectFromEligibleWorkers,
+        taskLimits
     );
   }
 
@@ -81,13 +93,14 @@ public boolean equals(final Object o)
       return false;
     }
     final FillCapacityWorkerSelectStrategy that = (FillCapacityWorkerSelectStrategy) o;
-    return Objects.equals(affinityConfig, that.affinityConfig);
+    return Objects.equals(affinityConfig, that.affinityConfig)
+           && Objects.equals(taskLimits, that.taskLimits);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(affinityConfig);
+    return Objects.hash(affinityConfig, taskLimits);
   }
 
   @Override
@@ -95,6 +108,7 @@ public String toString()
   {
     return "FillCapacityWorkerSelectStrategy{" +
            "affinityConfig=" + affinityConfig +
+           ", taskLimits=" + taskLimits +
            '}';
   }
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
new file mode 100644
index 000000000000..39e6333c62f5
--- /dev/null
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.common.config.Configs;
+import org.apache.druid.error.InvalidInput;
+import org.apache.druid.indexing.common.task.Task;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Defines global limits for task execution using absolute slot counts and proportional ratios.
+ *
+ * <p>Task count limits ({@code maxSlotCountByType}) define the maximum number of slots per task type.
+ * Task ratios ({@code maxSlotRatioByType}) define the proportion of total slots a task type can use.
+ * If both are set for a task type, the lower limit applies.</p>
+ *
+ * <p>Example:
+ * {@code maxSlotCountByType = {"index_parallel": 3, "query_controller": 5}}
+ * {@code maxSlotRatioByType = {"index_parallel": 0.5, "query_controller": 0.25}}</p>
+ */
+public class TaskLimits
+{
+  public static final TaskLimits EMPTY = new TaskLimits();
+  private final Map<String, Integer> maxSlotCountByType;
+  private final Map<String, Double> maxSlotRatioByType;
+
+  private TaskLimits()
+  {
+    this(Map.of(), Map.of());
+  }
+
+  @JsonCreator
+  public TaskLimits(
+      @JsonProperty("maxSlotCountByType") @Nullable Map<String, Integer> maxSlotCountByType,
+      @JsonProperty("maxSlotRatioByType") @Nullable Map<String, Double> maxSlotRatioByType
+  )
+  {
+    validateLimits(maxSlotCountByType, maxSlotRatioByType);
+    this.maxSlotCountByType = Configs.valueOrDefault(maxSlotCountByType, Collections.emptyMap());
+    this.maxSlotRatioByType = Configs.valueOrDefault(maxSlotRatioByType, Collections.emptyMap());
+  }
+
+  /**
+   * Determines whether the given task can be executed based on task limits and available capacity.
+   *
+   * @param task             The task to check.
+   * @param currentSlotsUsed The current capacity used by tasks of the same type.
+   * @param totalCapacity    The total available capacity across all workers.
+   * @return {@code true} if the task meets the defined limits and capacity constraints; {@code false} otherwise.
+   */
+  public boolean canRunTask(Task task, Integer currentSlotsUsed, Integer totalCapacity)
+  {
+    if (maxSlotRatioByType.isEmpty() && maxSlotCountByType.isEmpty()) {
+      return true;
+    }
+    return meetsTaskLimit(
+        task,
+        currentSlotsUsed,
+        totalCapacity
+    );
+  }
+
+  private boolean meetsTaskLimit(
+      Task task,
+      Integer currentSlotsUsed,
+      Integer totalCapacity
+  )
+  {
+    final Integer limit = getLimitForTask(task.getType(), totalCapacity);
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+
+    return hasCapacityBasedOnLimit(limit, currentSlotsUsed, requiredCapacity);
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Integer totalCapacity
+  )
+  {
+    Integer absoluteLimit = maxSlotCountByType.get(taskType);
+    Double ratioLimit = maxSlotRatioByType.get(taskType);
+
+    if (absoluteLimit == null && ratioLimit == null) {
+      return null;
+    }
+
+    if (ratioLimit != null) {
+      int ratioBasedLimit = calculateTaskCapacityFromRatio(ratioLimit, totalCapacity);
+      if (absoluteLimit != null) {
+        return Math.min(absoluteLimit, ratioBasedLimit);
+      } else {
+        return ratioBasedLimit;
+      }
+    } else {
+      return absoluteLimit;
+    }
+  }
+
+  private boolean hasCapacityBasedOnLimit(int limit, int currentCapacityUsed, int requiredCapacity)
+  {
+    return limit - currentCapacityUsed >= requiredCapacity;
+  }
+
+  private int calculateTaskCapacityFromRatio(double taskSlotRatio, int totalCapacity)
+  {
+    int workerParallelIndexCapacity = (int) Math.floor(taskSlotRatio * totalCapacity);
+    return Math.max(1, Math.min(workerParallelIndexCapacity, totalCapacity));
+  }
+
+  private void validateLimits(Map<String, Integer> taskCountLimits, Map<String, Double> taskRatios)
+  {
+    if (taskCountLimits != null && taskCountLimits.values().stream().anyMatch(val -> val < 0)) {
+      throw InvalidInput.exception(
+          "Max task slot count limit for any type must be greater than zero. Found[%s].",
+          taskCountLimits
+      );
+    } else if (taskRatios != null && taskRatios.values().stream().anyMatch(val -> val < 0 || val > 1)) {
+      throw InvalidInput.exception(
+          "Max task slot ratios should be in the interval of [0, 1]. Found[%s].",
+          taskCountLimits
+      );
+    }
+  }
+
+  @JsonProperty
+  public Map<String, Integer> getMaxSlotCountByType()
+  {
+    return maxSlotCountByType;
+  }
+
+  @JsonProperty
+  public Map<String, Double> getMaxSlotRatioByType()
+  {
+    return maxSlotRatioByType;
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    TaskLimits that = (TaskLimits) o;
+    return Objects.equals(maxSlotCountByType, that.maxSlotCountByType) && Objects.equals(
+        maxSlotRatioByType,
+        that.maxSlotRatioByType
+    );
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return Objects.hash(maxSlotCountByType, maxSlotRatioByType);
+  }
+
+  @Override
+  public String toString()
+  {
+    return "TaskLimits{" +
+           "maxSlotCountByType=" + maxSlotCountByType +
+           ", maxSlotRatioByType=" + maxSlotRatioByType +
+           '}';
+  }
+}
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfig.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfig.java
index ffc30f55e648..4b867a4bb10b 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfig.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerBehaviorConfig.java
@@ -40,7 +40,7 @@
 public interface WorkerBehaviorConfig
 {
   String CONFIG_KEY = "worker.config";
-  WorkerSelectStrategy DEFAULT_STRATEGY = new EqualDistributionWorkerSelectStrategy(null);
+  WorkerSelectStrategy DEFAULT_STRATEGY = new EqualDistributionWorkerSelectStrategy(null, null);
 
   WorkerSelectStrategy getSelectStrategy();
 }
diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerSelectUtils.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerSelectUtils.java
index c3832daae32f..be4e8426008b 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerSelectUtils.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerSelectUtils.java
@@ -26,6 +26,7 @@
 import org.apache.druid.indexing.overlord.config.WorkerTaskRunnerConfig;
 
 import javax.annotation.Nullable;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
@@ -54,10 +55,11 @@ public static ImmutableWorkerInfo selectWorker(
       final Map<String, ImmutableWorkerInfo> allWorkers,
       final WorkerTaskRunnerConfig workerTaskRunnerConfig,
       @Nullable final AffinityConfig affinityConfig,
-      final Function<ImmutableMap<String, ImmutableWorkerInfo>, ImmutableWorkerInfo> workerSelector
+      final Function<ImmutableMap<String, ImmutableWorkerInfo>, ImmutableWorkerInfo> workerSelector,
+      final TaskLimits taskLimits
   )
   {
-    final Map<String, ImmutableWorkerInfo> runnableWorkers = getRunnableWorkers(task, allWorkers, workerTaskRunnerConfig);
+    final Map<String, ImmutableWorkerInfo> runnableWorkers = getRunnableWorkers(task, allWorkers, workerTaskRunnerConfig, taskLimits);
 
     if (affinityConfig == null) {
       // All runnable workers are valid.
@@ -105,10 +107,11 @@ public static ImmutableWorkerInfo selectWorker(
       final Map<String, ImmutableWorkerInfo> allWorkers,
       final WorkerTaskRunnerConfig workerTaskRunnerConfig,
       @Nullable final WorkerCategorySpec workerCategorySpec,
-      final Function<ImmutableMap<String, ImmutableWorkerInfo>, ImmutableWorkerInfo> workerSelector
+      final Function<ImmutableMap<String, ImmutableWorkerInfo>, ImmutableWorkerInfo> workerSelector,
+      final TaskLimits taskLimits
   )
   {
-    final Map<String, ImmutableWorkerInfo> runnableWorkers = getRunnableWorkers(task, allWorkers, workerTaskRunnerConfig);
+    final Map<String, ImmutableWorkerInfo> runnableWorkers = getRunnableWorkers(task, allWorkers, workerTaskRunnerConfig, taskLimits);
 
     // select worker according to worker category spec
     if (workerCategorySpec != null) {
@@ -145,9 +148,17 @@ public static ImmutableWorkerInfo selectWorker(
   private static Map<String, ImmutableWorkerInfo> getRunnableWorkers(
       final Task task,
       final Map<String, ImmutableWorkerInfo> allWorkers,
-      final WorkerTaskRunnerConfig workerTaskRunnerConfig
+      final WorkerTaskRunnerConfig workerTaskRunnerConfig,
+      final TaskLimits taskLimits
   )
   {
+    if (!taskLimits.canRunTask(
+        task,
+        getTotalCapacityUsedByType(allWorkers, task.getType()),
+        getTotalCapacity(allWorkers)
+    )) {
+      return Collections.emptyMap();
+    }
     return allWorkers.values()
                      .stream()
                      .filter(worker -> worker.canRunTask(task, workerTaskRunnerConfig.getParallelIndexTaskSlotRatio())
@@ -193,4 +204,20 @@ private static ImmutableMap<String, ImmutableWorkerInfo> getNonAffinityWorkers(
         )
     );
   }
+
+  private static int getTotalCapacity(final Map<String, ImmutableWorkerInfo> allWorkers)
+  {
+    return allWorkers.values().stream().mapToInt(workerInfo -> workerInfo.getWorker().getCapacity()).sum();
+  }
+
+  private static int getTotalCapacityUsedByType(
+      final Map<String, ImmutableWorkerInfo> allWorkers,
+      final String taskType
+  )
+  {
+    return allWorkers.values()
+                     .stream()
+                     .mapToInt(workerInfo -> workerInfo.getCurrCapacityUsedByTaskType().getOrDefault(taskType, 0))
+                     .sum();
+  }
 }
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
index e373eb982a36..122e9892dc89 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
@@ -33,6 +33,8 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.Collections;
+
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -201,6 +203,7 @@ public void testEqualsAndSerde()
         ),
         3,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:01Z"),
@@ -211,6 +214,7 @@ public void testEqualsAndSerde()
         ),
         2,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z"),
@@ -225,6 +229,7 @@ public void test_canRunTask()
         new Worker("http", "testWorker2", "192.0.0.1", 10, "v1", WorkerConfig.DEFAULT_CATEGORY),
         6,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z")
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/RemoteTaskRunnerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/RemoteTaskRunnerTest.java
index 8207584f2136..7bbd6ef5e37e 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/RemoteTaskRunnerTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/RemoteTaskRunnerTest.java
@@ -632,7 +632,7 @@ public void testGetMaximumCapacity_noAutoScaler()
         new TestRemoteTaskRunnerConfig(TIMEOUT_PERIOD),
         new TestProvisioningStrategy<>(),
         httpClient,
-        new DefaultWorkerBehaviorConfig(new EqualDistributionWorkerSelectStrategy(null), null)
+        new DefaultWorkerBehaviorConfig(new EqualDistributionWorkerSelectStrategy(null, null), null)
     );
     Assert.assertEquals(-1, remoteTaskRunner.getMaximumCapacityWithAutoscale());
   }
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedProvisioningStrategyTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedProvisioningStrategyTest.java
index 8a1232b95714..eaba6f9e6f9a 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedProvisioningStrategyTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedProvisioningStrategyTest.java
@@ -92,7 +92,7 @@ public void setUp()
 
     workerConfig = new AtomicReference<>(
         new DefaultWorkerBehaviorConfig(
-            new FillCapacityWorkerSelectStrategy(null),
+            new FillCapacityWorkerSelectStrategy(null, null),
             autoScaler
         )
     );
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerTest.java
index 91b0778c9505..3e5c862d7a63 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerTest.java
@@ -1818,7 +1818,7 @@ public void testGetMaximumCapacity_noAutoScaler()
         TestHelper.makeJsonMapper(),
         new HttpRemoteTaskRunnerConfig(),
         EasyMock.createNiceMock(HttpClient.class),
-        DSuppliers.of(new AtomicReference<>(new DefaultWorkerBehaviorConfig(new EqualDistributionWorkerSelectStrategy(null), null))),
+        DSuppliers.of(new AtomicReference<>(new DefaultWorkerBehaviorConfig(new EqualDistributionWorkerSelectStrategy(null, null), null))),
         new TestProvisioningStrategy<>(),
         druidNodeDiscoveryProvider,
         EasyMock.createMock(TaskStorage.class),
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategyTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategyTest.java
index 0641d3a8d919..1e81be9cc978 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategyTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategyTest.java
@@ -32,7 +32,10 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 public class EqualDistributionWithAffinityWorkerSelectStrategyTest
 {
@@ -40,7 +43,8 @@ public class EqualDistributionWithAffinityWorkerSelectStrategyTest
   public void testFindWorkerForTask()
   {
     EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
-        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost1", "localhost2", "localhost3")), false)
+        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost1", "localhost2", "localhost3")), false),
+        null
     );
 
     NoopTask noopTask = NoopTask.forDatasource("foo");
@@ -85,7 +89,8 @@ public void testFindWorkerForTask()
   public void testFindWorkerForTaskWithNulls()
   {
     EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
-            new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+            new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+            null
     );
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
@@ -115,7 +120,8 @@ public void testFindWorkerForTaskWithNulls()
   public void testIsolation()
   {
     EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
-            new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+            new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+            null
     );
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
@@ -139,7 +145,8 @@ public void testSerde() throws Exception
   {
     final ObjectMapper objectMapper = TestHelper.makeJsonMapper();
     final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
-        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+        null
     );
     final WorkerSelectStrategy strategy2 = objectMapper.readValue(
         objectMapper.writeValueAsBytes(strategy),
@@ -147,4 +154,138 @@ public void testSerde() throws Exception
     );
     Assert.assertEquals(strategy, strategy2);
   }
+
+  @Test
+  public void testFindWorkerForTaskWithGlobalLimits()
+  {
+    Map<String, Integer> taskLimits = new HashMap<>();
+    taskLimits.put("noop", 2);
+
+    Map<String, Integer> capacityUsed = new HashMap<>();
+    capacityUsed.put("noop", 1);
+    EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
+        null,
+        new TaskLimits(taskLimits, null)
+    );
+
+    NoopTask noopTask = NoopTask.forDatasource("foo");
+    ImmutableWorkerInfo worker = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+    Assert.assertNotNull(worker);
+
+    ImmutableWorkerInfo worker1 = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+
+    Assert.assertNull(worker1);
+
+  }
+
+  @Test
+  public void testFindWorkerForTaskWithGlobalRatios()
+  {
+    Map<String, Double> taskRatios = new HashMap<>();
+    taskRatios.put("noop", 0.5);
+
+    Map<String, Integer> capacityUsed = new HashMap<>();
+    capacityUsed.put("noop", 1);
+    EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
+        null,
+        new TaskLimits(null, taskRatios)
+    );
+
+    NoopTask noopTask = NoopTask.forDatasource("foo");
+    ImmutableWorkerInfo worker = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+    Assert.assertNotNull(worker);
+
+    ImmutableWorkerInfo worker1 = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                Set.of(),
+                Set.of(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+
+    Assert.assertNull(worker1);
+
+  }
 }
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategyTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategyTest.java
index 99a690a738df..6ac04a8920fc 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategyTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategyTest.java
@@ -176,8 +176,8 @@ public void testStrongTierSpec()
 
   private ImmutableWorkerInfo selectWorker(WorkerCategorySpec workerCategorySpec)
   {
-    final EqualDistributionWithCategorySpecWorkerSelectStrategy strategy = new EqualDistributionWithCategorySpecWorkerSelectStrategy(
-        workerCategorySpec);
+    final EqualDistributionWithCategorySpecWorkerSelectStrategy strategy =
+        new EqualDistributionWithCategorySpecWorkerSelectStrategy(workerCategorySpec, null);
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
         new RemoteTaskRunnerConfig(),
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategyTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategyTest.java
index dac25a3c9e34..582806416457 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategyTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategyTest.java
@@ -69,7 +69,7 @@ public class EqualDistributionWorkerSelectStrategyTest
   @Test
   public void testFindWorkerForTask()
   {
-    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null);
+    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null, null);
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
         new RemoteTaskRunnerConfig(),
@@ -97,7 +97,7 @@ public void testFindWorkerForTask()
   @Test
   public void testFindWorkerForTaskWhenSameCurrCapacityUsed()
   {
-    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null);
+    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null, null);
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
         new RemoteTaskRunnerConfig(),
@@ -126,7 +126,7 @@ public void testFindWorkerForTaskWhenSameCurrCapacityUsed()
   public void testOneDisableWorkerDifferentUsedCapacity()
   {
     String disabledVersion = "";
-    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null);
+    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null, null);
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
         new RemoteTaskRunnerConfig(),
@@ -155,7 +155,7 @@ public void testOneDisableWorkerDifferentUsedCapacity()
   public void testOneDisableWorkerSameUsedCapacity()
   {
     String disabledVersion = "";
-    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null);
+    final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWorkerSelectStrategy(null, null);
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
         new RemoteTaskRunnerConfig(),
@@ -190,7 +190,8 @@ public void testWeakAffinity()
                 "bar", ImmutableSet.of("nonexistent-worker")
             ),
             false
-        )
+        ),
+        null
     );
 
     ImmutableWorkerInfo workerFoo = strategy.findWorkerForTask(
@@ -226,7 +227,8 @@ public void testStrongAffinity()
                 "bar", ImmutableSet.of("nonexistent-worker")
             ),
             true
-        )
+        ),
+        null
     );
 
     ImmutableWorkerInfo workerFoo = strategy.findWorkerForTask(
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategyTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategyTest.java
index 93f6c39b0f0c..207c6e43dda3 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategyTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithAffinityWorkerSelectStrategyTest.java
@@ -30,7 +30,9 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 
 public class FillCapacityWithAffinityWorkerSelectStrategyTest
 {
@@ -38,7 +40,8 @@ public class FillCapacityWithAffinityWorkerSelectStrategyTest
   public void testFindWorkerForTask()
   {
     FillCapacityWorkerSelectStrategy strategy = new FillCapacityWithAffinityWorkerSelectStrategy(
-        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+        null
     );
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
@@ -68,7 +71,8 @@ public void testFindWorkerForTask()
   public void testFindWorkerForTaskWithNulls()
   {
     FillCapacityWorkerSelectStrategy strategy = new FillCapacityWithAffinityWorkerSelectStrategy(
-        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+        null
     );
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
@@ -98,7 +102,8 @@ public void testFindWorkerForTaskWithNulls()
   public void testIsolation()
   {
     FillCapacityWorkerSelectStrategy strategy = new FillCapacityWithAffinityWorkerSelectStrategy(
-        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+        null
     );
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
@@ -116,4 +121,138 @@ public void testIsolation()
     );
     Assert.assertNull(worker);
   }
+
+  @Test
+  public void testFindWorkerForTaskWithGlobalLimits()
+  {
+    Map<String, Integer> taskLimits = new HashMap<>();
+    taskLimits.put("noop", 2);
+
+    Map<String, Integer> capacityUsed = new HashMap<>();
+    capacityUsed.put("noop", 1);
+    FillCapacityWorkerSelectStrategy strategy = new FillCapacityWorkerSelectStrategy(
+        null,
+        new TaskLimits(taskLimits, null)
+    );
+
+    NoopTask noopTask = NoopTask.forDatasource("foo");
+    ImmutableWorkerInfo worker = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+    Assert.assertNotNull(worker);
+
+    ImmutableWorkerInfo worker1 = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+
+    Assert.assertNull(worker1);
+
+  }
+
+  @Test
+  public void testFindWorkerForTaskWithGlobalRatios()
+  {
+    Map<String, Double> taskRatios = new HashMap<>();
+    taskRatios.put("noop", 0.5);
+
+    Map<String, Integer> capacityUsed = new HashMap<>();
+    capacityUsed.put("noop", 1);
+    FillCapacityWorkerSelectStrategy strategy = new FillCapacityWorkerSelectStrategy(
+        null,
+        new TaskLimits(null, taskRatios)
+    );
+
+    NoopTask noopTask = NoopTask.forDatasource("foo");
+    ImmutableWorkerInfo worker = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+    Assert.assertNotNull(worker);
+
+    ImmutableWorkerInfo worker1 = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+
+    Assert.assertNull(worker1);
+
+  }
 }
diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategyTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategyTest.java
index bb4795d029e1..880ef743dca6 100644
--- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategyTest.java
+++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategyTest.java
@@ -176,8 +176,8 @@ public void testStrongTierSpec()
 
   private ImmutableWorkerInfo selectWorker(WorkerCategorySpec workerCategorySpec)
   {
-    final FillCapacityWithCategorySpecWorkerSelectStrategy strategy = new FillCapacityWithCategorySpecWorkerSelectStrategy(
-        workerCategorySpec);
+    final FillCapacityWithCategorySpecWorkerSelectStrategy strategy =
+        new FillCapacityWithCategorySpecWorkerSelectStrategy(workerCategorySpec, null);
 
     ImmutableWorkerInfo worker = strategy.findWorkerForTask(
         new RemoteTaskRunnerConfig(),


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2024-08-13T08:17:37Z
user: login: asdf2014
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-08-14T09:18:14Z
user: login: asdf2014
body: Remove useless tail
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-08-14T09:20:38Z
user: login: kfaraz
body: > Currently, it could cause a significant slowdown in processing and may lead to temporary deadlock situations.

@nozjkoitop , rather than proceeding with a limit, I think we should try to figure out what is causing the slowdown in processing and/or the deadlock. Can you elaborate on this?

Once we have done an analysis of exactly what goes wrong when we have too many controller tasks and we have decided to impose a limit, it should not be done through the `TaskQueue` as done in this PR. Instead, it should be similar to the implementation of `parallelIndexTaskSlotRatio` and the config should most likely live in `WorkerTaskRunnerConfig`.

https://github.com/apache/druid/blob/73ff9f9047d69d55bf880e629efeec07ce92e528/indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java#L37-L50
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-06T17:33:50Z
user: login: cryptoe
body: I think this is a nice feature to have. Left some comments. 
Thanks @nozjkoitop for taking this up. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-08T02:07:53Z
user: login: kfaraz
body: We need to ensure that we do not remove any of the existing properties or config fields.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-23T08:31:55Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-23T14:34:05Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-23T14:34:24Z
user: login: cryptoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-24T08:39:49Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-24T12:34:21Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-24T12:34:36Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-24T12:59:14Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-24T13:58:24Z
user: login: kfaraz
body: Thanks for the quick response, @nozjkoitop !

I haven't reviewed the actual logic in `canRunTaskBasedOnCustomLimit` yet as I feel it will get simplified once the other feedback is addressed.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-24T14:22:17Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:46:42Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:46:55Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:47:16Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:52:15Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:52:46Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:52:55Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:53:28Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:53:55Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:54:07Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:54:47Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:56:51Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-25T10:57:28Z
user: login: kfaraz
body: Left some more comments.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T04:33:45Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:27:01Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:27:35Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:28:00Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:28:09Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:28:21Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:28:39Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:28:52Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T11:29:18Z
user: login: kfaraz
body: Final comments, rest looks good.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T14:38:11Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T14:47:09Z
user: login: kfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-26T15:04:52Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:26:38Z
user: login: kfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:31:22Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:34:30Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:47:16Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:47:26Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:50:48Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:51:31Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:51:38Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:51:44Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:51:50Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:52:02Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:52:19Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:52:26Z
user: login: kfaraz
body: LGTM 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2024-09-27T07:59:54Z
user: login: kfaraz
body: Making `currCapacityUsedByTaskType` null safe, otherwise several methods would throw NPE.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-01T13:40:08Z
user: login: cryptoe
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-02T02:37:15Z
user: login: cryptoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-02T02:39:02Z
user: login: cryptoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-02T02:40:54Z
user: login: kfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-02T02:41:40Z
user: login: cryptoe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T12:48:41Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:43:39Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:54:42Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:54:59Z
user: login: cryptoe
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T05:03:50Z
user: login: kfaraz
body: Left some suggestions.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:53:43Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:55:26Z
user: login: kfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:57:53Z
user: login: kfaraz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T10:58:19Z
user: login: nozjkoitop
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:00:41Z
user: login: kfaraz
body: Thanks for the changes, @nozjkoitop !
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:50:39Z

Review comments:
diff_hunk: @@ -64,6 +74,15 @@
     this.restartDelay = defaultDuration(restartDelay, "PT30S");
     this.storageSyncRate = defaultDuration(storageSyncRate, "PT1M");
     this.maxTaskPayloadSize = maxTaskPayloadSize;
+    if (controllerTaskSlotRatio != null && maxControllerTaskSlots != null) {
+      throw new IllegalArgumentException(
+          "Only one controller task limit parameter should be specified, controllerTaskSlotRatio or maxControllerTaskSlots");
+    } else if (controllerTaskSlotRatio != null && controllerTaskSlotRatio > 1 && controllerTaskSlotRatio <= 0) {
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/TaskQueueConfig.java
user: login: github-advanced-security[bot]
body: ## Useless comparison test

Test is always false, because of [this condition](1).

[Show more details](https://github.com/apache/druid/security/code-scanning/7707)
created_at: 2024-08-13T08:17:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,325 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
+import org.apache.druid.indexer.RunnerTaskState;
+import org.apache.druid.indexer.TaskLocation;
+import org.apache.druid.indexer.TaskStatus;
+import org.apache.druid.indexing.common.task.NoopTask;
+import org.apache.druid.indexing.common.task.Task;
+import org.apache.druid.indexing.overlord.autoscaling.ScalingStats;
+import org.apache.druid.java.util.common.Pair;
+import org.apache.druid.java.util.common.concurrent.ScheduledExecutors;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.joda.time.Duration;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+public class SimpleTaskRunner implements TaskRunner
+{
+  private static final Logger log = new Logger(TestTaskRunner.class);
+  private static final Duration T_PENDING_TO_RUNNING = Duration.standardSeconds(2);
+  private static final Duration T_SHUTDOWN_ACK = Duration.millis(8);
+  private static final Duration T_SHUTDOWN_COMPLETE = Duration.standardSeconds(2);
+  @GuardedBy("knownTasks")
+  private final Map<String, TestTaskRunnerWorkItem> knownTasks = new HashMap<>();
+  private final ScheduledExecutorService exec = ScheduledExecutors.fixed(8, "TaskQueueScaleTest-%s");
+  private int capacity;
+
+  public SimpleTaskRunner(final int capacity)
+  {
+    this.capacity = capacity;
+  }
+
+  public SimpleTaskRunner()
+  {
+    capacity = -1;
+  }
+
+  static void threadSleep(Duration duration)
+  {
+    try {
+      Thread.sleep(duration.getMillis());
+    }
+    catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public void start()
+  {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ListenableFuture<TaskStatus> run(Task task)
+  {
+    // Production task runners generally do not take a long time to execute "run", but may take a long time to
+    // go from "running" to "pending".
+    synchronized (knownTasks) {
+      final TestTaskRunnerWorkItem item = knownTasks.computeIfAbsent(
+          task.getId(),
+          id -> new TestTaskRunnerWorkItem(
+              id,
+              task.getType()
+          )
+      );
+      exec.schedule(
+          () -> {
+            try {
+              synchronized (knownTasks) {
+                final TestTaskRunnerWorkItem item2 = knownTasks.get(task.getId());
+                if (item2.getState() == RunnerTaskState.PENDING) {
+                  knownTasks.put(task.getId(), item2.withState(RunnerTaskState.RUNNING));
+                }
+              }
+
+              exec.schedule(
+                  () -> {
+                    try {
+                      final TestTaskRunnerWorkItem item2;
+                      synchronized (knownTasks) {
+                        item2 = knownTasks.get(task.getId());
+                        knownTasks.put(task.getId(), item2.withState(RunnerTaskState.NONE));
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/SimpleTaskRunner.java
user: login: github-advanced-security[bot]
body: ## Dereferenced variable may be null

Variable [item2](1) may be null at this access as suggested by [this](2) null guard.

[Show more details](https://github.com/apache/druid/security/code-scanning/7708)
created_at: 2024-08-13T08:17:37Z
author_association: NONE
diff_hunk: @@ -1124,6 +1124,8 @@ These Overlord static configurations can be defined in the `overlord/runtime.pro
 |`druid.indexer.queue.restartDelay`|Sleep this long when Overlord queue management throws an exception before trying again.|`PT30S`|
 |`druid.indexer.queue.storageSyncRate`|Sync Overlord state this often with an underlying task persistence mechanism.|`PT1M`|
 |`druid.indexer.queue.maxTaskPayloadSize`|Maximum allowed size in bytes of a single task payload accepted by the Overlord.|none (allow all task payload sizes)|
+|`druid.indexer.queue.controllerTaskSlotRatio`|Optional value which defines the proportion of available task slots that can be allocated to `query_controller` tasks. This is a floating-point value between 0 and 1. |`null`|
path: docs/configuration/index.md
user: login: asdf2014
body: ```suggestion
|`druid.indexer.queue.controllerTaskSlotRatio`|Optional value which defines the proportion of available task slots that can be allocated to `query_controller` tasks. This is a floating-point value between 0 and 1.|`null`|
```
created_at: 2024-08-14T09:18:10Z
author_association: MEMBER
diff_hunk: @@ -440,7 +442,7 @@ private void manageInternalCritical(
             notifyStatus(task, taskStatus, taskStatus.getErrorMsg());
             continue;
           }
-          if (taskIsReady) {
+          if (taskIsReady && !isControllerTaskLimitReached(task.getType(), true)) {
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/TaskQueue.java
user: login: cryptoe
body: Should It just be limited to MSQ controllers or other job types as well. Maybe Take a json as a input ? where key is the taskType and the value is the limit/float. 

Also what is the user behavior  if the task is pending for launch due to limit. 
* Can we communicate why the task is not launching to the user.
* If the cluster is totally starved, does the controller get timed out eventually and removed from queue?

created_at: 2024-09-08T02:07:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1134,7 +1134,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |--------|-----------|-------|
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
-| `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
path: docs/configuration/index.md
user: login: kfaraz
body: We cannot change the existing property as there are already people using it.
Please add a new property that can take precedence over the existing property.
You can call it `taskTypeToSlotRatio` or simply `taskSlotRatios`.
created_at: 2024-09-23T08:16:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,7 +60,7 @@ public class ImmutableWorkerInfo
   public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
-      @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: Please do not remove existing fields.
created_at: 2024-09-23T08:16:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,32 +20,66 @@
 package org.apache.druid.indexing.overlord.config;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.apache.druid.indexing.overlord.util.CustomJobTypeLimitsDeserializer;
+
+import java.util.HashMap;
+import java.util.Map;
 
 public class WorkerTaskRunnerConfig
 {
   @JsonProperty
   private String minWorkerVersion = "0";
 
   @JsonProperty
-  private double parallelIndexTaskSlotRatio = 1;
+  @JsonDeserialize(using = CustomJobTypeLimitsDeserializer.class)
+  private Map<String, Number> customJobTypeLimits = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: Why do we need a custom deserializer?
Can't we just have a map from String to Double?
created_at: 2024-09-23T08:31:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,32 +20,66 @@
 package org.apache.druid.indexing.overlord.config;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.apache.druid.indexing.overlord.util.CustomJobTypeLimitsDeserializer;
+
+import java.util.HashMap;
+import java.util.Map;
 
 public class WorkerTaskRunnerConfig
 {
   @JsonProperty
   private String minWorkerVersion = "0";
 
   @JsonProperty
-  private double parallelIndexTaskSlotRatio = 1;
+  @JsonDeserialize(using = CustomJobTypeLimitsDeserializer.class)
+  private Map<String, Number> customJobTypeLimits = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: nozjkoitop
body: It was added mostly for validation as the idea is to have double / integer values to have an option to specify not only the ratio but also a limit
created_at: 2024-09-23T14:34:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,7 +60,7 @@ public class ImmutableWorkerInfo
   public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
-      @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Removal rollbacked
created_at: 2024-09-23T14:34:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +63,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currTypeSpecificCapacityUsed") Map<String, Integer> typeSpecificCapacityMap,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: cryptoe
body: This should be nullable no ?
created_at: 2024-09-24T08:33:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,89 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  public boolean canRunTask(Task task, Map<String, Number> taskLimits)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: cryptoe
body: Can you please add java docs for this method. 
created_at: 2024-09-24T08:33:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,6 +167,29 @@ private int getCurrParallelIndexCapacityUsed(Map<String, TaskAnnouncement> tasks
     return currParallelIndexCapacityUsed;
   }
 
+  @JsonProperty("currTypeSpecificCapacityUsed")
+  public Map<String, Integer> getCurrTypeSpecificCapacityUsed()
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ZkWorker.java
user: login: cryptoe
body: I thought we had deprecated Zk based runner in favour of http. 
@kfaraz Does this change still make sense ?
created_at: 2024-09-24T08:37:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy.|Empty map|
path: docs/configuration/index.md
user: login: cryptoe
body: Could you please provide an example as well ?
created_at: 2024-09-24T08:38:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy.|Empty map|
path: docs/configuration/index.md
user: login: cryptoe
body: How does this interact with compaction slots ?
created_at: 2024-09-24T08:39:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +63,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currTypeSpecificCapacityUsed") Map<String, Integer> typeSpecificCapacityMap,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: You're right, thanks
created_at: 2024-09-24T12:34:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,89 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  public boolean canRunTask(Task task, Map<String, Number> taskLimits)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Done

created_at: 2024-09-24T12:34:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy.|Empty map|
path: docs/configuration/index.md
user: login: nozjkoitop
body: Example added! Good catch on the compaction slots. I'll need to test that, but based on the code, it looks like compaction slots availability will be checked twice (in the duty, and during the worker selection) if related entry will be included in the `taskSlotLimits` map. If there's a conflict, some tasks might end up in a pending state for a while.
created_at: 2024-09-24T12:59:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. For example, a value of 0.5 means that tasks of this type can occupy up to 50% of the task slots on a worker. A value of 0 means that tasks of this type can occupy no slots. A value of 1.0 means no restriction, allowing tasks of this type to occupy all available slots. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots on a worker.  `taskSlotLimits = {"index_parallel": 0.5, "query_controller": 3}`. In this example 'index_parallel' tasks can occupy up to 50% of task slots and 'query_controller' can occupy up to 3 task slots |Empty map|
path: docs/configuration/index.md
user: login: kfaraz
body: I don't think it is intuitive to put absolute limits and ratios in the same property.
It would be better to have two separate properties.

Edit: Moreover, is this intended to be an absolute limit per worker or overall?
created_at: 2024-09-24T13:53:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
+   * <ul>
+   *   <li>Whether the worker has sufficient capacity to handle the task.</li>
+   *   <li>Whether the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
+   *   <li>Whether the availability group of the task is currently available.</li>
+   * </ul>
+   *
+   * @param task The {@link Task} to be executed. The task contains details such as required capacity
+   *             and its type.
+   * @param taskLimits A map containing custom limits for different task types. The key is a string
+   *                   representing the task type, and the value is a {@link Number} which can be:
+   *                   <ul>
+   *                     <li>A {@code Double} representing a ratio of available slots the task type can use (0 to 1).</li>
+   *                     <li>An {@code Integer} representing an absolute limit of slots the task type can occupy.</li>
+   *                   </ul>
+   *                   If the task type is not present in this map, the task can use all available slots.
+   * @return {@code true} if the task can run, meaning the worker has sufficient capacity,
+   *         the task type does not exceed custom limits, and the task's availability group is available.
+   *         Returns {@code false} otherwise.
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: this is not really needed, seems too verbose.
created_at: 2024-09-24T13:54:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,6 +34,10 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  @JsonDeserialize(using = TaskSlotLimitsDeserializer.class)
+  private Map<String, Number> taskSlotLimits = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: Let's not try to merge task slot ratios and absolute limits into one field. They are best kept separate.
created_at: 2024-09-24T13:56:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy.|Empty map|
path: docs/configuration/index.md
user: login: nozjkoitop
body: Confirmed that the number of submitted tasks is linked to the compaction task slot limits. However, execution might be delayed if the custom limit is smaller than the one set for compaction.
![image](https://github.com/user-attachments/assets/71a7f10e-b23f-4282-80bd-f07a794a13ab)
For example here,  `taskSlotsMax = 3`, but in the overlord configuration, I have `druid.indexer.runner.taskSlotLimits={"compact": 2}`.
created_at: 2024-09-24T13:58:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,9 +46,11 @@
 @PublicApi
 public class ImmutableWorkerInfo
 {
+  private static final Logger logger = new Logger(ImmutableWorkerInfo.class);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: We should not be doing any logging in this class.
created_at: 2024-09-24T13:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
   * This method returns true only if:
```
created_at: 2024-09-24T14:00:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
+   * <ul>
+   *   <li>Whether the worker has sufficient capacity to handle the task.</li>
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
   *   <li>The worker has sufficient capacity to handle the task.</li>
```
created_at: 2024-09-24T14:00:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
+   * <ul>
+   *   <li>Whether the worker has sufficient capacity to handle the task.</li>
+   *   <li>Whether the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
+   *   <li>Whether the availability group of the task is currently available.</li>
+   * </ul>
+   *
+   * @param task The {@link Task} to be executed. The task contains details such as required capacity
+   *             and its type.
+   * @param taskLimits A map containing custom limits for different task types. The key is a string
+   *                   representing the task type, and the value is a {@link Number} which can be:
+   *                   <ul>
+   *                     <li>A {@code Double} representing a ratio of available slots the task type can use (0 to 1).</li>
+   *                     <li>An {@code Integer} representing an absolute limit of slots the task type can occupy.</li>
+   *                   </ul>
+   *                   If the task type is not present in this map, the task can use all available slots.
+   * @return {@code true} if the task can run, meaning the worker has sufficient capacity,
+   *         the task type does not exceed custom limits, and the task's availability group is available.
+   *         Returns {@code false} otherwise.
+   */
+  public boolean canRunTask(Task task, Map<String, Number> taskLimits)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: We don't need to have two separate `canRunTask` methods. You can just pass `WorkerTaskRunnerConfig` instead of `parallelIndexTaskSlotRatio` into the existing `canRunTask` method.

Once we do that, some of the new private methods will go away, and simplify the change set.
created_at: 2024-09-24T14:04:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +399,24 @@ public boolean equals(Object o)
              : that.blacklistedUntil != null);
   }
 
+  public Map<String, Integer> incrementTypeSpecificCapacity(String type, int capacityToAdd)
+  {
+    Map<String, Integer> result = new HashMap<>(typeSpecificCapacityMap);
+    if (result.containsKey(type)) {
+      result.put(type, result.get(type) + capacityToAdd);
+    } else {
+      result.put(type, capacityToAdd);
+    }
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: maybe simplified with `result.merge()`.
created_at: 2024-09-24T14:08:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +63,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currTypeSpecificCapacityUsed") @Nullable Map<String, Integer> typeSpecificCapacityMap,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: Please use the same name for the argument, the JSON serialized field and the class member for readability.

Also, this name is a little ambiguous, use something else like `currCapacityUsedByTaskType`.
created_at: 2024-09-24T14:10:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,4 +57,46 @@ public double getParallelIndexTaskSlotRatio()
   {
     return parallelIndexTaskSlotRatio;
   }
+
+  /**
+   * The `taskSlotLimits` configuration is a map where each key is a task type,
+   * and the corresponding value represents the limit on the number of task slots
+   * that a task of that type can occupy on a worker.
+   * <p>
+   * The key is a `String` that specifies the task type.
+   * The value can either be a Double or Integer:
+   * <p>
+   * 1. A `Double` in the range [0, 1], representing a ratio of the available task slots
+   * that tasks of this type can occupy. For example, a value of 0.5 means that tasks
+   * of this type can occupy up to 50% of the task slots on a worker.
+   * A value of 0 means that tasks of this type can occupy no slots (i.e., they are effectively disabled).
+   * A value of 1.0 means no restriction, allowing tasks of this type to occupy all available slots.
+   * <p>
+   * 2. An `Integer` that is greater than or equal to 0, representing an absolute limit
+   * on the number of task slots that tasks of this type can occupy. For example, a value of 5
+   * means that tasks of this type can occupy up to 5 task slots on a worker.
+   * <p>
+   * If a task type is not present in the `taskSlotLimits` map, there is no restriction
+   * on the number of task slots it can occupy, meaning it can use all available slots.
+   * <p>
+   * Example:
+   * <p>
+   * taskSlotLimits = {
+   * "index_parallel": 0.5,  // 'index_parallel' can occupy up to 50% of task slots
+   * "query_controller": 3     // 'query_controller' can occupy up to 3 task slots
+   * }
+   * <p>
+   * This configuration allows for granular control over the allocation of task slots
+   * based on the specific needs of different task types, helping to prevent any one type
+   * of task from monopolizing worker resources and reducing the risk of deadlocks.
+   *
+   * @return A map where the key is the task type (`String`), and the value is either a `Double` (0 to 1)
+   * representing the ratio of task slots available for that type, or an `Integer` (>= 0)
+   * representing the absolute limit of task slots for that type. If a task type is absent,
+   * it is not limited in terms of the number of task slots it can occupy.
+   */
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: Please try to simplify and shorten this javadoc retaining only the necessary information in a concise manner.
created_at: 2024-09-24T14:11:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.util;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class TaskSlotLimitsDeserializer extends JsonDeserializer<Map<String, Number>>
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/util/TaskSlotLimitsDeserializer.java
user: login: kfaraz
body: This should not be needed once task slot ratio and task slot limits are separated.
created_at: 2024-09-24T14:11:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,6 +167,29 @@ private int getCurrParallelIndexCapacityUsed(Map<String, TaskAnnouncement> tasks
     return currParallelIndexCapacityUsed;
   }
 
+  @JsonProperty("currTypeSpecificCapacityUsed")
+  public Map<String, Integer> getCurrTypeSpecificCapacityUsed()
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ZkWorker.java
user: login: kfaraz
body: Yes, ZK-based task runner is deprecated and we should not support the new feature with ZK.
created_at: 2024-09-24T14:12:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -201,6 +211,7 @@ public void testEqualsAndSerde()
         ),
         3,
         0,
+        new HashMap<>(),
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: kfaraz
body: Use `Collections.emptyMap()` instead.
created_at: 2024-09-24T14:13:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy.|Empty map|
path: docs/configuration/index.md
user: login: kfaraz
body: I think the limit on compaction tasks (or kill tasks for that matter) should not be a concern.
This is a runtime property, typically controlled by an admin.
So, if an admin wants to restrict the number of concurrent compaction tasks, it is fair to honor that irrespective of the value of `compactionTaskSlotRatio` or `maxCompactionTaskSlots` set in the coordinator dynamic configs.

We just need to call it out clearly in the release notes and the docs of the new property.
created_at: 2024-09-24T14:19:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`| A map where each key is a task type, and the corresponding value represents the limit on the number of task slots that a task of that type can occupy on a worker. The key is a `String` that specifies the task type. The value can either be a Double or Integer. A `Double` in the range [0, 1], representing a ratio of the available task slots that tasks of this type can occupy. For example, a value of 0.5 means that tasks of this type can occupy up to 50% of the task slots on a worker. A value of 0 means that tasks of this type can occupy no slots. A value of 1.0 means no restriction, allowing tasks of this type to occupy all available slots. An `Integer` that is greater than or equal to 0, representing an absolute limit on the number of task slots that tasks of this type can occupy. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots on a worker.  `taskSlotLimits = {"index_parallel": 0.5, "query_controller": 3}`. In this example 'index_parallel' tasks can occupy up to 50% of task slots and 'query_controller' can occupy up to 3 task slots |Empty map|
path: docs/configuration/index.md
user: login: nozjkoitop
body: I've slit the properties into separate ones. It should be per worker limit I'd say
created_at: 2024-09-25T10:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
+   * <ul>
+   *   <li>Whether the worker has sufficient capacity to handle the task.</li>
+   *   <li>Whether the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
+   *   <li>Whether the availability group of the task is currently available.</li>
+   * </ul>
+   *
+   * @param task The {@link Task} to be executed. The task contains details such as required capacity
+   *             and its type.
+   * @param taskLimits A map containing custom limits for different task types. The key is a string
+   *                   representing the task type, and the value is a {@link Number} which can be:
+   *                   <ul>
+   *                     <li>A {@code Double} representing a ratio of available slots the task type can use (0 to 1).</li>
+   *                     <li>An {@code Integer} representing an absolute limit of slots the task type can occupy.</li>
+   *                   </ul>
+   *                   If the task type is not present in this map, the task can use all available slots.
+   * @return {@code true} if the task can run, meaning the worker has sufficient capacity,
+   *         the task type does not exceed custom limits, and the task's availability group is available.
+   *         Returns {@code false} otherwise.
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Removed
created_at: 2024-09-25T10:46:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,6 +34,10 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  @JsonDeserialize(using = TaskSlotLimitsDeserializer.class)
+  private Map<String, Number> taskSlotLimits = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: nozjkoitop
body: Separated
created_at: 2024-09-25T10:47:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,9 +46,11 @@
 @PublicApi
 public class ImmutableWorkerInfo
 {
+  private static final Logger logger = new Logger(ImmutableWorkerInfo.class);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Logger removed, but tbh i don't see the reason why it shouldn't be in this class as we're making the decisions here, should the canRunTask logic be moved into some utility class where we can log what's going on? 
created_at: 2024-09-25T10:52:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Updated
created_at: 2024-09-25T10:52:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
+   * <ul>
+   *   <li>Whether the worker has sufficient capacity to handle the task.</li>
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Done
created_at: 2024-09-25T10:52:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,114 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * This method checks:
+   * <ul>
+   *   <li>Whether the worker has sufficient capacity to handle the task.</li>
+   *   <li>Whether the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
+   *   <li>Whether the availability group of the task is currently available.</li>
+   * </ul>
+   *
+   * @param task The {@link Task} to be executed. The task contains details such as required capacity
+   *             and its type.
+   * @param taskLimits A map containing custom limits for different task types. The key is a string
+   *                   representing the task type, and the value is a {@link Number} which can be:
+   *                   <ul>
+   *                     <li>A {@code Double} representing a ratio of available slots the task type can use (0 to 1).</li>
+   *                     <li>An {@code Integer} representing an absolute limit of slots the task type can occupy.</li>
+   *                   </ul>
+   *                   If the task type is not present in this map, the task can use all available slots.
+   * @return {@code true} if the task can run, meaning the worker has sufficient capacity,
+   *         the task type does not exceed custom limits, and the task's availability group is available.
+   *         Returns {@code false} otherwise.
+   */
+  public boolean canRunTask(Task task, Map<String, Number> taskLimits)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Merged canRunTask methods
created_at: 2024-09-25T10:53:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +399,24 @@ public boolean equals(Object o)
              : that.blacklistedUntil != null);
   }
 
+  public Map<String, Integer> incrementTypeSpecificCapacity(String type, int capacityToAdd)
+  {
+    Map<String, Integer> result = new HashMap<>(typeSpecificCapacityMap);
+    if (result.containsKey(type)) {
+      result.put(type, result.get(type) + capacityToAdd);
+    } else {
+      result.put(type, capacityToAdd);
+    }
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Nice catch, thanks!
created_at: 2024-09-25T10:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +63,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currTypeSpecificCapacityUsed") @Nullable Map<String, Integer> typeSpecificCapacityMap,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Updated
created_at: 2024-09-25T10:54:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,4 +57,46 @@ public double getParallelIndexTaskSlotRatio()
   {
     return parallelIndexTaskSlotRatio;
   }
+
+  /**
+   * The `taskSlotLimits` configuration is a map where each key is a task type,
+   * and the corresponding value represents the limit on the number of task slots
+   * that a task of that type can occupy on a worker.
+   * <p>
+   * The key is a `String` that specifies the task type.
+   * The value can either be a Double or Integer:
+   * <p>
+   * 1. A `Double` in the range [0, 1], representing a ratio of the available task slots
+   * that tasks of this type can occupy. For example, a value of 0.5 means that tasks
+   * of this type can occupy up to 50% of the task slots on a worker.
+   * A value of 0 means that tasks of this type can occupy no slots (i.e., they are effectively disabled).
+   * A value of 1.0 means no restriction, allowing tasks of this type to occupy all available slots.
+   * <p>
+   * 2. An `Integer` that is greater than or equal to 0, representing an absolute limit
+   * on the number of task slots that tasks of this type can occupy. For example, a value of 5
+   * means that tasks of this type can occupy up to 5 task slots on a worker.
+   * <p>
+   * If a task type is not present in the `taskSlotLimits` map, there is no restriction
+   * on the number of task slots it can occupy, meaning it can use all available slots.
+   * <p>
+   * Example:
+   * <p>
+   * taskSlotLimits = {
+   * "index_parallel": 0.5,  // 'index_parallel' can occupy up to 50% of task slots
+   * "query_controller": 3     // 'query_controller' can occupy up to 3 task slots
+   * }
+   * <p>
+   * This configuration allows for granular control over the allocation of task slots
+   * based on the specific needs of different task types, helping to prevent any one type
+   * of task from monopolizing worker resources and reducing the risk of deadlocks.
+   *
+   * @return A map where the key is the task type (`String`), and the value is either a `Double` (0 to 1)
+   * representing the ratio of task slots available for that type, or an `Integer` (>= 0)
+   * representing the absolute limit of task slots for that type. If a task type is absent,
+   * it is not limited in terms of the number of task slots it can occupy.
+   */
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: nozjkoitop
body: Split that into two blocks, one for each getter and simplified
created_at: 2024-09-25T10:54:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.util;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class TaskSlotLimitsDeserializer extends JsonDeserializer<Map<String, Number>>
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/util/TaskSlotLimitsDeserializer.java
user: login: nozjkoitop
body: Removed
created_at: 2024-09-25T10:56:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -201,6 +211,7 @@ public void testEqualsAndSerde()
         ),
         3,
         0,
+        new HashMap<>(),
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: nozjkoitop
body: Updated
created_at: 2024-09-25T10:57:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,8 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy on a worker. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots on a worker. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskSlotLimits = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|Empty map|
path: docs/configuration/index.md
user: login: kfaraz
body: Taking another look at this, I don't see this property being very useful or intuitive.
If someone wants to limit the number of tasks of a given type to be 1 per worker, they can already use availability groups. I don't see why someone would want to limit the number of tasks of a certain type on a worker to be exactly, say 2.

I think the ratio is a more useful setting. So for the time being, we should just have `taskSlotRatiosPerWorker`. If, in the future, we feel the need of such a property, we can add it back.
created_at: 2024-09-26T02:50:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,8 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy on a worker. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots on a worker. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskSlotLimits = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|Empty map|
+|`druid.indexer.runner.taskSlotRatios`|A map where each key is a task type (`String`), and the corresponding value is a `Double` which should be in the range [0, 1], representing the ratio of available task slots that tasks of this type can occupy. This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskSlotRatios = {"index_parallel": 0.5, "query_controller": 0.25}`. In this example, parallel indexing tasks can occupy up to 50% of the total task slots, and query controllers can occupy up to 25% of the total task slots.|Empty map|
path: docs/configuration/index.md
user: login: kfaraz
body: ```suggestion
|`druid.indexer.runner.taskSlotRatiosPerWorker`|Map from task type to the ratio of task slots that the given task type can occupy on a single worker. Each value must be in the range [0, 1]. A value of 0 for a task type means that it can take up no task slot on a worker (i.e. tasks of this type would never run). A value of 1 means that the task type may take up all task slots of a worker if available. For `index_parallel` tasks, the `parallelIndexTaskSlotRatio` overrides the value specified in this map. |Empty map i.e. no limit on task slots for any task type|
```
created_at: 2024-09-26T02:58:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: These two methods are not really needed. We can just continue with the original code where these checks were performed inline in the method `canRunTask`.
created_at: 2024-09-26T04:19:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,4 +56,36 @@ public double getParallelIndexTaskSlotRatio()
   {
     return parallelIndexTaskSlotRatio;
   }
+
+  /**
+   * Returns a map where each key is a task type (`String`), and the value is an `Integer`
+   * representing the absolute limit on the number of task slots that tasks of this type can occupy.
+   * <p>
+   * This absolute limit specifies the maximum number of task slots available to a specific task type.
+   * <p>
+   * If both an absolute limit and a ratio (from {@link #getTaskSlotRatios()}) are specified for the same task type,
+   * the effective limit will be the smaller of the two.
+   *
+   * @return A map of task types with their corresponding absolute slot limits.
+   */
+  public Map<String, Integer> getTaskSlotLimits()
+  {
+    return taskSlotLimits;
+  }
+
+  /**
+   * Returns a map where each key is a task type (`String`), and the value is a `Double` which should be in the
+   * range [0, 1], representing the ratio of available task slots that tasks of this type can occupy.
+   * <p>
+   * This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`.
+   * <p>
+   * If both a ratio and an absolute limit (from {@link #getTaskSlotLimits()}) are specified for the same task type,
+   * the effective limit will be the smaller of the two.
+   *
+   * @return A map of task types with their corresponding slot ratios.
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: ```suggestion
   * Map from task type to task slot ratio for that type per worker.
   * A value of 0 for a task type indicates that the task type cannot occupy any slots.
   * A value of 1 indicates that the task type may take up all available slots of a worker if available. 
```
created_at: 2024-09-26T04:22:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +381,20 @@ public boolean equals(Object o)
              : that.blacklistedUntil != null);
   }
 
+  public Map<String, Integer> incrementTypeSpecificCapacity(String type, int capacityToAdd)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
  public Map<String, Integer> incrementCurrCapacityUsedByTaskType(String taskType, int capacityToAdd)
```
created_at: 2024-09-26T04:28:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,13 +222,31 @@ public boolean isValidVersion(String minVersion)
     return worker.getVersion().compareTo(minVersion) >= 0;
   }
 
-  public boolean canRunTask(Task task, double parallelIndexTaskSlotRatio)
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * returns true only if:
+   * <ul>
+   *   <li>The worker has sufficient capacity to handle the task.</li>
+   *   <li>The task is of the parallel index type and can operate within the permitted ratio of slots designated for that task type.</li>
+   *   <li>The the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
+   *   <li>The availability group of the task is currently available.</li>
+   * </ul>
+   */
+  public boolean canRunTask(Task task, WorkerTaskRunnerConfig config)
   {
-    return (worker.getCapacity() - getCurrCapacityUsed() >= task.getTaskResource().getRequiredCapacity()
-            && canRunParallelIndexTask(task, parallelIndexTaskSlotRatio)
-            && !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup()));
+    return (hasSufficientWorkerCapacity(task)
+            && canRunParallelIndexTask(task, config.getParallelIndexTaskSlotRatio())
+            && canRunTaskBasedOnCustomLimit(task, config.getTaskSlotLimits(), config.getTaskSlotRatios())
+            && isAvailabilityGroupAvailable(task));
   }
 
+
+  /**
+   * To be removed in future as it could be fully covered with the limits map.
+   */
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: I don't think this is needed, especially since we have not deprecated the old property yet.
created_at: 2024-09-26T04:31:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
+
+  private boolean canRunTaskBasedOnCustomLimit(Task task, Map<String, Integer> limitsMap, Map<String, Double> ratiosMap)
+  {
+    final Integer limit = getLimitForTask(task.getType(), limitsMap, ratiosMap);
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    int currentCapacityUsed = getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0);
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+
+    return hasCapacityBasedOnLimit(limit, currentCapacityUsed, requiredCapacity);
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Integer> limitsMap,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Integer absoluteLimit = limitsMap.get(taskType);
+    Double ratioLimit = ratiosMap.get(taskType);
+
+    if (absoluteLimit == null && ratioLimit == null) {
+      return null;
+    }
+
+    // Validate the absolute limit if present
+    if (absoluteLimit != null) {
+      Preconditions.checkArgument(absoluteLimit >= 0, "Absolute limit for task %s must be non-negative.", taskType);
+    }
+
+    // Validate the ratio limit if present
+    if (ratioLimit != null) {
+      Preconditions.checkArgument(ratioLimit >= 0.0 && ratioLimit <= 1.0,
+                                  "Ratio for task %s must be between 0.0 and 1.0 inclusive.", taskType
+      );
+    }
+
+    final int totalCapacity = worker.getCapacity();
+
+    Integer ratioBasedLimit = ratioLimit != null ? calculateTaskCapacityFromRatio(ratioLimit, totalCapacity) : null;
+
+    if (absoluteLimit != null && ratioBasedLimit != null) {
+      return Math.min(absoluteLimit, ratioBasedLimit);
+    }
+
+    return absoluteLimit != null ? absoluteLimit : ratioBasedLimit;
+  }
+
+  private boolean hasCapacityBasedOnLimit(int limit, int currentCapacityUsed, int requiredCapacity)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: This method is not needed, it can be inlined.
created_at: 2024-09-26T04:32:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
+
+  private boolean canRunTaskBasedOnCustomLimit(Task task, Map<String, Integer> limitsMap, Map<String, Double> ratiosMap)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: This method will become simpler once we remove the absolute limits.
created_at: 2024-09-26T04:33:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
+
+  private boolean canRunTaskBasedOnCustomLimit(Task task, Map<String, Integer> limitsMap, Map<String, Double> ratiosMap)
+  {
+    final Integer limit = getLimitForTask(task.getType(), limitsMap, ratiosMap);
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    int currentCapacityUsed = getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0);
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+
+    return hasCapacityBasedOnLimit(limit, currentCapacityUsed, requiredCapacity);
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Integer> limitsMap,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Integer absoluteLimit = limitsMap.get(taskType);
+    Double ratioLimit = ratiosMap.get(taskType);
+
+    if (absoluteLimit == null && ratioLimit == null) {
+      return null;
+    }
+
+    // Validate the absolute limit if present
+    if (absoluteLimit != null) {
+      Preconditions.checkArgument(absoluteLimit >= 0, "Absolute limit for task %s must be non-negative.", taskType);
+    }
+
+    // Validate the ratio limit if present
+    if (ratioLimit != null) {
+      Preconditions.checkArgument(ratioLimit >= 0.0 && ratioLimit <= 1.0,
+                                  "Ratio for task %s must be between 0.0 and 1.0 inclusive.", taskType
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: This validation should happen in `WorkerTaskRunnerConfig` itself.
created_at: 2024-09-26T04:33:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,8 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy on a worker. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots on a worker. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskSlotLimits = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|Empty map|
path: docs/configuration/index.md
user: login: nozjkoitop
body: Absolute limits removed
created_at: 2024-09-26T11:27:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,8 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotLimits`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy on a worker. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots on a worker. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskSlotLimits = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|Empty map|
+|`druid.indexer.runner.taskSlotRatios`|A map where each key is a task type (`String`), and the corresponding value is a `Double` which should be in the range [0, 1], representing the ratio of available task slots that tasks of this type can occupy. This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskSlotRatios = {"index_parallel": 0.5, "query_controller": 0.25}`. In this example, parallel indexing tasks can occupy up to 50% of the total task slots, and query controllers can occupy up to 25% of the total task slots.|Empty map|
path: docs/configuration/index.md
user: login: nozjkoitop
body: Added, thanks
created_at: 2024-09-26T11:27:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,4 +56,36 @@ public double getParallelIndexTaskSlotRatio()
   {
     return parallelIndexTaskSlotRatio;
   }
+
+  /**
+   * Returns a map where each key is a task type (`String`), and the value is an `Integer`
+   * representing the absolute limit on the number of task slots that tasks of this type can occupy.
+   * <p>
+   * This absolute limit specifies the maximum number of task slots available to a specific task type.
+   * <p>
+   * If both an absolute limit and a ratio (from {@link #getTaskSlotRatios()}) are specified for the same task type,
+   * the effective limit will be the smaller of the two.
+   *
+   * @return A map of task types with their corresponding absolute slot limits.
+   */
+  public Map<String, Integer> getTaskSlotLimits()
+  {
+    return taskSlotLimits;
+  }
+
+  /**
+   * Returns a map where each key is a task type (`String`), and the value is a `Double` which should be in the
+   * range [0, 1], representing the ratio of available task slots that tasks of this type can occupy.
+   * <p>
+   * This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`.
+   * <p>
+   * If both a ratio and an absolute limit (from {@link #getTaskSlotLimits()}) are specified for the same task type,
+   * the effective limit will be the smaller of the two.
+   *
+   * @return A map of task types with their corresponding slot ratios.
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: nozjkoitop
body: Added, thanks
created_at: 2024-09-26T11:28:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +381,20 @@ public boolean equals(Object o)
              : that.blacklistedUntil != null);
   }
 
+  public Map<String, Integer> incrementTypeSpecificCapacity(String type, int capacityToAdd)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Added
created_at: 2024-09-26T11:28:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,13 +222,31 @@ public boolean isValidVersion(String minVersion)
     return worker.getVersion().compareTo(minVersion) >= 0;
   }
 
-  public boolean canRunTask(Task task, double parallelIndexTaskSlotRatio)
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * returns true only if:
+   * <ul>
+   *   <li>The worker has sufficient capacity to handle the task.</li>
+   *   <li>The task is of the parallel index type and can operate within the permitted ratio of slots designated for that task type.</li>
+   *   <li>The the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
+   *   <li>The availability group of the task is currently available.</li>
+   * </ul>
+   */
+  public boolean canRunTask(Task task, WorkerTaskRunnerConfig config)
   {
-    return (worker.getCapacity() - getCurrCapacityUsed() >= task.getTaskResource().getRequiredCapacity()
-            && canRunParallelIndexTask(task, parallelIndexTaskSlotRatio)
-            && !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup()));
+    return (hasSufficientWorkerCapacity(task)
+            && canRunParallelIndexTask(task, config.getParallelIndexTaskSlotRatio())
+            && canRunTaskBasedOnCustomLimit(task, config.getTaskSlotLimits(), config.getTaskSlotRatios())
+            && isAvailabilityGroupAvailable(task));
   }
 
+
+  /**
+   * To be removed in future as it could be fully covered with the limits map.
+   */
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Removed
created_at: 2024-09-26T11:28:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
+
+  private boolean canRunTaskBasedOnCustomLimit(Task task, Map<String, Integer> limitsMap, Map<String, Double> ratiosMap)
+  {
+    final Integer limit = getLimitForTask(task.getType(), limitsMap, ratiosMap);
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    int currentCapacityUsed = getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0);
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+
+    return hasCapacityBasedOnLimit(limit, currentCapacityUsed, requiredCapacity);
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Integer> limitsMap,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Integer absoluteLimit = limitsMap.get(taskType);
+    Double ratioLimit = ratiosMap.get(taskType);
+
+    if (absoluteLimit == null && ratioLimit == null) {
+      return null;
+    }
+
+    // Validate the absolute limit if present
+    if (absoluteLimit != null) {
+      Preconditions.checkArgument(absoluteLimit >= 0, "Absolute limit for task %s must be non-negative.", taskType);
+    }
+
+    // Validate the ratio limit if present
+    if (ratioLimit != null) {
+      Preconditions.checkArgument(ratioLimit >= 0.0 && ratioLimit <= 1.0,
+                                  "Ratio for task %s must be between 0.0 and 1.0 inclusive.", taskType
+      );
+    }
+
+    final int totalCapacity = worker.getCapacity();
+
+    Integer ratioBasedLimit = ratioLimit != null ? calculateTaskCapacityFromRatio(ratioLimit, totalCapacity) : null;
+
+    if (absoluteLimit != null && ratioBasedLimit != null) {
+      return Math.min(absoluteLimit, ratioBasedLimit);
+    }
+
+    return absoluteLimit != null ? absoluteLimit : ratioBasedLimit;
+  }
+
+  private boolean hasCapacityBasedOnLimit(int limit, int currentCapacityUsed, int requiredCapacity)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Done, thanks for notice 
created_at: 2024-09-26T11:28:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
+
+  private boolean canRunTaskBasedOnCustomLimit(Task task, Map<String, Integer> limitsMap, Map<String, Double> ratiosMap)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Simplified 
created_at: 2024-09-26T11:28:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +270,79 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean hasSufficientWorkerCapacity(Task task)
+  {
+    int capacityRemaining = worker.getCapacity() - getCurrCapacityUsed();
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+    return capacityRemaining >= requiredCapacity;
+  }
+
+  private boolean isAvailabilityGroupAvailable(Task task)
+  {
+    return !getAvailabilityGroups().contains(task.getTaskResource().getAvailabilityGroup());
+  }
+
+  private boolean canRunTaskBasedOnCustomLimit(Task task, Map<String, Integer> limitsMap, Map<String, Double> ratiosMap)
+  {
+    final Integer limit = getLimitForTask(task.getType(), limitsMap, ratiosMap);
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    int currentCapacityUsed = getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0);
+    int requiredCapacity = task.getTaskResource().getRequiredCapacity();
+
+    return hasCapacityBasedOnLimit(limit, currentCapacityUsed, requiredCapacity);
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Integer> limitsMap,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Integer absoluteLimit = limitsMap.get(taskType);
+    Double ratioLimit = ratiosMap.get(taskType);
+
+    if (absoluteLimit == null && ratioLimit == null) {
+      return null;
+    }
+
+    // Validate the absolute limit if present
+    if (absoluteLimit != null) {
+      Preconditions.checkArgument(absoluteLimit >= 0, "Absolute limit for task %s must be non-negative.", taskType);
+    }
+
+    // Validate the ratio limit if present
+    if (ratioLimit != null) {
+      Preconditions.checkArgument(ratioLimit >= 0.0 && ratioLimit <= 1.0,
+                                  "Ratio for task %s must be between 0.0 and 1.0 inclusive.", taskType
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Moved to the config using javax.validation
created_at: 2024-09-26T11:29:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,6 +180,12 @@ public int getCurrParallelIndexCapacityUsed()
     return currParallelIndexCapacityUsed;
   }
 
+  @JsonProperty("currCapacityUsedByTaskType")
+  public Map<String, Integer> getCurrCapacityUsedByTaskType()
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: Please mark this method as `@Nullable`.
created_at: 2024-09-26T12:58:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +34,17 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  private Map<String, @Min(value = 0, message = "Task slot ratio for must be at least 0.") @Max(value = 1, message = "Task slot ratio cannot be greater than 1") Double> taskSlotRatiosPerWorker = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: Maybe get rid of the Min/Max annotations here (as it somewhat compromises readability) and move the validation to the constructor (you would need to add a new constructor), to adhere to the style used in the rest of the Druid codebase.
created_at: 2024-09-26T13:01:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,6 +206,32 @@ public Message apply(String input)
     return config;
   }
 
+  // To have a detailed error message instead of noSuchFieldException if problematic field is in the superclass
path: processing/src/main/java/org/apache/druid/guice/JsonConfigurator.java
user: login: kfaraz
body: This is a core class that is used in a lot of places. Let's not change it as a part of this PR.
If this change is required, it can be in a separate PR.
created_at: 2024-09-26T13:03:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +34,17 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  private Map<String, @Min(value = 0, message = "Task slot ratio for must be at least 0.") @Max(value = 1, message = "Task slot ratio cannot be greater than 1") Double> taskSlotRatiosPerWorker = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: Please also add a unit test for the validation.
created_at: 2024-09-26T13:04:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() < 1) {
```
created_at: 2024-09-26T13:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getLimitForTask(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Double ratioLimit = ratiosMap.get(taskType);
+
+    if (ratioLimit == null) {
+      return null;
+    }
+
+    return calculateTaskCapacityFromRatio(ratioLimit, worker.getCapacity());
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: Method `calculateTaskCapacityFromRatio` is not required, it can be inlined.
created_at: 2024-09-26T13:07:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getLimitForTask(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Double ratioLimit = ratiosMap.get(taskType);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
    Double taskSlotRatio = ratiosMap.get(taskType);
```
created_at: 2024-09-26T13:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getLimitForTask(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
+  }
+
+  private Integer getLimitForTask(
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
  private Integer getMaxCapacityForTaskType(
```
created_at: 2024-09-26T13:08:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,11 +239,14 @@ public void test_canRunTask()
         new Worker("http", "testWorker2", "192.0.0.1", 10, "v1", WorkerConfig.DEFAULT_CATEGORY),
         6,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z")
     );
 
+    WorkerTaskRunnerConfig config = mock(WorkerTaskRunnerConfig.class);
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: kfaraz
body: Please use a concrete config object instead of a mock.
created_at: 2024-09-26T13:10:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,21 +221,24 @@ public boolean isValidVersion(String minVersion)
     return worker.getVersion().compareTo(minVersion) >= 0;
   }
 
-  public boolean canRunTask(Task task, double parallelIndexTaskSlotRatio)
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * returns true only if:
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
   * Returns true only if:
```
created_at: 2024-09-26T14:36:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,21 +221,24 @@ public boolean isValidVersion(String minVersion)
     return worker.getVersion().compareTo(minVersion) >= 0;
   }
 
-  public boolean canRunTask(Task task, double parallelIndexTaskSlotRatio)
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * returns true only if:
+   * <ul>
+   *   <li>The worker has sufficient capacity to handle the task.</li>
+   *   <li>The task is of the parallel index type and can operate within the permitted ratio of slots designated for that task type.</li>
+   *   <li>The the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
   *   <li>Task slot ratio for this task type has not been exhausted on this worker.</li>
```
created_at: 2024-09-26T14:36:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +34,17 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  private Map<String, @Min(value = 0, message = "Task slot ratio for must be at least 0.") @Max(value = 1, message = "Task slot ratio cannot be greater than 1") Double> taskSlotRatiosPerWorker = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: nozjkoitop
body: I'm not sure if adding the constructor would be the best solution here as this is the parent for a couple other classes, and it will require children refactoring as well (including default values specifying), min max annotations seems to be a better approach, if it'll go together with JsonConfigurator update, otherwise I'd better stick with CustomDeserializer validation approach 
created_at: 2024-09-26T14:47:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +34,17 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  private Map<String, @Min(value = 0, message = "Task slot ratio for must be at least 0.") @Max(value = 1, message = "Task slot ratio cannot be greater than 1") Double> taskSlotRatiosPerWorker = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: kfaraz
body: Hmm, fair point. In that case, let's just skip the validation altogether as there is no validation currently happening for `parallelIndexTaskSlotRatio` either. We can add it when needed later.
created_at: 2024-09-26T15:04:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,11 +239,14 @@ public void test_canRunTask()
         new Worker("http", "testWorker2", "192.0.0.1", 10, "v1", WorkerConfig.DEFAULT_CATEGORY),
         6,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z")
     );
 
+    WorkerTaskRunnerConfig config = mock(WorkerTaskRunnerConfig.class);
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: nozjkoitop
body: This will require changing the config class as currently we don't have setters or constructors (except NoArgs). To use actual config object the most painless approach is to add setters and mark them as VisibleForTesting, @kfaraz WDYT?
created_at: 2024-09-27T07:26:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,11 +239,14 @@ public void test_canRunTask()
         new Worker("http", "testWorker2", "192.0.0.1", 10, "v1", WorkerConfig.DEFAULT_CATEGORY),
         6,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z")
     );
 
+    WorkerTaskRunnerConfig config = mock(WorkerTaskRunnerConfig.class);
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: kfaraz
body: No, you can also create an inline class like:

```
WorkerTaskRunnerConfig config = new WorkerTaskRunnerConfig() {
    @Override
    public Map<String, Double> getTaskSlotRatiosPerWorker() {
          // return your value here
    }
}
```

Later, we can add a constructor.
created_at: 2024-09-27T07:31:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,11 +239,14 @@ public void test_canRunTask()
         new Worker("http", "testWorker2", "192.0.0.1", 10, "v1", WorkerConfig.DEFAULT_CATEGORY),
         6,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z")
     );
 
+    WorkerTaskRunnerConfig config = mock(WorkerTaskRunnerConfig.class);
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: nozjkoitop
body: Thanks for the hint, will do it this way
created_at: 2024-09-27T07:34:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,6 +180,12 @@ public int getCurrParallelIndexCapacityUsed()
     return currParallelIndexCapacityUsed;
   }
 
+  @JsonProperty("currCapacityUsedByTaskType")
+  public Map<String, Integer> getCurrCapacityUsedByTaskType()
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Done
created_at: 2024-09-27T07:47:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +34,17 @@ public class WorkerTaskRunnerConfig
   @JsonProperty
   private double parallelIndexTaskSlotRatio = 1;
 
+  @JsonProperty
+  private Map<String, @Min(value = 0, message = "Task slot ratio for must be at least 0.") @Max(value = 1, message = "Task slot ratio cannot be greater than 1") Double> taskSlotRatiosPerWorker = new HashMap<>();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/config/WorkerTaskRunnerConfig.java
user: login: nozjkoitop
body: Validation removed
created_at: 2024-09-27T07:47:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,6 +206,32 @@ public Message apply(String input)
     return config;
   }
 
+  // To have a detailed error message instead of noSuchFieldException if problematic field is in the superclass
path: processing/src/main/java/org/apache/druid/guice/JsonConfigurator.java
user: login: nozjkoitop
body: Rollbacked
created_at: 2024-09-27T07:50:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Nice catch, thanks
created_at: 2024-09-27T07:51:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getLimitForTask(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Double ratioLimit = ratiosMap.get(taskType);
+
+    if (ratioLimit == null) {
+      return null;
+    }
+
+    return calculateTaskCapacityFromRatio(ratioLimit, worker.getCapacity());
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Done
created_at: 2024-09-27T07:51:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getLimitForTask(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
+  }
+
+  private Integer getLimitForTask(
+      String taskType,
+      Map<String, Double> ratiosMap
+  )
+  {
+    Double ratioLimit = ratiosMap.get(taskType);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Renamed
created_at: 2024-09-27T07:51:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +254,43 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() != 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getLimitForTask(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
+  }
+
+  private Integer getLimitForTask(
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Renamed
created_at: 2024-09-27T07:51:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,11 +239,14 @@ public void test_canRunTask()
         new Worker("http", "testWorker2", "192.0.0.1", 10, "v1", WorkerConfig.DEFAULT_CATEGORY),
         6,
         0,
+        Collections.emptyMap(),
         ImmutableSet.of("grp1", "grp2"),
         ImmutableSet.of("task1", "task2"),
         DateTimes.of("2015-01-01T01:01:02Z")
     );
 
+    WorkerTaskRunnerConfig config = mock(WorkerTaskRunnerConfig.class);
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfoTest.java
user: login: nozjkoitop
body: Updated
created_at: 2024-09-27T07:52:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,21 +221,24 @@ public boolean isValidVersion(String minVersion)
     return worker.getVersion().compareTo(minVersion) >= 0;
   }
 
-  public boolean canRunTask(Task task, double parallelIndexTaskSlotRatio)
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * returns true only if:
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Done
created_at: 2024-09-27T07:52:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,21 +221,24 @@ public boolean isValidVersion(String minVersion)
     return worker.getVersion().compareTo(minVersion) >= 0;
   }
 
-  public boolean canRunTask(Task task, double parallelIndexTaskSlotRatio)
+  /**
+   * Determines if a specific task can be executed on the worker based on
+   * various capacity, custom limits, and availability conditions.
+   * <p>
+   * returns true only if:
+   * <ul>
+   *   <li>The worker has sufficient capacity to handle the task.</li>
+   *   <li>The task is of the parallel index type and can operate within the permitted ratio of slots designated for that task type.</li>
+   *   <li>The the task can run under custom-defined limits for its type,
+   *   such as a maximum number of tasks allowed or a ratio of slots the task type can occupy.</li>
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: Updated
created_at: 2024-09-27T07:52:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +253,37 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  private boolean canRunTaskBasedOnLimit(Task task, WorkerTaskRunnerConfig config)
+  {
+    if (task.getType().equals(ParallelIndexSupervisorTask.TYPE) && config.getParallelIndexTaskSlotRatio() < 1) {
+      return getWorkerParallelIndexCapacity(config.getParallelIndexTaskSlotRatio()) - getCurrParallelIndexCapacityUsed()
+             >= task.getTaskResource().getRequiredCapacity();
+    }
+
+    final Integer limit = getMaxCapacityForTaskType(task.getType(), config.getTaskSlotRatiosPerWorker());
+
+    if (limit == null) {
+      return true; // No limit specified, so task can run
+    }
+
+    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0) >= task.getTaskResource()
+                                                                                          .getRequiredCapacity();
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: Formatting:
```suggestion
    return limit - getCurrCapacityUsedByTaskType().getOrDefault(task.getType(), 0)
                       >= task.getTaskResource().getRequiredCapacity();
```
created_at: 2024-09-27T07:58:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +71,7 @@ public ImmutableWorkerInfo(
     this.worker = worker;
     this.currCapacityUsed = currCapacityUsed;
     this.currParallelIndexCapacityUsed = currParallelIndexCapacityUsed;
+    this.currCapacityUsedByTaskType = currCapacityUsedByTaskType;
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
    this.currCapacityUsedByTaskType = currCapacityUsedByTaskType == null ? Collections.emptyMap() : currCapacityUsedByTaskType;
```
created_at: 2024-10-01T13:37:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -160,6 +180,13 @@ public int getCurrParallelIndexCapacityUsed()
     return currParallelIndexCapacityUsed;
   }
 
+  @JsonProperty("currCapacityUsedByTaskType")
+  @Nullable
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: ```suggestion
  @JsonInclude(JsonInclude.Include.NON_EMPTY)
```
created_at: 2024-10-01T13:38:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotRatiosPerWorker`|Map from task type to the ratio of task slots that the given task type can occupy on a single worker. Each value must be in the range [0, 1]. A value of 0 for a task type means that it can take up no task slot on a worker (i.e. tasks of this type would never run). A value of 1 means that the task type may take up all task slots of a worker if available. For `index_parallel` tasks, the `parallelIndexTaskSlotRatio` overrides the value specified in this map.|Empty map i.e. no limit on task slots for any task type|
path: docs/configuration/index.md
user: login: cryptoe
body: This might not solve the user problem if its per worker. It has to be global. 
Some implementations do container packing. The approach will fail there. 
created_at: 2024-10-02T02:37:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -482,10 +482,13 @@ private static ImmutableWorkerInfo workerWithTask(ImmutableWorkerInfo immutableW
     int parallelIndexTaskCapacity = task.getType().equals(ParallelIndexSupervisorTask.TYPE)
                                     ? task.getTaskResource().getRequiredCapacity()
                                     : 0;
+    int taskCapacity = task.getTaskResource().getRequiredCapacity();
+
     return new ImmutableWorkerInfo(
         immutableWorker.getWorker(),
         immutableWorker.getCurrCapacityUsed() + 1,
         immutableWorker.getCurrParallelIndexCapacityUsed() + parallelIndexTaskCapacity,
+        immutableWorker.incrementCurrCapacityUsedByTaskType(task.getType(), taskCapacity),
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedWorkerProvisioningStrategy.java
user: login: cryptoe
body: Having this per worker is not what end user is expecting. Having a global limit/ratio is what the end user cares about. 
created_at: 2024-10-02T02:39:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,7 @@ The following configs only apply if the Overlord is running in remote mode. For
 |`druid.indexer.runner.taskAssignmentTimeout`|How long to wait after a task has been assigned to a Middle Manager before throwing an error.|`PT5M`|
 |`druid.indexer.runner.minWorkerVersion`|The minimum Middle Manager version to send tasks to. The version number is a string. This affects the expected behavior during certain operations like comparison against `druid.worker.version`. Specifically, the version comparison follows dictionary order. Use ISO8601 date format for the version to accommodate date comparisons. |"0"|
 | `druid.indexer.runner.parallelIndexTaskSlotRatio`| The ratio of task slots available for parallel indexing supervisor tasks per worker. The specified value must be in the range `[0, 1]`. |1|
+|`druid.indexer.runner.taskSlotRatiosPerWorker`|Map from task type to the ratio of task slots that the given task type can occupy on a single worker. Each value must be in the range [0, 1]. A value of 0 for a task type means that it can take up no task slot on a worker (i.e. tasks of this type would never run). A value of 1 means that the task type may take up all task slots of a worker if available. For `index_parallel` tasks, the `parallelIndexTaskSlotRatio` overrides the value specified in this map.|Empty map i.e. no limit on task slots for any task type|
path: docs/configuration/index.md
user: login: cryptoe
body: Please join dev discussion at : https://apachedruidworkspace.slack.com/archives/C030CMF6B70/p1727836829640959
created_at: 2024-10-02T02:40:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -482,10 +482,13 @@ private static ImmutableWorkerInfo workerWithTask(ImmutableWorkerInfo immutableW
     int parallelIndexTaskCapacity = task.getType().equals(ParallelIndexSupervisorTask.TYPE)
                                     ? task.getTaskResource().getRequiredCapacity()
                                     : 0;
+    int taskCapacity = task.getTaskResource().getRequiredCapacity();
+
     return new ImmutableWorkerInfo(
         immutableWorker.getWorker(),
         immutableWorker.getCurrCapacityUsed() + 1,
         immutableWorker.getCurrParallelIndexCapacityUsed() + parallelIndexTaskCapacity,
+        immutableWorker.incrementCurrCapacityUsedByTaskType(task.getType(), taskCapacity),
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/autoscaling/PendingTaskBasedWorkerProvisioningStrategy.java
user: login: kfaraz
body: In effect, the overall ratio would be more or less the same as every worker would have this same ratio.
`parallelIndexTaskSlotRatio` also does the same thing, so it only makes sense to retain a similar logic.
created_at: 2024-10-02T02:41:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1383,6 +1387,16 @@ field. If not provided, the default is to not use it at all.
 |`categoryMap`|A JSON map object mapping a task type String name to a [CategoryConfig](#categoryconfig) object, by which you can specify category config for different task type.|`{}`|
 |`strong`|With weak workerCategorySpec (the default), tasks for a dataSource may be assigned to other Middle Managers if the Middle Managers specified in `categoryMap` are not able to run all pending tasks in the queue for that dataSource. With strong workerCategorySpec, tasks for a dataSource will only ever be assigned to their specified Middle Managers, and will wait in the pending queue if necessary.|false|
 
+###### taskLimits
+
+The `taskLimits` field can be used with the `equalDistribution`, `fillCapacity`, `equalDistributionWithCategorySpec` and `fillCapacityWithCategorySpec` strategies.
+If you don't provide it, it will default to not being used.
+
+|Property|Description|Default|
+|--------|-----------|-------|
+|`taskLimits`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots in total. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskLimits = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|`{}`|
+|`taskRatios`|A map where each key is a task type (`String`), and the corresponding value is a `Double` which should be in the range [0, 1], representing the ratio of available task slots that tasks of this type can occupy. This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskRatios = {"index_parallel": 0.5, "query_controller": 0.25}`. In this example, parallel indexing tasks can occupy up to 50% of the total task slots, and query controllers can occupy up to 25% of the total task slots.|`{}`|
path: docs/configuration/index.md
user: login: cryptoe
body: ```suggestion
|`taskRatios`|A map where each key is a task type (`String`), and the corresponding value is a `Double` which should be in the range [0, 1], representing the ratio of task slots that tasks of this type can occupy. This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskRatios = {"index_parallel": 0.5, "query_controller": 0.25}`. In this example, parallel indexing tasks can occupy up to 50% of the total task slots, and query controllers can occupy up to 25% of the total task slots.|`{}`|
```
created_at: 2025-03-05T12:41:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import org.apache.druid.indexing.common.task.Task;
+
+import java.util.Map;
+
+public class LimiterUtils
+{
+
+  public static boolean canRunTask(Task task, TaskLimits taskLimits, Integer currentSlotsUsed, Integer totalCapacity)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/LimiterUtils.java
user: login: cryptoe
body: This method should have some java docs. 
created_at: 2025-03-05T12:44:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import org.apache.druid.indexing.common.task.Task;
+
+import java.util.Map;
+
+public class LimiterUtils
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/LimiterUtils.java
user: login: cryptoe
body: ```suggestion
public class TaskLimiterUtils
```
created_at: 2025-03-05T12:45:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +244,13 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  public Map<String, Integer> incrementTypeSpecificCapacity(String type, int capacityToAdd)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: cryptoe
body: Should there be a corresponding decrement ?
created_at: 2025-03-05T12:47:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +61,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currCapacityUsedByTaskType") Map<String, Integer> currCapacityUsedByTaskType,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: cryptoe
body: or this should be immutable no ?
created_at: 2025-03-05T12:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +61,7 @@ public ImmutableWorkerInfo(
       @JsonProperty("worker") Worker worker,
       @JsonProperty("currCapacityUsed") int currCapacityUsed,
       @JsonProperty("currParallelIndexCapacityUsed") int currParallelIndexCapacityUsed,
+      @JsonProperty("currCapacityUsedByTaskType") Map<String, Integer> currCapacityUsedByTaskType,
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: nozjkoitop
body: It is immutable. incrementTypeSpecificCapacity doesn't change the fields.  This typeSpecificCapacity is managed exactly like parallelIndexCapacityUsed, which is `immutableWorker.getCurrParallelIndexCapacityUsed() + parallelIndexTaskCapacity` in ImmutableWorkerInfo constructor arguments, but here the incremented value is created in ImmutableWorkerInfo itself. Wrt decrement, I dont see any for currParallelIndexCapacityUsed and currCapacityUsed either, if I'm not mistaken it's managed by Provisioning Strategy
created_at: 2025-03-05T14:43:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1383,6 +1387,16 @@ field. If not provided, the default is to not use it at all.
 |`categoryMap`|A JSON map object mapping a task type String name to a [CategoryConfig](#categoryconfig) object, by which you can specify category config for different task type.|`{}`|
 |`strong`|With weak workerCategorySpec (the default), tasks for a dataSource may be assigned to other Middle Managers if the Middle Managers specified in `categoryMap` are not able to run all pending tasks in the queue for that dataSource. With strong workerCategorySpec, tasks for a dataSource will only ever be assigned to their specified Middle Managers, and will wait in the pending queue if necessary.|false|
 
+###### taskLimits
+
+The `taskLimits` field can be used with the `equalDistribution`, `fillCapacity`, `equalDistributionWithCategorySpec` and `fillCapacityWithCategorySpec` strategies.
+If you don't provide it, it will default to not being used.
+
+|Property|Description|Default|
+|--------|-----------|-------|
+|`taskLimits`|A map where each key is a task type (`String`), and the corresponding value represents the absolute limit on the number of task slots that tasks of this type can occupy. The value is an `Integer` that is greater than or equal to 0. For example, a value of 5 means that tasks of this type can occupy up to 5 task slots in total. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskLimits = {"index_parallel": 3, "query_controller": 5}`. In this example, parallel indexing tasks can occupy up to 3 task slots, and query controllers can occupy up to 5 task slots.|`{}`|
+|`taskRatios`|A map where each key is a task type (`String`), and the corresponding value is a `Double` which should be in the range [0, 1], representing the ratio of available task slots that tasks of this type can occupy. This ratio defines the proportion of total task slots a task type can use, calculated as `ratio * totalSlots`. If both absolute and ratio limits are specified for the same task type, the effective limit will be the smaller of the absolute limit and the limit derived from the corresponding ratio. `taskRatios = {"index_parallel": 0.5, "query_controller": 0.25}`. In this example, parallel indexing tasks can occupy up to 50% of the total task slots, and query controllers can occupy up to 25% of the total task slots.|`{}`|
path: docs/configuration/index.md
user: login: nozjkoitop
body: Done, thanks
created_at: 2025-03-05T14:54:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import org.apache.druid.indexing.common.task.Task;
+
+import java.util.Map;
+
+public class LimiterUtils
+{
+
+  public static boolean canRunTask(Task task, TaskLimits taskLimits, Integer currentSlotsUsed, Integer totalCapacity)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/LimiterUtils.java
user: login: nozjkoitop
body: Docs added
created_at: 2025-03-05T14:54:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +244,13 @@ private int getWorkerParallelIndexCapacity(double parallelIndexTaskSlotRatio)
     return workerParallelIndexCapacity;
   }
 
+  public Map<String, Integer> incrementTypeSpecificCapacity(String type, int capacityToAdd)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/ImmutableWorkerInfo.java
user: login: kfaraz
body: This method is a little misleading given that this is an immutable class.
I would advise removing this method and simply inlining this change at the caller.

```
updatedMap = new HashMap(immutableWorkerInfo.getCurrCapacityUsedByTaskType()).merge(type, capacityToAdd, Integer::sum);
```
created_at: 2025-03-06T09:37:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,7 +85,10 @@ public boolean equals(final Object o)
       return false;
     }
     final EqualDistributionWithCategorySpecWorkerSelectStrategy that = (EqualDistributionWithCategorySpecWorkerSelectStrategy) o;
-    return Objects.equals(workerCategorySpec, that.workerCategorySpec);
+    if (!Objects.equals(workerCategorySpec, that.workerCategorySpec)) {
+      return false;
+    }
+    return Objects.equals(taskLimits, that.taskLimits);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategy.java
user: login: kfaraz
body: ```suggestion
    return Objects.equals(workerCategorySpec, that.workerCategorySpec)
        && Objects.equals(taskLimits, that.taskLimits);
```
created_at: 2025-03-06T09:40:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,7 +85,10 @@ public boolean equals(final Object o)
       return false;
     }
     final EqualDistributionWithCategorySpecWorkerSelectStrategy that = (EqualDistributionWithCategorySpecWorkerSelectStrategy) o;
-    return Objects.equals(workerCategorySpec, that.workerCategorySpec);
+    if (!Objects.equals(workerCategorySpec, that.workerCategorySpec)) {
+      return false;
+    }
+    return Objects.equals(taskLimits, that.taskLimits);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithCategorySpecWorkerSelectStrategy.java
user: login: kfaraz
body: Please update the hashCode method as well.
created_at: 2025-03-06T09:40:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,7 +95,10 @@ public boolean equals(final Object o)
       return false;
     }
     final EqualDistributionWorkerSelectStrategy that = (EqualDistributionWorkerSelectStrategy) o;
-    return Objects.equals(affinityConfig, that.affinityConfig);
+    if (!Objects.equals(affinityConfig, that.affinityConfig)) {
+      return false;
+    }
+    return Objects.equals(taskLimits, that.taskLimits);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategy.java
user: login: kfaraz
body: ```suggestion
    return Objects.equals(affinityConfig, that.affinityConfig)
        && Objects.equals(taskLimits, that.taskLimits);
```
created_at: 2025-03-06T09:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,7 +95,10 @@ public boolean equals(final Object o)
       return false;
     }
     final EqualDistributionWorkerSelectStrategy that = (EqualDistributionWorkerSelectStrategy) o;
-    return Objects.equals(affinityConfig, that.affinityConfig);
+    if (!Objects.equals(affinityConfig, that.affinityConfig)) {
+      return false;
+    }
+    return Objects.equals(taskLimits, that.taskLimits);
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategy.java
user: login: kfaraz
body: Please update the `hashCode` as well. 
created_at: 2025-03-06T09:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,7 +86,10 @@ public boolean equals(final Object o)
       return false;
     }
     final FillCapacityWithCategorySpecWorkerSelectStrategy that = (FillCapacityWithCategorySpecWorkerSelectStrategy) o;
-    return Objects.equals(workerCategorySpec, that.workerCategorySpec);
+    if (!Objects.equals(workerCategorySpec, that.workerCategorySpec)) {
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategy.java
user: login: kfaraz
body: fix this and hashCode as suggested for other classes.
created_at: 2025-03-06T09:42:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,13 +33,17 @@
 public class FillCapacityWithCategorySpecWorkerSelectStrategy implements WorkerSelectStrategy
 {
   private final WorkerCategorySpec workerCategorySpec;
+  private final TaskLimits taskLimits;
+
 
   @JsonCreator
   public FillCapacityWithCategorySpecWorkerSelectStrategy(
-      @JsonProperty("workerCategorySpec") WorkerCategorySpec workerCategorySpec
+      @JsonProperty("workerCategorySpec") WorkerCategorySpec workerCategorySpec,
+      @JsonProperty("taskLimits") @Nullable TaskLimits taskLimits
   )
   {
     this.workerCategorySpec = workerCategorySpec;
+    this.taskLimits = Configs.valueOrDefault(taskLimits, new TaskLimits());
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWithCategorySpecWorkerSelectStrategy.java
user: login: kfaraz
body: Rather than using `new TaskLimits()`, I would advise keeping a `TaskLimits.EMPTY` instance that can be used everywhere.
created_at: 2025-03-06T09:42:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,20 +93,26 @@ public boolean equals(final Object o)
       return false;
     }
     final FillCapacityWorkerSelectStrategy that = (FillCapacityWorkerSelectStrategy) o;
-    return Objects.equals(affinityConfig, that.affinityConfig);
+    if (!Objects.equals(affinityConfig, that.affinityConfig)) {
+      return false;
+    }
+    return Objects.equals(taskLimits, that.taskLimits);
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(affinityConfig);
+    int result = affinityConfig != null ? affinityConfig.hashCode() : 0;
+    result = 31 * result + (taskLimits != null ? taskLimits.hashCode() : 0);
+    return result;
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/FillCapacityWorkerSelectStrategy.java
user: login: kfaraz
body: ```suggestion
    return Objects.hash(affiinityConfig, taskLimits);
```
created_at: 2025-03-06T09:43:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import org.apache.druid.indexing.common.task.Task;
+
+import java.util.Map;
+
+public class TaskLimiterUtils
+{
+
+  /**
+   * Determines whether the given task can be executed based on task limits and available capacity.
+   *
+   * @param task The task to evaluate.
+   * @param taskLimits The limits and ratios defining task execution constraints.
+   * @param currentSlotsUsed The current capacity used by tasks of the same type.
+   * @param totalCapacity The total available capacity across all workers.
+   * @return {@code true} if the task meets the defined limits and capacity constraints; {@code false} otherwise.
+   */
+  public static boolean canRunTask(Task task, TaskLimits taskLimits, Integer currentSlotsUsed, Integer totalCapacity)
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimiterUtils.java
user: login: kfaraz
body: I think this need not be a static utility method.
This could very well live as a non-static method in `TaskLimits` itself.

```
public boolean canRunTask(Task task, Integer currentSlotsUsed, Integer totalCapacity) {
}
```
created_at: 2025-03-06T09:44:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import org.apache.druid.indexing.common.task.Task;
+
+import java.util.Map;
+
+public class TaskLimiterUtils
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimiterUtils.java
user: login: kfaraz
body: This class won't be needed if the utility method is moved to `TaskLimits`, where it would be a more natural fit.
created_at: 2025-03-06T09:45:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.common.config.Configs;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+public class TaskLimits
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
user: login: kfaraz
body: Please add a javadoc and keep an `EMPTY` constant instance.
created_at: 2025-03-06T09:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.common.config.Configs;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+public class TaskLimits
+{
+  private final Map<String, Integer> taskLimits;
+  private final Map<String, Double> taskRatios;
+
+  public TaskLimits()
+  {
+    this(Collections.emptyMap(), Collections.emptyMap());
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
user: login: kfaraz
body: Since we have now moved to Java 11, you may use this instead for brevity.

```suggestion
    this(Map.of(), Map.of());
```
created_at: 2025-03-06T09:46:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.common.config.Configs;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+public class TaskLimits
+{
+  private final Map<String, Integer> taskLimits;
+  private final Map<String, Double> taskRatios;
+
+  public TaskLimits()
+  {
+    this(Collections.emptyMap(), Collections.emptyMap());
+  }
+
+  @JsonCreator
+  public TaskLimits(
+      @JsonProperty("taskLimits") @Nullable Map<String, Integer> taskLimits,
+      @JsonProperty("taskRatios") @Nullable Map<String, Double> taskRatios
+  )
+  {
+    validateLimits(taskLimits, taskRatios);
+    this.taskLimits = Configs.valueOrDefault(taskLimits, Collections.emptyMap());
+    this.taskRatios = Configs.valueOrDefault(taskRatios, Collections.emptyMap());
+  }
+
+  private void validateLimits(Map<String, Integer> taskLimits, Map<String, Double> taskRatios)
+  {
+    if (taskLimits != null && taskLimits.values().stream().anyMatch(val -> val < 0)) {
+      throw new IllegalArgumentException("Task limits should be bigger than 0");
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
user: login: kfaraz
body: ```suggestion
      throw InvalidInput.exception("Task limit for any type must be greater than zero. Found[%s].", taskLimits);
```

same suggestion for the other exception.
created_at: 2025-03-06T09:47:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.common.config.Configs;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+public class TaskLimits
+{
+  private final Map<String, Integer> taskLimits;
+  private final Map<String, Double> taskRatios;
+
+  public TaskLimits()
+  {
+    this(Collections.emptyMap(), Collections.emptyMap());
+  }
+
+  @JsonCreator
+  public TaskLimits(
+      @JsonProperty("taskLimits") @Nullable Map<String, Integer> taskLimits,
+      @JsonProperty("taskRatios") @Nullable Map<String, Double> taskRatios
+  )
+  {
+    validateLimits(taskLimits, taskRatios);
+    this.taskLimits = Configs.valueOrDefault(taskLimits, Collections.emptyMap());
+    this.taskRatios = Configs.valueOrDefault(taskRatios, Collections.emptyMap());
+  }
+
+  private void validateLimits(Map<String, Integer> taskLimits, Map<String, Double> taskRatios)
+  {
+    if (taskLimits != null && taskLimits.values().stream().anyMatch(val -> val < 0)) {
+      throw new IllegalArgumentException("Task limits should be bigger than 0");
+    } else if (taskRatios != null && taskRatios.values().stream().anyMatch(val -> val < 0 || val > 1)) {
+      throw new IllegalArgumentException("Task ratios should be in the interval of [0, 1]");
+    }
+  }
+
+  @JsonProperty
+  public Map<String, Integer> getTaskLimits()
+  {
+    return taskLimits;
+  }
+
+  @JsonProperty
+  public Map<String, Double> getTaskRatios()
+  {
+    return taskRatios;
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    TaskLimits that = (TaskLimits) o;
+    return Objects.equals(taskLimits, that.taskLimits) && Objects.equals(taskRatios, that.taskRatios);
+  }
+
+  @Override
+  public int hashCode()
+  {
+    int result = taskLimits != null ? taskLimits.hashCode() : 0;
+    result = 31 * result + (taskRatios != null ? taskRatios.hashCode() : 0);
+    return result;
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
user: login: kfaraz
body: Use `Objects.hashCode` for symmetry with `Objects.equals`.
created_at: 2025-03-06T09:47:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -145,9 +148,16 @@ public static ImmutableWorkerInfo selectWorker(
   private static Map<String, ImmutableWorkerInfo> getRunnableWorkers(
       final Task task,
       final Map<String, ImmutableWorkerInfo> allWorkers,
-      final WorkerTaskRunnerConfig workerTaskRunnerConfig
+      final WorkerTaskRunnerConfig workerTaskRunnerConfig,
+      final TaskLimits taskLimits
   )
   {
+    if (!TaskLimiterUtils.canRunTask(task,
+                                     taskLimits,
+                                     getTotalCapacityUsedByType(allWorkers, task.getType()), getTotalCapacity(allWorkers)
+    )) {
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/WorkerSelectUtils.java
user: login: kfaraz
body: style:

```suggestion
    if (
        !TaskLimiterUtils.canRunTask(
            task,
            taskLimits,
            getTotalCapacityUsedByType(allWorkers, task.getType()), getTotalCapacity(allWorkers)
        )
    ) {
```
created_at: 2025-03-06T09:49:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -139,12 +144,147 @@ public void testSerde() throws Exception
   {
     final ObjectMapper objectMapper = TestHelper.makeJsonMapper();
     final EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
-        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false)
+        new AffinityConfig(ImmutableMap.of("foo", ImmutableSet.of("localhost")), false),
+        null
     );
     final WorkerSelectStrategy strategy2 = objectMapper.readValue(
         objectMapper.writeValueAsBytes(strategy),
         WorkerSelectStrategy.class
     );
     Assert.assertEquals(strategy, strategy2);
   }
+
+  @Test
+  public void testFindWorkerForTaskWithGlobalLimits()
+  {
+    Map<String, Integer> taskLimits = new HashMap<>();
+    taskLimits.put("noop", 2);
+
+    Map<String, Integer> capacityUsed = new HashMap<>();
+    capacityUsed.put("noop", 1);
+    EqualDistributionWorkerSelectStrategy strategy = new EqualDistributionWithAffinityWorkerSelectStrategy(
+        null,
+        new TaskLimits(taskLimits, null)
+    );
+
+    NoopTask noopTask = NoopTask.forDatasource("foo");
+    ImmutableWorkerInfo worker = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            ),
+            "localhost1",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost1", "localhost1", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
+                DateTimes.nowUtc()
+            )
+        ),
+        noopTask
+    );
+    Assert.assertNotNull(worker);
+
+    ImmutableWorkerInfo worker1 = strategy.findWorkerForTask(
+        new RemoteTaskRunnerConfig(),
+        ImmutableMap.of(
+            "localhost0",
+            new ImmutableWorkerInfo(
+                new Worker("http", "localhost0", "localhost0", 2, "v1", WorkerConfig.DEFAULT_CATEGORY), 0,
+                0,
+                capacityUsed,
+                new HashSet<>(),
+                new HashSet<>(),
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWithAffinityWorkerSelectStrategyTest.java
user: login: kfaraz
body: Suggestion:

You may use `Set.of()` instead. Similar, `List.of()` and `Map.of()` are pretty handy too for creating immutable objects.
created_at: 2025-03-06T09:50:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -226,7 +226,7 @@ public void testStrongAffinity()
                 "bar", ImmutableSet.of("nonexistent-worker")
             ),
             true
-        )
+        ), null
path: indexing-service/src/test/java/org/apache/druid/indexing/overlord/setup/EqualDistributionWorkerSelectStrategyTest.java
user: login: kfaraz
body: Please put this in the next line. Same comment above.
created_at: 2025-03-06T09:50:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2132,6 +2132,7 @@ sqlQuery
 successfulSending
 [S]igar
 taskBlackListCleanupPeriod
+taskLimits
path: website/.spelling
user: login: kfaraz
body: You wouldn't need to add it here if you use `taskLimits` in the heading too.
created_at: 2025-03-06T09:51:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.druid.indexing.overlord.setup;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.common.config.Configs;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+public class TaskLimits
+{
+  private final Map<String, Integer> taskLimits;
+  private final Map<String, Double> taskRatios;
+
+  public TaskLimits()
+  {
+    this(Collections.emptyMap(), Collections.emptyMap());
+  }
+
+  @JsonCreator
+  public TaskLimits(
+      @JsonProperty("taskLimits") @Nullable Map<String, Integer> taskLimits,
+      @JsonProperty("taskRatios") @Nullable Map<String, Double> taskRatios
path: indexing-service/src/main/java/org/apache/druid/indexing/overlord/setup/TaskLimits.java
user: login: kfaraz
body: This is a little confusing, since these two fields are already inside a field named `taskLimits`, right?

I think it would be cleaner if we called these fields something like `maxCountByType`, `maxRatioByType`.
created_at: 2025-03-06T09:53:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2132,6 +2132,7 @@ sqlQuery
 successfulSending
 [S]igar
 taskBlackListCleanupPeriod
+taskLimits
path: website/.spelling
user: login: nozjkoitop
body: wdym? It is in `######` heading of index.md but some checks were failing. Am i getting it wrong? 
created_at: 2025-03-06T10:55:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1383,6 +1387,16 @@ field. If not provided, the default is to not use it at all.
 |`categoryMap`|A JSON map object mapping a task type String name to a [CategoryConfig](#categoryconfig) object, by which you can specify category config for different task type.|`{}`|
 |`strong`|With weak workerCategorySpec (the default), tasks for a dataSource may be assigned to other Middle Managers if the Middle Managers specified in `categoryMap` are not able to run all pending tasks in the queue for that dataSource. With strong workerCategorySpec, tasks for a dataSource will only ever be assigned to their specified Middle Managers, and will wait in the pending queue if necessary.|false|
 
+###### taskLimits
path: docs/configuration/index.md
user: login: kfaraz
body: Use this to avoid spell check failure and adhere with the existing docs style:

```suggestion
###### `taskLimits`
```
created_at: 2025-03-06T10:57:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2132,6 +2132,7 @@ sqlQuery
 successfulSending
 [S]igar
 taskBlackListCleanupPeriod
+taskLimits
path: website/.spelling
user: login: kfaraz
body: https://github.com/apache/druid/pull/16889#discussion_r1983144494
created_at: 2025-03-06T10:58:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1383,6 +1387,16 @@ field. If not provided, the default is to not use it at all.
 |`categoryMap`|A JSON map object mapping a task type String name to a [CategoryConfig](#categoryconfig) object, by which you can specify category config for different task type.|`{}`|
 |`strong`|With weak workerCategorySpec (the default), tasks for a dataSource may be assigned to other Middle Managers if the Middle Managers specified in `categoryMap` are not able to run all pending tasks in the queue for that dataSource. With strong workerCategorySpec, tasks for a dataSource will only ever be assigned to their specified Middle Managers, and will wait in the pending queue if necessary.|false|
 
+###### taskLimits
path: docs/configuration/index.md
user: login: nozjkoitop
body: Thanks, understood 
created_at: 2025-03-06T11:00:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: nozjkoitop
created_at: 2024-08-14T10:03:45Z
body: > Remove useless tail

Done, thanks
author_association: CONTRIBUTOR
user: login: nozjkoitop
created_at: 2024-09-12T15:34:25Z
body: Thanks @kfaraz, @cryptoe  for your comments 
The most trivial deadlock scenario occurs when we queue a group of controller tasks but don't have available task slots for actual workers. This results in tasks hanging and eventually timing out. Thanks for highlighting the WorkerTaskRunnerConfig, it seems like the great place for this configuration. I've updated the behavior and merged it with `parallelIndexTaskSlotRatio`, now it's more flexible, also I've added the logging to inform the user why the task is Pending
author_association: CONTRIBUTOR
user: login: kfaraz
created_at: 2024-09-28T02:38:28Z
body: @cryptoe , do you have any further comments on this PR?
author_association: CONTRIBUTOR
user: login: kfaraz
created_at: 2024-10-01T13:41:51Z
body: @nozjkoitop , sorry I had missed these null checks in the earlier review. Could you please include these?
I am unable to commit the suggestions myself.
author_association: CONTRIBUTOR
user: login: nozjkoitop
created_at: 2024-10-01T14:03:21Z
body: > Could you please include these? I am unable to commit the suggestions myself.

Done, thanks
author_association: CONTRIBUTOR
user: login: kfaraz
created_at: 2024-10-02T02:49:47Z
body: @nozjkoitop , @cryptoe has expressed some concerns around having a per-worker config

He makes some good points here:
https://apachedruidworkspace.slack.com/archives/C030CMF6B70/p1727836829640959
author_association: CONTRIBUTOR
user: login: nozjkoitop
created_at: 2024-10-02T10:39:47Z
body: > He makes some good points here: https://apachedruidworkspace.slack.com/archives/C030CMF6B70/p1727836829640959

I'm afraid I don't have access to this Slack workspace 
author_association: CONTRIBUTOR
user: login: kfaraz
created_at: 2024-10-02T10:58:28Z
body: > I'm afraid I don't have access to this Slack workspace

You can try this link [https://druid.apache.org/community/join-slack](https://druid.apache.org/community/join-slack?v=1) from the [Druid docs](https://druid.apache.org/community/).
author_association: CONTRIBUTOR
user: login: nozjkoitop
created_at: 2024-10-10T10:38:17Z
body: Hey @cryptoe, what do you think about this solution? I've used SelectWorkerStrategies to utilize dynamic configuration of global limits, and it seems like a good option to me.
author_association: CONTRIBUTOR
user: login: cryptoe
created_at: 2024-11-21T13:40:45Z
body: Apologies, I have been meaning to get to this PR. Will try to finish it by EOW.  
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-01-21T00:20:25Z
body: This pull request has been marked as stale due to 60 days of inactivity.
It will be closed in 4 weeks if no further activity occurs. If you think
that's incorrect or this pull request should instead be reviewed, please simply
write any comment. Even if closed, you can still revive the PR at any time or
discuss it on the dev@druid.apache.org list.
Thank you for your contributions.
author_association: NONE
user: login: nozjkoitop
created_at: 2025-01-21T08:21:38Z
body: Hi @cryptoe will you have some time to review this changes? 
author_association: CONTRIBUTOR
user: login: cryptoe
created_at: 2025-03-04T15:21:29Z
body: @nozjkoitop Could you please rebase this PR. Will take a look again. Overall LGTM 
cc @kfaraz 
author_association: CONTRIBUTOR
user: login: nozjkoitop
created_at: 2025-03-05T11:13:29Z
body: @cryptoe Done 
author_association: CONTRIBUTOR
user: login: kfaraz
created_at: 2025-03-05T11:36:07Z
body: Thanks for your work on this, @nozjkoitop !
I will try to finish the review of this PR later this week.
author_association: CONTRIBUTOR
user: login: cryptoe
created_at: 2025-03-06T05:04:14Z
body: Thanks for the changes. 
Lets wait for @kfaraz review and then we can get this merged. 

author_association: CONTRIBUTOR