Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1349,7 +1349,12 @@ public UserDataCache getCache(UUID uuid) {
if (hasSharedSqlBackend() && isPlatformOwnedThread()) {
UserDataCache cache = userDataCache.get(uuid);
if (cache == null) cache = ensureSharedCachePlaceholder(uuid);
if (!completedSharedCachePopulations.contains(uuid)) cacheUser(uuid, false);
// A completed population can later be unpublished by invalidateStorageSnapshot().
// Queue a worker refill so the cache heals even if no caller arranged one;
// deferred populations are deduplicated per user, so this stays bounded.
if (!completedSharedCachePopulations.contains(uuid) || !cache.hasPublishedStorageSnapshot()) {
cacheUser(uuid, false);
}
return cache;
}
cacheUserIfNeeded(uuid);
Expand Down Expand Up @@ -1381,23 +1386,51 @@ public boolean isPlatformOwnedThread() {
static boolean isFoliaTickThread(Object server) {
if (server == null) return false;
try {
java.lang.reflect.Method global = server.getClass().getMethod("isGlobalTickThread");
if (Boolean.TRUE.equals(global.invoke(server))) return true;
java.lang.reflect.Method global = GLOBAL_TICK_METHODS.get(server.getClass());
if (global != null && Boolean.TRUE.equals(global.invoke(server))) return true;
} catch (ReflectiveOperationException | RuntimeException ignored) { }
if (reflectiveStaticBoolean("ca.spottedleaf.moonrise.common.util.TickThread", "isTickThread")) return true;
if (reflectiveStaticBoolean("io.papermc.paper.threadedregions.RegionizedServer", "isGlobalTickThread")) return true;
if (invokeStaticBoolean(FoliaProbes.MOONRISE_IS_TICK_THREAD)) return true;
if (invokeStaticBoolean(FoliaProbes.REGIONIZED_IS_GLOBAL_TICK_THREAD)) return true;
try {
Class<?> scheduler = Class.forName("io.papermc.paper.threadedregions.TickRegionScheduler");
java.lang.reflect.Method current = scheduler.getMethod("getCurrentRegion");
if (current.invoke(null) != null) return true;
java.lang.reflect.Method current = FoliaProbes.TICK_REGION_CURRENT_REGION;
if (current != null && current.invoke(null) != null) return true;
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) { }
return false;
}

private static boolean reflectiveStaticBoolean(String className, String methodName) {
/**
* Lookups are resolved once. Resolving them on every call threw and filled a
* NoSuchMethodException/ClassNotFoundException per user data access on non-Folia servers.
*/
private static final ClassValue<java.lang.reflect.Method> GLOBAL_TICK_METHODS = new ClassValue<java.lang.reflect.Method>() {
@Override
protected java.lang.reflect.Method computeValue(Class<?> type) {
try {
return type.getMethod("isGlobalTickThread");
} catch (ReflectiveOperationException | RuntimeException e) {
return null;
}
}
};

private static final class FoliaProbes {
static final java.lang.reflect.Method MOONRISE_IS_TICK_THREAD = staticMethod("ca.spottedleaf.moonrise.common.util.TickThread", "isTickThread");
static final java.lang.reflect.Method REGIONIZED_IS_GLOBAL_TICK_THREAD = staticMethod("io.papermc.paper.threadedregions.RegionizedServer", "isGlobalTickThread");
static final java.lang.reflect.Method TICK_REGION_CURRENT_REGION = staticMethod("io.papermc.paper.threadedregions.TickRegionScheduler", "getCurrentRegion");

private static java.lang.reflect.Method staticMethod(String className, String methodName) {
try {
return Class.forName(className).getMethod(methodName);
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
return null;
}
}
}

private static boolean invokeStaticBoolean(java.lang.reflect.Method method) {
if (method == null) return false;
try {
Class<?> type = Class.forName(className);
return Boolean.TRUE.equals(type.getMethod(methodName).invoke(null));
return Boolean.TRUE.equals(method.invoke(null));
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,45 @@ void primaryThreadCachePopulationIsQueuedAndGetCacheReturnsAPopulationPlaceholde
}
}

@Test
void invalidatedCompletedCacheQueuesOneWorkerRefillOnPrimaryThreadAccess() throws Exception {
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class);
UserDataManager manager = new UserDataManager(plugin);
manager.getTimer().shutdownNow();
ScheduledExecutorService worker = mock(ScheduledExecutorService.class);
Field timer = UserDataManager.class.getDeclaredField("timer");
timer.setAccessible(true);
timer.set(manager, worker);
UUID uuid = UUID.randomUUID();
UserDataCache cache = new UserDataCache(manager, uuid);
cache.updateCache(new HashMap<>(Map.of("Points", new DataValueInt(38))));
manager.getUserDataCache().put(uuid, cache);
Field completed = UserDataManager.class.getDeclaredField("completedSharedCachePopulations");
completed.setAccessible(true);
@SuppressWarnings("unchecked")
Set<UUID> completedPopulations = (Set<UUID>) completed.get(manager);
completedPopulations.add(uuid);
SqlUserBackend backend = mock(SqlUserBackend.class);
when(backend.isOpen()).thenReturn(true);
manager.bindSharedSqlBackend(backend, (user, operation) -> {
throw new AssertionError("primary thread must not admit cache storage work");
});
Server server = mock(Server.class);
try (var bukkit = mockStatic(Bukkit.class)) {
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(Bukkit::isPrimaryThread).thenReturn(true);
assertSame(cache, manager.getCache(uuid));
verify(worker, never()).execute(any(Runnable.class));
cache.invalidateStorageSnapshot("Points");
assertSame(cache, manager.getCache(uuid));
verify(worker, times(1)).execute(any(Runnable.class));
// The queued refill is still pending, so repeated reads must not pile up work.
assertSame(cache, manager.getCache(uuid));
verify(worker, times(1)).execute(any(Runnable.class));
assertFalse(cache.hasPublishedStorageSnapshot());
}
}

@Test
void removingCompletedPlaceholderAllowsAReplacementPopulation() throws Exception {
AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class);
Expand Down
Loading