Skip to content
Snippets Groups Projects
Commit bc2610a4 authored by Marwan Azuz's avatar Marwan Azuz :thumbsup: Committed by Clément Pit-Claudel
Browse files

server: Add a garbage collector

The main idea is straightforward: every 1 minute (`GC_EVERY`),
- if there are more than 10 unused instances (`GC_AFTER_UNUSED_INSTANCES`), take
  the least recently used instances and shut it down.
- otherwise, check every unused instance and if it hasn't been used for 24 hours,
  shut it down.

The criterias to consider a webapp unused is if no one is connected.
“Strategic” handlers are added in `ServerApp.scala`
parent 3c86c99d
No related branches found
No related tags found
1 merge request!31server: Add a GarbageCollection
package cs214.webapp
package server.web
import scala.collection.concurrent
import scala.concurrent.duration.Duration
import java.util.concurrent.TimeUnit
object GarbageCollector:
/// When to do GC?
private val GC_EVERY = Duration(1, TimeUnit.MINUTES)
/// Forces GC after this number of unused instances
private val GC_AFTER_UNUSED_INSTANCES = 10
/// GC an instance after this unused time.
private val GC_AFTER_DURATION = Duration(24, TimeUnit.HOURS)
type LastUsedAt = Long
/** Contains the list of instances that are not used */
private val scheduledForDeletion = concurrent.TrieMap[InstanceId, LastUsedAt]()
/** Shedules an instance for a garbage collection */
def scheduleForGC(i: InstanceId) =
scheduledForDeletion += ((i, System.currentTimeMillis()))
/** Unschedules an instance already scheduled for GC */
def unscheduleForGC(i: InstanceId) =
scheduledForDeletion -= i
def shutdownApp(i: InstanceId) =
unscheduleForGC(i)
WebServer.shutdownApp(i)
/** Garbage collect instances */
def garbageCollect =
val snap = scheduledForDeletion.snapshot()
// Conditions to trigger garbage collection
if snap.size >= GC_AFTER_UNUSED_INSTANCES then
val (i, _) = snap.toList.minBy(_._2)
println(f"Stopping instance $i because there is more than $GC_AFTER_UNUSED_INSTANCES inactive instances.")
shutdownApp(i)
else
for
(i, t) <- snap
if (Duration(t, TimeUnit.MILLISECONDS) + GC_AFTER_DURATION).toMillis <= System.currentTimeMillis()
do
println(f"Stopping instance $i after $GC_AFTER_DURATION inactivity.")
shutdownApp(i)
/** The thread running the garbage collection */
class Deleter extends Thread("gc"):
override def run(): Unit =
println("GC started.")
try
while true do
Thread.sleep(GC_EVERY.toMillis)
garbageCollect
catch
case e: InterruptedException => println("GC stopped.")
private var gcThread: Option[Thread] = None
/** Starts the garbage collector thread */
def start() =
gcThread = Some(Deleter())
gcThread.foreach(_.start())
/** Stops the garbage collection thread */
def shutdown() =
gcThread.map(_.interrupt())
gcThread = None
...@@ -39,6 +39,9 @@ private[web] abstract class ServerApp: ...@@ -39,6 +39,9 @@ private[web] abstract class ServerApp:
val instanceId: InstanceId val instanceId: InstanceId
val registeredUserIds: Seq[UserId] val registeredUserIds: Seq[UserId]
// The instance should be considered inactive until a user connects.
inactive()
protected object instanceLock protected object instanceLock
def appInfo: AppInfo def appInfo: AppInfo
...@@ -65,6 +68,7 @@ private[web] abstract class ServerApp: ...@@ -65,6 +68,7 @@ private[web] abstract class ServerApp:
def connect(userId: UserId) def connect(userId: UserId)
(implicit cc: castor.Context, log: cask.Logger): cask.WebsocketResult = instanceLock.synchronized: (implicit cc: castor.Context, log: cask.Logger): cask.WebsocketResult = instanceLock.synchronized:
cask.WsHandler: channel => cask.WsHandler: channel =>
active()
channels.getOrElseUpdate(userId, mutable.Set()).add(channel) channels.getOrElseUpdate(userId, mutable.Set()).add(channel)
println(f"[${appInfo.id}/$instanceId] client \"$userId\" connected") println(f"[${appInfo.id}/$instanceId] client \"$userId\" connected")
send(userId): send(userId):
...@@ -83,6 +87,13 @@ private[web] abstract class ServerApp: ...@@ -83,6 +87,13 @@ private[web] abstract class ServerApp:
channels(userId).remove(channel) channels(userId).remove(channel)
if channels(userId).isEmpty then channels.remove(userId) if channels(userId).isEmpty then channels.remove(userId)
println(f"[${appInfo.id}/$instanceId] client \"$userId\" disconnected") println(f"[${appInfo.id}/$instanceId] client \"$userId\" disconnected")
if channels.isEmpty then inactive()
/** Called when the app becomes active */
def active() = GarbageCollector.unscheduleForGC(instanceId)
/** Called when the app becomes inactive */
def inactive() = GarbageCollector.scheduleForGC(instanceId)
/** Enumerates clients connected to this instance. */ /** Enumerates clients connected to this instance. */
def connectedClients: Seq[UserId] = instanceLock.synchronized: def connectedClients: Seq[UserId] = instanceLock.synchronized:
......
...@@ -23,6 +23,8 @@ private case class Wordlist(fname: String): ...@@ -23,6 +23,8 @@ private case class Wordlist(fname: String):
/** Contains the web server state and functionalities */ /** Contains the web server state and functionalities */
object WebServer: object WebServer:
GarbageCollector.start()
private val instanceIdsFileName = "eff_short_wordlist_1.txt" // Where to find the list of possible instance ids private val instanceIdsFileName = "eff_short_wordlist_1.txt" // Where to find the list of possible instance ids
private val instanceIdLength = 4 // How many words do we concatenate for the instance id private val instanceIdLength = 4 // How many words do we concatenate for the instance id
private val instanceIdWords = Wordlist(instanceIdsFileName) private val instanceIdWords = Wordlist(instanceIdsFileName)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment