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
4 changes: 2 additions & 2 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/integrationTest/resources/master-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ database:
password: test

redis:
user: ""
host: localhost
port: 6379
password: ""
Expand Down
1 change: 1 addition & 0 deletions src/integrationTest/resources/readonly-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ database:
password: test

redis:
user: ""
host: localhost
port: 6379
password: ""
Expand Down
1 change: 1 addition & 0 deletions src/integrationTest/resources/slave-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ database:
password: test

redis:
user: ""
host: localhost
port: 6379
password: ""
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/net/azisaba/simplequest/SimpleQuest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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.")
}
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/net/azisaba/simplequest/data/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "",
Expand Down
92 changes: 92 additions & 0 deletions src/main/kotlin/net/azisaba/simplequest/database/RedisManager.kt
Original file line number Diff line number Diff line change
@@ -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<String, String>? = 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<String, String>? = connection?.sync()

/**
* Returns an asynchronous command API, or null when Redis is unavailable.
*/
fun asyncCommands(): io.lettuce.core.api.async.RedisAsyncCommands<String, String>? = connection?.async()
}
4 changes: 4 additions & 0 deletions src/main/kotlin/net/azisaba/simplequest/di/ConfigModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,4 +64,7 @@ object ConfigModule {

@Provides
fun providePanelConfig(config: SimpleQuestConfig): PanelConfig = config.panel

@Provides
fun provideRedisConfig(config: SimpleQuestConfig): RedisConfig = config.redis
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +36,8 @@ interface SimpleQuestComponent {

fun databaseManager(): DatabaseManager

fun redisManager(): RedisManager

fun questManager(): QuestManager

fun questService(): QuestService
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ database:
password: ""

redis:
user: ""
host: "localhost"
port: 6379
password: ""
Expand Down
4 changes: 3 additions & 1 deletion src/test/kotlin/net/azisaba/simplequest/data/ConfigTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
})
2 changes: 2 additions & 0 deletions wiki/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ database:

# ── Redis 接続設定 ──
redis:
user: "" # Redis 6+ ACL のユーザー名(default ユーザー利用時は空文字)
host: "localhost" # Redis のホスト名 or IP
port: 6379 # ポート番号
password: "" # 認証パスワード(requirepass 設定時)
Expand Down Expand Up @@ -74,6 +75,7 @@ MariaDB(または MySQL)への接続情報です。
Redis への接続情報です。キャッシュと PubSub メッセージングに使用されます。

- **password**: Redis 側で `requirepass` を設定している場合に指定。未設定なら空文字 `""` で問題ありません
- **user**: Redis 6 以降の ACL で `default` 以外のユーザーを使う場合に指定。未設定(空文字)なら `default` ユーザーとして接続します

### `maxPartySize` / `partyInviteLimit`

Expand Down
Loading