# -*- test-case-name: mv3d.test.test_iddispenser -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ An IDDispenser is a transactional way to get a unique identifier for a given item. """ import sys import warnings from uuid import uuid4 from twisted.spread import pb from twisted.internet.defer import inlineCallbacks, returnValue from mv3d.net.ha import HighlyAvailable from mv3d.util.persist import Integer, List, Text, UUID, IDTuple, Persistable class IDError(Exception): """ Raised when there is a problem getting an ID """ class IDDispenser(Persistable, HighlyAvailable): """ An ID Dispenser can assign ids. For redundancy, it is shared across a pool of servers. All of these servers will have a complete copy of the available ids along with an up to date nextID counter. It is incredibly important that this pool have only one master because of the transactional nature of the dispenser. It can never be be allowed to give out the same ID to two different objects. """ parentID = IDTuple(autoSave=True, partialSave=True) nextID = Integer(default=0, autoSave=True, partialSave=True) available = List(Integer()) uid = Text(autoSave=True, partialSave=True) allowedState = ("nextID", "parentID", "allowed", "denied", "available", "uid") def __init__(self, parentID=None, datastore=None, uid=None): Persistable.__init__(self) HighlyAvailable.__init__(self) self.parentID = parentID self.datastore = datastore self.available = [] self.uid = uid or uuid4().hex self.addPermission('giveids') def getID(self): """ Return the ID of the object """ return self.uid def setParentID(self, pid): """ Set the id of our parent """ if self.priority != 1: return self.updateMaster("setParentID", pid) if not isinstance(pid, (tuple, list)): pid = (pid,) self.parentID = pid return self.updateAllSlaves("setParentID", pid) def observe_setParentID(self, pid): """ Set the parent id called from master """ self.parentID = pid return self.updateAllSlaves("setParentID", pid) def master_setParentID(self, _client, pid): """ Set the parent id called from slave """ return self.setParentID(pid) def isEmpty(self): """ Returns true if we can't give any more ids out """ return self.count() == 0 def count(self): """ Returns the number of ids available """ count = 0 if self.priority == 1: count = sys.maxint - self.nextID return count + len(self.available) def formatID(self, iid): """ Format this id """ if self.parentID is None: return iid if isinstance(self.parentID, int): par = [self.parentID] else: par = list(self.parentID) return tuple(par + [iid]) @inlineCallbacks def newID(self): """ Get the next id. This always returns a deferred. """ if self.priority != 1: theID = yield self.updateMaster("newID") returnValue(theID) if len(self.available): theID = self.available.pop(0) self.queueSave(selectAttributes=["available"]) yield self.updateAllSlaves("removeAvailable", [theID]) returnValue(self.formatID(theID)) if self.nextID == sys.maxint: raise IDError("No more IDs available") self.nextID += 1 yield self.updateAllSlaves("setNextID", self.nextID) returnValue(self.formatID(self.nextID - 1)) def observe_removeAvailable(self, toRemove): """ Removes a list of items from available list """ for iid in toRemove: self.available.remove(iid) def observe_setNextID(self, nextID): """ Set the next id """ self.nextID = nextID def master_newID(self, _client): """ Called by slaves """ return self.newID() @inlineCallbacks def newIDs(self, count): """ Get a number of new ids as specified by count. Returns a deferred. Makes sure that it fails early if there aren't that many available so that no ids are allocated and lost. """ if self.priority != 1: theIDs = yield self.updateMaster("newIDs", count) returnValue(theIDs) if count > self.count(): raise IDError("There aren't %d ids available" % count) theIDs = list(self.available[:count]) if len(theIDs): for usedID in theIDs: self.available.remove(usedID) yield self.updateAllSlaves("removeAvailable", theIDs) if len(theIDs) < count: count = count - len(theIDs) theIDs += range(self.nextID, self.nextID + count) self.nextID += count yield self.updateAllSlaves("setNextID", self.nextID) formattedIDs = [] for iid in theIDs: formattedIDs.append(self.formatID(iid)) returnValue(formattedIDs) def _scrubIDForReturn(self, iid): """ Get an ID ready to return but do not return it. Checks for various errors and makes sure the id is an int and not a tuple. """ if isinstance(iid, tuple): if self.parentID != iid[:-1] and (self.parentID,) != iid[:-1]: raise IDError("Tried returning an id that belongs to" " another dispenser! (%s != %s)" % (self.parentID, iid[:-1])) iid = iid[-1] if iid < 0 or iid > self.nextID or iid in self.available: raise IDError("Invalid ID passed to returnID %d" % iid) return iid def returnID(self, iid): """ Return a single ID into use with this dispenser. """ if self.priority != 1: return self.updateMaster("returnIDs", [iid]) self.available.append(self._scrubIDForReturn(iid)) self.queueSave(selectAttributes=["available"]) return self.updateAllSlaves("returnIDs", [iid]) def returnIDs(self, iids): """ Return multiple ids to use with this dispenser. ids should be a list of ids to return. This is transactional-- either they all succeed or they all fail! """ if self.priority != 1: return self.updateMaster("returnIDs", iids) corrected = [] for iid in iids: corrected.append(self._scrubIDForReturn(iid)) self.available.extend(corrected) self.queueSave(selectAttributes=["available"]) return self.updateAllSlaves("returnIDs", corrected) def observe_returnIDs(self, iids): """ Returns a list of ids to the dispenser """ self.available.extend(iids) self.queueSave(selectAttributes=["available"]) return self.updateAllSlaves("returnIDs", iids) def master_returnIDs(self, _client, iids): """ Return a list of ids to the master dispenser """ return self.returnIDs(iids) def getActiveIDs(self, offset=0, limit=0): """ Returns a list of ids that are supposedly active starting at the offset id and ending after the specified limit has been reached or we run out of active ids. If limit is 0, this will return all remaining active ids after start. """ if isinstance(offset, tuple): offset = offset[-1] if limit == 0: limit = self.nextID - offset ids = [] current = offset while len(ids) < limit and current < self.nextID: if not current in self.available: ids.append(self.formatID(current)) current += 1 return ids def iterActiveIDs(self, offset=0, limit=0): """ Returns a list of ids that are supposedly active starting at the offset id and ending after the specified limit has been reached or we run out of active ids. If limit is 0, this will return all remaining active ids after start. """ if isinstance(offset, tuple): offset = offset[-1] if limit == 0: limit = self.nextID - offset current = offset count = 0 while count != limit and current < self.nextID: if not current in self.available: yield self.formatID(current) count += 1 current += 1 def datastoreUpgrade(self, store, oldVersion): """ An old version of this object is being loaded from the store. If possible, convert it to the new version. """ if oldVersion == 1: return self.datastoreUpgradeV1(store) raise ValueError("Can't upgrade IDDispenser from version %d" % oldVersion) def datastoreUpgradeV1(self, store): """ Upgrade this object from version 1. This version had IDRanges instead of an available list. There were also some instance variable name changes. """ assert IDDispenser.datastoreVersion == 2 self.nextID = self.nextid del self.nextid self.parentID = self.parentid if isinstance(self.parentID, int): self.parentID = (self.parentID,) del self.parentid del self.refillamt query = store.query(IDRange) ranges = query.fetchResults(query.owner == self) self.available = [] for range_ in ranges: for iid in range(range_.start, range_.start + range_.length): self.available.append(iid) self.datastoreVersion = 2 pb.setUnjellyableForClass(IDDispenser, IDDispenser)