~launchpad-pqm/launchpad/devel

12652.2.1 by Jeroen Vermeulen
Clean up some FakeLogger-related issues.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.18 by Karl Fogel
Add the copyright header block to files under lib/canonical/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4271.3.2 by Jeroen Vermeulen
Renamed TunedLoop to TunableLoop based on pre-impl chat with BjornT.
3
4
__metaclass__ = type
5
7675.599.4 by Graham Binns
Added a first test and a query. O happy day.
6
__all__ = [
7
    'DBLoopTuner',
14560.2.12 by Curtis Hovey
Move looptuner to lp.services.
8
    'ITunableLoop',
7675.599.4 by Graham Binns
Added a first test and a query. O happy day.
9
    'LoopTuner',
10
    'TunableLoop',
11
    ]
4271.3.2 by Jeroen Vermeulen
Renamed TunedLoop to TunableLoop based on pre-impl chat with BjornT.
12
13
8083.1.11 by Stuart Bishop
timedelta conversion
14
from datetime import timedelta
9893.11.3 by Stuart Bishop
Tests for exception handling edge cases
15
import sys
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
16
import time
17
8083.1.10 by Stuart Bishop
Ensure dblooptuner doesn't hold open transactions when blocked.
18
import transaction
8083.1.4 by Stuart Bishop
Missing imports
19
from zope.component import getUtility
14560.2.12 by Curtis Hovey
Move looptuner to lp.services.
20
from zope.interface import (
14612.2.1 by William Grant
format-imports on lib/. So many imports.
21
    implements,
14560.2.12 by Curtis Hovey
Move looptuner to lp.services.
22
    Interface,
23
    )
8083.1.4 by Stuart Bishop
Missing imports
24
14565.2.15 by Curtis Hovey
Moved canonical.launchpad.scripts __init__ to lp.services.scripts.
25
import lp.services.scripts
14600.2.2 by Curtis Hovey
Moved webapp to lp.services.
26
from lp.services.webapp.interfaces import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
27
    IStoreSelector,
28
    MAIN_STORE,
29
    MASTER_FLAVOR,
30
    )
4271.3.21 by Jeroen Vermeulen
Few more improvements suggested by reviewer.
31
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
32
14560.2.12 by Curtis Hovey
Move looptuner to lp.services.
33
class ITunableLoop(Interface):
34
    """Interface for self-tuning loop bodies to be driven by LoopTuner.
35
36
    To construct a self-tuning batched loop, define your loop body as a class
37
    implementing TunableLoop, and pass an instance to your LoopTuner.
38
    """
39
    def isDone():
40
        """Is this loop finished?
41
42
        Once this returns True, the LoopTuner will no longer touch this
43
        object.
44
        """
45
46
    def __call__(chunk_size):
47
        """Perform an iteration of the loop.
48
49
        The chunk_size parameter says (in some way you define) how much work
50
        the LoopTuner believes you should try to do in this iteration in order
51
        to get as close as possible to your time goal.
14625.2.6 by Colin Watson
Document that chunk_size is a float.
52
53
        Note that chunk_size is a float, so, for example, if you use it to
54
        slice a list, be careful to round it to an int first.
14560.2.12 by Curtis Hovey
Move looptuner to lp.services.
55
        """
56
57
    def cleanUp(self):
58
        """Clean up any open resources.
59
60
        Optional.
61
62
        This method is needed because loops may be aborted before
63
        completion, so clean up code in the isDone() method may
64
        never be invoked.
65
        """
66
67
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
68
class LoopTuner:
69
    """A loop that tunes itself to approximate an ideal time per iteration.
70
71
    Use this for large processing jobs that need to be broken down into chunks
72
    of such size that processing a single chunk takes approximately a given
73
    ideal time.  For example, large database operations may have to be
74
    performed and committed in a large number of small steps in order to avoid
75
    locking out other clients that need to access the same data.  Regular
76
    commits allow other clients to get their work done.
77
78
    In such a situation, committing for every step is often far too costly.
79
    Imagine inserting a million rows and committing after every row!  You
80
    could hand-pick a static number of steps per commit, but that takes a lot
81
    of experimental guesswork and it will still waste time when things go
82
    well, and on the other hand, it will still end up taking too much time per
83
    batch when the system slows down for whatever reason.
84
4271.3.16 by Jeroen Vermeulen
More feedback from the review team; made TunableLoop an interface; replaced performChunk() with __call__()
85
    Instead, define your loop body in an ITunableLoop; parameterize it on the
86
    number of steps per batch; say how much time you'd like to spend per
87
    batch; and pass it to a LoopTuner.  The LoopTuner will execute your loop,
88
    dynamically tuning its batch-size parameter to stay close to your time
89
    goal.  If things go faster than expected, it will ask your loop body to do
90
    more work for the next batch.  If a batch takes too much time, the next
91
    batch will be smaller.  There is also some cushioning for one-off spikes
92
    and troughs in processing speed.
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
93
    """
