Skip to content

Fix concurrent modification errors when serializing item/fluid storage off-thread - #63

Open
andriihorpenko wants to merge 1 commit into
Low-Drag-MC:1.21from
andriihorpenko:fix/sync-cme
Open

Fix concurrent modification errors when serializing item/fluid storage off-thread#63
andriihorpenko wants to merge 1 commit into
Low-Drag-MC:1.21from
andriihorpenko:fix/sync-cme

Conversation

@andriihorpenko

Copy link
Copy Markdown

Description

With ISyncPersistRPCBlockEntity.useAsyncThread() returning true, every managed block entity runs passivelySync() on LDLib async thread every 50ms.

Dirty-checking a @Persisted/@DescSynced INBTSerializable field serializes it in full, while the server thread is mutating the same storage.

Solution

Copy into a local first so the empty check and the encode see the same value. ItemStackTransfer needs a new override because the racy implementation is NeoForge's. Output NBT is unchanged.

Observed issues

Issue №1 - Fluid handler

[05Sep2026 03:16:51.426] [LDLib Async Thread-0/ERROR] [net.minecraft.Util/]: Error saving [0 minecraft:empty]. Original cause: java.lang.IllegalStateException: Value must be positive: 0; Fluid must not be minecraft:empty
With components:
{
}
[05Sep2026 03:16:51.428] [LDLib Async Thread-0/ERROR] [LowDragLib2/]: asyncThreadLogic error with an async logic <reducted>.blockentity.MachineBlockEntity@6de79b76
java.lang.IllegalStateException: Value must be positive: 0; Fluid must not be minecraft:empty
	at MC-BOOTSTRAP/datafixerupper@8.0.16/com.mojang.serialization.DataResult$Error.getOrThrow(DataResult.java:287) ~[datafixerupper-8.0.16.jar%23100!/:?]
	at MC-BOOTSTRAP/datafixerupper@8.0.16/com.mojang.serialization.DataResult.getOrThrow(DataResult.java:81) ~[datafixerupper-8.0.16.jar%23100!/:?]
	at TRANSFORMER/neoforge@21.1.247/net.neoforged.neoforge.common.util.DataComponentUtil.wrapEncodingExceptions(DataComponentUtil.java:36) ~[neoforge-21.1.247-universal.jar%23261!/:?]
	at TRANSFORMER/neoforge@21.1.247/net.neoforged.neoforge.fluids.FluidStack.save(FluidStack.java:287) ~[neoforge-21.1.247-universal.jar%23261!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.misc.FluidStorage.serializeNBT(FluidStorage.java:57) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.misc.FluidStorage.serializeNBT(FluidStorage.java:16) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.misc.FluidTransferList.serializeNBT(FluidTransferList.java:151) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.misc.FluidTransferList.serializeNBT(FluidTransferList.java:22) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.accessor.readonly.INBTSerializableReadOnlyAccessor.readReadOnlyValue(INBTSerializableReadOnlyAccessor.java:30) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.accessor.readonly.INBTSerializableReadOnlyAccessor.readReadOnlyValue(INBTSerializableReadOnlyAccessor.java:16) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.ref.ReadOnlyRef.readOnlyUpdate(ReadOnlyRef.java:81) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.ref.ReadOnlyManagedRef.updateSync(ReadOnlyManagedRef.java:49) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.ref.Ref.update(Ref.java:66) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.holder.ISyncMangedHolder.sync(ISyncMangedHolder.java:42) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.holder.ISyncMangedHolder.passivelySync(ISyncMangedHolder.java:75) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.holder.ISyncMangedHolder.asyncTick(ISyncMangedHolder.java:186) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.async.AsyncThreadData.searchingTask(AsyncThreadData.java:93) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[?:?]
	at java.base/java.lang.Thread.run(Unknown Source) [?:?]

Issue №2 - Item handler

[05Sep2026 10:16:12.335] [LDLib Async Thread-0/ERROR] [LowDragLib2/]: asyncThreadLogic error with an async logic <reducted>.blockentity.MachineBlockEntity@4e0a1cb4
java.lang.IllegalStateException: Cannot encode empty ItemStack
	at TRANSFORMER/minecraft@1.21.1/net.minecraft.world.item.ItemStack.save(ItemStack.java:400) ~[server-1.21.1-20240808.144430-srg.jar%23260!/:?]
	at TRANSFORMER/neoforge@21.1.247/net.neoforged.neoforge.items.ItemStackHandler.serializeNBT(ItemStackHandler.java:145) ~[neoforge-21.1.247-universal.jar%23261!/:?]
	at TRANSFORMER/neoforge@21.1.247/net.neoforged.neoforge.items.ItemStackHandler.serializeNBT(ItemStackHandler.java:17) ~[neoforge-21.1.247-universal.jar%23261!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.accessor.readonly.INBTSerializableReadOnlyAccessor.readReadOnlyValue(INBTSerializableReadOnlyAccessor.java:30) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.accessor.readonly.INBTSerializableReadOnlyAccessor.readReadOnlyValue(INBTSerializableReadOnlyAccessor.java:16) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.ref.ReadOnlyRef.readOnlyUpdate(ReadOnlyRef.java:81) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.ref.ReadOnlyManagedRef.updateSync(ReadOnlyManagedRef.java:49) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.ref.Ref.update(Ref.java:66) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.holder.ISyncMangedHolder.sync(ISyncMangedHolder.java:42) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.holder.ISyncMangedHolder.passivelySync(ISyncMangedHolder.java:75) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.syncdata.holder.ISyncMangedHolder.asyncTick(ISyncMangedHolder.java:186) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at TRANSFORMER/ldlib2@2.2.36.a/com.lowdragmc.lowdraglib2.async.AsyncThreadData.searchingTask(AsyncThreadData.java:93) ~[ldlib2-neoforge-1.21.1-2.2.36.a-all.jar%23322!/:?]
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.FutureTask.runAndReset(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) ~[?:?]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) ~[?:?]
	at java.base/java.lang.Thread.run(Unknown Source) [?:?]

@Yefancy

Yefancy commented Sep 12, 2026

Copy link
Copy Markdown
Member

yeah, I know it. I cannot say that it was designed as it. but crash on a thread is currently allowed. and re-sync during next run of this thread.

Because I want to reduce the cost and still maintain correctness. Currently, we may occasionally fail to retrieve the latest state, but eventually (usually in the next run), it will be retrieved. However, if copying is used, the same problem will still occur because the copy may also read the incorrect value. This is not a correct solution.

I know that annoying log errors, and I'm also thinking about how to deal with it. I will keep this reminder to alert me of certain matters that require attention. Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants