diff --git a/README.md b/README.md index 8133f93..6bd3325 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,5 @@ git submodule update --init --recursive `MoonBridgeBackend` 后端宿主与独立 API 已通过两个真实 Uranium / Java 8 服务端的注册、通信、主线程回调、插件停用、重连及注销/租约验收,见[改名与独立 API 验收记录](smoke/results/2026-09-27-moonbridge-api.md)。这是无玩家、隔离服务端上的功能验收,不代表生产容量测试。 最新初次路由专项记录包含自动化测试,以及通过实际 `ProxyMain` 连接真实 Java 8 Uranium 后端的协议探针结果;未进行本分支的完整模组包玩家视觉验收,见[路由验收记录](smoke/results/2026-09-27-initial-routing.md)。2026-09-26 Prism 记录是当日代码和环境下的历史真实客户端及配对测量,不应视作当前路由改动的完整模组包证明,见[历史验收记录](benchmarks/results/2026-09-26-prism-pack.md)。生产容量仍需按部署环境测量。已知 Uranium 候选服登录停滞的调查见[记录](smoke/results/2026-09-26-uranium-login-stalls.md)及 [Uranium #585](https://git.nest.potatolab.uk:8443/TDLM/Uranium/issues/585)。 + +生产权限存储统一使用 MariaDB 13.0.2;默认配置、已有数据边界与实际双实例验收入口见 [MariaDB 权限数据库](docs/mariadb.md)。 diff --git a/docs/mariadb.md b/docs/mariadb.md new file mode 100644 index 0000000..6c2d6e4 --- /dev/null +++ b/docs/mariadb.md @@ -0,0 +1,40 @@ +# MariaDB 13.0.2 权限数据库 + +MoonBridge 的 SQL 存储位于原生 LuckPerms 平台插件:保存用户权限、组、继承、track 和管理记录,同时可提供 SQL 消息同步。生产目标为 MariaDB Server 13.0.2。新生成的 LuckPerms 配置默认选择 `storage-method: mariadb`;启用插件前需要配置共享数据库。代理核心没有另一个独立 JDBC 数据库。 + +LuckPerms 使用其原生 MariaDB 存储实现、表结构和连接工厂,按 `data.address`、数据库名及凭据生成 `jdbc:mariadb:` 连接。固定的官方上游版本由依赖管理器下载、校验和隔离 MariaDB Connector/J **3.5.2**。这是 LuckPerms 自身管理的驱动,不是由 MoonBridge 普通 Gradle runtime 依赖打包的驱动;服务端版本 **13.0.2** 与驱动版本分别管理。 + +配置示例、插件数据目录和自动 SQL/可选 Redis 同步见 [权限文档](permissions.md)。远程部署使用 `data.pool-settings.properties.sslMode: verify-full`,并按 [MariaDB TLS 文档](https://mariadb.com/docs/connectors/mariadb-connector-j/using-tls-ssl-with-mariadb-java-connector) 配置和验证数据库身份;本机开发示例及隔离验收显式使用 `disable`。 + +## 已有数据与部署 + +已有 `config.yml` 不会被默认资源覆盖。手动更新 `storage-method: mariadb`、数据库连接信息和 TLS 属性,保留原有 `table-prefix` 与权限数据。不要因为切换驱动删除表或创建一个空的权限数据库替代已有数据。 + +PDS、AetherShard 与 LuckPerms 可以使用同一台 MariaDB 服务,各自保留独立数据库/表与数据权威。多台 MoonBridge 代理使用同一个 LuckPerms 数据库和表前缀,各自设置不同的 `server`。 + +数据库服务迁移需要另行停写、备份、独立恢复并核验用户、组和继承数据,然后统一切换所有代理及共用该数据的 Bukkit LuckPerms。保留源数据库作为回退来源;禁止同时向两套数据库写入。本次代码适配不执行现有服务的数据迁移。 + +## 真实数据库验收 + +需要 JDK 25、Node.js 和 Docker。先初始化固定上游并构建分发包,普通 `check` 保留显式 H2 的离线内部验收: + +```sh +git submodule update --init --recursive +bash ./gradlew clean check :proxy-core:distZip --no-daemon --max-workers=2 +mkdir -p build/permission-network-classes +javac -cp 'proxy-core/build/install/moonbridge/lib/*' -d build/permission-network-classes smoke/PermissionNetworkAcceptance.java +node smoke/permission-services.cjs start +``` + +运行下面两轮后,务必执行 stop(失败也需清理): + +```sh +node smoke/permission-network.cjs sql outage +node smoke/permission-network.cjs redis outage +node smoke/permission-services.cjs query +node smoke/permission-services.cjs stop +``` + +fixture 只创建带本轮所有权标签的本机容器,固定使用 `mariadb:13.0.2` 并检查实际服务器版本。SQL 模式使用 MariaDB 原生自动 SQL messenger;Redis 模式仍使用 MariaDB 保存权限。测试执行双实例授权/撤销/组继承、上下文、临时权限、插件重启持久化,以及 MariaDB/Redis 停机恢复。普通构建不会自动执行这些外部服务测试。 + +stop 删除本轮容器和数据卷、移除凭据文件,并对保留的测试配置密码脱敏。[当前验收记录](../smoke/results/2026-10-02-mariadb.md) 与早期 MySQL/H2/游戏客户端证据分别记录,避免将旧运行结果当成本次 MariaDB 联测。 diff --git a/docs/permissions.md b/docs/permissions.md index 2e59f17..e87d6a3 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -74,7 +74,7 @@ plugins: 插件数据位于 `plugins/data/dev.moonbridge.luckperms.LuckPermsMoonBridgePlugin/`;LuckPerms 的存储与规则配置使用此目录中的配置文件。不要同时启用另一个权限提供者。 -默认存储为本地 H2。首次启动会由 LuckPerms 自身的依赖管理器下载带固定校验值的运行库,缓存到数据目录的 `libs/`;离线部署应先在可联网环境完成启动并带上该缓存。插件 JAR 也作为 `uk.potatolab.moonbridge:luckperms-moonbridge` 发布,版本与同次构建的 MoonBridge API 一致。 +新配置默认使用共享 MariaDB 13.0.2,启用插件前需创建权限数据库并填写连接信息。单机开发与内部验收可显式选择 H2。首次启动会由 LuckPerms 自身的依赖管理器下载带固定校验值的运行库,缓存到数据目录的 `libs/`;离线部署应先在可联网环境完成启动并带上该缓存。插件 JAR 也作为 `uk.potatolab.moonbridge:luckperms-moonbridge` 发布,版本与同次构建的 MoonBridge API 一致。 组、继承、临时节点、缓存、存储和管理命令由 LuckPerms 引擎处理。MoonBridge 的 `backend` 上下文在适配器中映射到 LuckPerms 的 `world` 上下文;LuckPerms 的 `server` 保留其配置的代理服务器标识,沿用 Velocity 的语义。 @@ -82,27 +82,32 @@ plugins: 固定的上游 API 枚举没有 MoonBridge 项,因此 LuckPerms 信息页显示 `Standalone - MoonBridge`,并不表示运行的是上游独立服务。平台插件使用隔离的类加载器;业务插件应依赖 MoonBridge 的权限接口,当前不提供跨插件的 `LuckPermsProvider.get()` 服务访问。 -同步使用 LuckPerms 原生实现。`messaging-service: auto` 根据共享 SQL 或启用的 Redis 等配置选择消息同步;默认本地 H2 不启用 messenger,`sync-minutes: -1` 不执行周期重载。上游将旧值 `none` 按 `auto` 处理,不应把它当作强制关闭。Velocity 的 `pluginmsg` 不适用于 MoonBridge。 +同步使用 LuckPerms 原生实现。`messaging-service: auto` 根据共享 SQL 或启用的 Redis 等配置选择消息同步;显式选择本地 H2 时不启用 messenger,`sync-minutes: -1` 不执行周期重载。上游将旧值 `none` 按 `auto` 处理,不应把它当作强制关闭。Velocity 的 `pluginmsg` 不适用于 MoonBridge。 -### 使用 MySQL 与跨代理同步 +### 使用 MariaDB 13.0.2 与跨代理同步 在 LuckPerms 数据目录的 `config.yml` 中设置以下配置,然后重启代理。数据库需预先创建,账号需能创建、查询和修改 LuckPerms 的表: ```yaml server: proxy-1 -storage-method: mysql +storage-method: mariadb data: - address: "mysql.example.net:3306" + address: "mariadb.example.net:3306" database: moonbridge_permissions username: moonbridge password: "替换为数据库密码" table-prefix: "luckperms_" + pool-settings: + properties: + sslMode: verify-full messaging-service: auto ``` -多台代理使用相同的数据库和表前缀,分别设置各自的 `server`。MySQL 本身即可提供 LuckPerms 的 SQL 消息同步,无需额外安装 Redis。身份模式与 UUID 必须一致;共享存储不会自动转换离线 UUID 与正版 UUID。 +MariaDB 的远程连接需按证书部署配置 `sslMode: verify-full`;测试和默认本机示例使用 `disable`。原生 LuckPerms 按地址、端口和数据库名组成 `jdbc:mariadb:` URL,并由自身依赖管理器下载、校验和隔离 MariaDB 驱动;当前固定上游使用 Connector/J 3.5.2。已有配置不会自动覆盖,需手动更新 `storage-method` 和连接属性,并保留已有表前缀及数据。 -若已有 Redis,可继续以 MySQL 存储权限,将消息同步改为: +多台代理使用相同的数据库和表前缀,分别设置各自的 `server`。MariaDB 本身即可提供 LuckPerms 的 SQL 消息同步,无需额外安装 Redis。身份模式与 UUID 必须一致;共享存储不会自动转换离线 UUID 与正版 UUID。 + +若已有 Redis,可继续以 MariaDB 存储权限,将消息同步改为: ```yaml messaging-service: redis @@ -112,9 +117,9 @@ redis: password: "替换为Redis密码" ``` -Redis 负责通知其他实例重载权限,权限数据仍保存在 MySQL。Redis 中断期间不会保证其他实例即时看到修改;恢复连接后的新通知可触发刷新,也可以在需要时使用 `lp sync` 主动同步。数据库中断时,已加载玩家可继续使用现有缓存,新玩家加载失败会被拒绝登录;缓存不是数据库最新状态的保证。 +Redis 负责通知其他实例重载权限,权限数据仍保存在 MariaDB。Redis 中断期间不会保证其他实例即时看到修改;恢复连接后的新通知可触发刷新,也可以在需要时使用 `lp sync` 主动同步。数据库中断时,已加载玩家可继续使用现有缓存,新玩家加载失败会被拒绝登录;缓存不是数据库最新状态的保证。 -MySQL 8.4 与 Redis 7.4 已用实际分发包完成双实例同步、重启持久化和故障恢复验收,详见 [验收记录](../smoke/results/2026-09-28-permissions.md)。其他数据库实现由上游提供,本次没有据此宣称 PostgreSQL 或 MariaDB 已完成联调。 +此前 MySQL 8.4 与 Redis 7.4 的结果保留为 [历史验收记录](../smoke/results/2026-09-28-permissions.md)。当前 MariaDB 13.0.2 的验收入口和记录见 [MariaDB 验证](mariadb.md)。 backend 可安装与其 Minecraft / Java 版本兼容的原生 Bukkit LuckPerms,使用相同的身份模式、共享数据库与 LuckPerms 支持的同步方式;MoonBridge 平台插件 JAR 只供代理使用。共享数据库不意味着所有场景的检查结果相同:代理知道后端名称,但不知道玩家在 Bukkit 世界中的位置或其他后端私有上下文。玩家首次登录的权限加载不依赖某个 backend 在线。 diff --git a/luckperms-moonbridge/src/main/resources/config.yml b/luckperms-moonbridge/src/main/resources/config.yml index f37b93b..2c0d7ff 100644 --- a/luckperms-moonbridge/src/main/resources/config.yml +++ b/luckperms-moonbridge/src/main/resources/config.yml @@ -71,9 +71,10 @@ server: proxy # | and all stored in the same file by switching to a combined storage variant. # | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' # -# - A H2 database is the default option. +# - MoonBridge defaults to shared MariaDB 13.0.2. Configure the database before enabling this plugin. +# - H2 remains available when explicitly selected for single-process development. # - If you want to edit data manually in "traditional" storage files, we suggest using YAML. -storage-method: h2 +storage-method: mariadb # The following block defines the settings for remote database storage methods. # @@ -85,22 +86,22 @@ data: # - The standard DB engine port is used by default # (MySQL: 3306, PostgreSQL: 5432, MongoDB: 27017) # - Specify as "host:port" if differs - address: localhost + address: 127.0.0.1:3306 # The name of the database to store LuckPerms data in. # - This must be created already. Don't worry about this setting if you're using MongoDB. - database: minecraft + database: moonbridge_permissions # Credentials for the database. - username: root + username: moonbridge password: '' - # These settings apply to the MySQL connection pool. + # These settings apply to the SQL connection pool. # - The default values will be suitable for the majority of users. # - Do not change these settings unless you know what you're doing! pool-settings: - # Sets the maximum size of the MySQL connection pool. + # Sets the maximum size of the SQL connection pool. # - Basically this value will determine the maximum number of actual # connections to the database backend. # - More information about determining the size of connection pools can be found here: @@ -130,18 +131,10 @@ data: # This setting allows you to define extra properties for connections. # - # By default, the following options are set to enable utf8 encoding. (you may need to remove - # these if you are using PostgreSQL) - # useUnicode: true - # characterEncoding: utf8 - # - # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and - # 'verifyServerCertificate' options below. + # The shipped address is loopback; production network connections should use verify-full + # with a configured database TLS identity and client trust. properties: - useUnicode: true - characterEncoding: utf8 - #useSSL: false - #verifyServerCertificate: false + sslMode: disable # The prefix for all LuckPerms SQL tables. # diff --git a/smoke/PermissionNetworkAcceptance.java b/smoke/PermissionNetworkAcceptance.java index 70564e2..312a65f 100644 --- a/smoke/PermissionNetworkAcceptance.java +++ b/smoke/PermissionNetworkAcceptance.java @@ -35,10 +35,10 @@ public final class PermissionNetworkAcceptance { } String configTemplate = Files.readString(Path.of(args[1]).toAbsolutePath()); if (mode.equals("sql")) { - require(configTemplate.contains("storage-method: mysql"), "SQL mode requires MySQL storage"); + require(configTemplate.contains("storage-method: mariadb"), "SQL mode requires MariaDB storage"); require(configTemplate.contains("messaging-service: auto"), "SQL mode must exercise automatic SQL messenger selection"); } else { - require(configTemplate.contains("storage-method: mysql"), "Redis mode still requires shared MySQL persistence"); + require(configTemplate.contains("storage-method: mariadb"), "Redis mode still requires shared MariaDB persistence"); require(configTemplate.contains("messaging-service: redis"), "Redis mode requires the Redis messenger"); } @@ -94,7 +94,7 @@ public final class PermissionNetworkAcceptance { == PermissionResult.UNDEFINED, "temporary permission expiry"); if (outage && mode.equals("sql")) { - runMysqlOutage(first, second, a, b, playersA, playerId); + runMariaDbOutage(first, second, a, b, playersA, playerId); } else if (outage) { runRedisOutage(first, second, a, b, playerId, user); } @@ -147,49 +147,49 @@ public final class PermissionNetworkAcceptance { return configured; } - private static void runMysqlOutage(Running first, Running second, PlayerView a, PlayerView b, + private static void runMariaDbOutage(Running first, Running second, PlayerView a, PlayerView b, TestPlayers playersA, UUID playerId) throws Exception { - command(first, "lp user " + playerId + " permission set acceptance.network.mysql-cache true"); - expect(first, a, "acceptance.network.mysql-cache", PermissionResult.ALLOW); - expect(second, b, "acceptance.network.mysql-cache", PermissionResult.ALLOW); + command(first, "lp user " + playerId + " permission set acceptance.network.mariadb-cache true"); + expect(first, a, "acceptance.network.mariadb-cache", PermissionResult.ALLOW); + expect(second, b, "acceptance.network.mariadb-cache", PermissionResult.ALLOW); - handshake("REQUEST_STOP_MYSQL", "MYSQL_STOPPED"); - expect(first, a, "acceptance.network.mysql-cache", PermissionResult.ALLOW); - expect(second, b, "acceptance.network.mysql-cache", PermissionResult.ALLOW); + handshake("REQUEST_STOP_MARIADB", "MARIADB_STOPPED"); + expect(first, a, "acceptance.network.mariadb-cache", PermissionResult.ALLOW); + expect(second, b, "acceptance.network.mariadb-cache", PermissionResult.ALLOW); UUID freshId = UUID.randomUUID(); - PlayerView fresh = new PlayerView(new PlayerIdentity(freshId, 40), "FreshMysqlProbe", "lobby"); + PlayerView fresh = new PlayerView(new PlayerIdentity(freshId, 40), "FreshMariaProbe", "lobby"); playersA.current = fresh; try { first.host.permissionService().prepare(fresh).toCompletableFuture().get(20, TimeUnit.SECONDS); - throw new AssertionError("prepare unexpectedly succeeded while MySQL was stopped"); + throw new AssertionError("prepare unexpectedly succeeded while MariaDB was stopped"); } catch (ExecutionException | CompletionException | TimeoutException expected) { - first.transcript.add("MYSQL_OUTAGE_FRESH_PREPARE_FAILED " + expected.getClass().getSimpleName()); + first.transcript.add("MARIADB_OUTAGE_FRESH_PREPARE_FAILED " + expected.getClass().getSimpleName()); } - require(first.probe.context.permissions().check(fresh.identity(), "acceptance.network.mysql-cache") + require(first.probe.context.permissions().check(fresh.identity(), "acceptance.network.mariadb-cache") == PermissionResult.UNAVAILABLE, - "failed new-user preparation must remain UNAVAILABLE during MySQL outage"); + "failed new-user preparation must remain UNAVAILABLE during MariaDB outage"); - handshake("REQUEST_START_MYSQL", "MYSQL_STARTED"); + handshake("REQUEST_START_MARIADB", "MARIADB_STARTED"); playersA.current = a; PlayerView recovered = null; Throwable lastFailure = null; for (int attempt = 0; attempt < 6 && recovered == null; attempt++) { - PlayerView candidate = new PlayerView(new PlayerIdentity(freshId, 50 + attempt), "FreshMysqlProbe", "lobby"); + PlayerView candidate = new PlayerView(new PlayerIdentity(freshId, 50 + attempt), "FreshMariaProbe", "lobby"); try { first.host.permissionService().prepare(candidate).toCompletableFuture().get(15, TimeUnit.SECONDS); recovered = candidate; - first.transcript.add("MYSQL_OUTAGE_RECOVERED_PREPARE attempt=" + (attempt + 1)); + first.transcript.add("MARIADB_OUTAGE_RECOVERED_PREPARE attempt=" + (attempt + 1)); } catch (ExecutionException | CompletionException | TimeoutException failure) { lastFailure = failure; - first.transcript.add("MYSQL_OUTAGE_RETRY_FAILED attempt=" + (attempt + 1)); + first.transcript.add("MARIADB_OUTAGE_RETRY_FAILED attempt=" + (attempt + 1)); Thread.sleep(1000); } } - if (recovered == null) throw new AssertionError("prepare did not recover after MySQL restart", lastFailure); + if (recovered == null) throw new AssertionError("prepare did not recover after MariaDB restart", lastFailure); expect(first, recovered, "acceptance.network.no-such-recovery-node", PermissionResult.UNDEFINED); - command(first, "lp user " + freshId + " permission set acceptance.network.mysql-recovered true"); - expect(first, recovered, "acceptance.network.mysql-recovered", PermissionResult.ALLOW); + command(first, "lp user " + freshId + " permission set acceptance.network.mariadb-recovered true"); + expect(first, recovered, "acceptance.network.mariadb-recovered", PermissionResult.ALLOW); first.host.permissionService().release(recovered.identity()); } diff --git a/smoke/permission-network.cjs b/smoke/permission-network.cjs index fe0adf1..da60bde 100644 --- a/smoke/permission-network.cjs +++ b/smoke/permission-network.cjs @@ -8,7 +8,7 @@ if (!['sql','redis'].includes(mode)) throw new Error('Usage: node smoke/permissi const outage = process.argv[3] === 'outage'; const log = fs.createWriteStream(path.join(root,'build',`permission-network-${mode}${outage?'-outage':''}.log`)); const args = ['-cp',`${path.join(root,'proxy-core/build/install/moonbridge/lib/*')}${path.delimiter}${path.join(root,'build/permission-network-classes')}`, - 'PermissionNetworkAcceptance','proxy-core/build/install/moonbridge',`build/permission-services/${mode==='sql'?'mysql':'redis'}-base.yml`,'build/permission-network',mode]; + 'PermissionNetworkAcceptance','proxy-core/build/install/moonbridge',`build/permission-services/${mode==='sql'?'mariadb':'redis'}-base.yml`,'build/permission-network',mode]; if (outage) args.push('outage'); const child = spawn('java',args,{cwd:root,windowsHide:true,stdio:['pipe','pipe','pipe']}); let pending = ''; @@ -18,7 +18,7 @@ child.stdout.on('data',chunk=>{ let end; while ((end=pending.indexOf('\n'))>=0) { const line=pending.slice(0,end).trim(); pending=pending.slice(end+1); - const request=/^REQUEST_(STOP|START)_(MYSQL|REDIS)$/.exec(line); + const request=/^REQUEST_(STOP|START)_(MARIADB|REDIS)$/.exec(line); if (request) { const [,verb,kind]=request; console.log(line); diff --git a/smoke/permission-services.cjs b/smoke/permission-services.cjs index 1108d90..8be1ee3 100644 --- a/smoke/permission-services.cjs +++ b/smoke/permission-services.cjs @@ -28,26 +28,26 @@ function owned(id, run) { throw new Error('Container ownership mismatch'); } function clearTestSecrets() { - for (const file of ['mysql.env','redis.conf']) { + for (const file of ['mariadb.env','redis.conf']) { const target=path.join(dir,file); if(fs.existsSync(target)) fs.unlinkSync(target); } const redact=file=>{if(fs.existsSync(file))fs.writeFileSync(file,fs.readFileSync(file,'utf8').replace(/^(\s*password:).*$/gm,'$1 ""'));}; - for (const file of ['mysql-base.yml','redis-base.yml']) redact(path.join(dir,file)); + for (const file of ['mariadb-base.yml','redis-base.yml']) redact(path.join(dir,file)); const evidence=path.join(root,'build','permission-network'); if (fs.existsSync(evidence)) for (const entry of fs.readdirSync(evidence,{withFileTypes:true})) { - if (!entry.isDirectory() || !/^permission-network-(sql|redis)-\d+$/.test(entry.name)) continue; + if (!entry.isDirectory() || !/^permission-network-(sql|redis)-/.test(entry.name)) continue; for (const instance of ['instance-a','instance-b']) redact(path.join(evidence,entry.name,instance,'plugins','data','dev.moonbridge.luckperms.LuckPermsMoonBridgePlugin','config.yml')); } } function cleanup(state) { - for (const kind of ['redis','mysql']) if (state[kind]) {owned(state[kind],state.run);docker(['rm','-f','-v',state[kind]]);} + for (const kind of ['redis','mariadb']) if (state[kind]) {owned(state[kind],state.run);docker(['rm','-f','-v',state[kind]]);} clearTestSecrets(); fs.renameSync(stateFile,path.join(dir,`completed-${state.run}.json`)); } async function ready(id, kind) { - const command = kind === 'mysql' - ? 'MYSQL_PWD="$MYSQL_PASSWORD" mysql -u"$MYSQL_USER" "$MYSQL_DATABASE" -N -e "SELECT 1"' + const command = kind === 'mariadb' + ? 'MYSQL_PWD="$MARIADB_PASSWORD" mariadb -u"$MARIADB_USER" "$MARIADB_DATABASE" -N -e "SELECT 1"' : 'REDISCLI_AUTH=$(sed -n "s/^requirepass //p" /usr/local/etc/redis/redis.conf) redis-cli ping'; for (let i=0; i<90; i++) { const r = spawnSync('docker',['exec',id,'sh','-c',command],{encoding:'utf8',windowsHide:true}); @@ -58,43 +58,46 @@ async function ready(id, kind) { } async function main() { const action = process.argv[2]; - fs.mkdirSync(dir,{recursive:true}); + fs.mkdirSync(dir,{recursive:true,mode:0o700}); if (action === 'start') { if (fs.existsSync(stateFile)) throw new Error('Existing fixture state; clean it before starting another run'); const run = crypto.randomBytes(6).toString('hex'); - const mysqlPassword = crypto.randomBytes(24).toString('hex'); + const mariadbPassword = crypto.randomBytes(24).toString('hex'); const redisPassword = crypto.randomBytes(24).toString('hex'); - const env = path.join(dir,'mysql.env'); + const env = path.join(dir,'mariadb.env'); const redisConfig = path.join(dir,'redis.conf'); - fs.writeFileSync(env,`MYSQL_ROOT_PASSWORD=${crypto.randomBytes(24).toString('hex')}\nMYSQL_DATABASE=moonbridge_permissions\nMYSQL_USER=moonbridge\nMYSQL_PASSWORD=${mysqlPassword}\n`); - fs.writeFileSync(redisConfig,`bind 0.0.0.0\nprotected-mode yes\nrequirepass ${redisPassword}\nappendonly no\n`); + fs.writeFileSync(env,`MARIADB_ROOT_PASSWORD=${crypto.randomBytes(24).toString('hex')}\nMARIADB_DATABASE=moonbridge_permissions\nMARIADB_USER=moonbridge\nMARIADB_PASSWORD=${mariadbPassword}\n`,{mode:0o600}); + fs.writeFileSync(redisConfig,`bind 0.0.0.0\nprotected-mode yes\nrequirepass ${redisPassword}\nappendonly no\n`,{mode:0o600}); const state = {run}; const save = () => fs.writeFileSync(stateFile,JSON.stringify(state,null,2)); save(); createdThisRun = true; // Bind an explicit free port: Docker can change an ephemeral mapping across stop/start. - state.mysqlPort = await freePort(); - state.mysql = docker(['run','-d','--name',`moonbridge-permissions-mysql-${run}`,'--label',`${label}=${run}`,'-p',`127.0.0.1:${state.mysqlPort}:3306`,'--env-file',env,'mysql:8.4']); save(); + state.mariadbPort = await freePort(); + state.mariadb = docker(['run','-d','--name',`moonbridge-permissions-mariadb-${run}`,'--label',`${label}=${run}`,'-p',`127.0.0.1:${state.mariadbPort}:3306`,'--env-file',env,'mariadb:13.0.2']); save(); state.redisPort = await freePort(); state.redis = docker(['run','-d','--name',`moonbridge-permissions-redis-${run}`,'--label',`${label}=${run}`,'-p',`127.0.0.1:${state.redisPort}:6379`,'--mount',`type=bind,source=${redisConfig},target=/usr/local/etc/redis/redis.conf,readonly`,'redis:7.4','redis-server','/usr/local/etc/redis/redis.conf']); save(); - await Promise.all([ready(state.mysql,'mysql'),ready(state.redis,'redis')]); - state.mysqlPort = Number(docker(['port',state.mysql,'3306/tcp']).split(':').pop()); + await Promise.all([ready(state.mariadb,'mariadb'),ready(state.redis,'redis')]); + const serverVersion = docker(['exec',state.mariadb,'sh','-c','MYSQL_PWD="$MARIADB_PASSWORD" mariadb -u"$MARIADB_USER" "$MARIADB_DATABASE" -N -e "SELECT VERSION()"']); + if (!serverVersion.startsWith('13.0.2-')) throw new Error('Fixture requires MariaDB 13.0.2'); + state.serverVersion = serverVersion; + state.mariadbPort = Number(docker(['port',state.mariadb,'3306/tcp']).split(':').pop()); state.redisPort = Number(docker(['port',state.redis,'6379/tcp']).split(':').pop()); - state.mysqlImage = docker(['image','inspect','mysql:8.4','--format','{{index .RepoDigests 0}}']); + state.mariadbImage = docker(['image','inspect','mariadb:13.0.2','--format','{{index .RepoDigests 0}}']); state.redisImage = docker(['image','inspect','redis:7.4','--format','{{index .RepoDigests 0}}']); - for (const mode of ['mysql','redis']) { - const config = `server: "@SERVER@"\nstorage-method: mysql\ndata:\n address: "127.0.0.1:${state.mysqlPort}"\n database: moonbridge_permissions\n username: moonbridge\n password: "${mysqlPassword}"\n pool-settings:\n maximum-pool-size: 4\n minimum-idle: 1\n connection-timeout: 5000\n properties:\n useSSL: false\n allowPublicKeyRetrieval: true\n table-prefix: "${mode}_lp_"\nmessaging-service: ${mode === 'mysql' ? 'auto' : 'redis'}\nsync-minutes: -1\nredis:\n enabled: ${mode === 'redis'}\n address: "127.0.0.1:${state.redisPort}"\n password: "${redisPassword}"\n`; - fs.writeFileSync(path.join(dir,`${mode}-base.yml`),config); + for (const mode of ['mariadb','redis']) { + const config = `server: "@SERVER@"\nstorage-method: mariadb\ndata:\n address: "127.0.0.1:${state.mariadbPort}"\n database: moonbridge_permissions\n username: moonbridge\n password: "${mariadbPassword}"\n pool-settings:\n maximum-pool-size: 4\n minimum-idle: 1\n connection-timeout: 5000\n properties:\n sslMode: disable\n table-prefix: "${mode}_lp_"\nmessaging-service: ${mode === 'mariadb' ? 'auto' : 'redis'}\nsync-minutes: -1\nredis:\n enabled: ${mode === 'redis'}\n address: "127.0.0.1:${state.redisPort}"\n password: "${redisPassword}"\n`; + fs.writeFileSync(path.join(dir,`${mode}-base.yml`),config,{mode:0o600}); } save(); - console.log(JSON.stringify({ready:true,mysqlPort:state.mysqlPort,redisPort:state.redisPort,mysqlImage:state.mysqlImage,redisImage:state.redisImage})); + console.log(JSON.stringify({ready:true,mariadbPort:state.mariadbPort,redisPort:state.redisPort,mariadbImage:state.mariadbImage,redisImage:state.redisImage})); } else if (action === 'stop') { const state = readState(); cleanup(state); console.log('Fixture containers removed; test credentials removed from retained configs'); } else if (action === 'query') { - const state = readState(); owned(state.mysql,state.run); - const sql = "SELECT VERSION(); SHOW TABLES; SELECT COUNT(*) FROM mysql_lp_user_permissions; SELECT COUNT(*) FROM redis_lp_user_permissions; SELECT uuid,permission,value FROM mysql_lp_user_permissions WHERE permission='acceptance.network.mysql-recovered'; SELECT uuid,permission,value FROM redis_lp_user_permissions WHERE permission IN ('acceptance.network.redis-during-outage','acceptance.network.redis-after-recovery');"; - console.log(docker(['exec',state.mysql,'sh','-c',`MYSQL_PWD="$MYSQL_PASSWORD" mysql -u"$MYSQL_USER" "$MYSQL_DATABASE" -N -e "${sql}"`])); + const state = readState(); owned(state.mariadb,state.run); + const sql = "SELECT VERSION(); SHOW TABLES; SELECT COUNT(*) FROM mariadb_lp_user_permissions; SELECT COUNT(*) FROM redis_lp_user_permissions; SELECT uuid,permission,value FROM mariadb_lp_user_permissions WHERE permission='acceptance.network.mariadb-recovered'; SELECT uuid,permission,value FROM redis_lp_user_permissions WHERE permission IN ('acceptance.network.redis-during-outage','acceptance.network.redis-after-recovery');"; + console.log(docker(['exec',state.mariadb,'sh','-c',`MYSQL_PWD="$MARIADB_PASSWORD" mariadb -u"$MARIADB_USER" "$MARIADB_DATABASE" -N -e "${sql}"`])); console.log(docker(['exec',state.redis,'redis-server','--version'])); } else if (action === 'wait-redis-subscribers') { const state = readState(); owned(state.redis,state.run); @@ -104,12 +107,12 @@ async function main() { await delay(1000); } throw new Error('The two LuckPerms Redis subscriptions did not reconnect within 30 seconds'); - } else if (['pause-mysql','resume-mysql','pause-redis','resume-redis'].includes(action)) { + } else if (['pause-mariadb','resume-mariadb','pause-redis','resume-redis'].includes(action)) { const state = readState(); const [verb,kind] = action.split('-'); owned(state[kind],state.run); docker([verb === 'pause' ? 'stop' : 'start',state[kind]]); if (verb === 'resume') await ready(state[kind],kind); console.log(`${action} completed`); - } else throw new Error('Usage: node smoke/permission-services.cjs start|stop|query|pause-mysql|resume-mysql|pause-redis|resume-redis|wait-redis-subscribers'); + } else throw new Error('Usage: node smoke/permission-services.cjs start|stop|query|pause-mariadb|resume-mariadb|pause-redis|resume-redis|wait-redis-subscribers'); } main().catch(error => { console.error(error.message);process.exitCode=1; diff --git a/smoke/results/2026-09-28-permissions.md b/smoke/results/2026-09-28-permissions.md index 3ab66e9..d60099e 100644 --- a/smoke/results/2026-09-28-permissions.md +++ b/smoke/results/2026-09-28-permissions.md @@ -1,5 +1,7 @@ # MoonBridge 权限与 LuckPerms 验收 +这是旧 MySQL/H2 构建的历史验收记录。当前 MariaDB 13.0.2 的入口和结果见 [MariaDB 验证](../../docs/mariadb.md),旧数据库脚本参数不适用于当前版本。 + 环境:Windows、Zulu JDK 25、Gradle 9.6.1。实现基于 MoonBridge `0344f0bc65da327c69385ab07c77e69158606d22`,LuckPerms 子模块固定为 `25f223317a9ec2b6e73369126b630eca07d79506`,上游源码未修改。 ## 已完成的专项验收 diff --git a/smoke/results/2026-10-02-mariadb.md b/smoke/results/2026-10-02-mariadb.md new file mode 100644 index 0000000..c80a8e7 --- /dev/null +++ b/smoke/results/2026-10-02-mariadb.md @@ -0,0 +1,43 @@ +# MoonBridge MariaDB 13.0.2 验收 + +环境:macOS arm64、Temurin JDK 25.0.3、Gradle 9.6.1、Node.js 26.8.1、OrbStack Docker。基于 MoonBridge `52ffadcd2649`,LuckPerms 官方子模块固定为 `25f223317a9ec2b6e73369126b630eca07d79506`,上游源码未修改。 + +## 构建与实际驱动 + +新检出的代码完成 `bash ./gradlew check assemble --no-daemon --max-workers=2`:49 个 JUnit suite、309 个测试,0 失败、0 错误、0 跳过,包含实际分发包的显式 H2 内部验收。最终配置资源修改后再次执行相同检查与打包,成功;未改动的测试任务复用结果。 + +已读取实际分发插件 JAR 中的 `config.yml`,确认默认 `storage-method: mariadb`。验收使用该分发包中的真正插件,经正式 PluginHost 的独立类加载器加载 LuckPerms。插件 SHA-256:`d5b8a73c795ef2e5437e0ec3ea151ba4df30d8880ca7b5448fe7fae1520c3416`。 + +LuckPerms 原生依赖管理器实际下载、校验并重定位 `mariadb-driver-3.5.2.jar`。MariaDB **服务器**版本为 `SELECT VERSION()` 返回的 **13.0.2-MariaDB-ubu2604**;Redis 为 7.4.11。隔离服务只监听本机回环地址,使用随机测试凭据与独立表前缀: + +- MariaDB:`mariadb@sha256:d4fdec0510ad498e4f3127da30a99df3745bd6d5e611ae6ac5f76403d9284a8d` +- Redis:`redis@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f` + +## 实际服务验收 + +同一 JVM 内的两个独立 PluginHost、插件副本、类加载器和数据目录连接真正的 MariaDB/Redis。两个实例先加载相同 UUID,随后由 A 修改权限并等待 B 的已加载缓存更新,没有调用手工同步命令。SQL 模式使用 `messaging-service: auto`,日志确认选择原生 SQL messenger;Redis 模式使用 Redis messenger,权限仍写入 MariaDB。 + +| 场景 | 结果 | +| --- | --- | +| 跨实例授权、显式拒绝、撤销、组继承与个人覆盖 | 两种 messenger 均 PASS | +| 不同 server 上下文、临时权限到期 | 两种模式均 PASS | +| 关闭插件后以新类加载器读取持久化数据 | 两种模式均 PASS | +| MariaDB 停机:已有缓存仍可查,新 UUID 准备失败且结果为 UNAVAILABLE | PASS | +| MariaDB 恢复:新连接加载成功,新的权限写入可读 | PASS,首次恢复尝试成功 | +| Redis 停机:A 写入 MariaDB,B 缓存没有即时收到修改 | PASS | +| Redis 两个订阅恢复后:新变更同步到 B,重启 B 后可读取停机期间和恢复后的两项权限 | PASS | + +最终数据库查询确认 `mariadb_lp_user_permissions` 有 6 行、`redis_lp_user_permissions` 有 5 行,恢复后的权限及 Redis 停机期间写入的权限均在 MariaDB 中。两轮验收均返回 `PERMISSION_NETWORK_ACCEPTANCE_PASS`,退出码 0;插件关闭后等待 500ms,没有未捕获后台异常。预期停机期间的连接失败日志不属于未捕获异常。 + +本机原始证据保存在忽略的构建目录: + +- `build/permission-network/permission-network-sql-18344677148309595004/` +- `build/permission-network/permission-network-redis-16265096492059865389/` +- `build/permission-network-sql-outage.log`、`build/permission-network-redis-outage.log` +- `build/permission-services/database-evidence.txt`、`build/mariadb-check.log` + +已删除本轮两个容器和数据卷、移除凭据文件,并核验保留配置的密码已脱敏。复现入口见 [MariaDB 文档](../../docs/mariadb.md)。 + +## 证据边界 + +本轮验证实际插件包与真实数据库服务,不包含新的游戏客户端、Uranium/Bukkit 联测、Web Editor 往返、远程 TLS 部署或生产数据迁移。既有 MySQL/H2/客户端记录保留为历史证据。Redis pub/sub 不补发断线窗口通知,不能据此宣称故障期间所有实例保持一致。