task-66: 限制 RustFS 并发读写与重试的总资源预算
- 新增 aiimage.transient-storage.max-total-concurrent-operations 配置 (默认 0 不启用):跨 upload/read/delete 的总并发预算 - executeWithRetry 入口获取一个总许可,整次操作(含全部重试)全程占用, 重试不额外消耗,超预算立即拒绝并计入 operation=total rejected 指标 - 失败/完成后 finally 释放总许可,无残留锁;负值/0 视为未启用保持原行为
This commit is contained in:
@@ -23,6 +23,11 @@ public class TransientStorageProperties {
|
|||||||
private int maxConcurrentUploads = 16;
|
private int maxConcurrentUploads = 16;
|
||||||
private int maxConcurrentReads = 32;
|
private int maxConcurrentReads = 32;
|
||||||
private int maxConcurrentDeletes = 8;
|
private int maxConcurrentDeletes = 8;
|
||||||
|
/**
|
||||||
|
* 跨操作类型的总并发预算(含重试期间):一次操作全程占用一个总许可,
|
||||||
|
* 防止多任务叠加时读写与重试之和突破对后端的总压力上限。0/负值表示不启用。
|
||||||
|
*/
|
||||||
|
private long maxTotalConcurrentOperations = 0;
|
||||||
private long acquirePermitTimeoutMillis = 2000;
|
private long acquirePermitTimeoutMillis = 2000;
|
||||||
private long baseRetryDelayMillis = 500;
|
private long baseRetryDelayMillis = 500;
|
||||||
private long maxRetryDelayMillis = 5000;
|
private long maxRetryDelayMillis = 5000;
|
||||||
|
|||||||
+54
-33
@@ -35,6 +35,7 @@ public class RustfsObjectStorageService {
|
|||||||
private static final String OP_READ = "read";
|
private static final String OP_READ = "read";
|
||||||
private static final String OP_DELETE = "delete";
|
private static final String OP_DELETE = "delete";
|
||||||
private static final String OP_STAT = "stat";
|
private static final String OP_STAT = "stat";
|
||||||
|
private static final String OP_TOTAL = "total";
|
||||||
|
|
||||||
private final TransientStorageProperties properties;
|
private final TransientStorageProperties properties;
|
||||||
private final ObjectProvider<MeterRegistry> meterRegistryProvider;
|
private final ObjectProvider<MeterRegistry> meterRegistryProvider;
|
||||||
@@ -43,6 +44,7 @@ public class RustfsObjectStorageService {
|
|||||||
private final Semaphore uploadSemaphore;
|
private final Semaphore uploadSemaphore;
|
||||||
private final Semaphore readSemaphore;
|
private final Semaphore readSemaphore;
|
||||||
private final Semaphore deleteSemaphore;
|
private final Semaphore deleteSemaphore;
|
||||||
|
private final Semaphore totalSemaphore;
|
||||||
private final AtomicInteger windowFailureCount = new AtomicInteger();
|
private final AtomicInteger windowFailureCount = new AtomicInteger();
|
||||||
private volatile long failureWindowStartedAtMillis;
|
private volatile long failureWindowStartedAtMillis;
|
||||||
private volatile long circuitOpenUntilMillis;
|
private volatile long circuitOpenUntilMillis;
|
||||||
@@ -66,6 +68,13 @@ public class RustfsObjectStorageService {
|
|||||||
this.uploadSemaphore = new Semaphore(Math.max(1, properties.getMaxConcurrentUploads()));
|
this.uploadSemaphore = new Semaphore(Math.max(1, properties.getMaxConcurrentUploads()));
|
||||||
this.readSemaphore = new Semaphore(Math.max(1, properties.getMaxConcurrentReads()));
|
this.readSemaphore = new Semaphore(Math.max(1, properties.getMaxConcurrentReads()));
|
||||||
this.deleteSemaphore = new Semaphore(Math.max(1, properties.getMaxConcurrentDeletes()));
|
this.deleteSemaphore = new Semaphore(Math.max(1, properties.getMaxConcurrentDeletes()));
|
||||||
|
// 总预算为 0/负值时不启用(Semaphore(0) 的 tryAcquire 永远失败,须用启用开关区分)
|
||||||
|
long totalBudget = Math.max(0L, properties.getMaxTotalConcurrentOperations());
|
||||||
|
this.totalSemaphore = new Semaphore((int) Math.min(totalBudget, Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTotalBudgetEnabled() {
|
||||||
|
return properties.getMaxTotalConcurrentOperations() > 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConfigured() {
|
public boolean isConfigured() {
|
||||||
@@ -185,45 +194,57 @@ public class RustfsObjectStorageService {
|
|||||||
CheckedSupplier<T> supplier) {
|
CheckedSupplier<T> supplier) {
|
||||||
long startedAt = System.nanoTime();
|
long startedAt = System.nanoTime();
|
||||||
Exception last = null;
|
Exception last = null;
|
||||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
// 总资源预算:一次操作(含全部重试)全程占用一个总许可,重试不额外消耗。
|
||||||
checkDeadline(operation, objectKey, deadlineNanos);
|
boolean totalAcquired = false;
|
||||||
rejectIfCircuitOpen(operation, objectKey);
|
if (isTotalBudgetEnabled()) {
|
||||||
acquirePermit(operation, objectKey, semaphore, deadlineNanos);
|
acquirePermit(OP_TOTAL, objectKey, totalSemaphore, deadlineNanos);
|
||||||
long delayMillis = 0L;
|
totalAcquired = true;
|
||||||
try {
|
}
|
||||||
|
try {
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
checkDeadline(operation, objectKey, deadlineNanos);
|
||||||
|
rejectIfCircuitOpen(operation, objectKey);
|
||||||
|
acquirePermit(operation, objectKey, semaphore, deadlineNanos);
|
||||||
|
long delayMillis = 0L;
|
||||||
try {
|
try {
|
||||||
checkDeadline(operation, objectKey, deadlineNanos);
|
try {
|
||||||
T result = supplier.get();
|
checkDeadline(operation, objectKey, deadlineNanos);
|
||||||
checkDeadline(operation, objectKey, deadlineNanos);
|
T result = supplier.get();
|
||||||
if (!OP_UPLOAD.equals(operation)) {
|
checkDeadline(operation, objectKey, deadlineNanos);
|
||||||
resetFailureWindow(operation);
|
if (!OP_UPLOAD.equals(operation)) {
|
||||||
}
|
resetFailureWindow(operation);
|
||||||
recordOperation(operation, "success", elapsedNanos(startedAt));
|
}
|
||||||
log.debug("[rustfs] operation success operation={} objectKey={} attempt={}/{} durationMs={} bytes={}",
|
recordOperation(operation, "success", elapsedNanos(startedAt));
|
||||||
operation, objectKey, attempt, maxRetries, elapsedMillis(startedAt), bytes);
|
log.debug("[rustfs] operation success operation={} objectKey={} attempt={}/{} durationMs={} bytes={}",
|
||||||
return result;
|
operation, objectKey, attempt, maxRetries, elapsedMillis(startedAt), bytes);
|
||||||
} catch (Exception ex) {
|
return result;
|
||||||
last = ex;
|
} catch (Exception ex) {
|
||||||
recordOperation(operation, attempt < maxRetries ? "retry" : "failure", elapsedNanos(startedAt));
|
last = ex;
|
||||||
recordFailure(operation, objectKey, ex);
|
recordOperation(operation, attempt < maxRetries ? "retry" : "failure", elapsedNanos(startedAt));
|
||||||
if (attempt < maxRetries) {
|
recordFailure(operation, objectKey, ex);
|
||||||
delayMillis = retryDelayMillis(attempt);
|
if (attempt < maxRetries) {
|
||||||
log.warn("[rustfs] operation failed, retrying operation={} objectKey={} attempt={}/{} delayMs={} err={}",
|
delayMillis = retryDelayMillis(attempt);
|
||||||
operation, objectKey, attempt, maxRetries, delayMillis, ex.getMessage());
|
log.warn("[rustfs] operation failed, retrying operation={} objectKey={} attempt={}/{} delayMs={} err={}",
|
||||||
|
operation, objectKey, attempt, maxRetries, delayMillis, ex.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
semaphore.release();
|
||||||
|
}
|
||||||
|
if (last instanceof OperationTimeoutException timeoutException) {
|
||||||
|
throw timeoutException;
|
||||||
|
}
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
sleepQuietly(delayMillis, "retrying rustfs " + operation,
|
||||||
|
operation, objectKey, deadlineNanos);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
semaphore.release();
|
|
||||||
}
|
}
|
||||||
if (last instanceof OperationTimeoutException timeoutException) {
|
throw new IllegalStateException("failed to " + operation + " payload in transient storage", last);
|
||||||
throw timeoutException;
|
} finally {
|
||||||
}
|
if (totalAcquired) {
|
||||||
if (attempt < maxRetries) {
|
totalSemaphore.release();
|
||||||
sleepQuietly(delayMillis, "retrying rustfs " + operation,
|
|
||||||
operation, objectKey, deadlineNanos);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("failed to " + operation + " payload in transient storage", last);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acquirePermit(String operation, String objectKey, Semaphore semaphore, long deadlineNanos) {
|
private void acquirePermit(String operation, String objectKey, Semaphore semaphore, long deadlineNanos) {
|
||||||
|
|||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.service.object;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.config.TransientStorageProperties;
|
||||||
|
import io.micrometer.core.instrument.Counter;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
|
import io.minio.GetObjectArgs;
|
||||||
|
import io.minio.GetObjectResponse;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.PutObjectArgs;
|
||||||
|
import io.minio.StatObjectArgs;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task 66:限制 RustFS 并发读写与重试的总资源预算,防止多任务叠加爆发。
|
||||||
|
* RustfsObjectStorageService 在分类信号量(upload/read/delete)之上增加总并发预算
|
||||||
|
* maxTotalConcurrentOperations:单次操作(含重试)全程占用一个总许可,
|
||||||
|
* 超预算立即拒绝(rejected 指标),失败/完成后释放,避免多任务叠加时
|
||||||
|
* 读写与重试叠加突破对后端的总压力上限。
|
||||||
|
*/
|
||||||
|
class RustfsTotalBudgetTest {
|
||||||
|
|
||||||
|
private TransientStorageProperties properties;
|
||||||
|
private SimpleMeterRegistry simpleRegistry;
|
||||||
|
private MinioClient client;
|
||||||
|
private ObjectProvider<MeterRegistry> meterRegistryProvider;
|
||||||
|
private ObjectProvider<RustfsDeleteRetryService> deleteRetryProvider;
|
||||||
|
private RustfsObjectStorageService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void setUp() {
|
||||||
|
properties = new TransientStorageProperties();
|
||||||
|
simpleRegistry = new SimpleMeterRegistry();
|
||||||
|
meterRegistryProvider = mock(ObjectProvider.class);
|
||||||
|
when(meterRegistryProvider.getIfAvailable()).thenReturn(simpleRegistry);
|
||||||
|
deleteRetryProvider = mock(ObjectProvider.class);
|
||||||
|
client = mock(MinioClient.class);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
simpleRegistry.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setConfigured(TransientStorageProperties properties) {
|
||||||
|
properties.setEndpoint("http://rustfs.local:9000");
|
||||||
|
properties.setBucket("bucket");
|
||||||
|
properties.setAccessKeyId("ak");
|
||||||
|
properties.setAccessKeySecret("sk");
|
||||||
|
properties.setRegion("us-east-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stubUploadSuccess() {
|
||||||
|
try {
|
||||||
|
when(client.putObject(ArgumentMatchers.any(PutObjectArgs.class)))
|
||||||
|
.thenReturn(mock(io.minio.ObjectWriteResponse.class));
|
||||||
|
when(client.statObject(ArgumentMatchers.any(StatObjectArgs.class))).thenReturn(null);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stubReadSuccess(String content) throws Exception {
|
||||||
|
GetObjectResponse response = mock(GetObjectResponse.class);
|
||||||
|
when(response.readAllBytes()).thenReturn(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
when(client.getObject(ArgumentMatchers.any(GetObjectArgs.class))).thenReturn(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Semaphore totalSemaphore() {
|
||||||
|
return (Semaphore) ReflectionTestUtils.getField(service, "totalSemaphore");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long rejectedTotal(MeterRegistry registry) {
|
||||||
|
Counter counter = registry.find("aiimage.rustfs.operation.total")
|
||||||
|
.tags("operation", "total", "result", "rejected").counter();
|
||||||
|
return counter == null ? 0L : (long) counter.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_normal_default_path() throws Exception {
|
||||||
|
// 默认路径:配置总预算后正常上传成功,操作结束后总许可释放回池。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(8);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
stubUploadSuccess();
|
||||||
|
|
||||||
|
String objectKey = service.uploadText("task-parsed/test/1/scope/latest.json", "{\"a\":1}");
|
||||||
|
|
||||||
|
assertEquals("task-parsed/test/1/scope/latest.json", objectKey);
|
||||||
|
assertEquals(8, totalSemaphore().availablePermits(), "上传完成后总许可全部释放");
|
||||||
|
assertEquals(0L, rejectedTotal(simpleRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_normal_multiple_items() throws Exception {
|
||||||
|
// 批量场景:多任务并发在总预算内全部成功,无拒绝无丢失。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(16);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
stubUploadSuccess();
|
||||||
|
int concurrency = 4;
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
AtomicInteger successCount = new AtomicInteger();
|
||||||
|
Thread[] threads = new Thread[concurrency];
|
||||||
|
for (int i = 0; i < concurrency; i++) {
|
||||||
|
final int index = i;
|
||||||
|
threads[i] = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
start.await(2, TimeUnit.SECONDS);
|
||||||
|
service.uploadText("task-parsed/test/" + index + "/scope/latest.json", "{\"i\":" + index + "}");
|
||||||
|
successCount.incrementAndGet();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
threads[i].setDaemon(true);
|
||||||
|
threads[i].start();
|
||||||
|
}
|
||||||
|
start.countDown();
|
||||||
|
for (Thread thread : threads) {
|
||||||
|
thread.join(5_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(concurrency, successCount.get(), "全部并发任务成功");
|
||||||
|
verify(client, times(concurrency)).putObject(ArgumentMatchers.any(PutObjectArgs.class));
|
||||||
|
assertEquals(16, totalSemaphore().availablePermits());
|
||||||
|
assertEquals(0L, rejectedTotal(simpleRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_normal_repeated_operation_is_idempotent() throws Exception {
|
||||||
|
// 幂等:总预算 1 时串行重复操作每次都能获取并释放许可,不产生重复拒绝。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(1);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
stubUploadSuccess();
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
assertEquals("task-parsed/test/1/scope/latest.json",
|
||||||
|
service.uploadText("task-parsed/test/1/scope/latest.json", "{\"x\":1}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(client, times(3)).putObject(ArgumentMatchers.any(PutObjectArgs.class));
|
||||||
|
assertEquals(1, totalSemaphore().availablePermits(), "每次操作后许可释放");
|
||||||
|
assertEquals(0L, rejectedTotal(simpleRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_boundary_empty_input() throws Exception {
|
||||||
|
// 空输入:总预算未配置(0)时保持原行为,不启用总量限制。
|
||||||
|
setConfigured(properties);
|
||||||
|
stubUploadSuccess();
|
||||||
|
|
||||||
|
assertEquals("k", service.uploadText("k", "v"));
|
||||||
|
|
||||||
|
verify(client, times(1)).putObject(ArgumentMatchers.any(PutObjectArgs.class));
|
||||||
|
assertEquals(0L, rejectedTotal(simpleRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_boundary_single_item() throws Exception {
|
||||||
|
// 单元素:总预算 1 时单次读取成功,许可释放,跨操作类型互不残留。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(1);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
stubReadSuccess("{}");
|
||||||
|
|
||||||
|
assertEquals("{}", service.readObjectAsString("task-parsed/test/1/scope/latest.json"));
|
||||||
|
assertEquals(1, totalSemaphore().availablePermits());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_boundary_limit_and_overflow() throws Exception {
|
||||||
|
// 上限/超限:总预算 1 时第二个并发操作(不同分类)立即被拒绝,
|
||||||
|
// rejected 指标 +1,不发生无界排队;操作结束后许可释放。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(1);
|
||||||
|
properties.setAcquirePermitTimeoutMillis(0);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
stubUploadSuccess();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
org.mockito.Mockito.doAnswer(invocation -> {
|
||||||
|
entered.countDown();
|
||||||
|
Thread.sleep(500);
|
||||||
|
throw new java.io.IOException("hold");
|
||||||
|
}).when(client).putObject(ArgumentMatchers.any(PutObjectArgs.class));
|
||||||
|
|
||||||
|
Thread first = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
service.uploadText("block", "v");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
first.setDaemon(true);
|
||||||
|
first.start();
|
||||||
|
try {
|
||||||
|
assertTrue(entered.await(2, TimeUnit.SECONDS), "首线程已持有总预算");
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> service.readObjectAsString("blocked"),
|
||||||
|
"跨操作类型并发超预算被拒绝");
|
||||||
|
assertEquals(1L, rejectedTotal(simpleRegistry), "拒绝计入 total rejected 指标");
|
||||||
|
first.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_invalid_input_rejected() throws Exception {
|
||||||
|
// 非法参数:总预算为负或 0 时视为未启用(构造不崩溃,行为与默认一致)。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(-5);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
stubUploadSuccess();
|
||||||
|
|
||||||
|
assertEquals("k", service.uploadText("k", "v"));
|
||||||
|
assertEquals(0L, rejectedTotal(simpleRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_066_rustfs_dependency_failure_releases_resources() throws Exception {
|
||||||
|
// 依赖失败:上传失败后总许可释放(错误可恢复),恢复后操作成功且无残留锁。
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setMaxTotalConcurrentOperations(1);
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider,
|
||||||
|
() -> client);
|
||||||
|
try {
|
||||||
|
when(client.putObject(ArgumentMatchers.any(PutObjectArgs.class)))
|
||||||
|
.thenThrow(new java.io.IOException("rustfs down"));
|
||||||
|
assertThrows(Exception.class, () -> service.uploadText("task-parsed/test/1/scope/latest.json", "{\"a\":1}"));
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
stubUploadSuccess();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(1, totalSemaphore().availablePermits(), "失败后总许可释放");
|
||||||
|
|
||||||
|
assertEquals("task-parsed/test/1/scope/latest.json",
|
||||||
|
service.uploadText("task-parsed/test/1/scope/latest.json", "{\"b\":2}"));
|
||||||
|
assertEquals(1, totalSemaphore().availablePermits());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user