94
8870.1.2 by Stuart Bishop
Add abort times to loop tuner and garbo
95
    def __init__(
96
        self, operation, goal_seconds,
97
        minimum_chunk_size=1, maximum_chunk_size=1000000000,
98
        abort_time=None, cooldown_time=None, log=None):
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
99
        """Initialize a loop, to be run to completion at most once.
100
101
        Parameters:
102
4271.3.21 by Jeroen Vermeulen
Few more improvements suggested by reviewer.
103
        operation: an object implementing the loop body.  It must support the
104
            ITunableLoop interface.
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
105
4271.3.15 by Jeroen Vermeulen
Changes based on review by bac.
106
        goal_seconds: the ideal number of seconds for any one iteration to
107
            take.  The algorithm will vary chunk size in order to stick close
108
            to this ideal.
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
109
110
        minimum_chunk_size: the smallest chunk size that is reasonable.  The
111
            tuning algorithm will never let chunk size sink below this value.
4271.3.15 by Jeroen Vermeulen
Changes based on review by bac.
112
113
        maximum_chunk_size: the largest allowable chunk size.  A maximum is
4271.3.16 by Jeroen Vermeulen
More feedback from the review team; made TunableLoop an interface; replaced performChunk() with __call__()
114
            needed even if the ITunableLoop ignores chunk size for whatever
4271.3.15 by Jeroen Vermeulen
Changes based on review by bac.
115
            reason, since reaching floating-point infinity would seriously
116
            break the algorithm's arithmetic.
8083.1.14 by Stuart Bishop
Lower loglevel used by LoopTuner
117
7667.12.12 by Danilo Šegan
Clean up LoopTuner and provide cooldown_time.
118
        cooldown_time: time (in seconds, float) to sleep between consecutive
119
            operation runs.  Defaults to None for no sleep.
8083.1.14 by Stuart Bishop
Lower loglevel used by LoopTuner
120
8870.1.2 by Stuart Bishop
Add abort times to loop tuner and garbo
121
        abort_time: abort the loop, logging a WARNING message, if the runtime
122
            takes longer than this many seconds.
123
8083.1.14 by Stuart Bishop
Lower loglevel used by LoopTuner
124
        log: The log object to use. DEBUG level messages are logged
125
            giving iteration statistics.
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
126
        """
4271.3.21 by Jeroen Vermeulen
Few more improvements suggested by reviewer.
127
        assert(ITunableLoop.providedBy(operation))
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
128
        self.operation = operation
4271.3.15 by Jeroen Vermeulen
Changes based on review by bac.
129
        self.goal_seconds = float(goal_seconds)
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
130
        self.minimum_chunk_size = minimum_chunk_size
4271.3.8 by Jeroen Vermeulen
Make chunk_size a float (but round it in MultiTableCopy!), lots more tests
131
        self.maximum_chunk_size = maximum_chunk_size
7667.12.12 by Danilo Šegan
Clean up LoopTuner and provide cooldown_time.
132
        self.cooldown_time = cooldown_time
8870.1.2 by Stuart Bishop
Add abort times to loop tuner and garbo
133
        self.abort_time = abort_time
8083.1.6 by Stuart Bishop
Fix logging
134
        if log is None:
14565.2.15 by Curtis Hovey
Moved canonical.launchpad.scripts __init__ to lp.services.scripts.
135
            self.log = lp.services.scripts.log
8083.1.6 by Stuart Bishop
Fix logging
136
        else:
137
            self.log = log
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
138
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
139
    # True if this task has timed out. Set by _isTimedOut().
140
    _has_timed_out = False
141
8758.2.16 by Stuart Bishop
Review feedback
142
    def _isTimedOut(self, extra_seconds=0):
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
143
        """Return True if the task will be timed out in extra_seconds.
144
145
        If this method returns True, all future calls will also return
146
        True.
147
        """
148
        if self.abort_time is None:
149
            return False
150
        if self._has_timed_out:
151
            return True
152
        if self._time() + extra_seconds >= self.start_time + self.abort_time:
153
            self._has_timed_out = True
154
            return True
155
        return False
