Repository: iterative/dvc

Issue:
push: freezes for a long time without write access to gdrive remote [qa]
```console
λ dvc version
DVC version: 0.91.2
Python version: 3.7.5
Platform: Windows-10-10.0.18362-SP0
Binary: True
Package: exe
Supported remotes: azure, gdrive, gs, hdfs, http, https, s3, ssh, oss
Cache: reflink - not supported, hardlink - supported, symlink - not supported
Filesystem type (cache directory): ('NTFS', 'C:\\')
Repo: dvc (no_scm)
Filesystem type (workspace): ('NTFS', 'C:\\')

λ dvc push -r sharedfolder  # FREEZES ON ENTER FOR A WHILE
ERROR: failed to upload '.dvc\cache\61\37cde4893c59f76f005a8123d8e8e6' 
to 'gdrive://1sNSAH4rmBqq8S7yUwZCx0wDYvFxVjH_b/61/37cde4893c59f76f005a8123d8e8e6' - 
<HttpError 403 when requesting https://www.googleapis.com/drive/v2/files?supportsAllDrives=true&alt=json returned "Insufficient permissions for the specified parent.">
```

took like a minute for the error message to happen and it didn't let me interrupt it with Ctrl+C. Can someone try from MacOS/Linux?

Also the error message could be improved.

Cc @maxhora. Thanks!


Discussion:
This is a good one and should be fixed. I expect a long of similar one because of the way retries are organized. Will see if we can improve those somehow - be less aggressive.
So I guess this may not be specific to gdrive only but any remote that requires access? I think that the server/api will probably return a 403 unauthorized code in most cases so you could stop retrying there.
@jorgeorpinel no, other remotes are not affected by this problem. it takes 1 minute because we retry 16 times before giving up. We don't have any retries logic in other remotes yet.

Resolving patch:
diff --git a/dvc/remote/gdrive.py b/dvc/remote/gdrive.py
--- a/dvc/remote/gdrive.py
+++ b/dvc/remote/gdrive.py
@@ -38,6 +38,17 @@ def __init__(self, path):
         )
 
 
+def _extract(exc, field):
+    from pydrive2.files import ApiRequestError
+
+    assert isinstance(exc, ApiRequestError)
+
+    # https://cloud.google.com/storage/docs/json_api/v1/status-codes#errorformat
+    return (
+        exc.error["errors"][0].get(field, "") if "errors" in exc.error else ""
+    )
+
+
 def _gdrive_retry(func):
     def should_retry(exc):
         from pydrive2.files import ApiRequestError
@@ -45,10 +56,19 @@ def should_retry(exc):
         if not isinstance(exc, ApiRequestError):
             return False
 
-        retry_codes = [403, 500, 502, 503, 504]
-        result = exc.error.get("code", 0) in retry_codes
+        error_code = exc.error.get("code", 0)
+        result = False
+        if 500 <= error_code < 600:
+            result = True
+
+        if error_code == 403:
+            result = _extract(exc, "reason") in [
+                "userRateLimitExceeded",
+                "rateLimitExceeded",
+            ]
         if result:
             logger.debug("Retrying GDrive API call, error: {}.".format(exc))
+
         return result
 
     # 16 tries, start at 0.5s, multiply by golden ratio, cap at 20s
@@ -59,19 +79,6 @@ def should_retry(exc):
     )(func)
 
 
-def _location(exc):
-    from pydrive2.files import ApiRequestError
-
-    assert isinstance(exc, ApiRequestError)
-
-    # https://cloud.google.com/storage/docs/json_api/v1/status-codes#errorformat
-    return (
-        exc.error["errors"][0].get("location", "")
-        if exc.error.get("errors", [])
-        else ""
-    )
-
-
 class GDriveURLInfo(CloudURLInfo):
     def __init__(self, url):
         super().__init__(url)
@@ -339,7 +346,7 @@ def _gdrive_delete_file(self, item_id):
             if (
                 http_error_code == 403
                 and self._list_params["corpora"] == "drive"
-                and _location(exc) == "file.permissions"
+                and _extract(exc, "location") == "file.permissions"
             ):
                 raise DvcException(
                     "Insufficient permissions to {}. You should have {} "


Test patch: