From 7d271c3f221d5fade656cd94d2a56fab0fc3b928 Mon Sep 17 00:00:00 2001
From: willyn <willyn@google.com>
Date: Thu, 18 Feb 2021 17:39:14 +0000
Subject: [PATCH 1/9] Update dsub version to 0.4.5.dev0

PiperOrigin-RevId: 358198209
---
 dsub/_dsub_version.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dsub/_dsub_version.py b/dsub/_dsub_version.py
index c1c51da..118272f 100644
--- a/dsub/_dsub_version.py
+++ b/dsub/_dsub_version.py
@@ -26,4 +26,4 @@
   0.1.3.dev0 -> 0.1.3 -> 0.1.4.dev0 -> ...
 """
 
-DSUB_VERSION = '0.4.4'
+DSUB_VERSION = '0.4.5.dev0'

From f2a0a040ac250a7d85a363b604ed26fda7972015 Mon Sep 17 00:00:00 2001
From: cym <cym@google.com>
Date: Thu, 18 Feb 2021 20:50:18 +0000
Subject: [PATCH 2/9] Quiet a warning about 'oauth2client' (which dsub no
 longer uses).

PiperOrigin-RevId: 358242992
---
 dsub/lib/dsub_util.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/dsub/lib/dsub_util.py b/dsub/lib/dsub_util.py
index 996ce7e..dd29e0f 100644
--- a/dsub/lib/dsub_util.py
+++ b/dsub/lib/dsub_util.py
@@ -140,8 +140,10 @@ def get_storage_service(credentials):
       'ignore', 'Your application has authenticated using end user credentials')
   if credentials is None:
     credentials, _ = google.auth.default()
+  # Set cache_discovery to False because we use google-auth
+  # See https://github.com/googleapis/google-api-python-client/issues/299
   return googleapiclient.discovery.build(
-      'storage', 'v1', credentials=credentials)
+      'storage', 'v1', credentials=credentials, cache_discovery=False)
 
 
 # Exponential backoff retrying downloads of GCS object chunks.

From 9ead9bb2251f162ccf337575c234c5dd2218a2b4 Mon Sep 17 00:00:00 2001
From: willyn <willyn@google.com>
Date: Thu, 18 Feb 2021 21:23:46 +0000
Subject: [PATCH 3/9] Update Travis Python3 version from 3.7 to 3.8

PiperOrigin-RevId: 358250178
---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index 16a497f..7f1b3ba 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
 language: python
 python:
-  - "3.7"
+  - "3.8"
 # command to install dependencies
 install: "python setup.py install"
 # command to run tests

From 8e3c5d74cccbc3adc85d16b3cdb2ede221df9e71 Mon Sep 17 00:00:00 2001
From: cym <cym@google.com>
Date: Fri, 26 Feb 2021 20:55:34 +0000
Subject: [PATCH 4/9] Fix one other instance of cache_discovery=True raising
 ImportError.

PiperOrigin-RevId: 359819958
---
 dsub/providers/google_base.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/dsub/providers/google_base.py b/dsub/providers/google_base.py
index 32ef403..1423e75 100644
--- a/dsub/providers/google_base.py
+++ b/dsub/providers/google_base.py
@@ -449,8 +449,10 @@ def setup_service(api_name, api_version, credentials=None):
       'ignore', 'Your application has authenticated using end user credentials')
   if not credentials:
     credentials, _ = google.auth.default()
+  # Set cache_discovery to False because we use google-auth
+  # See https://github.com/googleapis/google-api-python-client/issues/299
   return googleapiclient.discovery.build(
-      api_name, api_version, credentials=credentials)
+      api_name, api_version, cache_discovery=False, credentials=credentials)
 
 
 def credentials_from_service_account_info(credentials_file):

From c03d4c6412ac4c500ed07cfe2c10eb332c9155fe Mon Sep 17 00:00:00 2001
From: cym <cym@google.com>
Date: Wed, 24 Mar 2021 01:15:34 +0000
Subject: [PATCH 5/9] Add `flush` method to _Printer.

PiperOrigin-RevId: 364691499
---
 dsub/lib/dsub_util.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/dsub/lib/dsub_util.py b/dsub/lib/dsub_util.py
index dd29e0f..5e4aac8 100644
--- a/dsub/lib/dsub_util.py
+++ b/dsub/lib/dsub_util.py
@@ -56,6 +56,9 @@ def __init__(self, fileobj):
   def write(self, buf):
     self._fileobj.write(buf)
 
+  def flush(self):
+    self._fileobj.flush()
+
 
 @contextlib.contextmanager
 def replace_print(fileobj=sys.stderr):

From 154ec6101b46c75d67f3ea3b3d3384285006ffb9 Mon Sep 17 00:00:00 2001
From: willyn <willyn@google.com>
Date: Wed, 18 Aug 2021 19:59:08 +0000
Subject: [PATCH 6/9] setup.py: Update dsub dependent libraries to pick up
 newer versions.

PiperOrigin-RevId: 391591605
---
 setup.py                          | 20 ++++++++------------
 test/integration/script_python.py |  2 +-
 test/unit/batch_handling_test.py  |  8 +++++++-
 3 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/setup.py b/setup.py
index deb0bc8..f421f52 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,6 @@
 """
 
 import os
