Write custom timeout logic for DelayedDataSourceCache

This should make the code a bit more transparent vs using the CacheBuilder, plus hopefully resolve a concurrent writing issue that causes monoliths
This commit is contained in:
James Seibel
2025-10-02 20:29:26 -05:00
parent 85e52301d6
commit 721124b886
11 changed files with 401 additions and 92 deletions
@@ -0,0 +1,138 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package tests;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* A few very basic tests to confirm {@link DelayedFullDataSourceSaveCache}
* is working properly.
*
* @author James Seibel
* @version 2025-10-02
*/
public class DelayedSaveCacheTest
{
@Test
public void CacheExpirationAndPoolingTest() throws InterruptedException
{
// how many times any data source has been "written to disk"
AtomicInteger diskSaveCountRef = new AtomicInteger(0);
DelayedFullDataSourceSaveCache cache = new DelayedFullDataSourceSaveCache((FullDataSourceV2 fullDataSource) ->
{
diskSaveCountRef.getAndIncrement();
return this.onDataSourceSaveAsync(fullDataSource);
}, 1_000);
//==============================//
// single item and manual flush //
//==============================//
PhantomArrayListCheckout initialCheckout;
try (FullDataSourceV2 initialSource = FullDataSourceV2.createEmpty(DhSectionPos.encode((byte)6, 0, 0)))
{
initialCheckout = initialSource.getPhantomArrayCheckoutForUnitTesting();
cache.writeDataSourceToMemoryAndQueueSave(initialSource);
}
Assert.assertEquals("only 1 item should be in the cache", 1, cache.getUnsavedCount());
Assert.assertEquals("no disk saves should have happened yet", 0, diskSaveCountRef.get());
// manual flush
cache.flush();
Assert.assertEquals("memory cache should be empty after", 0, cache.getUnsavedCount());
Assert.assertEquals("1 manual flush was expected", 1, diskSaveCountRef.get());
//======================//
// quick group position //
//======================//
// write multiple items for the same position
for (int i = 0; i < 4; i++)
{
try (FullDataSourceV2 loopSource = FullDataSourceV2.createEmpty(DhSectionPos.encode((byte) 6, 0, 0)))
{
PhantomArrayListCheckout loopCheckout = loopSource.getPhantomArrayCheckoutForUnitTesting();
Assert.assertEquals(initialCheckout, loopCheckout);
cache.writeDataSourceToMemoryAndQueueSave(loopSource);
}
}
// each item writes to the same place
Assert.assertEquals("exactly 1 item should be in the cache", 1, cache.getUnsavedCount());
Assert.assertEquals("no new saves should have happened yet", 1, diskSaveCountRef.get());
// wait for the cache to clear
Thread.sleep(2_000);
Assert.assertEquals("Cache should have automatically cleared due to inactivity", 0, cache.getUnsavedCount());
Assert.assertEquals("second save after timeout expected", 2, diskSaveCountRef.get());
//=====================//
// slow group position //
//=====================//
// write multiple items for the same position
for (int i = 0; i < 4; i++)
{
try (FullDataSourceV2 loopSource = FullDataSourceV2.createEmpty(DhSectionPos.encode((byte) 6, 0, 0)))
{
PhantomArrayListCheckout loopCheckout = loopSource.getPhantomArrayCheckoutForUnitTesting();
Assert.assertEquals(initialCheckout, loopCheckout);
cache.writeDataSourceToMemoryAndQueueSave(loopSource);
}
// long enough to prevent a timeout, but short enough that they don't happen all at once
Thread.sleep(500);
}
// each item writes to the same place
Assert.assertEquals("exactly 1 item should be in the cache", 1, cache.getUnsavedCount());
Assert.assertEquals("no new saves should have happened yet", 2, diskSaveCountRef.get());
// wait for the cache to clear
Thread.sleep(2_000);
Assert.assertEquals("Cache should have automatically cleared due to inactivity", 0, cache.getUnsavedCount());
Assert.assertEquals("third timeout expected", 3, diskSaveCountRef.get());
}
private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource)
{ return CompletableFuture.completedFuture(null); }
}
@@ -0,0 +1,65 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package tests;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.locks.ReentrantLock;
/**
* @see KeyedLockContainer
*
* @author James Seibel
* @version 2025-10-02
*/
public class KeyedLockTest
{
@Test
public void BasicKeyedLockTest()
{
KeyedLockContainer<Long> lockContainer = new KeyedLockContainer<>();
for (long a = -10; a < 10; a++)
{
ReentrantLock aLock = lockContainer.getLockForPos(a);
for (long b = -10; b < 10; b++)
{
ReentrantLock bLock = lockContainer.getLockForPos(a);
// we only care that the same position always map to the same object
// if different positions map to the same object,
// that's expected hash-collision behavior and is fine
if (a == b)
{
Assert.assertEquals("long values ["+a+"] and ["+b+"] should have returned the same lock", aLock, bLock);
}
}
}
}
}
@@ -0,0 +1,66 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package tests;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* @see PhantomArrayListCheckout
*
* @author James Seibel
* @version 2025-10-02
*/
public class PooledDataSourceCheckoutTest
{
@Test
public void TestCheckouts()
{
PhantomArrayListCheckout initialCheckout;
try (FullDataSourceV2 initialSource = FullDataSourceV2.createEmpty(DhSectionPos.encode((byte)6, 0, 0)))
{
initialCheckout = initialSource.getPhantomArrayCheckoutForUnitTesting();
}
try (FullDataSourceV2 outerSource = FullDataSourceV2.createEmpty(DhSectionPos.encode((byte) 6, 0, 0)))
{
PhantomArrayListCheckout outerCheckout = outerSource.getPhantomArrayCheckoutForUnitTesting();
Assert.assertEquals("the first checkout object should be pooled", initialCheckout, outerCheckout);
try (FullDataSourceV2 innerSource = FullDataSourceV2.createEmpty(DhSectionPos.encode((byte) 6, 0, 0)))
{
PhantomArrayListCheckout innerCheckout = innerSource.getPhantomArrayCheckoutForUnitTesting();
Assert.assertNotEquals("the second checkout object should not be shared when the first is still in use", initialCheckout, innerCheckout);
Assert.assertNotEquals("the second checkout object should not be shared when the first is still in use", outerCheckout, innerCheckout);
}
}
}
}