import collections import threading import contextlib from buzzard._tools import MultiOrderedDict _ERR_FMT = 'Dataset is configured for a maximum of {} simultaneous active driver objects \ but there are already {} idle objects and {} used objects' class BackDatasetActivationPoolMixin(object): """Private mixin for the Dataset class containing subroutines for proxies' driver objects pooling""" def __init__(self, max_active, **kwargs): self.max_active = max_active self._ap_lock = threading.Lock() self._ap_idle = MultiOrderedDict() self._ap_used = collections.Counter() super(BackDatasetActivationPoolMixin, self).__init__(**kwargs) def activate(self, uid, allocator): """Make sure at least one driver object is idle or used for uid""" with self._ap_lock: if self._ap_used[uid] == 0 and uid not in self._ap_idle: self._ensure_one_slot() self._ap_idle.push_front(uid, allocator()) def deactivate(self, uid): """Flush all occurrences of uid from _ap_idle. Raises an exception if uid is in _ap_used """ with self._ap_lock: if self._ap_used[uid] > 0: raise RuntimeError('Attempting to deactivate a source currently used') self._ap_idle.pop_all_occurrences(uid) def deactivate_many(self, uid_set): # TODO idea: allow recursive uids to group activated rasters and allow group deactivation if len(uid_set) == 0: return with self._ap_lock: being_used = uid_set & self._ap_used.keys() if being_used: raise RuntimeError('Attempting to deactivate {} source currently used'.format( len(being_used) )) idle = self._ap_idle & uid_set for uid in idle: self._ap_idle.pop_all_occurrences(uid) def used_count(self, uid=None): """Count how many driver objects exist for uid""" with self._ap_lock: if uid is None: return sum(self._ap_used.values()) else: return self._ap_used[uid] def idle_count(self, uid=None): """Count how many driver objects exist for uid""" with self._ap_lock: if uid is None: return len(self._ap_idle) else: return self._ap_idle.count(uid) def active_count(self, uid=None): """Count how many driver objects exist for uid""" with self._ap_lock: if uid is None: return len(self._ap_idle) + sum(self._ap_used.values()) else: return self._ap_idle.count(uid) + self._ap_used[uid] def acquire_driver_object(self, uid, allocator): """Return a context manager to acquire a driver object Example ------- >>> with back_ds.acquire(uid) as gdal_obj: ... pass """ @contextlib.contextmanager def _acquire(): with self._ap_lock: if uid in self._ap_idle: obj = self._ap_idle.pop_first_occurrence(uid) allocate = False else: self._ensure_one_slot() allocate = True self._ap_used[uid] += 1 if allocate: try: obj = allocator() except: with self._ap_lock: self._ap_used[uid] -= 1 raise try: yield obj finally: with self._ap_lock: self._ap_used[uid] -= 1 assert self._ap_used[uid] >= 0 self._ap_idle.push_front(uid, obj) if self._ap_used[uid] == 0: del self._ap_used[uid] return _acquire() def _ensure_one_slot(self): total = sum(self._ap_used.values()) + len(self._ap_idle) assert total <= self.max_active if total == self.max_active: if len(self._ap_idle) == 0: raise RuntimeError(_ERR_FMT.format( self.max_active, len(self._ap_idle), sum(self._ap_used.values()), )) self._ap_idle.pop_back()