diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 19:38:18 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 19:38:18 +0900 |
| commit | 7e7d0875087185e6c68bb29f1d60bd51b300bfef (patch) | |
| tree | 4c17b939b7d73813c5eebe8a29f382fc736e76af | |
| parent | 288d1e4a4babc136e8b3837d19970cc37023eb8e (diff) | |
| download | LunaticChat-7e7d0875087185e6c68bb29f1d60bd51b300bfef.tar.gz LunaticChat-7e7d0875087185e6c68bb29f1d60bd51b300bfef.tar.bz2 LunaticChat-7e7d0875087185e6c68bb29f1d60bd51b300bfef.zip | |
refactor: extract the debounced async save
YamlPlayerSettingsStorage and ConversionCache each carried a line-for-line
identical AtomicBoolean-plus-runDelayed debounce, five-second constant
included, so changing the save cadence meant changing it twice and
noticing that it was written twice.
Extracting it also removed the only reason those two classes held a
JavaPlugin: they took the whole plugin to reach the scheduler. They now
take the collaborator they actually use, which is both narrower and
testable without a running server.
ChannelStorage is deliberately left alone - it saves through runNow with
no debounce at all, and giving it one would change when writes happen.
Co-Authored-By: Claude <noreply@anthropic.com>
4 files changed, 45 insertions, 39 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt new file mode 100644 index 0000000..bc31785 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt @@ -0,0 +1,36 @@ +package dev.m1sk9.lunaticChat.paper + +import org.bukkit.plugin.java.JavaPlugin +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Coalesces a burst of save requests into one asynchronous write. + * + * The first [request] after an idle period schedules the write [delaySeconds] later; requests + * arriving before it fires are absorbed by it, so a player toggling a setting repeatedly costs one + * file write rather than one per toggle. + */ +class DebouncedSaver( + private val plugin: JavaPlugin, + private val delaySeconds: Long = 5, +) { + private val pending = AtomicBoolean(false) + + /** + * Schedules [save] to run asynchronously, unless a write is already pending. + */ + fun request(save: () -> Unit) { + if (!pending.compareAndSet(false, true)) return + + plugin.server.asyncScheduler.runDelayed( + plugin, + { + pending.set(false) + save() + }, + delaySeconds, + TimeUnit.SECONDS, + ) + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt index 5ed8ed6..6213348 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt @@ -169,7 +169,7 @@ class ServiceInitializer( val storage = YamlPlayerSettingsStorage( settingsFile = settingsFile, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) @@ -194,7 +194,7 @@ class ServiceInitializer( ConversionCache( cacheFile = plugin.dataFolder.resolve(configuration.features.japaneseConversion.cacheFilePath).toPath(), maxEntries = configuration.features.japaneseConversion.cacheMaxEntries, - plugin = plugin, + saver = DebouncedSaver(plugin), logger = logger, ) cache.loadFromDisk() diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt index bab4e8d..ad0a9e0 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt @@ -1,12 +1,10 @@ package dev.m1sk9.lunaticChat.paper.converter import dev.m1sk9.lunaticChat.engine.converter.CacheData +import dev.m1sk9.lunaticChat.paper.DebouncedSaver import kotlinx.serialization.json.Json -import org.bukkit.plugin.java.JavaPlugin import java.nio.file.Path import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean import java.util.logging.Logger import kotlin.io.path.bufferedReader import kotlin.io.path.exists @@ -15,11 +13,10 @@ import kotlin.io.path.writeText class ConversionCache( private val cacheFile: Path, private val maxEntries: Int = 500, - private val plugin: JavaPlugin, + private val saver: DebouncedSaver, private val logger: Logger, ) { private val conversionMemoryCache = ConcurrentHashMap<String, String>() - private val conversionSaveQueue = AtomicBoolean(false) companion object { private const val CACHE_VERSION = "1" @@ -84,7 +81,7 @@ class ConversionCache( } conversionMemoryCache[key] = value - queueSaveToDisk() + saver.request(::saveToDisk) } /** @@ -108,20 +105,6 @@ class ConversionCache( } } - private fun queueSaveToDisk() { - if (conversionSaveQueue.compareAndSet(false, true)) { - plugin.server.asyncScheduler.runDelayed( - plugin, - { - conversionSaveQueue.set(false) - saveToDisk() - }, - 5, - TimeUnit.SECONDS, - ) - } - } - // FIXME: ConcurrentHashMap keys are unordered, so evicting "oldest" entries // actually evicts random entries. Consider using LinkedHashMap with access-order // or implement proper LRU cache with timestamp tracking. diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt index 5513303..7a727b0 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt @@ -2,10 +2,8 @@ package dev.m1sk9.lunaticChat.paper.settings import com.charleskorn.kaml.Yaml import dev.m1sk9.lunaticChat.engine.settings.PlayerSettingsData -import org.bukkit.plugin.java.JavaPlugin +import dev.m1sk9.lunaticChat.paper.DebouncedSaver import java.nio.file.Path -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean import java.util.logging.Logger import kotlin.io.path.bufferedReader import kotlin.io.path.exists @@ -16,16 +14,15 @@ import kotlin.io.path.writeText * Provides async save with debouncing. * * @property settingsFile The path to the YAML settings file - * @property plugin The plugin instance for scheduling async tasks + * @property saver Coalesces bursts of save requests into one asynchronous write * @property logger The logger for logging operations */ class YamlPlayerSettingsStorage( private val settingsFile: Path, - private val plugin: JavaPlugin, + private val saver: DebouncedSaver, private val logger: Logger, ) { private val yaml = Yaml.default - private val saveFlag = AtomicBoolean(false) /** * Loads player settings from the YAML file. @@ -72,16 +69,6 @@ class YamlPlayerSettingsStorage( * @param data The settings data to save */ fun queueAsyncSave(data: PlayerSettingsData) { - if (saveFlag.compareAndSet(false, true)) { - plugin.server.asyncScheduler.runDelayed( - plugin, - { - saveFlag.set(false) - saveToDisk(data) - }, - 5, - TimeUnit.SECONDS, - ) - } + saver.request { saveToDisk(data) } } } |
