Ticket #1396: callLater.patch.2.txt

File callLater.patch.2.txt, 10.9 KB (added by zooko, 6 years ago)
Line 
1Index: twisted/python/runtime.py
2===================================================================
3--- twisted/python/runtime.py   (revision 23668)
4+++ twisted/python/runtime.py   (working copy)
5@@ -24,22 +24,77 @@
6     'org.python.modules.os': 'java',
7     }
8 
9-_timeFunctions = {
10-    #'win32': time.clock,
11-    'win32': time.time,
12-    }
13+try:
14+    import _posix_clock
15+    # Make sure the monotonic clock source exists
16+    _posix_clock.gettime(_posix_clock.CLOCK_MONOTONIC)
17+except:
18+    _posix_clock = None
19 
20 class Platform:
21     """Gives us information about the platform we're running on"""
22 
23     type = knownPlatforms.get(os.name)
24-    seconds = staticmethod(_timeFunctions.get(type, time.time))
25+    seconds = time.time
26 
27+    _lastTicks = 0
28+    _offset = 0
29+   
30     def __init__(self, name=None):
31         if name is not None:
32             self.type = knownPlatforms.get(name)
33-            self.seconds = _timeFunctions.get(self.type, time.time)
34+
35+    def monotonicTicks(self):
36+        """Returns the current number of nanoseconds as an integer
37+        since some undefined epoch. The only hard requirement is that
38+        the number returned from this function is always strictly
39+        greater than any other number returned by it.
40+       
41+        Additionally, it is very good if time never skips around (such
42+        as by the user setting their system clock).
43+
44+        This default implementation doesn't have that property, as it
45+        is based upon the system clock, which can be set forward and
46+        backward in time. An implementation based upon
47+        clock_gettime(CLOCK_MONOTONIC) is used when available.
48+        """
49+       
50+        cur = int(self.seconds() * 1000000000) + self._offset
51+        if self._lastTicks >= cur:
52+            if self._lastTicks < cur + 10000000: # 0.01s
53+                # Just pretend epsilon more time has gone by.
54+                cur = self._lastTicks + 1
55+            else:
56+                # If lastSeconds is much larger than cur time, clock
57+                # must've moved backwards! Adjust the offset to keep
58+                # monotonicity.
59+                self._offset += self._lastTicks - cur
60+       
61+        self._lastTicks = cur
62+        return cur
63     
64+    if _posix_clock:
65+        def monotonicTicks2(self):
66+            cur = _posix_clock.gettime(_posix_clock.CLOCK_MONOTONIC)
67+            if self._lastTicks >= cur:
68+                cur += 1
69+            self._lastTicks = cur
70+            return cur
71+       
72+        monotonicTicks2.__doc__=monotonicTicks.__doc__
73+        monotonicTicks=monotonicTicks2
74+        del monotonicTicks2
75+
76+    def ticksToTime(self, ticks):
77+        """Returns the time (as returned by time.time) that
78+        corresponds to the given ticks value. If the time epoch
79+        changes via the user setting their system time,
80+        the time value of given ticks may or may not also change.
81+        """
82+        curticks = self.monotonicTicks()
83+        curtime = time.time()
84+        return (ticks - curticks)/1000000000. + curtime
85+       
86     def isKnown(self):
87         """Do we know about this platform?"""
88         return self.type != None
89@@ -80,3 +135,5 @@
90 platform = Platform()
91 platformType = platform.getType()
92 seconds = platform.seconds
93+monotonicTicks = platform.monotonicTicks
94+ticksToTime = platform.ticksToTime
95Index: twisted/python/dist.py
96===================================================================
97--- twisted/python/dist.py      (revision 23668)
98+++ twisted/python/dist.py      (working copy)
99@@ -8,7 +8,7 @@
100 
101 import sys, os
102 from distutils.command import build_scripts, install_data, build_ext, build_py
103-from distutils.errors import CompileError
104+from distutils.errors import CompileError, CCompilerError
105 from distutils import core
106 from distutils.core import Extension
107 
108@@ -328,6 +328,12 @@
109         build_ext.build_ext.build_extensions(self)
110 
111 
112+    def build_extension(self, ext):
113+        try:
114+            return build_ext.build_ext.build_extension(self, ext)
115+        except CCompilerError:
116+            sys.stderr.write("NOTE: failed to compile optional extension %s\n" % ext.name)
117+       
118     def _remove_conftest(self):
119         for filename in ("conftest.c", "conftest.o", "conftest.obj"):
120             try:
121Index: twisted/test/test_internet.py
122===================================================================
123--- twisted/test/test_internet.py       (revision 23668)
124+++ twisted/test/test_internet.py       (working copy)
125@@ -815,11 +815,11 @@
126 
127             clock.pump(reactor, [0, 1])
128 
129-            self.assertEquals(callbackTimes[0], 3)
130+            self.assertEquals(callbackTimes[0], 3*10**9)
131             self.assertEquals(callbackTimes[1], None)
132 
133             clock.pump(reactor, [0, 3])
134-            self.assertEquals(callbackTimes[1], 6)
135+            self.assertEquals(callbackTimes[1], 6*10**9)
136         finally:
137             clock.uninstall()
138 
139@@ -875,8 +875,7 @@
140         reactor.callLater(0.2, d.callback, None)
141         return d
142 
143-    testCallLaterOrder.todo = "See bug 1396"
144-    testCallLaterOrder.skip = "Trial bug, todo doesn't work! See bug 1397"
145+   
146     def testCallLaterOrder2(self):
147         # This time destroy the clock resolution so that it fails reliably
148         # even on systems that don't have a crappy clock resolution.
149@@ -884,19 +883,15 @@
150         def seconds():
151             return int(time.time())
152 
153-        base_original = base.seconds
154-        runtime_original = runtime.seconds
155-        base.seconds = seconds
156-        runtime.seconds = seconds
157+        from twisted.python import runtime
158+        runtime_original = runtime.platform.seconds
159+        runtime.platform.seconds = seconds
160 
161         def cleanup(x):
162-            runtime.seconds = runtime_original
163-            base.seconds = base_original
164+            runtime.platform.seconds = runtime_original
165             return x
166         return maybeDeferred(self.testCallLaterOrder).addBoth(cleanup)
167 
168-    testCallLaterOrder2.todo = "See bug 1396"
169-    testCallLaterOrder2.skip = "Trial bug, todo doesn't work! See bug 1397"
170 
171     def testDelayedCallStringification(self):
172         # Mostly just make sure str() isn't going to raise anything for
173Index: twisted/topfiles/setup.py
174===================================================================
175--- twisted/topfiles/setup.py   (revision 23668)
176+++ twisted/topfiles/setup.py   (working copy)
177@@ -47,6 +47,10 @@
178                                '-framework','CoreServices',
179                                '-framework','Carbon'],
180               condition=lambda builder: sys.platform == "darwin"),
181+
182+    Extension("twisted.python._posix_clock",
183+              ["twisted/python/_posix_clock.c"],
184+              libraries=['rt']),
185 ]
186 
187 # Figure out which plugins to include: all plugins except subproject ones
188Index: twisted/internet/task.py
189===================================================================
190--- twisted/internet/task.py    (revision 23668)
191+++ twisted/internet/task.py    (working copy)
192@@ -14,6 +14,7 @@
193 
194 from zope.interface import implements
195 
196+from twisted.python.runtime import monotonicTicks
197 from twisted.python import reflect
198 
199 from twisted.internet import base, defer
200@@ -76,7 +77,7 @@
201             raise ValueError, "interval must be >= 0"
202         self.running = True
203         d = self.deferred = defer.Deferred()
204-        self.starttime = self.clock.seconds()
205+        self.starttime = monotonicTicks() / 1000000000.
206         self._lastTime = self.starttime
207         self.interval = interval
208         if now:
209@@ -124,6 +125,7 @@
210             self.call = self.clock.callLater(0, self)
211             return
212 
213+        #fromNow = self.starttime - monotonicTicks() / 1000000000.
214         currentTime = self.clock.seconds()
215         # Find how long is left until the interval comes around again.
216         untilNextTime = (self._lastTime - currentTime) % self.interval
217Index: twisted/internet/base.py
218===================================================================
219--- twisted/internet/base.py    (revision 23668)
220+++ twisted/internet/base.py    (working copy)
221@@ -27,8 +27,9 @@
222 from twisted.internet.interfaces import IResolverSimple, IReactorPluggableResolver
223 from twisted.internet.interfaces import IConnector, IDelayedCall
224 from twisted.internet import main, error, abstract, defer, threads
225-from twisted.python import log, failure, reflect
226+from twisted.python import log, failure, reflect, components
227 from twisted.python.runtime import seconds as runtimeSeconds, platform, platformType
228+from twisted.python.runtime import monotonicTicks, platform, ticksToTime
229 from twisted.internet.defer import Deferred, DeferredList
230 from twisted.persisted import styles
231 
232@@ -79,7 +80,7 @@
233         @return: The number of seconds after the epoch at which this call is
234         scheduled to be made.
235         """
236-        return self.time + self.delayed_time
237+        return ticksToTime(self.time + self.delayed_time)
238 
239     def cancel(self):
240         """Unschedule this call
241@@ -115,7 +116,7 @@
242         elif self.called:
243             raise error.AlreadyCalled
244         else:
245-            newTime = self.seconds() + secondsFromNow
246+            newTime = monotonicTicks() + int(secondsFromNow * 1000000000)
247             if newTime < self.time:
248                 self.delayed_time = 0
249                 self.time = newTime
250@@ -138,7 +139,7 @@
251         elif self.called:
252             raise error.AlreadyCalled
253         else:
254-            self.delayed_time += secondsLater
255+            self.delayed_time += int(secondsLater * 1000000000)
256             if self.delayed_time < 0:
257                 self.activate_delay()
258                 self.resetter(self)
259@@ -174,7 +175,7 @@
260 
261         now = self.seconds()
262         L = ["<DelayedCall %s [%ss] called=%s cancelled=%s" % (
263-                id(self), self.time - now, self.called, self.cancelled)]
264+                id(self), (self.time - monotonicTicks()) / 1000000000., self.called, self.cancelled)]
265         if func is not None:
266             L.extend((" ", func, "("))
267             if self.args:
268@@ -640,7 +641,7 @@
269         assert callable(_f), "%s is not callable" % _f
270         assert sys.maxint >= _seconds >= 0, \
271                "%s is not greater than or equal to 0 seconds" % (_seconds,)
272-        tple = DelayedCall(self.seconds() + _seconds, _f, args, kw,
273+        tple = DelayedCall(monotonicTicks() + int(_seconds * 1000000000), _f, args, kw,
274                            self._cancelCallLater,
275                            self._moveCallLaterSooner,
276                            seconds=self.seconds)
277@@ -701,7 +702,7 @@
278         if not self._pendingTimedCalls:
279             return None
280 
281-        return max(0, self._pendingTimedCalls[0].time - self.seconds())
282+        return max(0, (self._pendingTimedCalls[0].time - monotonicTicks()) / 1000000000.)
283 
284 
285     def runUntilCurrent(self):
286@@ -729,7 +730,7 @@
287         # insert new delayed calls now
288         self._insertNewDelayedCalls()
289 
290-        now = self.seconds()
291+        now = monotonicTicks()
292         while self._pendingTimedCalls and (self._pendingTimedCalls[0].time <= now):
293             call = heappop(self._pendingTimedCalls)
294             if call.cancelled: