Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/branches/crazycache/dejavu/storage/storememcached.py

Revision 573 (checked in by fumanchu, 3 years ago)

Crazycache: promoted IndexSet?.indices.

Line 
1 import md5
2 import memcache
3
4 try:
5     set
6 except NameError:
7     from sets import Set as set
8
9 import dejavu
10 from dejavu import errors, logflags, storage
11 from geniusql import logic
12
13
14 simple_attr_lookup = logic.Expression(lambda x: x.Thing == 4
15                                       ).func.func_code.co_code
16
17
18 class MemcachedStorageManager(storage.StorageManager):
19     """A Storage Manager which keeps all data in memcached.
20     
21     memcached is a high-performance, distributed memory object caching
22     system, generic in nature, but intended for use in speeding up
23     dynamic web applications by alleviating database load.
24     
25     See http://www.danga.com/memcached/
26     and ftp://ftp.tummy.com/pub/python-memcached/
27     
28     IMPORTANT: data stuck into memcached is not guaranteed to be stable.
29     It may disappear at any time, according to an internal LRU algorithm.
30     In particular, you should be aware that the LRU algorithm is itself
31     partitioned by object size (into "slabs"), so that a newer object
32     may be removed before an older one if they are of significantly
33     different sizes.
34     
35     Options:
36         memcached.servers: a list of strings of the form 'IP-address:port'.
37             These will be passed directly into the memcache.Client instance.
38         
39         memcached.global_index: if True (the default), this store will
40             maintain a index over the identifiers of all stored objects in
41             memcached itself. This is the 'safe' choice, and necessary if
42             your only store is memcached. However, if you run this store as
43             an ObjectCache.cache, you should turn this off, allowing
44             ObjectCache.nextstore to maintain the primary indexes--this
45             allows the cache to run orders of magnitude faster.
46         
47         memcached.index_time: the timeout, in seconds, for any cached indexes.
48             Default is 300 seconds.
49     """
50    
51     def __init__(self, allOptions={}):
52         storage.StorageManager.__init__(self, allOptions)
53        
54         self.name = allOptions['name']
55         self.global_index = allOptions.pop("memcached.global_index", True)
56         self.index_time = allOptions.pop("memcached.index_time", 5 * 60)
57         self.primary_keys = {}
58         self.indexsets = {}
59         self.index_stride = 50
60        
61         cache_opts = dict([(k[10:], v) for k, v in allOptions.iteritems()
62                            if k.startswith("memcached.")])
63         self.client = memcache.Client(**cache_opts)
64    
65     def hash(self, object):
66         """Return a consistent hash for object (for use in a memcached key)."""
67         # TODO: can we add overflow support for collisions?
68         return md5.new(repr(object)).hexdigest()
69    
70     def _unit_key(self, unit):
71         """Return (ident, memcached key) for the given unit."""
72         cls = unit.__class__
73         ident = tuple([getattr(unit, name) for name in self.primary_keys[cls]])
74         key = "%s:%s:%s" % (self.name, cls.__name__, self.hash(ident))
75         return key
76    
77     def unit(self, cls, **kwargs):
78         """A single Unit which matches the given kwargs, else None.
79         
80         The first Unit matching the kwargs is returned; if no Units match,
81         None is returned.
82         """
83         keyset = set(kwargs.keys())
84        
85         # Try to retrieve a matching unit using its primary_keys.
86         # This will skip grabbing any indices (a HUGE optimization).
87         pk = self.primary_keys[cls]
88         if keyset >= set(pk):
89             return self._unit_by_primary_key(cls, pk, kwargs)
90        
91         # Try to retrieve a matching unit using an index.
92         # If self.global_index is True, the last one should
93         # be an index with propnames == []. See self.register.
94         indexset = self.indexsets[cls]
95         for index in indexset:
96             if keyset >= set(index):
97                 unit = indexset.unit(index, kwargs)
98                 if unit is not None:
99                     if self.logflags & logflags.RECALL:
100                         self.log(logflags.RECALL.message(cls, ('HIT', kwargs)))
101                     return unit
102        
103         # Return None since we have no more access paths.
104         if self.logflags & logflags.RECALL:
105             self.log(logflags.RECALL.message(cls, ('DEFER', kwargs)))
106         return None
107    
108     def xrecall(self, classes, expr=None, order=None, limit=None, offset=None):
109         """Yield units of the given cls which match the given expr."""
110         if isinstance(classes, dejavu.UnitJoin):
111             for units in self._xmultirecall(classes, expr, order=order,
112                                             limit=limit, offset=offset):
113                 yield units
114             return
115        
116         cls = classes
117         indexset = self.indexsets[cls]
118        
119         if expr:
120             if not isinstance(expr, logic.Expression):
121                 expr = logic.Expression(expr)
122             fc = expr.func.func_code
123             compkeys = fc.co_names[1:]
124             # TODO: allow multiple filter keys.
125             if (fc.co_code == simple_attr_lookup and len(compkeys) == 1):
126                 compvals = fc.co_consts[1:]
127                 filters = dict([(k, v) for k, v in zip(compkeys, compvals)])
128                
129                 # Try to retrieve a matching unit using its primary_keys.
130                 # This will skip grabbing any indices (a HUGE optimization).
131                 pk = self.primary_keys[cls]
132                 if set(compkeys) >= set(pk):
133                     yield self._unit_by_primary_key(cls, pk, filters)
134                     return
135                
136                 # Try to retrieve matching units using an index.
137                 # If self.global_index is True, the last one should
138                 # be an index with propnames == []. See self.register.
139                 for index in indexset:
140                     if set(compkeys) >= set(index):
141                         data = indexset.xrecall(index, filters)
142                         data = self._xrecall_inner(data, expr)
143                         for unit in self._paginate(data, order, limit, offset, single=True):
144                             yield unit
145                         return
146        
147         if self.global_index:
148             data = indexset.xrecall([], {})
149             data = self._xrecall_inner(data, expr)
150             for unit in self._paginate(data, order, limit, offset, single=True):
151                 yield unit
152         else:
153             # Yield nothing since we have no access paths.
154             pass
155    
156     def _xrecall_inner(self, units, expr=None):
157         """Private helper for self.xrecall."""
158         for unit in units:
159             if expr is None or expr(unit):
160                 # Must yield a sequence for use in _paginate.
161                 yield (unit,)
162    
163     def save(self, unit, forceSave=False):
164         """Store the unit."""
165         if self.logflags & logflags.SAVE:
166             self.log(logflags.SAVE.message(unit, forceSave))
167        
168         if forceSave or unit.dirty():
169             # Cleanse first because pickle state
170             # includes _initial_property_hash.
171             unit.cleanse()
172             self.client.set(self._unit_key(unit), unit)
173             self.indexsets[unit.__class__].add(unit)
174    
175     def destroy(self, unit):
176         """Delete the unit."""
177         if self.logflags & logflags.DESTROY:
178             self.log(logflags.DESTROY.message(unit))
179        
180         self.client.delete(self._unit_key(unit))
181         self.indexsets[unit.__class__].discard(unit)
182    
183     def reserve(self, unit):
184         """Reserve storage space for the Unit."""
185         if unit.identifiers:
186             cls = unit.__class__
187             indexset = self.indexsets[cls]
188            
189             if not unit.sequencer.valid_id(unit.identity()):
190                 if self.global_index:
191                     # Try to generate an identifier by looking
192                     # up all units in the global index.
193                     index = indexset.get({}) or []
194                     ids = [u.identity()
195                            for u in indexset.scan(index).itervalues()]
196                     unit.sequencer.assign(unit, ids)
197                 else:
198                     raise NotImplementedError(
199                         "Unindexed memcache cannot generate identifiers.")
200            
201             unit.cleanse()
202            
203             # Add the unit to the cache.
204             try:
205                 self.client.add(self._unit_key(unit), unit)
206             except IOError, exc:
207                 if exc.args[0] == 'NOT_STORED':
208                     pass
209                 raise
210            
211             # Add the unit to all indices.
212             indexset.add(unit)
213         else:
214             # This class has no identifiers, so skip reserve and wait for save.
215             pass
216        
217         # Usually we log ASAP, but here we log after
218         # the unit has had a chance to get an auto ID.
219         if self.logflags & logflags.RESERVE:
220             self.log(logflags.RESERVE.message(unit))
221    
222     def shutdown(self, conflicts='error'):
223         """Shut down all connections to internal storage.
224         
225         conflicts: see errors.conflict.
226         """
227         self.client.disconnect_all()
228    
229     def create_database(self, conflicts='error'):
230         """Create internal structures for the entire database.
231         
232         conflicts: see errors.conflict.
233         """
234         pass
235    
236     def drop_database(self, conflicts='error'):
237         """Destroy internal structures for the entire database.
238         
239         conflicts: see errors.conflict.
240         """
241         for cls in self.classes:
242             self.flush(cls)
243    
244     def create_storage(self, cls, conflicts='error'):
245         """Create internal structures for the given class.
246         
247         conflicts: see errors.conflict.
248         """
249         if self.logflags & logflags.DDL:
250             self.log(logflags.DDL.message("create storage %s" % cls))
251        
252         if self.global_index:
253             try:
254                 self.client.add(self.indexsets[cls].key({}), [])
255             except IOError, exc:
256                 if exc.args[0] == 'NOT STORED':
257                     errors.conflict(conflicts, "Class %r already has storage."
258                                     % cls)
259                 else:
260                     raise
261    
262     def has_storage(self, cls):
263         """If storage structures exist for the given class, return True."""
264         return True
265    
266     def drop_storage(self, cls, conflicts='error'):
267         """Destroy internal structures for the given class.
268         
269         conflicts: see errors.conflict.
270         """
271         if self.logflags & logflags.DDL:
272             self.log(logflags.DDL.message("drop storage %s" % cls))
273         self.flush(cls)
274    
275     def add_property(self, cls, name, conflicts='error'):
276         """Add internal structures for the given property.
277         
278         conflicts: see errors.conflict.
279         """
280         clsname = cls.__name__
281         if self.logflags & logflags.DDL:
282             self.log(logflags.DDL.message("add property %s %s" %
283                                           (clsname, name)))
284        
285         if self.global_index:
286             # TODO: recalculate if primary_keys changed
287             ci = self.client.get(self.indexsets[cls].key({})) or []
288             for id in ci:
289                 key = "%s:%s:%s" % (self.name, clsname, self.hash(id))
290                 unit = self.client.get(key)
291                 if unit is not None:
292                     unit._properties[name] = None
293                     unit.cleanse()
294                     self.client.set(key, unit)
295    
296     def has_property(self, cls, name):
297         """If storage structures exist for the given property, return True."""
298         if self.global_index:
299             clsname = cls.__name__
300             ci = self.client.get(self.indexsets[cls].key({}))
301            
302             if not ci:
303                 # We don't have any items, so there's nothing to
304                 # declare as 'unprepared'.
305                 return True
306            
307             for id in ci:
308                 key = "%s:%s:%s" % (self.name, clsname, self.hash(id))
309                 unit = self.client.get(key)
310                 if unit is not None:
311                     return name in unit._properties
312        
313         return True
314    
315     def drop_property(self, cls, name, conflicts='error'):
316         """Destroy internal structures for the given property.
317         
318         conflicts: see errors.conflict.
319         """
320         clsname = cls.__name__
321         if self.logflags & logflags.DDL:
322             self.log(logflags.DDL.message("drop property %s %s" %
323                                           (clsname, name)))
324        
325         if self.global_index:
326             ci = self.client.get(self.indexsets[cls].key({})) or []
327             for id in ci:
328                 key = "%s:%s:%s" % (self.name, clsname, self.hash(id))
329                 unit = self.client.get(key)
330                 if unit is not None:
331                     del unit._properties[name]
332                     unit.cleanse()
333                     self.client.set(key, unit)
334    
335     def rename_property(self, cls, oldname, newname, conflicts='error'):
336         """Rename internal structures for the given property.
337         
338         conflicts: see errors.conflict.
339         """
340         clsname = cls.__name__
341         if self.logflags & logflags.DDL:
342             self.log(logflags.DDL.message("rename property %s from %s to %s"
343                                           % (cls, oldname, newname)))
344        
345         if self.global_index:
346             ci = self.client.get(self.indexsets[cls].key({})) or []
347             for id in ci:
348                 key = "%s:%s:%s" % (self.name, clsname, self.hash(id))
349                 unit = self.client.get(key)
350                 if unit is not None:
351                     unit._properties[newname] = unit._properties[oldname]
352                     del unit._properties[oldname]
353                     unit.cleanse()
354                     self.client.set(key, unit)
355    
356    
357     #                   Extra methods for use as a cache                   #
358    
359     def cachelen(self, cls):
360         if self.global_index:
361             return len(self.client.get(self.indexsets[cls].key({})))
362         else:
363             return 0
364    
365     def cached_units(self, cls):
366         units = []
367         if self.global_index:
368             for key in self.client.get(self.indexsets[cls].key({})):
369                 unit = self.client.get(key)
370                 if unit is not None:
371                     units.append(unit)
372         return units
373    
374     def flush(self, cls):
375         """Dump all objects of the given class."""
376         clsname = cls.__name__
377        
378         if self.global_index:
379             gi_key = self.indexsets[cls].key({})
380             # Delete all units in the global index.
381             for id in self.client.get(gi_key) or []:
382                 key = "%s:%s:%s" % (self.name, clsname, self.hash(id))
383                 self.client.delete(key)
384            
385             # Delete the global index.
386             self.client.delete(gi_key)
387         # TODO:
388         # else:
389         #     self.increment_generation(cls)
390    
391     def register(self, cls):
392         """Assert that Units of class 'cls' will be handled."""
393         # Set a default primary key for the class. Consumers are free to
394         # change this if another unique property is looked up more often.
395         self.primary_keys[cls] = tuple(cls.identifiers or cls.properties)
396        
397         # Add indices based on the .index attribute of each UnitProperty.
398         self.indexsets[cls] = i = IndexSet(self, cls)
399         for propname in cls.properties:
400             prop = getattr(cls, propname)
401             if prop.index:
402                 # No need for an index on the primary key;
403                 # we can just fetch each one directly by cache key.
404                 if propname not in cls.identifiers:
405                     i.add_index(propname)
406        
407         # Add an index with no propnames. This is a special
408         # sentinel value for the global index that keeps us DRY.
409         if self.global_index:
410             i.add_index()
411        
412         storage.StorageManager.register(self, cls)
413    
414     def _unit_by_primary_key(self, cls, keys, filters):
415         """Return a unit (or None) by primary keys which matches the filters dict.
416         
417         The filters argument must contain an entry for each key in the
418         given list of keys, although it may and often should contain
419         additional entries.
420         """
421         ident = tuple([filters[k] for k in keys])
422         key = "%s:%s:%s" % (self.name, cls.__name__, self.hash(ident))
423         unit = self.client.get(key)
424         if unit is not None:
425             matching = True
426             if set(filters.keys()) > set(keys):
427                 # We retrieved the Unit using a subset of the filters.
428                 # Filter in full now.
429                 for k, v in filters.iteritems():
430                     if getattr(unit, k) != v:
431                         matching = False
432                         break
433            
434             if matching:
435                 if self.logflags & logflags.IO:
436                     self.log(logflags.IO.message('PK HIT (%s) %s' % (key, filters)))
437                 unit.cleanse()
438                 return unit
439        
440         if self.logflags & logflags.IO:
441             self.log(logflags.IO.message('PK MISS (%s) %s' % (key, filters)))
442         return None
443    
444     def scan(self, mainstore, cls, filters, order):
445         """Return units from a cached index, if possible."""
446         indexset = self.indexsets[cls]
447         keyattrs = self.primary_keys[cls]
448        
449         # Get a cached list of identifier-tuples, ordered if requested.
450         # TODO: add order to the idkey.
451         for index in indexset:
452             if set(filters.keys()) >= set(index):
453                 indexcriteria = dict([(k, filters[k]) for k in index])
454                 break
455         else:
456             raise ValueError("The given filters %r are not indexed for %r." %
457                              (tuple(filters.keys()), cls.__name__))
458        
459         ids = indexset.get(indexcriteria)
460         if ids is None:
461             # Not in the cache. Grab the list of id-tuples from nextstore.
462             ids = mainstore.view((cls, keyattrs, filters), order=order)
463             # Then cache the list result for next time.
464             indexset.put(indexcriteria, ids, time=self.index_time)
465            
466             # Query the cache for multiple units (by id).
467             units = indexset.scan(ids)
468             print units
469             misses = [k for k in ids if k not in units]
470             print misses
471         else:
472             # Query the cache for multiple units (by id).
473             units = indexset.scan(ids)
474            
475             # Remove any idents from the index node that no longer
476             # satisfy the index criteria. This is how we update
477             # index nodes--eager adds but late discards.
478             removals = False
479             for id, unit in units.items():
480                 for key, value in filters.iteritems():
481                     if getattr(unit, key) != value:
482                         removals = True
483                         del units[id]
484                         ids.remove(id)
485                         break
486             if removals:
487                 indexset.put(indexcriteria, ids, time=self.index_time)
488            
489             misses = [k for k in ids if k not in units]
490        
491         # Now query the nextstore for any units that the cache missed...
492         if self.index_stride:
493             # ...in chunks of length: self.index_stride.
494             for step in xrange(0, len(misses), self.index_stride):
495                 # TODO: allow for multiple identifiers
496                 misstep = zip(*misses[step:step + self.index_stride])[0]
497                 f = lambda x: getattr(x, keyattrs[0]) in misstep
498                 print f
499                 for unit in mainstore.recall(cls, f):
500                     units[tuple([getattr(unit, a) for a in keyattrs])] = unit
501         elif misses:
502             # ...or all in one chunk if desired.
503             # TODO: allow for multiple identifiers
504             misstep = zip(*misses)[0]
505             f = lambda x: getattr(x, keyattrs[0]) in misstep
506             print f
507             for unit in mainstore.recall(cls, f):
508                 units[tuple([getattr(unit, a) for a in keyattrs])] = unit
509        
510         # Preserve order
511         for k in ids:
512             yield units[k]
513
514
515 class IndexSet(object):
516     """A set of indices for a single class.
517     
518     Each index covers a tuple of unit attributes.
519     
520     Each leaf node of each index is stored in memcached under its own key;
521     each value is a list of tuple([unit.k for k in primary_keys[cls]]).
522     For example, given an index over ("age", ), each distinct recall
523     operation will produce its own index node:
524         
525         recall(Person, {age: 31}) -> ns:Person:index(age=31) = [(1132, 663)]
526         recall(Person, {age: 25}) -> ns:Person:index(age=25) = [(12, 34, 22)]
527         recall(Person, {age: 64}) -> ns:Person:index(age=64) = [(7, 17, 27)]
528     
529     In general, callers should use get, put, and scan together:
530         
531         ids = index.get(filters)
532         if ids is None:
533             ids = expensive_lookup(cls, filters)
534             index.put(filters, ids)
535         units = index.scan(ids)
536         misses = [k for k in ids if k not in units]
537     """
538    
539     def __init__(self, store, cls):
540         self.store = store
541         self.cls = cls
542         self._key_template = '%s:%s:index(%%s)' % (store.name, cls.__name__)
543         self.indices = []
544    
545     def add_index(self, *attributes):
546         """Add an index over the given attributes."""
547         # Sort them from most-specific (most properties) to least.
548         if attributes not in self.indices:
549             self.indices.append(attributes)
550             self.indices.sort(lambda x, y: cmp(len(y), len(x)))
551    
552     def __iter__(self):
553         return iter(self.indices)
554    
555     def key(self, filters):
556         """Return the cache key for the given filters.
557         
558         If filters is an empty dict, the 'global index' key is returned.
559         """
560         criteria = ["%s=%s" % (k, str(v).replace(" ", "+"))
561                     for k, v in filters.iteritems()]
562         return self._key_template % ",".join(criteria)
563    
564     def get(self, filters):
565         """Return a cached list of unit ids which match the given filters dict.
566         
567         The ids returned will be a list of tuples of the form:
568             tuple([getattr(unit, name) for name in primary_keys[cls]])
569         """
570         cache_key = self.key(filters)
571         ids = self.store.client.get(cache_key)
572         if self.store.logflags & logflags.IO:
573             if ids is None:
574                 idlen = None
575             else:
576                 idlen = len(ids)
577             self.store.log(logflags.IO.message("INDEX GET (%s) len %r" %
578                                                (cache_key, idlen)))
579         return ids
580    
581     def put(self, filters, ids, time=0):
582         """Cache a list of unit identifiers which match the given filters dict.
583         
584         The ids provided MUST be a list of tuples of the form:
585             tuple([getattr(unit, name) for name in primary_keys[cls]])
586         """
587         cache_key = self.key(filters)
588         if self.store.logflags & logflags.IO:
589             self.store.log(logflags.IO.message("INDEX PUT (%s) len %r: %r" %
590                                                (cache_key, len(ids), ids)))
591         self.store.client.set(cache_key, ids, time=time)
592    
593     def scan(self, ids):
594         """Return a dict of multiple units from the given list of ids.
595         
596         The ids provided MUST be a list of tuples of the form:
597             tuple([getattr(unit, name) for name in primary_keys[cls]])
598         
599         The returned dict will not contain entries for missed ids.
600         """
601         clsname = self.cls.__name__
602         if ids:
603             keys = ["%s:%s:%s" % (self.store.name, clsname, self.store.hash(id))
604                     for id in ids]
605             data = self.store.client.get_multi(keys)
606            
607             # Transform the dict back to id keys instead of cache keys.
608             units = {}
609             for i, k in zip(ids, keys):
610                 unit = data.get(k, None)
611                 if unit is not None:
612                     units[i] = unit
613         else:
614             units = {}
615        
616         if self.store.logflags & logflags.IO:
617             self.store.log(logflags.IO.message("INDEX SCAN %s (%r hits of %r)" %
618                                                (clsname, len(units), len(ids))))
619         return units
620    
621     def unit(self, index, filters):
622         """Return a unit from the index which matches the filters dict (or None).
623         
624         The filters argument must contain an entry for each key in the given
625         index, although it may and often should contain additional entries.
626         """
627         if set(filters.keys()) > set(index):
628             for unit in self.xrecall(index, filters):
629                 return unit
630         else:
631             clsname = self.cls.__name__
632             # If the filters and index keys are equal, it should be faster
633             # to perform single gets against memcached, rather than the
634             # get_multi calls that self.xrecall performs.
635             ids = self.get(dict([(k, filters[k]) for k in index]))
636             if ids:
637                 for id in ids:
638                     cache_key = "%s:%s:%s" % (self.store.name, clsname, self.hash(id))
639                     unit = self.client.get(cache_key)
640                     if unit is None:
641                         if self.store.logflags & logflags.IO:
642                             self.store.log(logflags.IO.message(
643                                 'INDEX MISS (%s) %s' % (cache_key, filters)))
644                     else:
645                         if self.store.logflags & logflags.IO:
646                             self.store.log(logflags.IO.message(
647                                 'INDEX HIT (%s) %s' % (cache_key, filters)))
648                         unit.cleanse()
649                         return unit
650             else:
651                 if self.store.logflags & logflags.IO:
652                     self.store.log(logflags.IO.message(
653                         'INDEX EMPTY (%s) %s' % (clsname, filters)))
654         return None
655    
656     def xrecall(self, index, filters):
657         """Yield units from the given index which match the filters dict.
658         
659         The filters argument must contain an entry for each key in the given
660         index, although it may and often should contain additional entries.
661         """
662         partial_index = set(filters.keys()) > set(index)
663         indexcriteria = dict([(k, filters[k]) for k in index])
664         ids = self.get(indexcriteria)
665         if ids:
666             units = self.scan(ids)
667            
668             removals = False
669             # Preserve order by iterating over the retrieved ids
670             # instead of the retrieved units.
671             for id in ids:
672                 unit = units.get(id, None)
673                 if unit is not None:
674                     for k, v in filters.iteritems():
675                         if getattr(unit, k) != v:
676                             if k in index:
677                                 removals = True
678                                 del units[id]
679                                 ids.remove(id)
680                             break
681                     else:
682                         unit.cleanse()
683                         yield unit
684            
685             if removals:
686                 # Remove any idents from the index node that no longer
687                 # satisfy the index criteria. This is how we update
688                 # index nodes--eager adds but late discards.
689                 self.put(indexcriteria, ids, time=self.store.index_time)
690    
691     def add(self, unit):
692         """Add the given unit to all indices."""
693         ident = tuple([getattr(unit, name)
694                        for name in self.store.primary_keys[self.cls]])
695         for index in self.indices:
696             indexcriteria = dict([(k, getattr(unit, k)) for k in index])
697             indexnode = self.get(indexcriteria) or []
698             if ident not in indexnode:
699                 indexnode.append(ident)
700             self.put(indexcriteria, indexnode)
701    
702     def discard(self, unit):
703         """Discard the given unit from all indices."""
704         ident = tuple([getattr(unit, name)
705                        for name in self.store.primary_keys[self.cls]])
706         for index in self.indices:
707             indexcriteria = dict([(k, getattr(unit, k)) for k in index])
708             indexnode = self.get(indexcriteria) or []
709             if ident in indexnode:
710                 indexnode.remove(ident)
711             self.put(indexcriteria, indexnode)
712
Note: See TracBrowser for help on using the browser.