Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -965,5 +965,7 @@ private PipeMessages() {}
public static final String MESSAGE_DATAPARTITIONTABLE_GENERATION_COMPLETED_SUCCESSFULLY_E076E3B2 = "DataPartitionTable generation completed successfully";
public static final String MESSAGE_DATAPARTITIONTABLE_GENERATION_FAILED_D85CD23A = "DataPartitionTable generation failed: ";
public static final String MESSAGE_UNKNOWN_TASK_STATUS_E05D98F0 = "Unknown task status: ";
public static final String LOG_PIPE_RATE_LIMIT_IS_SMALLER_THAN_THE_SINGLE_REQUEST_SIZE_SO_ARG_BYTES_WILL_BE_TRANSFERRED_IN_ARG_SIZED_PIECES_AT_ARG_BYTES_PER_SECOND_CONSIDER_SETTING_A_LARGER_RATE_LIMIT_FOR_BETTER_PERFORMANCE_4450D4DC =
"Pipe rate limit is smaller than the single request size, so %s bytes will be transferred in %s-sized pieces at %s bytes per second. Consider setting a larger rate limit for better performance.";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] Use SLF4J placeholders in both locale templates

This constant is passed directly to LOGGER.warn(message, bytes, piece, rate), which substitutes {} rather than %s. The added tests actually print this warning with all three literal %s placeholders, so the request size, piece size, and configured rate are missing from the diagnostic. Please change the placeholders to {} in both the English and Chinese templates.


}
Original file line number Diff line number Diff line change
Expand Up @@ -931,5 +931,7 @@ private PipeMessages() {}
public static final String MESSAGE_DATAPARTITIONTABLE_GENERATION_COMPLETED_SUCCESSFULLY_E076E3B2 = "DataPartitionTable 生成已成功完成";
public static final String MESSAGE_DATAPARTITIONTABLE_GENERATION_FAILED_D85CD23A = "DataPartitionTable 生成失败:";
public static final String MESSAGE_UNKNOWN_TASK_STATUS_E05D98F0 = "未知任务状态:";
public static final String LOG_PIPE_RATE_LIMIT_IS_SMALLER_THAN_THE_SINGLE_REQUEST_SIZE_SO_ARG_BYTES_WILL_BE_TRANSFERRED_IN_ARG_SIZED_PIECES_AT_ARG_BYTES_PER_SECOND_CONSIDER_SETTING_A_LARGER_RATE_LIMIT_FOR_BETTER_PERFORMANCE_4450D4DC =
"Pipe 限速值小于单次请求大小,因此 %s 字节将按 %s 大小的分片以 %s 字节每秒的速度传输。建议设置更大的限速值以获得更好的性能。";

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import com.google.common.util.concurrent.AtomicDouble;
import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.TimeUnit;

public abstract class GlobalRateLimiter {

private final AtomicDouble throughputBytesPerSecond =
Expand All @@ -47,26 +45,11 @@ public void acquire(long bytes) {
return;
}

while (bytes > 0) {
if (bytes > Integer.MAX_VALUE) {
tryAcquireWithRateCheck(Integer.MAX_VALUE);
bytes -= Integer.MAX_VALUE;
} else {
tryAcquireWithRateCheck((int) bytes);
return;
}
}
}

private void tryAcquireWithRateCheck(final int bytes) {
while (!rateLimiter.tryAcquire(
RateLimitUtils.acquireWithRateLimit(
rateLimiter,
bytes,
PipeConfig.getInstance().getRateLimiterHotReloadCheckIntervalMs(),
TimeUnit.MILLISECONDS)) {
if (reloadParams()) {
return;
}
}
() -> !reloadParams(),
PipeConfig.getInstance().getRateLimiterHotReloadCheckIntervalMs());
}

private boolean reloadParams() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

