|
1 | 1 | from decimal import Decimal |
2 | 2 | import sqlite3 |
| 3 | +import threading |
3 | 4 |
|
4 | 5 | import pytest |
5 | 6 | from sqlalchemy import MetaData, Table |
@@ -497,6 +498,63 @@ def test_stale_initializer_compensation_preserves_concurrent_verified_catalog( |
497 | 498 |
|
498 | 499 | assert _table_names(path) == before |
499 | 500 |
|
| 501 | +def test_sqlite_initializer_serializes_failure_before_concurrent_winner( |
| 502 | + tmp_path, monkeypatch, |
| 503 | +): |
| 504 | + path = tmp_path / "serialized-concurrent-init.sqlite" |
| 505 | + database = f"sqlite:///{path}" |
| 506 | + loser_inside_migration = threading.Event() |
| 507 | + winner_started = threading.Event() |
| 508 | + release_loser = threading.Event() |
| 509 | + original_migrate = wide._migrate_catalog_columns |
| 510 | + failures = {} |
| 511 | + |
| 512 | + def migrate(connection, datasets, variables, multiple_response): |
| 513 | + if threading.current_thread().name == "losing-initializer": |
| 514 | + loser_inside_migration.set() |
| 515 | + assert winner_started.wait(5) |
| 516 | + assert release_loser.wait(5) |
| 517 | + raise RuntimeError("injected catalog migration failure") |
| 518 | + return original_migrate(connection, datasets, variables, multiple_response) |
| 519 | + |
| 520 | + def compensation_must_not_run(*_args, **_kwargs): |
| 521 | + raise AssertionError("SQLite must roll back DDL without stale compensation") |
| 522 | + |
| 523 | + def initialize(name): |
| 524 | + if name == "winner": |
| 525 | + winner_started.set() |
| 526 | + try: |
| 527 | + openstatspec.initialize_catalog(database_url=database) |
| 528 | + except Exception as error: |
| 529 | + failures[name] = error |
| 530 | + |
| 531 | + monkeypatch.setattr(wide, "_migrate_catalog_columns", migrate) |
| 532 | + monkeypatch.setattr( |
| 533 | + wide, "_compensate_catalog_initialization", compensation_must_not_run, |
| 534 | + ) |
| 535 | + |
| 536 | + loser = threading.Thread( |
| 537 | + target=initialize, args=("loser",), name="losing-initializer", |
| 538 | + ) |
| 539 | + winner = threading.Thread( |
| 540 | + target=initialize, args=("winner",), name="winning-initializer", |
| 541 | + ) |
| 542 | + loser.start() |
| 543 | + assert loser_inside_migration.wait(5) |
| 544 | + winner.start() |
| 545 | + assert winner_started.wait(5) |
| 546 | + release_loser.set() |
| 547 | + loser.join(10) |
| 548 | + winner.join(10) |
| 549 | + |
| 550 | + assert not loser.is_alive() |
| 551 | + assert not winner.is_alive() |
| 552 | + assert type(failures.get("loser")) is RuntimeError |
| 553 | + assert str(failures["loser"]) == "injected catalog migration failure" |
| 554 | + assert "winner" not in failures |
| 555 | + assert openstatspec.initialize_catalog(database_url=database)["catalog"] == "verified" |
| 556 | + |
| 557 | + |
500 | 558 | def test_mysql_catalog_initialization_lock_spans_mutation_boundary(): |
501 | 559 | events = [] |
502 | 560 |
|
|
0 commit comments