docs: add retry loop to version check in K8s tutorial charm - #2689
docs: add retry loop to version check in K8s tutorial charm#2689dwilding wants to merge 9 commits into
Conversation
| The workload version is available after the workload starts, which happens after Pebble starts the `fastapi` service. We'll use the `src/fastapi_demo.py` helper module for this step. | ||
|
|
||
| In `src/charm.py`, add the following lines to the `_on_demo_server_pebble_ready` function before the final `self.unit.status = ops.ActiveStatus()`: | ||
| In `src/charm.py`, add the following lines to the `_on_demo_server_pebble_ready` function after the `container.replan()` line: |
There was a problem hiding this comment.
I'm changing this part so that talking about replan() later makes more sense.
|
Moving this back into draft status. An integration test run hit the race condition, so we ought to update the charm code. I'll propose an update. |
…2696) The integration tests for our COS-enabled K8s charm (k8s-5-observe) keep failing in CI. For example, https://github.com/canonical/operator/actions/runs/32157684730. The root cause is an IP range issue with microk8s. See canonical/concierge#251. This PR switches to Canonical K8s, to match our K8s tutorial. I'm using the same custom `k8s` Concierge preset as elsewhere in our CI. Our custom preset allows 30 mins for bootstrap to complete. In addition, I'm setting a 90 min timeout for the whole Concierge job. During testing yesterday, I observed several runs that spun indefinitely - presumably a transient issue with the runner, as the problem didn't appear today. The problem didn't seem to be related to the Concierge preset, so I've set the same timeout for the machine charm job. **[Passing run in my fork](https://github.com/dwilding/operator/actions/runs/32230089789)**. The k8s-4-action failure is unrelated; it's related to #2689.
| We're using the `ActiveStatus` class to set the charm status to active. Note that almost everything you need to define your charm is in the `ops` package that you imported earlier - there's no need to add additional imports. | ||
| We're using the `ActiveStatus` class to set the charm status to active. Note that almost everything you need to define your charm is in the `ops` package that you imported earlier. |
There was a problem hiding this comment.
I'm removing the last part because we actually do need additional imports now.
|
@tromai I updated the charm code and tutorial to include a retry loop - see the updated PR description for details. Would you mind reviewing again? Thanks! I also thought how we might test what the charm does when the race occurs. I think we'd have to change |
There was a problem hiding this comment.
Thanks for the PR. It looks good to me.
I think we'd have to change mock_version in the unit tests so that it fails a certain number of times, or until sufficient time has passed.
Ah, that makes sense.
I originally thought about an "un-happy" path test, where I just mock get_version to always trigger urllib.error.URLError. The test would run ctx.run(ctx.on.pebble_ready(container), state_in) and expects RuntimeError. An example:
mock_sleep = Mock()
monkeypatch.setattr("time.sleep", mock_sleep)
mock_get = Mock(side_effect=urllib.error.URLError("not ready"))
monkeypatch.setattr("fastapi_demo.get_version", mock_get)
ctx = testing.Context(FastAPIDemoCharm)
container = testing.Container(
name="demo-server", can_connect=True, layers={"rock": ROCK_LAYER}
)
state_in = testing.State(
containers={container},
leader=True,
)
with pytest.raises(RuntimeError, match="workload is not available"):
ctx.run(ctx.on.pebble_ready(container), state_in)I agree that the setup can be fiddly. I am happy to exclude this test.
|
@hpidcock this is ready for review now after rework - thanks! |
| # The workload may not be ready immediately after replan(), so try get_version() in a loop. | ||
| for attempt in range(3): | ||
| if attempt: | ||
| time.sleep(1) # If not the first attempt, wait before retrying. | ||
| try: | ||
| version = fastapi_demo.get_version(port=8000) | ||
| break | ||
| except urllib.error.URLError: | ||
| continue | ||
| else: | ||
| logger.error("The workload was not available within the expected time") | ||
| raise RuntimeError("workload is not available") |
There was a problem hiding this comment.
Showing a more durable retry strategy in tutorials is a good idea.
| # The workload may not be ready immediately after replan(), so try get_version() in a loop. | |
| for attempt in range(3): | |
| if attempt: | |
| time.sleep(1) # If not the first attempt, wait before retrying. | |
| try: | |
| version = fastapi_demo.get_version(port=8000) | |
| break | |
| except urllib.error.URLError: | |
| continue | |
| else: | |
| logger.error("The workload was not available within the expected time") | |
| raise RuntimeError("workload is not available") | |
| # The workload may not be ready immediately after replan(), so try get_version() in a loop. | |
| for attempt in range(6): | |
| if attempt: | |
| time.sleep(2 ** attempt) # If not the first attempt, an exponential back-off before retrying. | |
| try: | |
| version = fastapi_demo.get_version(port=8000) | |
| break | |
| except urllib.error.URLError: | |
| continue | |
| else: | |
| logger.error("The workload was not available within the expected time") | |
| raise RuntimeError("workload is not available") |
There was a problem hiding this comment.
It's overkill for this particular charm, because the workload really does just need a couple more seconds to get ready. If it's going to fail for someone working through the tutorial, I would want it to fail in less than ~1 minute.
But I get your point that we want to teach a robust strategy. And now I remember that we use exponential back-off in the httpbin demo charm too (here).
How about this:
for attempt in range(3): # In general, allow more attempts for a complex workload.
This part of the K8s tutorial charm has a potential race:
In practice we're OK because our workload starts serving with Pebble's 1 second early-exit check. But for a production workload, it would be better to wrapget_version()in a retry loop.I observed the race in an integration test run, so we ought to make the charm code more robust.
This PR:
timeandurllib.error.logger.Preview doc