diff --git a/.data/raw-cache/server/npcs.toml b/.data/raw-cache/server/npcs.toml index e472a5caa..3701a5bd2 100644 --- a/.data/raw-cache/server/npcs.toml +++ b/.data/raw-cache/server/npcs.toml @@ -3773,6 +3773,8 @@ inherit = "npc.mm2_demon_gorilla_2_magic" "param.death_anim"="seq.demonic_gorilla_death" "param.killcount_varp"="varp.kc_demonic_gorilla" "param.killcount_notify"=false + +[[npc]] id = "npc.0_43_51_saltfish" inherit = "npc.0_43_51_saltfish" contentGroup = "content.net_bait_fishing_spot" diff --git a/api/net/src/main/kotlin/org/rsmod/api/net/central/embed/CentralEmbeddedLifecycle.kt b/api/net/src/main/kotlin/org/rsmod/api/net/central/embed/CentralEmbeddedLifecycle.kt index 1e6abbb1c..b786f5978 100644 --- a/api/net/src/main/kotlin/org/rsmod/api/net/central/embed/CentralEmbeddedLifecycle.kt +++ b/api/net/src/main/kotlin/org/rsmod/api/net/central/embed/CentralEmbeddedLifecycle.kt @@ -1,12 +1,13 @@ package org.rsmod.api.net.central.embed import com.github.michaelbull.logging.InlineLogger -import dev.or2.central.util.config.centralRuntimeConfigFromJdbc -import dev.or2.central.embed.OpenRuneCentralEmbeddedServer import dev.or2.central.auth.PasswordAuthConfig +import dev.or2.central.embed.OpenRuneCentralEmbeddedServer +import dev.or2.central.util.config.centralRuntimeConfigFromJdbc import jakarta.inject.Inject import jakarta.inject.Singleton import java.sql.DriverManager +import java.sql.SQLException import org.rsmod.api.db.jdbc.EmbeddedSameInstancePostgres import org.rsmod.api.db.jdbc.PostgresPublicSchemaReset import org.rsmod.api.net.central.OpenRuneCentralWorldLink @@ -81,10 +82,13 @@ constructor( server = centralServer } catch (t: Throwable) { runCatching { centralServer.stop() } - if (!usesEmbeddedJdbc) { + if (!usesEmbeddedJdbc || !CentralStartupFailure.indicatesUninitializedSchema(t)) { + // Either a non-embedded database (never safe to wipe someone else's DB), or a + // startup failure unrelated to the schema itself - e.g. a port-bind conflict. + // Only a genuinely missing/uninitialized schema is safe to auto-recover from. throw t } - logger.warn(t) { "Embedded OpenRune Central failed to start." } + logger.warn(t) { "Embedded OpenRune Central failed to start: schema appears uninitialized." } runCatching { DriverManager.getConnection(jdbc, dbUser, dbPassword).use { conn -> conn.autoCommit = true @@ -108,3 +112,35 @@ constructor( server = null } } + +/** + * Classifies whether a Central startup failure is safe to auto-recover from by wiping the + * embedded database's `public` schema. Only a genuinely missing/uninitialized schema qualifies - + * unrelated failures such as a port-bind conflict must never trigger a wipe. + */ +internal object CentralStartupFailure { + /** + * Only true for SQL errors whose SQLSTATE indicates Central's expected schema/tables don't + * exist yet (a fresh, never-initialized embedded database) - not for unrelated startup + * failures such as a port-bind conflict, which have no such cause. Walks the full cause chain + * since the originating [SQLException] is typically wrapped by higher-level framework/driver + * exceptions before reaching the caller's catch block. + */ + internal fun indicatesUninitializedSchema(t: Throwable): Boolean { + var cause: Throwable? = t + while (cause != null) { + if (cause is SQLException && cause.sqlState in UNINITIALIZED_SCHEMA_SQL_STATES) { + return true + } + val next = cause.cause + cause = if (next === cause) null else next + } + return false + } + + /** + * Postgres SQLSTATEs for missing schema objects: undefined_table, undefined_column, + * invalid_schema_name. See https://www.postgresql.org/docs/current/errcodes-appendix.html + */ + private val UNINITIALIZED_SCHEMA_SQL_STATES = setOf("42P01", "42703", "3F000") +} diff --git a/api/net/src/test/kotlin/org/rsmod/api/net/central/embed/CentralStartupFailureTest.kt b/api/net/src/test/kotlin/org/rsmod/api/net/central/embed/CentralStartupFailureTest.kt new file mode 100644 index 000000000..888b8cf45 --- /dev/null +++ b/api/net/src/test/kotlin/org/rsmod/api/net/central/embed/CentralStartupFailureTest.kt @@ -0,0 +1,53 @@ +package org.rsmod.api.net.central.embed + +import java.net.BindException +import java.sql.SQLException +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** + * Verifies [CentralStartupFailure.indicatesUninitializedSchema] only recognizes SQL errors that + * mean Central's schema hasn't been created yet, not unrelated startup failures such as a port + * already being bound - the bug the real fix in [CentralEmbeddedLifecycle] addresses (the + * embedded database used to get wiped on any startup exception whatsoever). + */ +class CentralStartupFailureTest { + @Test + fun `port bind conflict does not indicate an uninitialized schema`() { + val bindFailure = IllegalStateException("boom", BindException("Address already in use")) + assertFalse(CentralStartupFailure.indicatesUninitializedSchema(bindFailure)) + } + + @Test + fun `unrelated runtime exception does not indicate an uninitialized schema`() { + assertFalse( + CentralStartupFailure.indicatesUninitializedSchema(RuntimeException("something else broke")), + ) + } + + @Test + fun `undefined table sql error indicates an uninitialized schema`() { + val sqlEx = SQLException("relation \"world\" does not exist", "42P01") + val wrapped = RuntimeException("startup failed", sqlEx) + assertTrue(CentralStartupFailure.indicatesUninitializedSchema(wrapped)) + } + + @Test + fun `undefined column sql error indicates an uninitialized schema`() { + val sqlEx = SQLException("column \"foo\" does not exist", "42703") + assertTrue(CentralStartupFailure.indicatesUninitializedSchema(sqlEx)) + } + + @Test + fun `invalid schema name sql error indicates an uninitialized schema`() { + val sqlEx = SQLException("schema \"public\" does not exist", "3F000") + assertTrue(CentralStartupFailure.indicatesUninitializedSchema(sqlEx)) + } + + @Test + fun `unrelated sql error does not indicate an uninitialized schema`() { + val sqlEx = SQLException("connection refused", "08001") + assertFalse(CentralStartupFailure.indicatesUninitializedSchema(sqlEx)) + } +}