task-67: 复用 RustFS/MinIO 客户端与 HTTP 连接池
- buildClient 改为 double-check 懒加载缓存共享 MinioClient(volatile 单例), 同一实例持有同一 OkHttpClient,连接池随实例共享,不再每次操作新建客户端 - supplier 注入路径优先且不写共享缓存,测试隔离不受污染 - 未配置/空白 endpoint 时不创建实例,拒绝操作并保持缓存为空
This commit is contained in:
+20
-6
@@ -49,6 +49,11 @@ public class RustfsObjectStorageService {
|
|||||||
private volatile long failureWindowStartedAtMillis;
|
private volatile long failureWindowStartedAtMillis;
|
||||||
private volatile long circuitOpenUntilMillis;
|
private volatile long circuitOpenUntilMillis;
|
||||||
private volatile OkHttpClient httpClient;
|
private volatile OkHttpClient httpClient;
|
||||||
|
/**
|
||||||
|
* 懒加载缓存的共享 MinioClient(double-check 单例):复用同一实例与同一
|
||||||
|
* OkHttpClient(连接池随实例共享),避免每次操作创建客户端。
|
||||||
|
*/
|
||||||
|
private volatile MinioClient sharedMinioClient;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public RustfsObjectStorageService(TransientStorageProperties properties,
|
public RustfsObjectStorageService(TransientStorageProperties properties,
|
||||||
@@ -275,12 +280,21 @@ public class RustfsObjectStorageService {
|
|||||||
if (minioClientSupplier != null) {
|
if (minioClientSupplier != null) {
|
||||||
return minioClientSupplier.get();
|
return minioClientSupplier.get();
|
||||||
}
|
}
|
||||||
return MinioClient.builder()
|
MinioClient shared = sharedMinioClient;
|
||||||
.endpoint(properties.getEndpoint())
|
if (shared != null) {
|
||||||
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
return shared;
|
||||||
.region(properties.getRegion())
|
}
|
||||||
.httpClient(getHttpClient(deadlineNanos))
|
synchronized (this) {
|
||||||
.build();
|
if (sharedMinioClient == null) {
|
||||||
|
sharedMinioClient = MinioClient.builder()
|
||||||
|
.endpoint(properties.getEndpoint())
|
||||||
|
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
||||||
|
.region(properties.getRegion())
|
||||||
|
.httpClient(getHttpClient(deadlineNanos))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return sharedMinioClient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OkHttpClient getHttpClient() {
|
OkHttpClient getHttpClient() {
|
||||||
|
|||||||
+218
@@ -0,0 +1,218 @@
|
|||||||
|
package com.nanri.aiimage.modules.file.service.object;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.config.TransientStorageProperties;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.PutObjectArgs;
|
||||||
|
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.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
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 67:复用 RustFS/MinIO 客户端与 HTTP 连接池,减少每次操作创建客户端。
|
||||||
|
* RustfsObjectStorageService 懒加载缓存共享 MinioClient(double-check 单例),
|
||||||
|
* 后续操作复用同一实例与同一 OkHttpClient(连接池随实例共享),
|
||||||
|
* 不再每次操作新建客户端;supplier 注入路径优先且不污染共享缓存;
|
||||||
|
* 未配置/非法配置时不创建实例。
|
||||||
|
*/
|
||||||
|
class RustfsClientReuseTest {
|
||||||
|
|
||||||
|
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);
|
||||||
|
// 默认无 supplier → 走共享 MinioClient 缓存路径
|
||||||
|
service = new RustfsObjectStorageService(properties, meterRegistryProvider, deleteRetryProvider, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Object sharedMinioClient() {
|
||||||
|
return ReflectionTestUtils.getField(service, "sharedMinioClient");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object buildClient(long deadlineNanos) {
|
||||||
|
return ReflectionTestUtils.invokeMethod(service, "buildClient", deadlineNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_normal_default_path() {
|
||||||
|
// 默认路径:共享缓存懒加载创建 MinioClient,第二次调用复用同一实例。
|
||||||
|
setConfigured(properties);
|
||||||
|
|
||||||
|
Object first = buildClient(Long.MAX_VALUE);
|
||||||
|
Object second = buildClient(Long.MAX_VALUE);
|
||||||
|
|
||||||
|
assertNotNull(first, "首次构建出客户端");
|
||||||
|
assertSame(first, second, "复用同一 MinioClient 实例");
|
||||||
|
assertSame(first, sharedMinioClient(), "共享缓存字段已填充且一致");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_normal_multiple_items() throws Exception {
|
||||||
|
// 批量场景:并发多操作同时构建客户端只产生一个共享实例,无实例爆炸。
|
||||||
|
setConfigured(properties);
|
||||||
|
int concurrency = 16;
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
Set<Object> instances = ConcurrentHashMap.newKeySet();
|
||||||
|
Thread[] threads = new Thread[concurrency];
|
||||||
|
for (int i = 0; i < concurrency; i++) {
|
||||||
|
threads[i] = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
start.await(2, TimeUnit.SECONDS);
|
||||||
|
instances.add(buildClient(Long.MAX_VALUE));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
threads[i].setDaemon(true);
|
||||||
|
threads[i].start();
|
||||||
|
}
|
||||||
|
start.countDown();
|
||||||
|
for (Thread thread : threads) {
|
||||||
|
thread.join(5_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, instances.size(), "并发构建只产生一个共享实例");
|
||||||
|
assertNotNull(sharedMinioClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_normal_repeated_operation_is_idempotent() {
|
||||||
|
// 幂等:重复调用不创建重复客户端,共享实例引用稳定。
|
||||||
|
setConfigured(properties);
|
||||||
|
|
||||||
|
Object first = buildClient(Long.MAX_VALUE);
|
||||||
|
Object second = buildClient(Long.MAX_VALUE);
|
||||||
|
Object third = buildClient(Long.MAX_VALUE);
|
||||||
|
|
||||||
|
assertSame(first, second);
|
||||||
|
assertSame(second, third);
|
||||||
|
assertSame(first, sharedMinioClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_boundary_empty_input() {
|
||||||
|
// 空输入:未配置时操作直接拒绝,不创建任何客户端实例。
|
||||||
|
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||||
|
() -> service.uploadText("k", "v"));
|
||||||
|
org.junit.jupiter.api.Assertions.assertTrue(ex.getMessage().contains("not configured"));
|
||||||
|
assertNull(sharedMinioClient(), "未配置不创建客户端");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_boundary_single_item() {
|
||||||
|
// 单元素:单次构建创建一次并缓存,不依赖批量路径。
|
||||||
|
setConfigured(properties);
|
||||||
|
|
||||||
|
Object built = buildClient(Long.MAX_VALUE);
|
||||||
|
|
||||||
|
assertNotNull(built);
|
||||||
|
assertSame(built, sharedMinioClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_boundary_limit_and_overflow() throws Exception {
|
||||||
|
// 上限/超限:更大并发下仍只创建一个共享实例;
|
||||||
|
// HTTP 客户端(含连接池)同样复用同一实例。
|
||||||
|
setConfigured(properties);
|
||||||
|
int concurrency = 32;
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
Set<Object> instances = ConcurrentHashMap.newKeySet();
|
||||||
|
Thread[] threads = new Thread[concurrency];
|
||||||
|
for (int i = 0; i < concurrency; i++) {
|
||||||
|
threads[i] = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
start.await(2, TimeUnit.SECONDS);
|
||||||
|
instances.add(buildClient(Long.MAX_VALUE));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
threads[i].setDaemon(true);
|
||||||
|
threads[i].start();
|
||||||
|
}
|
||||||
|
start.countDown();
|
||||||
|
for (Thread thread : threads) {
|
||||||
|
thread.join(5_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, instances.size(), "高并发下不产生客户端实例爆炸");
|
||||||
|
assertSame(service.getHttpClient(), service.getHttpClient(), "HTTP 客户端复用同一实例(连接池共享)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_invalid_input_rejected() {
|
||||||
|
// 非法参数:endpoint 为空白时视为未配置,拒绝操作且不创建客户端。
|
||||||
|
properties.setEndpoint(" ");
|
||||||
|
properties.setBucket("bucket");
|
||||||
|
properties.setAccessKeyId("ak");
|
||||||
|
properties.setAccessKeySecret("sk");
|
||||||
|
|
||||||
|
IllegalStateException ex = assertThrows(IllegalStateException.class,
|
||||||
|
() -> service.uploadText("k", "v"));
|
||||||
|
org.junit.jupiter.api.Assertions.assertTrue(ex.getMessage().contains("not configured"));
|
||||||
|
assertNull(sharedMinioClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_task_067_rustfs_dependency_failure_releases_resources() throws Exception {
|
||||||
|
// 依赖失败:supplier 注入路径优先且失败后可恢复,
|
||||||
|
// 不污染共享缓存(sharedMinioClient 保持未初始化)。
|
||||||
|
RustfsObjectStorageService supplierService = new RustfsObjectStorageService(
|
||||||
|
properties, meterRegistryProvider, deleteRetryProvider, () -> client);
|
||||||
|
setConfigured(properties);
|
||||||
|
properties.setUploadMaxRetries(1);
|
||||||
|
when(client.putObject(ArgumentMatchers.any(PutObjectArgs.class)))
|
||||||
|
.thenThrow(new java.io.IOException("rustfs down"));
|
||||||
|
assertThrows(Exception.class, () -> supplierService.uploadText("k", "v"));
|
||||||
|
|
||||||
|
when(client.putObject(ArgumentMatchers.any(PutObjectArgs.class)))
|
||||||
|
.thenReturn(mock(io.minio.ObjectWriteResponse.class));
|
||||||
|
when(client.statObject(ArgumentMatchers.any(io.minio.StatObjectArgs.class))).thenReturn(null);
|
||||||
|
assertEquals("k", supplierService.uploadText("k", "v"));
|
||||||
|
verify(client, times(2)).putObject(ArgumentMatchers.any(PutObjectArgs.class));
|
||||||
|
assertNull(ReflectionTestUtils.getField(supplierService, "sharedMinioClient"),
|
||||||
|
"supplier 路径不写共享缓存");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user