root/projects/sAsync/trunk/sasync/pdict.py

Revision 56, 14.9 kB (checked in by edsuom, 1 year ago)

Removed task queueing from Twisted-Goodies, as it is now on its own in the AsynQueue? project; updated sAsync to use asynqueue package instead of twisted_goodies.taskqueue

Line 
1 # sAsync:
2 # An enhancement to the SQLAlchemy package that provides persistent
3 # dictionaries, text indexing and searching, and an access broker for
4 # conveniently managing database access, table setup, and
5 # transactions. Everything can be run in an asynchronous fashion using the
6 # Twisted framework and its deferred processing capabilities.
7 #
8 # Copyright (C) 2006-2007 by Edwin A. Suominen, http://www.eepatents.com
9 #
10 # This program is free software; you can redistribute it and/or modify it under
11 # the terms of the GNU General Public License as published by the Free Software
12 # Foundation; either version 2 of the License, or (at your option) any later
13 # version.
14 #
15 # This program is distributed in the hope that it will be useful, but WITHOUT
16 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17 # FOR A PARTICULAR PURPOSE.  See the file COPYING for more details.
18 #
19 # You should have received a copy of the GNU General Public License along with
20 # this program; if not, write to the Free Software Foundation, Inc., 51
21 # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22
23 """
24 Dictionary-like objects with behind-the-scenes database persistence
25
26 """
27
28 # Imports
29 from UserDict import DictMixin
30 from twisted.internet import defer
31 from misc import DeferredTracker
32
33 import items
34
35
36 class AsyncError(Exception):
37     """
38     The requested action is incompatible with asynchronous operations.
39     """
40
41
42 class PersistentDictBase(DictMixin, object):
43     """
44     I am a base class for a database-persistent dictionary-like object uniquely
45     identified by the hashable constructor argument I{ID}.
46
47     Before you use any instance of me, you must specify the parameters for
48     creating an SQLAlchemy database engine. A single argument is used, which
49     specifies a connection to a database via an RFC-1738 url. In addition, the
50     following keyword options can be employed, which are listed in the API docs
51     for L{sasync} and L{sasync.database.AccessBroker}.
52
53     You can set an engine globally, for all instances of me, via the
54     L{sasync.engine} package-level function, or via the L{AccessBroker.engine}
55     class method. Alternatively, you can specify an engine for one particular
56     instance by supplying the parameters to my constructor.
57     
58     In my default mode of operation, both read and write item accesses occur
59     asynchronously and return deferreds. However, you can put me into B{load}
60     mode by calling my L{preload} method. At that point, all my items will be
61     accessed synchronously as with any other dictionary. No other deferreds
62     will be returned from any item access. Lazy writing will still be done, but
63     behind the scenes and with no API access to write completions.
64
65     B{IMPORTANT}: As with all sasync data store objects, make sure you call my
66     L{shutdown} method for an instance of me that you're done with before
67     allowing that instance to be deleted.
68
69     @ivar isPreloadMode: Boolean flag that indicates if I am operating in
70         preload mode.
71
72     """
73     def __init__(self, ID, *url, **kw):
74         """
75         Instantiates me with an item store keyed to the supplied hashable
76         I{ID}.  Ensures that I have access to a class-wide instance of a
77         L{Search} object so that I can update the database's full-text index
78         when writing values containing text content.
79
80         In addition to any engine-specifying keywords supplied, the following
81         are particular to this constructor:
82
83         @param ID: A hashable object that is used as my unique identifier.
84         
85         @keyword search: Set C{True} if text indexing is to be updated when items
86           are added, updated, or deleted.
87
88         """
89         try:
90             self.ID = hash(ID)
91         except:
92             raise TypeError("Item IDs must be hashable")
93         # In-memory Caches
94         self.data, self.keyCache = {}, {}
95         # For tracking lazy writes
96         self.writeTracker = DeferredTracker()
97         # My very own persistent items store
98         if url:
99             self.i = items.Items(self.ID, url[0], **kw)
100         else:
101             self.i = items.Items(self.ID)
102         self.isPreloadMode = False
103
104     def preload(self):
105         """
106         This method preloads all my items from the database (which may take a
107         while), returning a C{Deferred} that fires when everything's ready and
108         I've completed the transition into B{preload} mode.
109         """
110         d = self.loadAll()
111         d.addCallback(lambda _: setattr(self, 'isPreloadMode', True))
112         return d
113    
114     def shutdown(self, *null):
115         """
116         Shuts down my database L{Transactor} and its synchronous task queue.
117         """
118         d = self.writeTracker.deferToAll()
119         d.addCallback(self.i.shutdown)
120         return d
121    
122     def loadAll(self, *null):
123         """
124         Loads all items from the database, setting my in-memory dict and key
125         cache accordingly.
126         """
127         def loaded(items):
128             self.data.clear()
129             self.data.update(items)
130             self.keyCache = dict.fromkeys(items.keys(), True)
131             return self.data
132
133         return self.i.loadAll().addCallback(loaded)
134
135     def deferToWrites(self, lastOnly=False):
136         """
137         @see: L{DeferredTracker.deferToAll}
138         
139         """
140         if lastOnly:
141             d = self.writeTracker.deferToLast()
142         else:
143             d = self.writeTracker.deferToAll()
144         return d
145
146
147 class PersistentDict(PersistentDictBase):
148     """
149     I am a database-persistent dictionary-like object with memory caching of
150     items and lazy writing.
151     
152     Getting, setting, or deleting my items returns C{Deferred} objects of the
153     Twisted asynchronous framework that fire when the underlying database
154     accesses are completed. Returning a deferred value avoids forcing the
155     client code to block while the real value is being read from the
156     database.
157
158     @ivar data: The in-memory dictionary that each instance of me uses to cache
159       values for a given ID.
160         
161     """
162    
163     #--- Core dict operations -------------------------------------------------
164    
165     def __getitem__(self, name):
166         """
167         Returns a C{Deferred} to the value of item I{name} or the value itself
168         if in preload mode.
169
170         The value is only loaded from the database if it isn't already in the
171         in-memory dictionary.
172         """
173         def valueLoaded(value):
174             if isinstance(value, items.Missing):
175                 raise KeyError(
176                     "No item '%s' in the database" % name)
177             self.data[name] = value
178             self.keyCache.setdefault(name, False)
179             return value
180
181         if name in self.data:
182             value = self.data[name]
183             if self.isPreloadMode:
184                 return value
185             else:
186                 return defer.succeed(value)
187         elif self.isPreloadMode:
188             raise KeyError(
189                 "No item '%s' in the database" % name)
190         else:
191             return self.i.load(name).addCallback(valueLoaded)
192
193     def __setitem__(self, name, value):
194         """
195         Sets item I{name} to I{value}, saving it to the database if there
196         isn't already an in-memory dictionary item with that exact value.
197         """
198         def valueLoaded(loadedValue):
199             if isinstance(loadedValue, items.Missing):
200                 # Item isn't in the database, so insert it
201                 return self.i.insert(name, value)
202             else:
203                 # Update current value of item in the database
204                 return self.i.update(name, value)
205
206         oldValue = self.data.get(name, None)
207         self.data[name] = value
208         self.keyCache.setdefault(name, False)
209         # Everything from here on is just lazy writing
210         if oldValue is None:
211             # We're writing an item that hasn't been loaded from the database
212             # yet
213             if self.isPreloadMode:
214                 # If it hasn't been loaded yet, in preload mode, it ain't there
215                 d = self.i.insert(name, value)
216             else:
217                 # Not in preload mode, so it may be in the database but not yet
218                 # loaded
219                 d = self.i.load(name)
220                 d.addCallback(valueLoaded)
221         else:
222             # There's already a value in the in-memory dictionary, update it
223             d = self.i.update(name, value)
224         self.writeTracker.put(d)
225
226     def __delitem__(self, name):
227         """
228         Deletes item I{name}, removing its entry from both the in-memory
229         dictionary and the database
230         """
231         if name in self.data:
232             del self.data[name]
233             self.keyCache.pop(name, None)
234             d = self.i.delete(name)
235             self.writeTracker.put(d)
236         else:
237             raise KeyError(name)
238
239     def __contains__(self, key):
240         """
241         Indicates if I contain item I{key}.
242
243         In I{preload} mode, returns C{True} if the item is present in my
244         in-memory dictionary and C{False} if not.
245
246         In normal mode, returns an immediate C{Deferred} firing with C{True}
247         without a transaction if the item is already present in my in-memory
248         dictionary. If it isn't, tries to load the item (it will probably be
249         requested soon anyhow) and returns a C{Deferred} that will ultimately
250         fire with C{True} unless the load resulted in a L{Missing} object. In
251         that case, deletes the loaded C{Missing} object from my in-memory
252         dictionary and fires the deferred with C{False}.
253
254         Using the C{<key> in <dict>} Python construct doesn't seem to work in
255         normal mode. Use L{has_key} instead.
256         """
257         if self.isPreloadMode:
258             return self.data.__contains__(key)
259         elif key in self.data or key in self.keyCache:
260             return defer.succeed(True)
261         else:
262             d = self.i.load(key)
263             d.addCallback(lambda value: not isinstance(value, items.Missing))
264             return d
265
266     def keys(self):
267         """
268         Returns an immediate or deferred list of the names of all my items in
269         the database.
270         """
271         def gotKeyList(keyList):
272             self.keyCache = dict.fromkeys(keyList, True)
273             return keyList
274
275         if self.isPreloadMode:
276             return self.data.keys()
277         if True in self.keyCache.values():
278             # The key cache is valid as long as it has entries (=True) that were
279             # retrieved from preloading or a previous call of this method. The
280             # __setitem__ method will add new keys to the cache, but that
281             # doesn't initialize it.
282             keys = self.keyCache.keys()
283             return defer.succeed(keys)
284         # Empty or invalid key cache, load and cache a list of keys
285         return self.i.names().addCallback(gotKeyList)
286
287     #--- Replacement dict methods as needed -----------------------------------
288
289     def has_key(self, key):
290         """
291         Returns an immediate or deferred Boolean indicating whether the key is
292         present.
293         """
294         return self.__contains__(key)
295
296     def clear(self):
297         """
298         Clears the in-memory dictionary of all items and deletes all their
299         database entries.
300         """
301         self.keyCache.clear()
302         self.data.clear()
303         d = self.writeTracker.deferToAll()
304         d.addCallback(lambda _: self.i.names())
305         d.addCallback(lambda names: self.i.delete(*names))
306         self.writeTracker.put(d)
307         return d
308
309     def iteritems(self):
310         """
311         B{Only for preload mode}: Iterate over all my items.
312         """
313         if self.isPreloadMode:
314             for item in self.data.iteritems():
315                 yield item
316         else:
317             raise AsyncError("Can't iterate asynchronously")
318
319     def iterkeys(self):
320         """
321         B{Only for preload mode}: Iterate over all my keys.
322         """
323         if self.isPreloadMode:
324             for key in self.data.iterkeys():
325                 yield key
326         else:
327             raise AsyncError("Can't iterate asynchronously")
328
329     def itervalues(self):
330         """
331         B{Only for preload mode}: Iterate over all my values.
332         """
333         if self.isPreloadMode:
334             for value in self.data.itervalues():
335                 yield values
336         else:
337             raise AsyncError("Can't iterate asynchronously")
338
339     def items(self):
340         """
341         Returns an immediate or deferred sequence of (name, value) tuples
342         representing all my items.
343         """
344         if self.isPreloadMode:
345             return self.data.items()
346         else:
347             return self.loadAll().addCallback(lambda x: x.items())
348            
349     def values(self):
350         """
351         Returns an immediate or deferred sequence of all my values.
352         """
353         if self.isPreloadMode:
354             return self.data.values()
355         else:
356             return self.loadAll().addCallback(lambda x: x.values())
357
358     def get(self, *args):
359         """
360         Returns an immediate or deferred value of the value for the key
361         specified as the first argument, or a default value if specified as an
362         optional second argument. If the item is not present and no default
363         value is supplied, raises the appropriate exception.
364         """
365         def gotItem(loadedValue, key, defaultValue):
366             if isinstance(loadedValue, items.Missing):
367                 return defaultValue
368             else:
369                 self.data[key] = loadedValue
370                 return loadedValue
371
372         key = args[0]
373         if len(args) == 1:
374             return self[key]
375         defaultValue = args[1]
376         if self.isPreloadMode:
377             if self.has_key(key):
378                 return self[key]
379             return defaultValue
380         d = self.i.load(key)
381         d.addCallback(gotItem, key, defaultValue)
382         return d
383
384     def setdefault(self, key, value):
385         """
386         Sets my item specified by I{key} to I{value} if it doesn't exist
387         already.  Returns an immediate or deferred reference to the item's
388         value after its new value (if any) is set.
389         """
390         def gotItem(loadedValue):
391             if isinstance(loadedValue, items.Missing):
392                 self.__setitem__(key, value)
393                 d = self.writeTracker.deferToLast()
394                 d.addCallback(lambda _: value)
395                 return d
396             else:
397                 self.data[key] = loadedValue
398                 return loadedValue
399
400         if self.isPreloadMode:
401             if key in self.data:
402                 return self.data[key]
403             else:
404                 self.__setitem__(key, value)
405                 return value
406         elif key in self.data:
407             return defer.succeed(self.data[key])
408         else:
409             d = self.i.load(key)
410             d.addCallback(gotItem)
411             return d
412    
413     def copy(self):
414         """
415         Returns an immediate or deferred copy of me that is a conventional
416         (non-persisted) dictionary.
417         """
418         if self.isPreloadMode:
419             return self.data.copy()
420         else:
421             return self.loadAll()
422
423
424            
425    
426        
Note: See TracBrowser for help on using the browser.