Compare commits

...

8 Commits

Author SHA1 Message Date
Spottedleaf
7ea20cde31 Set version to 0.1.0-beta.14 2025-06-20 18:29:35 -07:00
Spottedleaf
5e2b1ad7d3 Do not allow ticket level decreases to be processed asynchronously
Note: This cannot happen on the Fabric/NeoForge versions since
async ticket level processing is not allowed, but can happen on
Paper. This change is made here so that Paper can
remain in sync.

Ticket level decreases may be handled asynchronously when the
off-thread invokes processTicketUpdates() when the main thread
is running ChunkHolderManager#tick(). This is because the ticket
update is queued during tick(), but not executed (invoking
processTicketUpdates) until after releasing the ticket lock.
This creates a small window for an off-thread to invoke
processTicketUpdates() and steal the update.

When the update is stolen, the full chunk status update (if any)
will be eventually queued to execute via the chunk task queue.
If the chunk queue is processed during the server tick at any
point other than the ChunkHolderManager tick, then any ticket
level decrease will violate an important invariant in the
Moonrise chunk system: ticket level decreases only occur during
ChunkHolderManager tick. This invariant exists to make interfacing
with the chunk system easier, especially working with off-thread
contexts.

This change is specifically made to work towards fixing
https://github.com/PaperMC/Folia/issues/363
2025-06-20 18:28:16 -07:00
Spottedleaf
3146aa2e3e Add chunk unload delay config option
The default config option is no unload delay, so that
it maintains parity with Vanilla.

Fixes https://github.com/Tuinity/Moonrise/issues/115
2025-06-20 15:41:45 -07:00
Spottedleaf
035093d2c6 Fix incompatibility with "Fast IP Ping"
Inject in a different place to prevent the mixin from failing to
apply. It looks like we do essentially the same logic anyways.
2025-06-20 15:41:33 -07:00
Spottedleaf
21d106ffa2 Set version to 0.1.0-SNAPSHOT 2025-06-09 02:17:56 -07:00
Spottedleaf
6841805446 Set version to 0.1.0-beta.13 2025-06-09 02:13:51 -07:00
Spottedleaf
eb20846213 Fix infinite loop in RegionFile IO
If an exception is thrown during decompress then the read process
would be started again, which of course would eventually throw in
the decompress process.
2025-06-09 02:12:01 -07:00
Spottedleaf
66a0850ac9 Set version to 0.1.0-SNAPSHOT 2025-02-24 20:59:03 -08:00
7 changed files with 102 additions and 60 deletions

View File

@@ -15,6 +15,6 @@ cloth_version=15.0.128
fabric_lithium_version=frXUdgvL
neo_lithium_version=KhdehJ6l
# Mod Properties
mod_version=0.1.0-beta.12
mod_version=0.1.0-beta.14
maven_group=ca.spottedleaf.moonrise
archives_base_name=moonrise

View File

