diff --git a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala index e360df53..e574c0ec 100644 --- a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala +++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala @@ -203,8 +203,8 @@ class NumberPoolHub(private val source : NumberSource) { val slctr = pool.Selector import net.psforever.objects.guid.selector.SpecificSelector val specific = new SpecificSelector - specific.SelectionIndex = number pool.Selector = specific + specific.SelectionIndex = number pool.Get() pool.Selector = slctr register_GetAvailableNumberFromSource(number) diff --git a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala new file mode 100644 index 00000000..7769b696 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub2.scala @@ -0,0 +1,313 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid + +import net.psforever.objects.entity.{IdentifiableEntity, NoGUIDException} +import net.psforever.objects.guid.key.LoanedKey +import net.psforever.objects.guid.pool.{ExclusivePool, GenericPool, NumberPool} +import net.psforever.objects.guid.source.NumberSource +import net.psforever.packet.game.PlanetSideGUID + +import scala.util.{Failure, Success, Try} + +class NumberPoolHub2(private val source : NumberSource) { + import scala.collection.mutable + private val hash : mutable.HashMap[String, NumberPool] = mutable.HashMap[String, NumberPool]() + private val bigpool : mutable.LongMap[String] = mutable.LongMap[String]() + hash += "generic" -> new GenericPool(bigpool, source.Size) + source.FinalizeRestrictions.foreach(i => bigpool += i.toLong -> "") //these numbers can never be pooled; the source can no longer restrict numbers + + def apply(number : PlanetSideGUID) : Option[IdentifiableEntity] = this(number.guid) + + def apply(number : Int) : Option[IdentifiableEntity] = source.Get(number).orElse(return None).get.Object + + def Numbers : List[Int] = bigpool.keys.map(key => key.toInt).toList + + def AddPool(name : String, pool : List[Int]) : NumberPool = { + if(hash.get(name).isDefined) { + throw new IllegalArgumentException(s"can not add pool $name - name already known to this hub?") + } + if(source.Size <= pool.max) { + throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size") + } + val collision = bigpool.keys.map(n => n.toInt).toSet.intersect(pool.toSet) + if(collision.nonEmpty) { + throw new IllegalArgumentException(s"can not add pool $name - it contains the following redundant numbers: ${collision.toString}") + } + pool.foreach(i => bigpool += i.toLong -> name) + hash += name -> new ExclusivePool(pool) + hash(name) + } + + def RemovePool(name : String) : List[Int] = { + if(name.equals("generic") || name.equals("")) { + throw new IllegalArgumentException("can not remove pool - generic or restricted") + } + val pool = hash.get(name).orElse({ + throw new IllegalArgumentException(s"can not remove pool - $name does not exist") + }).get + if(pool.Count > 0) { + throw new IllegalArgumentException(s"can not remove pool - $name is being used") + } + + hash.remove(name) + pool.Numbers.foreach(number => bigpool -= number) + pool.Numbers + } + + def GetPool(name : String) : Option[NumberPool] = if(name.equals("")) { None } else { hash.get(name) } + + def Pools : mutable.HashMap[String, NumberPool] = hash + + def WhichPool(number : Int) : Option[String] = { + val name = bigpool.get(number) + if(name.contains("")) { None } else { name } + } + + def WhichPool(obj : IdentifiableEntity) : Option[String] = { + try { + val number : Int = obj.GUID.guid + val entry = source.Get(number) + if(entry.isDefined && entry.get.Object.contains(obj)) { WhichPool(number) } else { None } + } + catch { + case _ : Exception => + None + } + } + + def register(obj : IdentifiableEntity) : Try[Int] = register(obj, "generic") + + def register(obj : IdentifiableEntity, number : Int) : Try[Int] = { + bigpool.get(number.toLong) match { + case Some(name) => + register_GetSpecificNumberFromPool(name, number) match { + case Success(key) => + key.Object = obj + Success(obj.GUID.guid) + case Failure(ex) => + Failure(new Exception(s"trying to register an object to a specific number but, ${ex.getMessage}")) + } + case None => + import net.psforever.objects.guid.selector.SpecificSelector + hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number + register(obj, "generic") + } + } + + private def register_GetSpecificNumberFromPool(name : String, number : Int) : Try[LoanedKey]= { + hash.get(name) match { + case Some(pool) => + val slctr = pool.Selector + import net.psforever.objects.guid.selector.SpecificSelector + val specific = new SpecificSelector + specific.SelectionIndex = number + pool.Selector = specific + pool.Get() + pool.Selector = slctr + register_GetAvailableNumberFromSource(number) + case None => + Failure(new Exception(s"number pool $name not defined")) + } + } + + private def register_GetAvailableNumberFromSource(number : Int) : Try[LoanedKey] = { + source.Available(number) match { + case Some(key) => + Success(key) + case None => + Failure(new Exception(s"number $number is unavailable")) + } + } + + def register(obj : IdentifiableEntity, name : String) : Try[Int] = { + try { + register_CheckNumberAgainstDesiredPool(obj, name, obj.GUID.guid) + } + catch { + case _ : Exception => + register_GetPool(name) match { + case Success(key) => + key.Object = obj + Success(obj.GUID.guid) + case Failure(ex) => + Failure(new Exception(s"trying to register an object but, ${ex.getMessage}")) + } + } + } + + private def register_CheckNumberAgainstDesiredPool(obj : IdentifiableEntity, name : String, number : Int) : Try[Int] = { + val directKey = source.Get(number) + if(directKey.isEmpty || !directKey.get.Object.contains(obj)) { + Failure(new Exception("object already registered, but not to this source")) + } + else if(!WhichPool(number).contains(name)) { + //TODO obj is not registered to the desired pool; is this okay? + Success(number) + } + else { + Success(number) + } + } + + private def register_GetPool(name : String) : Try[LoanedKey] = { + hash.get(name) match { + case Some(pool) => + register_GetNumberFromDesiredPool(pool) + case _ => + Failure(new Exception(s"number pool $name not defined")) + } + } + + private def register_GetNumberFromDesiredPool(pool : NumberPool) : Try[LoanedKey] = { + pool.Get() match { + case Success(number) => + register_GetMonitorFromSource(number) + case Failure(ex) => + Failure(ex) + } + } + + private def register_GetMonitorFromSource(number : Int) : Try[LoanedKey] = { + source.Available(number) match { + case Some(key) => + Success(key) + case _ => + throw NoGUIDException(s"a pool gave us a number $number that is actually unavailable") //stop the show; this is terrible! + } + } + + def register(number : Int) : Try[LoanedKey] = { + WhichPool(number) match { + case None => + import net.psforever.objects.guid.selector.SpecificSelector + hash("generic").Selector.asInstanceOf[SpecificSelector].SelectionIndex = number + register_GetPool("generic") + case Some(name) => + register_GetSpecificNumberFromPool(name, number) + } + } + + def register(name : String) : Try[LoanedKey] = register_GetPool(name) + + def latterPartRegister(obj : IdentifiableEntity, number : Int) : Try[IdentifiableEntity] = { + register_GetMonitorFromSource(number) match { + case Success(monitor) => + monitor.Object = obj + Success(obj) + case Failure(ex) => + Failure(ex) + } + } + + def unregister(obj : IdentifiableEntity) : Try[Int] = { + unregister_GetPoolFromObject(obj) match { + case Success(pool) => + val number = obj.GUID.guid + pool.Return(number) + source.Return(number) + obj.Invalidate() + Success(number) + case Failure(ex) => + Failure(new Exception(s"can not unregister this object: ${ex.getMessage}")) + } + } + + def unregister_GetPoolFromObject(obj : IdentifiableEntity) : Try[NumberPool] = { + WhichPool(obj) match { + case Some(name) => + unregister_GetPool(name) + case None => + Failure(throw new Exception("can not find a pool for this object")) + } + } + + private def unregister_GetPool(name : String) : Try[NumberPool] = { + hash.get(name) match { + case Some(pool) => + Success(pool) + case None => + Failure(new Exception(s"no pool by the name of '$name'")) + } + } + + def unregister(number : Int) : Try[Option[IdentifiableEntity]] = { + if(source.Test(number)) { + unregister_GetObjectFromSource(number) + } + else { + Failure(new Exception(s"can not unregister a number $number that this source does not own") ) + } + } + + private def unregister_GetObjectFromSource(number : Int) : Try[Option[IdentifiableEntity]] = { + source.Return(number) match { + case Some(obj) => + unregister_ReturnObjectToPool(obj) + case None => + unregister_ReturnNumberToPool(number) //nothing is wrong, but we'll check the pool + } + } + + private def unregister_ReturnObjectToPool(obj : IdentifiableEntity) : Try[Option[IdentifiableEntity]] = { + val number = obj.GUID.guid + unregister_GetPoolFromNumber(number) match { + case Success(pool) => + pool.Return(number) + obj.Invalidate() + Success(Some(obj)) + case Failure(ex) => + source.Available(number) //undo + Failure(new Exception(s"started unregistering, but ${ex.getMessage}")) + } + } + + private def unregister_ReturnNumberToPool(number : Int) : Try[Option[IdentifiableEntity]] = { + unregister_GetPoolFromNumber(number) match { + case Success(pool) => + pool.Return(number) + Success(None) + case _ => //though everything else went fine, we must still fail if this number was restricted all along + if(!bigpool.get(number).contains("")) { + Success(None) + } + else { + Failure(new Exception(s"can not unregister this number $number")) + } + } + } + + private def unregister_GetPoolFromNumber(number : Int) : Try[NumberPool] = { + WhichPool(number) match { + case Some(name) => + unregister_GetPool(name) + case None => + Failure(new Exception(s"no pool using number $number")) + } + } + + def latterPartUnregister(number : Int) : Option[IdentifiableEntity] = source.Return(number) + + def isRegistered(obj : IdentifiableEntity) : Boolean = { + try { + source.Get(obj.GUID.guid) match { + case Some(monitor) => + monitor.Object.contains(obj) + case None => + false + } + } + catch { + case _ : NoGUIDException => + false + } + } + + def isRegistered(number : Int) : Boolean = { + source.Get(number) match { + case Some(monitor) => + monitor.Policy == AvailabilityPolicy.Leased + case None => + false + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala new file mode 100644 index 00000000..9e37624f --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala @@ -0,0 +1,348 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.guid.actor + +import akka.actor.{Actor, ActorContext, ActorRef, Props} +import net.psforever.objects.entity.IdentifiableEntity +import net.psforever.objects.guid.NumberPoolHub + +import scala.util.{Failure, Success} + +/** + * An `Actor` that wraps around converted `NumberPool`s and synchronizes a portion of the number registration process. + * The ultimate goal is to manage a coherent group of unique identifiers for a given "region" (`Zone`). + * Both parts of the UID system sit atop the `Zone` for easy external access. + * The plain part - the `NumberPoolHub` here - is used for low-priority requests such as checking for existing associations. + * This `Actor` is the involved portion that paces registration and unregistration.
+ *
+ * A four part process is used for object registration tasks. + * First, the requested `NumberPool` is located among the list of known `NumberPool`s. + * Second, an asynchronous request is sent to that pool to retrieve a number. + * (Only any number. Only a failing case allows for selection of a specific number.) + * Third, the asynchronous request returns and the original information about the request is recovered. + * Fourth, both sides of the contract are completed by the object being assigned the number and + * the underlying "number source" is made to remember an association between the object and the number. + * Short circuits and recoveries as available on all steps though reporting is split between logging and callbacks. + * The process of removing the association between a number and object (unregistering) is a similar four part process.
+ *
+ * The important relationship between this `Actor` and the `Map` of `NumberPoolActors` is an "gate." + * A single `Map` is constructed and shared between multiple entry points to the UID system where requests are messaged. + * Multiple entry points send messages to the same `NumberPool`. + * That `NumberPool` deals with the messages one at a time and sends reply to each entry point that communicated with it. + * This process is almost as fast as the process of the `NumberPool` selecting a number. + * (At least, both should be fast.) + * @param guid the `NumberPoolHub` that is partially manipulated by this `Actor` + * @param poolActors a common mapping created from the `NumberPool`s in `guid`; + * there is currently no check for this condition save for requests failing + */ +class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActors : Map[String, ActorRef]) extends Actor { + /** Information about Register and Unregister requests that persists between messages to a specific `NumberPool`. */ + private val requestQueue : collection.mutable.LongMap[UniqueNumberSystem.GUIDRequest] = new collection.mutable.LongMap() + /** The current value for the next request entry's index. */ + private var index : Long = Long.MinValue + private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case Register(obj, Some(pname), None, call) => + val callback = call.getOrElse(sender()) + try { + obj.GUID //stop if object already has a GUID; sometimes this happens + AlreadyRegistered(obj, pname) + callback ! Success(obj) + } + catch { + case _ : Exception => + val id : Long = index + index += 1 + requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback) + RegistrationProcess(pname, id) + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.GiveNumber(number, id) => + id match { + case Some(nid : Long) => + RegistrationProcess(requestQueue.remove(nid), number, nid) + case _ => + log.warn(s"received a number but there is no request to process it; returning number to pool") + NoCallbackReturnNumber(number) //recovery? + //no callback is possible + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.NoNumber(ex, id) => + id match { + case Some(nid : Long) => + requestQueue.remove(nid) match { + case Some(entry) => + entry.replyTo ! Failure(ex) //ONLY callback that is possible + case None => ; + log.warn(s"awkward no number error $ex") //neither a successful request nor an entry of making the request + } + case None => ; + log.warn(s"awkward no number error $ex") //neither a successful request nor an entry of making the request + case _ => ; + log.warn(s"unrecognized request $id accompanying a no number error $ex") + } + + case Unregister(obj, call) => + val callback = call.getOrElse(sender()) + try { + val number = obj.GUID.guid + guid.WhichPool(number) match { + case Some(pname) => + val id : Long = index + index += 1 + requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback) + UnregistrationProcess(pname, number, id) + case None => + callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool")) + } + } + catch { + case _ : Exception => + log.info(s"$obj is already unregistered") + callback ! Success(obj) + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.ReturnNumberResult(number, None, id) => + id match { + case Some(nid : Long) => + UnregistrationProcess(requestQueue.remove(nid), number, nid) + case _ => + log.error(s"returned a number but there is no request to process it; recovering the number from pool") + NoCallbackGetSpecificNumber(number) //recovery? + //no callback is possible + } + + //this message is automatically sent by NumberPoolActor + case NumberPoolActor.ReturnNumberResult(number, Some(ex), id) => //if there is a problem when returning the number + id match { + case Some(nid : Long) => + requestQueue.remove(nid) match { + case Some(entry) => + entry.replyTo ! Failure(new Exception(s"for ${entry.target} with number $number, ${ex.getMessage}")) + case None => ; + log.error(s"could not find original request $nid that caused error $ex, but pool was $sender") + //no callback is possible + } + case None => ; + log.error(s"could not find original request $id that caused error $ex, but pool was $sender") + //no callback is possible + } + + case msg => + log.warn(s"unexpected message received - $msg") + } + + /** + * A step of the object registration process. + * Send a message to the `NumberPool` to request a number back. + * @param poolName the pool to which the object is trying to register + * @param id a potential identifier to associate this request + */ + private def RegistrationProcess(poolName : String, id : Long) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.GetAnyNumber(Some(id)) + case None => + //do not log; use callback + requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was registered")) + } + } + + /** + * A step of the object registration process. + * If there is a successful request object to be found, continue the registration request. + * @param request the original request data + * @param number the number that was drawn from a `NumberPool` + */ + private def RegistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = { + request match { + case Some(entry) => + processRegisterResult(entry, number) + case None => + log.error(s"returned a number but the rest of the request is missing (id:$id)") + if(id != Long.MinValue) { //check to ignore endless loop of error-catching + log.warn("returning number to pool") + NoCallbackReturnNumber(number) //recovery? + //no callback is possible + } + } + } + + /** + * A step of the object registration process. + * This step completes the registration by asking the `NumberPoolHub` to sort out its `NumberSource`. + * @param entry the original request data + * @param number the number to use + */ + private def processRegisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = { + val obj = entry.target + guid.latterPartRegister(obj, number) match { + case Success(_) => + entry.replyTo ! Success(obj) + case Failure(ex) => + //do not log; use callback + NoCallbackReturnNumber(number, entry.targetPool) //recovery? + entry.replyTo ! Failure(ex) + } + } + + /** + * A step of the object unregistration process. + * Send a message to the `NumberPool` to restore the availability of one of its numbers. + * @param poolName the pool to which the number will try to be returned + * @param number the number that was previously drawn from the specified `NumberPool` + * @param id a potential identifier to associate this request + */ + private def UnregistrationProcess(poolName : String, number : Int, id : Long) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.ReturnNumber(number, Some(id)) + case None => + //do not log; use callback + requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was de-registered")) + } + } + + /** + * A step of the object unregistration process. + * If there is a successful request object to be found, continue the registration request. + * @param request the original request data + * @param number the number that was drawn from the `NumberPool` + */ + private def UnregistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = { + request match { + case Some(entry) => + processUnregisterResult(entry, number) + case None => + log.error(s"returned a number but the rest of the request is missing (id:$id)") + if(id != Long.MinValue) { //check to ignore endless loop of error-catching + log.error("recovering the number from pool") + NoCallbackGetSpecificNumber(number) //recovery? + //no callback is possible + } + } + } + + /** + * A step of the object unregistration process. + * This step completes revoking of the object's registration by consulting the `NumberSource`. + * @param entry the original request data + * @param number the number to use + */ + private def processUnregisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = { + val obj = entry.target + guid.latterPartUnregister(number) match { + case Some(_) => + obj.Invalidate() + entry.replyTo ! Success(obj) + case None => + //do not log; use callback + NoCallbackGetSpecificNumber(number, entry.targetPool) //recovery? + entry.replyTo ! Failure(new Exception(s"failed to unregister a number; this may be a critical error")) + } + } + + /** + * Generate a relevant logging message for an object that is trying to register is actually already registered. + * @param obj the object that was trying to register + * @param poolName the pool to which the object was trying to register + */ + private def AlreadyRegistered(obj : IdentifiableEntity, poolName : String) : Unit = { + val msg = + guid.WhichPool(obj) match { + case Some(pname) => + if(poolName.equals(pname)) { + s"to pool $poolName" + } + else { + s"but to different pool $pname" + } + case None => + "but not to any pool known to this system" + } + log.warn(s"$obj already registered $msg") + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers. + * @param number the number that was drawn from a `NumberPool` + */ + private def NoCallbackReturnNumber(number : Int) : Unit = { + guid.WhichPool(number) match { + case Some(pname) => + NoCallbackReturnNumber(number, pname) + case None => + log.error(s"critical: tried to return number $number but could not find containing pool") + } + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers. + * To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine. + * @param number the number that was drawn from a `NumberPool` + * @param poolName the `NumberPool` from which the `number` was drawn + * @see `UniqueNumberSystem.UnregistrationProcess(Option[GUIDRequest], Int, Int)` + */ + private def NoCallbackReturnNumber(number : Int, poolName : String) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.ReturnNumber(number, Some(Long.MinValue)) + case None => + log.error(s"critical: tried to return number $number but did not find pool $poolName") + } + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers. + * @param number the number to be drawn from a `NumberPool` + */ + private def NoCallbackGetSpecificNumber(number : Int) : Unit = { + guid.WhichPool(number) match { + case Some(pname) => + NoCallbackGetSpecificNumber(number, pname) + case None => + log.error(s"critical: tried to re-register number $number but could not find containing pool") + } + } + + /** + * Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers. + * To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine. + * @param number the number to be drawn from a `NumberPool` + * @param poolName the `NumberPool` from which the `number` is to be drawn + * @see `UniqueNumberSystem.RegistrationProcess(Option[GUIDRequest], Int, Int)` + */ + private def NoCallbackGetSpecificNumber(number : Int, poolName : String) : Unit = { + poolActors.get(poolName) match { + case Some(pool) => + pool ! NumberPoolActor.GetSpecificNumber(number, Some(Long.MinValue)) + case None => + log.error(s"critical: tried to re-register number $number but did not find pool $poolName") + } + } +} + +object UniqueNumberSystem { + /** + * Persistent record of the important information between the time fo request and the time of reply. + * @param target the object + * @param targetPool the name of the `NumberPool` being used + * @param replyTo the callback `ActorRef` + */ + private final case class GUIDRequest(target : IdentifiableEntity, targetPool : String, replyTo : ActorRef) + + /** + * Transform `NumberPool`s into `NumberPoolActor`s and pair them with their name. + * @param poolSource where the raw `NumberPools` are located + * @param context used to create the `NumberPoolActor` instances + * @return a `Map` of the pool names to the `ActorRef` created from the `NumberPool` + */ + def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit context : ActorContext) : Map[String, ActorRef] = { + poolSource.Pools.map({ case ((pname, pool)) => + pname -> context.actorOf(Props(classOf[NumberPoolActor], pool), pname) + }).toMap + } +}