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
2 changes: 2 additions & 0 deletions .data/raw-cache/server/npcs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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))
}
}