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

Watch stream should handle HTTP error before unmarshaling event #57

Closed

Description

@salilgupta1

I could be mistaken but looking at the infinite loop for the watch stream doesn't handle the case when you receive an event that is expired i.e. a HTTP status code of 410.

        while True:
            resp = func(*args, **kwargs)
            try:
                for line in iter_resp_lines(resp):
                    yield self.unmarshal_event(line, return_type)
                    if self._stop:
                        break
            finally:
                kwargs['resource_version'] = self.resource_version
                resp.close()
                resp.release_conn()

Looking at the code it seems that if the event is expired then resp should return something along the lines of

{'raw_object': {u'status': u'Failure', u'kind': u'Status', u'code': 410, u'apiVersion': u'v1', u'reason': u'Gone', u'message': u'too old resource version: 2428 (88826)', u'metadata': {}}, u'object': {'api_version': 'v1',
 'kind': 'Status',

And unmarshall_event should fail to deserialize the object and break. And self.resource_version should just be stuck on the resource_version of the event that was expired.

Am I missing something here?

Activity

  1. changed the title [-]Does the Watch stream handles expired Events[/-] [+]Does the Watch stream handle expired Events?[/+] on Apr 10, 2018
  2. Sturgelose commented on May 16, 2018

    @Sturgelose

    We are having exactly the issue the you described: our watch gets a 401, it disconnects and reconnects automatically ending in an undesired status.

    Our case is:

    1. Etcd/K8s API returns us a 401 error with a resource_version = None in its body (as you can see in the snippet in this issue: K8s Watch: How to deal with 401 Status Errors python#403). Reason of the error is that we are doing many calls to the API.
    2. Then, watch handles the event and restarts the connection. However, it saves resource_version value to the object, making it having a None value.
    3. As a result, the next request we are given a 500 error because resource_version in the request has a value of None and this is not allowed in K8s API.

    Possible solutions:

    • unmarshall_event should validate the status code before saving the state to the object, so an invalid state is not stored in the watch object.
    • After an error handling, stored values should be reseted to a default version.

    Stack trace:

    Exception in thread Thread-15:
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/threading.py", line 916, in _bootstrap_inner
        self.run()
      File "/usr/local/lib/python3.6/threading.py", line 864, in run
        self._target(*self._args, **self._kwargs)
      File "./main.py", line 109, in listen_k8s_changes
        resource_version=0):
      File "/usr/local/lib/python3.6/site-packages/kubernetes/watch/watch.py", line 122, in stream
        resp = func(*args, **kwargs)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/apis/batch_v1_api.py", line 643, in list_namespaced_job
        (data) = self.list_namespaced_job_with_http_info(namespace, **kwargs)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/apis/batch_v1_api.py", line 746, in list_namespaced_job_with_http_info
        collection_formats=collection_formats)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/api_client.py", line 321, in call_api
        _return_http_data_only, collection_formats, _preload_content, _request_timeout)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/api_client.py", line 155, in __call_api
        _request_timeout=_request_timeout)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/api_client.py", line 342, in request
        headers=headers)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/rest.py", line 231, in GET
        query_params=query_params)
      File "/usr/local/lib/python3.6/site-packages/kubernetes/client/rest.py", line 222, in request
        raise ApiException(http_resp=r)
    kubernetes.client.rest.ApiException: (500)
    Reason: Internal Server Error
    HTTP response headers: HTTPHeaderDict({'Content-Type': 'application/json', 'Date': 'Tue, 15 May 2018 15:39:15 GMT', 'Content-Length': '186'})
    HTTP response body: b'{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"resourceVersion: Invalid value: \\"None\\": strconv.ParseUint: parsing \\"None\\": invalid syntax","code":500}\n'
    

    Note that the only place where resource_version is managed inside of the watch logic is in watch.py. In the rest/http logic, this field is not touched.

    Which should be the option to fix this? I'd be willing to give a PR to fix the issue.

  3. roycaihw commented on Jun 13, 2018

    @roycaihw
    Member

    The current watch client code is dynamic and assumes that API server always responds with correct API object to deserialize. When API server responds with 410 in case where resource version is too old (e.g. kubernetes-client/python#484 (comment)) or 401 in @Sturgelose's case, the errors are not raised/handled but get "deserialized".

    I would suggest that we should check if the response is error code before deserializing, and reason about the proper behavior for different error code.

  4. changed the title [-]Does the Watch stream handle expired Events?[/-] [+]Watch stream should handle HTTP error before unmarshaling event[/+] on Jun 13, 2018
  5. added
    kind/bugCategorizes issue or PR as related to a bug.
    help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.
    on Jun 13, 2018
  6. rocky4570 commented on Sep 24, 2018

    @rocky4570

    Yeah we have the same issue with timeout from AWS ELB, resulting in "Invalid value for spec, must not be None".

    Was going to put in PR for unmarshal_event to raise TimeoutError so as to avoid too many changes affecting existing use cases:

    Line 82 or kubernetes/watch/watch.py
    if js.get('type') == 'Error':
    if js.get('object', {}).get('reason') == 'Expired':
    raise TimeoutError(js{'object']['message'])

    so that any logging is a little more clearer than potentially looking like its an issue elsewhere depending on what is being watched.

  7. added 4 commits that reference this issue on Nov 15, 2018
    18ef757
    1371f18
    c4f8562
    bd98fd4
  8. mitar commented on Feb 11, 2019

    @mitar
    Contributor

    From API concepts:

    A given Kubernetes server will only preserve a historical list of changes for a limited time. Clusters using etcd3 preserve changes in the last 5 minutes by default. When the requested watch operations fail because the historical version of that resource is not available, clients must handle the case by recognizing the status code 410 Gone, clearing their local cache, performing a list operation, and starting the watch from the resourceVersion returned by that new list operation. Most client libraries offer some form of standard tool for this logic. (In Go this is called a Reflector and is located in the k8s.io/client-go/cache package.)

    It would be great if Python code could correctly support this automatically.

  9. mitar commented on Feb 11, 2019

    @mitar
    Contributor

    Based on #102 I made an improved version:

    • It makes sure to not try to unmarshal an event if it is an error.
    • If error is 410, it tries to resume it automatically using the latest known resource_version from the latest event.
    • Only if this does not succeed (which can happen if the latest event is too old as well), then an exception is thrown.
    • Exception is thrown for any other error as well.
    • I added to docstring explanation of this behavior.

    This is almost what API concepts describe with one important difference: API concepts and go library provide a function called "list and watch" which ties listing and watching together. This package (to my knowledge) does not provide that. Because of that we have to throw an exception if the latest event is too old. Otherwise we could just retry the whole "list and watch".

    The problem with current approach (in my branch) is that if code was not able to resume automatically (because last event was too old) then just restarting watch with resource_version set to None is not good enough. It could happen that you just missed some event. In some cases this might be OK (if you do not need to capture all changes). But to my understanding, to not miss any event you should then do "list and watch" to recover. And this currently has to be done manually on the outside. We might want to provide such utility function covering all (edge) cases.

  10. zw0610 commented on Apr 10, 2019

    @zw0610

    Is there any reason not adding except clause in the while loop within stream method, as:

            while True:
                resp = func(*args, **kwargs)
                try:
                    for line in iter_resp_lines(resp):
                        yield self.unmarshal_event(line, return_type)
                        if self._stop:
                            break
                except Exception as e:
                   pass
                finally:
                    kwargs['resource_version'] = self.resource_version
                    resp.close()
                    resp.release_conn()
    
                if timeouts or self._stop:
                    break
  11. mitar commented on Apr 10, 2019

    @mitar
    Contributor

    I do not think so. The only reason why exception is thrown there is because we access wrong fields in the data object. Also, what you made as a snippet above eats all and every exception, this is definitely not something you want to do.

  12. 3 remaining items

  13. mitar commented on Sep 27, 2019

    @mitar
    Contributor

    /remove-lifecycle stale

  14. removed
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Sep 27, 2019
  15. fejta-bot commented on Dec 26, 2019

    @fejta-bot

    Issues go stale after 90d of inactivity.
    Mark the issue as fresh with /remove-lifecycle stale.
    Stale issues rot after an additional 30d of inactivity and eventually close.

    If this issue is safe to close now please do so with /close.

    Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
    /lifecycle stale

  16. added
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Dec 26, 2019
  17. mitar commented on Dec 26, 2019

    @mitar
    Contributor

    /remove-lifecycle stale

  18. removed
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Dec 26, 2019
  19. fejta-bot commented on Mar 25, 2020

    @fejta-bot

    Issues go stale after 90d of inactivity.
    Mark the issue as fresh with /remove-lifecycle stale.
    Stale issues rot after an additional 30d of inactivity and eventually close.

    If this issue is safe to close now please do so with /close.

    Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
    /lifecycle stale

  20. added
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Mar 25, 2020
  21. mitar commented on Mar 25, 2020

    @mitar
    Contributor

    /remove-lifecycle stale

  22. removed
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Mar 25, 2020
  23. fejta-bot commented on Jun 23, 2020

    @fejta-bot

    Issues go stale after 90d of inactivity.
    Mark the issue as fresh with /remove-lifecycle stale.
    Stale issues rot after an additional 30d of inactivity and eventually close.

    If this issue is safe to close now please do so with /close.

    Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
    /lifecycle stale

  24. added
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Jun 23, 2020
  25. mitar commented on Jun 23, 2020

    @mitar
    Contributor

    /remove-lifecycle stale

  26. removed
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale.
    on Jun 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.kind/bugCategorizes issue or PR as related to a bug.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions