Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ private KafkaSupervisorSpec createKafkaSupervisor(
.withConsumerProperties(kafkaServer.consumerProperties())
.withTaskCount(taskCount)
)
.withContext(Map.of("useConcurrentLocks", true))
.withId(supervisorId)
.build(dataSource, TOPIC);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Inject;
import org.apache.druid.common.config.Configs;
import org.apache.druid.common.guava.FutureUtils;
import org.apache.druid.common.utils.IdUtils;
import org.apache.druid.error.DruidException;
Expand All @@ -40,6 +41,9 @@
import org.apache.druid.indexing.seekablestream.supervisor.BoundedStreamConfig;
import org.apache.druid.indexing.seekablestream.supervisor.SeekableStreamSupervisor;
import org.apache.druid.indexing.seekablestream.supervisor.SeekableStreamSupervisorSpec;
import org.apache.druid.indexing.seekablestream.supervisor.autoscaler.CostBasedAutoScaler;
import org.apache.druid.indexing.seekablestream.supervisor.autoscaler.CostBasedAutoScalerConfig;
import org.apache.druid.indexing.seekablestream.supervisor.autoscaler.CostMetrics;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.Pair;
Expand Down Expand Up @@ -72,6 +76,9 @@ public class SupervisorManager implements SupervisorStatsProvider
{
private static final EmittingLogger log = new EmittingLogger(SupervisorManager.class);

// Caps the autoscaler simulation's range (we're not aware of workloads higher than 1k partitions)..
private static final int MAX_SIMULATION_TASK_COUNT = 1000;

private final MetadataSupervisorManager metadataSupervisorManager;
private final ConcurrentHashMap<String, Pair<Supervisor, SupervisorSpec>> supervisors = new ConcurrentHashMap<>();
// SupervisorTaskAutoScaler could be null
Expand Down Expand Up @@ -644,6 +651,90 @@ public boolean isAnotherTaskGroupPublishingToPartitions(
}
}

/**
* Simulates the effects of the {@code costBased} auto-scaler by computing the optimal
* task count under various values of aggregate lag.
*/
public Map<String, Object> simulateAutoscaling(
String supervisorId,
CostBasedAutoScalerConfig config,
int criticalLag,
int maxProcessingRatePerTask,
@Nullable Integer requestedTaskCount
)
{
// Validate that this is a streaming supervisor
final StreamSupervisor supervisor = requireStreamSupervisor(supervisorId, "simulateAutoscaling");

// Validate the inputs
InvalidInput.conditionalException(
criticalLag >= 1000,
"Value of critical lag[%d] must be 1000 or more",
criticalLag
);
InvalidInput.conditionalException(
maxProcessingRatePerTask >= 100,
"Value of maxProcessingRatePerTask[%d] must be 100 events per second or more",
maxProcessingRatePerTask
);
InvalidInput.conditionalException(
config.getTaskCountMin() >= 1,
"Value of taskCountMin[%d] must be 1 or more",
config.getTaskCountMin()
);
InvalidInput.conditionalException(
config.getTaskCountMax() <= MAX_SIMULATION_TASK_COUNT,
"Value of taskCountMax[%d] must be [%d] or less",
config.getTaskCountMax(), MAX_SIMULATION_TASK_COUNT
);

// Simulate from the supervisor's live task count unless the caller pins one.
final int currentTaskCount = Configs.valueOrDefault(
requestedTaskCount,
((SeekableStreamSupervisor<?, ?, ?>) supervisor).getIoConfig().getTaskCount()
);
InvalidInput.conditionalException(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Bound the simulation task counts at the API boundary

The request body can set taskCountMin to zero, which the config constructor accepts; with a positive live task count this reaches computeValidTaskCounts and divides by zero. It can also set an arbitrarily large taskCountMax, which is reused as partitionCount and makes each of the 40 samples scan linearly through that range, allowing an authorized request to monopolize the Overlord CPU. Require a positive minimum and impose a practical maximum before running the simulation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

requestedTaskCount == null
|| (currentTaskCount >= config.getTaskCountMin() && currentTaskCount <= config.getTaskCountMax()),
"Value of currentTaskCount[%d] must be within taskCountMin[%d] and taskCountMax[%d]",
currentTaskCount, config.getTaskCountMin(), config.getTaskCountMax()
);
final int simulationTaskCount = Math.max(
config.getTaskCountMin(),
Math.min(currentTaskCount, config.getTaskCountMax())
);

// Assumption: enough partitions to reach taskCountMax.
final int partitionCount = config.getTaskCountMax();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Simulate the supervisor's actual topology

The live cost scaler uses supervisor.getPartitionCount() and the supervisor IO config's task duration, but this endpoint substitutes taskCountMax and a hard-coded hour. For example, a two-partition supervisor configured with a maximum of ten is shown recommendations that cannot run, and a custom task duration changes the lag-recovery cost curve. Read both values from the selected supervisor so the chart predicts what its scaler would actually choose.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is under user control; let them play.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These values are not currently user-controlled: the panel/API do not expose partition count or task duration; they derive partition count from taskCountMax and fix duration at 3600 seconds. Thus a two-partition supervisor can still show impossible recommendations up to 10 tasks, and non-hour task durations produce a different curve. Please either read both from the selected supervisor, or expose them as explicit simulator inputs and label the assumptions.

Reviewed 11 of 11 changed files.

final int taskDurationSeconds = 3600;

// Assume that the tasks are fully used since there is some lag
final double avgProcessingRatePerTask = maxProcessingRatePerTask;
final double idleRatio = config.getOptimalTaskIdleRatio();

// Invoke the cost function for a variety of input values of lag
final Object[] rows = new Object[40];
final int lagStepSize = criticalLag / 20;
final CostBasedAutoScaler autoscaleSimulator = CostBasedAutoScaler.createSimulator(config, supervisorId);
for (int i = 0; i < 40; ++i) {
final double observedAggregateLag = (double) lagStepSize * i;
final CostMetrics costMetrics = new CostMetrics(
observedAggregateLag / partitionCount,
simulationTaskCount,
partitionCount,
idleRatio,
taskDurationSeconds,
avgProcessingRatePerTask,
maxProcessingRatePerTask * 1.0
);
final int optimalTaskCount = autoscaleSimulator.computeOptimalTaskCountInternal(costMetrics, true);
rows[i] = Map.of("lag", observedAggregateLag, "taskCount", optimalTaskCount);
}

// Collect the results and return
return Map.of("data", rows);
}

