5
5
from civis .response import Response
6
6
7
7
8
- _DEFAULT_POLLING_INTERVAL = 15
8
+ _MAX_POLLING_INTERVAL = 15
9
9
10
10
11
11
class _ResultPollingThread (threading .Thread ):
12
12
"""Poll a function until it returns a Response with a DONE state"""
13
13
14
14
# Inspired by `threading.Timer`
15
15
16
- def __init__ (self , poller , poller_args , polling_interval ):
16
+ def __init__ (self , pollable_result ):
17
17
super ().__init__ (daemon = True )
18
- self .polling_interval = polling_interval
19
- self .poller = poller
20
- self .poller_args = poller_args
18
+ self .pollable_result = pollable_result
21
19
self .finished = threading .Event ()
22
20
23
21
def cancel (self ):
@@ -31,11 +29,11 @@ def join(self, timeout=None):
31
29
32
30
def run (self ):
33
31
"""Poll until done."""
34
- while not self .finished .wait (self .polling_interval ):
32
+ while not self .finished .wait (self .pollable_result . _next_polling_interval ):
35
33
# Spotty internet connectivity can result in polling functions
36
34
# returning None. This treats None responses like responses which
37
35
# have a non-DONE state.
38
- poller_result = self .poller ( * self . poller_args )
36
+ poller_result = self .pollable_result . _check_result ( )
39
37
if poller_result is not None and poller_result .state in DONE :
40
38
self .finished .set ()
41
39
@@ -53,9 +51,14 @@ class PollableResult(CivisAsyncResultBase):
53
51
A function which returns an object that has a ``state`` attribute.
54
52
poller_args : tuple
55
53
The arguments with which to call the poller function.
56
- polling_interval : int or float
54
+ polling_interval : int or float, optional
57
55
The number of seconds between API requests to check whether a result
58
- is ready.
56
+ is ready. If an integer or float is provided, this number will be used
57
+ as the polling interval. If ``None`` (the default), the polling interval will
58
+ start at 1 second and increase geometrically up to 15 seconds. The ratio of
59
+ the increase is 1.2, resulting in polling intervals in seconds of
60
+ 1, 1.2, 1.44, 1.728, etc. This default behavior allows for a faster return for
61
+ a short-running job and a capped polling interval for longer-running jobs.
59
62
client : :class:`civis.APIClient`, optional
60
63
If not provided, an :class:`civis.APIClient` object will be
61
64
created from the :envvar:`CIVIS_API_KEY`.
@@ -103,18 +106,20 @@ def __init__(
103
106
client = None ,
104
107
poll_on_creation = True ,
105
108
):
106
- if polling_interval is None :
107
- polling_interval = _DEFAULT_POLLING_INTERVAL
108
- super ().__init__ (
109
- poller = poller ,
110
- poller_args = poller_args ,
111
- polling_interval = polling_interval ,
112
- client = client ,
113
- poll_on_creation = poll_on_creation ,
114
- )
115
- if self .polling_interval <= 0 :
109
+ super ().__init__ ()
110
+
111
+ self .poller = poller
112
+ self .poller_args = poller_args
113
+ self .polling_interval = polling_interval
114
+ self .client = client
115
+ self .poll_on_creation = poll_on_creation
116
+
117
+ if self .polling_interval is not None and self .polling_interval <= 0 :
116
118
raise ValueError ("The polling interval must be positive." )
117
119
120
+ self ._next_polling_interval = 1
121
+ self ._use_geometric_polling = True
122
+
118
123
# Polling arguments. Never poll more often than the requested interval.
119
124
if poll_on_creation :
120
125
self ._last_polled = None
@@ -153,8 +158,20 @@ def _check_result(self):
153
158
now = time .time ()
154
159
if (
155
160
not self ._last_polled
156
- or (now - self ._last_polled ) >= self .polling_interval
161
+ or (now - self ._last_polled ) >= self ._next_polling_interval
157
162
):
163
+ if self ._use_geometric_polling :
164
+ # Choosing a common ratio of 1.2 for these polling intervals:
165
+ # 1, 1.2, 1.44, 1.73, 2.07, 2.49, 2.99, ..., and capped at 15.
166
+ # Within the first 15 secs by wall time, we call the poller 7 times,
167
+ # which gives a short-running job's future.result()
168
+ # a higher chance to return faster.
169
+ # For longer running jobs, the polling interval will be capped
170
+ # at 15 secs when by wall time 87 secs have passed.
171
+ self ._next_polling_interval *= 1.2
172
+ if self ._next_polling_interval > _MAX_POLLING_INTERVAL :
173
+ self ._next_polling_interval = _MAX_POLLING_INTERVAL
174
+ self ._use_geometric_polling = False
158
175
# Poll for a new result
159
176
self ._last_polled = now
160
177
try :
@@ -204,18 +221,16 @@ def cleanup(self):
204
221
if self ._polling_thread .is_alive ():
205
222
self ._polling_thread .cancel ()
206
223
207
- def _reset_polling_thread (
208
- self , polling_interval = _DEFAULT_POLLING_INTERVAL , start_thread = False
209
- ):
224
+ def _reset_polling_thread (self , polling_interval , start_thread = False ):
210
225
with self ._condition :
211
226
if (
212
227
getattr (self , "_polling_thread" , None ) is not None
213
228
and self ._polling_thread .is_alive ()
214
229
):
215
230
self ._polling_thread .cancel ()
216
231
self .polling_interval = polling_interval
217
- self ._polling_thread = _ResultPollingThread (
218
- self ._check_result , (), polling_interval
219
- )
232
+ self ._next_polling_interval = 1 if ( pi := polling_interval ) is None else pi
233
+ self ._use_geometric_polling = polling_interval is None
234
+ self . _polling_thread = _ResultPollingThread ( self )
220
235
if start_thread :
221
236
self ._polling_thread .start ()
0 commit comments