fix(临时存储): RustFS 上传失败改为直接失败,不再静默回落 local 指针

多实例/容器化部署下 local 指针只有写入它的实例能读(跨节点读直接报错、容器重建即丢),
此前上传失败静默降级会把跨节点不可读的脏指针落库,故障延后到其它节点的合并/组装才爆。
现改为在写失败点抛 BusinessException(中文原因透传调用方),并新增
fallback-to-local-on-error 开关(默认 false)供单机部署回退旧行为;
跨实例读 local 指针的报错改中文并带 objectKey;超限回落策略不变。
This commit is contained in:
2026-09-14 02:31:16 +08:00
parent 3f5a234c59
commit c448f49e30
4 changed files with 51 additions and 9 deletions
@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.task.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.InstanceMetadata;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.config.TransientStorageProperties;
@@ -275,16 +276,39 @@ class TransientPayloadStorageServiceTest {
assertThrows(IllegalStateException.class, () -> service.resolvePayload(pointer, "read failed"));
}
@Test
void uploadFailureFailsFastByDefaultWithoutLocalPointer() {
// 默认(fallbackToLocalOnError=false):上传失败直接抛业务异常,不写本地文件、
// 不置 fallback 标记、不记降级指标——避免产生跨实例不可读的 local 脏指针。
RustfsObjectStorageService rustfs = mock(RustfsObjectStorageService.class);
when(rustfs.isConfigured()).thenReturn(true);
when(rustfs.uploadBytes(anyString(), any(byte[].class), anyBoolean()))
.thenThrow(new IllegalStateException("rustfs upload down"));
TransientPayloadStorageService service = newService(rustfs, propertiesWithLimit(1024, 4096, true));
BusinessException ex = assertThrows(BusinessException.class,
() -> service.storeChunkPayload("TEST", 9L, "scope", 1, "{\"a\":1}"));
assertTrue(ex.getMessage().contains("临时存储(RustFS)不可用"));
assertTrue(ex.getMessage().contains("objectKey=task-chunk/test/9/scope/chunk-1.json"));
assertFalse(service.wasLastStoreLocalFallback());
verify(rustfs, never()).recordLocalFallback();
assertFalse(java.nio.file.Files.exists(tempDir.resolve("transient-payload")),
"失败时不应落任何本地临时文件");
}
@Test
void test_task_064_payload_compression_dependency_failure_releases_resources() {
// 依赖失败:上传失败回落本地且标记 fallback;随后上传恢复重新走 rustfs
// 失败期间无残留文件泄漏(本地文件可被清理),错误路径不产生重复对象。
// 依赖失败:开关显式打开(单机降级模式)时上传失败回落本地且标记 fallback;
// 随后上传恢复重新走 rustfs失败期间无残留文件泄漏(本地文件可被清理),错误路径不产生重复对象。
RustfsObjectStorageService rustfs = mock(RustfsObjectStorageService.class);
when(rustfs.isConfigured()).thenReturn(true);
when(rustfs.uploadBytes(anyString(), any(byte[].class), anyBoolean()))
.thenThrow(new IllegalStateException("rustfs upload down"))
.thenAnswer(invocation -> invocation.getArgument(0));
TransientPayloadStorageService service = newService(rustfs, propertiesWithLimit(1024, 4096, true));
TransientStorageProperties properties = propertiesWithLimit(1024, 4096, true);
properties.setFallbackToLocalOnError(true);
TransientPayloadStorageService service = newService(rustfs, properties);
String fallbackPointer = service.storeParsedPayloadFast("TEST", 1L, "scope", "{\"a\":1}", false);
assertTrue(fallbackPointer.startsWith("local:"));