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) {
Repository.getDisconnectionQueue().remove(getName());
}
GameWorld.getLogoutListeners().forEach((it) -> it.logout(this));
if (!isArtificial())
GameWorld.getLogoutListeners().forEach((it) -> it.logout(this));
setPlaying(false);
getWalkingQueue().reset();
GameWorld.getLogoutListeners().forEach((it) -> it.logout(this));
if(!logoutListeners.isEmpty()){
logoutListeners.forEach((key,method) -> method.invoke(this));
}
@@ -5,6 +5,7 @@ import api.ShutdownListener;
import core.game.node.entity.player.Player;
import rs09.Server;
import rs09.ServerConstants;
import rs09.game.ai.AIRepository;
import rs09.game.system.SystemLogger;
import rs09.game.world.GameWorld;
import rs09.game.world.repository.Repository;
@@ -34,8 +35,13 @@ public final class SystemTermination {
public void terminate() {
SystemLogger.logInfo("[SystemTerminator] Initializing termination sequence - do not shutdown!");
try {
SystemLogger.logInfo("[SystemTerminator] Stopping all bots...");
AIRepository.clearAllBots();
SystemLogger.logInfo("[SystemTerminator] Shutting down networking...");
Server.setRunning(false);
Server.getReactor().terminate();
SystemLogger.logInfo("[SystemTerminator] Stopping all pulses...");
GameWorld.getMajorUpdateWorker().stop();
for (Iterator<Player> it = Repository.getPlayers().iterator(); it.hasNext();) {
try {
Player p = it.next();
@@ -42,5 +42,9 @@ class AIRepository {
fun getOffer(player: Player): GrandExchangeOffer? {
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() {
for (item in player.inventory.toArray()) {
if (item == null) continue
if (item.noteChange != item.id && item.noteChange != -1) {
if (item.definition.isUnnoted) {
player.inventory.remove(item)
player.inventory.add(Item(item.noteChange, item.amount))
}
@@ -10,6 +10,8 @@ import rs09.game.system.SystemLogger
import java.io.File
import java.io.FileReader
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
@@ -20,6 +22,8 @@ object GEDB {
public var connection: Connection? = null
private var initialized = false
private var connectionRefs = 0
private var obtainConnectionLock = ReentrantLock()
private var dbRunLock = ReentrantLock()
fun init() {
init(File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath)
@@ -36,23 +40,32 @@ object GEDB {
}
@JvmStatic fun run(closure: (conn: Connection) -> Unit) {
dbRunLock.tryLock(10000L, TimeUnit.MILLISECONDS)
connectionRefs++
val con = connect()
closure.invoke(con)
connectionRefs--
if(connectionRefs == 0) {
con.close()
}
dbRunLock.unlock()
}
private fun connect(): Connection
{
obtainConnectionLock.tryLock(10000L, TimeUnit.MILLISECONDS)
if (connection == null || connection!!.isClosed)
{
val ds = SQLiteDataSource()
ds.url = "jdbc:sqlite:$pathString"
connection = ds.connection
}
obtainConnectionLock.unlock()
return connection!!
}
@@ -43,37 +43,38 @@ class GrandExchange : StartupListener, Commands {
GEDB.run { conn ->
val botStmt = conn.createStatement()
val botOffers = botStmt.executeQuery("SELECT * from bot_offers")
while(botOffers.next()) {
while (botOffers.next()) {
val bot = GrandExchangeOffer.fromBotQuery(botOffers)
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>()
while(buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer))
while (buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer))
buyStmt.close()
for(offer in buyOffers.sortedBy { it.offeredValue }.reversed()) {
for (offer in buyOffers.sortedBy { it.offeredValue }.reversed()) {
if (bot.amountLeft <= 0) break
exchange(bot, offer)
}
}
botStmt.close()
}
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")
val activeOffers = ArrayList<GrandExchangeOffer>()
val activeOffer =
stmt.executeQuery("SELECT * from player_offers where offer_state < 4 AND NOT offer_state = 2 AND is_sale = true")
while(activeOffer.next())
{
while (activeOffer.next()) {
val offer = GrandExchangeOffer.fromQuery(activeOffer)
if(!offer.isActive) continue
if (!offer.isActive) continue
activeOffers.add(offer)
}
for(offer in activeOffers)
processOffer(offer)
stmt.close()
}
for(offer in activeOffers)
processOffer(offer)
Thread.sleep(15_000) //sleep for 15 seconds
}
}.start()
@@ -28,6 +28,8 @@ object PriceIndex {
}
fun allowItem(id: Int) {
if(canTrade(id)) return
GEDB.run { conn ->
val stmt = conn.prepareStatement(INSERT_QUERY)
stmt.setInt(1, id)
@@ -43,11 +43,10 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
reconnect(player)
return
}
lateinit var parser: PlayerSaveParser
try {
val parser = PlayerParser.parse(player)
parser = PlayerParser.parse(player)
?: 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)
{
@@ -74,6 +73,8 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) {
if (!Repository.players.contains(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)
flag(AuthResponse.Success)
player.init()
@@ -35,7 +35,7 @@ class RottenPotatoRSHDDialogue(player: Player? = null) : DialoguePlugin(player)
0 -> {
when (buttonId) {
//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
2 -> ImmerseWorld.spawnBots().also { player.sendMessage(colorize("%RBots Respawning...")); end() }
//Force Log All Online Players
@@ -32,6 +32,7 @@ import kotlin.system.exitProcess
* @author Ceikry
*/
class MajorUpdateWorker {
var running: Boolean = false
var started = false
val sequence = UpdateSequence()
val sdf = SimpleDateFormat("HHmmss")
@@ -39,7 +40,7 @@ class MajorUpdateWorker {
Thread.currentThread().name = "Major Update Worker"
started = true
Thread.sleep(600L)
while(true){
while(running){
val start = System.currentTimeMillis()
Server.heartbeat()
@@ -75,6 +76,8 @@ class MajorUpdateWorker {
ServerMonitor.eventQueue.add(GuiEvent.UpdatePulseCount(GameWorld.Pulser.TASKS.size))*/
Thread.sleep(max(600 - (end - start), 0))
}
SystemLogger.logInfo("Update worker stopped.")
}
fun handleTickActions() {
@@ -127,10 +130,13 @@ class MajorUpdateWorker {
fun start() {
if(!started){
running = true
worker.start()
}
}
//if (ServerConstants.ALLOW_GUI)
// ServerMonitor.open()
fun stop() {
running = false
worker.interrupt()
}
}
+15
View File
@@ -1,4 +1,7 @@
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.Assertions
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.GrandExchange
import rs09.game.ge.GrandExchangeOffer
import rs09.game.ge.PriceIndex
import rs09.game.system.SystemLogger
import java.io.File
import kotlin.random.Random
@@ -100,4 +104,15 @@ import kotlin.random.Random
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
import TestUtils
import core.game.node.item.Item
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.rs09.consts.Items
import rs09.game.content.ame.RandomEventNPC
import rs09.game.world.GameWorld
class RandomEventManager {
@@ -33,4 +36,41 @@ class RandomEventManager {
TestUtils.advanceTicks(rs09.game.content.ame.RandomEventManager.MAX_DELAY_TICKS + 5)
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))
}
}