combined NumberPoolHubActor and NumberPoolAccessorActor into UniqueNumberSystem; fixed issue with using NumberPoolHub straight-up

This commit is contained in:
FateJH 2017-09-26 15:51:37 -04:00
parent be0902fbc4
commit cc383420c8
3 changed files with 662 additions and 1 deletions

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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
}
}