Skip to content

fix: 修复内存泄漏和数据库连接泄漏问题#1

Open
MineSunshineone wants to merge 1 commit intoCherry7593:mainfrom
MineSunshineone:main
Open

fix: 修复内存泄漏和数据库连接泄漏问题#1
MineSunshineone wants to merge 1 commit intoCherry7593:mainfrom
MineSunshineone:main

Conversation

@MineSunshineone
Copy link

  • McediaGUI: 添加 PlayerQuitEvent 和 InventoryCloseEvent 监听器,清理 playerGUIState 防止玩家退出后状态残留

  • McediaListener: 添加 EntitiesUnloadEvent 监听器,当 ArmorStand 被外部删除时同步清理播放器缓存

  • McediaManager: 添加 removeFromCache() 方法供监听器调用

  • DatabaseManager: 添加 synchronized 锁和 @volatile 注解,确保多线程安全

  • McediaStorage: 移除对 Connection 的 .use{} 调用,只对 Statement 使用,保持连接持久打开,避免文件描述符泄漏

- McediaGUI: 添加 PlayerQuitEvent 和 InventoryCloseEvent 监听器,清理 playerGUIState 防止玩家退出后状态残留

- McediaListener: 添加 EntitiesUnloadEvent 监听器,当 ArmorStand 被外部删除时同步清理播放器缓存

- McediaManager: 添加 removeFromCache() 方法供监听器调用

- DatabaseManager: 添加 synchronized 锁和 @volatile 注解,确保多线程安全

- McediaStorage: 移除对 Connection 的 .use{} 调用,只对 Statement 使用,保持连接持久打开,避免文件描述符泄漏
Copilot AI review requested due to automatic review settings February 7, 2026 14:36
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 旨在修复插件运行过程中的内存/缓存残留以及数据库连接使用方式带来的资源泄漏与线程安全风险,提升长期运行稳定性。

Changes:

  • GUI 增加 InventoryClose / PlayerQuit 监听,退出或关闭界面时清理 playerGUIState
  • Listener 增加 EntitiesUnloadEvent 监听,并在 Manager 中提供 removeFromCache 以同步清理缓存
  • DatabaseManager 改为单一持久连接,并增加锁与 @volatile 以增强并发安全;Storage 调整为不再 .use {} 关闭 Connection

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/main/kotlin/org/mcediagui/McediaStorage.kt 调整 DB 使用方式:不再关闭 Connection,仅关闭 Statement/PreparedStatement
src/main/kotlin/org/mcediagui/McediaManager.kt 新增 removeFromCache() 供监听器清理玩家缓存
src/main/kotlin/org/mcediagui/McediaListener.kt 新增 EntitiesUnloadEvent 监听以在实体卸载/删除时触发缓存清理
src/main/kotlin/org/mcediagui/McediaGUI.kt 新增 InventoryCloseEvent / PlayerQuitEvent 清理 GUI 状态,避免状态残留
src/main/kotlin/org/mcediagui/DatabaseManager.kt 引入持久连接 + synchronized + @volatile,减少频繁开关连接并改善并发可见性

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 26 to +30
fun getConnection(): Connection? {
if (connection?.isClosed == true) {
val dbFile = File(plugin.dataFolder, "mcediagui.db")
connection = DriverManager.getConnection("jdbc:sqlite:${dbFile.absolutePath}")
synchronized(lock) {
val conn = connection
if (conn == null || conn.isClosed) {
val dbFile = File(plugin.dataFolder, "mcediagui.db")
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DatabaseManager 改为返回单一持久 Connection,但目前仅在 getConnection 内部创建/切换连接时加锁;实际 SQL 执行发生在多个 CompletableFuture 线程中,会并发共享同一个 Connection。JDBC 的 SQLite 连接通常不支持并发使用,容易出现竞态/database is locked 等问题。建议把数据库操作整体串行化(例如提供 withConnection {} 并在其中同步执行 SQL,或使用单线程 Executor 运行所有 storage future),而不是只同步获取连接。

Copilot uses AI. Check for mistakes.
* 从缓存中移除播放器(用于实体被外部删除时的清理)
*/
fun removeFromCache(uuid: UUID) {
players.remove(uuid)
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeFromCache 目前只移除了内存缓存,没有同步删除数据库中的 mcedia_players 记录。若 ArmorStand 被外部删除,此处清理会导致“运行中看不到但重启后又从数据库加载回来”的脏数据;也可能影响后续自动注册逻辑覆盖 createdBy/createdAt。建议在确认实体确实被永久删除时,同时调用 storage.delete(uuid)(以及必要时清理 pending ops)。

Suggested change
players.remove(uuid)
players.remove(uuid)
storage?.delete(uuid)

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +51
val conn = DatabaseManager.getConnection() ?: return@supplyAsync players
conn.prepareStatement("SELECT * FROM mcedia_players").use { stmt ->
val rs = stmt.executeQuery()
while (rs.next()) {
Bukkit.getWorld(rs.getString("world"))?.let { world ->
players.add(McediaPlayer(
UUID.fromString(rs.getString("uuid")), rs.getString("name"),
Location(world, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch")),
rs.getString("video_url") ?: "", rs.getString("start_time") ?: "",
rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"),
rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"),
rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1,
UUID.fromString(rs.getString("created_by")), rs.getLong("created_at")
))
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadAll() 通过 CompletableFuture.supplyAsync 在公共线程池异步执行,但会复用 DatabaseManager 的同一个持久 Connection;其他 save/delete 等方法也同样如此。这样会导致跨线程并发访问同一 SQLite Connection 的风险(驱动通常不保证线程安全)。建议为数据库层提供专用单线程 executor,或在执行每条 SQL 时统一加锁/排队,避免并发访问。

Suggested change
val conn = DatabaseManager.getConnection() ?: return@supplyAsync players
conn.prepareStatement("SELECT * FROM mcedia_players").use { stmt ->
val rs = stmt.executeQuery()
while (rs.next()) {
Bukkit.getWorld(rs.getString("world"))?.let { world ->
players.add(McediaPlayer(
UUID.fromString(rs.getString("uuid")), rs.getString("name"),
Location(world, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch")),
rs.getString("video_url") ?: "", rs.getString("start_time") ?: "",
rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"),
rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"),
rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1,
UUID.fromString(rs.getString("created_by")), rs.getLong("created_at")
))
synchronized(DatabaseManager::class.java) {
val conn = DatabaseManager.getConnection() ?: return@supplyAsync players
conn.prepareStatement("SELECT * FROM mcedia_players").use { stmt ->
val rs = stmt.executeQuery()
while (rs.next()) {
Bukkit.getWorld(rs.getString("world"))?.let { world ->
players.add(McediaPlayer(
UUID.fromString(rs.getString("uuid")), rs.getString("name"),
Location(world, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"), rs.getFloat("yaw"), rs.getFloat("pitch")),
rs.getString("video_url") ?: "", rs.getString("start_time") ?: "",
rs.getDouble("scale"), rs.getInt("volume"), rs.getDouble("max_volume_range"), rs.getDouble("hearing_range"),
rs.getDouble("offset_x"), rs.getDouble("offset_y"), rs.getDouble("offset_z"),
rs.getInt("looping") == 1, rs.getInt("no_danmaku") == 1,
UUID.fromString(rs.getString("created_by")), rs.getLong("created_at")
))
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants