Aleksei Sviridkin ec6a835251 fix(manifestutil): report one message when the CRD wait is cancelled (#3742)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

`WaitForCRDsEstablished` reported a cancelled context two different
ways. The check at the top of the poll loop returned a message with no
CRD name, the `select` at the bottom named the CRD the poll had stopped
on. When a tick and the deadline come ready in the same instant,
`select` picks between the ready cases at random, so one cancellation
reached the operator either way.

Both callers wrap this as `CRDs not established after apply`, so the CRD
name is what makes that log line actionable, and whether it is there was
a coin toss. `TestWaitForCRDsEstablished_timeout` asserts on the name,
which is where this surfaced: its 2s deadline is an exact multiple of
the 500ms ticker, so the two channels come ready together on the fourth
tick and the assertion failed at random.

The message is now built in one place and returned from both exits, so
the two are indistinguishable by construction rather than by inspection.
The name is carried across iterations, because a cancellation noticed at
the top of the loop has no poll of its own to take one from, and it
starts as the first CRD of the set for a wait cancelled before any poll
observed anything. That start value is a stand-in rather than an
observation, and the comment on it says so; the alternative, an empty
name and a second phrasing for it, would leave the loop-top message with
nothing to assert against and no test able to catch its return to the
unnamed form.

Deleting the top check instead, so there is literally one exit, is the
smaller diff and it moves the name to the head of the list in the very
interleaving this PR is about. A client refuses `Get` on a dead context
at the first name in the list whichever CRD is actually lagging, so a
poll entered on a cancelled context renames the report to that first
name. That premise is about the production client, `client.New` at
`cmd/cozystack-operator/main.go:176`, uncached, so the request is
rejected before it is issued, and it is worth saying that nothing in the
test surface pins it: replacing the `ctx` passed to `Get` with
`context.Background()` survives the whole package, and the fixtures that
need a context-honouring client check `ctx.Err()` by hand. That line
predates this change and is untouched by it. Measured with a client that
honours the context: with the check, a wait that re-enters the loop
after the deadline keeps the name its last poll settled on; without it,
the report moves to the head of the list. The check closes that window
and no more than that. A context that dies part-way through a poll still
records the first `Get` to fail, exactly as merge base does, because
this branch cannot tell a refused request from a CRD status. That is
unchanged here and written up separately.

Four tests cover the ways this wait can end on a cancelled context:
before any poll has observed the set, cut off inside a poll, cancelled
while waiting for the next tick, and re-entering the loop on a context
that died during that wait. Two of them are red on merge base and are
the regression pins for the defect above, both deterministically rather
than by repetition: merge base answers a wait that starts cancelled, and
a wait that re-enters the loop cancelled, with the unnamed message every
time. Its loop-top exit is a `select` whose `Done` case is ready, and a
ready case always beats `default`. The middle two are green on merge
base, which gets those cases right; they are here because each catches a
specific way of breaking the fix. Every one of the four is the sole
killer of at least one mutation, which is the property worth having: it
says none of them is redundant. They compare the whole message rather
than searching it for a name, because a second exit that happens to
carry a name is still the defect being fixed here, and a test that only
looks for the name cannot see it.

The re-entry test needs the bottom `select` to take the tick while
cancellation is already pending, which against a real context is the
same coin toss the bug rides on. The double it uses hands out an open
`Done` for the single read that `select` makes in that iteration and a
closed one for every read after, so the tick wins by construction and
the next iteration sees a context cancelled through both `Done` and
`Err`, the way a real one is. That pairing is guaranteed rather than
incidental: `cancelCtx.Err` reads `<-c.Done()` before returning a
non-nil error, so a double that reported cancellation through `Err`
alone would be modelling a state no context can be in, and would pin the
shape of the check rather than its behaviour. One property of that
double is worth knowing before editing the loop: it hands out its open
and closed channels per read, so a `Done()` read added inside the poll
would consume the open one and leave the bottom `select` a closed
channel, quietly turning this into a second bottom-exit test that still
passes. The failure direction there is false-green, which is the worse
half, and the cheap guard is a lower bound on elapsed time inside that
test, a run that leaves through the loop top spends a ticker interval
getting there, one that leaves at the bottom of the first iteration does
not. Not done here, because the test is correct today and provably kills
the mutation it exists for, so the hazard is conditional on a future
edit.

The timeout test does reproduce locally under load: 6 failures across
400 runs on the unfixed code while I was measuring, spread over five
batches, the last of which saw none. #3719 reports that it does not
reproduce locally, so this is worth writing down before someone spends
an evening re-testing that. That spread is also why the number is an
order of magnitude and not a rate.

`TestInstall_crdNotEstablished` in `internal/crdinstall` has the same 2s
deadline against the same 500ms ticker. It asserts on the `CRDs not
established` wrapper the caller adds, and both messages carried that
wrapper, so this coin flip could not reach it and nothing here touches
it.

I left the 2s deadline where it is. Moving it off the tick boundary
would have hidden the coin flip and left the second message in place.
The new tests catch a reintroduced second message deterministically,
which is what that job now rests on rather than the timeout test's
roughly one-in-sixty.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(manifestutil): a CRD wait cut short by a cancelled context now reports one error naming the CRD it was waiting on, where before the name was present or missing depending on which of two exits the scheduler happened to pick
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved cancellation handling while waiting for custom resources to
become established.
* Cancelled operations now stop before issuing unnecessary requests and
identify the affected resource.
  * Timeout errors remain distinguishable from cancellation errors.

* **Tests**
* Added coverage for cancellation before, during, after, and between
polling attempts.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-11 02:53:40 +03:00
2025-10-08 09:43:34 +05:00
2025-04-01 18:48:14 +02:00
2024-02-08 12:04:32 +01:00

Cozystack Cozystack

Open Source Apache-2.0 License Support Active GitHub Release GitHub Commit OpenSSF Best Practices OpenSSF Scorecard

Cozystack

Cozystack is a free platform and framework for building clouds.

Cozystack is a CNCF Sandbox Level Project that was originally built and sponsored by Ænix.

With Cozystack, you can transform a bunch of servers into an intelligent system with a simple REST API for spawning Kubernetes clusters, Database-as-a-Service, virtual machines, load balancers, HTTP caching services, and other services with ease.

Use Cozystack to build your own cloud or provide a cost-effective development environment.

Cozystack user interface

Use-Cases

Documentation

The documentation is located on the cozystack.io website.

Read the Getting Started section for a quick start.

If you encounter any difficulties, start with the troubleshooting guide and work your way through the process that we've outlined.

Versioning

Versioning adheres to the Semantic Versioning principles.
A full list of the available releases is available in the GitHub repository's Release section.

Contributions

Contributions are highly appreciated and very welcomed!

In case of bugs, please check if the issue has already been opened by checking the GitHub Issues section. If it isn't, you can open a new one. A detailed report will help us replicate it, assess it, and work on a fix.

You can express your intention to on the fix on your own. Commits are used to generate the changelog, and their author will be referenced in it.

If you have a concrete feature request, open an issue. For a cross-cutting or architectural change, open a design proposal in the community repo. For questions or early ideas, use GitHub Discussions. See CONTRIBUTING.md for the full routing.

Community

You are welcome to join our Telegram group and come to our weekly community meetings. Add them to your Google Calendar or iCal for convenience.

License

Cozystack is licensed under Apache 2.0.
The code is provided as-is with no warranties.

Commercial Support

A list of companies providing commercial support for this project can be found on official site.

Languages
Go 47.2%
Shell 37.6%
TypeScript 7%
Go Template 5.4%
Makefile 1.4%
Other 1.2%