-import sys
 import unittest
 # Always prefer setuptools over distutils
 from setuptools import find_packages
@@ -15,22 +14,22 @@
     # dependencies for dsub, ddel, dstat
     # Pin to known working versions to prevent episodic breakage from library
     # version mismatches.
-    # This version list generated: 02/01/2021
+    # This version list generated: 08/18/2021
 
     # direct dependencies
-    'google-api-python-client<=1.12.8',
-    'google-auth<=1.24.0',
-    'python-dateutil<=2.8.1',
+    'google-api-python-client<=2.17.0',
+    'google-auth<=1.35.0',
+    'python-dateutil<=2.8.2',
     'pytz<=2021.1',
     'pyyaml<=5.4.1',
     'tenacity<=5.0.4',
-    'tabulate<=0.8.7',
+    'tabulate<=0.8.9',
 
     # downstream dependencies
     'funcsigs<=1.0.2',
-    'google-api-core<=1.25.1',
-    'google-auth-httplib2<=0.0.4',
-    'httplib2<=0.19.0',
+    'google-api-core<=1.31.2',
+    'google-auth-httplib2<=0.1.0',
+    'httplib2<=0.19.1',
     'pyasn1<=0.4.8',
     'pyasn1-modules<=0.2.8',
     'rsa<=4.7',
@@ -41,9 +40,6 @@
     'mock<=4.0.3',
 ]
 
-if sys.version_info[0] == 2:
-  _DEPENDENCIES.append('cachetools==3.1.1')
-
 
 def unittest_suite():
   """Get test suite (Python unit tests only)."""
diff --git a/test/integration/script_python.py b/test/integration/script_python.py
index 164f3bc..fff5042 100755
--- a/test/integration/script_python.py
+++ b/test/integration/script_python.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 """Minimal Python script."""
 
 from __future__ import print_function
diff --git a/test/unit/batch_handling_test.py b/test/unit/batch_handling_test.py
index 4aab5bf..ad9b5cc 100644
--- a/test/unit/batch_handling_test.py
+++ b/test/unit/batch_handling_test.py
@@ -23,13 +23,19 @@ def callback_mock(request_id, response, exception):
   raise exception
 
 
+class ResponseMock(object):
+
+  def __init__(self):
+    self.reason = None
+
+
 class CancelMock(object):
 
   def __init__(self):
     pass
 
   def execute(self):
-    raise apiclient.errors.HttpError(None, b'test_exception')
+    raise apiclient.errors.HttpError(ResponseMock(), b'test_exception')
 
 
 class TestBatchHandling(unittest.TestCase):

