refactor(cluster): wall-clock mark staleness and xdist_group pairing - #3565
Merged
Conversation
An abandoned test mark (a group of marked tests that stopped running, e.g. because the pytest worker moved on) used to be detected by counting scheduler loop iterations - a mark was cleaned up after 20 iterations of a single worker with no marked test running. The expiry time therefore depended on the worker's sleep backoff (roughly 25 seconds to 2 minutes) and each newly arriving worker started counting from zero. The counters were kept in a cache dict threaded through `get_cluster_instance`. Timestamp the "current mark" records instead: `refresh_curr_mark` updates the records' creation time whenever a marked test is seen running by a polling worker and whenever a marked test finishes (in `on_test_stop`), and the mark is cleaned up when no marked test was running for `MARK_STALENESS_SEC` (60 seconds) - a deterministic wall-clock rule shared by all workers. The refresh on test stop is essential: pollers are not guaranteed to observe a long-running marked test, and without it the mark could expire right between two back-to-back marked tests. The iteration counters, the cache dict and its threading through the scheduler are gone. A mark can have one record per worker; the records are refreshed together and the newest one decides staleness for the whole mark. Pollers skip the refresh while the records are fresh (`MARK_REFRESH_SEC`), so the polling hot path stays write-free. The stale-mark cleanup is done before the scheduler reads the mark state for the current test, so a test never acts on mark or resource records that were just deleted. `StatusRow` now exposes the `created_at` column so the staleness check (and future consumers) can read record age.
Document how to share expensive setup across multiple tests: prefer pytest-subtests, and for cases where subtests don't work (hypothesis property based tests) use the cluster manager `mark` paired with `@pytest.mark.xdist_group` of the same name, so the marked tests are scheduled back-to-back on a single pytest worker and the assigned "marked" cluster instance is reused instead of being cleaned up as stale and prepared again. The marked tests documentation lives in `subtests.md` as the special case for when subtests are not applicable; `resource_management.md` points to it.
The `TestPoolCost` tests share one expensive cluster setup via
`cluster_manager.get(mark="minPoolCost", ...)`, but nothing kept them
scheduled together - the `order` and `long` markers apply only to
testnet variants that start from Byron. Add
`@pytest.mark.xdist_group("minPoolCost")` so the tests always run
back-to-back on a single pytest worker and the marked cluster
instance is reused instead of being cleaned up as stale and prepared
again.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR refactors the cluster manager’s “marked tests” lifecycle handling by replacing the previous loop-iteration heuristic with deterministic wall-clock based staleness/refresh rules shared across workers, and documents the recommended pairing of mark=... with @pytest.mark.xdist_group(...) to keep marked tests scheduled back-to-back.
Changes:
- Add timestamp-based staleness/refresh for “current mark” records (including a new
refresh_curr_markAPI andcreated_atexposure viaStatusRow/ snapshot reads). - Refresh mark timestamps both from scheduler polling and on test stop to prevent inadvertent mark expiry between back-to-back marked tests.
- Document and apply
xdist_grouppairing (including updatingTestPoolCost) and add a framework test for mark refresh behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| framework_tests/test_status_db.py | Adds coverage for refresh_curr_mark updating only the targeted mark+instance records. |
| cardano_node_tests/tests/test_pools.py | Adds @pytest.mark.xdist_group("minPoolCost") to keep marked tests scheduled together. |
| cardano_node_tests/cluster_management/status_db.py | Adds created_at to StatusRow and implements refresh_curr_mark for wall-clock refresh semantics. |
| cardano_node_tests/cluster_management/manager.py | Refreshes mark timestamps on marked test completion during on_test_stop. |
| cardano_node_tests/cluster_management/cluster_getter.py | Replaces iteration-count heuristic with wall-clock staleness cleanup + periodic refresh in the scheduler loop. |
| agent_docs/subtests.md | Documents when to use marked tests and the need to pair mark with xdist_group. |
| agent_docs/resource_management.md | Adds guidance pointing readers to subtests.md for expensive-setup sharing approaches. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Follow-up to #3563. An abandoned test mark (a group of marked tests
that stopped running) used to be detected by counting scheduler loop
iterations - a heuristic whose expiry time depended on a single
worker's sleep backoff (roughly 25 seconds to 2 minutes) and where
each newly arriving worker started counting from zero. It also
required a cache dict threaded through
get_cluster_instance.What changed
Wall-clock mark staleness
The "current mark" records are now timestamped and the mark is cleaned
up when no marked test was running for
MARK_STALENESS_SEC(60 seconds) - a deterministic wall-clock rule shared by all workers
that sits inside the old heuristic's expiry range.
The records are refreshed via the new
refresh_curr_markfrom twoplaces:
cluster instance, and
on_test_stopwhenever a marked test finishes.The refresh on test stop is essential: pollers are not guaranteed to
observe a long-running marked test (e.g. a hypothesis test running for
minutes while all other workers are busy), and without it the mark
could expire right between two back-to-back marked tests, causing a
respin of the exact setup the mark exists to protect.
A mark can have one record per worker; the records are refreshed
together and the newest one decides staleness for the whole mark, so
the cleanup fires once per mark. Pollers skip the refresh while the
records are younger than
MARK_REFRESH_SEC(15 seconds), so thepolling hot path under the global cluster lock stays write-free.
The stale-mark cleanup now runs before the scheduler reads the mark
state for the current test. Previously the mark records were read
first and the cleanup could delete them right after, letting a test
act on dangling data - skip resource resolution as a "non-initial"
marked test and run with no resource records at all.
The iteration counters, the cache dict and its threading through the
scheduler are gone.
StatusRownow exposes thecreated_atcolumn.Documentation
How to share expensive setup across multiple tests: prefer
pytest-subtests; for cases where subtests don't work (hypothesis
property based tests) use the cluster manager
markpaired with@pytest.mark.xdist_groupof the same name. The custom xdistscheduler then schedules the marked tests as one work unit on a single
pytest worker, so they run back-to-back and the assigned "marked"
cluster instance is reused instead of expiring as stale between tests.
Documented in
agent_docs/subtests.md(withresource_management.mdpointing to it) and in the
markargument docstring ofget_cluster_instance.Apply to TestPoolCost
TestPoolCostis the only current user ofmark=..., but nothingkept its tests scheduled together - the
ORDER5_BYRONpriorityapplies only to testnet variants that start from Byron. The class now
carries
@pytest.mark.xdist_group("minPoolCost").Verification
test_refresh_curr_markinframework_tests/test_status_db.py;all 37 framework tests pass.
make lintclean, each commit tested individually.TestPoolCost(itsscheduling changes with the new xdist group).