8758.2.15 by Stuart Bishop
Let looptuner jobs abort if they timeout during the cooldown, which might be hours in the case of dblooptuner waiting for replication lag to clear
156
7667.12.12 by Danilo Šegan
Clean up LoopTuner and provide cooldown_time.
157
    def run(self):
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
158
        """Run the loop to completion."""
9893.11.2 by Stuart Bishop
Better LoopTuner exception logging
159
        # Cleanup function, if we have one.
160
        cleanup = getattr(self.operation, 'cleanUp', lambda: None)
8758.2.35 by Stuart Bishop
Add optional cleanUp method to ITunableLoop
161
        try:
162
            chunk_size = self.minimum_chunk_size
163
            iteration = 0
164
            total_size = 0
165
            self.start_time = self._time()
166
            last_clock = self.start_time
167
            while not self.operation.isDone():
168
169
                if self._isTimedOut():
170
                    self.log.warn(
8758.3.2 by Stuart Bishop
Fix bad debug2 format string in looptuner, and make logging consistent
171
                        "Task aborted after %d seconds.", self.abort_time)
8758.2.35 by Stuart Bishop
Add optional cleanUp method to ITunableLoop
172
                    break
173
174
                self.operation(chunk_size)
175
176
                new_clock = self._time()
177
                time_taken = new_clock - last_clock
178
                last_clock = new_clock
12652.2.3 by Jeroen Vermeulen
Should call log.debug2, not log.log(DEBUG2, ...); XXX comment for later.
179
9893.6.44 by Stuart Bishop
Revert workarounds to Bug #741650
180
                self.log.debug2(
12652.2.1 by Jeroen Vermeulen
Clean up some FakeLogger-related issues.
181
                    "Iteration %d (size %.1f): %.3f seconds",
182
                    iteration, chunk_size, time_taken)
8758.2.35 by Stuart Bishop
Add optional cleanUp method to ITunableLoop
183
184
                last_clock = self._coolDown(last_clock)
185
186
                total_size += chunk_size
187
188
                # Adjust parameter value to approximate goal_seconds.
189
                # The new value is the average of two numbers: the
190
                # previous value, and an estimate of how many rows would
191
                # take us to exactly goal_seconds seconds. The weight in
192
                # this estimate of any given historic measurement decays
193
                # exponentially with an exponent of 1/2. This softens
194
                # the blows from spikes and dips in processing time. Set
195
                # a reasonable minimum for time_taken, just in case we
196
                # get weird values for whatever reason and destabilize
197
                # the algorithm.
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
198
                time_taken = max(self.goal_seconds / 10, time_taken)
199
                chunk_size *= (1 + self.goal_seconds / time_taken) / 2
8758.2.35 by Stuart Bishop
Add optional cleanUp method to ITunableLoop
200
                chunk_size = max(chunk_size, self.minimum_chunk_size)
201
                chunk_size = min(chunk_size, self.maximum_chunk_size)
202
                iteration += 1
203
204
            total_time = last_clock - self.start_time
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
205
            average_size = total_size / max(1, iteration)
206
            average_speed = total_size / max(1, total_time)
9893.6.44 by Stuart Bishop
Revert workarounds to Bug #741650
207
            self.log.debug2(
8758.3.2 by Stuart Bishop
Fix bad debug2 format string in looptuner, and make logging consistent
208
                "Done. %d items in %d iterations, %3f seconds, "
12652.2.1 by Jeroen Vermeulen
Clean up some FakeLogger-related issues.
209
                "average size %f (%s/s)",
210
                total_size, iteration, total_time, average_size,
211
                average_speed)
9893.11.2 by Stuart Bishop
Better LoopTuner exception logging
212
        except Exception:
9893.11.3 by Stuart Bishop
Tests for exception handling edge cases
213
            exc_info = sys.exc_info()
9893.11.2 by Stuart Bishop
Better LoopTuner exception logging
214
            try:
215
                cleanup()
216
            except Exception:
9893.11.3 by Stuart Bishop
Tests for exception handling edge cases
217
                # We need to raise the original exception, but we don't
218
                # want to lose the information about the cleanup
219
                # failure, so log it.
9893.11.2 by Stuart Bishop
Better LoopTuner exception logging
220
                self.log.exception("Unhandled exception in cleanUp")
9893.11.3 by Stuart Bishop
Tests for exception handling edge cases
221
            # Reraise the original exception.
222
            raise exc_info[0], exc_info[1], exc_info[2]
223
        else:
9893.11.2 by Stuart Bishop
Better LoopTuner exception logging
224
            cleanup()
4271.3.1 by Jeroen Vermeulen
First stab at extracting goal-oriented loop-tuning code
225
7667.12.12 by Danilo Šegan
Clean up LoopTuner and provide cooldown_time.
226
    def _coolDown(self, bedtime):