From 025e83def7e46477775c6ba1b3f56680d8e35444 Mon Sep 17 00:00:00 2001
From: willyn <willyn@google.com>
Date: Fri, 20 Aug 2021 00:00:22 +0000
Subject: [PATCH 7/9] Run pytype on dsub, and fix type errors

PiperOrigin-RevId: 391876236
---
 dsub/commands/dsub.py            | 15 ++++++++-------
 dsub/lib/job_model.py            |  2 +-
 dsub/lib/output_formatter.py     |  2 +-
 dsub/lib/retry_util.py           |  4 ++--
 dsub/providers/base.py           |  2 +-
 dsub/providers/google_base.py    |  2 +-
 dsub/providers/google_v2_base.py |  2 +-
 dsub/providers/provider_base.py  |  2 +-
 8 files changed, 16 insertions(+), 15 deletions(-)

diff --git a/dsub/commands/dsub.py b/dsub/commands/dsub.py
index 23b24de..005054b 100644
--- a/dsub/commands/dsub.py
+++ b/dsub/commands/dsub.py
@@ -25,15 +25,14 @@
 import sys
 import time
 import uuid
-from dateutil.tz import tzlocal
 
+import dateutil
 from ..lib import dsub_errors
 from ..lib import dsub_util
 from ..lib import job_model
 from ..lib import output_formatter
 from ..lib import param_util
 from ..lib import resources
-from ..lib.dsub_util import print_error
 from ..providers import google_base
 from ..providers import provider_base
 
