Skip to content

docs: add retry loop to version check in K8s tutorial charm - #2689

Open
dwilding wants to merge 9 commits into
canonical:mainfrom
dwilding:k8s-tutorial-version-race
Open

docs: add retry loop to version check in K8s tutorial charm#2689
dwilding wants to merge 9 commits into
canonical:mainfrom
dwilding:k8s-tutorial-version-race

Conversation

@dwilding

@dwilding dwilding commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

This part of the K8s tutorial charm has a potential race:

container.replan()
version = fastapi_demo.get_version(port=8000)

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 wrap get_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:

  • Updates the charm code to include a retry loop (in all five charms)
  • In "Create a minimal Kubernetes charm":
    • Adds an explanation of why the loop is needed.
    • Adds an instruction to import time and urllib.error.
    • Moves the "Add logger functionality" section earlier so that we've introduced logger.

Preview doc

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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm changing this part so that talking about replan() later makes more sense.

@dwilding
dwilding requested review from hpidcock and tromai August 13, 2026 01:30
@dwilding

Copy link
Copy Markdown
Contributor Author

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.

@dwilding
dwilding marked this pull request as draft August 20, 2026 01:59
dwilding added a commit that referenced this pull request Aug 20, 2026
…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.
Comment on lines -187 to +198
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm removing the last part because we actually do need additional imports now.

@dwilding
dwilding marked this pull request as ready for review August 24, 2026 10:18
@dwilding

Copy link
Copy Markdown
Contributor Author

@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 mock_version in the unit tests so that it fails a certain number of times, or until sufficient time has passed. It seems fiddly for the tutorial, so I didn't include it. What do you think?

@tromai tromai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dwilding

Copy link
Copy Markdown
Contributor Author

@hpidcock this is ready for review now after rework - thanks!

@dwilding dwilding changed the title docs: in K8s tutorial, explain that version check might need a retry loop docs: add retry loop to version check in K8s tutorial charm Aug 26, 2026
@dwilding dwilding linked an issue Aug 26, 2026 that may be closed by this pull request

@hpidcock hpidcock left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +227 to +238
# 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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Showing a more durable retry strategy in tutorials is a good idea.

Suggested change
# 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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scheduled workflow 'Example Charm Integration Tests' failed

3 participants