@@ -2,6 +2,7 @@ package ca.spottedleaf.moonrise.common.config.moonrise;
import ca.spottedleaf.moonrise.common.config.ui.ClothConfig;
import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.yamlconfig.InitialiseHook;
import ca.spottedleaf.yamlconfig.annotation.Adaptable;
@@ -38,7 +39,7 @@ public final class MoonriseConfig {
@Adaptable
public static final class Basic {
public static final class Basic implements InitialiseHook {
@Serializable(
comment = """
The maximum rate of chunks to send to any given player, per second. If this value is <= 0,
@@ -72,6 +73,20 @@ public final class MoonriseConfig {
section = CHUNK_SYSTEM_SECTION
)
public double playerMaxGenRate = -1.0;
@Serializable(
comment = """
The delay before chunks are unloaded around players once they leave their view distance.
The Vanilla value is 0 ticks. Setting this value higher (i.e 5s) will allow pets to teleport
to their owners when they teleport.
"""
)
public Duration playerChunkUnloadDelay = Duration.parse("0t");
@Override
public void initialise() {
RegionizedPlayerChunkLoader.setUnloadDelay(this.playerChunkUnloadDelay.getTimeTicks());
}
}
@Serializable(

View File

@@ -1,10 +1,13 @@
package ca.spottedleaf.moonrise.mixin.serverlist;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.multiplayer.resolver.ServerAddress;
import net.minecraft.client.multiplayer.resolver.ServerAddressResolver;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
@Mixin(ServerAddressResolver.class)
@@ -15,24 +18,23 @@ interface ServerAddressResolverMixin {
* @author Spottedleaf
*/
@Redirect(
method = {
"method_36903",
"*(Lnet/minecraft/client/multiplayer/resolver/ServerAddress;)Ljava/util/Optional;"
},
at = @At(
value = "INVOKE",
target = "Ljava/net/InetAddress;getByName(Ljava/lang/String;)Ljava/net/InetAddress;"
)
method = {
"method_36903",
"lambda$static$0"
},
at = @At(
value = "NEW",
target = "(Ljava/net/InetAddress;I)Ljava/net/InetSocketAddress;"
)
)
private static InetAddress eliminateRDNS(final String name) throws UnknownHostException {
final InetAddress ret = InetAddress.getByName(name);
final byte[] address = ret.getAddress();
private static InetSocketAddress eliminateRDNS(InetAddress addr, final int port,
@Local(ordinal = 0, argsOnly = true) final ServerAddress serverAddress) throws UnknownHostException {
final byte[] address = addr.getAddress();
if (address != null) {
// pass name to prevent rDNS
return InetAddress.getByAddress(name, address);
addr = InetAddress.getByAddress(serverAddress.getHost(), address);
}
return ret;
return new InetSocketAddress(addr, port);
}
}

View File

@@ -1042,7 +1042,7 @@ public final class MoonriseRegionFileIO {
LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr);
}
if (compoundTag == null) {
if (throwable == null && compoundTag == null) {
// need to re-try from the start
this.scheduleReadIO();
return;

View File

@@ -46,7 +46,7 @@ import java.util.function.Function;
public final class RegionizedPlayerChunkLoader {
public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 1);
public static final int MIN_VIEW_DISTANCE = 2;
public static final int MAX_VIEW_DISTANCE = 32;
@@ -55,6 +55,10 @@ public final class RegionizedPlayerChunkLoader {
public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
public static void setUnloadDelay(final long ticks) {
PLAYER_TICKET_DELAYED.timeout = Math.max(1, ticks);
}
public static final class ViewDistanceHolder {
private volatile ViewDistances viewDistances;

View File

@@ -1,5 +1,6 @@
package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
import ca.spottedleaf.concurrentutil.util.Priority;
@@ -82,6 +83,7 @@ public final class ChunkHolderManager {
private long currentTick;
private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
private final MultiThreadedQueue<NewChunkHolder> offThreadPendingFullLoadUpdate = new MultiThreadedQueue<>();
private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
if (c1 == c2) {
return 0;
@@ -110,20 +112,20 @@ public final class ChunkHolderManager {
this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
}
public boolean processTicketUpdates(final int posX, final int posZ) {
public boolean processTicketUpdates(final int chunkX, final int chunkZ) {
final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
final int ticketMask = (1 << ticketShift) - 1;
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
final boolean ret;
final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
((posX >> ticketShift) - 1) << ticketShift,
((posZ >> ticketShift) - 1) << ticketShift,
(((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
((chunkX >> ticketShift) - 1) << ticketShift,
((chunkZ >> ticketShift) - 1) << ticketShift,
(((chunkX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((chunkZ >> ticketShift) + 1) << ticketShift) | ticketMask
);
try {
ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
ret = this.processTicketUpdatesNoLock(chunkX >> ticketShift, chunkZ >> ticketShift, scheduledTasks, changedFullStatus);
} finally {
this.ticketLockArea.unlock(ticketLock);
}
@@ -721,6 +723,9 @@ public final class ChunkHolderManager {
return removeDelay <= 0L;
};
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
final long sectionKey = iterator.nextLong();
@@ -729,9 +734,16 @@ public final class ChunkHolderManager {
continue;
}
final int lowerChunkX = CoordinateUtils.getChunkX(sectionKey) << sectionShift;
final int lowerChunkZ = CoordinateUtils.getChunkZ(sectionKey) << sectionShift;
final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
final int ticketMask = (1 << ticketShift) - 1;
final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
CoordinateUtils.getChunkX(sectionKey) << sectionShift,
CoordinateUtils.getChunkZ(sectionKey) << sectionShift
((lowerChunkX >> ticketShift) - 1) << ticketShift,
((lowerChunkZ >> ticketShift) - 1) << ticketShift,
(((lowerChunkX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((lowerChunkZ >> ticketShift) + 1) << ticketShift) | ticketMask
);
try {
@@ -778,9 +790,23 @@ public final class ChunkHolderManager {
if (chunkToExpireCount.isEmpty()) {
this.sectionToChunkToExpireCount.remove(sectionKey);
}
// In order to prevent a race condition where an off-thread invokes processTicketUpdates(), we need to process ticket updates here
// so that we catch any additions to the changed full status list. If an off-thread were to process tickets here, it would not be guaranteed
// that it would be added to the full changed status set by the end of the call - possibly allowing ticket level decreases to be processed
// outside of this call, which is not an intended or expected of this chunk system.
this.processTicketUpdatesNoLock(lowerChunkX >> ThreadedTicketLevelPropagator.SECTION_SHIFT, lowerChunkZ >> ThreadedTicketLevelPropagator.SECTION_SHIFT, scheduledTasks, changedFullStatus);
} finally {
this.ticketLockArea.unlock(ticketLock);
}
this.addChangedStatuses(changedFullStatus);
changedFullStatus.clear(); // clear for next loop iteration
for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
scheduledTasks.get(i).schedule();
}
scheduledTasks.clear(); // clear for next loop iteration
}
this.processTicketUpdates();
@@ -1007,14 +1033,9 @@ public final class ChunkHolderManager {
return;
}
if (!TickThread.isTickThread()) {
this.taskScheduler.scheduleChunkTask(() -> {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
pendingFullLoadUpdate.add(changedFullStatus.get(i));
}
ChunkHolderManager.this.processPendingFullUpdate();
}, Priority.HIGHEST);
// These will be handled on the next ServerChunkCache$MainThreadExecutor#pollTask, as it runs the distance manager update
// which will invoke processTicketUpdates
this.offThreadPendingFullLoadUpdate.addAll(changedFullStatus);
} else {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
@@ -1295,36 +1316,20 @@ public final class ChunkHolderManager {
}
public boolean processTicketUpdates() {
return this.processTicketUpdates(true, null);
}
private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
return CURRENT_TICKET_UPDATE_SCHEDULING.get();
}
private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
}
if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) {
final boolean isTickThread = TickThread.isTickThread();
if (!PlatformHooks.get().allowAsyncTicketUpdates() && isTickThread) {
TickThread.ensureTickThread("Cannot asynchronously process ticket updates");
}
List<NewChunkHolder> changedFullStatus = null;
final boolean isTickThread = TickThread.isTickThread();
boolean ret = false;
final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
final boolean canProcessScheduling = scheduledTasks == null;
if (this.ticketLevelPropagator.hasPendingUpdates()) {
if (scheduledTasks == null) {
scheduledTasks = new ArrayList<>();
}
changedFullStatus = new ArrayList<>();
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
this.blockTicketUpdates();
try {
@@ -1335,27 +1340,42 @@ public final class ChunkHolderManager {
} finally {
this.unblockTicketUpdates(Boolean.FALSE);
}
}
if (changedFullStatus != null) {
this.addChangedStatuses(changedFullStatus);
}
if (canProcessScheduling && scheduledTasks != null) {
for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
scheduledTasks.get(i).schedule();
}
}
if (canProcessFullUpdates) {
if (isTickThread) {
ret |= this.processPendingFullUpdate();
}
return ret;
}
private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
return CURRENT_TICKET_UPDATE_SCHEDULING.get();
}
// only call on tick thread
private void processOffThreadFullUpdates() {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
final MultiThreadedQueue<NewChunkHolder> offThreadPendingFullLoadUpdate = this.offThreadPendingFullLoadUpdate;
NewChunkHolder toUpdate;
while ((toUpdate = offThreadPendingFullLoadUpdate.poll()) != null) {
pendingFullLoadUpdate.add(toUpdate);
}
}
// only call on tick thread
private boolean processPendingFullUpdate() {
this.processOffThreadFullUpdates();
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
boolean ret = false;

View File

@@ -210,6 +210,7 @@ accessible class net/minecraft/server/level/DistanceManager$FixedPlayerDistanceC
# Ticket
accessible field net/minecraft/server/level/Ticket key Ljava/lang/Object;
accessible field net/minecraft/server/level/TicketType timeout J
mutable field net/minecraft/server/level/TicketType timeout J
# ServerChunkCache