Since key refresh is prone to failure, retry using exponential
backoff with random jitter. This adds the following sync-openpgp-*
configuration settings:

sync-openpgp-key-refresh-retry-count = 40

  Maximum number of times to retry key refresh if it fails.  Between
  each  key  refresh attempt, there is an exponential delay with a
  constant multiplier and a uniform random multiplier between 0 and 1.

sync-openpgp-key-refresh-retry-delay-exp-base = 2

  The base of the exponential expression. The exponent is the number
  of previous refresh attempts.

sync-openpgp-key-refresh-retry-delay-max = 60

  Maximum  delay between each retry attempt, in units of seconds. This
  places a limit on the length of the exponential delay.

sync-openpgp-key-refresh-retry-delay-mult = 4

  Multiplier for the exponential delay.

sync-openpgp-key-refresh-retry-overall-timeout = 1200

  Combined time limit for all refresh attempts, in units of seconds.

Bug: https://bugs.gentoo.org/649276
---
 cnf/repos.conf                          |  5 ++
 man/portage.5                           | 19 ++++++++
 pym/portage/repository/config.py        | 22 +++++++++
 pym/portage/sync/modules/rsync/rsync.py | 16 ++++++-
 pym/portage/sync/syncbase.py            | 85 ++++++++++++++++++++++++++++++++-
 5 files changed, 144 insertions(+), 3 deletions(-)

diff --git a/cnf/repos.conf b/cnf/repos.conf
index 984ecd220..5759b8b43 100644
--- a/cnf/repos.conf
+++ b/cnf/repos.conf
@@ -9,6 +9,11 @@ auto-sync = yes
 sync-rsync-verify-metamanifest = yes
 sync-rsync-verify-max-age = 24
 sync-openpgp-key-path = 
/var/lib/gentoo/gkeys/keyrings/gentoo/release/pubring.gpg
+sync-openpgp-key-refresh-retry-count = 40
+sync-openpgp-key-refresh-retry-overall-timeout = 1200
+sync-openpgp-key-refresh-retry-delay-exp-base = 2
+sync-openpgp-key-refresh-retry-delay-max = 60
+sync-openpgp-key-refresh-retry-delay-mult = 4
 
 # for daily squashfs snapshots
 #sync-type = squashdelta
diff --git a/man/portage.5 b/man/portage.5
index 549c51c73..d3e258a43 100644
--- a/man/portage.5
+++ b/man/portage.5
@@ -1081,6 +1081,25 @@ only for protocols supporting cryptographic 
verification, provided
 that the respective verification option is enabled. If unset, the user's
 keyring is used.
 .TP
+.B sync\-openpgp\-key\-refresh\-retry\-count = 40
+Maximum number of times to retry key refresh if it fails. Between each
+key refresh attempt, there is an exponential delay with a constant
+multiplier and a uniform random multiplier between 0 and 1.
+.TP
+.B sync\-openpgp\-key\-refresh\-retry\-delay\-exp\-base = 2
+The base of the exponential expression. The exponent is the number of
+previous refresh attempts.
+.TP
+.B sync\-openpgp\-key\-refresh\-retry\-delay\-max = 60
+Maximum delay between each retry attempt, in units of seconds. This
+places a limit on the length of the exponential delay.
+.TP
+.B sync\-openpgp\-key\-refresh\-retry\-delay\-mult = 4
+Multiplier for the exponential delay.
+.TP
+.B sync\-openpgp\-key\-refresh\-retry\-overall\-timeout = 1200
+Combined time limit for all refresh attempts, in units of seconds.
+.TP
 .B sync-rsync-vcs-ignore = true|false
 Ignore vcs directories that may be present in the repository. It is the
 user's responsibility to set sync-rsync-extra-opts to protect vcs
diff --git a/pym/portage/repository/config.py b/pym/portage/repository/config.py
index b5db4855f..1d897bb90 100644
--- a/pym/portage/repository/config.py
+++ b/pym/portage/repository/config.py
@@ -87,6 +87,11 @@ class RepoConfig(object):
                'update_changelog', '_eapis_banned', '_eapis_deprecated',
                '_masters_orig', 'module_specific_options', 
'manifest_required_hashes',
                'sync_openpgp_key_path',
+               'sync_openpgp_key_refresh_retry_count',
+               'sync_openpgp_key_refresh_retry_delay_max',
+               'sync_openpgp_key_refresh_retry_delay_exp_base',
+               'sync_openpgp_key_refresh_retry_delay_mult',
+               'sync_openpgp_key_refresh_retry_overall_timeout',
                )
 
        def __init__(self, name, repo_opts, local_config=True):
