From 0b5ed08211242ad9e04e843dc8e2760c472a1234 Mon Sep 17 00:00:00 2001 From: sysnote8main Date: Mon, 20 Jul 2026 06:58:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20GitHub=20Actions=20=E3=81=AE=20SHA?= =?UTF-8?q?=20pin=20=E3=82=92=E6=9B=B4=E6=96=B0=20(actions-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/integration-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 3498485..0d21bd5 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -50,12 +50,12 @@ jobs: cache: gradle - name: Setup Node.js (for bot scenarios) - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - name: Setup pnpm (for bot dependencies) - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: latest From 1b0c243055fd7e34484786df884d2d3566097ee7 Mon Sep 17 00:00:00 2001 From: sysnote8main Date: Mon, 20 Jul 2026 06:58:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20RedisConfig=20=E3=81=AB=20user=20?= =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB=E3=83=89=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(issue=20#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redis 6+ ACL のユーザー名を指定可能にする。 空文字の場合は default ユーザーとして接続する。 設定ファイル・wiki・ConfigTest も更新。 --- src/integrationTest/resources/master-config.yml | 1 + src/integrationTest/resources/readonly-config.yml | 1 + src/integrationTest/resources/slave-config.yml | 1 + src/main/kotlin/net/azisaba/simplequest/data/Config.kt | 1 + src/main/resources/config.yml | 1 + src/test/kotlin/net/azisaba/simplequest/data/ConfigTest.kt | 4 +++- wiki/CONFIGURATION.md | 2 ++ 7 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/resources/master-config.yml b/src/integrationTest/resources/master-config.yml index 5883a20..d08e5b8 100644 --- a/src/integrationTest/resources/master-config.yml +++ b/src/integrationTest/resources/master-config.yml @@ -7,6 +7,7 @@ database: password: test redis: + user: "" host: localhost port: 6379 password: "" diff --git a/src/integrationTest/resources/readonly-config.yml b/src/integrationTest/resources/readonly-config.yml index 5bd9e6f..cd1d096 100644 --- a/src/integrationTest/resources/readonly-config.yml +++ b/src/integrationTest/resources/readonly-config.yml @@ -7,6 +7,7 @@ database: password: test redis: + user: "" host: localhost port: 6379 password: "" diff --git a/src/integrationTest/resources/slave-config.yml b/src/integrationTest/resources/slave-config.yml index dfd31d9..61c7d9d 100644 --- a/src/integrationTest/resources/slave-config.yml +++ b/src/integrationTest/resources/slave-config.yml @@ -7,6 +7,7 @@ database: password: test redis: + user: "" host: localhost port: 6379 password: "" diff --git a/src/main/kotlin/net/azisaba/simplequest/data/Config.kt b/src/main/kotlin/net/azisaba/simplequest/data/Config.kt index f810eb7..bd8edb1 100644 --- a/src/main/kotlin/net/azisaba/simplequest/data/Config.kt +++ b/src/main/kotlin/net/azisaba/simplequest/data/Config.kt @@ -26,6 +26,7 @@ data class DatabaseConfig( @Serializable data class RedisConfig( + val user: String = "", val host: String = "localhost", val port: Int = 6379, val password: String = "", diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 675044a..9248c22 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -6,6 +6,7 @@ database: password: "" redis: + user: "" host: "localhost" port: 6379 password: "" diff --git a/src/test/kotlin/net/azisaba/simplequest/data/ConfigTest.kt b/src/test/kotlin/net/azisaba/simplequest/data/ConfigTest.kt index cb0c828..b184698 100644 --- a/src/test/kotlin/net/azisaba/simplequest/data/ConfigTest.kt +++ b/src/test/kotlin/net/azisaba/simplequest/data/ConfigTest.kt @@ -35,13 +35,15 @@ class ConfigTest : context("RedisConfig") { test("defaults") { val c = RedisConfig() + c.user shouldBe "" c.host shouldBe "localhost" c.port shouldBe 6379 c.password shouldBe "" } test("custom values") { - val c = RedisConfig(host = "redis.local", port = 6380, password = "redis-pass") + val c = RedisConfig(user = "cache", host = "redis.local", port = 6380, password = "redis-pass") + c.user shouldBe "cache" c.host shouldBe "redis.local" c.port shouldBe 6380 c.password shouldBe "redis-pass" diff --git a/wiki/CONFIGURATION.md b/wiki/CONFIGURATION.md index 5f038b1..29685c6 100644 --- a/wiki/CONFIGURATION.md +++ b/wiki/CONFIGURATION.md @@ -20,6 +20,7 @@ database: # ── Redis 接続設定 ── redis: + user: "" # Redis 6+ ACL のユーザー名(default ユーザー利用時は空文字) host: "localhost" # Redis のホスト名 or IP port: 6379 # ポート番号 password: "" # 認証パスワード(requirepass 設定時) @@ -74,6 +75,7 @@ MariaDB(または MySQL)への接続情報です。 Redis への接続情報です。キャッシュと PubSub メッセージングに使用されます。 - **password**: Redis 側で `requirepass` を設定している場合に指定。未設定なら空文字 `""` で問題ありません +- **user**: Redis 6 以降の ACL で `default` 以外のユーザーを使う場合に指定。未設定(空文字)なら `default` ユーザーとして接続します ### `maxPartySize` / `partyInviteLimit` From f24cf44b00b18b706a99f464a069a9bb164e3d9d Mon Sep 17 00:00:00 2001 From: sysnote8main Date: Mon, 20 Jul 2026 06:58:34 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20Redis=20=E6=8E=A5=E7=B6=9A=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20(RedisManager)=20=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=20user=20=E3=82=92=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lettuce で RedisURI/RedisClient を構築し、RedisConfig.user を ACL ユーザー名にマップ。接続失敗時は DB 同様にプラグイン起動を継続 (Redis 依存機能を無効化)。DI と onEnable/onDisable ライフサイクルに 統合。実接続しない buildUri の単体テストを追加。 --- .../net/azisaba/simplequest/SimpleQuest.kt | 6 ++ .../simplequest/database/RedisManager.kt | 92 +++++++++++++++++++ .../azisaba/simplequest/di/ConfigModule.kt | 4 + .../simplequest/di/SimpleQuestComponent.kt | 3 + .../simplequest/database/RedisManagerTest.kt | 40 ++++++++ 5 files changed, 145 insertions(+) create mode 100644 src/main/kotlin/net/azisaba/simplequest/database/RedisManager.kt create mode 100644 src/test/kotlin/net/azisaba/simplequest/database/RedisManagerTest.kt diff --git a/src/main/kotlin/net/azisaba/simplequest/SimpleQuest.kt b/src/main/kotlin/net/azisaba/simplequest/SimpleQuest.kt index b596d1e..974f0f8 100644 --- a/src/main/kotlin/net/azisaba/simplequest/SimpleQuest.kt +++ b/src/main/kotlin/net/azisaba/simplequest/SimpleQuest.kt @@ -7,6 +7,7 @@ import net.azisaba.simplequest.database.BackupService import net.azisaba.simplequest.database.DatabaseManager import net.azisaba.simplequest.database.DiscordWebhook import net.azisaba.simplequest.database.MigrationRunner +import net.azisaba.simplequest.database.RedisManager import net.azisaba.simplequest.database.SyncService import net.azisaba.simplequest.di.BukkitModule import net.azisaba.simplequest.di.DaggerSimpleQuestComponent @@ -37,6 +38,8 @@ class SimpleQuest : JavaPlugin() { private set lateinit var databaseManager: DatabaseManager private set + lateinit var redisManager: RedisManager + private set lateinit var questManager: QuestManager private set lateinit var questService: QuestService @@ -60,6 +63,7 @@ class SimpleQuest : JavaPlugin() { diComponent = DaggerSimpleQuestComponent.builder().bukkitModule(BukkitModule(this)).build() configData = diComponent.configData() databaseManager = diComponent.databaseManager() + redisManager = diComponent.redisManager() questManager = diComponent.questManager() questService = diComponent.questService() syncService = diComponent.syncService() @@ -74,12 +78,14 @@ class SimpleQuest : JavaPlugin() { registerCommands() registerListeners() startBackupIfConnected() + if (redisManager.isConnected) logger.info("Redis connected.") else logger.info("Redis disabled.") logger.info("SimpleQuest enabled.") } override fun onDisable() { if (::backupService.isInitialized) backupService.stop() if (::discordWebhook.isInitialized) discordWebhook.shutdown() + if (::redisManager.isInitialized) redisManager.disconnect() if (::databaseManager.isInitialized) databaseManager.disconnect() logger.info("SimpleQuest disabled.") } diff --git a/src/main/kotlin/net/azisaba/simplequest/database/RedisManager.kt b/src/main/kotlin/net/azisaba/simplequest/database/RedisManager.kt new file mode 100644 index 0000000..02b95c1 --- /dev/null +++ b/src/main/kotlin/net/azisaba/simplequest/database/RedisManager.kt @@ -0,0 +1,92 @@ +package net.azisaba.simplequest.database + +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisURI +import io.lettuce.core.api.StatefulRedisConnection +import jakarta.inject.Inject +import jakarta.inject.Singleton +import net.azisaba.simplequest.data.RedisConfig + +/** + * Manages the Lettuce-based Redis connection used for caching and PubSub messaging. + * + * The plugin can still load without Redis: when the connection fails, the manager + * disables itself and downstream Redis-dependent features are skipped, mirroring the + * graceful-degradation behaviour of [DatabaseManager]. + */ +@Singleton +class RedisManager + @Inject + constructor( + private val config: RedisConfig, + ) { + private var client: RedisClient? = null + private var connection: StatefulRedisConnection? = null + private var failed: Boolean = false + + init { + try { + connect() + } catch (e: Exception) { + // Plugin can still load without Redis. + // Redis-dependent features (cache, PubSub) will be skipped. + System.err.println("[SimpleQuest] Failed to connect to Redis: ${e.message}") + System.err.println("[SimpleQuest] Redis-dependent features are disabled.") + disconnect() + } + } + + /** + * Builds the [RedisURI] from the current [RedisConfig], mapping the `user` + * field to the Redis 6+ ACL username (empty means the default user). + */ + internal fun buildUri(): RedisURI { + val uri = + RedisURI + .Builder + .redis(config.host, config.port) + .apply { + if (config.password.isNotBlank()) withPassword(config.password) + }.build() + // Redis 6+ ACL: empty user means the default user. + // withUsername is not available on the Builder, so set it on the built URI. + if (config.user.isNotBlank()) uri.username = config.user + return uri + } + + fun connect() { + if (failed) return + val uri = buildUri() + client = RedisClient.create(uri) + connection = client!!.connect() + // Verify the connection eagerly so misconfiguration fails fast. + connection!!.sync().ping() + failed = false + } + + fun disconnect() { + try { + connection?.close() + } catch (_: Exception) { + } + try { + client?.shutdown() + } catch (_: Exception) { + } + connection = null + client = null + } + + val isConnected: Boolean + get() = !failed && connection != null && connection!!.isOpen + + /** + * Returns a synchronous command API, or null when Redis is unavailable. + */ + fun syncCommands(): io.lettuce.core.api.sync.RedisCommands? = connection?.sync() + + /** + * Returns an asynchronous command API, or null when Redis is unavailable. + */ + fun asyncCommands(): io.lettuce.core.api.async.RedisAsyncCommands? = connection?.async() + } diff --git a/src/main/kotlin/net/azisaba/simplequest/di/ConfigModule.kt b/src/main/kotlin/net/azisaba/simplequest/di/ConfigModule.kt index 356f86b..8e9562b 100644 --- a/src/main/kotlin/net/azisaba/simplequest/di/ConfigModule.kt +++ b/src/main/kotlin/net/azisaba/simplequest/di/ConfigModule.kt @@ -8,6 +8,7 @@ import net.azisaba.simplequest.data.DatabaseConfig import net.azisaba.simplequest.data.DiscordConfig import net.azisaba.simplequest.data.MultiServerConfig import net.azisaba.simplequest.data.PanelConfig +import net.azisaba.simplequest.data.RedisConfig import net.azisaba.simplequest.data.SimpleQuestConfig import java.io.File import java.util.logging.Level @@ -63,4 +64,7 @@ object ConfigModule { @Provides fun providePanelConfig(config: SimpleQuestConfig): PanelConfig = config.panel + + @Provides + fun provideRedisConfig(config: SimpleQuestConfig): RedisConfig = config.redis } diff --git a/src/main/kotlin/net/azisaba/simplequest/di/SimpleQuestComponent.kt b/src/main/kotlin/net/azisaba/simplequest/di/SimpleQuestComponent.kt index e7c9cde..63de2a9 100644 --- a/src/main/kotlin/net/azisaba/simplequest/di/SimpleQuestComponent.kt +++ b/src/main/kotlin/net/azisaba/simplequest/di/SimpleQuestComponent.kt @@ -9,6 +9,7 @@ import net.azisaba.simplequest.database.BackupService import net.azisaba.simplequest.database.DatabaseManager import net.azisaba.simplequest.database.DiscordWebhook import net.azisaba.simplequest.database.MigrationRunner +import net.azisaba.simplequest.database.RedisManager import net.azisaba.simplequest.database.SyncService import net.azisaba.simplequest.infrastructure.di.InfrastructureModule import net.azisaba.simplequest.listener.QuestProgressListener @@ -35,6 +36,8 @@ interface SimpleQuestComponent { fun databaseManager(): DatabaseManager + fun redisManager(): RedisManager + fun questManager(): QuestManager fun questService(): QuestService diff --git a/src/test/kotlin/net/azisaba/simplequest/database/RedisManagerTest.kt b/src/test/kotlin/net/azisaba/simplequest/database/RedisManagerTest.kt new file mode 100644 index 0000000..f0846cb --- /dev/null +++ b/src/test/kotlin/net/azisaba/simplequest/database/RedisManagerTest.kt @@ -0,0 +1,40 @@ +package net.azisaba.simplequest.database + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import net.azisaba.simplequest.data.RedisConfig + +class RedisManagerTest : + FunSpec({ + + context("buildUri") { + test("maps host/port from config (default user, no password)") { + val manager = RedisManager(RedisConfig(host = "redis.local", port = 6380)) + val uri = manager.buildUri() + uri.host shouldBe "redis.local" + uri.port shouldBe 6380 + // Empty user defaults to the `default` Redis user (username is null on the URI). + uri.username shouldBe null + uri.password shouldBe null + } + + test("maps ACL username when user is set") { + val manager = RedisManager(RedisConfig(user = "cache", host = "redis.local", port = 6379)) + val uri = manager.buildUri() + uri.username shouldBe "cache" + } + + test("maps password when set") { + val manager = RedisManager(RedisConfig(host = "redis.local", password = "secret")) + val uri = manager.buildUri() + uri.password?.concatToString() shouldBe "secret" + } + + test("omits username and password when blank") { + val manager = RedisManager(RedisConfig(user = "", password = "")) + val uri = manager.buildUri() + uri.username shouldBe null + uri.password shouldBe null + } + } + })