多提供者回退链(save-section-fallback)
基本信息
| 属性 | 值 |
|---|---|
| 实现类 | cubicchunks.regionlib.api.storage.SaveSection<S extends SaveSection<S, K>, K extends IKey<K>>(抽象类) |
| 持有物 | private final List<IRegionProvider<K>> regionProviders(顺序即优先级) |
| 构造 1 | SaveSection(IRegionProvider<K>) → this(Arrays.asList(regionProvider)) |
| 构造 2 | SaveSection(List<IRegionProvider<K>>) → 每个下一个 provider 作为上一个失败时的回退 |
| 线程安全 | save / load 的 javadoc 均标注"This method is thread safe" |
类 javadoc 里写明它存在的两个理由:一是数据组织方式变更时的向后兼容(老数据还在老 provider 里),二是给标准 IRegion 实现放不下的条目换一个存储方式(标准实现有大小上限)。
写入:一个 key 只落在一个 provider
save(K key, ByteBuffer value) 按 regionProviders 顺序逐个尝试:
| 步骤 | 行为 |
|---|---|
| 1 | prov.forRegion(key, r -> r.writeValue(key, value)) |
| 2a | 写入成功 → exceptions.clear() |
| 2b | 抛 UnsupportedDataException → 记进 exceptions,并立刻 r.writeValue(key, null) 擦除这次失败的半成品 |
| 3 | 该 provider 没留下异常 → toWrite = null,于是后面所有 provider 收到的都是 null,即被擦除 |
结果:key 落在第一个接受它的 provider里,其余 provider 全部被清空——保证同一个 key 不会在多个后端各存一份。
全部 provider 都拒绝时抛:
SaveSectionException("No region provider supporting key " + key + " with data size " + value.capacity(), exceptions)
SaveSectionException(String description, Collection<? extends Throwable> causes) 继承 IOException,把每个 cause 用 addSuppressed 挂上。
批量写入 save(Map<K, ByteBuffer>)
- 复制输入 map 到
pendingEntries(注释:避免改动调用方传入的 map) - 按
IKey::getRegionKey分组 → 每个区域一批 - 对每批 × 每个 provider:
r.writeValues(regionEntries) - 捕
MultiUnsupportedDataException,用getChildren()拿到Map<K, UnsupportedDataException>,把这些 key 从本批剔除、逐个记异常,再对它们writeValues(toNulls)擦除 - 本批剩下的 key 视为成功 → 清异常、置
pendingEntries.put(k, null) - 仍有异常 → 抛
SaveSectionException("multiple write errors", 每个 key 各一个 SaveSectionException)
MultiUnsupportedDataException(MultiUnsupportedDataException.java)把所有UnsupportedDataException.WithKey作为 suppressed 异常挂在自身上,getChildren()再用WithKey::getKey转回Map<K, UnsupportedDataException>。
读取:第一个应答的 provider 说了算
load(K key, boolean createRegion):
ByteBuffer buf = createRegion
? prov.fromRegion(key, r -> r.readValue(key)).orElse(null)
: prov.fromExistingRegion(key, r -> r.readValue(key)).orElse(Optional.of(DUMMY_EMPTY)).orElse(null);
createRegion == false 时用哨兵 private static final ByteBuffer DUMMY_EMPTY = ByteBuffer.allocate(0) 区分"provider 区域不存在"与"provider 说这个 key 没值":一旦拿到 DUMMY_EMPTY 就直接返回 Optional.empty(),不会再问后面的 provider。createRegion == true 拿到 null 才继续下一个。
load(Collection<K>, boolean) 按区域分组后用 BatchReadResult<K>(ImmutableMap<K, ByteBuffer> read / ImmutableMap<K, IOException> errored)聚合,错误项统一包成 SaveSectionException("Could not load object: " + e.getKey(), ...)。
遍历与去重
allKeys(boolean ensureUnique) / allEntries(boolean ensureUnique):
ensureUnique |
行为 |
|---|---|
true |
第一个 provider 直接给;第 i 个(i ≥ 1)的流被 filter 掉在之前任何 provider 里已存在的 key(逐个 fromExistingRegion(key, r -> r.hasValue(key)) 查询)。javadoc 明说"comes at a substantial performance cost" |
false |
直接把所有 provider 的流拼起来,允许重复;推荐值 |
拼接用 Utils.concat(List<Stream<T>>):0 个返回空流,1 个直接返回,≥2 个从中间对半递归切分成平衡二叉树(注释:对并行流的性能提升明显)。构造流过程中若抛 IOException,已建好的流全部 close() 后再抛。
forAllKeys(CheckedConsumer) 标了 @Deprecated,内部就是 allKeys(true) 的 stream + forEach,并把 UncheckedIOException 拆回 IOException。
其它方法
| 方法 | 行为 |
|---|---|
hasEntry(K) |
任一 provider fromExistingRegion(key, r -> r.hasValue(key)) 为 true 即返回 true |
flush() |
遍历所有 provider(含回退层) |
close() |
遍历所有 provider |
本 mod 自带的回退链
| SaveSection | provider 1 | provider 2 |
|---|---|---|
| SaveSection2D | SimpleRegionFactory.createDefault(EntryLocation2D.Provider, dir, 512) + SharedCachedRegionProvider |
同 keyProvider 的 SimpleRegionFactory,产出 ExtRegion(Collections.emptyList() 头表),存在判定 Files.exists(dir.resolve(name + ".ext")) |
| SaveSection3D | SimpleRegionFactory.createDefault(EntryLocation3D.Provider, dir, 512) + SharedCachedRegionProvider |
同上,换成 EntryLocation3D.Provider |
| MinecraftSaveSection | 4096 扇区 + 时间戳头表 | 无(只包一个 provider) |
即前两者是"先试打包区,写不下再落 .ext 目录";后者因为要保持原版 Anvil 格式,没有回退层。