Skip to content

Commit e657f4b

Browse files
committed
feat: improve task polling
Implements an improved task polling mechanism with configurable wait time. This helps to minimize resource consumption on the Connect server while still providing responsive feedback.
1 parent 8add419 commit e657f4b

File tree

2 files changed

+12
-53
lines changed

2 files changed

+12
-53
lines changed

src/posit/connect/tasks.py

+4-22
Original file line numberDiff line numberDiff line change
@@ -97,39 +97,21 @@ def update(self, *args, **kwargs) -> None:
9797
result = response.json()
9898
super().update(**result)
9999

100-
def wait_for(self, *, initial_wait: int = 1, max_wait: int = 10, backoff: float = 1.5) -> None:
100+
def wait_for(self, *, wait: int = 1) -> None:
101101
"""Wait for the task to finish.
102102
103103
Parameters
104104
----------
105-
initial_wait : int, default 1
106-
Initial wait time in seconds. First API request will use this as the wait parameter.
107-
max_wait : int, default 10
108-
Maximum wait time in seconds between polling requests.
109-
backoff : float, default 1.5
110-
Backoff multiplier for increasing wait times.
105+
wait : int, default 1
106+
Wait time in seconds between polling requests.
111107
112108
Examples
113109
--------
114110
>>> task.wait_for()
115111
None
116-
117-
Notes
118-
-----
119-
This method implements an exponential backoff strategy to reduce the number of API calls
120-
while waiting for long-running tasks. The first request uses the initial_wait value,
121-
and subsequent requests increase the wait time by the backoff factor, up to max_wait. To disable exponential backoff, set backoff to 1.0.
122112
"""
123-
wait_time = initial_wait
124-
125113
while not self.is_finished:
126-
self.update()
127-
128-
# Wait client-side
129-
time.sleep(wait_time)
130-
131-
# Calculate next wait time with backoff
132-
wait_time = min(wait_time * backoff, max_wait)
114+
self.update(wait=wait)
133115

134116

135117
class Tasks(resources.Resources):

tests/posit/connect/test_tasks.py

+8-31
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def test(self):
118118
responses.get(
119119
f"https://connect.example/__api__/v1/tasks/{uid}",
120120
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": True},
121+
match=[matchers.query_param_matcher({"wait": 1})],
121122
),
122123
]
123124

@@ -127,35 +128,27 @@ def test(self):
127128
assert not task.is_finished
128129

129130
# invoke
130-
task.wait_for()
131+
task.wait_for(wait=1)
131132

132133
# assert
133134
assert task.is_finished
134135
assert mock_tasks_get[0].call_count == 1
135136
assert mock_tasks_get[1].call_count == 1
136137

137138
@responses.activate
138-
@mock.patch("time.sleep", autospec=True)
139-
def test_exponential_backoff(self, mock_sleep):
139+
def test_with_custom_wait(self):
140140
uid = "jXhOhdm5OOSkGhJw"
141141

142142
# behavior
143143
mock_tasks_get = [
144-
responses.get(
145-
f"https://connect.example/__api__/v1/tasks/{uid}",
146-
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": False},
147-
),
148-
responses.get(
149-
f"https://connect.example/__api__/v1/tasks/{uid}",
150-
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": False},
151-
),
152144
responses.get(
153145
f"https://connect.example/__api__/v1/tasks/{uid}",
154146
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": False},
155147
),
156148
responses.get(
157149
f"https://connect.example/__api__/v1/tasks/{uid}",
158150
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": True},
151+
match=[matchers.query_param_matcher({"wait": 5})],
159152
),
160153
]
161154

@@ -165,31 +158,19 @@ def test_exponential_backoff(self, mock_sleep):
165158
assert not task.is_finished
166159

167160
# invoke
168-
task.wait_for(initial_wait=1, max_wait=5, backoff=2.0)
161+
task.wait_for(wait=5)
169162

170163
# assert
171164
assert task.is_finished
172165
assert mock_tasks_get[0].call_count == 1
173166
assert mock_tasks_get[1].call_count == 1
174167

175-
# Verify sleep calls
176-
mock_sleep.assert_has_calls([mock.call(1), mock.call(2), mock.call(4)], any_order=False)
177-
178168
@responses.activate
179-
@mock.patch("time.sleep", autospec=True)
180-
def test_no_backoff(self, mock_sleep):
169+
def test_immediate_completion(self):
181170
uid = "jXhOhdm5OOSkGhJw"
182171

183172
# behavior
184173
mock_tasks_get = [
185-
responses.get(
186-
f"https://connect.example/__api__/v1/tasks/{uid}",
187-
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": False},
188-
),
189-
responses.get(
190-
f"https://connect.example/__api__/v1/tasks/{uid}",
191-
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": False},
192-
),
193174
responses.get(
194175
f"https://connect.example/__api__/v1/tasks/{uid}",
195176
json={**load_mock_dict(f"v1/tasks/{uid}.json"), "finished": True},
@@ -199,18 +180,14 @@ def test_no_backoff(self, mock_sleep):
199180
# setup
200181
c = connect.Client("https://connect.example", "12345")
201182
task = c.tasks.get(uid)
202-
assert not task.is_finished
183+
assert task.is_finished
203184

204185
# invoke
205-
task.wait_for(initial_wait=2, max_wait=5, backoff=1.0)
186+
task.wait_for(wait=1)
206187

207188
# assert
208189
assert task.is_finished
209190
assert mock_tasks_get[0].call_count == 1
210-
assert mock_tasks_get[1].call_count == 1
211-
212-
# Verify sleep calls
213-
mock_sleep.assert_has_calls([mock.call(2), mock.call(2)], any_order=False)
214191

215192

216193
class TestTasksGet:

0 commit comments

Comments
 (0)