@@ -660,7 +659,8 @@ def _get_job_metadata(provider, user_id, job_name, script, task_ids,
   Returns:
     A dictionary of job-specific metadata (such as job id, name, etc.)
   """
-  create_time = dsub_util.replace_timezone(datetime.datetime.now(), tzlocal())
+  create_time = dsub_util.replace_timezone(datetime.datetime.now(),
+                                           dateutil.tz.tzlocal())
   user_id = user_id or dsub_util.get_os_user()
   job_metadata = provider.prepare_job_metadata(script.name, job_name, user_id)
   if unique_job_id:
@@ -811,7 +811,7 @@ def _wait_after(provider, job_ids, poll_interval, stop_on_failure, summary):
       jobs_not_found = jobs_completed.difference(jobs_found)
       for j in jobs_not_found:
         error = '%s: not found' % j
-        print_error('  %s' % error)
+        dsub_util.print_error('  %s' % error)
         error_messages += [error]
 
     # Print the dominant task for the completed jobs
@@ -997,7 +997,8 @@ def _importance_of_task(task):
   return (importance[task.get_field('task-status')],
           task.get_field(
               'end-time',
-              dsub_util.replace_timezone(datetime.datetime.max, tzlocal())))
+              dsub_util.replace_timezone(datetime.datetime.max,
+                                         dateutil.tz.tzlocal())))
 
 
 def _wait_for_any_job(provider, job_ids, poll_interval, summary):
@@ -1289,7 +1290,7 @@ def run(provider,
                                    summary)
       if error_messages:
         for msg in error_messages:
-          print_error(msg)
+          dsub_util.print_error(msg)
         raise dsub_errors.PredecessorJobFailureError(
             'One or more predecessor jobs completed but did not succeed.',
             error_messages, None)
@@ -1331,7 +1332,7 @@ def run(provider,
                                    poll_interval, False, summary)
     if error_messages:
       for msg in error_messages:
-        print_error(msg)
+        dsub_util.print_error(msg)
       raise dsub_errors.JobExecutionError(
           'One or more jobs finished with status FAILURE or CANCELED'
           ' during wait.', error_messages, launched_job)
diff --git a/dsub/lib/job_model.py b/dsub/lib/job_model.py
index c262248..1438e51 100644
--- a/dsub/lib/job_model.py
+++ b/dsub/lib/job_model.py
@@ -595,7 +595,7 @@ def get_complete_descriptor(cls, task_metadata, task_params, task_resources):
     return task_descriptor
 
   def __str__(self):
-    return 'task-id: {}'.format(self.job_metadata.get('task-id'))
+    return 'task-id: {}'.format(self.task_metadata.get('task-id'))
 
   def __repr__(self):
     return ('task_metadata: {}, task_params: {}').format(
diff --git a/dsub/lib/output_formatter.py b/dsub/lib/output_formatter.py
index 946fb0a..80522a6 100644
--- a/dsub/lib/output_formatter.py
+++ b/dsub/lib/output_formatter.py
@@ -271,7 +271,7 @@ def prepare_summary_table(rows):
   # Use the original table as the driver in order to preserve the order.
   new_rows = []
   for job_key in sorted(grouped.keys()):
-    group = grouped.get(job_key, None)
+    group = grouped[job_key]
     canonical_status = ['RUNNING', 'SUCCESS', 'FAILURE', 'CANCEL']
     # Written this way to ensure that if somehow a new status is introduced,
     # it shows up in our output.
diff --git a/dsub/lib/retry_util.py b/dsub/lib/retry_util.py
index ff33bad..70baaa7 100644
--- a/dsub/lib/retry_util.py
+++ b/dsub/lib/retry_util.py
@@ -21,7 +21,7 @@
 import sys
 
 import googleapiclient.errors
-from httplib2 import ServerNotFoundError
+import httplib2
 import tenacity
 
 import google.auth
@@ -127,7 +127,7 @@ def retry_api_check(retry_state: tenacity.RetryCallState) -> bool:
 
   # This has been observed as a transient error:
   #   ServerNotFoundError: Unable to find the server at genomics.googleapis.com
-  if isinstance(exception, ServerNotFoundError):
+  if isinstance(exception, httplib2.ServerNotFoundError):
     _print_retry_error(attempt_number, MAX_API_ATTEMPTS, exception)
     return True
 
diff --git a/dsub/providers/base.py b/dsub/providers/base.py
index 3fa967b..e4872ea 100644
--- a/dsub/providers/base.py
+++ b/dsub/providers/base.py
@@ -184,7 +184,7 @@ def get_tasks_completion_messages(self, tasks):
     raise NotImplementedError()
 
 
-class Task(object):
+class Task(object, metaclass=abc.ABCMeta):
   """Basic container for task metadata."""
 
   @abc.abstractmethod
diff --git a/dsub/providers/google_base.py b/dsub/providers/google_base.py
index 1423e75..a47ed3d 100644
--- a/dsub/providers/google_base.py
+++ b/dsub/providers/google_base.py
@@ -300,7 +300,7 @@ def parse_rfc3339_utc_string(rfc3339_utc_string):
     # When nanoseconds are provided, we round
     micros = int(round(int(fraction) // 1000))
   else:
-    assert False, 'Fraction length not 0, 6, or 9: {}'.len(fraction)
+    assert False, 'Fraction length not 0, 6, or 9: {}'.format(len(fraction))
 
   try:
     return datetime.datetime(
diff --git a/dsub/providers/google_v2_base.py b/dsub/providers/google_v2_base.py
index d5fe1ad..b56fb86 100644
--- a/dsub/providers/google_v2_base.py
+++ b/dsub/providers/google_v2_base.py
@@ -409,7 +409,7 @@ def get_filtered_normalized_events(self):
           continue
 
       if name == 'pulling-image':
-        if match.group(1) != user_image:
+        if match and match.group(1) != user_image:
           continue
 
       events[name] = mapped
diff --git a/dsub/providers/provider_base.py b/dsub/providers/provider_base.py
index e2bbce5..7d791c8 100644
--- a/dsub/providers/provider_base.py
+++ b/dsub/providers/provider_base.py
@@ -110,7 +110,7 @@ def parse_args(parser, provider_required_args, argv):
 
   # For the selected provider, check the required arguments
   for arg in provider_required_args[args.provider]:
-    if not args.__getattribute__(arg):
+    if not vars(args)[arg]:
       parser.error('argument --%s is required' % arg)
 
   return args

From 56e1f61853095c44348e6663d3b09f62af074da1 Mon Sep 17 00:00:00 2001
From: willyn <willyn@google.com>
Date: Fri, 20 Aug 2021 18:33:27 +0000
Subject: [PATCH 8/9] Update tenacity version

PiperOrigin-RevId: 392033960
---
 dsub/lib/dsub_util.py         |  21 +++--
 dsub/providers/google_base.py |   8 +-
 setup.py                      |   2 +-
 test/unit/retrying_test.py    | 158 +++++++++++++++++-----------------
 4 files changed, 96 insertions(+), 93 deletions(-)

diff --git a/dsub/lib/dsub_util.py b/dsub/lib/dsub_util.py
index 5e4aac8..e724c5d 100644
--- a/dsub/lib/dsub_util.py
+++ b/dsub/lib/dsub_util.py
@@ -21,15 +21,14 @@
 import pwd
 import sys
 import warnings
-from . import retry_util
 
+from . import retry_util
+import google.auth
 import googleapiclient.discovery
 import googleapiclient.errors
 import googleapiclient.http
 import tenacity
 
-import google.auth
-
 
 # this is the Job ID for jobs that are skipped.
 NO_JOB = 'NO_JOB'
@@ -154,14 +153,14 @@ def get_storage_service(credentials):
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_API_ATTEMPTS),
     retry=retry_util.retry_api_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=64),
+    wait=tenacity.wait_exponential(multiplier=1, max=64),
     retry_error_callback=retry_util.on_give_up)
 # For API errors dealing with auth, we want to retry, but not as often
 # Maximum 4 retries. Wait 1, 2, 4, 8 seconds.
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_AUTH_ATTEMPTS),
     retry=retry_util.retry_auth_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=8),
+    wait=tenacity.wait_exponential(multiplier=1, max=8),
     retry_error_callback=retry_util.on_give_up)
 def _downloader_next_chunk(downloader):
   """Downloads the next chunk."""
@@ -219,14 +218,14 @@ def load_file(file_path, credentials=None):
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_API_ATTEMPTS),
     retry=retry_util.retry_api_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=64),
+    wait=tenacity.wait_exponential(multiplier=1, max=64),
     retry_error_callback=retry_util.on_give_up)
 # For API errors dealing with auth, we want to retry, but not as often
 # Maximum 4 retries. Wait 1, 2, 4, 8 seconds.
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_AUTH_ATTEMPTS),
     retry=retry_util.retry_auth_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=8),
+    wait=tenacity.wait_exponential(multiplier=1, max=8),
     retry_error_callback=retry_util.on_give_up)
 def _file_exists_in_gcs(gcs_file_path, credentials=None, storage_service=None):
   """Check whether the file exists, in GCS.
@@ -257,14 +256,14 @@ def _file_exists_in_gcs(gcs_file_path, credentials=None, storage_service=None):
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_API_ATTEMPTS),
     retry=retry_util.retry_api_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=64),