public class PipeEndPointRateLimiter {

Expand Down Expand Up @@ -64,30 +63,15 @@ public void acquire(final TEndPoint endPoint, long bytes) {
endPointRateLimiterMap.computeIfAbsent(
endPoint, e -> RateLimiter.create(bytesPerSecondLimit));

while (bytes > 0) {
if (bytes > Integer.MAX_VALUE) {
if (!tryAcquireWithPipeCheck(rateLimiter, Integer.MAX_VALUE)) {
return;
}
bytes -= Integer.MAX_VALUE;
} else {
tryAcquireWithPipeCheck(rateLimiter, (int) bytes);
return;
}
}
RateLimitUtils.acquireWithRateLimit(
rateLimiter,
bytes,
() -> isPipeExisted(),
PipeConfig.getInstance().getRateLimiterHotReloadCheckIntervalMs());
}

private boolean tryAcquireWithPipeCheck(final RateLimiter rateLimiter, final int bytes) {
while (!rateLimiter.tryAcquire(
bytes,
PipeConfig.getInstance().getRateLimiterHotReloadCheckIntervalMs(),
TimeUnit.MILLISECONDS)) {
final PipeTaskAgent finalTaskAgent = taskAgent;
if (Objects.nonNull(finalTaskAgent)
&& !finalTaskAgent.isPipeExisted(pipeName, creationTime)) {
return false;
}
}
return true;
private boolean isPipeExisted() {
final PipeTaskAgent finalTaskAgent = taskAgent;
return Objects.isNull(finalTaskAgent) || finalTaskAgent.isPipeExisted(pipeName, creationTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* 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.iotdb.commons.pipe.sink.limiter;

import org.apache.iotdb.commons.i18n.PipeMessages;

import com.google.common.util.concurrent.RateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
* Bridges a byte-based request to {@link RateLimiter}, which is a permit-based limiter.
*
* <p>{@link RateLimiter#create(double)} builds a {@code SmoothBursty} limiter whose burst capacity
* is at most one second's worth of permits, and {@link RateLimiter#tryAcquire(int, long, TimeUnit)}
* only grants a request when its whole cost fits inside the given timeout. When a single request
* costs more than the timeout, every attempt fails instantly and the surrounding retry loop
* degenerates into a 100% CPU busy-wait that never logs anything. Such a request must therefore
* never be passed to {@code tryAcquire} in one piece, which is what this class takes care of.
*/
public class RateLimitUtils {

private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitUtils.class);

/** The default value of {@code RateLimiterHotReloadCheckIntervalMs}. */
public static final long DEFAULT_CHECK_INTERVAL_MS = 1000;

// The loop below is a busy-wait guard as well: a healthy chunk needs at most one timed attempt
// before it falls back to a blocking acquisition, so exceeding this budget means something is
// wrong (e.g. the limiter is stuck) rather than that we merely have to wait longer.
private static final int MAX_ATTEMPTS_PER_CHUNK = 20;

private RateLimitUtils() {
// Utility class
}

/** Decides whether to keep waiting for the rate limiter. */
@FunctionalInterface
public interface ContinueChecker {
/**
* @return {@code true} to keep acquiring, {@code false} to abort the whole acquisition
*/
boolean shouldContinue();
}

/**
* Acquires {@code bytes} permits from {@code rateLimiter}, checking the limiter every {@link
* #DEFAULT_CHECK_INTERVAL_MS}.
*
* @param rateLimiter the permit-based limiter to acquire from
* @param bytes the number of bytes to acquire, non-positive values return immediately
* @param continueChecker consulted between pieces to stop waiting for a dead pipe
* @return {@code true} if the whole request was acquired, {@code false} if {@code
* continueChecker} asked to abort
*/
public static boolean acquireWithRateLimit(
final RateLimiter rateLimiter, final long bytes, final ContinueChecker continueChecker) {
return acquireWithRateLimit(rateLimiter, bytes, continueChecker, DEFAULT_CHECK_INTERVAL_MS);
}

/**
* Acquires {@code bytes} permits from {@code rateLimiter}.
*
* <p>The request is split into pieces that the burst capacity can actually grant, each piece is
* acquired with a timeout that covers its own cost, and a piece that keeps timing out (which can
* only happen when the limiter's rate is reloaded downwards) falls back to a blocking acquisition
* so that the caller stops burning CPU. The average throughput always converges to the configured
* rate.
*
* @param rateLimiter the permit-based limiter to acquire from
* @param bytes the number of bytes to acquire, non-positive values return immediately
* @param continueChecker consulted between pieces and on a timeout, to stop waiting for a dead
* pipe
* @param checkIntervalMs how often {@code continueChecker} is consulted while waiting
* @return {@code true} if the whole request was acquired, {@code false} if {@code
* continueChecker} asked to abort
*/
public static boolean acquireWithRateLimit(
final RateLimiter rateLimiter,
final long bytes,
final ContinueChecker continueChecker,
final long checkIntervalMs) {
final long boundedCheckIntervalMs = Math.max(1L, checkIntervalMs);

long remaining = bytes;
boolean logPipeSizeMismatch = true;

while (remaining > 0) {
// A request that fits into a single piece is served by the limiter's initial burst without
// waiting, so the checker only matters for requests that need several pieces, i.e. the ones
// that actually take time to transfer.
if (remaining < bytes && !continueChecker.shouldContinue()) {
return false;
}

final long piece = calculatePieceSize(rateLimiter, remaining);
if (logPipeSizeMismatch && piece < remaining) {
logPipeSizeMismatch = false;
LOGGER.warn(
PipeMessages
.LOG_PIPE_RATE_LIMIT_IS_SMALLER_THAN_THE_SINGLE_REQUEST_SIZE_SO_ARG_BYTES_WILL_BE_TRANSFERRED_IN_ARG_SIZED_PIECES_AT_ARG_BYTES_PER_SECOND_CONSIDER_SETTING_A_LARGER_RATE_LIMIT_FOR_BETTER_PERFORMANCE_4450D4DC,
bytes,
piece,
rateLimiter.getRate());
}

final long timeoutMs = calculateTimeoutMs(rateLimiter, piece, boundedCheckIntervalMs);
int attempts = 0;
boolean acquired = false;
while (!acquired) {
acquired = rateLimiter.tryAcquire((int) piece, timeoutMs, TimeUnit.MILLISECONDS);
if (acquired) {
break;
}

if (!continueChecker.shouldContinue()) {
return false;
}

if (++attempts >= MAX_ATTEMPTS_PER_CHUNK) {
// The rate has most likely been reloaded downwards while we were waiting, so our
// estimate is far too optimistic. Wait for the permits without a deadline instead of
// retrying with a deadline that we now know we cannot meet, which would only spin.
rateLimiter.acquire((int) piece);
Comment on lines +139 to +143

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Keep cancellation and reload checks active under contention

Concurrent callers can reach this fallback without any rate change: Guava returns false immediately when the earliest available permit is beyond the timeout, so all 20 failures can occur without waiting. acquire() then reserves a slot and sleeps uninterruptibly, with no further continueChecker calls during that wait. Because the endpoint and global limiters are shared across callers, a dropped pipe or a hot reload that disables limiting can leave workers waiting far beyond the configured check interval.

Reproduced with eight threads sharing a 1-byte/s limiter, each requesting one byte, and a 1000 ms check interval. After setting the checker to false and interrupting the workers, five threads were still blocked in this acquire() 2.5 seconds later; the original timed retry loop had released every worker. The delay grows with the number of queued callers. Please retain bounded waits and periodic checker calls, with backoff to prevent spinning, and add a contention/cancellation regression test.

acquired = true;
}
}

remaining -= piece;
}

return true;
}

/**
* @return a piece size that is small enough for the limiter's burst capacity to grant
*/
private static long calculatePieceSize(final RateLimiter rateLimiter, final long bytes) {
// The burst capacity of RateLimiter.create(rate) is one second's worth of permits, so anything
// larger than that can never be granted by a single timeout-bounded attempt.
final double bytesPerSecond = rateLimiter.getRate();
if (!(bytesPerSecond > 0) || bytesPerSecond >= Integer.MAX_VALUE) {
// Rate limiting is effectively disabled (e.g. the rate is set to Double.MAX_VALUE), but the
// piece still has to stay within the int range of the limiter's API.
return Math.min(bytes, Integer.MAX_VALUE);
}
return Math.min(bytes, (long) bytesPerSecond);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Ensure positive requests produce at least one permit

Positive rates below 1 byte/s are currently accepted by the sink configuration. For example, sink.rate-limit-bytes-per-second='0.5' passes validation, but (long) bytesPerSecond becomes 0 here. Any positive request then calls tryAcquire(0, ...) and throws IllegalArgumentException: Requested permits (0) must be positive, so the pipe cannot transfer data at that accepted rate. This is also reproducible directly with acquireWithRateLimit(RateLimiter.create(0.5), 1, () -> true, 1000); the original implementation accepts the first one-byte request. Please preserve a positive piece size for these rates and cover this boundary in a regression test.

}

/**
* @return a timeout that covers the expected wait for {@code piece}, so that a legitimate wait is
* not mistaken for a timeout
*/
private static long calculateTimeoutMs(
final RateLimiter rateLimiter, final long piece, final long checkIntervalMs) {
final double rate = rateLimiter.getRate();
if (!(rate > 0)) {
return checkIntervalMs;
}

final double expectedWaitMs = piece / rate * 1000.0;
if (!(expectedWaitMs > 0) || expectedWaitMs >= Long.MAX_VALUE - checkIntervalMs) {
return checkIntervalMs;
}

return Math.max(checkIntervalMs, (long) expectedWaitMs + checkIntervalMs);
}
}
Loading
Loading