/**
* Stops a supervisor with a given id and then removes it from the list.
* <p/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.apache.druid.indexing.overlord.DataSourceMetadata;
import org.apache.druid.indexing.overlord.TaskMaster;
import org.apache.druid.indexing.overlord.http.security.SupervisorResourceFilter;
import org.apache.druid.indexing.seekablestream.supervisor.autoscaler.CostBasedAutoScalerConfig;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.UOE;
import org.apache.druid.segment.incremental.ParseExceptionReport;
Expand Down Expand Up @@ -183,7 +184,9 @@
);
}

/** Audits supervisor spec submissions that changed or restarted the supervisor. */
/**
* Audits supervisor spec submissions that changed or restarted the supervisor.
*/
private void auditSupervisorUpdate(final SupervisorSpec spec, final HttpServletRequest req)
{
final String auditPayload
Expand Down Expand Up @@ -534,6 +537,33 @@
);
}

@POST
@Path("/{id}/autoscaler")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We will rename this API based on what we decide the final result set to look like:

Suggested change
@Path("/{id}/autoscaler")
@Path("/{id}/autoscaler/simulate")

@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ResourceFilters(SupervisorResourceFilter.class)
public Response simulateAutoscaling(
@PathParam("id") String supervisorId,
CostBasedAutoScalerConfig autoScalerConfig,
@QueryParam("maxProcessingRatePerTask") int maxProcessingRatePerTask,
@QueryParam("criticalLag") int criticalLag,
@QueryParam("currentTaskCount") Integer currentTaskCount,
@Context HttpServletRequest request

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'request' is never used.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
)
{
return asLeaderWithSupervisorManager(
manager -> Response.ok(
manager.simulateAutoscaling(
supervisorId,
autoScalerConfig,
criticalLag,
maxProcessingRatePerTask,
currentTaskCount
)
).build()
);
}

@GET
@Path("/history")
@Produces(MediaType.APPLICATION_JSON)
Expand Down Expand Up @@ -562,7 +592,12 @@
{
if (count != null && count <= 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.of("error", StringUtils.format("Count must be greater than zero if set (count was %d)", count)))
.entity(ImmutableMap.of("error",
StringUtils.format(
"Count must be greater than zero if set (count was %d)",
count
)
))
.build();
}

Expand Down
Loading
Loading