Give each test session its own S3 Tables namespace - #815
Conversation
The S3 Tables tests shared one fixed namespace across the CI matrix. A table another job dropped during a listing made the listing fail with EntityNotFoundException, first in Glue GetTables and then in Athena ListTableMetadata itself, even when filtered by table name, so TestGlueMetadataClient.test_reads_s3_tables_catalog failed intermittently, more often once the suites ran in parallel. The session now creates a namespace named like its schema in the configured table bucket and deletes it, with any table left in it, when it finishes. AWS_ATHENA_S3_TABLES_NAMESPACE is no longer used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/sweep_databases.py also sweeps AWS_ATHENA_S3_TABLES_CATALOG's table bucket for namespaces named like a test schema and more than seven days old, deleting their tables first, with the same inventory-first and recheck-before-delete rules as databases. The sweep workflow sets the catalog. The CloudFormation template no longer creates the fixed namespace the tests used to share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The namespace is created first in pytest_sessionstart, so a failure leaves no Glue database or S3 rows behind; each pytest_sessionfinish step runs even if an earlier one fails; the xdist controller, which runs no tests, creates no namespace; and the table-bucket ARN uses the S3 Tables client's region, looked up once. The Glue S3 Tables test no longer lists the bucket's namespaces, which other sessions create and delete. The sweep reports each result as it finishes, rejects a malformed AWS_ATHENA_S3_TABLES_CATALOG before sweeping, and shares the test schema pattern between databases and namespaces. The testing docs say each test process creates a namespace and name both S3 Tables test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| @@ -13,16 +14,90 @@ | |||
|
|
|||
|
|
|||
| def pytest_sessionstart(session): | |||
There was a problem hiding this comment.
Self-review round one (behavior and implementation): FINDINGS, repaired
Base 08425ffe3c04144cb643f88ab5fedb5d4e67eab3, head 883510b73b3c55993a5ff11e5e2bdda941855111. Pass: /code-review (high) on a frozen snapshot at 60a83ab. It covered the session hooks under pytest-xdist and pytest-rerunfailures, the SQLAlchemy suites' separate conftest, the sweep, the workflow, the template and the docs.
Repaired in 883510b:
tests/pyathena/test_glue.py: the S3 Tables test also listed every namespace in the bucket. That listing races with other sessions' namespace create/delete exactly as table listings did, so it is removed.list_databasesstays covered inAwsDataCatalog.tests/pyathena/conftest.py:16:- The namespace was created last in
pytest_sessionstart. A failure then left the Glue database and S3 rows behind, because pytest skipspytest_sessionfinishafter a failed sessionstart. It is now created first. - Each
pytest_sessionfinishstep now runs even if an earlier one fails. - The xdist controller, which runs no tests, no longer creates a namespace. Before this,
pytest -n 8made 9 namespaces per job. - The bucket ARN now comes from the S3 Tables client's own region, instead of
AWS_DEFAULT_REGIONwhile the client could resolve another region. The account lookup is cached.
- The namespace was created last in
scripts/sweep_databases.py:- Each sweep reports its counts as it finishes, so a failing namespace sweep no longer discards the database report.
- A malformed
AWS_ATHENA_S3_TABLES_CATALOGis rejected before sweeping, instead of raisingIndexError. - The test schema pattern is shared between databases and namespaces.
docs/testing.md: says each test process creates a namespace (xdist workers, not the controller), and names both S3 Tables test files.
Deferred: the dry-run continue loop, which keeps the shape of sweep_databases.
Observed after CI: a superseded run (60a83ab, cancelled) left 45 empty namespaces, because cancellation kills the processes before pytest_sessionfinish. Glue test databases already behave this way. The successful run left none. The 45 were deleted by hand at the maintainer's request, and the weekly sweep covers such leftovers after this PR.
| ``` | ||
|
|
||
| The S3 Tables tests live in `tests/pyathena/sqlalchemy/test_base.py` and run under `just test pyathena`, not the SQLAlchemy compliance-suite commands. | ||
| Each test process creates its own namespace in the table bucket, named like its schema, and deletes it with any remaining tables at the end; with pytest-xdist that is each worker, not the controller. |
There was a problem hiding this comment.
Self-review round two (claims, callers, operations): CLEAN
Base 08425ffe3c04144cb643f88ab5fedb5d4e67eab3, head 883510b73b3c55993a5ff11e5e2bdda941855111.
Claims checked:
docs/testing.md:96-99:- One namespace per test process:
_runs_testsskips only the xdist controller. - Deletion with remaining tables:
_delete_s3tables_namespace. - Permissions: exactly the four
s3tablescalls used by the session. - Sweep after seven days:
sweep_s3tables_namespaces.
- One namespace per test process:
- The PR body's probe and CI claims:
- Run 36089493551 on
883510bpassed 15 of 15 test jobs (19 checks). - That run left no namespace; the 45 leftovers were all created 03:08–03:10, during the cancelled run.
- Run 36089493551 on
Callers and operations:
AWS_ATHENA_S3_TABLES_NAMESPACEis no longer read. Branches based on older master still set it and still expect the fixedpyathenanamespace, so they need a rebase before the stack update removes that namespace. This is noted in the PR body.- The template removal deletes
pyathenaon the next stack update, and that deletion fails while the namespace holds tables. Empty it first; the PR body gives the order. - The sweep workflow runs master's code, so namespace sweeping starts after merge, in the weekly run after the scheduled Test. A leftover is removed 7–14 days after creation.
CI duration: the sequential stages took 39.9 min, against 46.5 min before. Per-session namespaces add no measurable time, and the difference is within run-to-run variance.
… tables in the sweep Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| # The namespace goes first: a failure here then leaves nothing behind, as | ||
| # pytest skips pytest_sessionfinish after a failed pytest_sessionstart. | ||
| if _runs_tests(session.config): | ||
| _create_s3tables_namespace() |
There was a problem hiding this comment.
Independent review (Codex, gpt-6-sol, read-only at 883510b) — P1: the namespace is created before the upload and database setup. If a later step fails, pytest skips pytest_sessionfinish, so the namespace leaks, and the comment claiming nothing is left behind is wrong.
There was a problem hiding this comment.
Fixed in 70e3082: the setup steps after namespace creation now run in a try, and a failure deletes the namespace before re-raising; the comment was corrected. Checked live without xdist, with a nonexistent staging bucket so _upload_rows fails: 883510b leaked pyathena_test_g67abp2xgn (deleted afterwards) and 70e3082 left only default and pyathena. With xdist the controller runs _upload_rows first and fails before any worker starts, so that path never created a namespace.
| if ( | ||
| not _eligible_namespace(current, cutoff) | ||
| or current["createdAt"] != namespace["createdAt"] | ||
| ): |
There was a problem hiding this comment.
Independent review (Codex) — P1: after the get_namespace recheck, the tables and the namespace are deleted by name without checking identity again, so a namespace recreated under the same name in between could be hit.
There was a problem hiding this comment.
Not changed in the logic. Namespace names are pyathena_test_ plus 10 random characters per session, and the recheck already rejects any namespace younger than seven days, so a namespace recreated under the same name is not a realistic case. Another recheck before DeleteNamespace would not protect tables deleted before it anyway, and DeleteNamespace has no identity condition. 70e3082 documents this in the script header, whose previous wording (a recheck immediately before each deletion) described only the database sweep.
There was a problem hiding this comment.
Codex follow-up (session 01a0d6cb-e4bc-7e41-90cd-f2a07372b0cf, gpt-6-sol, read-only at 70e3082) keeps this at P1: random naming makes reuse unlikely but does not rule out a namespace recreated after GetNamespace.
Kept as a documented residual risk. The S3 Tables API gives no way to close the window: DeleteNamespace takes only tableBucketARN and namespace, with no identity condition. Recreating the name would take another session drawing the same 10 random characters (36^10) within the seconds between the recheck and the deletes, in the test bucket. This is left for the maintainer to decide.
There was a problem hiding this comment.
Repaired in 9aa3f31, at the maintainer's request.
- Identity. It is now the namespace ID (
namespaceId) rather thancreatedAt, and eligibility requires an ID. - Tables.
ListTablesdoes not returnnamespaceId(checked live), so each table is read withGetTable. It is deleted only if itsnamespaceIdmatches, andDeleteTablereceives thatGetTableresponse'sversionToken. - Namespace. The namespace ID is rechecked right before
DeleteNamespace. A namespace that still holds tables cannot be deleted. What remains is an empty namespace recreated between that last recheck andDeleteNamespace, which takes only a name. The script header documents this. - Tests.
- The 49 script tests pass.
- Removing the table filter, the final recheck, or
versionTokeneach makes a test fail, and the new tests fail against b77865d.
- Live run. A run against the real API swept a namespace I created with two tables (cutoff patched, no CI running):
eligible=1, deleted=1, skipped=0. Onlydefaultandpyathenaremained.
Codex follow-up (session 01a0d71e-7b2f-77d1-ba1c-fd4980445d28, gpt-6-sol, read-only at 9aa3f31): CLEAN.
| tableBucketARN=table_bucket_arn, namespace=name | ||
| ) | ||
| for table in page["tables"] | ||
| ] |
There was a problem hiding this comment.
Independent review (Codex) — P2: a NotFoundException from one delete_table goes to the outer handler, so the remaining tables and the namespace are skipped until the next sweep.
There was a problem hiding this comment.
| _create_table(cursor) | ||
| except BaseException: | ||
| if runs_tests: | ||
| _delete_s3tables_namespace() |
There was a problem hiding this comment.
Independent review follow-up (Codex, session 01a0d6cb-e4bc-7e41-90cd-f2a07372b0cf, gpt-6-sol, read-only at 70e3082) — P2: if namespace cleanup fails while handling a setup failure, its exception masks the original setup error.
There was a problem hiding this comment.
Kept as is. Python chains the exceptions implicitly, so the setup error still appears in pytest's INTERNALERROR output, above "During handling of the above exception, another exception occurred". Suppressing the cleanup error instead would hide the leak, which only the weekly sweep would then catch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WHAT
Per-session namespace. The test session creates its own S3 Tables namespace, named like its schema (
pyathena_test_<id>), inAWS_ATHENA_S3_TABLES_CATALOG's table bucket. At the end it deletes the namespace with any table left in it (tests/pyathena/conftest.py). If a later setup step fails,pytest_sessionstartdeletes the namespace itself, because pytest then skipspytest_sessionfinish.AWS_ATHENA_S3_TABLES_NAMESPACEis no longer read, anddocs/testing.mdlists the S3 Tables permissions the session needs.Sweep.
scripts/sweep_databases.pyalso sweeps that table bucket. It removes namespaces named like a test schema that are more than seven days old, deleting their tables first. The same rules as for Glue databases apply:NotFoundException) is ignored, including for a single tableThe namespace ID is checked before the tables are deleted and again before the namespace is deleted. Each table is deleted only if
GetTableshows it still belongs to that namespace, passing itsversionTokentoDeleteTable(ListTablesdoes not return the namespace ID).DeleteNamespacetakes only a name. So the one remaining gap is an empty namespace recreated under the same name between the last check andDeleteNamespace. Namespace names are random per session (10 characters).The sweep workflow sets
AWS_ATHENA_S3_TABLES_CATALOG.Template.
cloudformation/github_actions_oidc.yamlno longer creates the fixedpyathenanamespace or itsS3TablesNamespaceNameparameter.WHY
The S3 Tables tests shared one fixed namespace across all CI jobs. When another job dropped its table during a listing, the listing failed with
EntityNotFoundException(Service: S3Tables), even when filtered by table name:GetTablesin run 36009777585ListTableMetadatain run 36080115567This made
TestGlueMetadataClient::test_reads_s3_tables_catalogintermittent, and running the suites in parallel (#814) made it more frequent.A probe on 2026-09-25 created a namespace in the CI table bucket in 0.9 s. Athena and Glue both saw it immediately. Creating, listing, getting and dropping a table in it all worked, and the namespace could then be deleted.
Order for the template change.
pyathenanamespace.TEST
Tested commit:
9aa3f31.just lintpassed, including cfn-lint.just docs lintreported 0 errors.uv run --locked python -m pytest scripts/tests/: 49 passed.test_a_table_already_gone_does_not_stop_the_namespacefails against883510b.versionTokeneach makes a test fail.eligible=1, deleted=1, skipped=0. Onlydefaultandpyathenaremained._upload_rowsfails:883510bleaked a namespace (deleted afterwards).70e3082left onlydefaultandpyathena._upload_rowsbefore any worker starts, so no namespace is created.scripts/sweep_databases.pypreview against the CI account:Preview databases: eligible=0andPreview S3 Tables namespaces: eligible=0.d28077fbefore the review repairs:pytest -n 1 tests/pyathena/test_glue.py tests/pyathena/sqlalchemy/test_base.py -k "s3tables or s3_tables"gave 6 passed, 0 skipped, and left no namespace.a9c0a0c(test constants moved to the top of the file): all 19 checks green, 40.9 min, and the run left no namespace in the bucket. Run 36100312229 on9aa3f31was also all green (39.9 min). Runs on70e3082(36094239865) andb77865d(36096956008) were also all green.883510b:70e3082andb77865dtook 38.8 and 43.4 min. The 12 other PR Test runs over 2026-09-23..25 that passed on their first attempt took 38–46 min (median 41.5), so the namespaces add no measurable time.60a83ab) left 45 empty namespaces, because cancellation skipspytest_sessionfinish. Glue test databases already behave this way. These 45 were deleted by hand, and the weekly sweep removes such leftovers 7–14 days after creation.🤖 Generated with Claude Code