+    wait=tenacity.wait_exponential(multiplier=1, max=64),
     retry_error_callback=retry_util.on_give_up)
 # For API errors dealing with auth, we want to retry, but not as often
 # Maximum 4 retries. Wait 1, 2, 4, 8 seconds.
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_AUTH_ATTEMPTS),
     retry=retry_util.retry_auth_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=8),
+    wait=tenacity.wait_exponential(multiplier=1, max=8),
     retry_error_callback=retry_util.on_give_up)
 def _prefix_exists_in_gcs(gcs_prefix, credentials=None, storage_service=None):
   """Check whether there is a GCS object whose name starts with the prefix.
@@ -307,14 +306,14 @@ def folder_exists(folder_path, credentials=None, storage_service=None):
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_API_ATTEMPTS),
     retry=retry_util.retry_api_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=64),
+    wait=tenacity.wait_exponential(multiplier=1, max=64),
     retry_error_callback=retry_util.on_give_up)
 # For API errors dealing with auth, we want to retry, but not as often
 # Maximum 4 retries. Wait 1, 2, 4, 8 seconds.
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_AUTH_ATTEMPTS),
     retry=retry_util.retry_auth_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=8),
+    wait=tenacity.wait_exponential(multiplier=1, max=8),
     retry_error_callback=retry_util.on_give_up)
 def simple_pattern_exists_in_gcs(file_pattern,
                                  credentials=None,
diff --git a/dsub/providers/google_base.py b/dsub/providers/google_base.py
index a47ed3d..a1fe773 100644
--- a/dsub/providers/google_base.py
+++ b/dsub/providers/google_base.py
@@ -424,14 +424,14 @@ def cancel(batch_fn, cancel_fn, ops):
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_API_ATTEMPTS),
     retry=retry_util.retry_api_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=64),
+    wait=tenacity.wait_exponential(multiplier=1, max=64),
     retry_error_callback=retry_util.on_give_up)
 # For API errors dealing with auth, we want to retry, but not as often
 # Maximum 4 retries. Wait 1, 2, 4, 8 seconds.
 @tenacity.retry(
     stop=tenacity.stop_after_attempt(retry_util.MAX_AUTH_ATTEMPTS),
     retry=retry_util.retry_auth_check,
-    wait=tenacity.wait_exponential(multiplier=0.5, max=8),
+    wait=tenacity.wait_exponential(multiplier=1, max=8),
     retry_error_callback=retry_util.on_give_up)
 def setup_service(api_name, api_version, credentials=None):
   """Configures genomics API client.
