URL: https://github.com/freeipa/freeipa/pull/5165
Author: tiran
 Title: #5165: Reduce long sleeps in certmonger wait_for_request()
Action: opened

PR body:
"""
## Add helper for poll/sleep loops with timeout
    
The Sleeper class is a helper that makes poll/sleep loops with timeout
easier to write. It takes care of edge cases and does not oversleep
timeout deadline.

## Faster certmonger wait_for_request()
    
wait_for_request() now waits 0.5 instead of 5 seconds. This shoves off
15 to 20 seconds from ipa-server-install while marginally increased
load on the system.

Related: https://pagure.io/freeipa/issue/8521
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/5165/head:pr5165
git checkout pr5165
From c69a30f3a6e48616b31ca7aa76aedc0443464487 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Mon, 21 Sep 2020 12:39:52 +0200
Subject: [PATCH 1/2] Add helper for poll/sleep loops with timeout

The Sleeper class is a helper that makes poll/sleep loops with timeout
easier to write. It takes care of edge cases and does not oversleep
timeout deadline.

Related: https://pagure.io/freeipa/issue/8521
Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 ipapython/ipautil.py                    | 63 +++++++++++++++++++++++++
 ipatests/test_ipapython/test_ipautil.py | 34 +++++++++++++
 2 files changed, 97 insertions(+)

diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py
index ebf740be48..71992287c3 100644
--- a/ipapython/ipautil.py
+++ b/ipapython/ipautil.py
@@ -1654,3 +1654,66 @@ def datetime_from_utctimestamp(t, units=1):
         raise TypeError(t)
     epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
     return epoch + datetime.timedelta(seconds=v // units)
+
+
+class Sleeper:
+    """Helper for time.sleep() loop with timeout
+
+    A sleeper object sleeps *sleep* seconds when it is called. Close to its
+    deadline it sleeps shorter to not oversleep the *timeout* deadline. A
+    sleeper object is *True* and returns *True* before it reaches *timeout*
+    deadline. After its deadline a sleeper raises the exception object/class
+    in *raises*. If *raises* is not given, it returns False instead.
+
+    sleep = Sleeper(sleep=1, timeout=60, raises=TimeoutError)
+    while True:
+        do_something()
+        sleep()
+
+    sleep = Sleeper(sleep=0.5, timeout=60)
+    while True:
+        try:
+            do_something
+        except Exception:
+            # sleep duration can be extended
+            sleep(10)
+        else:
+            if not sleep():
+                log.info("timeout")
+                break
+
+    longsleep = Sleeper(sleep=1, timeout=sys.maxsize)
+    """
+    multiplier = 2
+
+    def __init__(self, *, sleep, timeout, raises=None):
+        if timeout <= 0:
+            raise ValueError(f"invalid timeout {timeout}")
+        if sleep < 0.01:
+            raise ValueError(f"sleep duration {sleep} is too short.")
+
+        self.timeout = timeout
+        self.sleep = sleep
+        self.raises = raises
+
+        self.deadline = time.monotonic() + self.timeout
+
+    def __bool__(self):
+        return time.monotonic() < self.deadline
+
+    def __call__(self, extended_sleep=None):
+        now = time.monotonic()
+        if now >= self.deadline:
+            if self.raises is not None:
+                raise self.raises
+            else:
+                return False
+
+        # caller can instruct sleeper to sleep longer
+        dur = self.sleep if extended_sleep is None else extended_sleep
+        # but don't sleep over deadline
+        dur = min(self.deadline - now, dur)
+
+        time.sleep(dur)
+
+        return True
diff --git a/ipatests/test_ipapython/test_ipautil.py b/ipatests/test_ipapython/test_ipautil.py
index 681313ce01..5137d80cce 100644
--- a/ipatests/test_ipapython/test_ipautil.py
+++ b/ipatests/test_ipapython/test_ipautil.py
@@ -28,6 +28,7 @@
 import sys
 import tempfile
 import textwrap
+import time
 
 import pytest
 import six
@@ -604,3 +605,36 @@ def test_config_replace_variables(tempdir):
     with open(conffile, 'r') as f:
         newconf = f.read()
     assert newconf == expected
+
+
+def test_sleeper():
+    start = time.monotonic()
+    sleep = ipautil.Sleeper(sleep=0.020, timeout=0.200, raises=TimeoutError)
+    assert sleep
+
+    with pytest.raises(TimeoutError):
+        while True:
+            sleep()
+
+    dur = time.monotonic() - start
+    assert not sleep
+    assert dur >= 0.2
+    # should finish in 0.2, accept longer time in case the system is busy
+    assert dur < 1.
+
+    start = time.monotonic()
+    loops = 0
+    sleep = ipautil.Sleeper(sleep=0.020, timeout=0.200)
+    assert sleep
+
+    while True:
+        if not sleep():
+            break
+        loops += 1
+
+    dur = time.monotonic() - start
+    assert not sleep
+    assert dur >= 0.2
+    assert dur < 1.
+    # should be 10 loops, accept 9 for slow systems
+    assert loops in {9, 10}

From da15baf0644b720afa8307ff80fbe3fe7be5b8a7 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Mon, 21 Sep 2020 12:45:08 +0200
Subject: [PATCH 2/2] Faster certmonger wait_for_request()

wait_for_request() now waits 0.5 instead of 5 seconds. This shoves off
15 to 20 seconds from ipa-server-install while marginally increased
load on the system.

request_and_wait_for_cert() now uses correct certmonger_wait_timeout
instead of http_timeout.

Related: https://pagure.io/freeipa/issue/8521
Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 ipalib/install/certmonger.py | 29 ++++++++++++++++++++---------
 1 file changed, 20 insertions(+), 9 deletions(-)

diff --git a/ipalib/install/certmonger.py b/ipalib/install/certmonger.py
index 9e8d46fc17..a5d410cf83 100644
--- a/ipalib/install/certmonger.py
+++ b/ipalib/install/certmonger.py
@@ -34,6 +34,7 @@
 from ipalib import api
 from ipalib.constants import CA_DBUS_TIMEOUT
 from ipapython.dn import DN
+from ipapython.ipautil import Sleeper
 from ipaplatform.paths import paths
 from ipaplatform import services
 
@@ -367,11 +368,15 @@ def request_and_wait_for_cert(
         certpath, subject, principal, nickname, passwd_fname, dns, ca,
         profile, pre_command, post_command, storage, perms
     )
+    # Don't wait longer than resubmit timeout if it is configured
+    certmonger_timeout = api.env.certmonger_wait_timeout
+    if resubmit_timeout and resubmit_timeout < certmonger_timeout:
+        certmonger_timeout = resubmit_timeout
 
     deadline = time.time() + resubmit_timeout
     while True:  # until success, timeout, or error
         try:
-            state = wait_for_request(req_id, api.env.http_timeout)
+            state = wait_for_request(req_id, certmonger_timeout)
         except RuntimeError as e:
             logger.debug("wait_for_request raised %s", e)
             state = 'TIMEOUT'
@@ -772,14 +777,20 @@ def check_state(dirs):
 
 
 def wait_for_request(request_id, timeout=120):
-    for _i in range(0, timeout, 5):
-        state = get_request_value(request_id, 'status')
-        logger.debug("certmonger request is in state %r", state)
-        if state in ('CA_REJECTED', 'CA_UNREACHABLE', 'CA_UNCONFIGURED',
-                     'NEED_GUIDANCE', 'NEED_CA', 'MONITORING'):
+    sleep = Sleeper(
+        sleep=0.5,  # getcert.c:waitfor() uses 125ms
+        timeout=timeout,
+        raises=RuntimeError("request timed out")
+    )
+    last_state = None
+    while True:
+        state = str(get_request_value(request_id, 'status'))
+        if state != last_state:
+            logger.debug("certmonger request is in state %r", state)
+        if state in {'CA_REJECTED', 'CA_UNREACHABLE', 'CA_UNCONFIGURED',
+                     'NEED_GUIDANCE', 'NEED_CA', 'MONITORING'}:
             break
-        time.sleep(5)
-    else:
-        raise RuntimeError("request timed out")
+        last_state = state
+        sleep()
 
     return state
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: 
https://docs.fedoraproject.org/en-US/project/code-of-conduct/
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org

Reply via email to