| 1 | # -*- test-case-name: twisted.test.test_pb -*- |
|---|
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 3 | # See LICENSE for details. |
|---|
| 4 | |
|---|
| 5 | """ |
|---|
| 6 | Persistently cached objects for PB. |
|---|
| 7 | |
|---|
| 8 | Maintainer: Glyph Lefkowitz |
|---|
| 9 | |
|---|
| 10 | Future Plans: None known. |
|---|
| 11 | """ |
|---|
| 12 | |
|---|
| 13 | import time |
|---|
| 14 | |
|---|
| 15 | from twisted.internet import defer |
|---|
| 16 | from twisted.spread import banana, jelly, flavors |
|---|
| 17 | |
|---|
| 18 | |
|---|
| 19 | class Publishable(flavors.Cacheable): |
|---|
| 20 | """An object whose cached state persists across sessions. |
|---|
| 21 | """ |
|---|
| 22 | def __init__(self, publishedID): |
|---|
| 23 | self.republish() |
|---|
| 24 | self.publishedID = publishedID |
|---|
| 25 | |
|---|
| 26 | def republish(self): |
|---|
| 27 | """Set the timestamp to current and (TODO) update all observers. |
|---|
| 28 | """ |
|---|
| 29 | self.timestamp = time.time() |
|---|
| 30 | |
|---|
| 31 | def view_getStateToPublish(self, perspective): |
|---|
| 32 | '(internal)' |
|---|
| 33 | return self.getStateToPublishFor(perspective) |
|---|
| 34 | |
|---|
| 35 | def getStateToPublishFor(self, perspective): |
|---|
| 36 | """Implement me to special-case your state for a perspective. |
|---|
| 37 | """ |
|---|
| 38 | return self.getStateToPublish() |
|---|
| 39 | |
|---|
| 40 | def getStateToPublish(self): |
|---|
| 41 | """Implement me to return state to copy as part of the publish phase. |
|---|
| 42 | """ |
|---|
| 43 | raise NotImplementedError("%s.getStateToPublishFor" % self.__class__) |
|---|
| 44 | |
|---|
| 45 | def getStateToCacheAndObserveFor(self, perspective, observer): |
|---|
| 46 | """Get all necessary metadata to keep a clientside cache. |
|---|
| 47 | """ |
|---|
| 48 | if perspective: |
|---|
| 49 | pname = perspective.perspectiveName |
|---|
| 50 | sname = perspective.getService().serviceName |
|---|
| 51 | else: |
|---|
| 52 | pname = "None" |
|---|
| 53 | sname = "None" |
|---|
| 54 | |
|---|
| 55 | return {"remote": flavors.ViewPoint(perspective, self), |
|---|
| 56 | "publishedID": self.publishedID, |
|---|
| 57 | "perspective": pname, |
|---|
| 58 | "service": sname, |
|---|
| 59 | "timestamp": self.timestamp} |
|---|
| 60 | |
|---|
| 61 | class RemotePublished(flavors.RemoteCache): |
|---|
| 62 | """The local representation of remote Publishable object. |
|---|
| 63 | """ |
|---|
| 64 | isActivated = 0 |
|---|
| 65 | _wasCleanWhenLoaded = 0 |
|---|
| 66 | def getFileName(self, ext='pub'): |
|---|
| 67 | return ("%s-%s-%s.%s" % |
|---|
| 68 | (self.service, self.perspective, str(self.publishedID), ext)) |
|---|
| 69 | |
|---|
| 70 | def setCopyableState(self, state): |
|---|
| 71 | self.__dict__.update(state) |
|---|
| 72 | self._activationListeners = [] |
|---|
| 73 | try: |
|---|
| 74 | dataFile = file(self.getFileName(), "rb") |
|---|
| 75 | data = dataFile.read() |
|---|
| 76 | dataFile.close() |
|---|
| 77 | except IOError: |
|---|
| 78 | recent = 0 |
|---|
| 79 | else: |
|---|
| 80 | newself = jelly.unjelly(banana.decode(data)) |
|---|
| 81 | recent = (newself.timestamp == self.timestamp) |
|---|
| 82 | if recent: |
|---|
| 83 | self._cbGotUpdate(newself.__dict__) |
|---|
| 84 | self._wasCleanWhenLoaded = 1 |
|---|
| 85 | else: |
|---|
| 86 | self.remote.callRemote('getStateToPublish').addCallbacks(self._cbGotUpdate) |
|---|
| 87 | |
|---|
| 88 | def __getstate__(self): |
|---|
| 89 | other = self.__dict__.copy() |
|---|
| 90 | # Remove PB-specific attributes |
|---|
| 91 | del other['broker'] |
|---|
| 92 | del other['remote'] |
|---|
| 93 | del other['luid'] |
|---|
| 94 | # remove my own runtime-tracking stuff |
|---|
| 95 | del other['_activationListeners'] |
|---|
| 96 | del other['isActivated'] |
|---|
| 97 | return other |
|---|
| 98 | |
|---|
| 99 | def _cbGotUpdate(self, newState): |
|---|
| 100 | self.__dict__.update(newState) |
|---|
| 101 | self.isActivated = 1 |
|---|
| 102 | # send out notifications |
|---|
| 103 | for listener in self._activationListeners: |
|---|
| 104 | listener(self) |
|---|
| 105 | self._activationListeners = [] |
|---|
| 106 | self.activated() |
|---|
| 107 | dataFile = file(self.getFileName(), "wb") |
|---|
| 108 | dataFile.write(banana.encode(jelly.jelly(self))) |
|---|
| 109 | dataFile.close() |
|---|
| 110 | |
|---|
| 111 | |
|---|
| 112 | def activated(self): |
|---|
| 113 | """Implement this method if you want to be notified when your |
|---|
| 114 | publishable subclass is activated. |
|---|
| 115 | """ |
|---|
| 116 | |
|---|
| 117 | def callWhenActivated(self, callback): |
|---|
| 118 | """Externally register for notification when this publishable has received all relevant data. |
|---|
| 119 | """ |
|---|
| 120 | if self.isActivated: |
|---|
| 121 | callback(self) |
|---|
| 122 | else: |
|---|
| 123 | self._activationListeners.append(callback) |
|---|
| 124 | |
|---|
| 125 | def whenReady(d): |
|---|
| 126 | """ |
|---|
| 127 | Wrap a deferred returned from a pb method in another deferred that |
|---|
| 128 | expects a RemotePublished as a result. This will allow you to wait until |
|---|
| 129 | the result is really available. |
|---|
| 130 | |
|---|
| 131 | Idiomatic usage would look like:: |
|---|
| 132 | |
|---|
| 133 | publish.whenReady(serverObject.getMeAPublishable()).addCallback(lookAtThePublishable) |
|---|
| 134 | """ |
|---|
| 135 | d2 = defer.Deferred() |
|---|
| 136 | d.addCallbacks(_pubReady, d2.errback, |
|---|
| 137 | callbackArgs=(d2,)) |
|---|
| 138 | return d2 |
|---|
| 139 | |
|---|
| 140 | def _pubReady(result, d2): |
|---|
| 141 | '(internal)' |
|---|
| 142 | result.callWhenActivated(d2.callback) |
|---|