Skip to content

test(spanner): add pytest-xdist parallel execution with state isolation#17344

Draft
chalmerlowe wants to merge 2 commits into
mainfrom
chore/add-nox-session-google-cloud-spanner-xdist
Draft

test(spanner): add pytest-xdist parallel execution with state isolation#17344
chalmerlowe wants to merge 2 commits into
mainfrom
chore/add-nox-session-google-cloud-spanner-xdist

Conversation

@chalmerlowe
Copy link
Copy Markdown
Contributor

@chalmerlowe chalmerlowe commented Jun 2, 2026

This PR adds pytest-xdist to parallelize unit tests and the core_deps_from_source nox sessions for the google-cloud-spanner package.

By running tests in parallel using -n auto, the execution time of the Spanner unit tests are reduced from ~8 minutes to 4 minutes.

State isolation and test reliability are achieved by:

  • Simplifying Subtests: We pass simple strings/names to subTest() instead of complex objects. This keeps subtests lightweight and prevents serialization errors.
  • Cleaning Up Global Singletons: We reset telemetry singleton states on test teardown (using pytest's idiomatic monkeypatch fixture). This ensures metric counters don't leak from one test into another.
  • Fixing Concurrent Mock Conflicts: We return fresh mocked iterators for concurrent calls (using side_effect instead of return_value). This prevents one thread from exhausting a mock's results before another thread can read them.
  • Robust Assertion Checks: We added a helper method (_assert_concurrent_transaction_invariants) that verifies the behavior of concurrent threads (ensuring exactly one thread starts the transaction while others wait/reuse it), rather than checking fragile call logs or counting sequential request IDs. This allowed us to safely run four concurrent tests.

Note

The long pole in the tent is still the system tests, which require about 30 minutes. It is not as simple as just adding xdist because there are other factors that limit velocity including the fact that system tests actually interact with live systems.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request parallelizes unit tests by installing pytest-xdist and running pytest with -n auto, while addressing test isolation issues by resetting metrics singletons, cleaning up asyncio event loops, and updating subtests to use string identifiers. Feedback on these changes suggests keeping the mypy session skipped as it is out of scope, using pytest's monkeypatch fixture for environment variable management, and declaring pytest-xdist in the package's primary dependency file.

Comment thread packages/google-cloud-spanner/noxfile.py
Comment thread packages/google-cloud-spanner/tests/unit/conftest.py Outdated
Comment thread packages/google-cloud-spanner/noxfile.py
@chalmerlowe chalmerlowe force-pushed the chore/add-nox-session-google-cloud-spanner-xdist branch 3 times, most recently from 22b979b to cc28cd2 Compare June 2, 2026 15:07

return mock.create_autospec(SpannerClient, instance=True)

def _assert_concurrent_transaction_invariants(self, call_args_list, expected_count=2):
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.

For the reviewer:
This new function is a helper that enables us to confirm test results with more stability in concurrently run systems by checking for elements that won't change during a concurrent transation rather than elements that might change depending on the order that a concurrent set of tests finishes.

There are four locations where we strip out some long test bodies that were very specific (e.g. did test 1.1 come before 2.1, etc) and replace them with:

Is there a single "begin" transaction.
Do other transactions reuse the same transaction ID throughout.


self._batch_update_helper(transaction=transaction, database=database, api=api)

api.execute_sql.assert_any_call(
Copy link
Copy Markdown
Contributor Author

@chalmerlowe chalmerlowe Jun 2, 2026

Choose a reason for hiding this comment

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

As noted above, here we are replacing the test body that was looking for specific id numbers (and became brittle when run in a concurrent transaction).

Replaced it with the more invariant test body from the helper function: _assert_concurrent_transaction_invariants

self._execute_update_helper(transaction=transaction, api=api)
self.assertEqual(api.execute_sql.call_count, 1)

api.execute_sql.assert_any_call(
Copy link
Copy Markdown
Contributor Author

@chalmerlowe chalmerlowe Jun 2, 2026

Choose a reason for hiding this comment

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

Replaced the test body with the more invariant test body: _assert_concurrent_transaction_invariants

)
self._assert_concurrent_transaction_invariants(api.execute_batch_dml.call_args_list, 2)

self.assertEqual(api.execute_batch_dml.call_count, 2)
Copy link
Copy Markdown
Contributor Author

@chalmerlowe chalmerlowe Jun 2, 2026

Choose a reason for hiding this comment

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

Replaced the test body with the more invariant test body: _assert_concurrent_transaction_invariants


self._execute_update_helper(transaction=transaction, api=api)

api.execute_sql.assert_any_call(
Copy link
Copy Markdown
Contributor Author

@chalmerlowe chalmerlowe Jun 2, 2026

Choose a reason for hiding this comment

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

Replaced the test body with the more invariant test body: _assert_concurrent_transaction_invariants


self._execute_update_helper(transaction=transaction, api=api)

begin_read_write_count = sum(
Copy link
Copy Markdown
Contributor Author

@chalmerlowe chalmerlowe Jun 2, 2026

Choose a reason for hiding this comment

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

Replaced the test body with the more invariant test body: _assert_concurrent_transaction_invariants

@chalmerlowe chalmerlowe force-pushed the chore/add-nox-session-google-cloud-spanner-xdist branch 7 times, most recently from 7821620 to ab88581 Compare June 2, 2026 17:08
@chalmerlowe chalmerlowe force-pushed the chore/add-nox-session-google-cloud-spanner-xdist branch from ab88581 to 72804c4 Compare June 2, 2026 17:25
@chalmerlowe chalmerlowe added this to the Update nox sessions milestone Jun 2, 2026
Copy link
Copy Markdown
Contributor

@daniel-sanche daniel-sanche left a comment

Choose a reason for hiding this comment

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

couple small comments, otherwise LGTM

called_with.append((txn, args, kw))
txn.insert(TABLE_NAME, COLUMNS, VALUES)

import threading
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.

can this be moved to to top of the file?

called_with.append((txn, args, kw))
txn.insert(TABLE_NAME, COLUMNS, VALUES)

import threading
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.

if we import threading globally, this shouldnt be needed

called_with.append((txn, args, kw))
txn.insert(TABLE_NAME, COLUMNS, VALUES)

import threading
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.

same comments about imports

CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
)
install_unittest_dependencies(session, "-c", constraints_path)
session.install("pytest-xdist")
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.

Can this be added to UNIT_TEST_STANDARD_DEPENDENCIES?

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.

2 participants