Skip to content
This repository was archived by the owner on Mar 13, 2022. It is now read-only.

Commit 06e48c5

Browse files
committed
Retry watch if request expires.
1 parent 3ea8003 commit 06e48c5

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

Diff for: watch/watch.py

+28-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import http
1516
import json
1617
import pydoc
1718

@@ -86,7 +87,7 @@ def get_watch_argument_name(self, func):
8687
def unmarshal_event(self, data, return_type):
8788
js = json.loads(data)
8889
js['raw_object'] = js['object']
89-
if return_type:
90+
if return_type and js['type'] != 'ERROR':
9091
obj = SimpleNamespace(data=json.dumps(js['raw_object']))
9192
js['object'] = self._api_client.deserialize(obj, return_type)
9293
if hasattr(js['object'], 'metadata'):
@@ -102,6 +103,14 @@ def unmarshal_event(self, data, return_type):
102103
def stream(self, func, *args, **kwargs):
103104
"""Watch an API resource and stream the result back via a generator.
104105
106+
Note that watching an API resource can expire. The method tries to
107+
resume automatically once from the last result, but if that last result
108+
is too old as well, an `ApiException` exception will be thrown with
109+
``code`` 410. In that case you have to recover yourself, probably
110+
by listing the API resource to obtain the latest state and then
111+
watching from that state on by setting ``resource_version`` to
112+
one returned from listing.
113+
105114
:param func: The API function pointer. Any parameter to the function
106115
can be passed after this parameter.
107116
@@ -134,14 +143,31 @@ def stream(self, func, *args, **kwargs):
134143
self.resource_version = kwargs['resource_version']
135144

136145
timeouts = ('timeout_seconds' in kwargs)
146+
retry_after_410 = False
137147
while True:
138148
resp = func(*args, **kwargs)
139149
try:
140150
for line in iter_resp_lines(resp):
141151
# unmarshal when we are receiving events from watch,
142152
# return raw string when we are streaming log
143153
if watch_arg == "watch":
144-
yield self.unmarshal_event(line, return_type)
154+
event = self.unmarshal_event(line, return_type)
155+
if isinstance(event, dict) \
156+
and event['type'] == 'ERROR':
157+
obj = event['raw_object']
158+
# Current request expired, let's retry,
159+
# but only if we have not already retried.
160+
if not retry_after_410 and \
161+
obj['code'] == http.HTTPStatus.GONE:
162+
retry_after_410 = True
163+
break
164+
else:
165+
reason = "%s: %s" % (obj['reason'], obj['message'])
166+
raise client.rest.ApiException(status=obj['code'],
167+
reason=reason)
168+
else:
169+
retry_after_410 = False
170+
yield event
145171
else:
146172
yield line
147173
if self._stop:

Diff for: watch/watch_test.py

+27
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
from mock import Mock, call
1818

19+
from kubernetes import client
20+
1921
from .watch import Watch
2022

2123

@@ -273,6 +275,31 @@ def test_watch_with_exception(self):
273275
fake_resp.close.assert_called_once()
274276
fake_resp.release_conn.assert_called_once()
275277

278+
def test_watch_with_error_event(self):
279+
fake_resp = Mock()
280+
fake_resp.close = Mock()
281+
fake_resp.release_conn = Mock()
282+
fake_resp.read_chunked = Mock(
283+
return_value=[
284+
'{"type": "ERROR", "object": {"code": 410, '
285+
'"reason": "Gone", "message": "error message"}}\n'])
286+
287+
fake_api = Mock()
288+
fake_api.get_thing = Mock(return_value=fake_resp)
289+
290+
w = Watch()
291+
try:
292+
for _ in w.stream(fake_api.get_thing):
293+
self.fail(self, "Should fail with ApiException.")
294+
except client.rest.ApiException:
295+
pass
296+
297+
fake_api.get_thing.assert_called_once_with(
298+
_preload_content=False, watch=True)
299+
fake_resp.read_chunked.assert_called_once_with(decode_content=False)
300+
fake_resp.close.assert_called_once()
301+
fake_resp.release_conn.assert_called_once()
302+
276303

277304
if __name__ == '__main__':
278305
unittest.main()

0 commit comments

Comments
 (0)