Improved server shutdown order

Improved GE threading, introduced locks fixing server lags and GE offers not executing
Moved player login hooks to the major update worker (from management server thread)
Fixed random events in rare circumstances causing noted item loss
This commit is contained in:
Ceikry
2022-05-24 11:02:05 +00:00
committed by Ryan
parent 87313ce1d5
commit a40dbfbc1c
12 changed files with 112 additions and 24 deletions
@@ -368,10 +368,10 @@ public class Player extends Entity {
if (force) { if (force) {
Repository.getDisconnectionQueue().remove(getName()); Repository.getDisconnectionQueue().remove(getName());
} }
if (!isArtificial())
GameWorld.getLogoutListeners().forEach((it) -> it.logout(this)); GameWorld.getLogoutListeners().forEach((it) -> it.logout(this));
setPlaying(false); setPlaying(false);
getWalkingQueue().reset(); getWalkingQueue().reset();
GameWorld.getLogoutListeners().forEach((it) -> it.logout(this));
if(!logoutListeners.isEmpty()){ if(!logoutListeners.isEmpty()){
logoutListeners.forEach((key,method) -> method.invoke(this)); logoutListeners.forEach((key,method) -> method.invoke(this));
} }
@@ -5,6 +5,7 @@ import api.ShutdownListener;
import core.game.node.entity.player.Player; import core.game.node.entity.player.Player;
import rs09.Server; import rs09.Server;
import rs09.ServerConstants; import rs09.ServerConstants;
import rs09.game.ai.AIRepository;
import rs09.game.system.SystemLogger; import rs09.game.system.SystemLogger;
import rs09.game.world.GameWorld; import rs09.game.world.GameWorld;
import rs09.game.world.repository.Repository; import rs09.game.world.repository.Repository;
@@ -34,8 +35,13 @@ public final class SystemTermination {
public void terminate() { public void terminate() {
SystemLogger.logInfo("[SystemTerminator] Initializing termination sequence - do not shutdown!"); SystemLogger.logInfo("[SystemTerminator] Initializing termination sequence - do not shutdown!");
try { try {
SystemLogger.logInfo("[SystemTerminator] Stopping all bots...");
AIRepository.clearAllBots();
SystemLogger.logInfo("[SystemTerminator] Shutting down networking...");
Server.setRunning(false); Server.setRunning(false);
Server.getReactor().terminate(); Server.getReactor().terminate();
SystemLogger.logInfo("[SystemTerminator] Stopping all pulses...");
GameWorld.getMajorUpdateWorker().stop();
for (Iterator<Player> it = Repository.getPlayers().iterator(); it.hasNext();) { for (Iterator<Player> it = Repository.getPlayers().iterator(); it.hasNext();) {
try { try {
Player p = it.next(); Player p = it.next();
@@ -42,5 +42,9 @@ class AIRepository {
fun getOffer(player: Player): GrandExchangeOffer? { fun getOffer(player: Player): GrandExchangeOffer? {
return GEOffers[player] return GEOffers[player]
} }
@JvmStatic fun clearAllBots() {
PulseRepository.toTypedArray().forEach { it.stop(); it.botScript.bot.clear(); AIPlayer.deregister((it.botScript.bot as AIPlayer).uid) }
}
} }
} }
@@ -92,7 +92,7 @@ abstract class RandomEventNPC(id: Int) : NPC(id) {
fun noteAndTeleport() { fun noteAndTeleport() {
for (item in player.inventory.toArray()) { for (item in player.inventory.toArray()) {
if (item == null) continue if (item == null) continue
if (item.noteChange != item.id && item.noteChange != -1) { if (item.definition.isUnnoted) {
player.inventory.remove(item) player.inventory.remove(item)
player.inventory.add(Item(item.noteChange, item.amount)) player.inventory.add(Item(item.noteChange, item.amount))
} }
@@ -10,6 +10,8 @@ import rs09.game.system.SystemLogger
import java.io.File import java.io.File
import java.io.FileReader import java.io.FileReader
import java.sql.Connection import java.sql.Connection
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
/** /**
* Collection of methods for interacting with the grand exchange databases * Collection of methods for interacting with the grand exchange databases
@@ -20,6 +22,8 @@ object GEDB {
public var connection: Connection? = null public var connection: Connection? = null
private var initialized = false private var initialized = false
private var connectionRefs = 0 private var connectionRefs = 0
private var obtainConnectionLock = ReentrantLock()
private var dbRunLock = ReentrantLock()
fun init() { fun init() {
init(File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath) init(File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath)
@@ -36,23 +40,32 @@ object GEDB {
} }
@JvmStatic fun run(closure: (conn: Connection) -> Unit) { @JvmStatic fun run(closure: (conn: Connection) -> Unit) {
dbRunLock.tryLock(10000L, TimeUnit.MILLISECONDS)
connectionRefs++ connectionRefs++
val con = connect() val con = connect()
closure.invoke(con) closure.invoke(con)
connectionRefs-- connectionRefs--
if(connectionRefs == 0) { if(connectionRefs == 0) {
con.close() con.close()
} }
dbRunLock.unlock()
} }
private fun connect(): Connection private fun connect(): Connection
{ {
obtainConnectionLock.tryLock(10000L, TimeUnit.MILLISECONDS)
if (connection == null || connection!!.isClosed) if (connection == null || connection!!.isClosed)
{ {
val ds = SQLiteDataSource() val ds = SQLiteDataSource()
ds.url = "jdbc:sqlite:$pathString" ds.url = "jdbc:sqlite:$pathString"
connection = ds.connection connection = ds.connection
} }
obtainConnectionLock.unlock()
return connection!! return connection!!
} }
@@ -43,37 +43,38 @@ class GrandExchange : StartupListener, Commands {
GEDB.run { conn -> GEDB.run { conn ->
val botStmt = conn.createStatement() val botStmt = conn.createStatement()
val botOffers = botStmt.executeQuery("SELECT * from bot_offers") val botOffers = botStmt.executeQuery("SELECT * from bot_offers")
while(botOffers.next()) { while (botOffers.next()) {
val bot = GrandExchangeOffer.fromBotQuery(botOffers) val bot = GrandExchangeOffer.fromBotQuery(botOffers)
val buyStmt = conn.createStatement() val buyStmt = conn.createStatement()
val buyOffer = buyStmt.executeQuery("SELECT * FROM player_offers WHERE item_id = ${bot.itemID} AND offer_state < 4 AND NOT offer_state = 2 AND offered_value >= ${bot.offeredValue}") val buyOffer =
buyStmt.executeQuery("SELECT * FROM player_offers WHERE item_id = ${bot.itemID} AND offer_state < 4 AND NOT offer_state = 2 AND offered_value >= ${bot.offeredValue}")
val buyOffers = ArrayList<GrandExchangeOffer>() val buyOffers = ArrayList<GrandExchangeOffer>()
while(buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer)) while (buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer))
buyStmt.close() buyStmt.close()
for(offer in buyOffers.sortedBy { it.offeredValue }.reversed()) { for (offer in buyOffers.sortedBy { it.offeredValue }.reversed()) {
if (bot.amountLeft <= 0) break if (bot.amountLeft <= 0) break
exchange(bot, offer) exchange(bot, offer)
} }
} }
botStmt.close() botStmt.close()
val stmt = conn.createStatement()
val activeOffer = stmt.executeQuery("SELECT * from player_offers where offer_state < 4 AND NOT offer_state = 2 AND is_sale = true")
val activeOffers = ArrayList<GrandExchangeOffer>()
while(activeOffer.next())
{
val offer = GrandExchangeOffer.fromQuery(activeOffer)
if(!offer.isActive) continue
activeOffers.add(offer)
} }
val activeOffers = ArrayList<GrandExchangeOffer>()
GEDB.run { conn ->
val stmt = conn.createStatement()
val activeOffer =
stmt.executeQuery("SELECT * from player_offers where offer_state < 4 AND NOT offer_state = 2 AND is_sale = true")
while (activeOffer.next()) {
val offer = GrandExchangeOffer.fromQuery(activeOffer)
if (!offer.isActive) continue
activeOffers.add(offer)
}
}
for(offer in activeOffers) for(offer in activeOffers)
processOffer(offer) processOffer(offer)
stmt.close()
}
Thread.sleep(15_000) //sleep for 15 seconds Thread.sleep(15_000) //sleep for 15 seconds
} }
}.start() }.start()
@@ -28,6 +28,8 @@ object PriceIndex {
} }
fun allowItem(id: Int) { fun allowItem(id: Int) {
if(canTrade(id)) return
GEDB.run { conn -> GEDB.run { conn ->
val stmt = conn.prepareStatement(INSERT_QUERY) val stmt = conn.prepareStatement(INSERT_QUERY)
stmt.setInt(1, id) stmt.setInt(1, id)
@@ -43,11 +43,10 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
reconnect(player) reconnect(player)
return return
} }
lateinit var parser: PlayerSaveParser
try { try {
val parser = PlayerParser.parse(player) parser = PlayerParser.parse(player)
?: throw IllegalStateException("Failed parsing save for: " + player.username) //Parse core ?: throw IllegalStateException("Failed parsing save for: " + player.username) //Parse core
loginListeners.forEach(Consumer { listener: LoginListener -> listener.login(player) }) //Run our login hooks
parser.runContentHooks() //Run our saved-content-parsing hooks
} }
catch (e: Exception) catch (e: Exception)
{ {
@@ -74,6 +73,8 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
if (!Repository.players.contains(player)) { if (!Repository.players.contains(player)) {
Repository.addPlayer(player) Repository.addPlayer(player)
} }
loginListeners.forEach(Consumer { listener: LoginListener -> listener.login(player) }) //Run our login hooks
parser.runContentHooks() //Run our saved-content-parsing hooks
player.details.session.setObject(player) player.details.session.setObject(player)
flag(AuthResponse.Success) flag(AuthResponse.Success)
player.init() player.init()
@@ -35,7 +35,7 @@ class RottenPotatoRSHDDialogue(player: Player? = null) : DialoguePlugin(player)
0 -> { 0 -> {
when (buttonId) { when (buttonId) {
//Wipe Bots //Wipe Bots
1 -> AIRepository.PulseRepository.toTypedArray().forEach { it.stop(); it.botScript.bot.clear(); AIPlayer.deregister((it.botScript.bot as AIPlayer).uid) }.also { player.sendMessage(colorize("%RBots wiped.")); end() } 1 -> AIRepository.clearAllBots().also { player.sendMessage(colorize("%RBots wiped.")); end() }
//Spawn Bots //Spawn Bots
2 -> ImmerseWorld.spawnBots().also { player.sendMessage(colorize("%RBots Respawning...")); end() } 2 -> ImmerseWorld.spawnBots().also { player.sendMessage(colorize("%RBots Respawning...")); end() }
//Force Log All Online Players //Force Log All Online Players
@@ -32,6 +32,7 @@ import kotlin.system.exitProcess
* @author Ceikry * @author Ceikry
*/ */
class MajorUpdateWorker { class MajorUpdateWorker {
var running: Boolean = false
var started = false var started = false
val sequence = UpdateSequence() val sequence = UpdateSequence()
val sdf = SimpleDateFormat("HHmmss") val sdf = SimpleDateFormat("HHmmss")
@@ -39,7 +40,7 @@ class MajorUpdateWorker {
Thread.currentThread().name = "Major Update Worker" Thread.currentThread().name = "Major Update Worker"
started = true started = true
Thread.sleep(600L) Thread.sleep(600L)
while(true){ while(running){
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
Server.heartbeat() Server.heartbeat()
@@ -75,6 +76,8 @@ class MajorUpdateWorker {
ServerMonitor.eventQueue.add(GuiEvent.UpdatePulseCount(GameWorld.Pulser.TASKS.size))*/ ServerMonitor.eventQueue.add(GuiEvent.UpdatePulseCount(GameWorld.Pulser.TASKS.size))*/
Thread.sleep(max(600 - (end - start), 0)) Thread.sleep(max(600 - (end - start), 0))
} }
SystemLogger.logInfo("Update worker stopped.")
} }
fun handleTickActions() { fun handleTickActions() {
@@ -127,10 +130,13 @@ class MajorUpdateWorker {
fun start() { fun start() {
if(!started){ if(!started){
running = true
worker.start() worker.start()
} }
}
//if (ServerConstants.ALLOW_GUI) fun stop() {
// ServerMonitor.open() running = false
worker.interrupt()
} }
} }
+15
View File
@@ -1,4 +1,7 @@
import core.game.ge.OfferState import core.game.ge.OfferState
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -7,6 +10,7 @@ import org.junit.jupiter.api.fail
import rs09.game.ge.GEDB import rs09.game.ge.GEDB
import rs09.game.ge.GrandExchange import rs09.game.ge.GrandExchange
import rs09.game.ge.GrandExchangeOffer import rs09.game.ge.GrandExchangeOffer
import rs09.game.ge.PriceIndex
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import java.io.File import java.io.File
import kotlin.random.Random import kotlin.random.Random
@@ -100,4 +104,15 @@ import kotlin.random.Random
Assertions.assertEquals(defaultPrice, GrandExchange.getRecommendedPrice(4151)) Assertions.assertEquals(defaultPrice, GrandExchange.getRecommendedPrice(4151))
} }
@Test fun concurrentlySubmittedOffersShouldNotThrowExceptions(){
runBlocking {
val a = GlobalScope.launch { for(i in 0 until 5) {PriceIndex.allowItem(i); GrandExchange.addBotOffer(i, 1)} }
val b = GlobalScope.launch { for(i in 0 until 5) {PriceIndex.allowItem(i); GrandExchange.addBotOffer(i, 1)} }
a.join()
b.join()
Assertions.assertEquals(false, a.isCancelled)
Assertions.assertEquals(false, b.isCancelled)
}
}
} }
@@ -1,8 +1,11 @@
package content package content
import TestUtils import TestUtils
import core.game.node.item.Item
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.rs09.consts.Items
import rs09.game.content.ame.RandomEventNPC
import rs09.game.world.GameWorld import rs09.game.world.GameWorld
class RandomEventManager { class RandomEventManager {
@@ -33,4 +36,41 @@ class RandomEventManager {
TestUtils.advanceTicks(rs09.game.content.ame.RandomEventManager.MAX_DELAY_TICKS + 5) TestUtils.advanceTicks(rs09.game.content.ame.RandomEventManager.MAX_DELAY_TICKS + 5)
Assertions.assertNotNull(p.getAttribute("re-npc", null)) Assertions.assertNotNull(p.getAttribute("re-npc", null))
} }
@Test fun teleportAndNotePunishmentShouldNotAffectAlreadyNotedItems() {
val p = TestUtils.getMockPlayer("Shitforbrains")
p.setAttribute("tutorial:complete", true)
rs09.game.content.ame.RandomEventManager().login(p)
p.inventory.add(Item(Items.RAW_SHARK_384, 1000))
rs09.game.content.ame.RandomEventManager.getInstance(p)?.fireEvent()
p.getAttribute<RandomEventNPC>("re-npc")!!.noteAndTeleport()
Assertions.assertEquals(1000, p.inventory.getAmount(Items.RAW_SHARK_384))
}
@Test fun teleportAndNotePunishmentShouldNoteNotableUnnotedItems() {
val p = TestUtils.getMockPlayer("shitforbrains2")
p.setAttribute("tutorial:complete", true)
rs09.game.content.ame.RandomEventManager().login(p)
p.inventory.add(Item(4151, 5))
rs09.game.content.ame.RandomEventManager.getInstance(p)?.fireEvent()
p.getAttribute<RandomEventNPC>("re-npc")!!.noteAndTeleport()
Assertions.assertEquals(5, p.inventory.getAmount(4152))
Assertions.assertEquals(0, p.inventory.getAmount(4151))
}
@Test fun teleportAndNotePunishmentShouldNotAffectUnnotableItems() {
val p = TestUtils.getMockPlayer("shitforbrains3")
p.setAttribute("tutorial:complete", true)
rs09.game.content.ame.RandomEventManager().login(p)
p.inventory.add(Item(Items.AIR_RUNE_556, 30))
rs09.game.content.ame.RandomEventManager.getInstance(p)?.fireEvent()
p.getAttribute<RandomEventNPC>("re-npc")!!.noteAndTeleport()
Assertions.assertEquals(30, p.inventory.getAmount(Items.AIR_RUNE_556))
}
} }