Skip to content

Commit 9ef5a3e

Browse files
authored
Merge pull request #905 from cderici/wait-for-idle-consider-app-status
#905 #### Description This adds logic to `wait_for_idle` to also consider application status when computing status via the unit statuses. Should fix #831 and #900 #### QA Steps This adds some scenarios to the existing unit tests for `wait_for_idle`. Also adds a pytest mark "wait_for_idle", so only the tests related to `wait_for_idle` can be run as a subset: ``` tox -e py3 -- -m wait_for_idle ``` ``` tox -e integration -- -m wait_for_idle ``` #### Notes & Discussion * As I wrote a TODO in the comments, `wait_for_idle` is becoming more and more convoluted. Basically we need two versions of this function. 1) `wait_for_applications()` for users who want to wait on an application to be in a certain state and doesn't particularly care about units, and 2) `wait_for_units()` for users who want finer detailed control and want to wait on certain number of units in certain applications to be in certain states etc. * @DnPlas, @ca-scribner, @beliaev-maksim, I'm gonna ask your help validating this patch. We got a bunch of unit and integration tests that should be covering most of the scenarios related to the `wait_for_idle`, however, I'd greatly appreciate if you guys could also give this PR a good stress test. I'm basically asking you to run all the integration test you have that uses `wait_for_idle` against this patch and see if it can take a punch. Thanks!
2 parents 4c44e9b + 6640174 commit 9ef5a3e

3 files changed

Lines changed: 106 additions & 18 deletions

File tree

juju/model.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2572,11 +2572,24 @@ def _raise_for_status(entities, status):
25722572
if raise_on_blocked and unit.workload_status == "blocked":
25732573
blocks.setdefault("Unit", []).append(unit.name)
25742574
continue
2575-
waiting_for_a_particular_status = status and unit.workload_status != status
2576-
if not waiting_for_a_particular_status and unit.agent_status == "idle":
2577-
# We'll be here in two cases:
2578-
# 1) We're not waiting for a particular status and the agent is "idle"
2579-
# 2) We're waiting for a particular status and the workload is in that status
2575+
# TODO (cderici): we need two versions of wait_for_idle, one for waiting on
2576+
# individual units, another one for waiting for an application.
2577+
# The convoluted logic below is the result of trying to do both at the same
2578+
# time
2579+
need_to_wait_more_for_a_particular_status = status and (unit.workload_status != status)
2580+
app_is_in_desired_status = (not status) or (app_status == status)
2581+
if not need_to_wait_more_for_a_particular_status and \
2582+
unit.agent_status == "idle" and \
2583+
(wait_for_units or app_is_in_desired_status):
2584+
# A unit is ready if either:
2585+
# 1) Don't need to wait more for a particular status and the agent is "idle"
2586+
# 2) We're looking for a particular status and the unit's workload,
2587+
# as well as the application, is in that status. If the user wants to
2588+
# see only a particular number of units in that state -- i.e. a subset of
2589+
# the units is needed, then we don't care about the application status
2590+
# (because e.g. app can be in 'waiting' while unit.0 is 'active' and unit.1
2591+
# is 'waiting')
2592+
25802593
# Either way, the unit is ready, start measuring the time period that
25812594
# it needs to stay in that state (i.e. idle_period)
25822595
units_ready.add(unit.name)

tests/integration/test_model.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -785,12 +785,12 @@ async def test_get_machines(event_loop):
785785

786786
@base.bootstrapped
787787
@pytest.mark.asyncio
788+
@pytest.mark.wait_for_idle
788789
async def test_wait_for_idle_without_units(event_loop):
789790
async with base.CleanModel() as model:
790791
await model.deploy(
791792
'ubuntu',
792793
application_name='ubuntu',
793-
series='bionic',
794794
channel='stable',
795795
num_units=0,
796796
)
@@ -800,12 +800,12 @@ async def test_wait_for_idle_without_units(event_loop):
800800

801801
@base.bootstrapped
802802
@pytest.mark.asyncio
803+
@pytest.mark.wait_for_idle
803804
async def test_wait_for_idle_with_not_enough_units(event_loop):
804805
async with base.CleanModel() as model:
805806
await model.deploy(
806807
'ubuntu',
807808
application_name='ubuntu',
808-
series='bionic',
809809
channel='stable',
810810
num_units=2,
811811
)
@@ -815,6 +815,7 @@ async def test_wait_for_idle_with_not_enough_units(event_loop):
815815

816816
@base.bootstrapped
817817
@pytest.mark.asyncio
818+
@pytest.mark.wait_for_idle
818819
async def test_wait_for_idle_more_units_than_needed(event_loop):
819820
async with base.CleanModel() as model:
820821
charm_path = TESTS_DIR / 'charm'
@@ -837,12 +838,12 @@ async def test_wait_for_idle_more_units_than_needed(event_loop):
837838

838839
@base.bootstrapped
839840
@pytest.mark.asyncio
841+
@pytest.mark.wait_for_idle
840842
async def test_wait_for_idle_with_enough_units(event_loop):
841843
async with base.CleanModel() as model:
842844
await model.deploy(
843845
'ubuntu',
844846
application_name='ubuntu',
845-
series='bionic',
846847
channel='stable',
847848
num_units=3,
848849
)
@@ -851,12 +852,12 @@ async def test_wait_for_idle_with_enough_units(event_loop):
851852

