Increase SessionReaper timeouts and add to config file

This should fix issues disconnecting at loading screens/zone changes as
no packets are being transmitted during this window. If the
WorldSessionsActor is also slightly overloaded, the session reaper can
drop the session mistakenly due to no outbound traffic.

Also fix-up WorldConfig.Get with better error messages along with more
tests.
This commit is contained in:
Chord 2019-12-21 14:18:19 -05:00 committed by pschord
parent d2732550e8
commit 83ac66a3bf
5 changed files with 82 additions and 3 deletions

View file

@ -105,7 +105,17 @@ trait ConfigParser {
protected val config_template : Seq[ConfigSection]
// Misuse of this function can lead to run time exceptions when the types don't match
def Get[T : ConfigTypeRequired](key : String) : T = config_map(key).asInstanceOf[T]
// ClassTag is needed due to type erasure on T
// https://dzone.com/articles/scala-classtag-a-simple-use-case
def Get[T : ConfigTypeRequired](key : String)(implicit m: ClassTag[T]) : T = {
config_map.get(key) match {
case Some(value : T) => value
case None =>
throw new NoSuchElementException(s"Config key '${key}' not found")
case Some(value : Any) =>
throw new ClassCastException(s"Incorrect type T = ${m.runtimeClass.getSimpleName} passed to Get[T]: needed ${value.getClass.getSimpleName}")
}
}
def Load(filename : String) : ValidationResult = {
val ini = new org.ini4j.Ini()

View file

@ -48,6 +48,35 @@ ListeningPort = 51001
ListeningPort = 51000
###################################################################################################
# NETWORK SETTINGS
###################################################################################################
[network]
# Session.InboundGraceTime (time)
# Description: The maximum amount of time since the last inbound packet from a UDP session
# before it is dropped.
# Important: Lower values will cause legitimate clients to be dropped during loading
# screens, but higher values will make the server be more susceptible to
# denial of service attacks and running out of memory.
# Range: [10 seconds, 10 minutes] - (10 second grace, 10 minute grace)
# Default: 1 minute - (Clients sending a packet at least
# once a minute stay alive)
Session.InboundGraceTime = 1 minute
# Session.OutboundGraceTime (time)
# Description: The maximum amount of time since the last outbound packet for a UDP session
# before it is dropped. Can be used as a watchdog for hung server sessions.
# Important: Lower values will cause legitimate clients to be dropped during server
# lag spikes or Zone transitions.
# Range: [10 seconds, 10 minutes] - (10 second grace, 10 minute grace)
# Default: 1 minute - (Clients receiving a packet at least
# once a minute stay alive)
Session.OutboundGraceTime = 1 minute
###################################################################################################
# DEVELOPER SETTINGS
# - NETWORK SIMULATOR

View file

@ -111,6 +111,9 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
log.error(s"Requested to drop non-existent session ID=$id from ${sender()}")
}
case SessionReaper() =>
val inboundGrace = WorldConfig.Get[Duration]("network.Session.InboundGraceTime").toMillis
val outboundGrace = WorldConfig.Get[Duration]("network.Session.OutboundGraceTime").toMillis
sessionById.foreach { case (id, session) =>
log.trace(session.toString)
if(session.getState == Closed()) {
@ -119,9 +122,9 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act
sessionById.remove(id)
idBySocket.remove(session.socketAddress)
log.debug(s"Reaped session ID=$id")
} else if(session.timeSinceLastInboundEvent > 10000) {
} else if(session.timeSinceLastInboundEvent > inboundGrace) {
removeSessionById(id, "session timed out (inbound)", graceful = false)
} else if(session.timeSinceLastOutboundEvent > 4000) {
} else if(session.timeSinceLastOutboundEvent > outboundGrace) {
removeSessionById(id, "session timed out (outbound)", graceful = true) // tell client to STFU
}
}

View file

@ -12,6 +12,10 @@ object WorldConfig extends ConfigParser {
ConfigSection("worldserver",
ConfigEntryInt("ListeningPort", 51001, Constraints.min(1), Constraints.max(65535))
),
ConfigSection("network",
ConfigEntryTime("Session.InboundGraceTime", 1 minute, Constraints.min(10 seconds)),
ConfigEntryTime("Session.OutboundGraceTime", 1 minute, Constraints.min(10 seconds))
),
ConfigSection("developer",
ConfigEntryBool ("NetSim.Active", false),
ConfigEntryFloat("NetSim.Loss", 0.02f, Constraints.min(0.0f), Constraints.max(1.0f)),

View file

@ -1,4 +1,5 @@
// Copyright (c) 2019 PSForever
import java.io._
import scala.io.Source
import org.specs2.mutable._
import net.psforever.config._
@ -11,6 +12,25 @@ class ConfigTest extends Specification {
"have no errors" in {
WorldConfig.Load("config/worldserver.ini.dist") mustEqual Valid
}
"be formatted correctly" in {
var lineno = 1
for (line <- Source.fromFile("config/worldserver.ini.dist").getLines) {
val linee :String = line
val ctx = s"worldserver.ini.dist:${lineno}"
val maxLen = 100
val lineLen = line.length
lineLen aka s"${ctx} - line length" must beLessThan(maxLen)
line.slice(0, 1) aka s"${ctx} - leading whitespace found" mustNotEqual " "
line.slice(line.length-1, line.length) aka s"${ctx} - trailing whitespace found" mustNotEqual " "
lineno += 1
}
ok
}
}
"TestConfig" should {
@ -26,6 +46,19 @@ class ConfigTest extends Specification {
TestConfig.Get[Boolean]("default.bool_false") mustEqual false
TestConfig.Get[Int]("default.missing") mustEqual 1337
}
"throw when getting non-existant keys" in {
TestConfig.Load(testConfig) mustEqual Valid
TestConfig.Get[Int]("missing.key") must throwA[NoSuchElementException](message = "Config key 'missing.key' not found")
TestConfig.Get[String]("missing.key") must throwA[NoSuchElementException](message = "Config key 'missing.key' not found")
}
"throw when Get is not passed the right type parameter" in {
TestConfig.Load(testConfig) mustEqual Valid
TestConfig.Get[Duration]("default.string") must throwA[ClassCastException](message = "Incorrect type T = Duration passed to Get\\[T\\]: needed String")
TestConfig.Get[String]("default.int") must throwA[ClassCastException](message = "Incorrect type T = String passed to Get\\[T\\]: needed Int")
ok
}
}
"TestBadConfig" should {