fix(D-01): atomic snapshot for registry to prevent torn reads (BUG-001)

Replace two separate volatile fields (DEFINITIONS, COMPONENT_HOLDERS)
with a single RegistrySnapshot record swapped atomically. This prevents
race conditions where a reader thread could see new definitions paired
with stale/empty component holders between the two volatile writes.
This commit is contained in:
NotEvil
2026-04-14 02:25:57 +02:00
parent dbacef66d5
commit 1327e3bfc3

View File

@@ -18,9 +18,9 @@ import org.jetbrains.annotations.Nullable;
* Thread-safe registry for data-driven bondage item definitions.
*
* <p>Populated by the reload listener that scans {@code tiedup_items/} JSON files.
* Uses volatile atomic swap (same pattern as {@link
* com.tiedup.remake.client.animation.context.ContextGlbRegistry}) to ensure
* the render thread always sees a consistent snapshot.</p>
* Uses a single volatile snapshot reference to ensure readers always see a
* consistent pair of definitions + component holders. This prevents torn reads
* where one map is updated but the other is stale.</p>
*
* <p>Lookup methods accept either a {@link ResourceLocation} ID directly
* or an {@link ItemStack} (reads the {@code tiedup_item_id} NBT tag).</p>
@@ -31,20 +31,22 @@ public final class DataDrivenItemRegistry {
public static final String NBT_ITEM_ID = "tiedup_item_id";
/**
* Volatile reference to an unmodifiable map. Reload builds a new map
* and swaps atomically; consumer threads always see a consistent snapshot.
* Immutable snapshot combining definitions and their component holders.
* Swapped atomically via a single volatile write to prevent torn reads.
*/
private static volatile Map<
ResourceLocation,
DataDrivenItemDefinition
> DEFINITIONS = Map.of();
private record RegistrySnapshot(
Map<ResourceLocation, DataDrivenItemDefinition> definitions,
Map<ResourceLocation, ComponentHolder> holders
) {
static final RegistrySnapshot EMPTY = new RegistrySnapshot(Map.of(), Map.of());
}
/**
* Parallel cache of instantiated component holders, keyed by item definition ID.
* Rebuilt every time DEFINITIONS changes.
* Single volatile reference to the current registry state.
* All read methods capture this reference ONCE at the start to ensure
* consistency within a single call.
*/
private static volatile Map<ResourceLocation, ComponentHolder>
COMPONENT_HOLDERS = Map.of();
private static volatile RegistrySnapshot SNAPSHOT = RegistrySnapshot.EMPTY;
private DataDrivenItemRegistry() {}
@@ -57,8 +59,9 @@ public final class DataDrivenItemRegistry {
public static void reload(
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
) {
DEFINITIONS = Collections.unmodifiableMap(new HashMap<>(newDefs));
COMPONENT_HOLDERS = buildComponentHolders(DEFINITIONS);
Map<ResourceLocation, DataDrivenItemDefinition> defs =
Collections.unmodifiableMap(new HashMap<>(newDefs));
SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs));
}
/**
@@ -67,7 +70,8 @@ public final class DataDrivenItemRegistry {
* <p>On an integrated server, both the client (assets/) and server (data/) reload
* listeners populate this registry. Using {@link #reload} would cause the second
* listener to overwrite the first's definitions. This method builds a new map
* from the existing snapshot + the new entries, then swaps atomically.</p>
* from the existing snapshot + the new entries, then swaps atomically as a
* single snapshot to prevent torn reads.</p>
*
* @param newDefs the definitions to merge (will overwrite existing entries with same key)
*/
@@ -75,11 +79,12 @@ public final class DataDrivenItemRegistry {
Map<ResourceLocation, DataDrivenItemDefinition> newDefs
) {
Map<ResourceLocation, DataDrivenItemDefinition> merged = new HashMap<>(
DEFINITIONS
SNAPSHOT.definitions
);
merged.putAll(newDefs);
DEFINITIONS = Collections.unmodifiableMap(merged);
COMPONENT_HOLDERS = buildComponentHolders(DEFINITIONS);
Map<ResourceLocation, DataDrivenItemDefinition> defs =
Collections.unmodifiableMap(merged);
SNAPSHOT = new RegistrySnapshot(defs, buildComponentHolders(defs));
}
/**
@@ -90,7 +95,7 @@ public final class DataDrivenItemRegistry {
*/
@Nullable
public static DataDrivenItemDefinition get(ResourceLocation id) {
return DEFINITIONS.get(id);
return SNAPSHOT.definitions.get(id);
}
/**
@@ -108,7 +113,7 @@ public final class DataDrivenItemRegistry {
tag.getString(NBT_ITEM_ID)
);
if (id == null) return null;
return DEFINITIONS.get(id);
return SNAPSHOT.definitions.get(id);
}
/**
@@ -117,15 +122,14 @@ public final class DataDrivenItemRegistry {
* @return unmodifiable collection of all definitions
*/
public static Collection<DataDrivenItemDefinition> getAll() {
return DEFINITIONS.values();
return SNAPSHOT.definitions.values();
}
/**
* Clear all definitions. Called on world unload or for testing.
*/
public static void clear() {
DEFINITIONS = Map.of();
COMPONENT_HOLDERS = Map.of();
SNAPSHOT = RegistrySnapshot.EMPTY;
}
// ===== COMPONENT HOLDERS =====
@@ -133,14 +137,27 @@ public final class DataDrivenItemRegistry {
/**
* Get the ComponentHolder for a data-driven item stack.
*
* <p>Captures the snapshot reference once to ensure consistent reads
* between the definition lookup and the holder lookup.</p>
*
* @param stack the ItemStack to inspect
* @return the holder, or null if the stack is not data-driven or has no definition
*/
@Nullable
public static ComponentHolder getComponents(ItemStack stack) {
DataDrivenItemDefinition def = get(stack);
if (stack.isEmpty()) return null;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(NBT_ITEM_ID)) return null;
ResourceLocation id = ResourceLocation.tryParse(
tag.getString(NBT_ITEM_ID)
);
if (id == null) return null;
// Capture snapshot once to ensure definition and holder come from
// the same atomic snapshot, preventing torn reads.
RegistrySnapshot snap = SNAPSHOT;
DataDrivenItemDefinition def = snap.definitions.get(id);
if (def == null) return null;
return COMPONENT_HOLDERS.get(def.id());
return snap.holders.get(def.id());
}
/**
@@ -151,7 +168,7 @@ public final class DataDrivenItemRegistry {
*/
@Nullable
public static ComponentHolder getComponents(ResourceLocation id) {
return COMPONENT_HOLDERS.get(id);
return SNAPSHOT.holders.get(id);
}
/**