227
        """Sleep for `self.cooldown_time` seconds, if set.
228
229
        Assumes that anything the main LoopTuner loop does apart from
230
        doing a chunk of work or sleeping takes zero time.
231
232
        :param bedtime: Time the cooldown started, i.e. the time the
233
        chunk of real work was completed.
234
        :return: Time when cooldown completed, i.e. the starting time
235
        for a next chunk of work.
236
        """
237
        if self.cooldown_time is None or self.cooldown_time <= 0.0:
238
            return bedtime
239
        else:
8758.2.16 by Stuart Bishop
Review feedback
240
            self._sleep(self.cooldown_time)
7667.12.12 by Danilo Šegan
Clean up LoopTuner and provide cooldown_time.
241
            return self._time()
242
4271.3.4 by Jeroen Vermeulen
Hook for tests to fake time.time(), thanks BjornT!
243
    def _time(self):
244
        """Monotonic system timer with unit of 1 second.
245
246
        Overridable so tests can fake processing speeds accurately and without
247
        actually waiting.
248
        """
249
        return time.time()
250
8758.2.16 by Stuart Bishop
Review feedback
251
    def _sleep(self, seconds):
252
        """Sleep.
253
254
        If the sleep interval would put us over the tasks timeout,
255
        do nothing.
256
        """
8758.2.20 by Stuart Bishop
Sleep the correct amount of time
257
        if not self._isTimedOut(seconds):
258
            time.sleep(seconds)
8758.2.16 by Stuart Bishop
Review feedback
259
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
260
8083.1.11 by Stuart Bishop
timedelta conversion
261
def timedelta_to_seconds(td):
262
    return 24 * 60 * td.days + td.seconds
263
264
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
265
class DBLoopTuner(LoopTuner):
8083.1.14 by Stuart Bishop
Lower loglevel used by LoopTuner
266
    """A LoopTuner that plays well with PostgreSQL and replication.
267
268
    This LoopTuner blocks when database replication is lagging.
269
    Making updates faster than replication can deal with them is
270
    counter productive and in extreme cases can put the database
271
    into a death spiral. So we don't do that.
272
273
    This LoopTuner also blocks when there are long running
274
    transactions. Vacuuming is ineffective when there are long
275
    running transactions. We block when long running transactions
276
    have been detected, as it means we have already been bloating
277
    the database for some time and we shouldn't make it worse. Once
278
    the long running transactions have completed, we know the dead
279
    space we have already caused can be cleaned up so we can keep
280
    going.
281
282
    INFO level messages are logged when the DBLoopTuner blocks in addition
283
    to the DEBUG level messages emitted by the standard LoopTuner.
284
    """
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
285
286
    # We block until replication lag is under this threshold.
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
287
    acceptable_replication_lag = timedelta(seconds=30)  # In seconds.
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
288
289
    # We block if there are transactions running longer than this threshold.
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
290
    long_running_transaction = 30 * 60  # In seconds.
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
291
292
    def _blockWhenLagged(self):
293
        """When database replication lag is high, block until it drops."""
294
        # Lag is most meaningful on the master.
295
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
296
        msg_counter = 0
8758.2.16 by Stuart Bishop
Review feedback
297
        while not self._isTimedOut():
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
298
            lag = store.execute("SELECT replication_lag()").get_one()[0]
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
299
            if lag is None or lag <= self.acceptable_replication_lag:
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
300
                return
301
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
302
            # Report just once every 10 minutes to avoid log spam.
303
            msg_counter += 1
304
            if msg_counter % 60 == 1:
305
                self.log.info(
306
                    "Database replication lagged %s. "
8758.3.2 by Stuart Bishop
Fix bad debug2 format string in looptuner, and make logging consistent
307
                    "Sleeping up to 10 minutes.", lag)
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
308
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
309
            # Don't become a long running transaction!
310
            transaction.abort()
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
311
            self._sleep(10)
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
312
313
    def _blockForLongRunningTransactions(self):
314
        """If there are long running transactions, block to avoid making
315
        bloat worse."""
8758.2.16 by Stuart Bishop
Review feedback
316
        if self.long_running_transaction is None:
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
317
            return
318
        store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
8758.2.16 by Stuart Bishop
Review feedback
319
        msg_counter = 0
