多提供者回退链(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>)

  1. 复制输入 map 到 pendingEntries(注释:避免改动调用方传入的 map)
  2. 按 IKey::getRegionKey 分组 → 每个区域一批
  3. 对每批 × 每个 provider:r.writeValues(regionEntries)
  4. 捕 MultiUnsupportedDataException,用 getChildren() 拿到 Map<K, UnsupportedDataException>,把这些 key 从本批剔除、逐个记异常,再对它们 writeValues(toNulls) 擦除
  5. 本批剩下的 key 视为成功 → 清异常、置 pendingEntries.put(k, null)
  6. 仍有异常 → 抛 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 格式,没有回退层。

相关条目