From 83ac66a3bfa608cc8aa1c7dfb8947152b2d0c2c6 Mon Sep 17 00:00:00 2001 From: Chord Date: Sat, 21 Dec 2019 14:18:19 -0500 Subject: [PATCH] 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. --- .../net/psforever/config/ConfigParser.scala | 12 ++++++- config/worldserver.ini.dist | 29 ++++++++++++++++ pslogin/src/main/scala/SessionRouter.scala | 7 ++-- pslogin/src/main/scala/WorldConfig.scala | 4 +++ pslogin/src/test/scala/ConfigTest.scala | 33 +++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/common/src/main/scala/net/psforever/config/ConfigParser.scala b/common/src/main/scala/net/psforever/config/ConfigParser.scala index 173a27eb9..24942d403 100644 --- a/common/src/main/scala/net/psforever/config/ConfigParser.scala +++ b/common/src/main/scala/net/psforever/config/ConfigParser.scala @@ -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() diff --git a/config/worldserver.ini.dist b/config/worldserver.ini.dist index a62c76033..1a240980d 100644 --- a/config/worldserver.ini.dist +++ b/config/worldserver.ini.dist @@ -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 diff --git a/pslogin/src/main/scala/SessionRouter.scala b/pslogin/src/main/scala/SessionRouter.scala index acc1d3c40..890512dd6 100644 --- a/pslogin/src/main/scala/SessionRouter.scala +++ b/pslogin/src/main/scala/SessionRouter.scala @@ -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 } } diff --git a/pslogin/src/main/scala/WorldConfig.scala b/pslogin/src/main/scala/WorldConfig.scala index e6de5ae7d..baca9d02b 100644 --- a/pslogin/src/main/scala/WorldConfig.scala +++ b/pslogin/src/main/scala/WorldConfig.scala @@ -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)), diff --git a/pslogin/src/test/scala/ConfigTest.scala b/pslogin/src/test/scala/ConfigTest.scala index 56f9285cd..6bbea4336 100644 --- a/pslogin/src/test/scala/ConfigTest.scala +++ b/pslogin/src/test/scala/ConfigTest.scala @@ -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 {