852853
@base.bootstrapped
853854
@pytest.mark.asyncio
855+
@pytest.mark.wait_for_idle
854856
async def test_wait_for_idle_with_exact_units(event_loop):
855857
async with base.CleanModel() as model:
856858
await model.deploy(
857859
'ubuntu',
858860
application_name='ubuntu',
859-
series='bionic',
860861
channel='stable',
861862
num_units=2,
862863
)
@@ -865,6 +866,7 @@ async def test_wait_for_idle_with_exact_units(event_loop):
865866

866867
@base.bootstrapped
867868
@pytest.mark.asyncio
869+
@pytest.mark.wait_for_idle
868870
async def test_wait_for_idle_with_exact_units_scale_down(event_loop):
869871
"""Deploys 3 units, waits for them to be idle, then removes 2 of them,
870872
then waits for exactly 1 unit to be left.
@@ -874,7 +876,7 @@ async def test_wait_for_idle_with_exact_units_scale_down(event_loop):
874876
app = await model.deploy(
875877
'ubuntu',
876878
application_name='ubuntu',
877-
series='bionic',
879+
series='jammy',
878880
channel='stable',
879881
num_units=3,
880882
)

tests/unit/test_model.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -296,25 +296,23 @@ async def test_timeout(self):
296296
self.assertEqual(str(cm.exception), "Timed out waiting for model:\nnonexisting_app (missing)")
297297

298298
@pytest.mark.asyncio
299+
@pytest.mark.wait_for_idle
299300
async def test_wait_for_active_status(self):
301+
app_status = 'active'
300302
# create a custom apps mock
301303
from types import SimpleNamespace
302304
app = SimpleNamespace(
303-
status="active",
305+
status=app_status,
304306
units=[SimpleNamespace(
305307
name="mockunit/0",
306-
workload_status="active",
308+
workload_status='active',
307309
workload_status_message="workload_status_message",
308310
machine=None,
309311
agent_status="idle",
310312
)],
311313
)
312-
# This is a small hack to act like we're getting 'unknown'
313-
# from the api (the get_status() call), which shouldn't
314-
# change the semantics of this test, as the 'unknown'
315-
# has the lowest severity (so the app's 'active' status
316-
# will overrule it)
317-
app.get_status = base.AsyncMock(return_value='unknown')
314+
315+
app.get_status = base.AsyncMock(return_value=app_status)
318316
apps = {"dummy_app": app}
319317

320318
with patch.object(Model, 'applications', new_callable=PropertyMock) as mock_apps:
@@ -331,3 +329,78 @@ async def test_wait_for_active_status(self):
331329
await m.wait_for_idle(apps=["dummy_app"], wait_for_active=True, status="doesn't matter")
332330

333331
mock_apps.assert_called_with()
332+
333+
@pytest.mark.asyncio
334+
@pytest.mark.wait_for_idle
335+
async def test_wait_for_active_units_waiting_application(self):
336+
# If the app is in waiting state, then wait more even if the units are ready
337+
app_status = 'waiting'
338+
# create a custom apps mock
339+
from types import SimpleNamespace
340+
app = SimpleNamespace(
341+
status=app_status,
342+
units=[SimpleNamespace(
343+
name="mockunit/0",
344+
workload_status='active',
345+
workload_status_message="workload_status_message",
346+
machine=None,
347+
agent_status="idle",
348+
),
349+
SimpleNamespace(
350+
name="mockunit/1",
351+
workload_status='active',
352+
workload_status_message="workload_status_message",
353+
machine=None,
354+
agent_status="idle",
355+
)],
356+
)
357+
358+
app.get_status = base.AsyncMock(return_value=app_status)
359+
apps = {"dummy_app": app}
360+
361+
with patch.object(Model, 'applications', new_callable=PropertyMock) as mock_apps:
362+
mock_apps.return_value = apps
363+
m = Model()
364+
365+
with self.assertRaises(jasyncio.TimeoutError):
366+
await m.wait_for_idle(apps=["dummy_app"], status="active")
367+
368+
mock_apps.assert_called_with()
369+
370+
@pytest.mark.asyncio
371+
@pytest.mark.wait_for_idle
372+
async def test_wait_for_active_units_waiting_for_units(self):
373+
# If user wants to see a particular number of units, then application may be in a waiting
374+
# state, return when there's at least that number of units in the desired state
375+
app_status = 'waiting'
376+
# create a custom apps mock
377+
from types import SimpleNamespace
378+
app = SimpleNamespace(
379+
status=app_status,
380+
units=[SimpleNamespace(
381+
name="mockunit/0",
382+
workload_status='active',
383+
workload_status_message="workload_status_message",
384+
machine=None,
385+
agent_status="idle",
386+
),
387+
SimpleNamespace(
388+
name="mockunit/1",
389+
workload_status='waiting',
390+
workload_status_message="workload_status_message",
391+
machine=None,
392+
agent_status="idle",
393+
)],
394+
)
395+
396+
app.get_status = base.AsyncMock(return_value=app_status)
397+
apps = {"dummy_app": app}
398+
399+
with patch.object(Model, 'applications', new_callable=PropertyMock) as mock_apps:
400+
mock_apps.return_value = apps
401+
m = Model()
402+
403+
await m.wait_for_idle(apps=["dummy_app"], status="active", wait_for_units=1,
404+
timeout=None)
405+
406+
mock_apps.assert_called_with()

0 commit comments

Comments
 (0)