@@ -186,6 +191,13 @@ class RepoConfig(object):
                self.sync_openpgp_key_path = repo_opts.get(
                        'sync-openpgp-key-path', None)
 
+               for k in ('sync_openpgp_key_refresh_retry_count',
+                       'sync_openpgp_key_refresh_retry_delay_max',
+                       'sync_openpgp_key_refresh_retry_delay_exp_base',
+                       'sync_openpgp_key_refresh_retry_delay_mult',
+                       'sync_openpgp_key_refresh_retry_overall_timeout'):
+                       setattr(self, k, repo_opts.get(k.replace('_', '-'), 
None))
+
                self.module_specific_options = {}
 
                # Not implemented.
@@ -523,6 +535,11 @@ class RepoConfigLoader(object):
                                                        'force', 'masters', 
'priority', 'strict_misc_digests',
                                                        'sync_depth', 
'sync_hooks_only_on_change',
                                                        'sync_openpgp_key_path',
+                                                       
'sync_openpgp_key_refresh_retry_count',
+                                                       
'sync_openpgp_key_refresh_retry_delay_max',
+                                                       
'sync_openpgp_key_refresh_retry_delay_exp_base',
+                                                       
'sync_openpgp_key_refresh_retry_delay_mult',
+                                                       
'sync_openpgp_key_refresh_retry_overall_timeout',
                                                        'sync_type', 
'sync_umask', 'sync_uri', 'sync_user',
                                                        
'module_specific_options'):
                                                        v = 
getattr(repos_conf_opts, k, None)
@@ -946,6 +963,11 @@ class RepoConfigLoader(object):
                bool_keys = ("strict_misc_digests",)
                str_or_int_keys = ("auto_sync", "clone_depth", "format", 
"location",
                        "main_repo", "priority", "sync_depth", 
"sync_openpgp_key_path",
+                       "sync_openpgp_key_refresh_retry_count",
+                       "sync_openpgp_key_refresh_retry_delay_max",
+                       "sync_openpgp_key_refresh_retry_delay_exp_base",
+                       "sync_openpgp_key_refresh_retry_delay_mult",
+                       "sync_openpgp_key_refresh_retry_overall_timeout",
                        "sync_type", "sync_umask", "sync_uri", 'sync_user')
                str_tuple_keys = ("aliases", "eclass_overrides", "force")
                repo_config_tuple_keys = ("masters",)
diff --git a/pym/portage/sync/modules/rsync/rsync.py 
b/pym/portage/sync/modules/rsync/rsync.py
index ac841545d..763f41699 100644
--- a/pym/portage/sync/modules/rsync/rsync.py
+++ b/pym/portage/sync/modules/rsync/rsync.py
@@ -7,6 +7,7 @@ import time
 import signal
 import socket
 import datetime
+import functools
 import io
 import re
 import random
@@ -22,7 +23,9 @@ good = create_color_func("GOOD")
 bad = create_color_func("BAD")
 warn = create_color_func("WARN")
 from portage.const import VCS_DIRS, TIMESTAMP_FORMAT, RSYNC_PACKAGE_ATOM
+from portage.util._eventloop.global_event_loop import global_event_loop
 from portage.util import writemsg, writemsg_stdout
+from portage.util.futures.futures import TimeoutError
 from portage.sync.getaddrinfo_validate import getaddrinfo_validate
 from _emerge.UserQuery import UserQuery
 from portage.sync.syncbase import NewBase
@@ -139,14 +142,23 @@ class RsyncSync(NewBase):
                        # will not be performed and the user will have to fix 
it and try again,
                        # so we may as well bail out before actual rsync 
happens.
                        if openpgp_env is not None and 
self.repo.sync_openpgp_key_path is not None:
+
                                try:
                                        out.einfo('Using keys from %s' % 
(self.repo.sync_openpgp_key_path,))
                                        with 
