From c5602be186305e920c63e250b33282fa09c3839a Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 11:50:57 +0100 Subject: [PATCH 1/9] Add constant_rps and constant_rps_total wait time functions (ported from locust-plugins) --- locust/core.py | 3 +++ locust/runners.py | 10 +++++++-- locust/wait_time.py | 51 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/locust/core.py b/locust/core.py index 9ad50d43bd..5527270712 100644 --- a/locust/core.py +++ b/locust/core.py @@ -152,6 +152,9 @@ class User(Locust): _setup_has_run = False # Internal state to see if we have already run _teardown_is_set = False # Internal state to see if we have already run _lock = gevent.lock.Semaphore() # Lock to make sure setup is only run once + # Used by constant_pacing timers + _cp_last_run = 0 + _cp_target_missed = False _state = False def __init__(self): diff --git a/locust/runners.py b/locust/runners.py index 8dde0006f7..6bebfc4625 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -43,6 +43,7 @@ def __init__(self, locust_classes, options): self.stepload_greenlet = None self.current_cpu_usage = 0 self.cpu_warning_emitted = False + self.rps_warning_emitted = False self.greenlet.spawn(self.monitor_cpu) self.exceptions = {} self.stats = global_stats @@ -77,8 +78,13 @@ def cpu_log_warning(self): """Called at the end of the test to repeat the warning & return the status""" if self.cpu_warning_emitted: logger.warning("Loadgen CPU usage was too high at some point during the test! See https://docs.locust.io/en/stable/running-locust-distributed.html for how to distribute the load over multiple CPU cores or machines") - return True - return False + return self.cpu_warning_emitted + + def rps_log_warning(self): + """Called at the end of the test to repeat the warning & return the status""" + if self.rps_warning_emitted: + logger.warning("Failed to reach target RPS (at some point during the test). The most common cause of this is target system overload or too few clients") + return self.rps_warning_emitted def weight_locusts(self, amount): """ diff --git a/locust/wait_time.py b/locust/wait_time.py index 1399f37810..0afb17ce97 100644 --- a/locust/wait_time.py +++ b/locust/wait_time.py @@ -1,5 +1,7 @@ import random from time import time +from locust import runners +import logging def between(min_wait, max_wait): @@ -47,7 +49,7 @@ def my_task(self): the next task. """ def wait_time_func(self): - if not hasattr(self,"_cp_last_run"): + if not self._cp_last_run: self._cp_last_wait_time = wait_time self._cp_last_run = time() return wait_time @@ -57,3 +59,50 @@ def wait_time_func(self): self._cp_last_run = time() return self._cp_last_wait_time return wait_time_func + + +def constant_rps(rps): + """ + This behaves exactly the same as constant_pacing but with an inverted parameter. + It takes requests per second as a parameter instead of time between requests. + """ + + return constant_pacing(1 / rps) + + +def constant_rps_total(rps): + """ + Returns a function that will track the run time of all tasks in this locust process, + and for each time it's called it will return a wait time that will try to make the + execution equal to the time specified by the wait_time argument. + + This is similar to constant_rps, but looks at all clients/locusts in a locust process. + + Note that in a distributed run, the RPS limit is applied per-slave, not globally. + + During rampup, the RPS is intentionally constrained to be the requested rps * the share of running clients. + + Will output a warning if RPS target is missed twice in a row + """ + + def wait_time_func(self): + lr = runners.locust_runner + if not lr: + # We're running a locust directly. Make some kind of effort to do the right thing, + # without overcomplicating things for what is really an edge case + return 1 / rps + current_time = float(time()) + unstarted_clients = lr.num_clients - len(lr.locusts) + next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / rps + if current_time > next_time: + if lr.state == runners.STATE_RUNNING and self._cp_target_missed and not lr.rps_warning_emitted: + logging.warning("Failed to reach target rps, even after rampup has finished") + lr.rps_warning_emitted = True # stop logging + self._cp_target_missed = True + self._cp_last_run = current_time + return 0 + self._cp_target_missed = False + self._cp_last_run = next_time + return next_time - current_time + + return wait_time_func From 31c52c03aff1152f4475d628890bdc16ed12a353 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 11:51:42 +0100 Subject: [PATCH 2/9] Use non-short-circuiting operators to ensure we get all warnings. --- locust/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/main.py b/locust/main.py index 704184cc01..c1f77a2312 100644 --- a/locust/main.py +++ b/locust/main.py @@ -571,7 +571,7 @@ def sig_term_handler(): main_greenlet.join() code = 0 lr = runners.locust_runner - if len(lr.errors) or len(lr.exceptions) or lr.cpu_log_warning(): + if len(lr.errors) | len(lr.exceptions) | lr.cpu_log_warning() | lr.rps_log_warning(): code = options.exit_code_on_error shutdown(code=code) except KeyboardInterrupt as e: From d96dffa24a5702c435b69d5271d121dcf8c9d1b5 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 14:06:51 +0100 Subject: [PATCH 3/9] Fix if users specify wait time on the task and not the locust. --- locust/core.py | 3 --- locust/wait_time.py | 10 +++++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/locust/core.py b/locust/core.py index 5527270712..9ad50d43bd 100644 --- a/locust/core.py +++ b/locust/core.py @@ -152,9 +152,6 @@ class User(Locust): _setup_has_run = False # Internal state to see if we have already run _teardown_is_set = False # Internal state to see if we have already run _lock = gevent.lock.Semaphore() # Lock to make sure setup is only run once - # Used by constant_pacing timers - _cp_last_run = 0 - _cp_target_missed = False _state = False def __init__(self): diff --git a/locust/wait_time.py b/locust/wait_time.py index 0afb17ce97..46746bb2b1 100644 --- a/locust/wait_time.py +++ b/locust/wait_time.py @@ -49,7 +49,7 @@ def my_task(self): the next task. """ def wait_time_func(self): - if not self._cp_last_run: + if not hasattr(self, "_cp_last_run"): self._cp_last_wait_time = wait_time self._cp_last_run = time() return wait_time @@ -88,11 +88,15 @@ def constant_rps_total(rps): def wait_time_func(self): lr = runners.locust_runner if not lr: - # We're running a locust directly. Make some kind of effort to do the right thing, - # without overcomplicating things for what is really an edge case + logging.warning( + "You asked for constant total rps, but you seem to be running a locust directly. Hopefully you are only running one locust, in which case this will give a somewhat reasonable estimate." + ) return 1 / rps current_time = float(time()) unstarted_clients = lr.num_clients - len(lr.locusts) + if not hasattr(self, "_cp_last_run"): + self._cp_last_run = 0 + self._cp_target_missed = False next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / rps if current_time > next_time: if lr.state == runners.STATE_RUNNING and self._cp_target_missed and not lr.rps_warning_emitted: From f27ae6ffaee9aa5b9a1f2844d4e77ac5f801c12c Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 14:07:24 +0100 Subject: [PATCH 4/9] Add unit tests for constant rps --- locust/test/test_runners.py | 22 +++++++++++++++++++++- locust/test/test_wait_time.py | 22 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 97b0792c2a..304cb65c26 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -13,7 +13,7 @@ SlaveLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING from locust.stats import global_stats, RequestStats from locust.test.testcases import LocustTestCase -from locust.wait_time import between, constant +from locust.wait_time import between, constant, constant_rps_total def mocked_rpc(): @@ -627,6 +627,26 @@ class MyLocust(Locust): self.assertTrue("HeyAnException" in exception["traceback"]) self.assertEqual(2, exception["count"]) + def test_constant_rps_total(self): + target_rps = 50 + run_time = 3 + + class MyTestLocust(Locust): + i = 0 + class task_set(TaskSet): + @task + def the_task(self): + MyTestLocust.i = MyTestLocust.i + 1 + wait_time = constant_rps_total(target_rps) + + options = mocked_options() + runner = LocalLocustRunner([MyTestLocust], options) + runners.locust_runner = runner # this is necessary for rps_total + runner.start_hatching(10, 999) + gevent.sleep(run_time) + runner.quit() + locust_runner = None # just in case someone depends on this not being set + self.assertAlmostEqual(MyTestLocust.i, target_rps * run_time, delta=25) class TestSlaveLocustRunner(LocustTestCase): def setUp(self): diff --git a/locust/test/test_wait_time.py b/locust/test/test_wait_time.py index 359c536dc8..a75e5c828b 100644 --- a/locust/test/test_wait_time.py +++ b/locust/test/test_wait_time.py @@ -3,7 +3,7 @@ from locust.core import HttpLocust, Locust, TaskSet, events, task from locust.exception import MissingWaitTimeError -from locust.wait_time import between, constant, constant_pacing +from locust.wait_time import between, constant, constant_pacing, constant_rps, constant_rps_total from .testcases import LocustTestCase, WebserverTestCase @@ -70,6 +70,26 @@ class TS(TaskSet): _ = ts2.wait_time() _ = ts2.wait_time() + def test_constant_rps(self): + # Note: constant_rps_total is tested in test_runners.py, because it requires a runner + class User(Locust): + wait_time = constant_rps(10) + class TS(TaskSet): + pass + ts = TS(User()) + + ts2 = TS(User()) + + previous_time = time.time() + for i in range(7): + ts.wait() + since_last_run = time.time() - previous_time + self.assertLess(abs(0.1 - since_last_run), 0.02) + previous_time = time.time() + time.sleep(random.random() * 0.1) + _ = ts2.wait_time() + _ = ts2.wait_time() + def test_missing_wait_time(self): class User(Locust): pass From f1a021b630237050dbefd16639072521235b8542 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 14:19:19 +0100 Subject: [PATCH 5/9] Maybe this is what is confusing py2.7? --- locust/wait_time.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/wait_time.py b/locust/wait_time.py index 46746bb2b1..48406ee0f8 100644 --- a/locust/wait_time.py +++ b/locust/wait_time.py @@ -67,7 +67,7 @@ def constant_rps(rps): It takes requests per second as a parameter instead of time between requests. """ - return constant_pacing(1 / rps) + return constant_pacing(1.0 / rps) def constant_rps_total(rps): @@ -91,7 +91,7 @@ def wait_time_func(self): logging.warning( "You asked for constant total rps, but you seem to be running a locust directly. Hopefully you are only running one locust, in which case this will give a somewhat reasonable estimate." ) - return 1 / rps + return 1.0 / rps current_time = float(time()) unstarted_clients = lr.num_clients - len(lr.locusts) if not hasattr(self, "_cp_last_run"): From d3698ff967ed375459010b04d009e4a23cbe2995 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 15:06:11 +0100 Subject: [PATCH 6/9] Change from wait_time naming from rps to ips (iterations per second) because that is what it really is. --- locust/runners.py | 10 +++++----- locust/test/test_runners.py | 12 ++++++------ locust/test/test_wait_time.py | 8 ++++---- locust/wait_time.py | 30 ++++++++++++++++-------------- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 6bebfc4625..31acf8fe30 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -43,7 +43,7 @@ def __init__(self, locust_classes, options): self.stepload_greenlet = None self.current_cpu_usage = 0 self.cpu_warning_emitted = False - self.rps_warning_emitted = False + self.ips_warning_emitted = False self.greenlet.spawn(self.monitor_cpu) self.exceptions = {} self.stats = global_stats @@ -80,11 +80,11 @@ def cpu_log_warning(self): logger.warning("Loadgen CPU usage was too high at some point during the test! See https://docs.locust.io/en/stable/running-locust-distributed.html for how to distribute the load over multiple CPU cores or machines") return self.cpu_warning_emitted - def rps_log_warning(self): + def ips_log_warning(self): """Called at the end of the test to repeat the warning & return the status""" - if self.rps_warning_emitted: - logger.warning("Failed to reach target RPS (at some point during the test). The most common cause of this is target system overload or too few clients") - return self.rps_warning_emitted + if self.ips_warning_emitted: + logger.warning("Failed to reach targeted number of iterations per second (at some point during the test). The most common cause of this is target system overload or too few clients") + return self.ips_warning_emitted def weight_locusts(self, amount): """ diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 304cb65c26..b8374ee791 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -13,7 +13,7 @@ SlaveLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING from locust.stats import global_stats, RequestStats from locust.test.testcases import LocustTestCase -from locust.wait_time import between, constant, constant_rps_total +from locust.wait_time import between, constant, constant_ips_total def mocked_rpc(): @@ -627,8 +627,8 @@ class MyLocust(Locust): self.assertTrue("HeyAnException" in exception["traceback"]) self.assertEqual(2, exception["count"]) - def test_constant_rps_total(self): - target_rps = 50 + def test_constant_ips_total(self): + target_ips = 50 run_time = 3 class MyTestLocust(Locust): @@ -637,16 +637,16 @@ class task_set(TaskSet): @task def the_task(self): MyTestLocust.i = MyTestLocust.i + 1 - wait_time = constant_rps_total(target_rps) + wait_time = constant_ips_total(target_ips) options = mocked_options() runner = LocalLocustRunner([MyTestLocust], options) - runners.locust_runner = runner # this is necessary for rps_total + runners.locust_runner = runner # this is necessary for ips_total runner.start_hatching(10, 999) gevent.sleep(run_time) runner.quit() locust_runner = None # just in case someone depends on this not being set - self.assertAlmostEqual(MyTestLocust.i, target_rps * run_time, delta=25) + self.assertAlmostEqual(MyTestLocust.i, target_ips * run_time, delta=25) class TestSlaveLocustRunner(LocustTestCase): def setUp(self): diff --git a/locust/test/test_wait_time.py b/locust/test/test_wait_time.py index a75e5c828b..fec7493288 100644 --- a/locust/test/test_wait_time.py +++ b/locust/test/test_wait_time.py @@ -3,7 +3,7 @@ from locust.core import HttpLocust, Locust, TaskSet, events, task from locust.exception import MissingWaitTimeError -from locust.wait_time import between, constant, constant_pacing, constant_rps, constant_rps_total +from locust.wait_time import between, constant, constant_pacing, constant_ips, constant_ips_total from .testcases import LocustTestCase, WebserverTestCase @@ -70,10 +70,10 @@ class TS(TaskSet): _ = ts2.wait_time() _ = ts2.wait_time() - def test_constant_rps(self): - # Note: constant_rps_total is tested in test_runners.py, because it requires a runner + def test_constant_ips(self): + # Note: constant_ips_total is tested in test_runners.py, because it requires a runner class User(Locust): - wait_time = constant_rps(10) + wait_time = constant_ips(10) class TS(TaskSet): pass ts = TS(User()) diff --git a/locust/wait_time.py b/locust/wait_time.py index 48406ee0f8..44ae44ee5f 100644 --- a/locust/wait_time.py +++ b/locust/wait_time.py @@ -48,6 +48,7 @@ def my_task(self): If a task execution exceeds the specified wait_time, the wait will be 0 before starting the next task. """ + def wait_time_func(self): if not hasattr(self, "_cp_last_run"): self._cp_last_wait_time = wait_time @@ -58,50 +59,51 @@ def wait_time_func(self): self._cp_last_wait_time = max(0, wait_time - run_time) self._cp_last_run = time() return self._cp_last_wait_time + return wait_time_func -def constant_rps(rps): +def constant_ips(ips): """ This behaves exactly the same as constant_pacing but with an inverted parameter. - It takes requests per second as a parameter instead of time between requests. + It takes iterations per second as a parameter instead of time between iterations. """ - return constant_pacing(1.0 / rps) + return constant_pacing(1.0 / ips) -def constant_rps_total(rps): +def constant_ips_total(ips): """ Returns a function that will track the run time of all tasks in this locust process, and for each time it's called it will return a wait time that will try to make the execution equal to the time specified by the wait_time argument. - This is similar to constant_rps, but looks at all clients/locusts in a locust process. + This is similar to constant_ips, but looks at all clients/locusts in a locust process. - Note that in a distributed run, the RPS limit is applied per-slave, not globally. + Note that in a distributed run, the iterations per second limit is applied per-slave, not globally. - During rampup, the RPS is intentionally constrained to be the requested rps * the share of running clients. + During rampup, the IPS is intentionally constrained to be the requested ips * the share of running clients. - Will output a warning if RPS target is missed twice in a row + Will output a warning if IPS target is missed twice in a row """ def wait_time_func(self): lr = runners.locust_runner if not lr: logging.warning( - "You asked for constant total rps, but you seem to be running a locust directly. Hopefully you are only running one locust, in which case this will give a somewhat reasonable estimate." + "You asked for constant total ips, but you seem to be running a locust directly. Hopefully you are only running one locust, in which case this will give a somewhat reasonable estimate." ) - return 1.0 / rps + return 1.0 / ips current_time = float(time()) unstarted_clients = lr.num_clients - len(lr.locusts) if not hasattr(self, "_cp_last_run"): self._cp_last_run = 0 self._cp_target_missed = False - next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / rps + next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / ips if current_time > next_time: - if lr.state == runners.STATE_RUNNING and self._cp_target_missed and not lr.rps_warning_emitted: - logging.warning("Failed to reach target rps, even after rampup has finished") - lr.rps_warning_emitted = True # stop logging + if lr.state == runners.STATE_RUNNING and self._cp_target_missed and not lr.ips_warning_emitted: + logging.warning("Failed to reach target ips, even after rampup has finished") + lr.ips_warning_emitted = True # stop logging self._cp_target_missed = True self._cp_last_run = current_time return 0 From db5ff3784092c61709186ea36244afc93a87a886 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 15:08:07 +0100 Subject: [PATCH 7/9] one more place to rename from rps to ips... --- locust/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/main.py b/locust/main.py index c1f77a2312..ae0f43e7c5 100644 --- a/locust/main.py +++ b/locust/main.py @@ -571,7 +571,7 @@ def sig_term_handler(): main_greenlet.join() code = 0 lr = runners.locust_runner - if len(lr.errors) | len(lr.exceptions) | lr.cpu_log_warning() | lr.rps_log_warning(): + if len(lr.errors) | len(lr.exceptions) | lr.cpu_log_warning() | lr.ips_log_warning(): code = options.exit_code_on_error shutdown(code=code) except KeyboardInterrupt as e: From 9377a7e08d58bd4221d16ff9e11bb018d3d856f6 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 15:17:45 +0100 Subject: [PATCH 8/9] Integer division in py2 is sneaky (C-style) --- locust/wait_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/wait_time.py b/locust/wait_time.py index 44ae44ee5f..bd4fbbf902 100644 --- a/locust/wait_time.py +++ b/locust/wait_time.py @@ -99,7 +99,7 @@ def wait_time_func(self): if not hasattr(self, "_cp_last_run"): self._cp_last_run = 0 self._cp_target_missed = False - next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / ips + next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / float(ips) if current_time > next_time: if lr.state == runners.STATE_RUNNING and self._cp_target_missed and not lr.ips_warning_emitted: logging.warning("Failed to reach target ips, even after rampup has finished") From 4a2da70be44d172073b0147da40d2bc08abbfb20 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Thu, 23 Jan 2020 15:38:17 +0100 Subject: [PATCH 9/9] Updated api & release notes for ips functions --- docs/api.rst | 2 +- docs/changelog.rst | 2 +- locust/runners.py | 4 +++- locust/wait_time.py | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 86e7838917..f53c75438e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -44,7 +44,7 @@ Built in wait_time functions ============================ .. automodule:: locust.wait_time - :members: between, constant, constant_pacing + :members: between, constant, constant_pacing, constant_ips, constant_ips_total HttpSession class ================= diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ae47c2ac5..190f023ab5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,7 +9,7 @@ In development (master) * Continuously measure CPU usage and emit a warning if we get a five second average above 90% * Show CPU usage of slave nodes in the Web UI - +* Add new timers that target iterations per second: constant_ips (the inverse of constant_pacing) and constant_ips_total (total iterations per second, across locusts) 0.13.5 ====== diff --git a/locust/runners.py b/locust/runners.py index 31acf8fe30..9d125f17c1 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -41,6 +41,7 @@ def __init__(self, locust_classes, options): self.state = STATE_INIT self.hatching_greenlet = None self.stepload_greenlet = None + self.target_user_count = None self.current_cpu_usage = 0 self.cpu_warning_emitted = False self.ips_warning_emitted = False @@ -216,6 +217,7 @@ def start_hatching(self, locust_count, hatch_rate, wait=False): self.exceptions = {} self.cpu_warning_emitted = False self.slave_cpu_warning_emitted = False + self.target_user_count = locust_count events.locust_start_hatching.fire() # Dynamically changing the locust count @@ -298,6 +300,7 @@ def on_locust_error(locust_instance, exception, tb): events.locust_error += on_locust_error def start_hatching(self, locust_count, hatch_rate, wait=False): + self.target_user_count = locust_count if hatch_rate > 100: logger.warning("Your selected hatch rate is very high (>100), and this is known to sometimes cause issues. Do you really need to ramp up that fast?") if self.hatching_greenlet: @@ -329,7 +332,6 @@ class MasterLocustRunner(DistributedLocustRunner): def __init__(self, *args, **kwargs): super(MasterLocustRunner, self).__init__(*args, **kwargs) self.slave_cpu_warning_emitted = False - self.target_user_count = None class SlaveNodesDict(dict): def get_by_state(self, state): diff --git a/locust/wait_time.py b/locust/wait_time.py index bd4fbbf902..0e2bf846b7 100644 --- a/locust/wait_time.py +++ b/locust/wait_time.py @@ -95,11 +95,11 @@ def wait_time_func(self): ) return 1.0 / ips current_time = float(time()) - unstarted_clients = lr.num_clients - len(lr.locusts) + unstarted_clients = lr.target_user_count - lr.user_count if not hasattr(self, "_cp_last_run"): self._cp_last_run = 0 self._cp_target_missed = False - next_time = self._cp_last_run + (lr.num_clients + unstarted_clients) / float(ips) + next_time = self._cp_last_run + (lr.target_user_count + unstarted_clients) / float(ips) if current_time > next_time: if lr.state == runners.STATE_RUNNING and self._cp_target_missed and not lr.ips_warning_emitted: logging.warning("Failed to reach target ips, even after rampup has finished")