@@ -469,14 +469,14 @@ class Api(object):
   @tenacity.retry(
       stop=tenacity.stop_after_attempt(retry_util.MAX_API_ATTEMPTS),
       retry=retry_util.retry_api_check,
-      wait=tenacity.wait_exponential(multiplier=0.5, max=64),
+      wait=tenacity.wait_exponential(multiplier=1, max=64),
       retry_error_callback=retry_util.on_give_up)
   # For API errors dealing with auth, we want to retry, but not as often
   # Maximum 4 retries. Wait 1, 2, 4, 8 seconds.
   @tenacity.retry(
       stop=tenacity.stop_after_attempt(retry_util.MAX_AUTH_ATTEMPTS),
       retry=retry_util.retry_auth_check,
-      wait=tenacity.wait_exponential(multiplier=0.5, max=8),
+      wait=tenacity.wait_exponential(multiplier=1, max=8),
       retry_error_callback=retry_util.on_give_up)
   def execute(self, api):
     """Executes operation.
diff --git a/setup.py b/setup.py
index f421f52..bbe8874 100644
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,7 @@
     'python-dateutil<=2.8.2',
     'pytz<=2021.1',
     'pyyaml<=5.4.1',
-    'tenacity<=5.0.4',
+    'tenacity<=7.0.0',
     'tabulate<=0.8.9',
 
     # downstream dependencies
diff --git a/test/unit/retrying_test.py b/test/unit/retrying_test.py
index a652397..8d87682 100644
--- a/test/unit/retrying_test.py
+++ b/test/unit/retrying_test.py
@@ -15,19 +15,25 @@
 
 import errno
 import socket
-import time
 import unittest
 
 import apiclient.errors
 from dsub.lib import retry_util
 from dsub.providers import google_base
+import fake_time
+import google.auth
+from mock import patch
 import parameterized
 
-import google.auth
 
+def chronology():
+  # Simulates the passing of time for fake_time.
+  while True:
+    yield 1  # Simulates 1 second passing.
 
-def current_time_ms():
-  return int(round(time.time() * 1000))
+
+def elapsed_time_in_seconds(fake_time_object):
+  return int(round(fake_time_object.now()))
 
 
 class GoogleApiMock(object):
@@ -56,41 +62,38 @@ def __init__(self, status, reason):
 class TestRetrying(unittest.TestCase):
 
   def test_success(self):
-    exception_list = []
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      exception_list = []
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
 
-    start = current_time_ms()
-    api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
+      api_wrapper_to_test.execute(mock_api_object)
 
-    # No expected retries, and no expected wait time
-    self.assertEqual(mock_api_object.retry_counter, 0)
-    self.assertLess(end - start, 1000)
+      # No expected retries, and no expected wait time
+      self.assertEqual(mock_api_object.retry_counter, 0)
+      self.assertLess(elapsed_time_in_seconds(ft), 1)
 
   @parameterized.parameterized.expand(
-      [(error_code,)
-       for error_code in list(retry_util.TRANSIENT_HTTP_ERROR_CODES) +
-       list(retry_util.HTTP_AUTH_ERROR_CODES)] +
       [(error_code,)
        for error_code in list(retry_util.TRANSIENT_HTTP_ERROR_CODES) +
        list(retry_util.HTTP_AUTH_ERROR_CODES)])
   def test_retry_once(self, error_code):
-    exception_list = [
-        apiclient.errors.HttpError(
-            ResponseMock(error_code, None), b'test_exception'),
-    ]
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      exception_list = [
+          apiclient.errors.HttpError(
+              ResponseMock(error_code, None), b'test_exception'),
+      ]
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
 
-    start = current_time_ms()
-    api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
+      api_wrapper_to_test.execute(mock_api_object)
 
-    # Expected to retry once, for about 1 second
-    self.assertEqual(mock_api_object.retry_counter, 1)
-    self.assertGreaterEqual(end - start, 1000)
-    self.assertLess(end - start, 1500)
+      # Expected to retry once, for about 1 second
+      self.assertEqual(mock_api_object.retry_counter, 1)
+      self.assertGreaterEqual(elapsed_time_in_seconds(ft), 1)
+      self.assertLess(elapsed_time_in_seconds(ft), 1.5)
 
   def test_auth_failure(self):
     exception_list = [
@@ -100,32 +103,32 @@ def test_auth_failure(self):
         apiclient.errors.HttpError(ResponseMock(401, None), b'test_exception'),
         apiclient.errors.HttpError(ResponseMock(403, None), b'test_exception'),
     ]
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
-    # We don't want to retry auth errors as aggressively, so we expect
-    # this exception to be raised after 4 retries,
-    # for a total of 1 + 2 + 4 + 8 = 15 seconds
-    start = current_time_ms()
-    with self.assertRaises(apiclient.errors.HttpError):
-      api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
-    self.assertGreaterEqual(end - start, 15000)
-    self.assertLess(end - start, 15500)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
+      # We don't want to retry auth errors as aggressively, so we expect
+      # this exception to be raised after 4 retries,
+      # for a total of 1 + 2 + 4 + 8 = 15 seconds
+      with self.assertRaises(apiclient.errors.HttpError):
+        api_wrapper_to_test.execute(mock_api_object)
+      self.assertGreaterEqual(elapsed_time_in_seconds(ft), 15)
+      self.assertLess(elapsed_time_in_seconds(ft), 15.5)
 
   def test_transient_retries(self):
     exception_list = [
         apiclient.errors.HttpError(ResponseMock(500, None), b'test_exception'),
         apiclient.errors.HttpError(ResponseMock(503, None), b'test_exception'),
     ]
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
-    start = current_time_ms()
-    api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
-    # Expected to retry twice, for a total of 1 + 2 = 3 seconds
-    self.assertEqual(mock_api_object.retry_counter, 2)
-    self.assertGreaterEqual(end - start, 3000)
-    self.assertLess(end - start, 3500)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
+      api_wrapper_to_test.execute(mock_api_object)
+      # Expected to retry twice, for a total of 1 + 2 = 3 seconds
+      self.assertEqual(mock_api_object.retry_counter, 2)
+      self.assertGreaterEqual(elapsed_time_in_seconds(ft), 3)
+      self.assertLess(elapsed_time_in_seconds(ft), 3.5)
 
   def test_broken_pipe_retries(self):
     # To construct a BrokenPipeError, we pass errno.EPIPE (32) to OSError
@@ -134,30 +137,30 @@ def test_broken_pipe_retries(self):
         OSError(errno.EPIPE, 'broken pipe exception test'),
         OSError(errno.EPIPE, 'broken pipe exception test'),
     ]
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
-    start = current_time_ms()
-    api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
-    # Expected to retry twice, for a total of 1 + 2 = 3 seconds
-    self.assertEqual(mock_api_object.retry_counter, 2)
-    self.assertGreaterEqual(end - start, 3000)
-    self.assertLess(end - start, 3500)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
+      api_wrapper_to_test.execute(mock_api_object)
+      # Expected to retry twice, for a total of 1 + 2 = 3 seconds
+      self.assertEqual(mock_api_object.retry_counter, 2)
+      self.assertGreaterEqual(elapsed_time_in_seconds(ft), 3)
+      self.assertLess(elapsed_time_in_seconds(ft), 3.5)
 
   def test_socket_timeout(self):
     exception_list = [
         socket.timeout(),
         socket.timeout(),
     ]
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
-    start = current_time_ms()
-    api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
-    # Expected to retry twice, for a total of 1 + 2 = 3 seconds
-    self.assertEqual(mock_api_object.retry_counter, 2)
-    self.assertGreaterEqual(end - start, 3000)
-    self.assertLess(end - start, 3500)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
+      api_wrapper_to_test.execute(mock_api_object)
+      # Expected to retry twice, for a total of 1 + 2 = 3 seconds
+      self.assertEqual(mock_api_object.retry_counter, 2)
+      self.assertGreaterEqual(elapsed_time_in_seconds(ft), 3)
+      self.assertLess(elapsed_time_in_seconds(ft), 3.5)
 
   @parameterized.parameterized.expand([
       (apiclient.errors.HttpError(ResponseMock(500, None), b'test_exception'),
@@ -178,16 +181,17 @@ def test_retry_then_succeed(self):
         apiclient.errors.HttpError(ResponseMock(500, None), b'test_exception'),
         apiclient.errors.HttpError(ResponseMock(500, None), b'test_exception'),
     ]
-    api_wrapper_to_test = google_base.Api()
-    mock_api_object = GoogleApiMock(exception_list)
-    start = current_time_ms()
-    api_wrapper_to_test.execute(mock_api_object)
-    end = current_time_ms()
-    # Expected to retry 5 times, for a total of 1 + 2 + 4 + 8 + 16 = 31 seconds
-    # At the end we expect a recovery message to be emitted.
-    self.assertEqual(mock_api_object.retry_counter, 5)
-    self.assertGreaterEqual(end - start, 31000)
-    self.assertLess(end - start, 31500)
+    ft = fake_time.FakeTime(chronology())
+    with patch('time.sleep', new=ft.sleep):
+      api_wrapper_to_test = google_base.Api()
+      mock_api_object = GoogleApiMock(exception_list)
+      api_wrapper_to_test.execute(mock_api_object)
+      # Expected to retry 5 times,
+      # for a total of 1 + 2 + 4 + 8 + 16 = 31 seconds
+      # At the end we expect a recovery message to be emitted.
+      self.assertEqual(mock_api_object.retry_counter, 5)
+      self.assertGreaterEqual(elapsed_time_in_seconds(ft), 31)
+      self.assertLess(elapsed_time_in_seconds(ft), 31.5)
 
 
 if __name__ == '__main__':

From ad1c654422413b90c78614f66af61c1410cc1a77 Mon Sep 17 00:00:00 2001
From: willyn <willyn@google.com>
Date: Thu, 26 Aug 2021 17:32:25 +0000
Subject: [PATCH 9/9] Update dsub version to 0.4.5

PiperOrigin-RevId: 393155372
---
 dsub/_dsub_version.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dsub/_dsub_version.py b/dsub/_dsub_version.py
index 118272f..64869f1 100644
--- a/dsub/_dsub_version.py
+++ b/dsub/_dsub_version.py
@@ -26,4 +26,4 @@
   0.1.3.dev0 -> 0.1.3 -> 0.1.4.dev0 -> ...
 """
 
-DSUB_VERSION = '0.4.5.dev0'
+DSUB_VERSION = '0.4.5'