io.open(self.repo.sync_openpgp_key_path, 'rb') as f:
                                                openpgp_env.import_key(f)
                                        out.ebegin('Refreshing keys from 
keyserver')
-                                       openpgp_env.refresh_keys()
+                                       retry_decorator = 
self._key_refresh_retry_decorator()
+                                       if retry_decorator is None:
+                                               openpgp_env.refresh_keys()
+                                       else:
+                                               loop = global_event_loop()
+                                               func_coroutine = 
functools.partial(loop.run_in_executor,
+                                                       None, 
openpgp_env.refresh_keys)
+                                               decorated_func = 
retry_decorator(func_coroutine)
+                                               
loop.run_until_complete(decorated_func())
                                        out.eend(0)
-                               except GematoException as e:
+                               except (GematoException, TimeoutError) as e:
                                        writemsg_level("!!! Manifest 
verification impossible due to keyring problem:\n%s\n"
                                                        % (e,),
                                                        level=logging.ERROR, 
noiselevel=-1)
diff --git a/pym/portage/sync/syncbase.py b/pym/portage/sync/syncbase.py
index 43b667fb0..9515dd8d8 100644
--- a/pym/portage/sync/syncbase.py
+++ b/pym/portage/sync/syncbase.py
@@ -6,12 +6,14 @@ Base class for performing sync operations.
 This class contains common initialization code and functions.
 '''
 
-
+from __future__ import unicode_literals
 import logging
 import os
 
 import portage
 from portage.util import writemsg_level
+from portage.util.backoff import RandomExponentialBackoff
+from portage.util.futures.retry import retry
 from . import _SUBMODULE_PATH_MAP
 
 class SyncBase(object):
@@ -106,6 +108,87 @@ class SyncBase(object):
                '''Get information about the head commit'''
                raise NotImplementedError
 
+       def _key_refresh_retry_decorator(self):
+               '''
+               Return a retry decorator, or None if retry is disabled.
+
+               If retry fails, the function reraises the exception raised
+               by the decorated function. If retry times out and no exception
+               is available to reraise, the function raises TimeoutError.
+               '''
+               errors = []
+
+               if self.repo.sync_openpgp_key_refresh_retry_count is None:
+                       return None
+               try:
+                       retry_count = 
int(self.repo.sync_openpgp_key_refresh_retry_count)
+               except Exception as e:
+                       errors.append('sync-openpgp-key-refresh-retry-count: 
{}'.format(e))
+               else:
+                       if retry_count <= 0:
+                               return None
+
+               if self.repo.sync_openpgp_key_refresh_retry_overall_timeout is 
None:
+                       retry_overall_timeout = None
+               else:
+                       try:
+                               retry_overall_timeout = 
float(self.repo.sync_openpgp_key_refresh_retry_overall_timeout)
+                       except Exception as e:
+                               
errors.append('sync-openpgp-key-refresh-retry-overall-timeout: {}'.format(e))
+                       else:
+                               if retry_overall_timeout < 0:
+                                       
errors.append('sync-openpgp-key-refresh-retry-overall-timeout: '
+                                               'value must be greater than or 
equal to zero: {}'.format(retry_overall_timeout))
+                               elif retry_overall_timeout == 0:
+                                       retry_overall_timeout = None
+
+               if self.repo.sync_openpgp_key_refresh_retry_delay_mult is None:
+                       retry_delay_mult = None
+               else:
+                       try:
+                               retry_delay_mult = 
float(self.repo.sync_openpgp_key_refresh_retry_delay_mult)
+                       except Exception as e:
+                               
errors.append('sync-openpgp-key-refresh-retry-delay-mult: {}'.format(e))
+                       else:
+                               if retry_delay_mult <= 0:
+                                       
errors.append('sync-openpgp-key-refresh-retry-mult: '
+                                               'value must be greater than 
zero: {}'.format(retry_delay_mult))
+
+               if self.repo.sync_openpgp_key_refresh_retry_delay_exp_base is 
None:
+                       retry_delay_exp_base = None
+               else:
+                       try:
+                               retry_delay_exp_base = 
float(self.repo.sync_openpgp_key_refresh_retry_delay_exp_base)
+                       except Exception as e:
+                               
errors.append('sync-openpgp-key-refresh-retry-delay-exp: {}'.format(e))
+                       else:
+                               if retry_delay_exp_base <= 0:
+                                       
errors.append('sync-openpgp-key-refresh-retry-delay-exp: '
+                                               'value must be greater than 
zero: {}'.format(retry_delay_mult))
+
+               if errors:
+                       lines = []
+                       lines.append('')
+                       lines.append('!!! Retry disabled for openpgp key 
refresh:')
+                       lines.append('')
+                       for msg in errors:
+                               lines.append('    {}'.format(msg))
+                       lines.append('')
+
+                       for line in lines:
+                               writemsg_level("{}\n".format(line),
+                                       level=logging.ERROR, noiselevel=-1)
+
+                       return None
+
+               return retry(
+                       reraise=True,
+                       try_max=retry_count,
+                       overall_timeout=(retry_overall_timeout if 
retry_overall_timeout > 0 else None),
+                       delay_func=RandomExponentialBackoff(
+                               multiplier=(1 if retry_delay_mult is None else 
retry_delay_mult),
+                               base=(2 if retry_delay_exp_base is None else 
retry_delay_exp_base)))
+
 
 class NewBase(SyncBase):
        '''Subclasses Syncbase adding a new() and runs it
-- 
2.13.6


Reply via email to