320
        while not self._isTimedOut():
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
321
            results = list(store.execute("""
322
                SELECT
323
                    CURRENT_TIMESTAMP - xact_start,
324
                    procpid,
325
                    usename,
326
                    datname,
327
                    current_query
7675.177.3 by Stuart Bishop
Make DBLoopTuner look up current activity via a SECURITY DEFINER wrapper to pg_stat_activity, so it can detect long running transactions when not running as a superuser
328
                FROM activity()
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
329
                WHERE xact_start < CURRENT_TIMESTAMP - interval '%f seconds'
8666.1.1 by Stuart Bishop
Transaction ids are now per-database, so no need to block on long running transactions in other databases
330
                    AND datname = current_database()
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
331
                ORDER BY xact_start LIMIT 4
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
332
                """ % self.long_running_transaction).get_all())
8083.1.5 by Stuart Bishop
Typo
333
            if not results:
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
334
                break
8758.2.15 by Stuart Bishop
Let looptuner jobs abort if they timeout during the cooldown, which might be hours in the case of dblooptuner waiting for replication lag to clear
335
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
336
            # Check for long running transactions every 10 seconds, but
8758.2.15 by Stuart Bishop
Let looptuner jobs abort if they timeout during the cooldown, which might be hours in the case of dblooptuner waiting for replication lag to clear
337
            # only report every 10 minutes to avoid log spam.
8758.2.16 by Stuart Bishop
Review feedback
338
            msg_counter += 1
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
339
            if msg_counter % 60 == 1:
8758.2.15 by Stuart Bishop
Let looptuner jobs abort if they timeout during the cooldown, which might be hours in the case of dblooptuner waiting for replication lag to clear
340
                for runtime, procpid, usename, datname, query in results:
341
                    self.log.info(
8758.3.2 by Stuart Bishop
Fix bad debug2 format string in looptuner, and make logging consistent
342
                        "Blocked on %s old xact %s@%s/%d - %s.",
343
                        runtime, usename, datname, procpid, query)
8758.2.15 by Stuart Bishop
Let looptuner jobs abort if they timeout during the cooldown, which might be hours in the case of dblooptuner waiting for replication lag to clear
344
                self.log.info("Sleeping for up to 10 minutes.")
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
345
            # Don't become a long running transaction!
346
            transaction.abort()
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
347
            self._sleep(10)
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
348
349
    def _coolDown(self, bedtime):
350
        """As per LoopTuner._coolDown, except we always wait until there
351
        is no replication lag.
352
        """
353
        self._blockForLongRunningTransactions()
354
        self._blockWhenLagged()
355
        if self.cooldown_time is not None and self.cooldown_time > 0.0:
356
            remaining_nap = self._time() - bedtime + self.cooldown_time
7675.670.2 by Stuart Bishop
Make timeout flag sticky, so loop bodies that detect things will timeout will cause the loop to terminate rather than spin
357
            if remaining_nap > 0.0:
358
                self._sleep(remaining_nap)
8083.1.1 by Stuart Bishop
DB specific looptuner that plays nicer with PostgreSQL
359
        return self._time()
360
7675.599.3 by Graham Binns
Moved TunableLoop to somewhere where we can import it without running the risk of getting all circular.
361
362
class TunableLoop:
363
    """A base implementation of `ITunableLoop`."""
364
    implements(ITunableLoop)
365
8758.3.21 by Stuart Bishop
Reduce default goal transaction time for tunable loops to 2 seconds
366
    goal_seconds = 2
7675.599.3 by Graham Binns
Moved TunableLoop to somewhere where we can import it without running the risk of getting all circular.
367
    minimum_chunk_size = 1
13980.2.1 by Jeroen Vermeulen
Lint. Lots of lint.
368
    maximum_chunk_size = None  # Override.
7675.599.3 by Graham Binns
Moved TunableLoop to somewhere where we can import it without running the risk of getting all circular.
369
    cooldown_time = 0
370
371
    def __init__(self, log, abort_time=None):
372
        self.log = log
373
        self.abort_time = abort_time
374
7675.809.6 by Robert Collins
Create a garbo daily task to clean up file access tokens.
375
    def isDone(self):
376
        """Return True when the TunableLoop is complete."""
377
        raise NotImplementedError(self.isDone)
378
7675.599.3 by Graham Binns
Moved TunableLoop to somewhere where we can import it without running the risk of getting all circular.
379
    def run(self):
380
        assert self.maximum_chunk_size is not None, (
381
            "Did not override maximum_chunk_size.")
382
        DBLoopTuner(
383
            self, self.goal_seconds,
8758.2.65 by Stuart Bishop
whitespace
384
            minimum_chunk_size=self.minimum_chunk_size,
385
            maximum_chunk_size=self.maximum_chunk_size,
386
            cooldown_time=self.cooldown_time,
387
            abort_time=self.abort_time,
388
            log=self.log).run()